当前位置: 首页 > 文档资料 > C++大学教程 >

8.10 实例研究:String 类

优质
小牛编辑
132浏览
2023-12-01

作为学习重载的练习,本节要建立一个能够处理字符串的建立和操作的类(图 8.5)。string 类已是 C++ 标准库中的一部分,第19章将详细介绍 string 类。现在我们用运算符重载建立一个 String 类。我们首先列出 String 类的首部,并讨论表示 String 的对象的 private 数据。然后,分析类的 Public 接口,讨论该类提供的每一种服务。

接着分析了 main 函数中的驱动程序。讨论了令人感兴趣的编码风格,也就是用新 String 类的对象和该类的重载运算符集编写的各种运算符表达式。

然后我们讨论了类 String 的成员函数的定义。对于每个重载的运算符函数,驱动程序都有调用重载的运算符的代码,并解释了这些函数的工作原理。

1 // Fig. 8.5: string1.h
2 // Definitien of a String class
3 #ifndef STRING1_H
4 #define STRING1_H
5
6 #include <iostream.h>
7
8 class String {
9 friend ostream &operator<<( ostream &, const String & );
10 friend istream &operator>>( istream &, String & );
11
12 public:
13 String( const char * ="" ); // conversion/default ctor
14 String( const String & ); // copy constructor
15 ~String(); // destructor
16 const String &operator=( const String & ); // assignment
17 const String &operator+=( const String & ); // concatenation
18 bool operator!() const; // is String empty?
19 bool operator==( const String & ) const; // test sl - s2
20 bool operator<( coost String & ) const; // test sl < s2
21
22 // test s1 != s2
23 bool operator!=( const String & right ) const
24 { return !( *this == right ); }
25
26 // test si > S2
27 bool operator>( const String &right ) const
28 { return right < *this; }
29
30 // test s1 <= s2
31 bool operator<=( const String &right ) const
32 { return !( right < *this ); (
33
34 // test s1 >= s2
35 bool operator>=( const String &right ) const
36 { return !( *this < right ); }
37
38 char &operator[] ( int ); // subscript operator
39 const char &operator[]( int ) const; // subscript operator
40 String &operator()( int, int ); // return a substring
41 int getLength() const; // return string length
42
43 private:
44 int length; // string length
45 char *sPtr; // pointer to start of string
46
47 void setString( const char * ); // utility function
48 };
50 #endif
51 // Fig. 8.5: string1.cpp
52 // Member function definitions for class String
53 #include <iostream.h>
54 #include <iomanip.h>
55 #include <string.h>
57 #include "string1.h"
58
59 // Conversion constructor: Convert char * to String
60 String::String( const char *s ) : length( strlen( s ))
61 {
62 cout << "Conversion constructor: "<< s << '\n';
63 setString( s ); // call utility function
64 }
65
66 // Copy constructor
67 String::String( const String &copy ) : length( copy.length )
68 {
69 cout << "Copy constructor: " << copy.sPtr << '\n';
70 setString( copy.sPtr ); // call utility function
71 }
72
73 // Destructor
74 String::~string()
75 {
76 cout << "Destructor: "<< sPtr << '\n';
77 delete [] sPtr; // reclaim string
78 }
79
80 // Overloaded = operator; avoids self assignment
81 const String &String::operator=( const String &right )
82 {
83 cout << "operator= called\n";
85 if ( &right != this ) { // avoid self assignment
86 delete [] sPtr; // prevents memory leak
87 length = right.length; // new String length
88 setString( right.sPtr ); // call utility function
89 }
90 else
91 cout << "Attempted assignment of a String to itself\n";
92
93 return *this; // enables cascaded assignments
94 }
95
96 // Concatenate right operand to this object and
97 // store in this object.
98 const String &String::operator+=( const String &right )
99 {
100 char *tempPtr = sPtr; // hold to be able to delete
101 length += right.length; // new String length
102 sPtr= new char[ length + 1 ]; // create space
103 assert( sPtr != 0 ); // terminate if memory not allocated
104 strcpy( sPtr, tempPtr ); // left part of new String
105 strcat( sPtr, right.sPtr ); // right part of new String
106 delete [] tempPtr; // reclaim old space
107 return *this; // enables cascaded calls
108 }
109
110 // Is this String empty?
111 bool String::operator!() const { return length == 0; }
112
113 // Is this String equal to right String?
114 bool String::oprator==( const String &right ) const
115 { return strcmp( sPtr, right.sPtr ) == 0; }
116
117 // Is this String less than right String?
118 bool String::oprator<( const String &right ) const
119 { return strcmp( sPtr, right.sPtr ) < 0; }
120
121 // Return a reference to a character in a String as an lvalue.
122 char &String::operator[] ( int subscript )
123 {
124 // First test for subscript out of range
125 assert( subscript >= 0 && subscript < length );
126
127 return sPtr[ subscript ]; // creates lvalue
128 }
129
130 // Return a reference to a character in a String as an rvalue.
131 const char &String::oprator[]( int subscript ) const
132 {
133 // First test for subscript out of range
134 assert( subscript >= 0 && subscript < length );
135
136 return sPtr[ subscript ]; // creates rvalue
137 }
138
139 // Return a substring beginning at index and
140 // of length subLength as a reference to a String object.
141 String &String::operator()( int index, int subLength )
142 {
143 // ensure index is in range and substring length >= 0
144 assert( index >= 0 && index < length && subLength >= 0 );
145
146 String *subPtr = new String; // empty String
147 assert( subPtr != 0 ); // ensure new String allocated
148
149 // determine length of substring
150 if ( ( subLength == 0 ) || ( index + subLength > length ) )
151 subPtr->length = length - index + 1;
152 else
153 subPtr->length = subnength + 1;
154
155 // allocate memory for substring
156 delete subPtr->sPtr; // delete character array from object
157 subPtr->sPtr = new char [ subPtr->length ];
158 assert( subPtr -> sPtr != 0 ); // ensure space allocated
159
160 // copy substring into new String
161 strncpy( subPtr->sPtr, &sPt[ index ], subPtr->length );
162 subPtr->sPtr[ subPtr -> length ] = '\0'; // terminate String
163
164 return *subPtr; // return new String
165 }
166
167 // Return string length
168 int String::getLength() const { return length; }
169
170 // Utility function to be called by constructors and
171 // assignment operator.
172 void String::setString( const char *string2 )
173 {
174 sPtr = new char[ length + 1 ]; // allocate storage
175 assert( sPtr != 0 ); // terminate if memory not allocated
176 strcpy( sptr, string2 ); // copy literal to object
177 }
178
179 // Overloaded output operator
180 ostream &operator<<( ostream &output, const String &s )
181 {
182 output << s.sPtr;
183 return output; // enables cascading
184 }
185
186 // Overloaded input operator
187 istream &operator>>( istream &input,String &s )
188 {
189 char temp[ 100 ]; // buffer to store input
190
191 input >> setw( 100 ) >> temp;
192 s = temp; // use String class assignment operator
193 return input; // enables cascading
194 }
195 // Fig. 8.5:fig08 05.cpp
196 // Driver for class String
197 #include <iostream.h>
198 #include "string1.h"
199
200 int main()
201 {
202 String s1( "happy" ), s2( "birthday" ), s3;
203
204 // test overloaded equality and relational operators
205 cout << "s1 is \"" << s1 << "\"; s2 is \"" << s2
206 << "\"; s3 is \"" << s3 << '\"'
207 << "\nThe results of comparing s2 and s1:"
208 << "\ns2 == s1 yields"
209 << ( s2 == s1 ? "true" : "false" )
210 << "\ns2 != s1 yields"
211 << ( s2 != s1 ? "true" : "false" }
212 << "\ns2 > s1 yields"
213 << ( s2 > s1 ? "true" : "false" )
214 << "\ns2 < s1 yields "
215 << ( s2 < s1 ? "true" : "false" )
216 << "\ns2 >= s1 yields
217 << ( s2 >= s1 ? "true" : "false" )
218 << "\ns2 <= s1 yields"
219 << ( s2 <= s1 ? "true" : "false" );
220
221 // test overloaded String empty (!) operator
222 cout << "\n\nTesting !s3:\n";
223 if ( !s3 ) {
224 cout << "s3 is empty; assigning s1 to s3;\n";
225 s3 = s1; // test overloaded assignment
226 cout << "s3 is \"" << s3 << "\"";
227 }
228
229 // test overloaded String concatenation operator
230 cout << "\n\ns1 += s2 yields s1 = ";
231 s1 += s2; // test overloaded concatenation
232 cout << s1;
233
234 // test conversion constructor
235 cout << "\n\ns1 += \" to you\" yields\n";
236 s1 +=" to you"; // test conversion constructor
237 cout << "s1 = "<< s1 << "\n\n";
238
239 // test overloaded function call operator () for substring
240 cout << "The substring of s1 starting at\n"
241 << "location 0 for 14 characters, s1(0, 14), is:\n"
242 << s1( 0, 14 ) << "\n\n";
243
244 // test substring "to-end-of-String" option
245 cout << "The substring of s1 starting at\n"
246 << "location 15, s1(15, 0), is:"
247 << s1( 15, 0 ) << "\n\n"; // 0 is "to end of string"
248
249 // test copy constructor
250 String *s4Ptr = new String(s1);
251 cout << "*s4Ptr = "<< *s4Ptr << "\n\n";
252
253 // test assignment (=) operator with self-assignment
254 cout << "assigning *s4Ptr to *s4Ptr\n";
255 *s4Ptr = *s4Ptr; // test overloaded assignment
256 cout << "*s4Ptr = "<< *s4Ptr << '\n';
257
258 // test destructor
259 delete s4Ptr;
260
261 // test using subscript operator to create lvalue
262 s1[ 0 ] = 'H';
263 s1[ 6 ] = 'B';
264 cout << "\nsl after s1[ 0 ] = 'H' and s1[ 6 ] = 'B' is:"
265 << s1 << "\n\n";
266
267 // test subscript out of range
268 cout << "Attempt to assign 'd' to s1[ 30 ] yields:" << endl;
269 s1[ 30 ] = 'd'; // ERROR: subscript out of range
270
271 return 0;
272 }

输出结果:

Conversion constructor: happy
Conversion constructor: birthday
Conversion constructor:
sl is "happy"; s2 is "birthday"; s3 is ""
The results of comparing s2 and s1:
s2 == s1 yields false
s2 != s1 yields true
s2 > sl yields false
s2 < sl yields true
s2 >= s1 yields false
s2 <= s1 yields true

Testing !s3:
s3 is empty; assigning s1 to s3;
operator = called
s3 is "happy"

s1 += s2 yields s1 = happy birthday

s1 +=" to you" yields
Conversion constructor: to you
Destructor: to you
s1 = happy birthday to you

Conversion constructor:
The substring of sl starting at
location 0 for 14 characters, sl(0, 14), is:
happy birthday

Conversion constructor:
The substring of sl starting at
location 15, s1(15,0}, is: to you copy constructor: happy birthday to you
*s4Ptr = happy birthday to you assigning *s4Ptr to *s4Ptr
operator = called
Attempted assignment of a string to itself
*s4Ptr = happy birthday to you
destructor: happy birthday to you

s1 after s1[ 0] = 'H' and si[ 6] = 'B' is: Happy Birthday to you

Attempt to assign 'd' to s1[30] yields:
Assertion failed: subscript >= 0 && subscript < length,
file String1.cpp,line 76
abnormal program termination

图 8.5 定义基本的 String 类

我们从 String 的内部表示开始讨论。第44行到第45行:

int length; // Strzng length
char*sPtr; // pointer to start of string

声明了类的private数据成员。String 的对象有一个 length 字段(表示字符串中除字符串终止符以外的字符个数)和一个指向动态分配内存(表示字符串)的指针sPtr。
现在分析一下图8.5中定义String类的头文件。下面的两行代码(第9行到第10行):

friend ostream &operator<<( ostream &,const String &);
friend istream &operator>>( istream &, String & );

把重载的流插入运算符函数 operator<<(第180行定义)和流读取运算符函数 operator>>(第187行定义)声明为类的友元。这两个函数的实现是显而易见的。

第13行:

String(const char * = ""); // conversion/default ctor

声明了一个转换构造函数,该构造函数(第60行定义)有一个const char*类型的参数(默认值是空字符串)。该函数实例化了String的一个对象,该对象包含了与参数相同的字符串。任何只带一个参数的构造函数都可以认为是一种转换构造函数。稍后就会看到,当使用char*参数对String类做任何操作时,转换构造函数是很有用的。转换构造函数把一个char*字符串转换为String的对象(然后该对象要赋给目标String对象)。

使用这种转换构造函数意味着不必再为将字符串赋给String的对象提供重载的赋值运算符,编译器先自动地调用该函数建立一个包含该字符串的临时String对象,然后再调用重载的赋值运算符将临时String对象赋给另一个String对象。

软件工程视点 8.7
当使用转换构造函数实现隐式转换时,C++ 只会使用一个隐式的构造函数调用来试图满足重载赋值运算符的需要。通过执行一系列隐式的、用户自定义的类型转换来满足重载运算符的需要是不可能的。

在做出像 String s1("happy") 这样的声明时,调用String的转换构造函数。转换构造函数计算了字符串的长度并将该长度赋给private数据成员length,然后调用private工具函数setString。函数setString(第172行定义)使用new为 private 数据成员sPtr分配足够的空间,并用assert来测试内存分配操作是否成功。如果成功,则用函数strcpy把字符串复制到对象中。

第14行:

String(const String &); // copy constructor

是一个复制构造函数(第67行定义),它通过复制已存在的String对象来初始化一个String对象。必须要小心对待这种复制操作,避免使两个String对象指向同一块动态分配的内存区,默认的成员复制更容易发生这种问题。

复制构造函数除了将源String对象的length成员复制到目标String对象外,其余操作和转换构造函数类似。注意,复制构造函数为目标对象的内部字符串分配了新的存储空间,如果它只是简单地将源对象中的sPtr复制到目标对象的sptr,则这两个对象将指向同一块动态分配的内存块。执行一个对象的析构函数将释放该内存块,从而使另一个对象的sPtr没有定义,这种情况可能会引起严重的运行时错误。

第15行:

~String(); // destructor

声明了类 String 的析构函数(第74行定义)。该析构函数用 delelte 回收构造函数中用new为字符串分配的动态内存。

第16行:

const String &operator=(const String &); // assignment

声明了重载的赋值运算符函数operator=(第81行定义)。当编译器遇到像string1=string2这样的表达式时,就会生成函数调用:

string1.operator=(string2);

重载的赋值运算符函数operator测试了这种赋值是否为自我赋值(正如在复制构造函数中所做的那样)。如果是自我赋值运算,由于该对象已存在,函数就简单地返回。如果忽略自我赋值测试,那么函数就会立即释放目标对象所占用的空间,这样会丢失字符串。

假如不是自我赋值,那么函数就释放目标对象所占用的内存空间,将源对象中的length字段复制到目标对象并调用setString(第172行)为目标对象建立新空间,用assert测试new操作是否成功,最后用函数strcpy将源对象的字符串复制到目标对象中。不管上述赋值是否为自我赋值,函数都返回*this以确保可以连续赋值。

第17行:

const String &operator+=( const String & ); // concatenation

声明了重载的字符串连接运算符(第98行定义)。当编译器遇到main函数中的表达式s1+=s2时,生成函数调用s1.operator+=(s2)。函数operator+=建立一个临时指针,该指针用来存放当前对象的字符串指针,直到可以撤消该字符串的内存为止,该函数还计算了连接后的字符串长度,用new为字符串分配空间,用assert测试new操作是否成功。

如果成功,则用函数strcpy将原先的字符串复制到分配的空间中,然后用函数strcat将源对象的字符串连接到所分配的空间中,最后再用delele释放该对象原来的字符串占据的空间,返回*this作为String&以确保运算符+=可以连续执行。

连接String类型的对象和char*类型的对象不需要再重载一个连接运算符,const char*转换构造函数将传统的字符串转换为临时的String类型的对象,然后由该对象匹配现有的重载连接运算符。C++为实现匹配只能在一层之内执行这样的转换。

在执行内部类型和类之间的转换前,C++还能在内部类型之间执行编译器隐式定义的类型转换。注意,生成临时String对象时,调用转换构造函数和析构函数(见图8.5中s1 += "to you" 产生的输出)。这是隐式转换期间生成和删除临时类对象时向类客户隐藏的函数调用开销的一个例子。复制构造函数按值调用传递参数和按值返回类对象时也
产生类似开销。

性能提示 8.2
与先执行隐式类型转换然后再执行连接操作相比,使重载的连接运算符+=只有一个const char*类型参数的执行效率更高。隐式类型转换需要较少的代码,出错也较少。

第18行:

bool operator!()const; // is String empty?

声明了重载的取非运算符(第111行定义)。该运算符通常与字符串类一起使用,测试字符串是否为空。例如,当编译器遇到表达式!string1时,就会生成函数调用:

strlng1.operator!()

该函数仅仅返回length是否等于0的测试结果:

代码行:

bool operator ==( const String & ) cOnst; // test s1 == s2
bool operator<( const String & ) const; // test s1 < s2

为类String声明了重载的相等运算符(第114行定义)和关系运算符(第ll8行定义)。其工作原理是相似的,因此我们只以重载的运算符==为例。当编译器遇到表达式string1==string2时,就会生成如下的函数调用:

string1.operator==(string2)

如果 string1 等于 string2,则返回true。上述运算符都用函数strcmp比较String对象中的字符串。注意我们使用C语言标准库中的函数strcmp。许多C++程序员提倡用一些重载运算符函数实现另外一些重载运算符函数,因此!=、>、<=和>=运算符都可以用operator==和operator<实现(第23行到第36行)。例如,重载函数operator>=在头文件中的实现(第33行)如下所示:

bool String::operator>=(const String &right) const
{ return ! ( *this<right );}

上述operator>=定义用重载的运算符<确定一个String对象是否大于或等于另一个String对象。注意!=、>、<=和>=运算符函数都在头文件中定义。编译器将这些定义内联起来,消除多余函数调用的开销。

软件工程视点8. 8
通过用前面定义的成员函数实现成员函数,程序员复用代码,从而减少要编写的代码量。

第38行到第39行:

char &operator[](int); // subscript operator
const char &operator[](int) const; // subscript operator

声明了重载的下标运算符(在第122行和第131行定义)。一个用于 const String,一个用于非 const String。当编译器遇到string1[O]这样的表达式时,就会生成函数调用string1,operator[](O)(根据String是否为const类型而使用相应的 operator[] 版本)。

函数 operator[] 首先用assert检查下标范围。如果下标越界,则打印一个出错信息井使程序异常中止。如果下标没有越界,则非const版本的operator[]返回一个char&类型的值,它是对String对象相应字符的引用,可用作左值,修改String对象中指定的字符。而const版本的operator[]返回String对象的相应字符,这里char&可以作为右值,读取该字符值。

测试与调试提示 8.1
从String类的重载下标运算符返回 char 引用是危险的。例如,客户可以用这个引用在字符串中任何位置插入null终止符('\0')。

第40行:

String &operator()( int,int ); // return a substring

声明了重载的函数调用运算符(第141行定义)。在字符串类中,为了从 String 对象中选择一个子串,经常要重载该运算符。两个整数参数指定了所选定子串的起始位置和长度。如果起始位置越界或者子串长度为负,则发出错误信息。

习惯上,如果子串长度为 0,则选择的子串为从选定的开始位置一直到 String 对象的末尾。例如,假设 string1 是一个包含字符串 AEIOU 的 String 对象,当编译器遇到表达式 string1(2,2) 时,生成函数调用 string1.operator()(2, 2) 。执行该函数调用时,产生一个包含串 IO,的动态分配的新 String 对象,并返回对该对象的引用。

因为函数可能会有一个冗长而复杂的参数表,所以重载的函数调用运算符()可以有很强大的功能,从而可以完成很多有意义的操作。函数调用运算符的另外一个用途是用作数组的下标符号。例如,有的程序员不愿意用C的两个方括号表示二维数组(如a[b][c]),他们更喜欢重载函数调用运算符,用a[b][c])表示二维数组。只有当 函数名 是类 String 的对象时才能使用该运算符。

第41行:

int getLength()const; // return string length

声明了返回 String 对象长度的函数。该函数(第 168 行定义)是通过返回类 String 的 private 数据值而获得字符串的长度。

读者现在应该深入到 main 函数的代码中,研究输出结果,了解每种重载运算符的用法。