About a year ago I spent a few days sightseeing in Rome. The weather was great: cool enough to walk around without getting overheated, but not so cold that I felt like I needed to bundle up to stay warm. So at about eight in the morning I walked the mile or so from my hotel to the Colloseum, through the Roman Forum and then through the Capitoline Museums. By the time I got out of the museums I had been on my feet for about five hours, and I soon found myself sitting on the steps of the Vittoriano, watching the early afternoon traffic flow around the Piazza Venezia.
The Piazza Venezia is an island of grass at the confluence of three major roads. Traffic circles counterclockwise around the Piazza, with no stop signs, no traffic lights, no controls of any kind. Here in Boston we’d call it a rotary, but that rather mundane word doesn’t do justice to the beauty of the Piazza, and to those of us who battle with the horn-honking and intimidation of rotaries it doesn’t convey the orderliness of the liquid flow that I saw that day as traffic patterns shifted to reflect the variations in the number of cars coming in and going out on each of the three roads. Here in the United States a traffic load like that would have resulted in utter chaos and near gridlock. In Rome the drivers seemed to view themselves as part of a larger enterprise, whose purpose was to get everyone to their destination as quickly as possible. They waited when waiting was appropriate, and moved when moving was appropriate. They trusted the other drivers and didn’t view them as competitors. The result was that nobody had to wait for very long, and traffic moved quickly and smoothly through this major intersection.
In the United States we learn to drive in accordance with the fundamental principle of defensive driving: assume that the driver of the car next to you is going to do something really stupid, and make sure you have a way out. For the most part we drive slowly and hesitantly, trying to make sure that we’re safe, and ultimately increasing congestion and slowing everyone down. The drivers in Rome work with a different fundamental principle: they assume that the drivers around them are reasonably competent, and that they’ll do something reasonable when faced with an unexpected situation; they drive accordingly, doing reasonable things that we view as too aggressive. The drivers in Rome know how to drive1.
Here in The Journeyman’s Shop we approach programming in the same way that drivers in Rome approach driving: we assume that the other programmers we are working with are reasonably competent, and we don’t put a lot of effort into making our code safe for use by beginners. That doesn’t mean that we don’t write code that’s reasonably robust even when it is misused. What it means is that we try to recognize the kinds of mistake that are most likely to happen when people use our code, and add some sort of protection against those mistakes. We don’t try to protect our code from every possible misuse. That takes far too much time, and offers very little benefit. We know that we’ll get to our goal much faster, with no loss of safety, if we protect ourselves only from things that are likely to happen, and don’t concern ourselves with things that shouldn’t happen.
I saw a good example of over-protectiveness the other day in a
message on comp.lang.c++. The writer explained that he always uses
void
in the declaration of a C++ function that takes no
arguments, in case someone who is reading the code doesn’t know that in
C++ an empty argument list means that the function takes no arguments.
That’s a mistake that someone who has just moved from C to C++ could
make, because the two languages give different meanings to an empty
argument list. But that’s not the sort of mistake that an experienced
C++ programmer ought to be worried about. The newcomer will quickly
learn this difference, and this memory aid will be of no value2. That’s not in itself bad, but this minor
protection comes at a fairly high price: when I read this sort of code
and see all those unnecessary voids it looks to me like the person who
wrote it doesn’t understand what an empty argument list means in C++.
Now I have to be much more cautious in what I expect this person to be
able to do, because the code suggests that he doesn’t really understand
C++. We’re in a traffic jam, caused entirely by this
tentativeness3. The solution is to
pretend that we’re in Rome: instead of slowing everyone down to
accommodate beginners, we should teach beginners good habits so that
when they are ready they can move smoothly into the flow. In the long
run we’ll be much more productive.
This month we’re going to begin talking about initialization and
cleanup. This is an area where errors are common, and often disastrous.
Many of us have had the experience of hunting down that uninitialized
pointer that crashes our application after it’s been running for ten
minutes (or, if you are particularly unlucky, ten days). Stabilizing
that problem so that we can reproduce it consistently, which is the
first step in identifying the problem and fixing it, can often be very
difficult - the problem comes and goes as we change our code, because
the value that the pointer takes on depends on what code has been run
before the function that creates it. This is typical of the kinds of
problems that result from uninitialized variables. They show up in
unpredictable places, they disappear when we run the program under the
debugger, and they move when we change the code. Because these problems
are often so hard to track down, we must make a careful effort to avoid
them. This means thinking about initializing every variable that we
create. Some folks even insist that every variable be initialized when
it is created, even if only to a recognizable value, such as
NULL
for a pointer. I don’t go quite that far, but I do
insist that not initializing a variable be the result of a deliberate,
defensible decision. If you can’t explain why it is safe to not
initialize a particular variable, you haven’t been paying enough
attention to the code that you are writing.
Initialization simply means setting a variable to a meaningful value. In some cases the compiler does this for you. That’s okay if the value that the compiler chooses really is appropriate for your program, but don’t fall into the habit of simply accepting what the compiler gives you. Think about what makes sense in your program, and choose a value accordingly. Then make sure that you understand how to initialize the variable and when that initialization will occur. Every time you create a variable in your program, think about those three things: what should I initialize it to, how should I initialize it, and when should I initialize it. If initializing a variable involves allocating resources, then add two more items to the list: how should I dispose of the resource, and when should I dispose of the resource. If you develop this habit you’ll find that your code becomes much more robust.
This month we’ll talk about C. Almost all of what we’re going to be looking at also applies to C++, but in somewhat more complicated ways. Rather than try to point out the exceptions and qualifications as I go along, I’ll sum them up when we get into C++ next month.
In C all file scope variables that are of arithmetic types are
initialized to 0. All file scope variables that are pointers are
initialized to NULL
. This lets you create global variables
that are automatically initialized when the program starts. All you have
to do is define them, and the compiler does the rest. For example:
int i; /* i is initialized to 0 */
long j; /* j is initialized to 0 */
void *data; /* initialized to NULL */
Now, that’s not particularly surprising to anyone with just a little bit of experience with C or C++. However, you’ll occasionally encounter people who don’t trust the compiler, and insist on explicitly initializing these variables. That’s not necessary: the language definition requires that this initialization take place.
The rule for static initializers is actually broader than what I’ve stated above: it also applies to arithmetic types and pointer types that are members of arrays and structs:
short data[20]; /* all elements of data are
initialized to 0 */
struct complex { double real; double imag; };
struct complex c; /* c.real, c.imag are
initialized to 0.0 */
Explicit initializers, as the name suggests, are initializers that you write yourself. Once again, you’re probably already quite familiar with them: they let you specify the value that your variable is initialized with.
For simple types such as integral types and pointers, an explicit initializer is simply an equal sign followed by the value that you want to initialize the variable with. In C, initializers for variables defined in file scope must be compile-time constants. That is, they must be values that the compiler can evaluate under a somewhat constrained set of rules. You cannot initialize file scope variables with values that must be computed at runtime. For example,
void *buffer = malloc(100);
This initialization is not legal in C. However, as we’ll see later, you can use an expression like this to initialize a variable that’s local to a function.
When you need to initialize an array or a struct you use a form known as "aggregate initialization." In its simplest form, an aggregate initializer is simply a pair of curly braces containing a list of the initial values for the fields being initialized. The initial values are assigned to the elements that are being initialized in the order that both occur. For example,
int data1[5] = { 0, 1, 2, 3, 4 };
This creates an array of five integers, and initializes
data1[0]
to 0, data1[1]
to 1, and so on.
Structs work pretty much the same way:
struct complex
{
double real;
double imag;
};
struct complex c1 = { 1.0, 0.0 };
This creates a variable named c1
of type
complex
, with the field real
initialized to
1.0 and the field imag
initialized to 0.0.
When you start working with more complicated data structures such as arrays of structs or structs that contain other structs you have a choice: you can continue to simply list initial values in the order that their corresponding elements occur, or you can add additional pairs of braces to reflect the actual structure of the data. The first approach is simpler to write, but you have to be careful not to get lost. The second is usually easier to read, and gives you more control. Let’s look at initializing an array of complex:
struct complex vector[3] =
{ 1.0, 0.0, 0.0, 1.0, 1.0, 1.0 };
This creates an array of three complex objects, and initializes
vector[0]
to (1.0,0.0), vector[1]
to
(0.0,1.0), and vector[2]
to (1.0,1.0). If you need to
figure out what the initial value assigned to vector[1]
is
from looking at this initializer, you have to count carefully. That’s
where the other form has an advantage:
struct complex vector2[3] =
{ {1.0, 0.0}, {0.0, 1.0}, {1.0, 1.0} };
Now it’s easier to see which values go with which array element.
In both cases, if you supply fewer initializer values than there are elements to be initialized, all the remaining elements are initialized as if the variable had been declared in file scope. That is, they get their bytes set to zero.
int data2[5] = { 1, 2 };
This statement initializes data2[0]
to 1,
data2[1]
to 2, and the remaining three elements of
data2
to 0.
There’s also a shortcut that you can use when you’re initializing an array: you can ask the compiler to figure out from the initializer how large the array actually is. We could have written the first example in this section like this:
int data3[] = { 0, 1, 2, 3, 4 };
The compiler would count the number of initializers and figure out
that data3
has five elements.
Finally, when you are initializing an array of char you can use a quoted string for an initializer:
char name[] = "The Journeyman's Shop";
This creates an array of 22 char elements, with each one holding the corresponding character from the initializer. Remember, this string has a terminating 0, and that’s the value that goes into the last element of the array.
In C but not in C++ you’re allowed to specify an array size that’s one character too small for the quoted string:
char name1[21] = "The Journeyman's Shop";
Now there’s no room for the terminating 0, and the array will be initialized with the characters up to but not including the 0. This only applies to character arrays that are one character too small: you can’t use this to chop out an initializer consisting of an arbitrary set of characters from the beginning of the string.
I said earlier that one of the things you have to consider when you think about initialization is when the initialization should occur. I haven’t said anything about that yet, because it really doesn’t make much difference when you are initializing file scope variables in C: they’ll be initialized before you get a chance to do anything with them. When you start looking at variables that are defined inside of functions, however, it’s a different matter. We’ll look at some examples shortly.
Auto variables are the variables that we define inside of functions
without the label static
. They can be initialized in
exactly the same way as file scope variables, with one important
difference: if you do not specify an initializer for an auto variable it
does not get initialized. This leaves it with an indeterminate value.
Don’t try and use the value held in an uninitialized variable: doing so
can crash your program. Make sure that you set it to a usable value
before you try to use it.
All of the examples that we looked at above are legal forms of initialization for auto variables:
void f()
{
int i; /* i has an indeterminate value */
long j; /* j has an indeterminate value */
void *data; /* data has an indeterminate value */
void *buffer = malloc(100);
int data1[5] = { 0, 1, 2, 3, 4 };
struct complex c1 = { 1.0, 0.0 };
struct complex vector[3] =
{ 1.0, 0.0, 0.0, 1.0, 1.0, 1.0 };
struct complex vector2[3] =
{ {1.0, 0.0}, {0.0, 1.0}, {1.0, 1.0} };
int data2[5] = { 1, 2 };
int data3[] = { 0, 1, 2, 3, 4 };
char name[] = "The Journeyman's Shop";
char name1[21] = "The Journeyman's Shop";
}
Each of these initialization will be performed each time that
f
is executed. This lets you begin execution with all of
your auto variables in a known state.
Of course, the variables that you haven’t initialized aren’t in a
known state, they’re indeterminate. That’s often a mistake, but
sometimes it’s intentional. You might not have enough information at the
start of a function to initialize some of your auto variables. Since C
only lets you define variables at the beginning of a block4, you have to either define such variables and
not initialize them, or create otherwise unnecessary blocks just to
allow initialization. For example, suppose you have written a function
that takes a pointer to char and returns an int. This function displays
the string on the console, reads an integer value that the user types in
in response to the prompt, and returns that value. Let’s call it, say,
get_integer
:
int get_integer(char *prompt);
Now let’s write a function that concatenates a couple of strings to
produce a prompt, then calls get_integer
:
void test(char *part1, char *part2)
{
char prompt[128];
int val;
strcpy(prompt, part1);
strcat(prompt, part2);
val = get_integer(prompt);
/* code continues from here. */
}
Now, notice that I haven’t initialized val
. I was
careful to assign a value to it as soon as I could, however, so that I
won’t accidentally use the indeterminate value that it has when
execution of the function begins. The alternative, adding a block so
that I can define val
after I’ve figured out how to
initialize it, looks like this:
void test(char *part1, char *part2)
{
char prompt[128];
strcpy(prompt, part1);
strcat(prompt, part2);
{
int val = get_integer(prompt);
/* code continues from here. */
}
}
Now, I don’t know about you, but I don’t particularly like adding
blocks just so that I can define new variables. I prefer to use blocks
only as part of flow control statements such as if
,
while
, switch
, etc. I find it confusing to add
blocks that don’t reflect flow control. That is, I don’t particularly
like this technique for avoiding uninitialized variables. I much prefer
the first version, even though it’s possible to accidentally use
val
before it has been assigned a value.
Some people would insist on initializing val
, even
though the value that it is initialized to will never be used. This
seems to come out of a sense of tidiness. I think it’s a mistake. You
should initialize variables to meaningful values when those values are
available. Don’t give them meaningless values, just in case. That’s not
programming, it’s fear.
In general, of course, when you write a function you should group statements that do related things together. That principle calls for initializing variables as close to the point where they are used as possible. For example:
void f()
{
int i;
/* do some things here that don’t involve i */
for (i = 0; i < 100; ++i)
compute(i);
}
I suspect that nobody has a problem with deferring initialization of
i
to the point where it is actually used. It makes the code
much easier to read.
The drawback to deferring initialization is that it isn’t really initialization when we defer it. That is, it’s not initialization in the technical sense in which the language definition uses the word. This means that some of the initialization constructs that we’ve looked at here cannot be used if we defer initialization:
void f()
{
char name[22];
struct complex c1;
name = "The Journeyman's Shop"; /* illegal */
c1 = { 1.0, 0.0 }; /* illegal */
}
However, in cases like this we do have alternatives available:
void f()
{
char name[22];
struct complex c1;
strcpy(name, "The Journeyman's Shop");
c1.real = 1.0;
c1.imag = 0.0;
}
Be a bit careful when you write late initializations like this: things sometimes aren’t quite what they seem. In particular, you might be tempted to replace an aggregate initializer with a call to memset. For example:
void f()
{
void *data[5] = { 0 };
}
This initialization initializes all five void pointers to
NULL
values. However, this code might not do the same
thing:
void f()
{
void *data[5];
memset(data, 0, sizeof(data));
}
The danger here is that a null pointer is not required to have a representation that is all zeroes, so setting the bytes of the pointers here to 0 does not necessarily initialize them to null. On most systems this will, in fact, work, but it’s not something you can count on. Assigning the value 0 to a pointer gives you a null pointer because the compiler is required to do whatever is needed to create the value of a null pointer. Setting the bytes of a pointer to 0 doesn’t give the compiler a chance to intervene, and need not work5.
You can also mark a local variable as static
, which
means that the variable stays around throughout the execution of the
program. It also means that, unlike an auto variable, it will be
initialized only once, the first time that the function that contains
its definition is called. For example:
void count_down()
{
static int sec = 10;
printf("%d\n", sec--);
if (sec == 0)
printf("Liftoff!\n");
}
int main()
{
int i;
for (i = 0; i < 10; ++i)
count_down();
return 0;
}
The first time that count_down
is called it will
initialize sec
to 10, print out its value, and decrement
it. Succeeding calls to count_down
will not reinitialize
sec
, but will only print out its value and decrement it.
That is, until sec
reaches 0. This is especially useful
when a function requires some sort of resource, but might not be called
at all during program execution. Instead of allocating that resource
whenever the program runs, allocate it in an initializer for a static
variable in the function that needs it:
void f()
{
static char *buffer = malloc(10000);
/* code that uses buffer */
}
If f
is never called its buffer will not be allocated,
and there will be more memory available for the rest of the
program6.
Whenever you allocate a resource you should think about how to dispose of it. In many cases this is easy to do: if you won’t need that resource after exiting from the function that you allocated it in, you can simply dispose of it at the end of that function. Like this:
void f()
{
char *buffer = malloc(1000);
if (buffer != NULL)
{
/* do something with buffer... */
}
free(buffer);
}
If you are allocating a resource that will be used in other parts of the program, however, making sure that this resource is released before the program exits is a bit more complicated. You might be tempted to put the cleanup code in main:
void *scarce_resource;
int main()
{
scarce_resource = get_resource();
process_data();
release_resource(scarce_resource);
return 0;
}
The problem with this approach is that if any of the code executed in
the call to process_data
calls exit
,
process_data
will not return and the cleanup code will not
be executed. If the resource that this cleanup code handles does not get
released by the operating system when the program terminates, you’ve got
a problem. The solution in C is to write a function that releases the
resource, and register it with atexit
7. After you’ve done this, whenever your program
terminates by returning from main
or by a call to
exit
, your cleanup function will be called. If the program
terminates by a call to abort
this cleanup does not
happen.
For example, the Java library that I’ve been working on recently has
a thread support package and a garbage collector, both of which need to
be initialized at program startup and shut down at program termination.
Each of these packages provides two functions for these operations:
int jt_init(void)
to initialize the thread package and
int gc_init(void)
to initialize the garbage collector; and
void jt_cleanup(void)
to shut down the thread package and
void gc_cleanup(void)
to shut down the garbage collector.
By an odd coincidence, both cleanup functions have exactly the signature
that’s required by atexit
. The library is written for use
with a Java to C translator, so execution of the Java program begins in
the C function main
, which in a somewhat simplified form
looks like this:
int main(int argc, char *argv[])
{
if (jt_init() != SUCCESS || atexit(jt_cleanup) != 0)
exit(EXIT_FAILURE);
if (gc_init() != SUCCESS || atexit(gc_cleanup) != 0)
exit(EXIT_FAILURE);
user_entry(format_args(argc, argv));
return 0;
}
The first line initializes the thread package and registers its
cleanup function. If either of these operations fails, the application
is terminated by the call to exit
in the second line.
Similarly, the third line initializes the garbage collector and
registers its cleanup function. Again, if either of these operations
fails, the application is terminated. Notice, however, that if this
second initialization fails it is not necessary to explicitly call the
thread package’s cleanup function. The code has registered that function
with atexit
, and the C runtime library will call that
function as the application exits.
There are a couple of things you need to know when you use
atexit
. First, the functions that you register are called
in the reverse order of their registration. In the Java main function
that I listed above, this means that gc_cleanup
will be
called first, then jt_cleanup
. Second, the C standard
requires implementors to support at least 32 registrations. Don’t go
over this limit, and always check that registration was successful, just
in case.
Fred Tydeman pointed out several mistakes I made in my discussion of floating point errors in my December column:
C9X is not adopting IEEE-754 specs for all floating-point. It is allowing that to be one of the bindings for floating-point.
HUGE_VAL
already can be positive infinity (see footnote
104) in C89. "Dividing positive infinity by zero produces
NaN." Is wrong. It is +/-infinity (depends upon the signs of the
infinity and the zero).
"... we won’t ever get back to a normal value." Is wrong. 1 / infinity is zero.
Next month we’ll talk about how C++ and Java affect all this. That means constructors, destructors, exceptions, and finally blocks.
1. Please note that I said that they assume that the other drivers are "reasonably competent." I am not advocating the Indy-500 tactics that you often see when driving between Santa Cruz and San Jose on Route 17. Competitive driving is for trained professionals only, and has no place on public highways.
2. I’m not claiming that using void in a declaration is never appropriate. If we’re writing a header that has to compile as both C and C++, for example, then we must follow the C rule. But we do this because there is an identifiable technical reason for doing it, not out of a general sense that it might be safer.
3. I’m not saying that beginners shouldn’t protect themselves with this sort of technique. But they should regard things like this as temporary aids, and at some point decide to take off the training wheels.
4. The next C standard, C9X, will allow auto variables to be defined at any point in a function, just as C++ does.
5. The same problem arises with floating point values: setting the bytes to 0 does not necessarily produce a valid floating point value.
6. One warning, however: many compilers don’t handle initialization of static variables correctly in multi- threaded programs. The danger here is that two threads will call the same function at the same time, and the initializer will be run twice. On the other hand, if you’re calling count_down from multiple threads without doing any synchronization yourself, the results probably won’t make sense anyway.
7. In C++ you can use atexit
, but
destructors provide a more powerful mechanism. We’ll look at destructors
next month.
Copyright © 1999-2006 by Pete Becker. All rights reserved.