Most projects in C++ have several parts. In the notes on make, we saw how to build several programs at the same time. Here we are going to build one program, but from several parts.
One of the purposes behind Object Oriented Programming is to make large programs easier to understand to to encourage re-use of code. We can help this another way by breaking the program into several files. Each file would hold one piece of the project. Most programming languages allow pieces of code to be separatly compiled and then the compiled pieces are linked together into a complete program.
In C++, we often do this by putting classes into seperate files. In fact, most classes are broken into two files. One contains the class declaration and the other contains most of the code. All the programs we have written used the first part. The declaration of the class will go into a header file that usually ends in .h. These are put into a seperate file because the compiler needs to see the declaration before it can compile the code. But it doesn't have to see the actual code at that time.
When we want to use the class in another program, we have to include the declaration in the new program. This is done by using the #include compiler directive. This copies the contents of the .h file into the program.
The main part of the code for the class usually goes into a different file ending in .c, .cc or .cpp. We saw hoe to seperate the code in the notes on out side code. This file can be compiled into an intermediate form called and object file, usually ending in .o.
We can seperatly compile the .c files into .o files using make. Then we can combine the .o files into an executable. This has been happening each time we compiled a .c file, but since there was only one file, the .o file was deleted.
As an example of this, we use the code that demonstrates the tracing class discussed here. I took the trace class and broke it into two files. The trace.h file contains the class declaration like this.
// trace.h : trace class header class Trace { public: Trace(char *); ~Trace(); private: char * fn; }; // TraceNote that there are function (constructor and destructor) declared here but no code. Kind of like function prototypes.
There code for these functions is stored in a trace.cc file like this:
// trace.cc : implements the trace class #includeThis line#include #include "trace.h" using namespace std; Trace::Trace(char * mesg) { cout << "now entering: " << mesg << endl; fn = strdup(mesg); } // trace constructor Trace :: ~Trace() { cout << "now leaving: " << fn << endl; free(fn); } // trace destructor
#include "trace.h"tells the commpiler to read the contents of the trace.h file before going on with this file. The use of "" rather than <> around the file name is to indicate that this is a local file. The <> tell the compiler to look for this file in a set of standard places, like /usr/include. Any program that wants to use this class will have to include the .h file into its source files.
The advantage to this extra work is that people who want to use my class only have to have the .h file as source code. I can send t hem the .o file that results from compiling the .cc file and they can link it into their code. If I change the way the code works, they don't have to recompile their code, just re-do the linking. Also, I only have to compile the .cc file once. If it doesn't change, I don't need to re-compile it. Other parts of the program might change and need to be re-compiled but this one won't. On large projects, not have to re-compiler parts of the system can save a lot of time.
Another advantage of seperating code into multiple files is that multiple people can work on the project at the same time. Only one person can work on a file at a time (for the most part) But we could each work on seperate files and then compile the whole project.
To show this at work, I took some code that shows how functions work and added the tracing mechanism to it. Each of the functions creates a local variable called t of type Trace and passes the name of the function to the constructor.
// functions.cpp : some function examples #include#include "trace.h" using namespace std; int bigger(int,int); // just the function prototype part, code is after main // returns the smaller of the two arguments int smaller(int one, int two) { int small; Trace t("smaller()"); if(one < two) small = one; else small = two; cout << "\tsmaller returning " << small << endl; return small; } // smaller int main(int argc, char* argv[]) { int big = bigger(1,2); int small,little; Trace t("main()"); cout << " big is " << big << endl; little = smaller(bigger(4,5),6) -1; cout << "\nlittle is " << little << endl; small = smaller(7,smaller(9,6)); cout << "\nsmall is " << small << endl; return 0; } // main // returns the bigger of the two arguments int bigger(int one, int two) { int big; Trace t("bigger()"); cout << "\tbigger(" << one << "," << two << ") called" << endl; if(one > two) { big=one; } else { big=two; } cout << "\tbigger returning " << big << endl; return big; } // bigger
We now have two .cc files and one .h file that we have to build into a program. We will use make to do this. First we have to build the trace.o file from the trace.cc file. We will make building the .o file dependent on the .h and .cc files. If either of those changes, we have to compile the .cc file. Since we are not producing an executable program from trace.cc, we use the -c option to the compiler. This stops compilation after it has produced the .o file.
The functions executable is dependent on the functions.cc file and the trace.o object. If either changes, we have to compile the .cc file. We could compile the functions.cc file into a functions.o and then link it with the trace.o file, but we will compress that. By adding the trace.o file to the end of the usual compile line, we tell the compiler to link the .o file into the program. Here is the complete make file:
functions: functions.cc trace.o g++ -o functions functions.cc trace.o trace.o : trace.cc trace.h g++ -c trace.cc clean : rm -f functions *.o rm -f functions.exe *.o