Point midpoint(Point P, Point Q) { double xx = ( P.x + Q.x ) / 2; double yy = ( P.y + Q.y ) / 2; return Point(xx,yy);
}
Efficiency
By coding the midpoint and dist procedures as friends of the Point class, we can bypass the calls to getX and getY. Consequently, these procedures should be faster than the standard (nonfriend) versions.
Another way to improve performance is to code these procedures to use call by reference (instead of call by value). In that case, the midpoint procedure would begin like this:
Point midpoint(const Point& P, const Point& Q)
To send a Point object by value requires the transmission of two double values; this comprises more bytes than call by reference where only a reference to the parameters needs to be sent to the procedure.
Let us experiment with the effects of these differences. Let us write a program that generates a long list of points and then finds the midpoints for all pairs of points on the list. In this experiment, we use four different versions of midpoint:
•A standard (nonfriend) version with call by value,
•A standard version with call by reference,
•A friend version with call by value, and
•A friend version with call by reference.
The results2 of this experiment are summarized in the following chart:
Time in seconds (no optimization)
Standard version
Friend version
Call by value
214
166
Call by reference
191
144
From these results, we observe that the friend version outperforms the corresponding standard version; this is thanks to the friend’s ability to access data elements directly. We also observe that the call-by-reference versions outperform the corresponding call-by-value versions; this is thanks to the reduced number of bytes that need to be sent to the midpoint procedure.
2This experiment was performed on an 800 MHz G4 Mac OS X system using version 3.3 of the Gnu compiler. The list of points contained 20,000 entries and so 400 million calls to midpoint were generated.
Odds and Ends
347
However, these results were obtained when none of the optimization options for the compiler was engaged. Modern compilers can deduce when a call to getX may be streamlined away and the private value can be safely sent directly into the procedure. Hence, when the experiment is repeated with all optimization options engaged, we get the following results.
Time in seconds (full optimization)
Standard version
Friend version
Call by value
66
72
Call by reference
45
44
From this second experiment we observe that there is no advantage (in this instance) to setting up the procedures as friends, but we still achieve better performance using call by reference.
15.5Other ways to create types
15.5.1 Structures
C++ is an extension to the C language. The centerpiece of this extension is the concept of a class. An ancestor to the class concept is known as a structure. Like classes, structures are assemblies of data, and can be used as data types. However, structures consist only of data (no associated methods) and one cannot extend structures using inheritance.
Structures are declared in a manner that is similar to how we declare classes. This is how we declare a structure type that represents an ordered pair of real numbers:
struct RealPair { double x; double y;
};
With this in place, we may define variables of type RealPair, like this:
RealPair p;
Now, to access the data held in p, we use the familiar dot notation: P.x and P.y. You never need to use structures in your programming. The exact same behavior
can be achieved using classes. Here is how RealPair would be declared.
class RealPair { public:
double x; double y;
};
348
C++ for Mathematicians
15.5.2 Enumerations
An enumeration is a variation on the integer data types. Suppose our program deals with different sorts of infinity. In the context of real numbers, we might have separate +∞ and −∞, but in the context of complex numbers we have a single “complex infinity.”
In C++ we can create a type to represent these three options. The syntax looks like this:
With this in place, infinity becomes a type and variables may be declared to be of type infinity:
infinity X;
Now X may be assigned one of the three values declared in the enumeration; for example, X = plusInfinity;.
Behind the scenes, the enumeration values (listed between the curly braces) are given integer values. Therefore, enumeration types may be used in switch statements:
switch(X) {
case minusInfinity: // action
break;
case plusInfinity: // action break;
case complexInfinity: // action
break;
}
It is not necessary to use enumeration types. The same effect can be achieved with constant integer values:
const int minusInfinity = 0; const int plusInfinity = 1; const int complexInfinity = 2;
Using enumerations does have some advantages. If you decide to add an addition kind of infinity, then you just add that new value to the enumeration list. If a variable is declared to be an enumeration type, then the compiler will complain if you attempt to assign a value to that variable that isn’t one of the allowed enumeration values. This can help prevent errors.
15.5.3 Unions
A union is a data structure that allows different types of data to be held in the same location in the computer’s memory. It is a trick that is designed to save memory. You should not use these things. We mention how they work just for your amusement.
Odds and Ends
349
A union is a type (like classes, structures, and enumerations). To declare a union that may hold an integer or a double value, you write this:
union number { double x; long i;
};
Now we can declare a variable to be of type number:
number Z;
The variable Z can hold only one data value, but that value may be either a double or a long. For example, to assign a real value to Z we write, for example, Z.x=0.12;. Or we can assign an integer value like this: Z.i=-11;.
What happens if we assign a value to a union using one of its types and then access its value using another? This is just begging for trouble. The value extracted is bound to be garbage. Consider this program.
The keyword typedef is used to define synonyms for existing types. For example, rather than using double and long to declare variables, we mathematicians might prefer to use the single letters R and Z instead. To do this we use the following statements,
typedef double R; typedef long Z;
The newly defined names may save you a lot of typing. For example, instead of writing complex<double> to declare complex variables, we can use a typedef:
typedef complex<double> C;
350
C++ for Mathematicians
Choosing different names for types can improve the readability of your code and save you a bit of typing. In addition, using your own type names may prove invaluable when revising your code. Suppose, for example, you initially used typedef to make R a new name for double. Subsequently, you find that you don’t really need the accuracy of double variables and that your arrays are too large for your computer. So you decide to use float variables instead. All you need to do is to edit the single line typedef double R; to read typedef float R; and recompile your code.
15.6Pointers
Our approach to C++ has scrupulously avoided pointers. We have been able to do this for two reasons. First, call by reference obviates much of the need to use pointers. Second, pointers are often used to create intricate data structures. Fortunately, the Standard Template Library provides a sufficiently rich assortment of ready-made structures, that it is not necessary for us to make new ones. In short, we simply don’t need pointers for our work.3
However, pointers are used by many C++ programmers, and a package that you might download from the Web may require you to use pointers. We therefore close our exploration of C++ with an overview of pointers.
15.6.1 Pointer basics
An ordinary variable holds a value that represents an integer, a real number, a character, or an object of some class. A pointer is a variable whose value is a location in the computer’s memory. Given an ordinary variable, one can learn the address of that variable using the address-of operator, &. For example, if x is an int variable, then &x is the location in memory where the value is actually held. This is illustrated by the following program.
3The primary exception to this rule is the this pointer by which an object refers to itself.
Odds and Ends
351
The output of this program looks like this.4
x = 12
&x = 0xbffff980
By convention, memory addresses are expressed in base 16; the 0x prefix signals that the value is in hexadecimal.
Pointer values can be saved in variables. In C++, every variable must have a type and we declare pointer variables using an asterisk *. If the pointer variable points to a location in memory holding a value of type base_type, then the pointer variable is declared to be of type base_type*. For example, if z is type double, then &z is type double*. This is illustrated in the following program.
Suppose the pointer variable zp is type double* (i.e., a pointer to a double value). We can use zp to inspect and to modify the value held in the memory location zp. The expression *zp stands for the value stored in the location to which zp points. So, if we have the statement cout << *zp;, the value held at location zp is printed. Similarly, the statement *zp = 3.2; changes the value held at location zp to 3.2, but does not change the pointer zp itself. These ideas are illustrated in the following example.
Program 15.5: Illustrating pointer dereferencing.
1 #include <iostream>
2using namespace std;
3
4int main() {
5double z = 1.2;
4If you run this program on your computer, the output might be different because the variable x might be stored at a different memory location.
352
C++ for Mathematicians
6
double* zp;
7zp = &z;
8
9cout << "zp = " << zp << endl;
10
cout << "*zp = " <<
*zp << endl << endl;
11
12
*zp = M_PI;
13
14
cout << "zp
= " <<
zp << endl;
15
cout << "z
= " <<
z << endl;
16
17return 0;
18}
On lines 9 and 10 we print the pointer itself and the value to which it refers. Then, on line 12, we use the pointer to modify the value pointed to by zp. Because zp points to the location that holds z, the value of z changes. Here is the output of the
program.
zp
= 0xbffff980
*zp
= 1.2
zp
= 0xbffff980
z
= 3.14159
The unary5 operations & (address of) and * (pointer dereference) are inverses of each other. The first converts a variable into a pointer to that variable, and the second converts a pointer to a memory location to the value held at that location.
Program 15.5 contains two hints about the perils of pointers. First, the variable z is modified by an expression that does not use the letter z (line 12). This makes the code more difficult to understand, more likely to contain bugs, and harder to debug. Second, if we neglected to initialize zp (line 7), then we don’t know where the pointer zp points. Consider this code.
#include <iostream> using namespace std;
int main() { double z = -1.1; double* zp;
*zp = 9.7;
cout << "z
= " <<
z << endl;
cout << "&z
= " <<
&z << endl;
cout << "zp
= " <<
zp << endl;
cout << "*zp
= " <<
*zp << endl;
return 0;
}
5The operations & and * have different meanings when used as binary operations: bitwise-and and multiplication.
Odds and Ends
353
When run on one computer we have the following result.
z= -1.1
&z
= 0xbffff980
zp
= 0xbffffa60
*zp
= 9.7
Note that zp points to some unknown location (it does not point to z) and so the statement *zp = 9.7; changed some unpredictable location in the computer’s memory.
When this program is run on another system, the following message is produced: Segmentation fault. This message was spawned by the statement *zp = 9.7; because, on this second computer, zp pointed to a memory location that was “off limits” to the program. (It is also possible to see the message Bus error; this also arises from using bad pointers.) This latter behavior is actually much better than the former because we see right away that something is wrong. In the first instance, no error messages were generated; this type of bug is insidious and difficult to find.
15.6.3 Arrays and pointer arithmetic
As we mentioned in Section 5.2, there is a connection between pointers and arrays. Recall that arrays can be declared in two ways. If, when we are writing our program, we know the size of the array, we declare it like this:
int A[10];
However, if the size of the array cannot be determined until the program is run, we use new and delete[]:
//determine the value of n int* A;
A = new int[n];
//use the array
delete[] A;
In this second case, the declaration of the variable A is indistinguishable from the declaration of a pointer-to-intvariable. Indeed, in both cases, the variable A is a pointer. By convention, the name of an array is a pointer to the first element (index 0) of the array. This is illustrated by the following example.
The expressions A[0] and *A have exactly the same meaning. Both stand for the first element of the array and may be used interchangeably. Thus, if we want to change the first element of A to, say, 28, we may use either of the statements *A = 28; or A[0] = 28;. The second, however, is easier to understand and therefore preferable.
Conversely, it is possible to use the subscript notation [] for pointers that were not created to be arrays.
#include <iostream> using namespace std;
int main() { int z = 23; int* A = &z;
cout << A[0] << endl; cout << A[1] << endl;
return 0;
}
23-1073743488
We see that A[0] yields the value 23. This follows from the fact that A[0] is identical to *A. However, for this example, the notation *A is preferable because A does not refer to an array.
Which leads to the question: In this context, what is A[1]? To answer, we need to understand pointer arithmetic.
Consider this program.
#include <iostream> using namespace std;
int main() {
int z = 23; int* A = &z;
cout << A << endl; cout << A+1 << endl;
return 0;
}
Odds and Ends
355
The first line of output shows that A equals 0xbffff980. It would therefore make
sense that the second output statement would produce 0xbffff981; that, however,
is incorrect. Here is the output.
0xbffff980
0xbffff984
We see that A+1 is 4 bigger than A. The reason adding one increases an int* pointer by 4 is that (at least on my computer) the size of an int is 4 bytes. That is, if A points to an int variable in the computer’s memory, then A+1 points to the next (possible) int.
In general, one may add an integer to (or subtract an integer from) any pointer. If p is a pointer to an object of type T, then adding n to p causes the pointer to change by n×sizeof(T) bytes. Likewise, p++ increases p by sizeof(T) bytes.
It does not make sense to add, to multiply, or to divide a pair of pointers. However, it does make sense to subtract a pair of pointers of the same type. The result is an integer value defined as the difference in bytes divided by the size of the kind of object to which the pointers point. The following code
int a = 34; int b = 49; int* A = &a; int* B = &b;
cout << B-A << endl;
prints 1 on the screen.
We now return to our discussion of arrays. An array A of objects of type T places the objects contiguously in memory. The location of A[1] is sizeof(T) bytes after the location of A[0]. Therefore, because A points to the first element of the array (i.e., to A[0]), A+1 points to the next element of the array (i.e., to A[1]). In general A+k points to A[k]. Ergo,
A[k] is exactly the same as *(A+k).
15.6.4 new and delete revisited
In Section 5.5 we introduced the new and delete[] operators. These were used to allocate and to release space for arrays. Here we discuss a slightly different use of new and delete.
Let T be a class. The usual way to declare a variable of type T is with a statement like this:
T x;
We find such declarations inside the body of a procedure. Such variables are local to the procedure. Once the procedure terminates, the variable is lost. Variables declared in this usual manner are stored in a portion of the computer’s memory called the stack.