You are on page 1of 23

http://c-faq.com/~scs/cclass/krnotes/sx7.

html

Chapter 4: Functions and Program Structure


page 67 Deep paragraph: Functions break large computing tasks into smaller ones, and enable people to build on what others have done instead of starting over from scratch. Appropriate functions hide details of operation from parts of the program that don't need to know about them, thus clarifying the whole, and easing the pain of making changes. Functions are probably the most import weapon in our battle against software complexity. You'll want to learn when it's appropriate to break processing out into functions (and also when it's not), and how to set up function interfaces to best achieve the qualities mentioned above: reuseability, information hiding, clarity, and maintainability. The quoted sentences above show that a function does more than just save typing: a well-defined function can be re-used later, and eases the mental burden of thinking about a complex program by freeing us from having to worry about all of it at once. For a well-designed function, at any one time, we should either have to think about: 1. that function's internal implementation (when we're writing or maintaining it); or 2. a particular call to the function (when we're working with code which uses it). But we should not have to think about the internals when we're calling it, or about the callers when we're implementing the internals. (We should perhaps think about the callers just enough to ensure that the function we're designing will be easy to call, and that we aren't accidentally setting up so that callers will have to think about any internal details.) Sometimes, we'll write a function which we only call once, just because breaking it out into a function makes things clearer and easier. Deep sentence: C has been designed to make functions efficient and easy to use; C programs generally consist of many small functions rather than a few big ones.

Some people worry about ``function call overhead,'' that is, the work that a computer has to do to set up and return from a function call, as opposed to simply doing the function's statements in-line. It's a risky thing to worry about, though, because as soon as you start worrying about it, you have a bit of a disincentive to use functions. If you're reluctant to use functions, your programs will probably be bigger and more complicated and harder to maintain (and perhaps, for various reasons, actually less efficient). The authors choose not to get involved with the system-specific aspects of separate compilation, but we'll take a stab at it here. We'll cover two possibilities, depending on whether you're using a traditional command-line compiler or a newer integrated development environment (IDE) or other graphical user interface (GUI) compiler. When using a command-line compiler, there are usually two main steps involved in building an executable program from one or more source files. First, each source file is compiled, resulting in an object file containing the machine instructions (generated by the compiler) corresponding to the code in that source file. Second, the various object files are linked together, with each other and with libraries containing code for functions which you did not write (such as printf), to produce a final, executable program. Under Unix, the cc command can perform one or both steps. So far, we've been using extremely simple invocations of cc such as
cc hello.c

(section 1.1, page 6). This invocation compiles a single source file, links it, and places the executable (somewhat inconveniently) in a file named a.out. Suppose we have a program which we're trying to build from three separate source files, x.c, y.c, and z.c. We could compile all three of them, and link them together, all at once, with the command
cc x.c y.c z.c

(see also page 70). Alternatively, we could compile them separately: the -c option to cc tells it to compile only, but not to link. Instead of building an executable, it merely creates an object file, with a name ending in .o, for each source file compiled. So the three commands
cc -c x.c cc -c y.c cc -c y.c

would compile x.c, y.c, and z.c and create object files x.o, y.o, and z.o. Then, the three object files could be linked together using
cc x.o y.o z.o

When the cc command is given an .o file, it knows that it does not have to compile it (it's an object file, already compiled); it just sends it through to the link process.

Here we begin to see one of the advantages of separate compilation: if we later make a change to y.c, only it will need recompiling. (At some point you may want to learn about a program called make, which keeps track of which parts need recompiling and issues the appropriate commands for you.) Above we mentioned that the second, linking step also involves pulling in library functions. Normally, the functions from the Standard C library are linked in automatically. Occasionally, you must request a library manually; one common situation under Unix is that certain math routines are in a separate math library, which is requested by using -lm on the command line. Since the libraries must typically be searched after your program's own object files are linked (so that the linker knows which library functions your program uses), any -l option must appear after the names of your files on the command line. For example, to link the object file mymath.o(previously compiled with cc -c mymath.c) together with the math library, you might use
cc mymath.o -lm

Two final notes on the Unix cc command: if you're tired of using the nonsense name a.out for all of your programs, you can use -o to give another name to the output (executable) file:
cc -o hello hello.c

would create an executable file named hello, not a.out. Finally, everything we've said about cc also applies to most other Unix C compilers. Many of you will be using acc (a semistandard name for a version of cc whichdoes accept ANSI Standard C) or gcc (the FSF's GNU C Compiler, which also accepts ANSI C and is free). There are command-line compilers for MS-DOS systems which work similarly. For example, the Microsoft C compiler comes with a CL (``compile and link'') command, which works almost the same as Unix cc. You can compile and link in one step:
cl hello.c

or you can compile only:


cl /c hello.c

creating an object file named hello.obj which you can link later. The preceding has all been about command-line compilers. If you're using some kind of integrated development environment, such as Turbo C or the Microsoft Programmer's Workbench or Think C, most of the mechanical details are taken care of for you. (There's also less I can say here about these environments, because they're all different.) Typically there's a way to specify the list of files (modules) which make up your project, and a single ``build'' button which does whatever's required to build (and perhaps even execute) your program.

section 4.1: Basics of Functions section 4.2: Functions Returning Non-Integers section 4.3: External Variables section 4.4: Scope Rules section 4.5: Header Files section 4.6: Static Variables section 4.7: Register Variables section 4.8: Block Structure section 4.9: Initialization section 4.10: Recursion section 4.11: The C Preprocessor section 4.11.1: File Inclusion section 4.11.2: Macro Substitution section 4.11.3: Conditional Inclusion

Read sequentially: prev next up top This page by Steve Summit // Copyright 1995, 1996 // mail feedback
section 4.1: Basics of Functions page 68

Once again, notice how a clear, simple description of the problem we're trying to solve leads to an (almost) equally clear program implementing it. Here are some more nice statements about the virtues of a clean, modular design: Although it's certainly possible to put the code for all of this in main, a better way is to use the structure to advantage by making each part a separate function. Three small pieces are easier to deal with than one big one, because irrelevant

details can be buried in the functions, and the chance of unwanted interactions is minimized. And the pieces may even be useful in other programs. Let's say a bit more about how and why functions can be useful. First, we can see that, having chosen to use a separate function for each part of the print-matchinglines program, the top-level main routine on page 69 is particularly simple and straightforward; it's little more than a transcription into C of the pseudocode on page 68. The authors don't tend to use too many comments in their code, anyway, but this code hardly needs any: the names of the functions called speak for themselves. (The only thing that might not be obvious at first is that strindex is being used not so much to find the index of a substring but just to determine whether a substring is present at all.) Second, we may be pleased to notice that we're already having a chance to re-use the getline function we first wrote in Chapter 1. Third, we note that the two functions which we've chosen to use (getline and strindex) are themselves reasonably simple and straightforward to write. Finally, note that sometimes what you re-use is not so much a function as a function interface. The code on page 69 uses a new implementation of getline, but the interface (the argument list, return value, and functionality) is the same as for the versions of getline in section 1.9 on page 29. We could have used that version here, or this new version there. Later, if we think of some even better way of reading lines, we can write yet another version of getline, and as long as it has the same interface, these programs can call it without their having to be rewritten. The ease with which a program like this comes together may be mildly deceptive, because nowhere have we discussed the the motivations which led to the particular pseudocode description on page 68 or the particular definitions of the functions which were chosen to break the problem down into. Choosing a design for a program, and defining subfunctions (their interfaces and their behavior) are both arts, and of course the tasks are not unrelated. A good design leads to the invention of functions which might well be useful later, and an existing body of good, general-purpose functions (all crying out to be re-used) can help to guide the design of the next program. What makes a good building block, either an abstract one that we use in a pseudocode description, or a concrete one in the form of a general-purpose function? The most important aspect of a good building block is that have a single, well-defined task to perform. Two of the three functions used in the line-matching program fill this role very well: getline's job is to read one line, and strindex'es job is to find one string in another string. printf's specification is considerably broader: its job is to print stuff. (It's not surprising that printf can therefore be the harder routine to call, and is certainly much harder to implement. Its saving virtue is that it is nonetheless broadly applicable and infinitely reusable.)

When you find that a program is hard to manage, it's often because if has not been designed and broken up into functions cleanly. Two obvious reasons for moving code down into a function are because: 1. It appeared in the main program several times, such that by making it a function, it can be written just once, and the several places where it used to appear can be replaced with calls to the new function. 2. The main program was getting too big, so it could be made (presumably) smaller and more manageable by lopping part of it off and making it a function. These two reasons are important, and they represent significant benefits of wellchosen functions, but they are not sufficient to automatically identify a good function. A good function has at least these two additional attributes: 3. It does just one well-defined task, and does it well. 4. Its interface to the rest of the program is clean and narrow. Attribute 3 is just a restatement of something we said above. Attribute 4 says that you shouldn't have to keep track of too many things when calling a function. If you know what a function is supposed to do, and if its task is simple and well-defined, there should be just a few pieces of information you have to give it to act upon, and one or just a few pieces of information which it returns to you when it's done. If you find yourself having to pass lots and lots of information to a function, or remember details of its internal implementation to make sure that it will work properly this time, it's often a sign that the function is not sufficiently well-defined. (It may be an arbitrary chunk of code that was ripped out of a main program that was getting too big, such that it essentially has to have access to all of that main function's variables.) The whole point of breaking a program up into functions is so that you don't have to think about the entire program at once; ideally, you can think about just one function at a time. A good function is a ``black box'': when you call it, you only have to know what it does (not how it does it); and when you're writing it, you only have to know what it's supposed to do (and you don't have to know why or under what circumstances its caller will be calling it). Some functions may be hard to write (if they have a hard job to do, or if it's hard to make them do it truly well), but that difficulty should be compartmentalized along with the function itself. Once you've written a ``hard'' function, you should be able to sit back and relax and watch it do that hard work on call from the rest of your program. If you find that difficulties pervade a program, that the hard parts can't be buried inside black-box functions and then forgotten about, if you find that there are hard parts which involve complicated interactions among multiple functions, then the program probably needs redesigning.

For the purposes of explanation, we've been seeming to talk so far only about ``main programs'' and the functions they call and the rationale behind moving some piece of code down out of a ``main program'' into a function. But in reality, there's obviously no need to restrict ourselves to a two-tier scheme. The ``main program,'' main(), is itself just a function, and any function we find ourself writing will often be appropriately written in terms of sub-functions, sub-sub-functions, etc. That's probably enough for now about functions in general. Here are a few more notes about the line-matching program. The authors mention that ``The standard library provides a function strstr that is similar to strindex, except that it returns a pointer instead of an index.'' We haven't met pointers yet (they're in chapter 5), so we aren't quite in a position to appreciate the difference between an index and a pointer. Generally, an index is a small number referring to some element of an array. A pointer is more general: it can point to any data object of a particular type, whether it's one element of an array, or some other object anywhere in memory. (Don't worry too much about the distinction yet, but bear in mind that there is a distinction. Note, too, that the distinction is not absolute; in fact, the word ``index'' seems to derive from the concept of pointing, as you can see if you think about what you use your index finger for, or if you notice that the entries in a book's index point at the referenced parts of the book. We frequently speak casually of an index variable ``pointing at'' some cell of an array, even though it's not a true pointer variable.) One facet of the getline function's interface might bear mentioning: its first argument, the character array s, is being used to return the line that it reads. This may seem to contradict the rule that a function can never modify the value of a variable in its caller. As was briefly mentioned on page 28, there's an exception for arrays, which well be learning about in chapter 5; for now, we'll gloss over the point. (Actually, we're glossing over two points: not only is getline able to return a value via an argument, but the argument isn't really an array, although it's declared as and looks like one. Please forgive these gentle fictions; explaining them completely would really be premature at this point. Perhaps they weren't worth mentioning yet, after all.) For comparison, here is yet another version of getline:
int getline(char s[], int lim) { int c, i = 0;

while(--lim > 0 && (c=getchar()) != EOF) { s[i++] = c; if(c == '\n')

break; }

s[i] = '\0';

return i; }

Note that by using break, we avoid having to test for '\n' in two different places. If you're having trouble seeing how the strindex function works, its algorithm is
for (each position i in s) if (t occurs at position i in s) return i;

(else) return -1;

Filling in the details of ``if

(t

occurs at position i in s)'', we have:

for (each position i in s) for (each character in t) if (it matches the corresponding character in s) if (it's '\0') return i; else keep going else no match at position i

(else) return -1;

A slightly less compressed implementation than the one on page 69 would be:
int strindex(char s[], char t[]) { int i, j, k;

for (i = 0; s[i] != '\0'; i++) { for(j = i, k = 0; t[k] != '\0'; j++, k++) if(s[j] != t[k]) break;

if(t[k] == '\0') return i;

return -1; }

Note that we have to check for the end of the string t twice: once to see if we're at the end of it in the innermost loop, and again to see why we terminated the innermost loop. (If we terminated the innermost loop because we reached the end of t, we found a match; otherwise, we didn't.) We could rearrange things to remove the duplicated test:
int strindex(char s[], char t[]) { int i, j, k;

for (i = 0; s[i] != '\0'; i++) { j = i; k = 0;

do { if(t[k] == '\0') return i; } while(s[j++] == t[k++]); }

return -1; }

It's a matter of style which implementation of strindex is preferable; it's impossible to say which is ``best.'' (Can you see a slight difference in the behavior of the version on page 69 versus the two here? Under what circumstance(s) would this difference be significant? How would the version on page 69 behave under those circumstances, and how would the two routines here behave?) page 70 Deep sentence: A program is just a set of definitions of variables and functions.

This sentence may or may not seem deep, and it may or may not be deep, but it's a fundamental definition of what a C program is. Note that a function's return value is automatically converted to the return type of the function, if necessary, just as in assignments like
f = i;

where f is float and i is int. Most programmers do use parentheses around the expression in a return statement, because that way it looks more like while(), for(), etc. The reason the parentheses are optional is that the formal syntax is
return expression ;

and, as we know, any expression surrounded by parentheses is another expression. It's debatable whether it's ``not illegal'' for a function to have return statements with and without values. It's a ``sign of trouble'' at best, and undefined at worst. Another clear sign of trouble (which is equally undefined) is when a function returns no value, or is declared as void, but a caller attempts to use the return value. The main program on page 69 returns the number of matching lines found. This is probably better than returning nothing, but the convention is usually that a C program returns 0 when it succeeds and a positive number when it fails.

Read sequentially: prev next up top This page by Steve Summit // Copyright 1995, 1996 // mail feedback
section 4.2: Functions Returning Non-Integers page 71

Actually, we may have seen at least one function returning a non-integer, in the Fahrenheit-Celsius conversion program in exercise 1-15 on page 27 in section 1.7. The type name which precedes the name of a function (and which sets its return type) looks just like (i.e. is syntactically the same as) the void keyword we've been using to identify functions which don't return a value. Note that the version of atof on page 71 does not handle exponential notation like 1.23e45; handling exponents is left for exercise 4-2 on page 73.

``The standard library includes an atof'' means that we're reimplementing something which would otherwise be provided for us anyway (i.e. just like printf). In general, it's a bad idea to rewrite standard library routines, because by doing so you negate the advantage of having someone else write them for you, and also because the compiler or linker are allowed to complain if you redefine a standard routine. (On the other hand, seeing how the standard library routines are implemented can be a good learning experience.) page 72 In the ``primitive calculator'' code at the top of page 72, note that the call to atof is buried in the argument list of the call to printf. Deep sentences: The function atof must be declared and defined consistently. If atof itself and the call to it in main have inconsistent types in the same source file, the error will be detected by the compiler. But if (as is more likely) atof were compiled separately, the mismatch would not be detected, atof would return a double that main would treat as an int, and meaningless answers would result. The problems of mismatched function declarations are somewhat reduced today by the widespread use of ANSI function prototypes, but they're still important to be aware of. The implicit function declarations mentioned at the bottom of page 72 are an older feature of the language. They were handy back in the days when most functions returned int and function prototypes hadn't been invented yet, but today, if you want to use prototypes, you won't want to rely on implicit declarations. If you don't like depending on defaults and implicit declarations, or if you do want to use function prototypes religiously, you're under no compunction to make use of (or even learn about) implicit function declarations, and you'll want to configure your compiler so that it will warn you if you call a function which does not have an explicit, prototyped declaration in scope. You may wonder why the compiler is able to get some things right (such as implicit conversions between integers and floating-point within expressions) whether or not you're explicit about your intentions, while in other circumstances (such as while calling functions returning non-integers) you must be explicit. The question of when to be explicit and when to rely on the compiler hinges on several questions: 1. How much information does the compiler have available to it? 2. How likely is it that the compiler will infer the right action?

3. How likely is it that a mistake which you the programmer might make will be caught by the compiler, or silently compiled into incorrect code? It's fine to depend on things like implicit conversions as long as the compiler has all the information it needs to get them right, unambiguously. (Relying on implicit conversions can make code cleaner, clearer, and easier to maintain.) Relying on implicit declarations, however, is discouraged, for several reasons. First, there are generally fewer declarations than expressions in a program, so the impact (i.e. work) of making them all explicit is less. Second, thinking about declarations is good discipline, and requiring that everything normally be declared explicitly can let the compiler catch a number of errors for you (such as misspelled functions or variables). Finally, since the compiler only compiles one source file at a time, it is never able to detect inconsistencies between files (such as a function or variable declared one way in once source file and some other way in another), so it's important that cross-file declarations be explicit and consistent. (Various strategies, such as placing common declarations in header files so that they can be #included wherever they're needed, and requesting that the compiler warn about function calls without prototypes in scope, can help to reduce the number of errors having to do with improper declarations.) For the most part, you can also ignore the ``old style'' function syntax, which hardly anyone is using any more. The only thing to watch out for is that an empty set of parentheses in a function declaration is an old-style declaration and means ``unspecified arguments,'' not ``no arguments.'' To declare a new-style function taking no arguments, you must include the keyword void between the parentheses, which makes the lack of arguments explicit. (A declaration like
int f(void);

does not declare a function accepting one argument of type void, which would be meaningless, since the definition of type void is that it is a type with no values. Instead, as a special case, a single, unnamed parameter of type void indicates that a function takes no arguments.) For example, the definition of the getchar function might look like
int getchar(void) { int c;

read next character into c somehow

if (no next character) return EOF;

return c; }

page 73 Note that this version of atoi, written in terms of atof, has very slightly different behavior: it reads past a '.' (and, assuming a fully-functional version of atof, an 'e'). The use of an explicit cast when returning a floating-point expression from a routine declared as returning int represents another point on the spectrum of what you should worry about explicitly versus what you should feel comfortable making use of implicitly. This is a case where the compiler can do the ``right thing'' safely and unambiguously, as long as what you said (in this case, to return a floatingpoint expression from a routine declared as returning int) is in fact what you meant. But since the real possibility exists that discarding the fractional part is not what you meant, some compilers will warn you about it. Typically, compilers which warn about such things can be quieted by using an explicit cast; the explicit cast (even though it appears to ask for the same conversion that would have happened implicitly) serves to silence the warning. (In general, it's best to silence spurious warnings rather than just ignoring them. If you get in the habit of ignoring them, sooner or later you'll overlook a significant one that you would have cared about.)

Read sequentially: prev next up top This page by Steve Summit // Copyright 1995, 1996 // mail feedback
section 4.3: External Variables

The word ``external'' is, roughly speaking, equivalent to ``global.'' page 74 A program with ``too many data connections between functions'' hasn't managed to achieve the desirable attributes we were talking about earlier, in particular that a function's ``interface to the rest of the program is clean and narrow.'' Another bit of jargon you may hear is the word ``coupling,'' which refers to how much one piece of a program has to know about another.

In general, as we have mentioned, the connections between functions should generally be few and well-defined, in which case they will be amenable to regular old function arguments, and you won't be tempted to pass lots of data around in global variables. (On the other hand, global variables are fine for some things, such as configuration information which the whole program cares about and which is set just once at program startup and then doesn't change.) The word ``lifetime'' refers to how long a variable and its value stick around. (The jargon term is ``duration.'') So far, we've seen that global variables persist for the life of the program, while local variables last only as long as the functions defining them are active. However, lifetime (duration) is a separate and orthogonal concept from scope; we'll soon be meeting local variables which persist for the life of the program. Deep sentence: Thus if two functions must share some data, yet neither calls the other, it is often most convenient if the shared data is kept in external variables rather than passed in and out via arguments. (Later, though, we'll learn about data structures which can make it more convenient to pass certain data around via function arguments, so we'll have less reason for using external variables for these sorts of purposes.) ``Reverse Polish'' is used by some (earlier, all) Hewlett-Packard calculators. (The name is based on the nationality of the mathematician who studied and formalized this notation.) It may seem strange at first, but it's natural if you observe that you need both numbers (operands) before you can carry out an operation on them. (This fact is one of the reasons that reverse Polish notation is ``easier to implement.'') The calculator example is a bit long and a bit involved, but I urge you to work through and understand it. A calculator is something that everyone's likely to be familiar with; it's interesting to see how one might work inside; and the techniques used here are generally useful in all sorts of programs. A ``stack'' is simply a last-in, first-out list. You ``push'' data items onto a stack, and whenever you ``pop'' an item from the stack, you get the one most recently pushed. pages 76-79 The code for the calculator may seem daunting at first, but it's much easier to follow if you look at each part in isolation (as good functions are meant to be looked at), and notice that the routines fall into three levels. At the top level is the

calculator itself, which resides in the function main. The main function calls three lower-level functions: push, pop, and getop. getop, in turn, is written in terms of the still lower-level functions getch andungetch. A few details of the communication among these functions deserve mention. The getop routine actually returns two values. Its formal return value is a character representing the next operation to be performed. Usually, that character is just the character the user typed, that is, +, -, *, or /. In the case of a number typed by the user, the special code NUMBER is returned (which happens to be #defined to be the character '0', but that's arbitrary). A return value of NUMBER indicates that an entire string of digits has been typed, and the string itself is copied into the array s passed to getop. In this case, therefore, the array s is the second return value. In some printings, the second line on page 76 reads
#include <math.h> /* for atof() */

which is incorrect; it should be


#include <stdlib.h> /* for atof() */

page 77 Make sure you understand why the code


push(pop() - pop()); /* WRONG */

might not work correctly. ``The representation can be hidden'' means that the declarations of these variables can follow main in the file, such that main can't ``see'' them (that is, can't attempt to refer to them). Furthermore, as we'll see, the declarations might be moved to a separate source file, and main won't care. pages 77-78 Note that getop does not incorporate the functionality of atoi or atof--it collects and returns the digits as a string, and main calls atof to convert the string to a floating-point number (prior to pushing it on the stack). (There's nothing profound about this arrangement; there's no particular reason why getop couldn't have been set up to do the conversion itself.) The reasons for using a routine like ungetch are good and sufficient, but they may not be obvious at first. The essential motivation, as the authors explain, is that when we're reading a string of digits, we don't know when we've reached the end of the string of digits until we've read a non-digit, and that non-digit is not part of the string of digits, so we really shouldn't have read it yet, after all. The rest of the program is set up based on the assumption that one call to getop will return the

string of digits, and the next call will return whatever operator followed the string of digits. To understand why the surprising and perhaps kludgeysounding getch/ungetch approach is in fact a good one, let's consider the alternatives. getop could keep track of the one-too-far character somehow, and remember to use it next time instead of reading a new character. (Exercise 4-11 asks you to implement exactly this.) But this arrangement of getop is considerably less clean from the standpoint of the ``invariants'' we were discussing earlier. getop can be written relatively cleanly if one of its invariants is that the operator it's getting is always formed by reading the next character(s) from the input stream. getop would be considerably messier if it always had to remember to use an old character if it had one, or read a new character otherwise. If getop were modified later to read new kinds of operators, and if reading them involved reading more characters, it would be easy to forget to take into account the possibility of an old character each time a new character was needed. In other words, everywhere that getop wanted to do the operation
read the next character

it would instead have to do


if (there's an old character) use it else read the next character

It's much cleaner to push the checking for an old character down into the getch routine. Devising a pair of routines like getch and ungetch is an excellent example of the process of abstraction. We had a problem: while reading a string of digits, we always read one character too far. The obvious solution--remembering the one-toofar character and using it later--would have been clumsy if we'd implemented it directly within getop. So we invented some new functions to centralize and encapsulate the functionality of remembering accidentally-read characters, so that getop could be written cleanly in terms of a simple ``get next character'' operation. By centralizing the functionality, we make it easy for getop to use it consistently, and by encapsulating it, we hide the (potentially ugly) details from the rest of the program. getch and ungetch may be tricky to write, but once we've written them, we can seal up the little black boxes they're in and not worry about them any more, and the rest of the program (especially getop) is cleaner. page 79 If you're not used to the conditional operator ?: yet, here's how getch would look without it:

int getch(void) { if (bufp > 0) return buf[--bufp]; else return getchar(); }

Also, the extra generality of these two routines (namely, that they can push back and remember several characters, a feature which the calculator program doesn't even use) makes them a bit harder to follow. Exercise 4-8 asks you two write simpler versions which allow only one character of pushback. (Also, as the text notes, we don't really have to be writing ungetch at all, because the standard library already provides an ungetc which can provide one character of pushback for getchar.) When we defined a stack, we said that it was ``last-in, first-out.'' Are the versions of getch and ungetch on page 79 last-in, first-out or first-in, first out? Do you agree with this choice? One last note: the name of the variable bufp suggests that it is a pointer, but it's actually an index into the buf array.

Read sequentially: prev next up top This page by Steve Summit // Copyright 1995, 1996 // mail feedback
section 4.4: Scope Rules page 80

With respect to the ``practical matter'' of splitting the calculator program up into multiple source files, though it's certainly small enough to fit comfortably into a single source file, it's not so small that there's anything wrong with splitting it up into multiple source files, especially if we start adding functionality to it. The scope of a name is what we have been calling its ``visibility.'' When we say things like ``calling a function with a prototype in scope'' we mean that a prototype is visible, that a declaration is in effect. The variables sp and val can be used by the push and pop routines because they're defined in the same file (and the definitions appear before push and pop). They can't be used in main because no declaration for them appears in main.c (nor in calc.h, which main.c #includes). If main attempted to refer to sp or val, they'd be flagged as undefined. (Don't worry about the visibility of ``push and pop themselves.'')

The paragraph beginning ``On the other hand'' is explaining how global (``external'') variables like sp and val could be accessed in a file other than the file where they are defined. In the examples we've been looking at, as we've said, sp and val can be used in push and pop because the variables are defined above the functions. If the variables were defined elsewhere (i.e. in some other file), we'd need a declaration above--and that's exactly what extern is for. (See page 81 for an example.) page 81 A definition creates a variable, and for any given global variable, you only want to do that once. Anywhere else, you want to refer to an existing variable, created elsewhere, without creating a new, conflicting one. Referring to an existing variable or function is exactly what a declaration is for. Note also that the definition may optionally initialize the variable. (Don't worry about why a declaration may optionally include an array dimension.) ``This same organization would also be needed if the definitions of sp and val followed their use in one file'' means that we could conceivably have, in one file,
extern int sp; extern double val[];

void push(double f) { ... } double pop(void) { ... }

int sp = 0; double val[MAXVAL];

So ``extern'' just means ``somewhere else''; it doesn't have to mean ``in a different file,'' though usually it does.

Read sequentially: prev next up top This page by Steve Summit // Copyright 1995, 1996 // mail feedback
section 4.5: Header Files page 82

By the way, the ``.h'' traditionally used in header file names simply stands for ``header.'' We can imagine several strategies for using header files. At one extreme would be to use zero header files, and to repeat declarations in each file which needed them. This would clearly be a poor strategy, because whenever a declaration changed, we would have to remember to change it in several places, and it would be easy to miss one of them, leading to stubborn bugs. At the other extreme would be to use one header file for each source file (declaring just the things defined in that source file, to be #included by files using those things), but such a proliferation of header files would usually be unwieldy. For small projects (such as the calculator example), it's a reasonable strategy to use one header file for the entire project. For larger projects, you'll usually have several header files for sets of related declarations.

Read sequentially: prev next up top This page by Steve Summit // Copyright 1995, 1996 // mail feedback
section 4.6: Static Variables page 83

Deep sentence: The static declaration, applied to an external variable or function, limits the scope of that object to the rest of the source file being compiled. External static thus provides a way to hide names like buf andbufp in the getchungetch combination, which must be external so they can be shared, yet which should not be visible to users of getch and ungetch. So we can have three kinds of declarations: local to one function, restricted to one source file, or global across potentially many source files. We can imagine other possibilities, but these three cover most needs. Notice that the static keyword does two completely different things. Applied to a local variable (one inside of a function), it modifies the lifetime (``duration'') of the variable so that it persists for as long as the program does, and does not disappear between invocations of the function. Applied to a variable outside of a function (or to a function) static limits the scope to the current file.

To summarize the scope of external and static functions and variables: when a function or global variable is defined without static, its scope is potentially the entire program, although any file which wishes to use it will generally need an extern declaration. A definition with static limits the scope by prohibiting other files from accessing a variable or function; even if they try to use an extern declaration, they'll get errors about ``undefined externals.'' The rules for declaring and defining functions and global variables, and using the extern and static keywords, are admittedly complicated and somewhat confusing. You don't need to memorize all of the rules right away: just use simple declarations and definitions at first, and as you find yourself needing some of the more complicated possibilities such as static variables, the rules will begin to make more sense.

Read sequentially: prev next up top This page by Steve Summit // Copyright 1995, 1996 // mail feedback
section 4.7: Register Variables page 83

The register keyword is only a hint. The compiler might not put something in a register even though you ask it to, and it might put something in a register even though you don't ask it to. Most modern compilers do a good job of deciding when to put things in registers, so most of the time, you don't need to worry about it, and you don't have to use the register keyword at all. (A note to assembly language programmers: there's no way to specify which register a register variable gets assigned to. Also, when you specify a function parameter as register, it just means that the local copy of the parameter should be copied to a register if possible; it does not necessarily indicate that the parameter is going to be passed in a register.)

Read sequentially: prev next up top This page by Steve Summit // Copyright 1995, 1996 // mail feedback
section 4.8: Block Structure pages 84-85

You've probably heard that global variables are ``bad'' because they exist everywhere and it can be hard to keep track of who's using them. In the same way, it can be useful to limit the scope of a local variable to just the bit of the function that uses it, which is exactly what happens if we declare a variable in an inner block.

Read sequentially: prev next up top This page by Steve Summit // Copyright 1995, 1996 // mail feedback
section 4.9: Initialization page 85

These are some of the rules on initialization; we'll learn a few more later as we learn about a few more data types. If you don't feel like memorizing the rules for default initialization, just go ahead and explicitly initialize everything you care about. Earlier we said that C is quite general in its treatment of expressions: anywhere you can use an expression, you can use any expression. Here's an exception to that rule: in an initialization of an external or static variable (strictly speaking, any variable of static duration; generally speaking, any global variable or local static variable), the initializer must be a constant expression, with value determinable at compile time, without calling any functions. (This rule is easy to understand: since these initializations happen conceptually at compile time, before the program starts running, there's no way for a function call--that is, some runtime action--to be involved.) page 86 It probably won't concern you right away, but it turns out that there's another exception about the allowable expressions in initializers: in the brace-enclosed list of initializers for an array, all of the expressions must be constant expressions (even for local arrays). There is an error in some printings: if there are fewer explicit initializers than required for an array, the others will be initialized to zero, for external, static, and automatic (local) arrays. (When an automatic array has no initializers at all, then it contains garbage, just as simple automatic variables do.) If the initialization

char pattern[] = "ould";

makes sense to you, you're fine. But if the statement that


char pattern[] = "ould";

is equivalent to
char pattern[] = { 'o', 'u', 'l', 'd', '\0' };

bothers you at all, study it until it makes sense. Also, note that a character array which seems to contain (for example) four characters actually contains five, because of the terminating '\0'.

Read sequentially: prev next up top This page by Steve Summit // Copyright 1995, 1996 // mail feedback
section 4.10: Recursion page 86

Recursion is a simple but deep concept which is occasionally presented somewhat bewilderingly. Please don't be put off by it. If this section stops making sense, don't worry about it; we'll revisit recursion in chapter 6. Earlier we said that a function is (or ought to be) a ``black box'' which does some job and does it well. Whenever you need to get that job done, you're supposed to be able to call that function. You're not supposed to have to worry about any reasons why the function might not be able to do that job for you just now. It turns out that some functions are naturally written in such a way that they can do their job by calling themselves to do part of their job. This seems like a crazy idea at first, but based on a strict interpretation of our observation about functions--that we ought to be able to call them whenever we need their job done--calling a function from within itself ought not to be illegal, and in fact in C it is legal. Such a call is called a recursive call, and it works because it's possible to have several instances of a function active simultaneously. They don't interfere with each other, because each instance has its own copies of its parameters and local variables. (However, if a function accesses any static or global data, it must be written carefully if it is to be called recursively, because then different instances of it could interfere with each other.) Let's consider the printd example rather carefully. First, remind yourself about the reverse-order problem from the itoa example on page 64 in section 3.6. The ``obvious'' algorithm for determining the digits in a number, which involves successively dividing it by 10 and looking at the remainders, generates digits in right-to-left order, but we'd usually like them in left-to-right order, especially if

we're printing them out as we go. Let's see if we can figure out another way to do it. It's easy to find the lowest (rightmost) digit; that's n % 10. It's easy to compute all but the lowest digit; that's n / 10. So we could print a number left-to-right, directly, without any explicit reversal step, if we had a routine to print all but the last digit. We could call that routine, then print the last digit ourselves. But--here's the surprise--the routine to ``print all but the last digit'' is printd, the routine we're writing, if we call it with an argument of n / 10. Recursion seems like cheating--it seems that if you're writing a routine to do something (in this case, to print digits) and instead of writing code to print digits you just punt and call a routine for printing digits and which is in fact the very routine you're supposed to write--it seems like you haven't done the job you came to do. A recursive function seems like circular reasoning; it seems to beg the question of how it does its job. But if you're writing a recursive function, as long as you do a little bit of work yourself, and only pass on a portion of the job to another instance of yourself, you haven't completely reneged on your responsibilities. Furthermore, if you're ever called with such a small job to do that the little bit you're willing to do encompasses the whole job, you don't have to call yourself again (there's no remaining portion that you can't do). Finally, since each recursive call does some work, passing on smaller and smaller portions to succeeding recursive calls, and since the last call (where the remaining portion is empty) doesn't generate any more recursive calls, the recursion is broken and doesn't constitute an infinite loop. Don't worry about the quicksort example if it seems impenetrable--quicksort is an important algorithm, but it is not easy to understand completely at first. Note that the qsort routine described here is very different from the standard library qsort (in fact, it probably shouldn't even have the same name).

Read sequentially: prev next up top This page by Steve Summit // Copyright 1995, 1996 // mail feedback

You might also like