Previous Table of Contents Next


8.3.3.4. Deleting Arrays

When we’re 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

  Allocating an array yields a pointer to its initial element. Use delete[] to free the array when you’re done with it.
  Allocating a single object yields a pointer to the object. Use delete (without []) to free the object when you’re done with it.
  There are no multidimensional arrays as such. Use either an array of arrays or an array of pointers.

Where possible, of course, wrap all this kind of stuff in a class so that you—and whoever might have to maintain your code—don’t 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.

8.3.4. Implicit Base Class Conversions

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 doesn’t 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