Previous | Table of Contents | Next |
8.3.3.4. Deleting Arrays
When were done with memory, we should use delete to return it to the system. If we allocate a single object, we use plain delete to free it:
Thing* tp; // as before tp = new Thing; delete tp;
When we allocate an array, we must use delete[] when it comes time to free it:
tp = new Thing[100]; delete[] tp;
C++ imposes this rule for several reasons, the most important of which is that single objects are often small and allocated in great profusion. It is therefore potentially expensive for the runtime system to keep track of the size of each object; the overhead to do so might require as much memory as the objects themselves. Some programmers can afford that much overhead, but not all.
Arrays, especially when dynamically allocated, are usually large compared to the amount of memory needed to store their sizes. Therefore C++ adopts a compromise position: The programmer must remember whether what is allocated is an array, but not how big the array is.
Thus, for example, to free the memory allocated for pp, we might write
for (int i = 0; i < m; i++) delete[] pp[i]; delete[] pp;
Note that the requirement for [] depends on whether what is allocated is an array, not on whether it was allocated using [] syntax. That dependency is necessary to ensure that
tp = new Thing[5];
and
tp = new Thingarray;
impose the same requirements on tp. After all, Thingarray and Thing[5] are exactly the same type, so they should behave identically when allocated.
8.3.3.5. Arrays Summary
What trips people up the most about allocating arrays is failing to realize that new behaves differently depending on whether what is allocated is an array. The things to remember are
Where possible, of course, wrap all this kind of stuff in a class so that youand whoever might have to maintain your codedont have to worry about the details more than once. Such classes have the additional advantage of making it possible to deal with memory management once, in the class definition, and not worry about it again.
One of the fundamental properties of inheritance in C++ is that a pointer to a derived class object may be converted to a pointer to any base class of that object. Thus, for example, if we have declarations such as
class Vehicle { /* ... */ }; class LandVehicle: public Vehicle { /* ... */ }; class Automobile: public LandVehicle { /* ... */ };
then an Automobile* can be converted into a LandVehicle* or a Vehicle*. This conversion makes sense: Because every Automobile is a kind of LandVehicle, we can treat a pointer to an Automobile as if it were a pointer to the LandVehicle that it is a kind of.
Every use of virtual functions relies on this property of conversions. To see this, realize that virtual functions are interesting only when the particular function actually called at runtime is different from the one that, during compilation, might appear to be called. Thus, for example, if we have
class Vehicle { public: virtual int weight () const; // ... }; class LandVehicle: public Vehicle { public: virtual int weight() const; // ... }; class Automobile: public LandVehicle { public: virtual int weight() const; // ... };
then the fact that weight is virtual is relevant only when we use a pointer (or reference) to a base class that actually points (or refers) to an object of a derived class:
Automobile a; Vehicle* vp = &a; int n = vp->weight(); // automobile::weight
Here, even though vp is declared to point to a Vehicle, the fact that it actually points to an Automobile means that the call to vp->weight calls Automobile::weight and not Vehicle::weight as casual observation might suggest. Only in such circumstances do we care that weight is virtual; if vp->weight refers to Vehicle::weight, the weight member might as well not be virtual. It would make no difference either way.
All this is well known. Less well known is the implication this behavior has for copying between derived class and base class objects. To begin, note that unless the class author goes to some trouble to prevent it, it is possible to copy class objects:
Automobile a1; Automobile a2 = a1;
Here, a2 is created as a copy of a1, whatever that means. If the author of class Automobile doesnt say what it means, the compiler defines it recursively in terms of copying the components of an object of class Automobile. Only if the class author explicitly bars copying are such things prohibited.
This means that under ordinary circumstances, class Vehicle, say, has a copy constructor that behaves as if declared as
class Vehicle { // ... Vehicle(const Vehicle&); // ... };
Earlier we saw that a reference to a class derived from Vehicle may be converted into a reference to a Vehicle. That implies that
Vehicle v = a1;
is completely legal even though v is a Vehicle and a1 an Automobile; a1 has a reference to Vehicle bound to it, which is then used to form v. In effect, v is a copy of the Vehicle part of a1.
When expressed this way, the behavior seems straightforward. After all, what else could it mean to ask that v be a copy of a1? However, straightforward behavior can be surprising if you forget that it is happening. Consider, for example, a function to test if a Vehicle is heavy:
int heavy(Vehicle v) { return v.weight() > 12000; }
Previous | Table of Contents | Next |