8.8 实例研究:Array 类
在 C 和 C++ 中,数组是一种指针,因而数组存在许多导致错误的陷阱。例如,由于 C 和 C++ 不检测下标是否超出数组的边界而使程序导致越界错误;大小为n的数组的下标必须是 0、1、2…、 n-1,下标是不允许改变的;不能一次入输人或输出整个数组,而只能单独读取或者输出每个数组元素;不能用相等运算符或者关系运算符比较两个数组(因为数组名仅仅是指向内存中数组起始位置的指针)。
当把一个数组传递给一个能处理任意大小数组的常用函数时,数组的大小也必须作为一个额外的参数传递给该函数;不能用赋值运算符把一个数组赋给另一个数组(因为数组名是 const 类型指针,而常量指针不能用于赋值运算符的左边)。尽管所有这些处理能力似乎应该是很自然的,但是C和C++都没有提供这种能力。然而,C++ 提供了实现这种能力的手段,这就是运算符重载。
本节的范例建立了一个数组类,它能检测范围以确保数组下标不会越界,允许用赋值运算符把一个数组赋给另外一个数组。数组对象自动知道数组的大小,因而不用将数组的大小传送给函数。可以用流读取运算符和流插入运算符输入输出整个数组。还可以用相等运算符 ==
和 !=
比较数组。范例程序中的数组类用一个 static 成员跟踪程序中实例化数组对象的数目。
本例将加深读者对数据抽象的认识。当然,读者还可以增加数组类的其他功能,类的开发是十分有趣并富有挑战性的。
图 8.4 中的程序演示了类 Array 和用于该类的重载运算符。首先来看一下 main 函数中的驱动程序,然后再探讨类的定义以及类的每个成员和友元函数的定义。
1 // Fig. 8.4: arrayl.h
2 // Simple class Array (for integers}
3 #ifndef ARRAY1_H
4 #define ARRAY1_H
5
6 #include <iostream.h>
7
8 class Array {
9 friend ostream &operator<<( ostream &, const Array & );
l0 friend istream &operator>>( istream &, Array & );
11 public:
12 Array( int = 10 ); // default constructor
13 Array( const Array & ); // copy constructor
14 ~Array(); // destructor
15 int getSize() const; // return size
16 const Array &operator=( const Array & ); // assign arrays
17 bool operator==( const Array & ) const; // compare equal
18
19 // Determine if two arrays are not equal and
20 // return true, otherwise return false (uses operator==).
21 bool operator!=( const Array &right ) const
22 { return ! ( *this == right ); }
23
24 int &operator[] ( int ); // subscript operator
25 const int &operator[]( int ) const; // subscript operator
26 static int getArrayCount(); // Return count of
27 // arrays instantiated.
28 private:
29 int size; // size of the array
30 int *ptr; // pointer to first element of array
31 static int arrayCount; // # of Arrays instantiated
32 } ;
33
34 #endif
35 // Fig 8.4: arrayl.cpp
36 // Member function definitions for class Array
37 #include <iostream.h>
38 #include <iomanip.h>
39 #include <stdlib.h>
40 #include <assert.h>
41 #include "array1.h"
42
43 // Initialize static data member at file scope
44 int Array::arrayCount = 0; // no objects yet
45
46 // Default constructor for class Array (default size 10)
47 Array::Array( int arraySize )
48 {
49 size = ( arraySize > 0 ? arraySize : 10 );
50 ptr = new int[ size ] ; // create space for array
51 assert( ptr != 0 ); // terminate if memory not allocated
52 ++arrayCount; // count one more object
53
54 for (int i = 0; i < size; i++ )
55 ptr[ i ] = 0; // initialize array
56 }
57
58 // Copy constructor for class Array
59 // must receive a reference to prevent infinite recursion
60 Array::Array( const Array &init ) : size( intit.size )
61 {
62 ptr = new int[ size ] ; // create space for array
63 assert( ptr != 0 ); // terminate if memory not allocated
64 ++arrayCount; // count one more object
65
66 for (int i = 0; i < size; i++ )
67 ptr[ i ] init.ptr[ i ]; // copy init into object
68 }
69
70 // Destructor foi class Array
71 Array::~Array()
72 {
73 delete [] ptr; // reclaim space for array
74 --arrayCount; // one fewer objects
75 }
76
77 // Get the size of the array
78 int Array::getSize() const { return size; }
79
80 // Overloaded assignment operator
81 // const return avoids: ( al = a2 } = a3
82 const Array &Array::operator=( const Array &right )
83 {
84 if ( &right != this ) { // check for self-assignment
85
86 // for arrays of different sizes, deallocate original
87 // left side array, then allocate new left side array.
88 if ( size != right.size ) {
89 delete [] ptr; // reclaim space
90 size = right.size; // resize this object
91 ptr = new int[ size ]; // create space for array copy
92 assert( ptr != 0 ); // terminate if not allocated
93 }
94
95 for (int i = 0; i < size; i++ )
96 ptr[ i ] = right.ptr[ i ]; // copy array into object
97 }
98
99 return *this; // enables x = y = z;
100 }
101
102 // Determine if two arrays are equal and
103 // return true, otherwise return false.
104 bool Array::oprator==( const Array &right )const
105 {
106 if ( size != right.size )
107 return false; // arrays of different sizes
108
109 for (int i =0; i < size; i++ )
110 if ( ptr[ i ] != right.ptr[ i ] )
111 return false; // arrays are not equal
113 return true; // arrays are equal
114 }
117 // reference return creates an lvalue
118 int &Array::operator[] ( int subscript )
119 {
120 // check for subscript out of range error
121 assert( 0 <= subscript && subscript < size );
122
123 return ptr[ subscript ]; // reference return
124 }
125
126 // Overloaded subscript operator for const Arrays
127 // const reference return creates an value
128 const int &Array::operator[ ] (int subscript ) const
129 {
130 // check for subscript out of range error
131 assert( 0 <= subscript && subscript < size );
132
133 return ptr[ subscript ]; // const reference return
134 }
135
136 // Return the number of Array objects instantiated
137 // static functions cannot be const
138 int Array::getArrayCount() { return arrayCount; }
139
140 // Overloaded input operator for class Array;
141 // inputs values for entire array.
142 istream &operator>>( istream &input, Array &a )
143 {
144 for ( int i = 0; i < a.size; i++ )
145 input >> a.ptr[ i ];
146
147 return input; // enables cin >> x >> y;
148 }
149
150 // Overloaded output operator for class Array
151 ostream &operator<<( ostream &output, const Array &a )
152 {
153 int i;
154
155 for ( i = O; i < a.size; i++ ) {
156 output << setw( 12 ) << a.ptr[ i ];
157
158 if ( ( i + 1 ) % 4 == 0 ) // 4 numbers per row of output
159 output << endl;
160 }
161
162 if( i % 4 != 0 )
163 output << endl;
164
165 return output; // enables cout << ~ << y;
166 }
167 // Fig. 8.4:fig08 04.cpp
168 // Driver for simple class Array
169 #include <iostream.h>
170 #include "arrayl.h"
171
172 int main()
173 {
174 // no objects yet
175 cout << "# of arrays instantiated = "
176 << Array::getArrayCount() << '\n';
177
178 // create two arrays and print Array count
179 Array integers1( 7 ), integers2;
180 cout << "# of arrays instantiated = "
181 << Array::getArrayCount() << "\n\n";
182
183 // print integersl size and contents
184 cout << "Size of array integers1 is"
185 << integers1.getSize()
186 << "\nArray after initialization:\n"
187 << integersl << '\n';
188
189 // print integers2 size and contents
190 cout << "Size of array integers2 is "
191 << integers2.getSize()
192 << "\nArray after initialization:\n"
193 << integers2 << '\n';
194
195 // input and print integersl and integers2
196 cout << "Input 17 integers:\n";
197 cin >> integers1 >> integers2;
198 cout << "After input, the arrays contain:\n"
199 << "integersl:\n" << infegers1
200 << "integers2:\n" << integers2 << '\n';
201
202 // use overloaded inequality (!=) operator
203 cout << "Evaluating: integers1 != integers2\n";
204 if ( integers1 != integers2 )
205 cout << "They are not equal\n";
206
207 // create array integers3 using integers1 as an
208 // initlizer; print size and contente
209 Array integers3( integers1 );
210
211 cout << "\nSize of array integers3 is"
212 << integers3.getSize()
213 << "\nArray after initialization:\n"
214 << integers3 << '\n';
215
216 // use overloaded assignment (=) operator
217 cout << "Assigning integers2 to integers1:\n";
218 integers1 = integers2;
219 cout << "integersl:\n" << integers1
220 << "integers2:\n" << integers2 << '\n';
221
222 // use overloaded equality (==) operator
223 cout << "Evaluating: integers1 == integers2\n";
224 if ( integers1 == integers2 )
225 cout << "They are equal\n\n";
226
227 // use overloaded subscript operator to create rvalue
228 cout << "integers1[ 5 ] is "<< integers1[ 5 ] << '\n';
229
230 // use overloaded subscript operator to create lvalue
231 cout << "Assigning 1000 to integers1[ 5 ]\ n";
232 << integers1[ 5 ] = 1000;
233 cout << "integers1:\n" << integers1 << '\n';
234
235 // attempt to use out of range subscript
236
237 integers1[ 15 ] = 1000; // ERROR: out of range
238
239 return 0;
240 }
输出结果:
# of arrays instantiated = 0
# of arrays instantiated 2
Size of array integersl is 7
Array after initialization:
0 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
Input 17 integers:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
After input, the arrays contain:
integersl:
1 2 3 4
5 6 7
integers2:
8 9 10 11
12 13 14 15
16 17
Evaluating:integers1 != integers2
They are not equal
Size of array integers3 is 7
Array after initialization:
1 2 3 4
5 6 7
Assigning integers2 to inteqersl:
integersl:
8 9 10 11
12 13 14 15
16 17
integers2:
8 9 10 11
12 13 14 15
16 17
Evaluating: integersl == integers2
They are equal
integersl[ 5] is = 13
Assigning 1000 to integersl[ 5 ]
integersl:
8 9 10 11
12 1000 14 15
16 17
Attempt to assign 1000 to integersl[15]
Assertion failed: 0 <= subscript && subscript < size,
file Arrayl.cpp, line 87
abnormal program termination
图 8.4 用重载运算符演示 Array 类
类 Array 的 static 类变量 arrayCount 包含了程序执行过程中实例化的 Array 对象的个数,该值由第176行的static成员函数getArrayCount返回。程序实例化子类Array的两个对象(第179行),对象 integees1 有7个元素.对象integers2有10个元素(默认的元素个数由类Array的构造函数指定)。
第181行再次调用getArrayCount,取得类变量arrayCount的值。第184到第187行用成员函数getSize确定arrayintegers1的长度,并使用Array重载流插入运算符输出integer1,以证实构造函数正确地初始化了数组的元素。接下来,第190到第193行的程序先输出数组integers2的长度,然后用重载的流插入运算符输出integer2数组。
完成上述工作后,程序提示用户输入17个整数,Array重载流读取运算符并使用下列语句(第197行):
cin >> integers1 >> integers2;
把这些值读入到两个数组中,前7个整数保存在integers1中,其余的则保存在intergers2中。为了证实输入操作的正确性,程序用流插入运算符输出了这两个数组(第198到第200行)。
接下来,程序通过测试条件(第204行):
integers1 != integers2
来验证重载的不相等运算符!=,输出结果表明这两个数组确实不相等。
第209行程序实例化第三个数组integers3并用数组integers1对其初始化,这将调用Array复制构造函数将integers1复制到integers3中。我们将在后面详细讨论复制构造函数。
程序在第211到第214行输出integers3的长度,并使用 Array 重载流插入运算符输出 integers3,以证实构造函数正确地初始化数组。
接下来,第218行通过下列语句测试重载的赋值运算符(=):
integers1 = integers2;
然后打印出这两个数组来验证赋值的正确性。原来的integere1中只有7个整数,现在必须要使其能容纳integers2中的10个元素的副本。重载的赋值运算符可以改变原先的integers1的大小并复制integer2中的元素。
接下来,第224行用重载的运算符==;测试赋值后 integers1 和 integers2 是否相等。
接下来,第228行用重载的下标运算符引用 integers1[5](integers1 数组范围内的一个元素),这个带下标的数组名作为右值来打印 integersl[5] 的值,第232行将 integers[5] 放在
赋值语句左边并赋给其一个新值 1000,注意,operator[] 返回引用并作为左值使用(在确定了5是在 integers1 的长度范围内)。
第237行程序试图将1000赋给integers[15](越界的元素)。Array重载了[]运算符捕获到该错误并中止程序。
有意思的是,数组下标运算符不仅仅可用于数组,还可以用于从其他各种容器类(如链表、字符串、字典等等)中选择元素。此外,下标不仅仅是整数,还可以是字符、字符串、浮点数甚至是用户自定义的对象。
上面介绍了程序是如何执行的。下面再分析—下类的首部和成员函数的定义。第29行到第3l行
int size;// size Of the array
int *ptr;// pointer to first element of array
static int arrayCount;// # of Arrays instantiated
是类的private数据成员,包括一个int类型指针ptr(指向Array对象中存储整型的动态分配数组)、一个表示数组元素个数的size成员以及一个表示已经实例化的数组对象数目的static成员arrayCount。
第9行和第10行:
friend ostream &operator<<(ostream &,const Array &);
friend istream &operator<<(istream &,Array &);
声明了重载的流插入、流读取运算符是类Array的友元。当编译器遇到表达式:
cout << arrayObject
通过生成 operator<<(cout,arrayObject) 来调用函数 operator<(ostream &, constArray &)。当编译器遇到表达式:
cin >> arrayObject
通过生成operator>>(cin,arrayObjeet)来调用函数operatpr>>(istream &, Array &)。
因为Array对象总是在流插入运算符和流读取运算符的右边,所以这两个运算符函数不能是Array的成员函数。如果这些运算符函数是Array的成员函数,则可以用下列的语句(可能会出现意外情况)输入和输出Array:
arrayObject << cout;
arrayObject >> cin;
函数operator<<(在第151行定义)打印由size指定的存储在ptr中的数组元素的个数,而函数operator>>(在第142行定义)则把数据直接输入到ptr所指向的数组中。为了能够分别实现连续的输入输出,这两个运算符都返回了一个合适的引用。
代码行:
Array(int= 1O); // default Constructor
声明了类的默认构造函数,并且指定数组元素的默认大小为10。当编译器遇到如下声明:
Array integers1(7);
或与之等价的形式:
Array integers1 = 7;
编译器将调用默认构造函数(本例中默认构造函数实际上接收一个int参数,默认值为10)。默认构造函数(第47行定义)验证参数并赋值给Size数据成员,用new分配数组所需的空间,将new返回的指针赋给数据成员ptr,然后用assert测试new操作是否成功,并递增arrayCount的值,最后用for循环将数组的所有元素初始化为0。如果没有将Array初始化,也可以在以后读取相应的值,但这样做会降低程序的可执行性。Array和任何对象都应随时保持正确初始化和一致的状态。
第13行:
Array(const Array &); // copy Constructor
是一个复制构造函数(第60行定义),它通过建立现有Array对象的副本来初始化Array对象。必须要小心对待这种复制操作,避免两个Array对象指向同一块动态分配的存储区,默认的成员复制更容易发生这种问题。不论何时需要复制对象时都会调用复制构造函数,如在传值调用时、从被调用函数返回一个对象时、或把某个对象初始化为同类的另外一个对象的副本时。当声明创建类Array的一个对象并用另外一个对象对它初始化时,调用复制构造函数。例如下列声明:
Array integers3(integers1);
或者与之等价的声明:
Array integers3 = integersl;
常见程错误 8.6
注意复制构造函数要按引用调用,而不是按值调用,否则复制构追函数调用会造成无穷递归(这是个致命逻辑错误),因为对于按值调用,建立传入复制构造函数的对象副本会造成复制构造函数的递归调用。
复制构造函数 Array 使用成员初始化值将数组的size值复制到新数组的数据成员 size 中,用new分配新数组所需的空间,把new返回的指针赋给数据成员Ptr,然后用assert测试new操作是否成功,并递增arrayCount的值,最后用for循环将数组的所有元素作为初始值复制到新数组中。
常见编程错误 8.7
如果构造函数简单地将源对象的指针复制到目标对象的指针,则这两个对象将指向同一块动态分配的内存块,执行析构函数时将释放该内存决,从而导致另外一个对象的Ptr没有定义,这种情况可能令引起严重的运行时错误。
软件工程视点 8.4
通常要把构造函数、析构函数、重载的赋值运算符以及复制构这造函数一起提供给使用动态内存分配的类。
第14行:
~Array(); // destructor
声明了类的析构函数(第71行定义)。当撤消类Array的某个对象时,自动调用析构函数。析构函数用delete[]释放在构造函数中用new动态分配的内存块,然后递减arraycount的值。
第15行:
int getSize() const; // return size
声明了读取数组大小的函数。
第16行:
const Array &operator= ( const Array &); // assign arrays
声明了重载的赋值运算符函数。当编译器遇到表达式:
integers1 = integers2;
就会通过产生如下代码调用函数operator=:
integers1.operator=(integers2)
成员函数operator=(第82行定义)测试了这种赋值是否是自我赋值。如果是,则跳过赋值操作(即对象已经是其自身,无需再赋值)。如果不是,则成员函数确定两个数组长度是否相同,如果是,则左边Array对象的原始整数数组不重新分配。否则成员函数operator=用delete释放目标数组原先动态分配的空间,将源数组的数据成员size复制到目标数组的size,用new分配目标数组所需的空间并将new返回的指针赋给数组的Ptr成员,用assert测试new操作是否成功,最后再用for循环将源数组的每一个元素复制到目标数组中。不管这种操作是否是自我赋值,成员函数都返回当前对象(即*this),这种处理方式允许诸如x=y=z这样的连续赋值。
常见编程错误 8.8
类的对象包含指向动态分配的内存的指针,但如果没有为它提供重载的赋值运算符和复制构造函数则会造成逻辑错误。
软件工程视点 8.5
把赋值运算符定义为类的private成员可以防止将一个类对象赋给另外一个类对象。
软件工程视点 8.6
只要重载的赋值运算符和复制构造函数为private,就可以防止复制类对象。
第17行:
bool operator=(const Array &)const;
// compare equal 声明了重载的相等运算符。当编译器遇到 malll 函数中的如下表达式时:
integers1 == integeis2
编译器通过生成如下代码来调用operator == 成员函数:
integers1.operator==(integers2)
如果数组的size成员不相等,则operator==成员函数立即返回false,否则,成员函数开始成对比较相应的元素。如果它们全都相等,则返回true,一旦发现某一对元素不同则立即返回false。
第21到第22行:
bool operator!=(const Array &right)const
{ return ! ( *this == right);}
声明了重载的不相等运算符(!=)。成员函数中oprator!=根据重载的相等运算符定义。该函数定义用重载operator==函数确定一个Array是否等于另一个Array,然后返回结果的相反值。这样编写oprator!=函数使程序员可以复用operator==函数,减少类中需要编写的代码量。另外,operator!=的完整函数定义在Array头文件中,使编译器可以内联operator!=的定义,消除额外函数调用的开销。
第24到第25行:
int &operator[](int); // subscript operator
const int &operator[](int)const; // subscript operator
声明了两个重载的下标运算符(分别在第118和128行定义)。当编译器遇到main函数中的如下表达式时:
integers1[ 5]
编译器通过生成下列代码来调用重载的 operator[] 成员函数:
integers1.operator[] (5)
constArray 对象使用下标运算符时,编译器调用 operator 的 [] 的 const 版本。operator[] 成员函数首先测试下标是否越界。如果越界,则程序异常中止。如果没有越界,则对 operator== 的非 const 版本返回相应的数组元素作为引用,以便使它能用作左值(如用在赋值语句的左边)。而对 operator[] 的 const 版本返回右值。
第26行:
static int getArrayCount(); // return count of Arrays
声明了 static 成员函数 getArrayCount。即使在不存在类 Array 的对象中,该成员函数也返回静态数据成员 arrayCount 的值。