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.
Lines 10–11 of Fig. 10.10 declare the overloaded stream insertion operator and the overloaded stream extraction operator as friend
s 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 friend
s 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 friend
s of class Array
.
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.
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. Array
s, and objects in general, should be properly initialized as they’re created.
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).
Software Engineering Observation 10.3
The argument to a copy constructor should be a const reference to allow a const object to be copied.
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.
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.
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.
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
.
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 Array
s 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.
Software Engineering Observation 10.4
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.
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 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.
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.
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 Array
s 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!=
.
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
.
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.
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 };
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;
3.16.81.33