C++从零开始(八):面对对象(中)运算符重载
|字数总计:3.2k|阅读时长:13分钟|阅读量:
🌟《C++从零开始》 系列,开始更新中…
六、运算符重载
在前面我们接触过函数重载,让我们可以使用多个同名函数,只要它们有唯一的函数签名(由函数参数类型、个数、函数名组成)可以让编译器区分即可。
在C++中,运算符被视为函数,自然也可以进行重载。
认识下运算符这个特殊的函数。
运算符operator+
作为函数,接收了两个int
类型参数:
1 2 3
| int x { 2 }; int y { 3 }; x+y;
|
重载函数operator+
,又接收了两个double
类型参数:
1 2 3
| double x { 2 }; double y { 3 }; x+y;
|
这都是C++为我们内置实现了operator+
重载,用于C++基本类型之间运算。
我们也可以自定义重载自己的运算符函数。
虽然C++ 中几乎所有的运算符我们都可以进行重载,但要注意以下规则:
- 重载运算符中的至少一个操作数必须是用户定义的类型,比如重载
operator+
函数,一个参数是int
,一个是double
是不行的;
- 无法更改运算符支持的操作数数量,优先级也被保留。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| #include<iostream> class myInt { private: int i; public: myInt(int a):i(a){ } int getValue(){return i;} };
myInt operator+(myInt& i1,myInt& i2) { return myInt(i1.getValue()+i2.getValue()); }
myInt add(myInt& i1,myInt& i2) { return myInt(i1.getValue()+i2.getValue()); }
|
普通函数也可以实现重载运算符所需的功能,重载运算符有什么好处?
重载运算符可以让程序表达更直观:
1 2 3 4 5 6 7 8 9
| int main() { myInt i1{1}; myInt i2{2};
myInt i3 = i1+i2; myInt i4 = add(i1,i2); return 0; }
|
如果重载运算符并不能让你的程序更清晰,请谨慎使用。
本章主要介绍:
- 重载运算符的友元/普通/成员三种方式。
- 特殊重载,如重载()、=介绍。
6.1 重载运算符的三种方式
重载运算符有三种不同的方式:
按照顺序我们依次来看下。
6.1.1 友元函数
重载友元初识
下面展示了重载operator+
,其余操作符-
、/
、*
等类似。
注意到函数operator+
是类MinMax
的友元函数,这样就可以使用MinMax
的私有成员。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| #include <iostream>
class Cents { private: int m_cents{};
public: Cents(int cents): m_cents{ cents }{} friend Cents operator+(const Cents& c1, const Cents& c2) };
Cents operator+(const Cents& c1, const Cents& c2) { return { c1.m_cents + c2.m_cents }; }
int main() { Cents cents1{ 6 }; Cents cents2{ 8 }; Cents centsSum = cents1 + cents2 ; std::cout << "I have " << centsSum.getCents() << " cents.\n"; return 0; }
|
输出:
更多例子:重载I/O运算符
下面是一个同时使用重载的 operator<<
和 operator>>
的示例 。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44
| include <iostream>
class Point { private: double m_x{}; double m_y{}; double m_z{};
public: Point(double x=0.0, double y=0.0, double z=0.0): m_x{x}, m_y{y}, m_z{z}{} friend std::ostream& operator<< (std::ostream& out, const Point& point); friend std::istream& operator>> (std::istream& in, Point& point); };
std::ostream& operator<< (std::ostream& out, const Point& point) { out << "Point(" << point.m_x << ", " << point.m_y << ", " << point.m_z << ')'; return out; }
std::istream& operator>> (std::istream& in, Point& point) { in >> point.m_x; in >> point.m_y; in >> point.m_z; return in; }
int main() { std::cout << "Enter a point: "; Point point1; Point point2; std::cin >> point1 >> point2; std::cout << "You entered: " << point1 << " " << point2<<'\n'; return 0; }
|
输出:
1 2 3 4
| [root@roy-cpp test]# ./test.out Enter a point: 1 2 3 4 5 6 You entered: Point(1, 2, 3) Point(4, 5, 6)
|
6.1.2 普通函数
使用友元函数虽然很方便,但也一定程度的破坏了类的封装性。因此我们建议尽量将重载运算符实现为普通函数。
下面是一个小例子。
注意到普通函数和实现为友元函数极为类似,除了友元函数需要在类中先声明函数原型 friend Cents operator+(const Cents&,const Cents&)
,其余没有区别。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
| #include <iostream> class Cents { private: int m_cents{};
public: Cents(int cents): m_cents{ cents }{} int getCents() const { return m_cents; } };
Cents operator+(const Cents& c1, const Cents& c2) { return Cents{ c1.getCents() + c2.getCents() }; }
int main() { Cents cents1{ 6 }; Cents cents2{ 8 }; Cents centsSum = cents1 + cents2 ; return 0; }
|
6.1.3 成员函数
重载成员初识
成员重载运算符和友元重载运算符很类似,但是也有些不同:
- 重载的运算符被定义为成员而不是友元(例如,是
Cents::operator+
而不是operator+
);
- 左边的类类型参数
const Cents&
被移除了,变成了隐含的 *this
对象。
看代码说话。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| #include <iostream>
class Cents { private: int m_cents {};
public: Cents(int cents): m_cents { cents } { }
Cents operator+ (int value); int getCents() const { return m_cents; } };
Cents Cents::operator+ (int value) { return Cents { m_cents + value }; }
int main() { Cents cents1 { 6 }; Cents cents2 { cents1 + 2 }; std::cout << "I have " << cents2.getCents() << " cents.\n"; return 0; }
|
输出:
重载一元函数
正 (+)、负 (-) 和逻辑非 (!) 运算符都是一元运算符,这意味着它们只对一个操作数进行操作,也就是只对应用它们的对象进行操作,所以通常一元运算符重载被实现为成员函数。
下面是个简单的例子,注意负运算符和减号运算符之间没有混淆,因为它们具有不同数量的参数。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include <iostream>
class Cents { private: int m_cents {};
public: Cents(int cents): m_cents(cents) {} Cents operator-() const; int getCents() const { return m_cents; } };
Cents Cents::operator-() const { return -m_cents; }
int main() { const Cents nickle{ 5 }; std::cout << "A nickle of debt is worth " << (-nickle).getCents() << " cents\n";
return 0; }
|
6.1.4 最佳实现:使用什么方式重载
下面是一些经验法则:
6.2 重载()运算符
到目前为止我们接触的运算符,参数类型虽然可以设置,但参数类型是固定的。
而重载括号运算符 operator()
,允许我们改变它的参数类型和数量。
这有什么用?
operator()
一大用处是用来实现函子(函数对象)。相比普通函数,
- 函子可以将任意数量的数据存储在成员变量,而不用在形参中定义避免了可能的值传递开销;
- 函子可以保存结果和状态,提高代码灵活性;
- 函数指针不能内联,函子可以,效率更高(没验证过)。
6.2.1 实现函子
请看下例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| #include <iostream>
class Accumulator { private: int m_counter{ 0 };
public: int operator() (int i); int getCounter() { return m_counter;} };
int Accumulator::operator() (int i) { return (m_counter += i); } int main() { Accumulator acc{}; std::cout << acc(10) << '\n'; std::cout << acc(20) << '\n'; acc.getCounter(); return 0; }
|
函子更多的是应用在关联容器和STL,其中许多算法都是可以用函数或函数对象自定义比较器的。
请看下例。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| #include <algorithm> #include <array> #include <iostream>
class MyGreater { public: bool operator()(int a, int b) { return (a > b); } };
bool myLesser(int a, int b) { return (a < b); }
void print(const std::array<int,6>& arr) { for (int i : arr) { std::cout << i << ' '; } std::cout << '\n'; }
int main() { std::array<int,6> arr = { 13, 90, 99, 5, 40, 80 }; std::sort(arr.begin(), arr.end(), myLesser); print(arr); std::sort(arr.begin(), arr.end(), MyGreater()); print(arr); return 0; }
|
输出:
1 2 3 4
| [root@roy-cpp test] [root@roy-cpp test] 5 13 40 80 90 99 99 90 80 40 13 5
|
6.3 重载=运算符
6.3.1 区分复制构造函数
复制构造函数和赋值运算符的用途几乎相同——都是将一个对象复制到另一个对象。
但它们最核心的区别在于作用的时机不同:
- 复制构造函数:只要对象在声明时用了另一个对象(可能是编译器创建的匿名对象)进行初始化,就会触发复制构造函数,也就是进行复制初始化。
- 赋值运算符:不是在对象声明时,而是一个已存在的对象被另一个对象重新赋值时 ,就会触发赋值运算符函数。
我们通过例子来理解。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class Date { private: int m_year{}; int m_month{}; int m_day{}; public: Date() = default; Date(int day,int month=1,int year=2022) { std::cout<< "构造函数被调用"<<std::endl; m_year=year;m_month=month;m_day=day; }; Date(Date const& tmp): m_year(tmp.m_year),m_month(tmp.m_month),m_day(tmp.m_day) { std::cout<< "复制构造函数被调用"<<std::endl; } Date& operator=(const Date& date) { std::cout<< "oprator=被调用"<<std::endl; m_year=date.m_year; m_month=date.m_year; m_day=date.m_year; } };
|
主函数中:
1 2 3 4 5 6 7
| int main() { Date date1 = 25; Date date2 = Date{25}; Date date3 = date2; }
|
- 1、2在对象date1、date2声明时用匿名对象进行初始化(编译器要使得
=
左右操作类型一致,分别在=
右侧创建了Date匿名对象,然后调用复制构造函数来初始化date1、date2)。
- 3处date3也是声明时使用了其它对象(date2)进行初始化,所以也会调用复制构造函数。
但是1、2处会被编译器优化使用普通构造函数初始化,所以最后输出:
1 2 3
| 构造函数被调用 构造函数被调用 复制构造函数被调用
|
我们再来看看调用赋值运算符=
的情况:
1 2 3 4 5 6 7
| int main() { Date date1 = 25; Date date2 = Date{25}; date2 = 31; date2 = date1; }
|
输出:
1 2 3 4
| 构造函数被调用 构造函数被调用 oprator=被调用 oprator=被调用
|
也就是3、4处都是调用了赋值运算符=
进行赋值(不是初始化):
- 3处,date2不是声明时进行初始化,而是使用编译器生成的匿名对象进行重新赋值 ;
- 4处,同理,date2不是声明时进行初始化 ,使用date1进行重新赋值 。
它们都符合前文所述:“而是一个已存在的对象被另一个对象重新赋值时 ,就会触发赋值运算符函数” 。
更新记录
参考资料