第11章 使用类
1.运算符重载
下面是一个时间类,重载了运算符+,将两个Time对象相加:
class Time
{
private:
int hours;
int minutes;
public:
Time();
Time(int h, int m = 0);
void AddMin(int m);
void AddHr(int h);
void Reset(int h = 0, int m = 0);
Time operator+(const Time & t) const;
void Show() const;
};
...
Time Time::operator+(const Time & t) const
{
Time sum;
sum.minutes = minutes + t.minutes;
sum.hours = hours + t.hours + sum.minutes / 60;
sum.minutes %= 60;
return sum;
}
这样就可以对两个Time对象相加:
Time A, B, C;
C = A + B;
实际上是调用了operator + ()函数:
C = A.operator+(B);
2.运算符重载限制
C++对用户定义的运算符重载的限制:
(1)重载后的运算符必须至少有一个操作数是用户定义的类型,这将防止用户为标准类型重载运算符。
(2)使用运算符是不能违反运算符原来的句法规则。例如,不能将求模运算符(%)重载成使用一个操作数。
同样,不能修改运算符的优先级。
(3)不能创建新的运算符。
(4)不能重载下面的运算符:
- sizeof :sizeof运算符
- . 成员运算符
- .* 成员指针运算符
- :: 作用域解析运算符
- ?: 条件运算符
- typeid 一个RTTI运算符
- const_cast 强制类型转换运算符
- dynamic_cast 强制类型转换运算符
- reinterpret_cast 强制类型转换运算符
- static_cast 强制类型转换运算符
(5)表11.1中的大多数运算符都可以通过成员或非成员函数进行重载,但下面的运算符只能通过成员函数进行重载:
- = :赋值运算符
- () :函数调用运算符
- [] :下标运算符
- -> :通过指针访问类成员的运算符
+ | - | * | / | % | ^ |
---|---|---|---|---|---|
& | | | ~= | ! | = | < |
> | += | -= | *= | /= | %= |
^= | &= | |= | << | >> | >>= |
<<= | == | != | <= | >= | && |
|| | ++ | – | , | ->* | -> |
() | [] | new | delete | new[] | delete[] |
3.友元函数
对Time类重载乘法时会出现一个问题:
A = B * 2.75;
A = 2.75 * B;
从概念上说,2.75 * B应与B * 2.75相同,但前者不对应于成员函数,因为2.75不是Time类型的对象。因此,编译器不能使用成员函数调用来替换该表达式。
大多数运算符都可以通过成员或非成员函数来重载。这样,就可以将*重载为非成员函数来解决上面的问题:
Time operator*(double m, const Time & t);
对于非成员重载运算符函数来说,运算符表达式左边的操作数对应于运算符函数的第一个参数,运算符表达式右边的操作数对应与运算符函数的第二个参数。而原来的成员函数则按相反的顺序处理操作数。
使用非成员函数可以按所需的顺序获得操作数,但引发了一个新问题:非成员函数不能直接访问类的私有数据。然而,有一类特殊的非成员函数可以访问类的私有成员,它们被称为友元函数。
创建友元函数的第一步是将其原型放在类的声明中,并在原型声明前加上关键字friend:
friend Time operator*(double m, const Time & t);
该原型意味着下面两点:
- 虽然operator*()函数是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用。
- 虽然operator*()函数不是成员函数,但它与成员函数的访问权限相同。
第二步是编写函数定义。因为它不是成员函数,所以不要使用Time::限定符。另外,不要在定义中使用关键字friend:
Time operator*(double m, const Time & t)
{
Time result;
long totalminutes = t.hours * m * 60 + t.minutes * m;
result.hours = totalminutes / 60;
result.minutes = totalminutes % 60;
return result;
}
有了上述的声明和定义后,下面的语句:
A = 2.75 * B;
将转换为如下语句,从而调用刚才定义的非成员友元函数:
A = operator*(2.75, B);
也可以按照下面的方式将这个友元函数编写为非友元函数:
Time operator*(double m, const Time & t)
{
return t * m;
}
4.重载<<运算符
之前为了输出Time类对象的值,使用的是成员函数Show(),如果可以用下面的语句直接输出Time对象将更好:
Time trip;
cout << trip;
要使用上面的语句对Time类对象进行输出,就必须对<<运算符进行重载。而要像上面那样,必须使用友元函数,因为输出语句的第一个是ostream类对象(cout)。如果使用一个Time成员函数来重载<<,Time对象将是第一个操作数:
trip << cout;
这样会令人困惑。但通过使用友元函数,可以像这样重载运算符:
void operator<<(ostream & os, const Time & t)
{
os << t.hours << “ hours, ” << t.minutes << “ minutes”;
}
这样就可以使用下面的语句来输出数据:
cout << trip;
但是这样的实现不允许像通常那样将重新定义的<<运算符与cout一起使用:
cout << “Trip time: ” << trip << “ (Tuesday)\n”;
因为operator<<()函数没有返回值,而<<运算符要求左边是一个ostream对象。所以需要将友元函数operator<<()改为返回ostream对象的引用:
ostream & operator<<(ostream & os, const Time & t)
{
os << t.hours << “hours, ” << t.minutes << “ minutes”;
return os;
}
5.重载运算符应考虑的问题
如果要重载的某个运算符的两个操作数的顺序会影响函数的正确性,如Time类对象与double类型数的*运算。应将运算符重载为友元非成员函数。否则可以重载为成员函数。
重载运算符时要考虑多个运算符一起使用的情况,如之前的Time类对象的输出。
6.转换函数
接受一个参数的构造函数或只有一个非默认参数的构造函数可用于将类型与该参数相同的值转换为类的转换构造函数。因此,下面的构造函数可以用于将double类型的值转换为Stonewt类型:
Stonewt(double lbs);
也就是说,可以编写这样的代码进行隐式类型转换:
Stonewt myCat;
myCat = 19.6;
当然,C++还新增了关键字explicit,同于关闭这一种自动特性。也就是说,可以这样声明构造函数:
explicit Stonewt(double lbs);
这将关闭上述示例中介绍的隐式转换,但仍然允许显式转换,即显式强制转换:
Stonewt myCat;
myCat = (Stonewt) 19.6;
也就是说,在Stonewt(double)声明中使用了关键字explicit,则该构造函数只用于显式强制类型转换。否则,还可以用于下面的隐式转换:
- 将Stonewt对象初始化为double值时。
- 将double值赋给Stonewt对象时。
- 将double值传递给接受Stonewt参数的函数时。
- 返回值被声明为Stonewt的函数试图返回double值时。
- 在上述任意一种情况下,使用可转换为double类型的内置类型时。
然而,如果这个类还定义了构造函数Stonewt(long),则编译器将拒绝隐式转换,因为int可被转换为long或double,调用存在二义性。
上面是将double值转换为Stonewt对象,那么怎么进行相反的转化?也就是说,怎么将Stonewt对象转换为double值呢?这就需要用户自己定义转换函数。
要转换为typeName类型,需要使用这种形式的转换函数:
operator typeName();
注意:
- 转换函数必须是类方法;
- 转换函数不能制定返回类型;
- 转换函数不能有参数。
例如,Stonewt对象转换为int类型的函数原型和定义如下:
operator int() const;
Stonewt::operator int() const
{
return int(pounds + 0.5);
}
这样就可以用下面的语句进行类型转换:
Stonewt poppins(9,2.8);
int a = poppins; //隐式转换
int b = int(poppins) //显式转换
和转换构造函数一样,转换函数也有其优缺点。在用户不希望进行转换时,转换函数也可能进行转换。因此,可以使用关键字explicit将转换函数声明为显式的:
explicit operator int() const;
应谨慎地使用隐式转换函数。通常,最好选择仅在被显式地调用时才会执行的函数。