从C学C++(9)——运算符重载
从C学C++(9)-运算符重载
若无特殊说明,本博客所执行的C++标准均为C++11.
运算符重载的基本形式
- 运算符重载可以使用成员函数进行重载,也可以使用非成员函数(友元函数)进行重载。
- 运算符重载仅仅只是语法上的方便,它是另一种函数调用的方式,本质上是函数重载 。
- 运算符重载允许把标准运算符(如
+
、-
、*
(乘法,不是解指针)、/
、>
、<
等)应用于自定义数据类型的对象, 可以提高程序的可读性。
友元函数重载
友元函数重载的原型格式是:
friend 函数返回类型 operator运算符(类类型&, 参数表);
友元函数重载定义的格式:
函数返回类型 operator运算符(类类型&, 参数表){ 函数体; }
- 需要注意的是: 由于友元函数不是成员函数,而通常运算符有一个参数是自身对象,所以,友元函数重载运算符时,需要类类型的引用作为一个参数。
- 因此,友元函数重载的多是双目运算符(有些双目运算符不能用友元重载,下面会有提及),如:四则运算,
+
,-
,*
,/
, 因为这些运算符没有明显的自身对象调用返回自身,而是两个对象调用,返回一个新对象(注意,此时返回新对象,应该返回对象自身,而不能是新对象的引用,因为新对象通常是函数内部的局部对象)。 - 流运算符
<<
,>>
只能以友元函数重载。
成员函数重载
成员函数重载原型格式是:
函数返回类型 operator运算符(参数表);
成员函数重载定义的格式:
函数返回类型 类名::operator运算符(参数表){ 函数体; }
-
一般情况下,单目运算符最好重载为类的成员函数。
-
以下这些双目运算符只能重载为类的成员函数,不能用友元函数重载。
-
=
、()
、[]
、->
-
以
=
赋值运算符为例,以下,class Test{public: Test& operator=(const Test& other); //赋值运算符重载private: int x_; }; Test t1; Test t2; t1 = t2; //等价于 t1.operator=(t2);//这样的成员函数调用 //假设有一个派生类D class D: public Test{ //父类是Test } //如果使用友元函数重载的话,应该像以下这么写 Test& operator=(Test& t1, Test& t2){t1.x_ = t2.x_;return t1; } //如果赋值操作发生在派生类和父类之间的话 D d; Test t; D = t; //此时用友元重载的话,函数参数传递和返回值传递的时候,都会有派生类到父类的类型转换,会产生其他问题,因此禁止。
-
-
类型转换运算符只能以成员函数方式重载。
运算符重载规则
除了上面说的,某些双目运算符(=
、 ()
、 []
、 ->
)和类型转换运算符必须使用成员函数重载 还有 流运算符<<
, >>
只能以友元函数重载 之外。
- 以下运算符不能被重载
- 作用域解析运算符
::
- 条件三元运算符
?:
- 直接成员访问运算符
.
- 类成员指针引用运算符
.*
(这里是指解指针) - sizeof运算符
sizeof
- 作用域解析运算符
++运算符的重载
前置++运算符重载(前置的–也一样,名字换成operator--
)
- 成员函数的方式重载,原型为:
类类型& operator++();
- 友元函数的方式重载,原型为:
friend 类类型& operator++(类类型&);
后置++/–运算符重载
- 成员函数的方式重载,原型为:
类类型& operator++(int);
- 友元函数的方式重载,原型为:
friend 类类型& operator++(类类型&,int);
- 对于后置++/–需要注意以下两点:
- 参数中多了一个
int
的参数只是为了和前置++/–区分开,没有什么实际的意义。 - 后置++/–是先返回值再+/-,所以,在实现时注意先保存没有+/-的值用于返回,在执行+/-。
- 参数中多了一个
=运算符的重载
=运算符重载的注意事项在构造函数那里说过一次(初始化中的=不是赋值运算符
这一小节)这里给个[链接](.\从C学C++(6)-构造函数和析构函数.md### 初始化中的=不是赋值运算符)。
主要是要注意,赋值运算符通常重载成返回值为对象的引用,参数时对象的引用的成员函数, 如下:
Test& Test::operator=(const Test& other){//需要的操作return *this; //返回对象本身
}
!运算符的重载
这个重载的基本形式如下,好像没有什么需要特别注意的地方
bool String::operator!() //!非运算符重载
{
}
[]运算符的重载
-
通常
[]
的重载会返回引用,好处是可以作为可修改的左值(返回引用的的函数都有这样的好处)。char& String::operator[](unsigned int index){ //返回引用return _str[index]; } //这样的话 String s1 = "abs"; s1[2] = 'A'; //这样修改便是有意义的
-
需要注意如果是
const
对象使用[]
的话,是会调用[]
的另一个重载(即const
限定的版本),如下:const char& String::operator[](unsigned int index) const { //const版本存在的问题是,如果不重载一个const版本,照常调用非const版本,那在作为左值被修改时,const对象就能够被修改,所以,我们实现一个const版本,保证const对象调用[]时,他的成员不能被修改,如果作为左值被修改时应该要报错,这里只要返回const的引用即可(因为const的引用是不能被修改的)return _str[index]; }
-
通常情况下由于,
const
版本函数和非const
版本是差不多的,所以我们通常会让非const
版本调用const
版本,以减少冗余的代码。所以我们可以将非const
版本的重载更改为如下的形式:char& String::operator[](unsigned char index){//首先要将对象转化为const对象,才能调用const版本=> static_cast<const String&>(*this)//然后调用[]运算符,这样就会调用const版本的[]重载 => static_cast<const String&>(*this)[index]//最后将返回的const引用转成非const引用,去掉const属性,并返回return const_cast<char&>(static_cast<const String&>(*this)[index]); }
+运算符的重载
-
最好实现为友元的方式,而不是用成员函数的方式重载。例子如下:
friend String operator+(const String& str1, const String& str1); //注意这个是在类中的友元声明,在外部实现时不需要friend前缀 //以友元方式重载的好处是,允许下面这样的调用 String s1 = "hello"; String s2 = "sheep"+ s1; //这样会调用友元函数,并且会调用转换构造函数将前面这个从char*转换成String类型,而如果是成员函数重载的话,只允许String类型+char*类型,而不能char*类型在前。
+=运算符重载
因为+=运算符返回的是自身这个对象(最好返回自身对象的引用,可以避免一次拷贝构造函数),所以+=运算符以成员函数实现比较方便,格式如下:
String& String::operator+=(const String& str){ //这里是在类体外实现,所以加上了域限定符*this = *this + str; //利用+运算符重载,实现+=return *this;
}
<<流输出运算符和>>流输入运算符的重载
需要注意的是,流运算符必须重载为友元的形式,因为第一个参数是流对象,基本格式如下:
friend istream& operator>>(istream&, 类类型&);
friend ostream& operator<<(ostream&, const 类类型&);
//注意,istream 和 ostream都是在std空间中的。
同时需要注意,返回的也是流对象的引用,这样才可以实现如下的多个流运算“拼接”使用。
String str1, str2;
cout<<str1<<str2<<endl; //因为前面返回了一个流对象
类型转换运算符的重载
-
类型转换运算符: 显示或者隐式的类型转换时调用的函数,如:
(int)a
这样的类型转换,将这个类转换成其他类型时要调用的函数。即:类型转换运算符是将该类型转换为其他类型的函数,而之前提及的转换构造函数时将其他类型转换为自己这个类型会调用的函数。(*这里有个值得思考的,那如果是两个类类型的转换,且两者都实现了对应的函数重载的话,在执行类型转换的时候是会调用哪一个呢?*🤔)
-
必须是成员函数,不能是友元函数。
-
没有参数(因为参数默认就是自身对象,所以无法使用友元函数重载)
-
不能指定返回类型(其实在重载的时候就已经指定了,一个类型一个重载的函数), 函数的原型如下:
operator 类型名(); //这里的类型名是要将要转换成的类型名,因此也不能指定返回值,因为这里的类型名就指定了返回值类型了
-
->运算符的重载
最常用的一种方式是用于实现类似智能指针的功能,利用栈上对象的确定性析构来保证动态内存申请的对象的申请和释放。(🧐感觉这个还是值得细品一下的)。
#include <iostream>
using namespace std;
class DBHelper{
public:DBHelper(){cout<<"DBhelper..."<<endl;};~DBHelper(){cout<<"~DBhelper..."<<endl;};void open(){ cout << "DB open called" << endl;};void close(){cout << "DB close called" << endl;};void query(){cout << "DB query called" << endl;};
};
class DB{
public:DB(){dbHelper = new DBhelper(); //创建DBhelper对象dbHelper->open();};~DB(){dbHelper->close(); //DBhelper这个动态内存对象会随着DB对象的销毁而销毁delete dbHelper;};//但这样我们每次访问要对DB对象open或者close时都要借助公用函数来访问内部的dbhelper对象,非常不方便,逻辑也不直接//所以这里重载->运算符,直接返回dbHelper对象的指针,这样DB对象就像是个智能指针一样了DBhelper* operator->(){return dbHelper;};
private:DBhelper* dbHelper;
};
operator new/ operator delete运算符的重载
new的三种用法
operator new
是指分配内存空间的new操作,可以重载。
new operator
new operator=operator new+构造函数的调用new\ operator = operator\ new + 构造函数的调用new operator=operator new+构造函数的调用
是指语法上的new
,它包含两部分操作,一是分配内存空间的operator new,二是调用类的构造函数。
String* ps1 = new String("abc"); //这样的操作为new operator,实际包含两部分,分配空间和调用构造函数
new operator 不能重载。
placement new
是指不分配空间的new操作,语法上如下:
char chunk[10];
String* ps2 = new(chunk) String("abc");
//不会分配新的内存空间,而是直接在提供的内存上调用构造函数,内部的实现原型实际上是直接把传入的指针返回。
需要注意的是,像这样placement new申请的空间是chunk
,也就是说是在栈上的,所以不能使用delete进行显示删除。但chunk
回收时,由于不是String对象,所以不会调用String的析构函数,而ps2
只是一个指针,回收时也不会调用析构函数。因此,对于placement new得到对象(指针指向的在指定空间上使用构造函数得到的对象),我们应该显示调用析构函数。
new/delete 重载
通常情况下,new和delete的重载是一一对应的,重载了对应形式的new,就必须重载对应形式的delete。
通常有以下几种对应的new和delete重载:
-
void* operator new(std::size_t size)
这个就是最常用的new的重载void operator delete(void* p)
这个也是我们最常用的delete的重载 -
void* operator new(std::size_t size, void p)
这个就是上面所说的placement new的重载,通常是直接将传入的指针原封不动返回void operator delete(void* p, void* p)
这个函数是上面placement new对应的delete重载,但需要注意的是,这个函数默认为空,正如我们上面所说,placement new 没有在栈上申请空间,所以并不能调用delete去释放,而是需要我们显示调用对应的析构函数。这个delete重载通常我们也不能显示调用,通常是在上面重载的placement new分配内存失败抛出异常时由编译器调用的钩子函数。 -
void* operator new[](std::size_t size)
这个就是最常用的new数组的重载void operator delete[](void* p)
这个也是我们最常用的delete[] 数组的重载
以下这个带有调试信息的new重载是在C++14才引入的,这里说一下:
void* operator new(std::size_t size, const char* file , long line)
这个是带有调试信息的new重载,也是在堆上申请空间,唯一区别就是允许传入文件和行号用于调试打印。调用时
String* ps3 = new(__FILE__, __LINE__) String("abc")
这样调用,手动释放时,调用的是,默认的delete
,即使用delete ps3
释放,而不是它对应的delete。它对应的delete也可以重载,如下:
void operator delete(void* p, const char* file , long line)
,但同样的这个delete也只是作为new分配空间异常时的钩子函数而已。
总结一下,就是用户重载各种new是可以通过new语法调用的,但能够调用的delete只有 原生的delete
和 delete[]
这两个,其他基本上都是作为分配空间失败时的钩子函数。同时需要注意,指定地址空间的placement new 无法使用delete
释放,需要自己显示调用析构函数以正常释放对象。