Introduction to C Programming by Rob Miles, Electronic Engineering
One area where C scores highly is in the development of very large programs. In C there is a standard way of performing separate compilation, we have already seen it in action when we considered how input and output is performed.
You can split your large C program into several smaller files. Each file will contain a number of C functions which do some parts of the project. In a large development different programmers will be working on particular files in the system, this is quite easy to manage in C.
When you want to build your working program you must compile each source file and then link them all together. The compiler does not produce the finished program, instead it produces an intermediate file which contains the machine code for a particular source file along with details of the names of the variables used and the names of the functions compiled. This is usually called the object file. C uses the file extension facility provided by the operating system to tell the files apart:
You may have several object files for a large project, each of which was produced from a single source file. These are fitted together, along with the library code, by the linker which produces:
Note that these language extensions are the ones which MS-DOS uses. If you use UNIX the extensions are different, but they are used to the same effect.
The linker ties up all the separate files, for example if one program refers to a function called setup the linker will convert this reference to a call to setup defined in another file. Note that if the linker does not find a function called setup the linking process fails with an error.
You get linker errors if you refer to functions that do not exist, or define a function with the same name as one somewhere else. This means that even if your program compiles OK, it may still have errors in the text if you have spelt a function incorrectly.
If you are writing one file which is part of a large system, you will want to refer to other functions which are not local to your part. (We are already doing this when we use scanf and printf.) In the case of these routines there is a standard file called stdio.h which contains the definitions of external functions we want to refer to. We can build our own file of such definitions if we wish, and use them to refer to distant code. C will let you set up a function prototype. This is just the top part of a function, analogous to the forward declaration facility of PASCAL. All the compiler needs to know about an external routine is what it looks like, i.e. the name, the type of value returned by it and the number and type of the parameters. That is what the prototype gives it:
void increment ( int * it ) ;
This is a prototype for the increment function we wrote earlier. When C sees this it just drops a reference to that function into the object file and then expects the linker to sort things out when it builds the final program.
If I want to refer to external variables in my file I must tell C what they are called and what type they are. I can do this using the extern declaration modifier:
extern int i ;
This says to the compiler there is a variable called i, which is of type integer defined somewhere else. I do not want you do set up a variable of that name, just pretend that one exists and let the linker sort things out!
If the project is very large you may have lots of separate files, each containing functions and variables that you want to share. There is a standard way of defining these things, we have already used it with the standard input/output definition file stdio.h. For each file which I want to refer to in other ones I create a ".H" file. This contains all the function prototypes and external variable definitions for other programs to use. If other files want to make use of these routines they simply have to #include this specification file, i.e.:
This is very useful, someone can use my menu routines without having to look at the actual code - the ".H" file contains all they need to refer to them.
If there were many programmers working on a large project the first things that they would write would be all the ".H" files which define how all the functions will fit together, each programmer can then go on and write the code to do his or her particular part and the others can use it without ever seeing it!
The other thing that you can put into your ".H" files is the design of any data structures that you are using. If your big project contains customised structures you might have a file called STRUCTS.H which everyone uses. This means that you are all using the same copy of the definitions.
Another effect of splitting your project up into a number of separate files is that it makes working on the system faster. If you change one of the files you need only re-compile that file and re-link in all the ones which have not changed. This is much better than having to re-compile everything. However it does bring another problem, that of keeping all your object files up to date and making sure that you do not use an out of date file at any time. Furthermore, if you are using vital ".H" files, you must re-compile those source files which use them if they are changed.
Doing all this manually is a bit of a pain, so instead C provides an automated make facility. This allows you to construct a project file which tells the make system the names of all the source files in the system, and which files they depend on. When you want to create a new version of your program you simply call the make program which reads this project file and then looks at the datestamps of all the source and object files. If any source file is newer than the corresponding object one it is re-compiled. Furthermore, if a file which has changed has other ones which depend on it, all the dependent files are re-compiled too.
Good versions of C have very powerful make systems, they are virtually a "programming language" which is used to specify how the application is to be built.
If you are using an integrated programming environment, for example Borland C or Microsoft C, you can also create projects, which are similar to make files but also allow you manage the files visually.
I have been using #include extensively throughout the examples. This is an instruction which tells the compiler to take the contents of a file and include it at that particular point in the program. This form of activity is actually handled by the C pre-processor. We have already looked at the pre-processor in the context of magic numbers and the #define directive.
Pre-processor directives are preceded by a # sign and are the only thing on a line :
When the pre-processor sees the #include directive it looks for a file with the name following it and opens that file. It then passes the contents of that file to the compiler. At the end of the include file it continues with the current source file. The file that you include can also contain #include directives and the pre-processor will nest them as required.
Note that we have enclosed the file name in <> characters. Enclosing the name in <> tells the pre-processor to look in a special system include area for the file. This is where standard definition files for all the C run time library routines are kept. If you want to tell the system to look in the local directory instead you use "" to enclose the filename:
You can get the pre-processor to selectively pass on parts of your program to the compiler. This is very useful when you are developing something and want to add additional debugging code. If you were using PASCAL you would have to remove or comment out all the debug statements when you produce the final version. In C you can just tell the pre-processor not to pass particular parts of the program to the compiler.
The decision is made depending on whether a particular symbol has been defined previously, for example:
#ifdef debug printf ( "Intermediate value %d\n", i ) ; #endif
If debug had been defined previously the printf statement is passed to the compiler. If this symbol does not exist all the text between the #ifdef and the #endif is removed by the pre-processor and the compiler never sees it. This means that I can turn all my debug statements on simply by typing:
#define debug 1
- at the beginning of the program and then rebuilding it. Note that the translation that I give to debug is not important, merely the fact that it has been defined.
You can add an else part if you wish:
#ifdef friendly printf ( "Sorry, you made a mistake." ) ; #else printf ( "You idiot!" ) ; #endif
Remember that these decisions are not made when the program runs, they actually control what the program actually contains.
Another popular use for conditional compilation is the building of code which can be customised for various different machines. A particular compiler often has a number of words pre-defined. Your source can check for these and then include code to customise the program for that particular version. This makes writing portable code a lot easier.