第十四章:重载运算与类型转换Ⅰ
1、基本概念:
-
重载的运算符是就有特殊名字的函数:名字由关键字operator和要定义的运算符号共同组成。也包含返回类型,参数列表以及函数体。
-
参数数量与运算符作用的运算对象数量一样多。
-
重载的运算符是成员函数,this绑定到左侧运算对象。成员运算符函数的显式参数数量比运算对象少一个。
-
对运算符函数来说,要么是类的成员,要么至少含有一个类类型的参数。暗示不能改变作用在内置类型的运算符作用。
-
只能重载已有的运算符,无权发明新的运算符,可以从参数数量上判断定义的是哪种运算符,对重载的运算符其优先级与结合律同对应的内置类型运算符保持一致。
-
直接调用重载运算符:
-
首先指定运行函数的对象(或指针)的名字,然后使用句点运算符(箭头运算符)访问希望调用的函数。
-
data1 += data2; data1.operator+=(data2); // 等价方式
-
-
通常情况下,不应该重载逗号、取地址、逻辑与和逻辑或运算符。
-
使用与内置类型一致的含义:
- 若类执行IO操作,则定义移位运算符使其与内置类型的IO保持一致。
- 若类检查相等性,则定义operator==,以及 operator!=。
- 若类包含一个内在的单序比较操作,则定义operator<及其他操作。
- 重载运算符的返回类型应与内置版本的返回类型兼容:逻辑运算符与关系运算符应返回bool,算术运算符应返回一个类类型,赋值运算符和符合赋值运算符应返回左侧对象的引用。
- 只有当操作的含义对于用户来说很清晰时,才使用运算符重载,否则可能会出现二义性。
-
选择作为成员或者非成员:
- 赋值(=)、下标([])、调用(())和成员访问箭头(->)运算符必须是成员。
- 复合赋值运算符一般应该是成员。
- 改变对象状态如递增、递减和解引用运算符应该是成员。
- 具有对称性的运算符如 算术、相等性、关系和位运算符等,通常是普通的非成员函数。
-
当运算符定义为成员函数时,其左侧运算对象必须是运算符所属类的一个对象。
2、输入和输出运算符:输入、输出运算符必须是非成员函数
-
IO库定义了用其读写内置类型的版本,而类则需要自定义适合其对象的新版本来支持IO操作。
-
重载输出运算符<<:
-
ostream& operator<<(ostream& os, const Sales_data& item){ os << item.isbn() << " " << item.units_sold << " " << item.revenue << " " << item.avg_price(); return os; }
-
第一个参数通常是非常量的ostream对象的引用。
- 非常量:向流写入内容会改变其状态;
- 引用:无法直接复制一个ostream对象;
-
第二个形参一般是一个常量的引用,且常量是想要打印的类类型。
- 引用:避免复制实参;
- 常量:打印不会改变对象的内容;
-
**输出运算符尽量减少格式化操作:**注意负责打印对象的内容而非控制格式,不应打印换行符。
-
输入、输出运算符必须是非成员函数:
-
与标准库兼容的输入输出运算符必须是普通的非成员函数,而不能是类的成员,否则,左侧运算符对象将是类的一个对象。
-
Sales_data data; data << cout;
-
IO运算符常常需要读写类的非公有数据成员,所以IO运算符一般声明为友元。
-
-
-
重载输入运算符:
-
istream& operator>>(istream& is, Sales_data& item){ double price; // 从流is中读取两个数据并赋值给item.bookNo和item.units_sold,如果语句执行成功返回true,如果输入失败则返回false; is >> item.bookNo >> item.units_sold >> price; // if检查读取操作,如果发生IO错误,将对象置为空,以确保对象处于正确状态。 if(is) item.revenue = item.units_sold * price; else item = Sales_data(); // 读取失败对象被赋予默认状态。 return is; }
-
第一个形参是运算符将要读取的流的引用;
-
第二个形参是要读到的(非常量)对象的引用;
- 运算符目的就是将数据读入到这个对象中。
-
运算符返回某个给定流的引用。
-
输入运算符必须处理输入可能失败的情况:
-
当流中含有错误类型的数据时,读取可能失败。
-
当读取到文件末尾或遇到输入流的其他错误也会失败。
-
在 C++ 中重载输入运算符
>>
时,可以通过在函数体内进行输入操作并返回输入流对象来处理输入失败的情况。 -
一般情况下,输入操作可能会出现错误,例如输入的数据类型与要求不符、输入的数据格式不正确等等,这时候可以在重载的输入运算符函数中设置输入流对象的错误状态,以便在程序中处理错误。
-
#include <iostream> class MyClass { public: int a, b; friend std::istream& operator>>(std::istream& input, MyClass& obj) { if (!(input >> obj.a >> obj.b)) { // 如果输入失败 input.setstate(std::ios::failbit); // 设置输入流对象的错误状态 } return input; } }; int main() { MyClass obj; std::cin >> obj; if (std::cin.fail()) { std::cout << "输入错误!" << std::endl; } else { std::cout << "输入成功!" << std::endl; } return 0; }
-
如果输入运算符函数
operator>>
的输入操作失败,就会设置输入流对象的错误状态为std::ios::failbit
。在main()
函数中,我们可以通过检查输入流对象的状态来判断输入是否成功。如果输入失败,就输出错误信息。 -
通常,输入运算符只设置failbit,还可以设置eofbit表示文件耗尽,设置badbit表示流被破坏,最好由IO标准库自己标示这些错误。
-
-
3、赋值运算符:
-
拷贝赋值和移动赋值运算符可以把类的一个对象赋值给该类的另一个对象。重载赋值运算符可以定义使用别的类型作为右侧运算对象。
-
vector<string> v; v = {"a","an","the"}; // vector中重载了赋值运算符,接受花括号内的元素列表作为参数。 class StrVec{ public: // 为了与拷贝赋值、移动赋值运算符保持一致,返回左侧对象的引用。 StrVect& operator=(std::initializer_list<std::string>); } StrVec& StrVec :: operator=(initializer_list<std::string>il){ auto data = alloc_n_copy(il.begin(),il.end()); // 分配内存空间并从给定范围拷贝元素。 free(); // 先释放当前内存空间再创建一片新空间。 elements = data.first; // 更新数据指向新空间 first_free = cap = data.second; return *this; }
-
可以重载赋值运算符,无论形参的类型是什么,赋值运算符都必须定义为成员函数。
-
复合赋值运算符:
-
倾向于将符合赋值运算符定义成类成员,复合赋值运算符通常也返回左侧运算对象的引用。
-
// 左侧运算对象绑定在隐式的this指针。 Sales_data& Sales_data::operator+=(const Sales_data &rhs){ units_sold += rhs.units_sold; revenue += rhs.revenue; return *this; }
-
赋值运算符必须定义为类的成员,复合赋值运算符通常也定义为类的成员,返回左侧运算对象的引用。
-
4、算术和关系运算符:
-
通常将算术和关系运算符定义为非成员函数以允许左右侧对象进行转换,一般不需要改变运算对象的抓过你太,所以形参通常是常量的引用。
-
算术运算符计算两个对象的并得到一个新值,值有别于任意一个运算符,常位于一个局部变量中,操作完成后返回局部变量的副本作为结果。
-
Sales_data operator+(const Sales_data& lhs, const Sales_data& rhs){ Sales_data sum = lhs; lhs += rhs; // 调用复合赋值运算符 += return sum; // 返回局部变量副本作为结果 }
-
如果类同时定义了算术运算符和相关的复合赋值运算符,通常可以使用复合赋值运算符实现算术运算符。
-
相等运算符:
- C++类通过定义相等运算符检验两个对象是否相等,比较对象的每一个对象,只有当所有对应的成员都相等才认为两个对象相等。
- 如果类中含有判断两个对象是否相等的操作,显然可以将函数定义为operator==而非普通的命名函数。
- 如果类定义了operator==则运算符应能判断一组给定的对象中是否含有重复书籍。
- 通常情况下,相等运算符应该具有传递性。
- 如果类定义了operator==则一般它也应该定义oprator!=。并且这两个运算符的一个应该将工作委托给另一个。
-
关系运算符:
- 如果存在唯一一种逻辑可靠的 < 定义,则应该考虑为这个类定义 < 运算符,如果类中还同时包含 ==,则仅当 < 的定义与 == 产生的结果一致时才定义 < 运算符。
5、下标运算符:
-
表示容器的类通常可以通过元素在容器中的位置访问元素,这些类一般定义下标运算符operator[].下标运算符必须是成员函数。
-
下标运算符通常以所访问元素的引用作为返回值。好处是下标运算符可以出现在赋值运算符的任意一端。
-
最好同时定义下标运算符的常量版本和非常量版本,当作用于常量对象时,下标运算符返回常量引用以确保不会给返回的对象赋值。
-
类包含下标运算符通常含有两个版本:一个返回普通引用,另一个是类的常量成员并返回常量引用。
-
class StrVec{ public: std::string& operator[](std::size_t n){return elements[n];} // 返回常量引用的下标运算符对对象取下标时,不能为其赋值。 const std::string& operator[](std::size_t n){return elements[n];} std::string* elements; }
6、递增、递减运算符:
-
定义递增、递减运算符的类应该同时定义前置版本和后置版本,且这些运算符通常应定义为类的成员。
-
前置递增、递减运算符:
-
前置运算符应该返回递增或递减后对象的引用。
-
class StrBlobPtr{ public: // 前置运算符 StrBlobPtr& operator++(); StrBlobPtr& operator--(); } // 前置运算符:返回递增、递减对象的引用 StrBlobPtr& StrBlobPtr::operator++(){ // 先检查curr,如果已经指向了容器的尾后位置,则无法递增它。 check(curr,"implement past end of StrBlobPtr"); ++curr; // 将curr在当前状态下向前移动一个元素。 return *this; } StrBlobPtr& StrBlobPtr::operator--(){ --curr; check(curr,"implement past end of StrBlobPtr"); // 如果curr递减后<0则越界,无效下标 return *this; }
-
-
后置运算符应该返回对象的原值(递增、递减之前的值),返回的形式是一个值而非引用。
-
为了区分前置、后置运算符,后置版本接受一个额外的不被使用的int类型的形参,当使用后置运算符时,编译器为这个形参提供一个值为0的形参,以区分前置、后置运算符。
-
对后置版本来说,在递增、递减对象之前应首先记录对象的状态。
-
class StrBlobPtr{ public: // 后置运算符,返回原值且含有一个int类型的形参 StrBlobPtr operator++(int); StrBlobPtr operator--(int); } // 不会使用到int形参,无需为其命名,但是这个参数必不可少,编译器只有通过这个参数辨别前后置版本。 StrBlobPtr StrBlobPtr::operator++(int){ StrBlobPtr ret = *this; // 记录当前值 ++*this; // 调用前置运算符 return ret; // 返回之前记录的状态 } //后置运算符各自调用各自前置版本完成实际工作。 StrBlobPtr StrBlobPtr::operator--(int){ StrBlobPtr ret = *this; // 记录当前值 ++*this; return ret; // 返回之前记录的状态 }
-
7、成员访问运算符:
-
C++中成员访问运算符 ‘*’ 和 '->'都可以被重载,他们的作用是用来访问类的成员变量和成员函数。
-
重载 ‘*’ 运算符:
-
定义一个返回值为引用类型的函数,这个函数可以用来返回指向类的成员变量的指针,并通过 '*'运算符来访问这个成员变量。
-
#include <iostream> class MyClass{ public: int a; // 重载了'*'运算符,定义了一个返回值为引用类型的函数,函数返回指向类成员a的指针,通过'*'运算符访问这个成员变量 int& operator*(){ return a; } }; int main(){ MyClass obj; obj.a = 10; // '*obj'调用了重载的'*'运算符,返回了指向 'obj.a'的指针,从而可以访问这个成员变量。 int& a_ref = *obj; // 通过重载 * 运算符访问成员变量 std::cout << a_ref << std::endl; // 输出结果10 a_ref = 20; // 修改成员变量的值 std::cout << obj.a << std::endl; return 0; }
-
-
重载 ‘->’ 运算符:
-
定义一个返回值为指针类型的函数,函数可以用来返回指向类的对象的指针,并通过’->'运算符来访问这个对象的成员函数或成员变量。
-
#include <iostream> class MyClass{ public: int a; MyClass* operator->(){ return this; } void print(){ std::cout << "a = " << a << std::endl; } }; // 调用了重载的 -> 运算符,返回了指向 obj 的指针,从而可以访问这个对象的成员函数或成员变量。 int main(){ MyClass obj; obj.a = 10; obj->print(); // 通过重载 -> 运算符调用成员函数 obj->a = 20; // 通过重载 -> 运算符访问成员变量 obj->print(); //输出20 return 0; }
-
重载
*
和->
运算符可以使得我们更方便地访问类的成员变量和成员函数。需要注意的是,为了避免出现歧义,通常我们只会在自定义的智能指针类中重载这两个运算符。
-
-
成员访问运算符重载在智能指针上的应用:
-
智能指针的实现方式通常是通过重载成员访问运算符
*
和->
来实现。在这种实现方式中,智能指针本质上是一个指向所管理对象的指针,它通过重载运算符*
和->
来提供与所管理对象相同的操作接口。 -
template <typename T> class SmartPtr { public: SmartPtr(T* ptr = nullptr) : ptr_(ptr) {} ~SmartPtr() { delete ptr_; } // 重载 * 运算符 T& operator*() const { return *ptr_; } // 重载 -> 运算符 T* operator->() const { return ptr_; } private: T* ptr_; };
-
定义了名为 “SmartPtr”的类模板,包含了一个指向所管理对象的指针’ptr_‘,类模板中,重载了 ‘*’和’->'运算符,可以通过智能指针来访问所管理对象的成员变量和成员函数。使用智能指针时,可以像使用原始指针那样使用它。
-
class MyClass { public: void print() { std::cout << "Hello, World!" << std::endl; } }; // 创建了一个MyClass类型的对象,调用ptr->print()时,先调用operator->()运算符函数,函数返回指向所管理对象的指针 ptr_是指向MyClass对象的指针,使用可以通过这个指针来访问所管理的对象的成员函数。 int main() { SmartPtr<MyClass> ptr(new MyClass()); ptr->print(); // 通过智能指针调用成员函数 (*ptr).print(); // 通过智能指针调用成员函数 return 0; }
-
因此,调用
ptr->print()
实际上是先通过重载的operator->()
函数获取指向所管理对象的指针,然后在这个指针上调用成员函数print()
。这种方式让我们可以像使用原始指针一样方便地访问所管理对象的成员函数和成员变量。
-
-
对箭头运算符返回值的限定:
-
重载箭头运算符时,可以改变的是箭头从哪个对象当中获取成员,箭头获取成员的事实不会改变。
-
(*point).mem; // point是一个内置的指针类型 point.operator()->mem; // point是类的一个对象
-
若point是指针,则使用内置的箭头运算符,等价于(*point).mem.解引用指针,从获得的对象中获取指定的成员。若point所指类型没有mem成员则报错。
-
若point是定义了operator->的类的一个对象,则使用point.operator->()的结果获取mem,若结果是指针,则重复上一条的过程,若结果是本身含有重载的operator->(),则重复调用当前过程,结束时程序返回所需内容或错误信息。
-
-
对于成员访问运算符的重载,如果我们想要支持 const 对象的访问,那么我们需要定义两个版本的运算符重载函数:一个是非 const 版本,另一个是 const 版本。
-
// 非 const 版本 返回类型 operator->(); // const 版本 返回类型 operator->() const;
-
非 const 版本的运算符重载函数返回的是一个指针,而 const 版本的运算符重载函数返回的是一个 const 指针,这是因为 const 对象只能访问 const 成员,因此返回的指针也必须是 const 指针。
-
-
也可以将成员访问运算符的重载函数定义为类的成员函数或者全局函数,具体实现方式如下:
-
// 定义为类的成员函数 class MyClass { public: // 非 const 版本 返回类型 operator->(); // const 版本 返回类型 operator->() const; }; // 定义为全局函数 class MyClass { public: // 非 const 版本 friend 返回类型 operator->(MyClass& obj); // const 版本 friend 返回类型 operator->(const MyClass& obj); };
-