|
CS 371
|
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 leftOverriding 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.
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 arrayPtrNotice 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.
#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_HThe 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.
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 programSome 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.ccThe 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.oThen you can compile the programs as follows:
cd Point3Df gmake Point3Df.o cd .. gmake Calc Calc # To run the programSome comments: You might say "why bother"? Typing
gmake Point3Df.oisn't much easier than typing
g++ -c Point3Df.ccis it? Notice,however, that typing
gmake Calcis easier than typing
g++ -c Calc.cc # This creates Calc.o g++ -o Calc Calc.o Point3Df/Point3Df.o # This links object filesThe 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 ... commandswhere 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).