概要
新年伊始,闲来无事,于故纸堆中翻到了《C++ Primer Plus》,正好有些许概念仍不清楚,所以在读完书后想写一点笔记帮助记忆。
关于运算符重载,这应该是一个很简单的概念,而友元也算不上多么复杂,但是当这两者碰撞在一起的时候,就诞生一个不好选择的十字路口:重载应该采用什么样的设计模式,是成员函数,非成员函数抑或友元函数?
基本的重载实现
假设现在有一个类的实现是这样的(省去构造函数和析构函数等不必要的内容):
// code Fragment 0
class person
{
public:
double cash; // 表示个人持有的资金
};
现在将这个类实例化,得到Tom
和John
两个类。若需要计算Tom和John二人手中共持有多少资金,最简单的方法是total = Tom.cash + John.cash
,这样写虽然直观但是无形中加大了开发成本,这种情况下可以(但不是必须)使用重载+
运算符的方式简化代码。
- 使用成员函数的重载
如此只需调用// codeFragment 1 class person { public: double cash; // 表示个人持有的资金 double operator+(const person& s) // 重载之后的+运算符 { return cash + s.cash; } }
total = Tom + John
即可完成运算 - 非成员函数的重载
这种写法也可以实现// code Fragment 2 class person { public: double cash; // 表示个人持有的资金 }; // 重载的+运算符 double operator+(const person& s1, const person& s2) { return s1.cash + s2.cash; }
total = Tom + John
的效果,但是这样的重载函数只能访问类的公开成员,如果cash
是person
类的私有成员变量,则无法访问,编译不能通过
运算的次序问题
重载的本质依然是函数的调用,在C++中,函数的调用必须遵守参数的顺序,那么在重载函数中,可能会出现如下的一种尴尬情况:
// code Fragment 3
class time
{
public:
int hours; // 小时
int minutes; // 分钟
// 运算符*重载,对时间乘一个倍数
time& operator*(double scaler)
{
time res;
long total_min = (minutes + hours * 60ul) * scaler;
res.hours = total_min / 60ul;
res.minutes = total_min % 60ul;
return res;
}
}
这段代码重定义了*
运算符,应该这样调用重载后的运算符:
A = B * 2.75; // 1
事实上,它等价于:
A = B.operator*(2.75); // 2
所以这样写是错误的:
A = 2.75 * B // 3
因为2.75
的类型是float/double
,而不是我们期望的time
类型,这个数字本身没有重载乘法运算符,所以编译器无法提供这样的适配
那么,如果我一定要按照3
的方式去调用怎么办?
实际上这里牵扯到软工中一个比较有争议的设计理念:“永远不要相信用户的输入”——不知道有没有人记得测试工程师与酒吧的笑话——“一位顾客点了一份炒饭,酒吧炸了”^_^
回到刚才的问题,按照3
的方式进行调用当然是可以的,不过需要额外提供一份非成员函数形式的乘法运算符重载:
// 非成员函数形式的乘法运算符重载
time& operator*(double scaler, const time& t)
{
// 此处代码省略
}
但是这样做依然有一定的问题,如果要访问类的私有成员变量,这个函数无论如何都是做不到的,这个时候可以考虑使用友元进行运算符重载
注意: 有些运算符只能使用成员函数的形式进行重载,它们分别是=
、[]
、()
和->
基本的友元重载
// code Fragment 4
class time
{
private:
int hours; // 小时
int minutes; // 分钟
public:
// 使用友元的重载声明
friend time& operator*(const time& s, double scaler);
}
// 重载的具体实现
time& operator*(const time& s, double scaler)
{
// 具体实现省略
}
可以看到,友元重载有这样的特点
operator*()
的声明位于类声明中,但是其具体实现不加::
限定符,也不是类成员函数operator*()
的声明前加了friend
关键字,告诉编译器这是一个友元函数operator*()
具有和类成员函数一样的访问权限,即可以访问由private
等修饰的域
事实上,友元函数作为类声明中接口的一部分早已得到广泛应用
最常用的友元重载——重载<<
运算符
偶尔我们可能希望使用过标准流输出打印一些信息到终端或者文件中,例如对code Fragment 4
中的代码,希望它可以打印出诸如xx hours, yy minutes
这样格式的时间;但是注意到这个操作涉及到的两个成员变量都是私有成员变量,所以我们可能需要一些特殊的函数来读取它们,例如:
// code Fragment 5
class time
{
private:
int hours; // 小时
int minutes; // 分钟
public:
int get_hours(void) { return hours; } // 获取小时数
int get_minutes(void) { return minutes; } // 获取分钟数
}
int main(void)
{
// 实例化
time A;
// 省去赋值操作......
// 按照既定格式输出时间
std::cout << A.get_hours() << " hours, " << A.get_minutes() << " minutes" << std::endl;
return 0;
}
可见这样对于私有成员变量的操作是较为繁琐的,能不能使用cout << A
这样的方式进行输出呢?答案是可以
下面讨论如何适当的重载<<
运算符
首先,<<
运算符最早的语义是向左移位计算,有过C语言基础的读者应该对这个符号并不陌生;但是在STL中,该符号被多次重载,作为流运算符而广泛应用
现在,先尝试基本的友元重载:
// code Fragment 6
#include <iostream>
class time
{
private:
int hours; // 小时
int minutes; // 分钟
public:
time(int h, int m) { hours = h; minutes = m; } // 构造函数
friend void operator<<(std::ostream& os, const time& s); // 重载<<的声明
};
// 重载<<的实现
void operator<<(std::ostream& os, const time& s)
{
os << s.hours << " hours, " << s.minutes << " minutes" << std::endl;
}
int main(void)
{
time A(15, 23);
std::cout << A;
return 0u;
}
这样构造的operator<<()
可以达到我们预期的目的,但是它并不能实现连续输出的功能,例如cout << A << B
这样的写法,这是为什么呢?解决这个问题就要了解在STL中该运算符的原理
插话:标准<<
原理和实现
有时候,经常可以看到这样的实现:
int x = 0, y = x;
cout << x << y;
这个操作的本质应该是:
cout = cout << x;
cout << y;
// 或者是这样:
(cout << x) << y;
所以我们可以得出这样的结论,即operator<<()
针对int
类型的函数原型应该是:
ostream& operator<<(const int& num);
// 或者
ostream& operator<<(ostream& os, const int& num);
当然实际的实现中,该函数的原型应该是第一种,因为它是basic_ostream
的类成员函数,可以在C++标准头文件<ostream>
中查到
了解到这一点之后,就可以对刚才的重载进行一点点修正,使之变成通用的<<
运算符
修正的实现
// code Fragment 7
#include <iostream>
class time
{
private:
int hours; // 小时
int minutes; // 分钟
public:
time(int h, int m) { hours = h; minutes = m; } // 构造函数
friend std::ostream& operator<<(std::ostream& os, const time& s); // 重载<<的声明
};
// 重载<<的实现
std::ostream& operator<<(std::ostream& os, const time& s)
{
os << s.hours << " hours, " << s.minutes << " minutes";
return os;
}
int main(void)
{
time A(15, 23), B(12, 21);
std::cout << A << std::endl << B;
return 0u;
}
比较code Fragment 6
和code Fragment 7
,对于operator<<()
的重载,其区别仅在于增加了返回值,这样就可以把自定义的类嵌入到标准流当中,而且由于输出流的继承关系,被重载的运算符甚至可以把类的内容输出到文件流中,可以说是非常方便的操作了