Case Study: Array Class

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 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


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 4 using std::cerr; 5 using std::cout; 6 using std::cin; 7 using std::endl; 8 9 #include 10 using std::setw; 11 12 #include // 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 << " Error: 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 << " Error: 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<<


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 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 << " Array after initialization: " << integers1; 19 20 // print integers2 size and contents 21 cout << " Size of Array integers2 is " 22 << integers2.getSize() 23 << " Array after initialization: " << integers2; 24 25 // input and print integers1 and integers2 26 cout << " Enter 17 integers:" << endl; 27 cin >> integers1 >> integers2; 28 29 cout << " After input, the Arrays contain: " 30 << "integers1: " << integers1 31 << "integers2: " << integers2; 32 33 // use overloaded inequality (!=) operator 34 cout << " Evaluating: 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 << " Size of Array integers3 is " 44 << integers3.getSize() 45 << " Array after initialization: " << integers3; 46 47 // use overloaded assignment (=) operator 48 cout << " Assigning integers2 to integers1:" << endl; 49 integers1 = integers2; // note target Array is smaller 50 51 cout << "integers1: " << integers1 52 << "integers2: " << integers2; 53 54 // use overloaded equality (==) operator 55 cout << " Evaluating: 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 << " integers1[5] is " << integers1[ 5 ]; 62 63 // use overloaded subscript operator to create lvalue 64 cout << " Assigning 1000 to integers1[5]" << endl; 65 integers1[ 5 ] = 1000; 66 cout << "integers1: " << integers1; 67 68 // attempt to use out-of-range subscript 69 cout << " Attempt 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  


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.


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.


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!


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.


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


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 ).[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.

Категории