|
Bad guesses
There is one more important issue you
should understand before we look at the general problems in creating a C
library. Note that the CLib.h header file must be included in any
file that refers to CStash because the compiler can’t even guess at
what that structure looks like. However, it can guess at what a function
looks like; this sounds like a feature but it turns out to be a major
C pitfall.
Although you should always declare
functions by including a header file,
function declarations
aren’t essential in C. It’s possible in C (but not in C++) to
call a function that you haven’t declared. A good compiler will warn you
that you probably ought to declare a function first, but it isn’t enforced
by the C language standard. This is a dangerous practice, because the C compiler
can assume that a function that you call with an int argument has an
argument list containing int, even if it may actually contain a
float. This can produce bugs that are very difficult to find, as
you will see.
Each separate C implementation file (with
an extension of .c) is a
translation unit. That
is, the compiler is run separately on each translation unit, and when it is
running it is aware of only that unit. Thus, any information you provide by
including header files is quite important because it determines the
compiler’s understanding of the rest of your program. Declarations in
header files are particularly important, because everywhere the header is
included, the compiler will know exactly what to do. If, for example, you have a
declaration in a header file that says void func(float), the compiler
knows that if you call that function with an integer argument, it should
convert
the int to a float as it passes the argument (this is called
promotion). Without the declaration, the C compiler would simply assume
that a function func(int) existed, it wouldn’t do the promotion,
and the wrong data would quietly be passed into
func( ).
For each translation unit, the compiler
creates an object file, with an extension of .o
or .obj or something similar. These object files, along with the
necessary start-up code, must be collected by the linker
into the executable program. During linking, all the
external references must be resolved. For example, in CLibTest.cpp,
functions such as initialize( ) and fetch( ) are
declared (that is, the compiler is told what they look like) and used, but not
defined. They are defined elsewhere, in CLib.cpp. Thus, the calls in
CLib.cpp are external references. The linker must, when it puts all the
object files together, take the unresolved external references
and find the addresses they
actually refer to. Those addresses are put into the executable program to
replace the external references.
It’s important to realize that in
C, the external references that the linker searches for are simply function
names, generally with an underscore in front of them. So all the linker has to
do is match up the function name where it is called and the function body in the
object file, and it’s done. If you accidentally made a call that the
compiler interpreted as func(int) and there’s a function body for
func(float) in some other object file, the linker will see _func
in one place and _func in another, and it will think everything’s
OK. The func( ) at the calling location will push an int onto
the stack, and the func( ) function body will expect a float
to be on the stack. If the function only reads the value and doesn’t write
to it, it won’t blow up the stack. In fact, the float value it
reads off the stack might even make some kind of sense. That’s worse
because it’s harder to find the
bug.
|
|