This topic is not required reading for ECE 250. It is provided for interest's sake. If you find it useful, please let me know. If you have some feedback as to how this page can be improved, also let me know.
In this tutorial, we will introduce the following advanced topics:
A very good reference, which I wish I read before writing this page, is David A. Wheeler's HOWTO on this topic.
In class, for ease of implementation and cross-platform development, we are both declaring and defining our classes within a single header file. For example,
demonstrates how the declaration and definitions of the Complex class may appear in the same header file. That header file is included in the main1.cpp file and successfully used.
{ecelinux:1} ls Complex1.h main1.cpp {ecelinux:2} g++ main1.cpp {ecelinux:3} ls Complex1.h a.out* main1.cpp {ecelinux:4} ./a.out -13.1288 - 15.2008j {ecelinux:5}
In this case, the #include "Complex1.h" preprocessor statement at the top of the main1.cpp file combines all user code into a single file which is then compiled. The compiler, however, still links in the appropriate libraries associated with the iostream.h header file because it is part of the C++ Standard Library. To have the linker not link the C++ Standard Library, use the -nostdlib option to g++.
To view the manual page for g++, type man g++ or view the page here.
One problem with this approach is that you could modify the main1.cpp file, however, then you would have to recompile both the definitions in Complex1.h and main1.cpp, even though the code in Complex1.h did not change. While this does not appear to be a serious problem at this level, it does, however pose serious problems for larger projects where compilation of the entire project from scratch may take hours.
If we split the Complex class into two files, the header file, Complex2.h, storing the class declaration and the source code, Complex2.cpp, storing the class definitions.
In this case, we cannot compile Complex2.cpp into an executable (a process involving both compilation and linking) because the file does not contain a int main (); function. Thus, we to restrict compilation simply to generating the object file:
{ecelinux:1} ls Complex2.cpp Complex2.h main2.cpp {ecelinux:2} g++ -c Complex2.cpp {ecelinux:3} ls Complex2.cpp Complex2.h Complex2.o main2.cpp
To read more about object files, see http://en.wikipedia.org/wiki/Object_file.
Now that we have generated the object file Complex2.o, we can continue to compile the main2.cpp command, linking in the Complex2.o file:
{ecelinux:4} g++ main2.cpp Complex2.o {ecelinux:5} ls Complex2.cpp Complex2.h Complex2.o a.out* main2.cpp {ecelinux:6} ./a.out -13.1288 - 15.2008j {ecelinux:7}
This does not re-compile the Complex2.cpp file, however, you will observe that main2.cpp still includes the header file Complex2.h. This is because the compile uses this header file to ensure that the use of the class is correct and that it generates acceptable object code.
The process of compiling and linking main2.cpp could itself be broken into two steps:
{ecelinux:7} rm a.out {ecelinux:8} g++ -c main2.cpp {ecelinux:9} ls Complex2.cpp Complex2.h Complex2.o main2.cpp main2.o {ecelinux:10} g++ main2.o Complex2.o {ecelinux:11} ls Complex2.cpp Complex2.h Complex2.o a.out* main2.cpp main2.o {ecelinux:12} ./a.out -13.1288 - 15.2008j {ecelinux:13}
Here, we generate object code from main2.cpp (generating main2.o) and then we link the two object files.
Note that, by default, the iostream and cmath standard libraries are automatically linked by g++.
References: Andrew Oram and Steve Talbott, Managing Projects with make 2nd ed., O'Reilly and Associates, Inc., 1993 and http://en.wikipedia.org/wiki/Makefile.
As you may suppose, it can get quite tedious generating object files each time you change a file. Suppose that you have the following pairs of files:
a.h, a.cpp
b.h, b.cpp
c.h, c.cpp
Suppose also that both b.h and c.h include a.h and that c.h includes b.h. These are called dependencies: if you change and recompile b.cpp, you must also recompile c.cpp, while if you change and recompile a.cpp, you must also recompile b.cpp and c.cpp, that is:
{ecelinux:1} g++ -c a.cpp {ecelinux:2} g++ -c b.cpp a.o {ecelinux:3} g++ -c c.cpp a.o b.o {ecelinux:3} g++ main.cpp a.o b.o c.o
The make utility automates this process (to a point) by allowing the author to specify:
Once the dependencies are specified, the make untility will use the time stamps of the various files. For example, in the above example, b.o depends on a.o and b.cpp, and therefore, if either a.o or b.cpp has a more recent time stamp than it will recompile b.o.
The method for specifying these files is in a file called Makefile. To demonstrate, the following makefile would be appropriate for the above situation:
CPP = g++ OPTS = # any options, e.g., -O for optimize DEBUG = # empty now, but assign -g for debugging executable: main.cpp a.o b.o c.o c.h ${CPP} ${OPTS} ${DEBUG} -o executable main.cpp a.o b.o c.o c.o: c.cpp c.h ${CPP} ${OPTS} ${DEBUG} -c c.cpp b.o: b.cpp b.h ${CPP} ${OPTS} ${DEBUG} -c b.cpp a.o: a.cpp a.h ${CPP} ${OPTS} ${DEBUG} -c a.cpp clean: rm -f executable *.o core
You can access this Makefile (as well as other files) in here.
The components of this file include:
If you simply type make at the command line, that is
{ecelinux:1} make
make will read Makefile and attempt to create the first target, in this case, executable. In this case, it will check the time stamp of the file executable (if it exists at all) and compare this to the time stamps of the dependencies main.cpp a.o b.o c.o c.h. For any dependency which is also a target, in this case, the three object files a.o b.o c.o, then it will ensure that these are up to date.
For example, the target a.o depends on a.cpp a.h. If either of these is more recent than a.o, make will execute g++ -c a.cpp. It will do the same for b.o and c.o.
Finally, having checked these three dependencies which are also targets, make will compare the time stamps of all the dependencies and if one of them is more recent than excecutable, make will execute g++ -o executable main.cpp a.o b.o c.o.
It is also possible to make other targets by specifying them at the command line, for example,
{ecelinux:2} make a.o
will ensure that a.o is up to date.
One common artificial target to include is clean. If you type
{ecelinux:3} make clean
make will search for the file clean, note that it is not present and consequently execute rm -f executable *.o core which will remove all object files, any core files, and the file executable. Because this does not actually create a file called clean, the command make clean can be run arbitrarily often.
First, on ecelinux, the header files for the C++ Standard Library are located in the directory /usr/include/g++-3/. These header files (or .h files) store the declarations for the various classes and objects associated with the Standard Library.
You can read more about include files (header files) at http://en.wikipedia.org/wiki/Include_file.
Now, the next question is "Where are the object files for the everything declared inside the corresponding header files?". You will not find a .o file in /usr/; instead you will find one of two possibilities:
We will look at thes two below, however, you can also read more about libraries at http://en.wikipedia.org/wiki/Library_(computer_science). This page includes information about both types of libraries listed below.
We will begin with a discussion on static archives. An archive is merely a collection of object files. For example, the archive for the math.h library archive is /usr/lib/libm.a. To inspect the object files stored in this archive, you can use the archive command:
{ecelinux:1} ar -t /usr/lib/libm.a | more __libx_errno.o acos.o acosh.o asin.o asinh.o atan2.o atan.o _TBL_atan.o atanh.o cbrt.o ceil.o copysign.o cosh.o erf.o fabs.o floor.o gamma.o --More-- {ecelinux:2} man ar Reformatting page. Please Wait... done User Commands ar(1) NAME ar - maintain portable archive or library SYNOPSIS /usr/ccs/bin/ar -d [ -Vv ] archive file ... . . .
Because the math library archive (cmath) are part of the C++ Standard Library, it is not necessary to explicitly specify that the math library archive should be linked. In C, however, it was necessary: if you used #include <math.h>, you were required to append a -lm to your compilation, e.g.,
{ecelinux:1} gcc main.c -lm {ecelinux:2}
Similarly, in C++ you must specify, using the -l option any library archives which should be linked in. We will go through the process of generating a static archive containing the Complex2.o object file.
When you specify -llibname, the compiler looks for a library archive with the name liblibname.a. Thus, we will name our library libComplex2.a and (quickly) include the one object file Complex2.o:
{ecelinux:1} ls Complex2.cpp Complex2.h Complex2.o main2.cpp {ecelinux:2} ar -cq libComplex2.a Complex2.o {ecelinux:3} ls Complex2.cpp Complex2.h Complex2.o libComplex2.a main2.cpp {ecelinux:3}
Thus, to compile main2.cpp including this library, we must specify the library using -lComplex2 and we must specify any additional directories in which the compiler should search for library archives (by default, the compiler only searches the installed libraries).
{ecelinux:4} g++ main2.cpp -lComplex2 -L. {ecelinux:5} ls Complex2.cpp Complex2.h Complex2.o a.out* libComplex2.a main2.cpp {ecelinux:6} ./a.out -13.1288 - 15.2008j {ecelinux:7} ls -al a.out -rwxr-xr-x 1 ece250 ece250 15560 Sep 16 04:58 a.out* {ecelinux:8}
A dynamically-linked, or shared, library is a library linked at run-time. This results in smaller executable files and greater efficiency as one dynamically-linked (shared) library is shared by many programs.
The first step is to generate a shared library (.so file) from the object files. This may be done through using the ld command:
{ecelinux:8} man ld Reformatting page. Please Wait... done User Commands ld(1) NAME ld - link-editor for object files SYNOPSIS /usr/ccs/bin/ld [ -64 ] [ -a | -r ] [ -b ] [ -c name ] [ -C ] [ -G ] [ -i ] [ -m ] [ -s ] [ -t ] [ -V ] [ -B . . . {ecelinux:9} ld -shared Complex2.o -o Complex2.so {ecelinux:10} ls Complex2.cpp Complex2.h Complex2.o Complex2.so a.out* libComplex2.a main2.cpp {ecelinux:11}
We can now compile this in as if it were a standard object file:
{ecelinux:11} rm a.out {ecelinux:12} g++ main2.cpp Complex2.so {ecelinux:13} ls Complex2.cpp Complex2.h Complex2.o Complex2.so a.out* libComplex2.a main2.cpp {ecelinux:14} ls -al a.out -rwxr-xr-x 1 ece250 ece250 11940 Sep 16 05:05 a.out* {ecelinux:15}
Note that the executable is significantly smaller.
Previously, we indicated to the compiler as to where to look to find the library archive. In this case, however, it is up tot he operating system to find and allow the executable a.out to find the shared object. Consequently, we modify the LD_LIBRARY_PATH environment variable by adding the current working directory:
{ecelinux:15} setenv LD_LIBRARY_PATH ${LD_LIBRARY_PATH}:. {ecelinux:16} echo $LD_LIBRARY_PATH /usr/local/lib:/usr/openwin/lib:/usr/openwin/lib/sparcv9:. {ecelinux:17} ./a.out -13.1288 - 15.2008j
Thus, we get the same result with a 23% smaller executable.