Previous | Table of Contents | Next |
Suppose we have a variable named x and we want to use the standard C++ library to print its value with an appropriate label. Then we can say something like
cout << x = << x << endl;
and be confident that the three values that we are printing will appear in the order we requested: First the string x =, then the value of x, and finally a newline.
Suppose next that p is a pointer to an element of an array and we want to print the value of that element and the next element of that array. Similarly to before, we can say
cout << *p << , << *(p+1) << endl;
or, equivalently,
cout << p[0] << , << p[1] << endl;
It is therefore tempting to assume that we can also say
cout << *p++ << , << *p << endl;
but, perhaps surprisingly, that doesnt work.
One way to understand this surprise is as a consequence of two rules:
These two rules seem straightforward enough: The first one should be well known to all C and C++ programmers and the second applies to any call-by-value language. Lets see how they might apply to the previous expressions.
Note first that when we write an expression such as
cout << x = << x << endl;
the << operator is left associative. In other words, that expression is equivalent to
((cout << x = ) << x) << endl;
We can view this expression as applying the << operator to two operands, the second of which is endl and the first of which is complicated. That application, in turn, has one of two possible meanings, depending on whether the particular operator<< being used is a member of the class of cout or not. Specifically, the expression above is equivalent either to
operator<<(((cout << x = ) << x), endl);
or to
((cout << x = ) << x).operator<<(endl);
Either way, our two rules apply. The first rule says that the implementation is allowed to evaluate our complicated expression and endl in either order. The second rule says that the complicated expression must be completely evaluated before executing operator<<. That implies that the string x = and the value of x must be written before writing endl, which is what we want.
A similar analysis of the complicated expression shows that x = must be written before the value of x.
Now lets look at the expression that doesnt work:
cout << *p++ << , << *p << endl;
As before, the uses of << group to the left, so that this statement means
((((cout << *p++) << , ) << *p) << endl);
Consider the subexpression
(((cout << *p++) << , ) << *p)
Similarly to before, we can view this as a call to operator<< whose right operand is *p and whose left operand is complicated, so that it means either
operator<<(((cout << *p++) << , ), *p);
or
((cout << *p++) << , ).operator<<(*p);
depending on whether or not the operator<< being used is a member of the class of cout. As before, both operands must be evaluated before calling operator<<. However, in this case, one operand uses the value of p and the other operand (uses and) modifies it. Because the operands are permitted to be evaluated in either order, it is up to the implementation whether *p uses the value of p before or after it has been modified. In other words, even though the << operation in the subexpression
cout << *p++
must be evaluated before the << operation that prints *p, that says nothing about the relative order in which *p++ and *p are evaluated. The implementation is permitted to evaluate them in either order and have their results until it is time to print them.
The lesson from the foregoing discussion may be familiar: Expressions with side effects can be dangerous. Although they can be useful, they always require thought. In particular, even though parts of an expression might be guaranteed to be evaluated in particular order, that guarantee is not universal.
Its virtually impossible to learn C++ without learning about the hazards of dangling pointers. For example, in
int* p = new int; delete p; *p = 42; // <*>?!%#@
the assignment to *p is an error because p no longer points anywhere meaningful. Indeed, the implementation is permitted to change the value of p itself to make it easier to detect the error.
8.4.3.1. Pointers to Dynamic Memory
Pointers often point to dynamically allocated memory, and it is hard to say how to avoid dangling pointers in such circumstances except by a very general rule: Dont delete an object while theres still a pointer to it somewhere. Most of the time, figuring out whether a use of delete meets this rule is either very easy or very difficult; in either case, the application of this rule is beyond the scope of this chapter.
However, one thing is well worth noting: Many uses of dynamic memory are intended as containers, which are flexible data structures that grow as needed to contain the objects placed in them. The way to make such things safe is to wrap them in appropriate container classes, think hard enough about those classes to ensure theyre safe, and then use them.
Thus, for instance, instead of saying
int* p = new int[n]; // ... delete[] p;
think about whether what you really want might be a flexible array. If it is, use an appropriate class from the standard library:
vector<int> p(n);
Previous | Table of Contents | Next |