Types with External Resources

A class constructor is invoked when an object comes into scope. The constructor prepares the object by creating an environment in which the member functions operate. For many classes, including the Date example, there are no issues when an object of that type goes out of scope or a copy of the object is required. The simple compiler-generated decomissioning of objects and making copys of objects suffices. We refer to these processes as the compiler-generated default versions of destructor, copy constructor, and assignment operator.

However, there are classes for which specific "decomissioning" of objects is necessary when an object goes out of scope and where a careful class-specific procedure must be followed when copying objects. When the environment created by a constructor acquires some resource (external to the object), that resource must be released when the object goes out of scope. Typical resources in this category are:

  1. Dynamically allocated memory
  2. An open file
  3. A lock on some external object

Of these, the first is the most common and the one we will concentrate on here.

Destructor

Consider the following:

class IntArray
{
  public:
    IntArray ( size_t size = 10 , int ivalue = 0 );
    // more later

  private:
    size_t size_;
    int *  data_;
};

We will fill in other member functions and even operators later in this chapter, but now concentrate on the constructor. The intended use of IntArray is to build arrays of type int as objects which can be declared and used as if they were native types. The constructor should therefore create an array (lower case, meaning "C-style array") of the appropriate size and initialize all of the elements. Its implementation would go something like the following:

IntArray::IntArray (size_t size, int ivalue) : size_(size)
{
  data_ = new int [size_];
  if (data_ == 0)  // check for failed memory allocation
  {
    std::cerr << "** Memory allocation for IntArray failed - aborting program\n";
    exit (1);
  }
  for (size = 0; size  < size_; ++size)
    data_[size] = ivalue;
}

Objects would be created by client programs as if they were native types, for example:

...
IntArray A (1000, -1);  // IntArray with 1000 elements, each initialized to -1
IntArray B (100);       // IntArray with 100 elements, each initialized to 0
IntArray C;             // IntArray dynamically of default size and initialized to 0
...

In each case, the constructor for IntArray has created an object with dynamically allocated memory associated with it. When the object goes out of scope, this memory allocation must be returned to the system. The client program has no way to do this explicitly, and furthermore should not have that responsibility. The memory de-allocation should be handled by the class destructor, prototyped in color below:

class IntArray
{
  public:
    IntArray  ( size_t size = 10 , int ivalue = 0 );
    ~IntArray ();
    // more later

  private:
    size_t size_;
    int *  data_;
}

The method whose name is the class name preceded by ~ is the destructor. The class destructor is called automatically for each object as it goes out of scope. In the case of IntArray, a simple call to operator delete[] will suffice:

IntArray::~IntArray ()
{
  delete [] data_;
}

This will ensure that memory allocated for the object is properly de-allocated.

Copy Constructor

There are two situations when an object is copied implictly

  1. When an object is passed to a function as a value parameter
  2. When an object is returned by a function as a value

In each of these cases, a copy is made implicitly by the compiler: in the first case this copy is the local variable of the function defined by the parameter, and in the second case the copy is the value returned to the calling process by the function. In either case, the object is being created as a copy of an existing object. The compiler calls the copy constructor to create these objects. For example, given this function prototype and call:

IntArray Fun (IntArray a);  // prototype
...
IntArray a,b;
b = Fun(a);    // copy of argument "a" made when function is called 
               // copy of return value made as function returns

Note that a copy of a must be made for Fun to have as a local variable and then a copy of the return value of Fun must be made at the point of return.

In addition a copy must be made explictly when one object is used to initialize another, as in:

IntArray a;
...
IntArray b(a); // explicit call to copy constructor - make "b" a copy of "a"

The compiler will generate code to copy the memory footprint of objects, unless a copy constructor is given in the class definition, in which case the copy constructor will be called. (If the copy constructor prototype is private, a compile error is generated by code like the above.)

A copy constructor for our IntArray class is highlighted in the following expanded class definition:

class IntArray
{
public:
  IntArray  ( size_t size = 10 , int ivalue = 0 );
  ~IntArray ();
  IntArray  (const IntArray& a);
  // more later

private:
  size_t size_;
  int *  data_;
} ;

An implementation for this copy constructor is as follows:

IntArray::IntArray  (const IntArray& a) : size_(a.size_)
{
  data_ = new int [size_];
  // check for failed allocation
  for (size_t i = 0; i < size_; ++i)
    data_[i] = a.data_[i];
}

If there is no copy constructor defined for a class, the compiler will simply make a raw memory copy of the object. Clearly, when there are dynamically allocated resources owned by the object, that approach will not be satisfactory and a copy constructor must be supplied explicitly.

Self-Reference

There are times when implementation of a class member function need to refer to the object itself, the one for which the method is being implemented. The keyword this contains the address of the object and can be used to access the location of the object as well as the object itself through pointer dereference.

// context/scope: implementation of a member function
this  // pointer to the current object
*this // the actual object

Self-reference is particularly necessary to define an assignment operator.

Assignment Operator

Assignment is an operator. Recall that it (like all operators) has operator function notation with the same meaning. For example, if a and b are objects of the same type in a client program, assignment of b to a can be done with either syntax:

a = b;           // operator notation
a.operator=(b);  // operator function notation 

The latter seems a bit clumsy, but it does have the advantage of clarifying exactly what role various things play. For example, it is clear that operator= is a member operator of a, the object on the left, and it takes as argument the object b on the right. Operator function notation is also used when prototyping and implementing operators.

Recall also that assignment associates right to left, so that statements like

a = b = c;

are meaningful and do the appropriate thing, namely

a = (b = c);

Assignment must return a reference to the calling object in order for the successive call to have the correct input argument. Taking these observations into account allows us to write the prototype for assignment in our IntArray class:

class IntArray
{
public:
  IntArray  ( size_t size = 10 , int ivalue = 0 );
  ~IntArray ();
  IntArray  (const IntArray& a);
  IntArray& operator =(const IntArray& b);
  ...
private:
  size_t size_;
  int *  data_;
} ;

The right-hand argument (named b in the prototype) is the object to be copied, so we can guarantee that it is unchanged during the call to operator=.

The basic task for assignment is similar to that solved by the copy constructor, and we can even re-use some of the code for implemention. Here is a first attempt:

IntArray& IntArray::operator =(const IntArray& b)
{
  size_ = b.size_;
  data_ = new int [size_];
  // check for failed allocation
  for (size_t i = 0; i < size_; ++i)
    data_[i] = b.data_[i];
  return *this;
}

where the only difference is the addition of the line that returns the object (by reference, which is determined by the return type).

There is one important distinction between the circumstances in which a copy constructor is called and when an assignment operator is called, however. The copy constructor is called implicitly, and always for a new object that is under construction, such as am argument for a function call or a function return value. Assignment is typically explicitly called in client programs, and usually the object on the left is not newly created but is being re-defined. In the case of IntArray objects, this means that the array data_ will already have been defined. This memory is orphaned by the second line of code in the above implementation. The fix for this problem is to first delete the old memory, as in the following revision:

IntArray& IntArray::operator =(const IntArray& b)
{
  delete [] data_;
  size_ = b.size_;
  data_ = new int [size_];
  // check for failed allocation
  for (size_t i = 0; i < size_; ++i)
    data_[i] = b.data_[i];
  return *this;
}

Now we have ensured that memory associated with the left hand side is correctly released before it is lost.

There is one remaining problem, however: what if the client has made a an alias for b and then makes the call a = b ? The code above would be a disaster, deleting the very memory that is to be copied, thus losing the client's precious information. The fix for this problem is to check for self-assignment and do nothing in that case (after all, when a and b are the same object, there is nothing to be accomplished by a = b).

IntArray& IntArray::operator =(const IntArray& b)
{
  if (this != &b)
    {
      delete [] data_;
      size_ = b.size_;
      data_ = new int [size_];
      // check for failed allocation
      for (size_t i = 0; i < size_; ++i)
        data_[i] = b.data_[i];
    }
  return *this;
}

The pattern illustrated above may be followed by all overloads of the assignment operator.

Proper Types

A type is called proper under any of these circumstances:

  1. It is a native type.
  2. It is a class with constructor, destructor, copy constructor, and assignment operator that manage the type as if it were a native type.
  3. It is a user defined type (class) whose variables are proper types.

Note that the effect of being proper type is that client programs can use the type exactly as if it were a native type, without any concerns about resource management for the objects.

...
{
  IntArray A;         // IntArray with default number of elements
  IntArray B(100);    // IntArray with 100 elements
  IntArray C(100,-1); // IntArray with 100 elements each initialized to -1
...
  B = A;
...
  A = B = C;
...
}
// destructor for each object A, B, C is called as they go out of scope

Objects of type IntArray are declared, at which time memory is allocated. The may be assigned or re-assigned without concern over memory management; and when they go out of scope the destructor is called. From the client's perspective, IntArray objects can be handled as easily as type int objects.

Constant Member Functions

The keyword const may be applied to member functions (or member operators). The meaning is that the object will not be changed when that method is invoked. This is a guarantee that is enforced by the compiler.

class IntArray
{
public:
  IntArray  ( size_t size = 10 , int ivalue = 0 );
  ~IntArray ();
  IntArray  (const IntArray& a);
  IntArray& operator =(const IntArray& b);
  size_t Size() const;

private:
  size_t size_;
  int *  data_;
} ;

We have promised that the method Size() will not change the calling object. We follow through by implementing the method without any change of state of the calling object:

size_t IntArray::Size() const
{
  return size_;
}

Note that const methods are distinct from non-const methods by the same name. In particular, if the keyword const is applied to either the prototype or the implementation of a method, it must be applied to both.

Operator Overloading

Member operators and non-member (stand-alone) operators may be overloaded for a particular class. The following adds the bracket operator (a class member) and the output operator (not a class member) to the interface of IntArray:

class IntArray
{
public:
  IntArray  ( size_t size = 10 , int ivalue = 0 );
  ~IntArray ();
  IntArray  (const IntArray& a);
  IntArray& operator =(const IntArray& b);
  size_t Size() const;
        int& operator [] (size_t i);       // member operator
  const int& operator [] (size_t i) const; // const version

private:
  size_t size_;
  int *  data_;
} ;

std::ostream& operator << (std::ostream& os, const IntArray& A); // stand-alone operator

These are implemented as follows:

int& IntArray::operator [] (size_t i)
{
  return *(data_ + i);
}

const int& IntArray::operator [] (size_t i) const
{
  return *(data_ + i);
}

std::ostream& operator << (std::ostream& os, const IntArray& A)
{
  for (size_t i = 0; i < A.Size() - 1; ++i)
    os << A[i] << ' ';
  os << A[A.Size() - 1] << '\n';
  return os;
}

The bracket operator is required to be a member of the class - a requirement that makes sense since the implementation of this operator, like the copy constructor, must depend on the most intimate details of the class design.

On the other hand, the following analysis shows why the I/O operators cannot be class members.

Begin with the fact that a member operator always has the object making the call as the "implicit" first argument, as illustrated in the discussion of the assignment operator earlier in this chapter. The I/O operators, on the other hand, always have a stream object as the first parameter. Therefore it can't work out to make the I/O operators class member operatots, these must be stand-alone.

In-Class Function Definitions

Class member functions may be implemented within the class definition, as in this example:

class IntArray
{
public:
  IntArray  ( size_t size = 10 , int ivalue = 0 );
  ~IntArray ();
  IntArray  (const IntArray& a);
  IntArray& operator =(const IntArray& b);
  size_t Size() const
  {
    return size_;
  }
  int& operator [] (size_t i)
  {
    return *(data_ + i);
  }
  const int& operator [] (size_t i) const
  {
    return *(data_ + i);
  }
private:
  size_t size_;
  int *  data_;
} ;

Any advantage of in-class implementation comes in the form of easing of human suffering - it may make the class definition easier to read (dubious) and it may make the class easier to implement (of questionable value, unless all methods are implemented in-class, for otherwise the separation of implementations into separate places creates a code readability/maintenance problem).

In-line implementation does not force the compiler to write more efficient code, although it is usually taken as a suggestion to in-line the code.

Dynamic Objects

Because objects created with operator new have global scope, and often the pointers used to locate these objects have local scope, the question is, when does the destructor get called? Here is some sample client code:

// run time bindings
IntArray * xptr, * yptr, * zptr;          // pointers to type IntArray
...
{
  IntArray * Aptr = new IntArray();       // ptr to default IntArray object
  IntArray * Bptr = new IntArray(100);    // ptr to 100-element IntArray object
  IntArray * Cptr = new IntArray(100,-1); // ptr to 100-element IntArray object
  // Note that IntArray constructors are called as operator new is invoked
  ...
  xptr = Aptr;            // simple assignment of integer type (addresses)
  yptr = Bptr;            // simple assignment of integer type
  zptr = Cptr;            // simple assignment of integer type
  ...
}
// Note that Aptr, Bptr, Cptr go out of scope here, but
// objects they point to are global scope and STILL EXIST
...
delete xptr;              // IntArray object goes out of scope, destructor called
delete yptr;              // IntArray object goes out of scope, destructor called
delete zptr;              // IntArray object goes out of scope, destructor called

The destructor is called implicitly with operator delete

Accessing Members: Dot and Arrow Notation

As mentioned earlier, objects access public members (member variables and member functions) using the dot notation, as in this example:

IntArray A, B; // create objects A and B of type IntArray
...
A.Size();      // object A accesses the member function Size()
B.Size();      // object B accesses the member function Size()
...

For pointers to objects, the pointer must first be de-referenced (using operator *) and then a member function can be accessed (using operator .). Because the resulting accumulation of operator evaluations can become cumbersome and unsightly, an alternative notation allows access of a member variable or function directly from the pointer, using the arrow operator ->, as in the following example:

IntArray A;          // create IntArray object
IntArray *Aptr;      // create pointer to type IntArray
Aptr = new IntArray; // create IntArray object at address Aptr
A.Size();            // object A accesses member function Size()
(*Aptr).Size();      // object *Aptr accesses member function Size()
Aptr -> Size();      // access member function through pointer

The last two lines of code have identical meaning. Note that the parentheses surrounding *Aptr are necessary, because the dot operator has higher precedence than the star operator.

Static Member Variables

The keyword static, inherited from C, retains its C meaning but also adds some semantics in the context of classes. The principle is that static associates with the class itself instead of with specific objects of the class. It may seem surprising that data may be associated with a class instead of one of its objects, but a simple (albeit somewhat contrived) example shows how this can be useful:

class IntArray
{
public:
  IntArray  ( size_t size = 10 , int ivalue = 0 );
  ~IntArray ();
  IntArray  (const IntArray& a);
  IntArray& operator =(const IntArray& b);
  size_t Size() const;
  int& operator [] (size_t i);
  const int& operator [] (size_t i) const;

public:
  static unsigned int objectCount; // the number of objects currently in existence

private:
  size_t size_;
  int *  data_;
} ;

The data item objectCount will keep a running tally of the number of objects of the class in existence at any given moment in a running program. For this to work, we have to make sure that objectCount is initialized to zero before any objects are cteated and then add increment/decrement code to the constructor/destructor:

IntArray::objectCount = 0;

IntArray::IntArray (size_t size, ivalue) : size_(size)
{
  data_ = new(std::nothrow) int [size_];
  if (data_ == 0)  // check for failed memory allocation
  {
    std::cerr << "** Memory allocation for IntArray failed - aborting program\n";
    exit (1);
  }
  for (size_t i = 0; i  < size_; ++i)
    data_[i] = ivalue;
  ++objectCount;
}

IntArray::~IntArray ()
{
  delete [] data_;
  --objectCount;
}

The number of objects can be useful, in applicatons (for example, keeping a running count of the number of records created in a database), in making efficient implementatons (for example, keepiong only one actual copy of large objects and letting all other instances point to this one), and in program correctness (where the object count can be tested, for example it should be zero at certain benchmarl locations in a text client).

Static Member Functions

A static member function is one that can be called from the class level (with no objects created) or from any object. Because it is not associated with any object, a static method may not access non-static data in an object, but it may access static class data. A typical use would be to access private static data:

class IntArray
{
public:
  IntArray  ( size_t size = 10 , int ivalue = 0 );
  ~IntArray ();
  IntArray  (const IntArray& a);
  IntArray& operator =(const IntArray& b);
  size_t Size() const;
  int& operator [] (size_t i);
  const int& operator [] (size_t i) const;
  static unsigned int ObjectCount () const;

private:
  size_t size_;
  int *  data_;
  static unsigned int objectCount;

} ;

unsigned int IntArray::ObjectCount () const
{
  return objectCount;
}

A static method that does not access any (static) class data is effectively a stand-alone function whose scope is restricted to the class scope. Here is an example, where the allocation of memory and error checking is captured in one private static member function that can be called by various other method implementations such as constructors and assignment:

class IntArray
{
public:
  IntArray  ( size_t size = 10 , int ivalue = 0 );
  ~IntArray ();
  IntArray  (const IntArray& a);
  IntArray& operator =(const IntArray& b);

  size_t Size() const;
  int& operator [] (size_t i);
  const int& operator [] (size_t i) const;

private:
  static int* NewArray (size_t size);
  size_t size_;
  int *  data_;
} ;

int* IntArray::NewArray (size_t size)
{
  int * ptr = new(std::nothrow) int [size];
  if (ptr == 0)
  {
    std::cerr << "** Allocation failure in class IntArray - aborting execution\n";
    exit (1);
  }
  return ptr;
}

The method NewArray is a "helper" function that encapsulates a repetitive housekeeping task into one place where it can be maintained. This makes all of the other method implementations that use dynamic memory easier to read, and it means that the policy for handling a failed aloocation can be changed in one place while ensuring consistent adherence to the policy.

Mutable Member Variables

A member variable may be declared mutable, which means that even when the object is constant or a const member function is called, the mutable member variable is allowed to change. The use of mutable variables should be restricted to cases where the "logical constness" of the object is not affected by that variable. Here is a contrived example using the class Date from the previous chapter:

class IntArray

Taking all these ideas into account, keeping some and discarding others, we come up with a fairly useful and not bloated storage class for tucking away when we need arrays again. We also organize the code into the usual header/implementation file pair, and include a short test client program.

First the header file:

/*   intarray.h

     definition of the IntArray class and interface
*/

#ifndef _INTARRAY_H // morph of the file name into a legal indentifier
#define _INTARRAY_H

#include <cstdlib>
#include <iostream>

class IntArray
{
public:  // note formatting for self-documentation
              IntArray     ( size_t size = 10 , int ivalue = 0 );
              ~IntArray    ();
              IntArray     (const IntArray& a);
  IntArray&   operator =   (const IntArray& b);
  size_t      Size         () const;
  int&        operator []  (size_t i);
  const int&  operator []  (size_t i) const;

private:
  // class variables
  size_t size_;
  int *  data_;
  // member function
  static int* NewArray (size_t size);
} ;

std::ostream& operator << (std::ostream& os, const IntArray& A);

#endif

Second the implementaton file:

/*   IntArray.cpp

     implenentation of IntArray methods and operators
*/

#include <intarray.h>

IntArray::IntArray ( size_t size , int ivalue )  :  size_(size), data_(0)
{
  data_ = NewArray(size_);
  for (size_t i = 0; i < size_; ++i)
    data_[i] = ivalue;
}

IntArray::~IntArray ()
{
  delete [] data_;
}

IntArray::IntArray  (const IntArray& a): size_(a.size_), data_(0)
{
  data_ = NewArray(size_);
  for (size_t i = 0; i < size_; ++i)
    data_[i] = a.data_[i];
}

IntArray& IntArray::operator =(const IntArray& b)
{
  if (this != &b)
    {
      delete [] data_;
      size_ = b.size_;
      data_ = NewArray(size_);
      for (size_t i = 0; i < size_; ++i)
	data_[i] = b.data_[i];
    }
  return *this;
}

size_t IntArray::Size() const
{
  return size_;
}

int& IntArray::operator [] (size_t i)
{
  return *(data_ + i);
}

const int& IntArray::operator [] (size_t i) const
{
  return *(data_ + i);
}

std::ostream& operator << (std::ostream& os, const IntArray& A)
{
  for (size_t i = 0; i < A.Size() - 1; ++i)
    os << A[i] << ' ';
  os << A[A.Size() - 1] << '\n';
  return os;
}

int* IntArray::NewArray (size_t size)
{
  int * ptr = new(std::nothrow) int [size];
  if (ptr == 0)
    {
      std::cerr << "** Allocation failure in class IntArray - aborting execution\n";
      exit (1);
    }
  return ptr;
}

Third a simple test client:

/* test client for IntArray

*/

#include <intarray.h>
#include <iostream>

int main()
{
  IntArray a;
  IntArray b(20);
  IntArray c(30, -1);
  int      num;
  size_t   count;

  std::cout << "Data in IntArray a: " << a
            << "Data in IntArray b: " << b
            << "Data in IntArray c: " << c;

  std::cout << "Enter integer data: ";
  for (count = 0; std::cin >> num && count < a.Size(); ++count)
    {
      a[count] = num;
    }

  std::cout << "Data in IntArray a: " << a;
  c = b = a;
  std::cout << "Data in IntArray b: " << b;
  std::cout << "Data in IntArray c: " << c;
}

Exercise: Create these three files, compile and test.