www.gibmonks.com

Main Page

Previous Page
Next Page

[Page 582 (continued)]

11.8. Case Study: Array Class

Pointer-based arrays have a number of problems. For example, a program can easily "walk off" either end of an array, because C++ does not check whether subscripts fall outside the range of an array (the programmer can still do this explicitly though). Arrays of size n must number their elements 0, ..., n 1; alternate subscript ranges are not allowed. An entire non-char array cannot be input or output at once; each array element must be read or written individually. Two arrays cannot be meaningfully compared with equality operators or relational operators (because the array names are simply pointers to where the arrays begin in memory and, of course, two arrays will always be at different memory locations). When an array is passed to a general-purpose function designed to handle arrays of any size, the size of the array must be passed as an additional argument. One array cannot be assigned to another with the assignment operator(s) (because array names are const pointers and a constant pointer cannot be used on the left side of an assignment operator). These and other capabilities certainly seem like "naturals" for dealing with arrays, but pointer-based arrays do not provide such capabilities. However, C++ does provide the means to implement such array capabilities through the use of classes and operator overloading.

In this example, we create a powerful array class that performs range checking to ensure that subscripts remain within the bounds of the Array. The class allows one array object to be assigned to another with the assignment operator. Objects of the Array class know their size, so the size does not need to be passed separately as an argument when passing an Array to a function. Entire Arrays can be input or output with the stream extraction and stream insertion operators, respectively. Array comparisons can be made with the equality operators == and !=.

This example will sharpen your appreciation of data abstraction. You will probably want to suggest other enhancements to this Array class. Class development is an interesting, creative and intellectually challenging activityalways with the goal of "crafting valuable classes."

The program of Figs. 11.611.8 demonstrates class Array and its overloaded operators. First we walk through main (Fig. 11.8). Then we consider the class definition (Fig. 11.6) and each of the class's member-function and friend-function definitions (Fig. 11.7).

Figure 11.6. Array class definition with overloaded operators.
(This item is displayed on pages 582 - 583 in the print version)

 1  // Fig. 11.6: Array.h
 2  // Array class for storing arrays of integers.
 3  #ifndef ARRAY_H
 4  #define ARRAY_H
 5
 6  #include <iostream>
 7  using std::ostream;
 8  using std::istream;
 9
10  class Array
11  {
12     friend ostream &operator<<( ostream &, const Array & );
13     friend istream &operator>>( istream &, Array & );      
14  public:
15     Array( int = 10 ); // default constructor
16     Array( const Array & ); // copy constructor
17     ~Array(); // destructor                    
18     int getSize() const; // return size
19
20     const Array &operator=( const Array & ); // assignment operator
21     bool operator==( const Array & ) const; // equality operator   
22
23     // inequality operator; returns opposite of == operator     
24     bool operator!=( const Array &right ) const                 
25     {                                                           
26        return ! ( *this == right ); // invokes Array::operator==
27     } // end function operator!=                                
28
29     // subscript operator for non-const objects returns modifiable lvalue
30     int &operator[]( int );                                              
31
32     // subscript operator for const objects returns rvalue
33     int operator[]( int ) const;                          
34  private:
35     int size; // pointer-based array size
36     int *ptr; // pointer to first element of pointer-based array
37  }; // end class Array
38
39  #endif


[Page 583]

Figure 11.7. Array class member- and friend-function definitions.
(This item is displayed on pages 583 - 586 in the print version)

 1   // Fig 11.7: Array.cpp
 2   // Member-function definitions for class Array
 3   #include <iostream>
 4   using std::cerr;
 5   using std::cout;
 6   using std::cin;
 7   using std::endl;
 8
 9   #include <iomanip>
10   using std::setw;
11
12   #include <cstdlib> // exit function prototype
13   using std::exit;
14
15   #include "Array.h" // Array class definition
16
17   // default constructor for class Array (default size 10)
18   Array::Array( int arraySize )
19   {
20      size = ( arraySize > 0 ? arraySize : 10 ); // validate arraySize
21      ptr = new int[ size ]; // create space for pointer-based array
22
23      for ( int i = 0; i < size; i++ )
24         ptr[ i ] = 0; // set pointer-based array element
25   } // end Array default constructor
26
27   // copy constructor for class Array;
28   // must receive a reference to prevent infinite recursion
29   Array::Array( const Array &arrayToCopy )
30      : size( arrayToCopy.size )
31   {
32      ptr = new int[ size ]; // create space for pointer-based array
33
34      for ( int i = 0; i < size; i++ )
35         ptr[ i ] = arrayToCopy.ptr[ i ]; // copy into object
36   } // end Array copy constructor
37
38   // destructor for class Array
39   Array::~Array()
40   {
41      delete [] ptr; // release pointer-based array space
42   } // end destructor
43
44   // return number of elements of Array
45   int Array::getSize() const
46   {
47      return size; // number of elements in Array
48   } // end function getSize
49
50   // overloaded assignment operator;
51   // const return avoids: ( a1 = a2 ) = a3
52   const Array &Array::operator=( const Array &right )
53   {
54      if ( &right != this ) // avoid self-assignment
55      {
56         // for Arrays of different sizes, deallocate original
57         // left-side array, then allocate new left-side array
58         if ( size != right.size )
59         {
60            delete [] ptr; // release space
61            size = right.size; // resize this object
62            ptr = new int[ size ]; // create space for array copy
63         } // end inner if
64
65         for ( int i = 0; i < size; i++ )
66            ptr[ i ] = right.ptr[ i ]; // copy array into object
67      } // end outer if
68
69      return *this; // enables x = y = z, for example
70   } // end function operator=
71
72   // determine if two Arrays are equal and
73   // return true, otherwise return false
74   bool Array::operator==( const Array &right ) const
75   {
76      if ( size != right.size )
77         return false; // arrays of different number of elements
78
79      for ( int i = 0; i < size; i++ )
80         if ( ptr[ i ] != right.ptr[ i ] )
81            return false; // Array contents are not equal
82
83      return true; // Arrays are equal
84   } // end function operator==
85
86   // overloaded subscript operator for non-const Arrays;
87   // reference return creates a modifiable lvalue
88   int &Array::operator[]( int subscript )
89   {
90      // check for subscript out-of-range error
91      if ( subscript < 0 || subscript >= size )
92      {
93         cerr << "\nError: Subscript " << subscript
94            << " out of range" << endl;
95         exit( 1 ); // terminate program; subscript out of range
96      } // end if
97
98      return ptr[ subscript ]; // reference return
99   } // end function operator[]
100
101  // overloaded subscript operator for const Arrays
102  // const reference return creates an rvalue
103  int Array::operator[]( int subscript ) const
104  {
105     // check for subscript out-of-range error
106     if ( subscript < 0 || subscript >= size )
107     {
108        cerr << "\nError: Subscript " << subscript
109           << " out of range" << endl;
110        exit( 1 ); // terminate program; subscript out of range
111     } // end if
112
113     return ptr[ subscript ]; // returns copy of this element
114  } // end function operator[]
115
116  // overloaded input operator for class Array;
117  // inputs values for entire Array
118  istream &operator>>( istream &input, Array &a )
119  {
120     for ( int i = 0; i < a.size; i++ )
121        input >> a.ptr[ i ];
122
123     return input; // enables cin >> x >> y;
124  } // end function
125
126  // overloaded output operator for class Array
127  ostream &operator<<( ostream &output, const Array &a )
128  {
129     int i;
130
131     // output private ptr-based array
132     for ( i = 0; i < a.size; i++ )
133     {
134        output << setw( 12 ) << a.ptr[ i ];
135
136        if ( ( i + 1 ) % 4 == 0 ) // 4 numbers per row of output
137           output << endl;
138     } // end for
139
140     if ( i % 4 != 0 ) // end last line of output
141        output << endl;
142
143     return output; // enables cout << x << y;
144  } // end function operator<<


[Page 586]

Figure 11.8. Array class test program.
(This item is displayed on pages 586 - 588 in the print version)

 1  // Fig. 11.8: fig11_08.cpp
 2  // Array class test program.
 3  #include <iostream>
 4  using std::cout;
 5  using std::cin;
 6  using std::endl;
 7
 8  #include "Array.h"
 9
10  int main()
11  {
12     Array integers1( 7 ); // seven-element Array   
13     Array integers2; // 10-element Array by default
14
15     // print integers1 size and contents
16     cout << "Size of Array integers1 is "
17        << integers1.getSize()
18        << "\nArray after initialization:\n" << integers1;
19
20     // print integers2 size and contents
21     cout << "\nSize of Array integers2 is "
22        << integers2.getSize()
23        << "\nArray after initialization:\n" << integers2;
24
25     // input and print integers1 and integers2
26     cout << "\nEnter 17 integers:" << endl;
27     cin >> integers1 >> integers2;
28
29     cout << "\nAfter input, the Arrays contain:\n"
30        << "integers1:\n" << integers1
31        << "integers2:\n" << integers2;
32
33     // use overloaded inequality (!=) operator
34     cout << "\nEvaluating: integers1 != integers2" << endl;
35
36     if ( integers1 != integers2 )
37        cout << "integers1 and integers2 are not equal" << endl;
38
39     // create Array integers3 using integers1 as an          
40     // initializer; print size and contents                  
41     Array integers3( integers1 ); // invokes copy constructor
42
43     cout << "\nSize of Array integers3 is "
44        << integers3.getSize()
45        << "\nArray after initialization:\n" << integers3;
46
47     // use overloaded assignment (=) operator
48     cout << "\nAssigning integers2 to integers1:" << endl;
49     integers1 = integers2; // note target Array is smaller
50
51     cout << "integers1:\n" << integers1
52        << "integers2:\n" << integers2;
53
54     // use overloaded equality (==) operator
55     cout << "\nEvaluating: integers1 == integers2" << endl;
56
57     if ( integers1 == integers2 )
58        cout << "integers1 and integers2 are equal" << endl;
59
60     // use overloaded subscript operator to create rvalue
61     cout << "\nintegers1[5] is " << integers1[ 5 ];
62
63     // use overloaded subscript operator to create lvalue
64     cout << "\n\nAssigning 1000 to integers1[5]" << endl;
65     integers1[ 5 ] = 1000;
66     cout << "integers1:\n" << integers1;
67
68     // attempt to use out-of-range subscript
69     cout << "\nAttempt to assign 1000 to integers1[15]" << endl;
70     integers1[ 15 ] = 1000; // ERROR: out of range
71     return 0;
72  } // end main

 Size of Array integers1 is 7
 Array after initialization:
            0           0           0           0
            0           0           0
 Size of Array integers2 is 10
 Array after initialization:
            0           0           0           0
            0           0           0           0
            0           0

 Enter 17 integers:
 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

 After input, the Arrays contain:
 integers1:
            1           2           3           4
            5           6           7
 integers2:
            8           9          10          11
           12          13          14          15
           16          17

 Evaluating: integers1 != integers2
 integers1 and integers2 are not equal

 Size of Array integers3 is 7
 Array after initialization:
            1           2           3           4
            5           6           7

 Assigning integers2 to integers1:
 integers1:
            8           9          10          11
           12          13          14          15
           16          17
 integers2:
            8           9          10          11
           12          13          14          15
           16          17

 Evaluating: integers1 == integers2
 integers1 and integers2 are equal

 integers1[5] is 13

 Assigning 1000 to integers1[5]
 integers1:
            8           9          10          11
           12        1000          14          15
           16          17

 Attempt to assign 1000 to integers1[15]

 Error: Subscript 15 out of range



[Page 589]

Creating Arrays, Outputting Their Size and Displaying Their Contents

The program begins by instantiating two objects of class Arrayintegers1 (Fig. 11.8, line 12) with seven elements, and integers2 (Fig. 11.8, line 13) with the default Array size10 elements (specified by the Array default constructor's prototype in Fig. 11.6, line 15). Lines 1618 use member function getSize to determine the size of integers1 and output integers1, using the Array overloaded stream insertion operator. The sample output confirms that the Array elements were set correctly to zeros by the constructor. Next, lines 2123 output the size of Array integers2 and output integers2, using the Array overloaded stream insertion operator.

Using the Overloaded Stream Insertion Operator to Fill an Array

Line 26 prompts the user to input 17 integers. Line 27 uses the Array overloaded stream extraction operator to read these values into both arrays. The first seven values are stored in integers1 and the remaining 10 values are stored in integers2. Lines 2931 output the two arrays with the overloaded Array stream insertion operator to confirm that the input was performed correctly.

Using the Overloaded Inequality Operator

Line 36 tests the overloaded inequality operator by evaluating the condition

      integers1 != integers2

The program output shows that the Arrays indeed are not equal.

Initializing a New Array with a Copy of an Existing Array's Contents

Line 41 instantiates a third Array called integers3 and initializes it with a copy of Array integers1. This invokes the Array copy constructor to copy the elements of integers1 into integers3. We discuss the details of the copy constructor shortly. Note that the copy constructor can also be invoked by writing line 41 as follows:

      Array integers3 = integers1;

The equal sign in the preceding statement is not the assignment operator. When an equal sign appears in the declaration of an object, it invokes a constructor for that object. This form can be used to pass only a single argument to a constructor.

Lines 4345 output the size of integers3 and output integers3, using the Array overloaded stream insertion operator to confirm that the Array elements were set correctly by the copy constructor.

Using the Overloaded Assignment Operator

Next, line 49 tests the overloaded assignment operator (=) by assigning integers2 to integers1. Lines 5152 print both Array objects to confirm that the assignment was successful. Note that integers1 originally held 7 integers and was resized to hold a copy of the 10 elements in integers2. As we will see, the overloaded assignment operator performs this resizing operation in a manner that is transparent to the client code.

Using the Overloaded Equality Operator

Next, line 57 uses the overloaded equality operator (==) to confirm that objects integers1 and integers2 are indeed identical after the assignment.


[Page 590]

Using the Overloaded Subscript Operator

Line 61 uses the overloaded subscript operator to refer to integers1[ 5 ]an in-range element of integers1. This subscripted name is used as an rvalue to print the value stored in integers1[ 5 ]. Line 65 uses integers1[ 5 ] as a modifiable lvalue on the left side of an assignment statement to assign a new value, 1000, to element 5 of integers1. We will see that operator[] returns a reference to use as the modifiable lvalue after the operator confirms that 5 is a valid subscript for integers1.

Line 70 attempts to assign the value 1000 to integers1[ 15 ]an out-of-range element. In this example, operator[] determines that the subscript is out of range, prints a message and terminates the program. Note that we highlighted line 70 of the program in red to emphasize that it is an error to access an element that is out of range. This is a runtime logic error, not a compilation error.

Interestingly, the array subscript operator [] is not restricted for use only with arrays; it also can be used, for example, to select elements from other kinds of container classes, such as linked lists, strings and dictionaries. Also, when operator[] functions are defined, subscripts no longer have to be integerscharacters, strings, floats or even objects of user-defined classes also could be used. In Chapter 23, Standard Template Library (STL), we discuss the STL map class that allows noninteger subscripts.

Array Class Definition

Now that we have seen how this program operates, let us walk through the class header (Fig. 11.6). As we refer to each member function in the header, we discuss that function's implementation in Fig. 11.7. In Fig. 11.6, lines 3536 represent the private data members of class Array. Each Array object consists of a size member indicating the number of elements in the Array and an int pointerptrthat points to the dynamically allocated pointer-based array of integers managed by the Array object.

Overloading the Stream Insertion and Stream Extraction Operators as friends

Lines 1213 of Fig. 11.6 declare the overloaded stream insertion operator and the overloaded stream extraction operator to be friends of class Array. When the compiler sees an expression like cout << arrayObject, it invokes global function operator<< with the call

      operator<<( cout, arrayObject )

When the compiler sees an expression like cin >> arrayObject, it invokes global function operator>> with the call

      operator>>( cin, arrayObject )

We note again that these stream insertion and stream extraction operator functions cannot be members of class Array, because the Array object is always mentioned on the right side of the stream insertion operator and the stream extraction operator. If these operator functions were to be members of class Array, the following awkward statements would have to be used to output and input an Array:

      arrayObject << cout;
      arrayObject >> cin;

Such statements would be confusing to most C++ programmers, who are familiar with cout and cin appearing as the left operands of << and >>, respectively.


[Page 591]

Function operator<< (defined in Fig. 11.7, lines 127144) prints the number of elements indicated by size from the integer array to which ptr points. Function operator>> (defined in Fig. 11.7, lines 118124) inputs directly into the array to which ptr points. Each of these operator functions returns an appropriate reference to enable cascaded output or input statements, respectively. Note that each of these functions has access to an Array's private data because these functions are declared as friends of class Array. Also, note that class Array's getSize and operator[] functions could be used by operator<< and operator>>, in which case these operator functions would not need to be friends of class Array. However, the additional function calls might increase execution-time overhead.

Array Default Constructor

Line 15 of Fig. 11.6 declares the default constructor for the class and specifies a default size of 10 elements. When the compiler sees a declaration like line 13 in Fig. 11.8, it invokes class Array's default constructor (remember that the default constructor in this example actually receives a single int argument that has a default value of 10). The default constructor (defined in Fig. 11.7, lines 1825) validates and assigns the argument to data member size, uses new to obtain the memory for the internal pointer-based representation of this array and assigns the pointer returned by new to data member ptr. Then the constructor uses a for statement to set all the elements of the array to zero. It is possible to have an Array class that does not initialize its members if, for example, these members are to be read at some later time; but this is considered to be a poor programming practice. Arrays, and objects in general, should be properly initialized and maintained in a consistent state.

Array Copy Constructor

Line 16 of Fig. 11.6 declares a copy constructor (defined in Fig. 11.7, lines 2936) that initializes an Array by making a copy of an existing Array object. Such copying must be done carefully to avoid the pitfall of leaving both Array objects pointing to the same dynamically allocated memory. This is exactly the problem that would occur with default memberwise copying, if the compiler is allowed to define a default copy constructor for this class. Copy constructors are invoked whenever a copy of an object is needed, such as in passing an object by value to a function, returning an object by value from a function or initializing an object with a copy of another object of the same class. The copy constructor is called in a declaration when an object of class Array is instantiated and initialized with another object of class Array, as in the declaration in line 41 of Fig. 11.8.

Software Engineering Observation 11.4

The argument to a copy constructor should be a const reference to allow a const object to be copied.


Common Programming Error 11.6

Note that a copy constructor must receive its argument by reference, not by value. Otherwise, the copy constructor call results in infinite recursion (a fatal logic error) because receiving an object by value requires the copy constructor to make a copy of the argument object. Recall that any time a copy of an object is required, the class's copy constructor is called. If the copy constructor received its argument by value, the copy constructor would call itself recursively to make a copy of its argument!



[Page 592]

The copy constructor for Array uses a member initializer (Fig. 11.7, line 30) to copy the size of the initializer Array into data member size, uses new (line 32) to obtain the memory for the internal pointer-based representation of this Array and assigns the pointer returned by new to data member ptr.[1] Then the copy constructor uses a for statement to copy all the elements of the initializer Array into the new Array object. Note that an object of a class can look at the private data of any other object of that class (using a handle that indicates which object to access).

[1] Note that new could fail to obtain the needed memory. We deal with new failures in Chapter 16, Exception Handling.

Common Programming Error 11.7

If the copy constructor simply copied the pointer in the source object to the target object's pointer, then both objects would point to the same dynamically allocated memory. The first destructor to execute would then delete the dynamically allocated memory, and the other object's ptr would be undefined, a situation called a dangling pointerthis would likely result in a serious runtime error (such as early program termination) when the pointer was used.


Array Destructor

Line 17 of Fig. 11.6 declares the destructor for the class (defined in Fig. 11.7, lines 3942). The destructor is invoked when an object of class Array goes out of scope. The destructor uses delete [] to release the memory allocated dynamically by new in the constructor.

getSize Member Function

Line 18 of Fig. 11.6 declares function getSize (defined in Fig. 11.7, lines 4548) that returns the number of elements in the Array.

Overloaded Assignment Operator

Line 20 of Fig. 11.6 declares the overloaded assignment operator function for the class. When the compiler sees the expression integers1 = integers2 in line 49 of Fig. 11.8, the compiler invokes member function operator= with the call

      integers1.operator=( integers2 )

The implementation of member function operator= (Fig. 11.7, lines 5270) tests for self assignment (line 54) in which an object of class Array is being assigned to itself. When this is equal to the address of the right operand, a self-assignment is being attempted, so the assignment is skipped (i.e., the object already is itself; in a moment we will see why self-assignment is dangerous). If it is not a self-assignment, then the member function determines whether the sizes of the two arrays are identical (line 58); in that case, the original array of integers in the left-side Array object is not reallocated. Otherwise, operator= uses delete (line 60) to release the memory originally allocated to the target array, copies the size of the source array to the size of the target array (line 61), uses new to allocate memory for the target array and places the pointer returned by new into the array's ptr member.[2] Then the for statement at lines 6566 copies the array elements from the source array to the target array. Regardless of whether this is a self-assignment, the member function returns the current object (i.e., *this at line 69) as a constant reference; this enables cascaded Array assignments such as x = y = z. If self-assignment occurs, and function operator= did not test for this case, operator= would delete the dynamic memory associated with the Array object before the assignment was complete. This would leave ptr pointing to memory that had been deallocated, which could lead to fatal runtime errors.

[2] Once again, new could fail. We discuss new failures in Chapter 16.


[Page 593]

Software Engineering Observation 11.5

A copy constructor, a destructor and an overloaded assignment operator are usually provided as a group for any class that uses dynamically allocated memory.


Common Programming Error 11.8

Not providing an overloaded assignment operator and a copy constructor for a class when objects of that class contain pointers to dynamically allocated memory is a logic error.


Software Engineering Observation 11.6

It is possible to prevent one object of a class from being assigned to another. This is done by declaring the assignment operator as a private member of the class.


Software Engineering Observation 11.7

It is possible to prevent class objects from being copied; to do this, simply make both the overloaded assignment operator and the copy constructor of that class private.


Overloaded Equality and Inequality Operators

Line 21 of Fig. 11.6 declares the overloaded equality operator (==) for the class. When the compiler sees the expression integers1 == integers2 in line 57 of Fig. 11.8, the compiler invokes member function operator== with the call

      integers1.operator==( integers2 )

Member function operator== (defined in Fig. 11.7, lines 7484) immediately returns false if the size members of the arrays are not equal. Otherwise, operator== compares each pair of elements. If they are all equal, the function returns TRue. The first pair of elements to differ causes the function to return false immediately.

Lines 2427 of the header file define the overloaded inequality operator (!=) for the class. Member function operator!= uses the overloaded operator== function to determine whether one Array is equal to another, then returns the opposite of that result. Writing operator!= in this manner enables the programmer to reuse operator==, which reduces the amount of code that must be written in the class. Also, note that the full function definition for operator!= is in the Array header file. This allows the compiler to inline the definition of operator!= to eliminate the overhead of the extra function call.

Overloaded Subscript Operators

Lines 30 and 33 of Fig. 11.6 declare two overloaded subscript operators (defined in Fig. 11.7 at lines 8899 and 103114, respectively). When the compiler sees the expression integers1[ 5 ] (Fig. 11.8, line 61), the compiler invokes the appropriate overloaded operator[] member function by generating the call

      integers1.operator[]( 5 )

The compiler creates a call to the const version of operator[] (Fig. 11.7, lines 103114) when the subscript operator is used on a const Array object. For example, if const object z is instantiated with the statement


[Page 594]
      const Array z( 5 );

then the const version of operator[] is required to execute a statement such as

      cout << z[ 3 ] << endl;

Remember, a program can invoke only the const member functions of a const object.

Each definition of operator[] determines whether the subscript it receives as an argument is in range. If it is not, each function prints an error message and terminates the program with a call to function exit (header <cstdlib>).[3] If the subscript is in range, the non-const version of operator[] returns the appropriate array element as a reference so that it may be used as a modifiable lvalue (e.g., on the left side of an assignment statement). If the subscript is in range, the const version of operator[] returns a copy of the appropriate element of the array. The returned character is an rvalue.

[3] Note that it is more appropriate when a subscript is out of range to "throw an exception" indicating the out-of-range subscript. Then the program can "catch" that exception, process it and possibly continue execution. See Chapter 16 for more information on exceptions.


Previous Page
Next Page