第 8 章 使用类
文章目录
8.1 运算符重载
在之前的部分中已经介绍过函数重载,而 C++ 将重载的概念扩展到了运算符上,允许赋予 C++ 运算符多种含义。实际上,很多 C++ 运算符已经被重载。例如,将 *
运算符用于地址,将得到储存在这个地址里的值;但它用于两个数字时,将得到它们的乘积。C++ 根据操作数的数目和类型来决定采用哪种操作。
C++ 允许将运算符重载扩展到用户定义的类型,例如允许使用 +
将两个对象相加。要重载运算符,需使用被称为运算符函数的特殊函数形式,其格式如下:
operator op(argument-list);
例如,operator+()
将重载 +
运算符,operator*()
重载 *
运算符。op
必须是有效的 C++ 运算符,不能虚构一个新的符号。例如,不能有 operator@()
这样的函数。
8.2 计算时间:一个运算符重载的示例
下面定义一个处理时间的类:
mytime0.h
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 Sum(const Time& t) const;
void Show() const;
};
mytime0.cpp
#include "Time.h"
#include <iostream>
Time::Time()
{
hours = minutes = 0;
}
Time::Time(int h, int m = 0)
{
hours = h;
minutes = m;
}
void Time::AddMin(int m)
{
minutes += m;
hours += minutes / 60;
minutes %= 60;
}
void Time::AddHr(int h)
{
hours += h;
}
void Time::Reset(int h = 0, int m = 0)
{
hours = h;
minutes = m;
}
Time Time::Sum(const Time& t) const
{
Time sum;
sum.minutes = minutes + t.minutes;
sum.hours = hours + t.hours + sum.minutes / 60;
sum.minutes %= 60;
return sum;
}
void Time::Show() const
{
std::cout << hours << " hours, " << minutes << " minutes\n";
}
8.2.1 重载加法
上述代码中,使用 Sum()
函数来对两个对象进行相加,我们也可以重载运算符,只需要将 Sum()
的名称改成 operator+()
即可,如下:
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 total,a,b;
total = a.operator+(b);
total = a + b;
这两种表示法都将调用 operator+()
方法。注意,在运算符表示法 a + b
中,运算符左侧的对象是调用对象,运算符右边的对象是作为参数被传递的对象。
还可以将两个以上的对象相加,如:
Time a, b, c, d;
d = a + b + c;
//首先转换为 d = a.operator+(b + c)
//然后转换为 d = a.operator+(b.operator+(c))
8.2.2 重载限制
多数 C++ 运算符都可以用这样的方式重载。重载的运算符不一定必须是成员函数,但必须至少有一个操作数是用户定义的类型。下面详细介绍 C++ 对用户定义的运算符重载的限制:
-
重载后的运算符必须至少有一个操作数是用户定义的类型,这将防止用户为标准类型重载运算符。因此,不能将
-
重载为计算两个double
值的和,而不是它们的差。 -
使用运算符时不能违反运算符原来的句法规则。例如,不能将求模运算符重载成只使用一个操作数:
int x; Time shiva; % x; //error % shiva; //error
同样,不能修改运算符的优先级。因此,如果将加法运算符重载成将两个类相加,则新的运算符与原来的加号具有相同的优先级。
-
不能创建新的运算符。例如,不能定义
operator**()
函数来表示幂。 -
不能重载以下运算符
sizeof
.
(成员运算符).*
(成员指针运算符)::
(作用域解析运算符)?:
(条件运算符)typeid
(一个 RTTI 运算符)const_cast、dynamic_cast、reinterpret_cast、static_cast
(强制类型转换运算符)
-
下表中大多数运算符都可以通过成员或非成员函数进行重载,但
=、()、[]、->
只能通过成员函数进行承载。+ - * / % ^ & | ~= ! = < > += -= *= /= %= ^= &= |= << >> >>= <<= == != <= >= && || ++ – , ->*
->
() [] new delete new [] delete []
8.3 友元
C++ 控制对类对象私有部分的访问。通常,公有类方法提供唯一的访问途径,但有时候这种限制太严格,以至于不适合特定的编程问题。在这种情况下,C++ 提供了另一种形式的访问权限:友元。友元有以下三种:友元函数、友元类、友元成员函数。
通过让函数成为类的友元,可以赋予该函数与类的成员函数相同的访问权限。下面先介绍友元函数,其他两种友元将在以后介绍。
首先看下面一种情况:
Time Time::operator*(double n) const
{
Time res;
long totalmin = hours * n * 60 + minutes * n;
res.hours = totalmin / 60;
res.minutes = totalmin % 60;
return res;
}
该函数重载了乘法,重载该运算符与之前重载加法不太相同,因为它使用了两种不同的类型。这限制了该运算符的使用方式,例如:
A = B * 2.75; //正确,可以转换为 B.operator*(2.75)
A = 2.75 * B; //错误!2.75.operator*(B) 是错误表达式
从概念上说,2.75 * B
和 B * 2.75
应该相同,但前者不对应于成员函数,因为 2.75
不是 Time
类对象。记住,左侧的操作数应是调用成员函数的对象。为了解决这个问题,需要使用一类特殊的非成员函数——友元函数。它们可以直接访问类的私有成员。
8.3.1 创建友元
创建友元函数的第一步是将其原型放在类声明中,并在原型前面加上关键字 friend
:
friend Time operator*(double m, const Time & t);
该原型意味着以下两点:
- 虽然
operator*()
函数是在类声明中声明的,但它不是成员函数,因此不能用成员运算符来调用; - 虽然
operator*()
不是成员函数,但它与成员函数的访问权限相同。
第二步是编写函数定义。因为它不是成员函数,所以不需要使用 Time::
限定符。另外,不要在定义中使用关键字 friend
,定义应如下:
Time operator*(double n, const Time& t)
{
Time res;
long totalmin = t.hours * n * 60 + t.minutes * n;
res.hours = totalmin / 60;
res.minutes = totalmin % 60;
return res;
}
有了上述声明和定义后,语句 2.75 * B
将转换为 operator*(2.75, B)
。
总之,类的友元函数是非成员函数,其访问权限与成员函数相同。
8.3.2 常用的友元:重载 <<
运算符
一个很有用的类特性是重载 <<
运算符,使之能与 cout
一起来显示对象的内容。下面以 Time
类为例介绍此特性。
要使 Time
类知道使用 cout
,必须使用友元函数,因为 cout << Time对象
语句中第一个对象是 ostream
类对象 cout
;如果使用一个 Time
成员函数来重载 <<
,Time
对象将是第一个操作数,这意味着必须这样使用:Time对象 << cout;
。
这样会令人困惑,但通过友元函数,可以像下面这样重载运算符:
ostream & operator<<(ostream & os, const Time & t)
{
os << t.hours<<" hours, " << t.minutes << " minutes";
return os;
}
其中,调用 cout<<trip
应使用 cout
对象本身,而不是它的拷贝,因此该函数按引用传递对象。
此外,函数通过返回 ostream &
,使得可以按照以下方式使用 cout
:
cout << "Trip time: " << Time对象 << "\n";
之所以可以这样做,是因为 ostream
类将 operator<<()
实现返回一个指向 ostream
对象的引用。具体地说,它返回一个指向调用对象(这里指 cout
)的引用。因此,表达式 (cout << x)
本身就是 ostream
对象 cout
,从而可以像上面这样使用 cout
。如果将函数返回类型声明为 void
,则上述语句将会报错。
有趣的是,这个 operator<<()
版本还可用于 ofstream
对象。正如之前所指出的,类继承属性让 ostream
引用能够指向 ostream
对象和 ofstream
对象。
提示:只有在类声明中才能使用
friend
关键字。除非函数定义也是原型,否则不能在函数定义中使用该关键字。
8.4 类的自动转换和强制类型转换
可以将类定义成与基本类型或另一个类相关,使得从一种类型转换为另一种类型是有意义的。在这种情况下,程序员可以指示 C++ 如何自动进行切换,或通过强制类型转换来完成。为了说明这是如何进行的,以下面表示磅和英石的类来说明:
stonewt.h
#ifndef STONEWT_H_
#define STONEWT_H_
class Stonewt
{
private:
enum { Lbs_per_stn = 14 };
int stone;
double pds_left;
double pounds;
public:
Stonewt(double lbs);
Stonewt(int stn, double lbs);
Stonewt();
~Stonewt();
void show_lbs() const;
void show_stn() const;
};
#endif
stonewt.cpp
#include "stonewt.h"
#include <iostream>
using std::cout;
Stonewt::Stonewt(double lbs)
{
stone = int(lbs) / Lbs_per_stn;
pds_left = int(lbs) % Lbs_per_stn + lbs - int(lbs);
pounds = lbs;
}
Stonewt::Stonewt(int stn, double lbs)
{
stone = stn;
pds_left = lbs;
pounds = stn * Lbs_per_stn + lbs;
}
Stonewt::Stonewt()
{
stone = pounds = pds_left = 0;
}
Stonewt::~Stonewt()
{
}
void Stonewt::show_lbs() const
{
cout << stone << " stone, " << pds_left << " pounds\n";
}
void Stonewt::show_stn() const
{
cout << pounds << " pounds\n";
}
在 C++ 中,接受一个参数的构造函数可以实现将其他类型转换为类。在上面的程序中,构造函数 Stonewt(double lbs)
可以用于将 double
类型的值转换为 Stonewt
类型,也就是说,可以这样写代码:
Stonewt myCat = 19.6;
程序将使用该构造函数创建一个临时的 Stonewt
对象,并将
19.6
19.6
19.6 作为初始化值,随后将内容复制到 myCat
中。这一过程称为隐式转换,因为它是自动进行的,而不需要显式强制类型转换。
只有接受一个参数的构造函数才能作为转换函数,因此函数 Stonewt(int stn, double lbs)
不能用来转换类型。然而,如果为第二个参数提供默认值,则其可以用来转换 int
类型。
将构造函数用作自动类型转换函数似乎是一项不错的特性,但这种自动特性可能会导致意外的类型转换。因此,C++ 新增了关键字 explicit
用于关闭这种特性。也就是说,可以这样声明构造函数:
explicit Stonewt(double lbs);
这将关闭隐式转换,但仍允许显式转换:
Stonewt myCat;
myCat = 19.6; //error,不允许隐式转换
myCat = Stonewt(19.6);
myCat = (Stonewt)19.6;
注意:只接受一个参数的构造函数定义了从参数类型到类的转换。如果使用
explicit
限定构造函数,则它只能用于显式转换,否则也可用于隐式转换。
当使用可以转换为 double
类型的内置类型时,也能进行隐式转换,例如:Stonewt a = 1;
。该语句中 1
首先将从 int
转换为 double
类型,再转换为 Stonewt
类型。然而,当且仅当这种转换不存在二义性时,才会进行这种二步转换。也就是说,如果还定义了 Stonewt(long)
,则编译器将拒绝这些语句,并指出:int
可被转换为 long
或 double
,因此调用存在二义性。
8.4.1 转换函数
使用构造函数可以将 double
转换为 Stonewt
类型,是否可以做相反的转换呢?
可以这样做,但不是使用构造函数。构造函数只用于从某种类型到类的转换。要进行相反的转换,必须使用特殊的 C++ 运算符函数——转换函数。转换函数是用户定义的强制类型转换,可以像使用强制类型转换那样使用它们。例如,如果定义了 Stonewt
到 double
的转换函数,就可以使用下面的转换函数:
Stonewt wolfe(285.7);
double host = double(wolfe); //显式转换
double thinker = (double)wolfe; //显式转换
double star = wolfe; //隐式转换
那么,应该如何创建转换函数呢?要转换为 typeName
类型,需要使用这种形式的转换函数:
operator typeName();
注意:
- 转换函数必须是类方法
- 转换函数不能有返回类型
- 转换函数不能有参数
例如,转换为 double
类型的函数原型如下:
operator double();
添加转换函数后,Stonewt
类定义如下:
stonewt1.h
#ifndef STONEWT_H_
#define STONEWT_H_
class Stonewt
{
private:
enum { Lbs_per_stn = 14 };
int stone;
double pds_left;
double pounds;
public:
Stonewt(double lbs);
Stonewt(int stn, double lbs);
Stonewt();
~Stonewt();
void show_lbs() const;
void show_stn() const;
operator int() const;
operator double() const;
};
#endif
stonewt1.cpp
...
Stonewt::operator int() const
{
return int(pounds + 0.5); //返回pound四舍五入的整数值
}
Stonewt::operator double() const
{
return pounds;
}
原则上说,最好使用显式转换,因为隐式转换可能会在用户不希望进行转换时自动进行转换。在 C++11 中,可将转换函数声明为显式的,这样只有在强制转换时才会调用转换函数:
explicit operator int() const;
explicit operator double() const;
另一种可以避免隐式转换的方法是,用一个功能相同的非转换函数替换该转换函数即可,但仅在被显式地调用时,该函数才会执行。也就是说,可以将:
Stonewt::operator int() {...}
替换为:
int Stonewt::Stone_to_Int() {...}
这样,下面的语句将是非法的:
Stonewt a;
int b = a; //error!
但如果确实需要这种转换,可以这样做:
Stonewt a;
int b = a.Stone_to_Int();
警告:应谨慎地使用隐式转换函数。通常,最好选择仅在被显示地调用时才会执行的函数。
总之,C++ 为类提供了下面的类型转换:
- 只有一个参数的类构造函数用于将类型与该参数相同的值转换为类类型。例如,将
int
值赋给Stonewt
对象时,接受int
参数的类构造函数将自动被调用。然而,在构造函数声明中使用explicit
可防止隐式转换,而只允许显式转换。 - 被称为转换函数的特殊类成员函数用于将类对象转换为其他类型。转换函数是类成员,没有返回类型、没有参数、名为
operator typeName()
,其中typeName
是对象将被转换成的类型。将类对象赋给typeName
类型的变量或者将其强制转换为typeName
类型时,该转换函数将自动被调用。