Kenneth Pugh, a principal in Pugh-Killeen Associates, teaches C language courses for corporations. He is the author of C Language for Programmers and All On C, and was a member on the ANSI C committee. He also does custom C programming for communications, graphics, image databases, and hypertext. His address is 4201 University Dr., Suite 102, Durham, NC 27707. You may fax questions for Ken to (919) 489-5239. Ken also receives email at kpugh@dukemvs. ac. duke. edu (Internet).
UNIX Shell Exit Code
The April CUJ had a question from Tim Reilly regarding the exit code from a process. The value of the shell variable $? was 2000 if the process returned a 0, but the interrupt key had signaled an interrupt. The UNIX manual states:"The value of a simple-command is its exit status if it terminates normally, or (octal) 200 + status if it terminates abnormally (see signal (2) for a list of status values)."
However the value does not fit into that definition. It is a decimal 2000.
I received an answer straight from the horse's mouth (or pretty close to it). David Korn, a fellow speaker at the UNIX conference in Boston in April, explained the situation. The shell receives the signal, as well as the child process (and all its prodigy).
If the child process exits with a 0 value and SIGINT has been received, the Bourne shell alters the value for the shell variable $? to 2000. This is #defined in the Bourne shell code. The selection of this value appears to have been completely arbitrary. The reason for this alteration of the exit value is not clear.
The Korn shell is designed to return a 0. This #define has been removed.
Letters on this topic were also received from Rick Smith of McLean, VA; James A. Capp of Harrisburg, PA; and David Carlson of Sylmar, CA (KP)
Readers' Replies
Coding Style
I would like to remark about a letter sent by Kim Tsang of Hong Kong to your CUJ Q?/A! (December 1990). A product I stumbled across advertised in CUJ adds end comments to closing functions and statements during reformatting. (The product is C-Clearly). The comment added consists of the function/statement keyword for the block followed by the first identifier on the line of the opening statement. Assume the following code:
if (switch_flag_on) { code goes here }After running it through the reformatter the code looks like
if (switch_flag_on) { formatted code goes here }/*if switch_flag_on */Note the added end comment. After end comments are inserted I precede them with the word END as in
} /* END if switch_flag_on */Using the vi editor, the command for global replacement adding the word END to all end comments would be:
:s/}\/\*/} \/\* END/gSome would consider the end comment utility trivial but it comes in handy when you have cascades of closing braces.Phil Pistone
Chicago, ILWith large compound statements (over 10 or 20 lines) and lots of nesting, I use end comments as you have shown. With lots of nesting however, I try to look at the function to see if it can be broken up into smaller ones. A nest level past three or four is a suggestion that the function may be too complex. (KP)
This letter is in response to Donal Lyons' reply (The C Users Journal, January 1991) to a letter I sent to you regarding indentation styles, and your subsequent comments, (September 1990). First, let me say that, as usual, I enjoyed your article thoroughly. Now, let me recap what Mr. Lyons had to say. Briefly, he felt that my style was a solution to a non-problem that good coding style would avoid. Let me just say that I agree completely with that sentiment.
However, in the real world, we are not always in the position of being able to design things from the ground up. Sometimes we must support code written by other people. The people may have long since left the company, and the code may have no formatting, terrible style, and a multitude of other "unprofessionalisms" in it. However, it might also work.
Given a lot of time with nothing else to do, you certainly could rewrite the entire thing using wonderfully descriptive variable names. This might even be the ideal way to go. However, many companies frown on tying up a development resource to overhaul unprofitable old (working) code, especially when that resource could be developing profitable new code. Thankfully, I am not often in this unfortunate position.
However, experience has shown me that when you are in such a predicament, the only safe (I use the word loosely) way to proceed is to start cleaning up the style (indentation, commenting etc.) and leave the working code alone. Then, you can begin to examine the code to see what it does, and how it does it. Only at this point can you even consider making changes to executable pieces of the program (such as symbols). Inevitably, if you start changing variable names before this, that once-working code will stop running (usually, in the most hideous manner possible).
So, you see, my style was not just developed for when I create my own code, and, therefore, have complete control over style and content, but it has also evolved to suit my needs and style over the last 10 years. I hope that Mr. Lyons is fortunate enough to avoid the problem of having to update less than ideally styled code, but, if not, he may find that there is a lot of code out there that contains variables like aflag and eflag scattered throughout it.
Brian Merson
Marlboro, MAI prefer not to change variable names in programs that are running, just for the problems you mentioned. I consider it important to give proper names for variables and functions in the first place. I could go into a long diatribe regarding the names used by many of the programmers whose code I have to look at in the classes that I teach. Suffice it to say that eflag and aflag are not my cup of tea, but better to not break what is working. (KP)
Timing Programs
I read with some interest both Mr. Whitford's question and your reply to the dynamic string allocation. There seem to be several problems with the program as it was finally presented.1. In the function wordcount, the input string, which apparently has no limits, is copied into a character array tstr with a defined size. The function in Listing 1 accomplishes the same result without either copying the string or allocating memory. In addition, the original function would not work if it passed a string with a single word in it. It would return 0.
2. The str_to_ptrarray function in Mr. Witford's allocates a temporary string, parses it using strtok, then allocates space for each word. This leaves a hole in the heap when the temporary string is freed. Although this is not a problem in the application shown, it may be a problem in a more general sense. The three functions in Listing 2 not only leave the heap intact but do fewer allocations.
There is a difference between the way this allocation is done here and in Mr. Witford's example. Here only one pointer is allocated for the entire array. At first it might appear that the space taken up by the pointers is greater than that taken by the strings, but each of the allocations in the example not only requires what is directly allocated, but also the overhead that this system takes to keep up with the space allocated to each pointer. In any case, these sort of considerations are not usually critical.
In addition it might still be useful to have a guard null. To do this, nbr in the above calloc statement should be changed to nbr+1. For most C compilers the result of the calloc will be null pointers, but if not this can be explictly set in the function.
Listing 3 uses these definitions. The resulting program seems easier to understand. It emphasizes the fact that ptrarray and string will be altered much in the same way as using the & operator in scanf. Note that in this code fragment, allocate_space does not use its return value, but it can be used to test for adequate space for the pointer allocation.
Perhaps the most interesting part of this exercise, at least for me, was when I attempted to generalize the results. It would be useful to allow all whitespace characters to be word separators, or at the very least to allow space, tab, and newline to be word breaks. The resulting program is given in Listing 4, which is a complete program with output to confirm the results.
There is no use of strtok. Interestingly enough the code in Listing 4 ran much faster than comparable code using strtok to break apart the words, with strspn and strpbrk to count the words. The code using these functions is shown in Listing 5.
The timing differences were quite remarkable, using strtok with the absolute minimum separators in word_breaks, i.e." ", the direct code using isspace was faster by 54/122, using three characters in word_breaks "\t\n", the ratio was 54/181 and using five characters the ratio was 54/269. It is rare that a piece of code like this will be rate limiting in a program, but the differences are much more than I would have expected. This is a useful example of the power of table lookup vs. calculation. In this case the overhead for the table lookup is probably very small, the ctype array that is used to access the isXXXX functions uses 257 integers (Turbo C probably all versions), the functions are all macros for minimum overhead.
The code given here for the str_to_ptrarray is easier for me to understand, mostly because it avoids the confusion of the triple pointer except in the function definition lines. I would be curious to have your comments on whether this approach is less prone to confusion.
Gary Mallard
Thank you for your insights. I like your approach. It does cut down on the overhead and makes the code more readable. It is cleaner to know how much room you are going to need to allocate and do it all at once, rather than in separate chunks. Also, table lookup is definitely the way to go (if you have the space) for many programming problems.
A brief comment on the code although your allocate_space function does return a success indicator, you do not test it in the main program. (KP)
This program originates from CUJ (March 1991, p. 106). In this discussion, Art Shipman attempts to rebuff R. Palmer Benedict's November 1990 letter about the access time to C structures and arrays. This, at the same time, has lead me to question Mr. Shipman's results.
After reviewing the article and listings, I recreated the entire program (except that I did it entirely in main). I became puzzled something was wrong. His results were somewhat contradictory to mine. The major difference between his program and my version of it was the use of function calls and prepocessor biostime and print functions or #defines! My program, as I rewrote his, simply accessed the routines directly, instead of being called. This seemed to make a big difference in the timing behavior and results.
In these C and .exe program listings (not shown here, but available on disks), I present to you some answers to these contradictions and why they happen. Perhaps you all at CUJ may offer some additional insight into the whole affair. I have re-written the original timing program in an attempt that it be correct, fast, exacter timewise, plus easier to understand for the beginning C users, (Most importantly, to that end, you can step through the program with Debug in the IDE, which is not possible in functions which are #defined in the preprocessor area before main.)
I moved the biostime and printf functions into main away from the preprocessor #define location they had before. This adds to its access speed and again makes it easier to understand for both the well-versed and novice C users. I also put all the remaining function calls in main for the same reasons.
My rewrite of his program and my results prove once again three key points I have learned about C that should help all novice C users:
1) KIS (Keep It Simple).
2) Calling functions can make C modular, (each function acting as a unique tool), however, the overhead of calling functions recursively is time-consuming and should be avoided!
3) Beware when using functions you or other people wrote. Before using them, make certain you fully understand them.
The large timing differences I have observed and the access timing variations using my version compared to Mr. Shipman prove my point. Using Turbo C v2, and compiler optimization switches set to (size, on, off, on) with my version, was much faster in its results than the original. Also, it made Structure access come in second behind Object access via the dot operator. My program behaved very differently depending on the type of compiler optimization employed, while Art's showed very little behavioral variation and always gave much longer timing results than did my version.
Please explain to me and the readers these (if they are correct) and any additional reasons for the timing differences using both programs. One way tends to support Mr. Shipman's findings and the other, with the right optimization, supports Mr. Benedict's.
Chris Meyer
Montreal, Quebec,Thank you for your tests. Your listings were very long, so they are not included here, but are available on the disk for this issue.
I've tried to simplify the timing program (see Listing 6) . This same program could be used to time any function. After running this a few times, I realized that the loop would have to be run a considerable number of times to generate any detectable difference between the access methods.
The compiler assembly language output (using the large memory model) looks like Listing 7. It's obvious the only difference between the two access methods is the add ax,2 instruction that accounts for the offset into the structure. (KP)
Storing Data in EXE files
The recent information in your Q?/A! column in CUJ (May 1991) concerning data stored at the end of .EXE files reminded me of a problem I had with the technique when I first read about it in Inside Turbo C about a year ago. I think the sample code in the column suffers from the same problem. Perhaps you can straighten me out.In Inside Turbo C, as well as in Listing 3 in your column, a formula to seek the end of the EXE file is given as
512*(Header[2] -1)+Header[1]+1I've found that as long as you position the file pointer using this formula to write the data, you'll have no trouble reading it later on.However, if you use the DOS COPY/B command to append data to the EXE, you cannot read the data with the formula. You will skip the first byte of the data. Unless you change the formula to
512* (Header [2]-1)+Header[1]I think that the extra byte added to the file offset in the original formula is unnecessary. The corrected formula works with COPY/B and with data written from the program itself.William Giel
Riverside, CT
char And long Pointers
In response to Hans-Gabriel Ridder's question, the editor's note had:
((long*)&ptr)++This should be
(*((long**)&ptr))++ /* (get **char convert to **long deref to *long (lvalue) then inc.*/This could be put into a macro:
#//define ptr_type(type, ptr) (*((type**) &ptr))See Listing 8 for an example.Stephen D. Williams
SDW SystemsThanks. (KP)