10.10.2. Array Class Definition

Now that we’ve seen how this program operates, let’s walk through the class header (Fig. 10.10). As we refer to each member function in the header, we discuss that function’s implementation in Fig. 10.11. In Fig. 10.10, lines 34–35 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 pointer—ptr—that 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 10–11 of Fig. 10.10 declare the overloaded stream insertion operator and the overloaded stream extraction operator as friends of class Array. When the compiler sees an expression like cout << arrayObject, it invokes non-member function operator<< with the call

operator<<( cout, arrayObject )

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

operator>>( cin, arrayObject )

Again, 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 or stream extraction operator.

Function operator<< (defined in Fig. 10.11, lines 111–126) prints the number of elements indicated by size from the integer array to which ptr points. Function operator>> (defined in Fig. 10.11, lines 102–108) 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. These functions have access to an Array’s private data because they’re declared as friends of class Array. We could have used class Array’s getSize and operator[] functions in the bodies of operator<< and operator>>, in which case these operator functions would not need to be friends of class Array.

Image

You might be tempted to replace the counter-controlled for statement in lines 104–105 and many of the other for statements in class Array’s implementation with the C++11 range-based for statement. Unfortunately, range-based for does not work with dynamically allocated built-in arrays.

Array Default Constructor

Line 14 of Fig. 10.10 declares the default constructor for the class and specifies a default size of 10 elements. When the compiler sees a declaration like line 11 in Fig. 10.9, it invokes class Array’s default constructor to set the size of the Array to 10 elements. The default constructor (defined in Fig. 10.11, lines 11–18) 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’s 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 as they’re created.

Array Copy Constructor

Line 15 of Fig. 10.10 declares a copy constructor (defined in Fig. 10.11, lines 22–28) 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 39 of Fig. 10.9.

The copy constructor for Array copies the size of the initializer Array into 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 copy constructor uses a for statement to copy all the elements of the initializer Array into the new Array object. 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).


Image Software Engineering Observation 10.3

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



Image Common Programming Error 10.4

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


Array Destructor

Line 16 of Fig. 10.10 declares the class’s destructor (defined in Fig. 10.11, lines 31–34). 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.


Image Error-Prevention Tip 10.3

If after deleting dynamically allocated memory, the pointer will continue to exist in memory, set the pointer’s value to nullptr to indicate that the pointer no longer points to memory in the free store. By setting the pointer to nullptr, the program loses access to that free-store space, which could be reallocated for a different purpose. If you do not set the pointer to nullptr, your code could inadvertently access the reallocated memory, causing subtle, nonrepeatable logic errors. We did not set ptr to nullptr in line 33 of Fig. 10.11 because after the destructor executes, the Array object no longer exists in memory.


getSize Member Function

Line 17 of Fig. 10.10 declares function getSize (defined in Fig. 10.11, lines 37–40) that returns the number of elements in the Array.

Overloaded Assignment Operator

Line 19 of Fig. 10.10 declares the overloaded assignment operator function for the class. When the compiler sees the expression integers1 = integers2 in line 47 of Fig. 10.9, the compiler invokes member function operator= with the call

integers1.operator=( integers2 )

Member function operator=’s implementation (Fig. 10.11, lines 44–62) tests for self-assignment (line 46) in which an Array object is being assigned to itself. When this is equal to the right operand’s address, a self-assignment is being attempted, so the assignment is skipped (i.e., the object already is itself; in a moment we’ll see why self-assignment is dangerous). If it isn’t a self-assignment, then the function determines whether the sizes of the two Arrays are identical (line 50); in that case, the original array of integers in the left-side Array object is not reallocated. Otherwise, operator= uses delete [] (line 52) 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 53), uses new to allocate the memory for the target Array and places the pointer returned by new into the Array’s ptr member. Then the for statement in lines 57–58 copies the 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 in line 61) as a constant reference; this enables cascaded Array assignments such as x = y = z, but prevents ones like (x = y) = z because z cannot be assigned to the const Array reference that’s returned by (x = y). If self-assignment occurs, and function operator= did not test for this case, operator= would unnecessarily copy the elements of the Array into itself.


Image Software Engineering Observation 10.4

Image

A copy constructor, a destructor and an overloaded assignment operator are usually provided as a group for any class that uses dynamically allocated memory. With the addition of move semantics in C++11, other functions should also be provided, as you’ll see in Chapter 24.



Image Common Programming Error 10.5

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


C++11: Move Constructor and Move Assignment Operator
Image

C++11 adds the notions of a move constructor and a move assignment operator. We defer a discussion of these new functions until Chapter 24, C++11: Additional Features. This discussion will affect the two preceding tips.

C++11: Deleting Unwanted Member Functions from Your Class
Image

Prior to C++11, you could prevent class objects from being copied or assigned by declaring as private the class’s copy constructor and overloaded assignment operator. As of C++11, you can simply delete these functions from your class. To do so in class Array, replace the prototypes in lines 15 and 19 of Fig. 10.10 with:

Array( const Array & ) = delete;
const Array &operator=( const Array & ) = delete;

Though you can delete any member function, it’s most commonly used with member functions that the compiler can auto-generate—the default constructor, copy constructor, assignment operator, and in C++ 11, the move constructor and move assignment operator.

Overloaded Equality and Inequality Operators

Line 20 of Fig. 10.10 declares the overloaded equality operator (==) for the class. When the compiler sees the expression integers1 == integers2 in line 55 of Fig. 10.9, the compiler invokes member function operator== with the call

integers1.operator==( integers2 )

Member function operator== (defined in Fig. 10.11, lines 66–76) immediately returns false if the size members of the Arrays are not equal. Otherwise, operator== compares each pair of elements. If they’re all equal, the function returns true. The first pair of elements to differ causes the function to return false immediately.

Lines 23–26 of Fig. 10.9 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 you to reuse operator==, which reduces the amount of code that must be written in the class. Also, the full function definition for operator!= is in the Array header. This allows the compiler to inline the definition of operator!=.

Overloaded Subscript Operators

Lines 29 and 32 of Fig. 10.10 declare two overloaded subscript operators (defined in Fig. 10.11 in lines 80–87 and 91–98, respectively). When the compiler sees the expression integers1[5] (Fig. 10.9, line 59), it 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. 10.11, lines 91–98) when the subscript operator is used on a const Array object. For example, if you pass an Array to a function that receives the Array as a const Array & named z, 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 and—if not, each throws an out_of_range exception. 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.

C++11: Managing Dynamically Allocated Memory with unique_ptr
Image

In this case study, class Array’s destructor used delete [] to return the dynamically allocated built-in array to the free store. As you recall, C++11 enables you to use unique_ptr to ensure that this dynamically allocated memory is deleted when the Array object goes out of scope. In Chapter 17, we introduce unique_ptr and show how to use it to manage a dynamically allocated objects or dynamically allocated built-in arrays.

C++11: Passing a List Initializer to a Constructor
Image

In Fig. 7.4, we showed how to initialize an array object with a comma-separated list of initializers in braces, as in

array< int, 5 > n = { 32, 27, 64, 18, 95 };

Recall from Section 4.8 that C++11 now allows any object to be initialized with a list initializer and that the preceding statement can also be written without the =, as in

array< int, 5 > n{ 32, 27, 64, 18, 95 };

C++11 also allows you to use list initializers when you declare objects of your own classes. For example, you can now provide an Array constructor that would enabled the following declarations:

Array integers = { 1, 2, 3, 4, 5 };

or

Array integers{ 1, 2, 3, 4, 5 };

each of which creates an Array object with five elements containing the integers from 1 to 5.

To support list initialization, you can define a constructor that receives an object of the class template initializer_list. For class Array, you’d include the <initializer_list> header. Then, you’d define a constructor with the first line:

Array::Array( initializer_list< int > list )

You can determine the number of elements in the list parameter by calling its size member function. To obtain each initializer and copy it into the Array object’s dynamically allocated built-in array, you can use a range-based for as follows:

size_t i = 0;
for ( int item : list )
   ptr[ i++ ] = item;

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.12.166.76