待定
本章内容:
运算符重载
友元函数
重载<<运算符,以便用于输出
状态成员
使用rand()生成随机值
类的自动转换和强制类型转换
类转换函数
11.1 运算符重载
运算符重载是一种形式的C++多态(函数重载(多态)也是),将重载的概念扩展到运算符上。例如将*运算符用于地址将得到存储在整个地址中的值;用于两个数字时,得到它们的乘积。C++根据操作数的数目和类型来决定采用哪种操作。
C++允许将运算符重载扩展到用户定义的类型,比如使用+
将两个对象相加,编译器将根据操作数的数目和类型决定使用哪种加法。
对于两个数组相加,常规实现:
for(int i = 0; i < 20; i++)
evening[i] = sam[i] + janet[i];
对此,C++可定义一个表示数组的类,再重载+
运算符来实现:
evening = sam + janet;
运算符重载方法:需使用被称为运算符函数的特殊函数形式,运算符函数格式:
operatorop(argument-list)
//重载+运算符
operator+():
//重载*运算符
operator*():
op
必须是有效的C++运算符(operator@()
则不合法)。
假设district2
、sid
和sara
都是Salesperson类对象,为其定义一个operator+()
成员函数重载+
运算符以便实现将两个Salesperson对象的销售额相加,便可编写如下代码:
district2 = sid + sara;
编译器读取到操作数是Salesperson类对象,便会调用相应的运算符函数替换上述运算符:
distric2 = sid.operator+(sara);
至此,便可以使用简便的+运算符表示法替换掉函数表示法。
11.2 运算符重载示例
原函数:
Time Sum(const Time &t) const
Time Time::Sum(const Time &t) const
{
Time sum;
sum.minutes = minutes + t.minites;
sum.hours = hours + t.hours + sum.minutes / 60;
sum.minutes %= 60;
return sum;
}
重载加法运算符,只需要将Sum()
名称改为operator+()
即可:
Time operator+(const Time &t) const;
Time Time:: operator+ (const Time &t) const
{
Time sum;
sum.minutes = minutes + t.minites;
sum.hours = hours + t.hours + sum.minutes / 60;
sum.minutes %= 60;
return sum;
}
上述代码参数是引用,但返回类型不是引用。将参数声明为引用的目的是为提高效率,且使用内存更少;但返回值不能是引用(若是引用会报警告返回类型指向的是局部变量),sum为局部变量,在函数结束时将被删除,该引用将指向一个不存在的对象。
警告:不要返回指向局部变量或临时对象的引用。函数执行完毕后,局部变量和临时对象将消失,引用将指向不存在的数据。
重载+运算符后可像调用Sum()
那样调用operator+()
方法,但将该方法命名为operator+()
后也可以使用+
运算符表示:
total = coding.operator+(fixing); //函数表示法
等价于:
total = coding + fixing; //运算符表示法
也适用于连续相加:
total = coding +fixing + jinx; 等价于 total = coding.operator+(fixing + jinx);
进一步等价于:
total = coding.operator+(fixing.operator+(jinx));
11.2.2 重载限制
- 重载后的运算符必须至少有一个操作数是用户定义的类型(防止用户为标准类型重载运算符),比如
+
不能定义为两个double
的差。 - 使用运算符不能违反原来的句话规则;不能修改运算符的优先级。比如不能将求模运算符(
%
)重载成使用一个操作数,“int x; % x
”——invalid
,求模对应两个参数,不能重载为一个操作数。 - 不能创建新的运算符。比如,不能定义
operator**()
表示求幂。 - 不能重载以下运算符:
sizeof | : 成员运算符 |
---|---|
* 成员指针运算符 | :: 作用域解析运算符 |
?: 条件运算符 | typeid 一个RTTI运算符 |
const_cast 、dynamic_cast 、reinterpret_cast 、static_cast :强制类型转换运算符 |
- 表11.1中大多数运算符都可以通过成员或非成员函数进行重载,但以下运算符只能通过成员函数重载:
=
:赋值运算符()
:函数调用运算符[]
:下标运算符->
:通过指针访问类成员的运算符
重载“+
”代码示例:
class Num
{
private: ///public:///
int n;
public:
Num();
Num(int m);
Num operator+(const Num &t) const; //成员函数重载“+”
void Show() const;
};
Num::Num(){n = 0;}
Num::Num(int m){n = m;}
//成员函数重载“+”:
Num Num::operator+(const Num &t) const
{
Num temp;
temp.n = n + t.n;
return temp;
}
///
//非成员函数重载“+”:
//此处不能加第二个const,因为其非成员函数。
Num operator+(const Num &m. const Num &l)
{
Num temp;
temp.n = m.n + l.n;//会报错,n为私有的;除非把n改为public
return temp;
}
///
void Num::Show() const
{
cout << “ n = ” << n << endl;
}
int main(void)
{
Num a(10);
a.Show;
Num b(20);
b.Show;
Num d;
d = a + b;
return 0;
}
上述代码中:///
为不同处代码。
补充:为何只能通过成员函数赋值。
以上代码中如果没有成员函数对=进行重载,而是一个非成员函数(全局的)对=
进行重载,此时构造函数和非成员函数都可以实现赋值运算符的重载,编译器编译时则会产生矛盾不知道该调用哪个函数便会报错。+运算符不会有这种问题,是因为+
不会触发调用构造函数。
11.2.3 其他重载运算符
运算符*
重载代码:
Time Time::operator*(double mult) const
{
Time result;
long totalminutes = hourse * mult * 60 + minutes * mult;
result.hours = totalminutes / 60;
result.minutes = totalminutes % 60;
return result;
}
//调用:
A = B * 2.75; //valid,等价于 A = B.operator*(2.75),运算符左侧应该是调用对象
A = 2.75 * B; //会报错,没有与之对应的成员函数,2.75不是对象
解决上述问题有两种方式:
告知书写规则,只能按照 B * 2.75
这种格式书写
非成员函数(大多数运算符都可以通过成员或非成员函数来重载)
非成员函数不由对象调用,其使用的所有值(包括对象)都是显示参数,上式便可写为:
A = operator*(2.75, B);
//对应函数原型:
Time operator*(double m, const Time &t);
//具体实现,不再是成员函数不能用const:
Time operator*(double mult, const Time &t)
{
Time result;
long totalminutes = t.hourse * mult * 60 + t.minutes * mult;
result.hours = totalminutes / 60;
result.minutes = totalminutes % 60;
return result;
}
上述代码引发了一个新的问题——非成员函数不能直接访问累的私有数据,改进代码:
Time operator*(double mult, const Time &t)
{
return t * result; //此处调用重载*的成员函数
}
事实上,有一类特殊的非成员函数可以访问类的私有成员,即友元函数。
11.3友元
11.3.1 创建友元
第一步:函数声明
将函数原型放在类声明中,并在原型声明前加上关键字friend
,格式如下:
friend Time operator*(double m, const Time &t);
该原型表示:
虽然operator*()
是在类声明中声明的,但它不是成员函数,因此不能使用成员运算符来调用;
虽然operator*()
不是成员函数,但它与成员函数的访问权限相同。
第二步:编写函数定义
它并非成员函数所以不能使用Time::
限定符,且也不能使用关键字friend
,格式如下:
Time operator*(double mult, const Time &t) //没有freind
{
Time result;
long totalminutes = t.hourse * mult * 60 + t.minutes * mult;
result.hours = totalminutes / 60;
result.minutes = totalminutes % 60;
return result;
}
建议写非成员函数时尽量都定义为友元函数,此为最标准的写法。
提示:如果要为类重载运算符,并将非类的项作为其第一个操作数,则可用友元函数来反转操作数的顺序。
11.3.2 常用的友元:重载<<运算符
假设trip
为一个Time对象,可通过show()
来显示Time的值,如何通过“ cout << trip;
”来显示其值。
cout
是一个ostream对象,能够识别所有的C++基本类型,因为对于每种基本类型ostream类声明中都包含了相应的重载的operator<<()
定义。
因此要使cout
能够识别Time对象:
一种方法是将一个新的函数运算符定义添加到ostream类声明中,但ostream已经是一个成熟的接口,因此尽量不要修改ostream,会在标准接口上浪费时间;
另一种是通过Time类重载<<
。
1.<<
的第一种重载版本
要使Time类知道使用cout
,必须使用友元函数。因为如果是成员函数重载<<
,意味着Time对将是第一个操作数,即必须通过“ trip << cout;
”来使用<<
。这使得代码看起来很怪异。但通过友元函数,即可解决此问题,重载函数:
void operator<<(ostream &os, const Time &t)
{
os << t.hours << “hours, ” << t.minutes << “minutes”;
}
//调用:
cout << trip; //输出:4 hours,23 minutes
上述代码operator<<()
函数是Time的友元而非ostream的友元。可通过查看该函数里的操作数到底调用的是哪个类的操作数,即为该类的友元函数。
2.<<
的第二种重载版本
上述介绍并不能实现<<
的连续使用,比如:
cout << x << y;
上述代码实质等同于“ (cout << x) << y;
”(C++从左至右读取输出语句),意味着返回的(cout << x
)亦是一个cout
(ostream),因此只需要修改operator<<()
,使其返回ostream对象的引用即可:
friend ostream &operator<<(ostream &os, const Time &t);
ostream &operator<<(ostream &os, const Time &t)
{
os << t.hours << “hours, ” << t.minutes << “minutes”;
}
此时的调用“ cout << trip
”等价于“ operator<<(cout, trip)
”,其返回的是cout对象,因此可继续“ <<
”,比如:
cout << trip << “ (Tuesday)\n”; //cout << trip返回一个cout,即cout << “ (Tuesday)\n”;
11.4 重载运算符:作为成员函数还是非成员函数
大多数运算符既可使用成员函数也可使用非成员函数来实现运算符重载,但一般来说,非成员函数应是友元函数,这样它才可以直接访问类的私有数据。
//成员函数:
Time operator+(const Time &t) const;
//友元函数(非成员):
friend Time operator+(const Time &a, const Time &b);
加法运算符需要两个操作数。成员函数一个通过this指针
隐式传递,另一个作为函数参数显示传递;友元函数则是两个都作为参数显示传递。
注意:非成员函数的重载运算符函数所需的形参数目与运算符使用的操作数数目相同,成员函数则少一个(this
隐式传递);在定义运算符时只能选择其中一种格式而不能同时都选择,否则会发生二义性错误导致编译错误。
11.5 再谈重载:一个矢量类
使用了运算符重载和友元的类设计,一个表示矢量的类。
11.5.2 为vector类重载算数运算符
代码:
此时可用构造函数来替代上述代码,更简单可靠:
Vector Vector::operator+(const Vector &b) const
{
return Vector(x + b.x, y + b,y); //返回构造函数
}
上述代码将新的x
和y
传递给Vector
构造函数,再进一步创建无名的新对象并返回该对象的副本。
提示:如果方法通过计算得到一个新的类对象,则应该考虑是否可以使用类构造函数来完成这种工作,这样做不仅可以避免麻烦,而且可以确保新的对象按照正确的方式创建。
11.6 类的自动转换和强制类型转换
C++内置类型转换:将一个标准类型变量的值赋给另一种标准类型的变量时,如果两种类型兼容,则C++自动将这个值转换为接收变量的类型。
11.6.1 转换函数
可以将数字转换为类对象(类的构造函数存在形参只有一个数字的情况),要进行相反的转换时需使用特殊的C++运算符函数——转换函数。构造函数只适用于从某种类型到类类型的转换。
例如:
Stonewt wolfe(285.7);
double host = wolfe; //invalid
double host = double (wolfe); //或
double thinker = (double) wolfe;
而对于如下代码:
Stonewt wells(20, 3);
double star = wells;
此时左侧是double
类型,右侧是Stonewt类型,编译器会查看是否定义了与此匹配的转换函数,如果没有,将生成错误信息——无法将Stonewt赋给double
。
创建转换函数格式:
operator typeName();
注意:
转换函数必须是类方法
转换函数不能指定返回类型
转换函数不能有参数
例如上例,转换为double/int
类型的函数原型如下:
operator double(); //需要添加到类声明中
operator int();
double
(即typeName
)表示要转换成的类型,因此无需指定返回类型;转换函数是类方法意味着需要通过类对象调用,从而告知函数要转换的值,因此不需要参数。
对应的函数定义:
Stonewt::operator double() const
{
return pounds;
}
Stonewt::operator int() const
{
return int(pounds + 0.5); //四舍五入
}
调用:
Stonewt poppings(9, 2.8);
double p_wt = poppings; //隐式转换(double poppings 显式)
Stonewt poppings(9, 2.8);
cout << int (popings);
上述p_wt
代码在执行时,左侧为double
类型,编译器便会寻找转换后类型为double()
的转换函数。同理int
。
警告:谨慎选择隐式转换函数,对于“ cout << popings
”,int和double
不唯一,会产生二义性错误。最好使用显式转换,且选择仅在被显示地调用才会执行的函数。
当类定义两种或更多的转换时,仍可以用显式强制转换类型指出要使用哪个转换函数。
转换函数缺点:在用户不希望进行转换时,转换函数也可能进行转换。C++11中可使用关键字explicit将运算符声明为显式的:
class Stonewt
{
…
explicit operator int() const;
explicit operator double() const;
}
此时在需要强制转换时便只能显式地调用这些运算符。
总结,C++为类提供如下类型转换:
只有一个参数的类构造函数用于将类型与该参数相同的值转换为类类型。例如(Stonewt(int a)
)在构造函数声明中使用explicit
可防止隐式转换,而值允许显示转换。
被称为转换函数的特殊类成员运算符函数用于将类对象转换为其他类型。转换函数时类成员,没有返回类型和参数,形如“ operator typeName()
”,typeName
为转换成或转换后的类型。将类对象赋给typeName
变量或将其强制转换为typeName
类型时将自动调用该函数。