CS 371
Computer Graphics
Spring 2005


4 More on C++

4.1 A note about Constructors, Operators and the Like

Given the potential complexity that arises in correctly overloading operators, remembering to define default and copy constructors, the potential for introducing error, and so on, you might understandably be put off by C++.

The goal, however, of this complexity is, perhaps paradoxically, simplicity. Careful programming in C++ allows the developer to create data types whose behavior appears identical to the behavior of the built-in types. When built-in types are passed as parameters or return values of functions, for example, there is clearly copying or creating of values that occurs. For user types to work the same way, methods must be provided to facilitate this behind-the-scenes activity. Parameter passing behaves like object initialization: the formal parameter has memory allocated for it, but is not initialized until the actual argument is passed to it. Hence the need for a copy constructor. Function value return behaves like this also: "the return statement initializes some unnamed variable of the return type" (quoting Stroustrup's The C++ Programming Language). A temporary object to receive the return value is created and the function return value is used to initialize it. However, this is not the only way in which objects are created behind your back. If you allocate an array of objects of a user defined type, a default constructor (i.e., a no-parameter constructor) is invoked to define each array element. Hence you need a default constructor. To conveniently fill in the values, you would then presumably assign values to each array element. Hence the need for an assignment operator.

Why return an object reference (e.g., Point3Df&) from a function? Well, if you want Point3Df objects to behave like built-in types, you would want, for example, the ability to write.

Point3Df a,b,c(1,2,3);  // c is initialized to (1,2,3);
a=b=c; // evaluate from right to left
Overriding the = operator to return void won't work since then b=c does not return a value to assign to a. Returning a Point3Df object would accomplish this, but at the expense of creating additional temporary objects (the return value of b=c is copied into a temporary variable). Returning a reference avoids the creating of additional temporaries.

However, there are pitfalls to using references as return values. For example returning a reference to a local object is so problematic that the g++ compiler generates a warning for such code. Some of the problems with references in general can be avoided with the appropriate use of const. For example, declaring reference parameters of functions to be const will help to guarantee that these parameters are not changed by the function.

4.2 Const

The keyword const is ubiquitous in C++ programs, and most of the usage is straight-forward. Declaring a variable const prohibits its value from being changed (at least inadvertently). Function parameters can also be declared const, which prohibits the function from changing those values. Placing const at the end of the declaration of a member function prohibits the function from changing the object that invoked the function.

One potentially confusing instance of pointer use is with pointers to arrays. Consider the situation of a function which is passed a pointer to some type. The purpose of the function is to create an array of that type and return via the pointer the location of the first element of the array. Such a declaration might look like:

void CreateArray(Point3Df*& arrayPtr, int size);
// create an array of Point3Df objects of length 'size' and return location
// of array in arrayPtr
Notice the declaration of arrayPtr. It must be a reference to a pointer since the pointer is created by CreateArray and must be returned via arrayPtr.

Suppose now that we want a method to print the array. How should const be used? Note that we do not need to make arrayPtr a reference here.

4.3 Not all Functions are Methods

In Java, every function belongs to some class, so is a method. C++ allows functions which belong to no class. Their scope is that of the files in which they are included. See FileInputEx for an example, as well as the overloaded operators << and >> defined for the Point3Df class.

4.4 Header files and #include

Most C++ programs begin with several lines of the form
#include <iostream>
//...
#include <math.h>
//...
#include "Point3Df/Point3Df.h"

The #include compiler directive causes the contents of the named file to be pasted into the source code file being processed. The files included are typically header files. They containd declarations of useful functions and classes which have been defined (and often compiled) elsewhere. System header files appear bracketed by the symbols < and >, while user header files appear in double quotes (this helps the compiler to determine which directories need to be searched for which header files).

A common error is to forget to include a particular header file. This results in compiler error messages stating that certain names being used have not been declared (namely those names that were declared in the header. Another common error is to have a name defined more than once because the header file defining it was included in several source files. When the source files are compiled, or are linked together (as they are by g++ -o ...), an error is generated that some quantity is multiply defined. To avoid this, it is standard practice to "wrap" the contents of header files with the following compiler directives

#ifndef FILENAME_H

// the actual declarations of the header file...

#endif FILENAME_H
The first time the header file is included, the macro FILENAME_H is undefined, so the contents between the #ifndef and #endif lines are processed normally. This includes the #define FILENAME_H directive, which then defines the macro FILENAME_H. Subsequent inclusions of the same header file will not process the contents of the file since FILENAME_H now is defined.

4.5 Compiling C++ Programs on Unix Systems

Suppose now that you have written a program called Calc.cc, which uses a class Point3Df. Suppose further that your directory structure is as follows: You have a directory called cs371progs which contains Calc.cc as well as containing a directory called Point3Df with contains Point3Df.h and Point3Df.cc. How do you compile your program? By the way, under bash, the symbol tt # is a comment symbol. You can add it to the end of a command line and it will be ignored. Method 1:
cd Point3Df
g++ -c Point3Df.cc # This creates Point3Df.o (object code)
cd ..
g++ -c Calc.cc # This creates Calc.o
g++ -o Calc Calc.o Point3Df/Point3Df.o # This links object files
Calc # or ./Calc runs the program
Some notes: This assumes that Calc.cc contains the line
#include "Point3Df/Point3Df.h"
If instead, Calc.cc contains the line
#include "Point3Df.h"
you would need to type g++ -c Calc.c -IPoint3Df in place of the line above. This instructs the compiler to look for user header files in the directory Point3Df. By the way, you can have as many -I options as you need on the g++ command line in order to describe locations of all header files. Method 2: Create a file called Makefile in your cs371Progs directory, as well as one in your Point3Df directory. The Point3Df Makefile should contain the following:
# makefile for Point3Df

Point3Df.o : Point3Df.cc
	g++ -c Point3Df.cc
The Makefile in cs371Progs should contain this
# makefile for programs

Calc.o : Calc.cc
	g++ -c Calc.cc  #possibly with -I options (see above)

Calc : Calc.o
	g++ -o Calc Calc.o Point3Df/Point3Df.o
Then you can compile the programs as follows:
cd Point3Df
gmake Point3Df.o
cd ..
gmake Calc
Calc # To run the program
Some comments: You might say "why bother"? Typing
gmake Point3Df.o
isn't much easier than typing
g++ -c Point3Df.cc
is it? Notice,however, that typing
gmake Calc
is easier than typing
g++ -c Calc.cc # This creates Calc.o
g++ -o Calc Calc.o Point3Df/Point3Df.o # This links object files
The gmake command is used for updating files based on their dates. It consists (for the most part) of a collection of rules of the form
targetfile : dependfile1 dependfile2 ...
	commands
where the commands line must begin with a TAB character (don't ask). If the timestamp on any of the dependfiles is more recent than that of the target file, then the commands are executed. So, for example, if you change Calc.cc, then it has a newer date than Calc.o, so typing gmake Calc.o would force the compile command to be executed. Even better, depends are checked recursively, so that if Calc.cc is changed and you type gmake Calc, then gmake checks to see if Calc.o is newer than Calc or if anything upon which Calc.o depends (i.e., Calc.cc) is newer than Calc. If so, the commands are executed.

As your C++ programs become larger and require keeping track of more files, you will learn to appreciate the power of gmake (gmake is the GNU version of make), and we will learn more about its capabilities. Those interested in learning more about gmake on their own can type info into a terminal window (or use info from inside emacs).


lenhart@cs.williams.edu