ITEEDU

8.9 类型之间的转换

大多数程序能处理各种数据类型的信息。有时候所有的操作还会集中于某一种类型上,例如,整数加整数还是整数(只要结果不是太大,能用整数表示出来)。但是,常常需要将一种类型的数据转换为另外一种类型的数据,赋值、计算、给函数传值以及从函数返回值都可能会发生这种情况。对于内部的类到,编译器知道如何转换类型。程序员也可以用强制类型转换运算符实现内部类型之间的强制转换。

但是怎样转换用户自定义类型呢编译器不知道怎样实现用户自定义类型和内部类型之间的转换,程序员必须明确地指明如何转换。这种转换可以用转换构造函数实现,也就是使用单个参数的构造函数,这种函数仅仅把其他类型(包括内部类型)的对象转换为某个特定类的对象。本章梢后要用一个转换构造函数把正常的char*类型的字符串转换为类Siring的对象。

转换运算符(也称为强制类型转换运算符)可以把一种类的对象转换为其他类的对象或内部类型的对象。这种运算符必须是一个非static成员函数,而不能是友元函数。

函数原型:

A::operator char *() const;

声明了一个重载的强制类型转换运算符函数,它根据用户自定义类型A的对象建立一个临时的char*类型的对象。重载的强制类型转换运算符函数不能指定返回类型(返回类型是要转换后的对象类型)。如果s是某个类对象,当编译器遇到表达式(char*),时,会产生函数调用s.operator char*(),操作数s是调用成员函数operator char*的类对象s。

为了把用户自定义类型的对象转换为内部类型的的对象或用户自定义的其他类型的对象,我们可以定义重载的强制类型转换运算符函数。函数原型:

A::operator int()const;

A::operator otherClass()const;

声明了两个重载的强制类型转换运算符函数,分别用来把用户自定义类型A的对象转换为一个整数和用户自定义类型otherClass的对象。

强制类型转换运算符和转换构造函数一个很好的特点就是:当需要的时候,编译器可以为建立一个临时对象而自动地调用这些函数。例如,如果用户自定义的类String的某个对象s出现在程序中需要使用char*类型的对象的位置上,例如:

cout << s;

编译器调用重载的强制类型转换运算符函数operator char*将对象转换为char*类型,并在表达式中使用转换后的char*类型的结果。String类提供该转换运算符后,不需要重载流插入运算符用cout输出String。

8.10 实例研究:String类

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

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

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

// Fig. 8.5: string1.h
// Definitien of a String class
#ifndef STRING1_H
#define STRING1_H
#include <iostream.h>
class String {
friend ostream &operator<<( ostream &, const String & );
friend istream &operator>>( istream &, String & );
public:
String( const char * ="" ); // conversion/default ctor
String( const String & ); // copy constructor
~String(); // destructor
const String &operator=( const String & ); // assignment
const String &operator+=( const String & ); // concatenation
bool operator!() const; // is String empty?
bool operator==( const String & ) const; // test sl - s2
bool operator<( coost String & ) const; // test sl < s2
// test s1 != s2
bool operator!=( const String & right ) const
{ return !( *this == right ); }
// test si > S2
bool operator>( const String &right ) const
{ return right < *this; }
// test s1 <= s2
bool operator<=( const String &right ) const
{ return !( right < *this ); (
// test s1 >= s2
bool operator>=( const String &right ) const
{ return !( *this < right ); }
char &operator[] ( int ); // subscript operator
const char &operator[]( int ) const; // subscript operator
String &operator()( int, int ); // return a substring
int getLength() const; // return string length
private:
int length; // string length
char *sPtr; // pointer to start of string
void setString( const char * ); // utility function
};
#endif
// Fig. 8.5: string1.cpp
// Member function definitions for class String
#include <iostream.h>
#include <iomanip.h>
#include <string.h>
#include "string1.h"
// Conversion constructor: Convert char * to String
String::String( const char *s ) : length( strlen( s ))
{
cout << "Conversion constructor: "<< s << '\n';
setString( s ); // call utility function
}
// Copy constructor
String::String( const String &copy ) : length( copy.length )
{
cout << "Copy constructor: " << copy.sPtr << '\n';
setString( copy.sPtr ); // call utility function
}
// Destructor
String::~string()
{
cout << "Destructor: "<< sPtr << '\n';
delete [] sPtr; // reclaim string
}
// Overloaded = operator; avoids self assignment
const String &String::operator=( const String &right )
{
cout << "operator= called\n";
if ( &right != this ) { // avoid self assignment
delete [] sPtr; // prevents memory leak
length = right.length; // new String length
setString( right.sPtr ); // call utility function
}
else
cout << "Attempted assignment of a String to itself\n";
return *this; // enables cascaded assignments
}
// Concatenate right operand to this object and
// store in this object.
const String &String::operator+=( const String &right )
{
char *tempPtr = sPtr; // hold to be able to delete
length += right.length; // new String length
sPtr= new char[ length + 1 ]; // create space
assert( sPtr != 0 ); // terminate if memory not allocated
strcpy( sPtr, tempPtr ); // left part of new String
strcat( sPtr, right.sPtr ); // right part of new String
delete [] tempPtr; // reclaim old space
return *this; // enables cascaded calls
}
// Is this String empty?
bool String::operator!() const { return length == 0; }
// Is this String equal to right String?
bool String::oprator==( const String &right ) const
{ return strcmp( sPtr, right.sPtr ) == 0; }
// Is this String less than right String?
bool String::oprator<( const String &right ) const
{ return strcmp( sPtr, right.sPtr ) < 0; }
// Return a reference to a character in a String as an lvalue.
char &String::operator[] ( int subscript )
{
// First test for subscript out of range
assert( subscript >= 0 && subscript < length );
return sPtr[ subscript ]; // creates lvalue
}
// Return a reference to a character in a String as an rvalue.
const char &String::oprator[]( int subscript ) const
{
// First test for subscript out of range
assert( subscript >= 0 && subscript < length );
return sPtr[ subscript ]; // creates rvalue
}
// Return a substring beginning at index and
// of length subLength as a reference to a String object.
String &String::operator()( int index, int subLength )
{
// ensure index is in range and substring length >= 0
assert( index >= 0 && index < length && subLength >= 0 );
String *subPtr = new String; // empty String
assert( subPtr != 0 ); // ensure new String allocated
// determine length of substring
if ( ( subLength == 0 ) || ( index + subLength > length ) )
subPtr->length = length - index + 1;
else
subPtr->length = subnength + 1;
// allocate memory for substring
delete subPtr->sPtr; // delete character array from object
subPtr->sPtr = new char [ subPtr->length ];
assert( subPtr -> sPtr != 0 ); // ensure space allocated
// copy substring into new String
strncpy( subPtr->sPtr, &sPt[ index ], subPtr->length );
subPtr->sPtr[ subPtr -> length ] = '\0'; // terminate String
return *subPtr; // return new String
}
// Return string length
int String::getLength() const { return length; }
// Utility function to be called by constructors and
// assignment operator.
void String::setString( const char *string2 )
{
sPtr = new char[ length + 1 ]; // allocate storage
assert( sPtr != 0 ); // terminate if memory not allocated
strcpy( sptr, string2 ); // copy literal to object
}
// Overloaded output operator
ostream &operator<<( ostream &output, const String &s )
{
output << s.sPtr;
return output; // enables cascading
}
// Overloaded input operator
istream &operator>>( istream &input,String &s )
{
char temp[ 100 ]; // buffer to store input
input >> setw( 100 ) >> temp;
s = temp; // use String class assignment operator
return input; // enables cascading
}
// Fig. 8.5:fig08 05.cpp
// Driver for class String
#include <iostream.h>
#include "string1.h"
int main()
{
String s1( "happy" ), s2( "birthday" ), s3;
// test overloaded equality and relational operators
cout << "s1 is \"" << s1 << "\"; s2 is \"" << s2
<< "\"; s3 is \"" << s3 << '\"'
<< "\nThe results of comparing s2 and s1:"
<< "\ns2 == s1 yields"
<< ( s2 == s1 ? "true" : "false" )
<< "\ns2 != s1 yields"
<< ( s2 != s1 ? "true" : "false" }
<< "\ns2 > s1 yields"
<< ( s2 > s1 ? "true" : "false" )
<< "\ns2 < s1 yields "
<< ( s2 < s1 ? "true" : "false" )
<< "\ns2 >= s1 yields
<< ( s2 >= s1 ? "true" : "false" )
<< "\ns2 <= s1 yields"
<< ( s2 <= s1 ? "true" : "false" );
// test overloaded String empty (!) operator
cout << "\n\nTesting !s3:\n";
if ( !s3 ) {
cout << "s3 is empty; assigning s1 to s3;\n";
s3 = s1; // test overloaded assignment
cout << "s3 is \"" << s3 << "\"";
}
// test overloaded String concatenation operator
cout << "\n\ns1 += s2 yields s1 = ";
s1 += s2; // test overloaded concatenation
cout << s1;
// test conversion constructor
cout << "\n\ns1 += \" to you\" yields\n";
s1 +=" to you"; // test conversion constructor
cout << "s1 = "<< s1 << "\n\n";
// test overloaded function call operator () for substring
cout << "The substring of s1 starting at\n"
<< "location 0 for 14 characters, s1(0, 14), is:\n"
<< s1( 0, 14 ) << "\n\n";
// test substring "to-end-of-String" option
cout << "The substring of s1 starting at\n"
<< "location 15, s1(15, 0), is:"
<< s1( 15, 0 ) << "\n\n"; // 0 is "to end of string"
// test copy constructor
String *s4Ptr = new String(s1);
cout << "*s4Ptr = "<< *s4Ptr << "\n\n";
// test assignment (=) operator with self-assignment
cout << "assigning *s4Ptr to *s4Ptr\n";
*s4Ptr = *s4Ptr; // test overloaded assignment
cout << "*s4Ptr = "<< *s4Ptr << '\n';
// test destructor
delete s4Ptr;
// test using subscript operator to create lvalue
s1[ 0 ] = 'H';
s1[ 6 ] = 'B';
cout << "\nsl after s1[ 0 ] = 'H' and s1[ 6 ] = 'B' is:"
<< s1 << "\n\n";
// test subscript out of range
cout << "Attempt to assign 'd' to s1[ 30 ] yields:" << endl;
s1[ 30 ] = 'd'; // ERROR: subscript out of range
return 0;
}

输出结果:

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函数的代码中,研究输出结果,了解每种重载运算符的用法。