1.不能重载的操作符,包括:
- : : (域操作符)
- .* (指针成员操作符)(这货很少用到啊)
- .(点操作符)
- ?: (问号表达式)
2.不建议重载的操作符,包括:
- 因为无法保证求值顺序,不应该重载:逻辑与&&、逻辑或||、逗号运算符,
- 重载operator&之后会造成很多问题(尤其是返回类型与默认返回类型不一致时),所以避免重载operator&。
3.操作符重载的基本规则:
- 函数名称为:operator加上操作符
- 有返回类型、函数体和参数列表
- 一元操作符一个参数,二元操作符两个参数
- 成为类成员的操作符重载第一个参数被默认为this,所以参数个数再减一
- 只有调用操作符()可以有参数默认值,其他都不可以
- 操作符重载函数或者是类成员,或者必须含有一个类类型的参数(所以你不能重载内置类型的操作符)
- 只能重载已经存在的操作符,不能创造新的操作符(废话)
- 重载操作符的优先级和结合律与默认的保持一致
是不是很啰嗦?在这里举个例子吧。
class Foo {
friend Foo operator+(const Foo&, const Foo&);
public:
explicit Foo(int i):data(i){}
private:
int data;
}
Foo operator+(const Foo &left, const Foo &right) {
return Foo(left.data + right.data);
}
4.下面将各种操作符的重载列在如下表中:
操作符名称 | 函数名 | 返回值 | 参数列表 | 声明位置 | 其他说明 |
---|---|---|---|---|---|
输入输出操作符 | operator<< operator>> | stream对象的引用 (保证>>和<<可以连续使用) | stream &s, const Type &t (输入输出会改变流状态,所以引用不能为const) | 类外friend (因为第一个参数不能为this) | 输入操作符必须处理流失败 输出操作符则不需要 |
算数操作符 | operator+ operator- operator* operator/ %^!~&| | Type的临时对象 (与内置类型的行为保持一致) | const Type &t1, const Type &t2 | 类外friend | 推荐连同对应的复合运算符 一起定义,在内部可以使用 定义好的复合运算符来实现 对应的外部运算符 |
关系运算符 | operator== operator!= operator< <= > >= | bool | const Type &t1, const Type &t2 | 类外friend | 推荐成对定义,例如:定义 了==后把!=也定义了,并用 ==实现!=;推荐定义<,因 为这样可以在更多的STL容 器中使用;最好能够保障传 递性 |
赋值运算符 | operator= | Type & (返回左侧对象的引用) | const Type & (拷贝赋值) Type && (移动赋值) 以及其他各种类型 | 类member (因为要返回左侧对象的引用) | 只建议定义拷贝、移动以及 initializer_list版本的,其他 类型的版本容易让人费解;注意处理赋值给自己的情况(比较参数地址与this) |
复合赋值运算符 | operator+= operator-= operator*= operator/= %=^=&=|= <<= >>= | Type & | const Type & 以及其他各种类型 | 推荐类member | 不建议定义奇怪的,定以后 建议同时定义对应的运算 |
下标运算符 | operator[] | 类内对象类型的引用 | size_t | 类member (因为要返回类内对象的引用) | 建议同时定义const与非 const版本,来保证const 对象也可以使用 |
前置递增递减 | operator++ operator-- | Type & | 无 | 类member (因为改变了对象本身) | 定义前置的同时建议也定 义后置版本 |
后置递增递减 | operator++ operator-- | Type的临时对象 (递增递减前的拷贝) | int (注意,int只是为了与前置版本区分,从来不用) | 类member | 定义后置的同时建议也定 义前置版本 |
成员访问运算符 | operator* operator-> | Type & Type * | 无 | 类member | 推荐成对定义,注意保证-> 的语义不变 |
函数调用运算符 | operator() | 任意 | 任意 | 类member (不然就不是函数对象了) | 可以有多个重载版本 |
类型转换运算符 | operator type | type | 无 | 类member | 注意类型转换的二义性 |
注意:解引用操作符operator*和operator->作用的是对象而不是指针,指针会使用语言默认的operator*和operator->,所以这两个重载是为了指针的container(例如:智能指针)准备的(妄想使用CRTP在成员访问前加点私活的可以放弃了)。
class Dref {
public:
Dref* operator->() { return this; }
Dref& operator*() { return *this; }
void do_sth() {...}
};
Dref *dref = new Dref;
dref->do_sth(); //使用指针调用,并没有调用重载的operator->,使用的是系统的
(*dref)->do_sth(); //使用对象调用的operator->才是你重载的
(*dref).do_sth(); //使用指针调用,并没有调用重载的operator*,使用的是系统的
(**dref).do_sth(); //使用对象调用的operator*才是你重载的
5.lambda表达式与函数对象:定义了函数调用操作符(operator())的类型的对象被称为函数对象,它们可以被调用函数那样子调用。
class Foo{
public:
int operator() (int a, int b) const {
return a - b;
}
};
Foo minus; //函数对象
int c = minus(5, 4); //c = 1
而lambda表达式的背后,由编译器生成了一个重载了operator()的临时类的临时对象。值捕获的局部变量会被临时类的构造函数“拷贝”到类中;非mutable标记的lambda表达式对应的operator()是const的(所以不能修改捕获的变量);引用捕获的局部变量就不需要这一“拷贝”过程,而直接被编译器使用。lambda表达式和函数对象通常与泛型算法合作完成任务。
6.function类型:在c++当中,以下四种类型的对象是可以被调用的。
- 函数
- 函数指针
- lambda表达式
- 可调用对象
在混合使用它们的时候,相当的不方便,因为它们是四种不同类型的对象。为了解决这一问题,c++11中新增了function类型模板,以调用形式(“返回值-参数列表”)为Type,可以兼容以上全部类型,让它们像同一个类型的对象一样工作。
int add(int a, int b) {
return a + b;
}
int (*add_ptr) (int, int) = add;
auto add_lambda = [](int a, int b) {
return a + b;
}
class ADD {
int operator() (int a, int b) {
return a + b;
}
};
ADD add_object;
function<int(int, int)> f1 = add;
function<int(int, int)> f2 = add_ptr;
function<int(int, int)> f3 = add_lambda;
function<int(int, int)> f4 = add_object;
注意:不能将重载函数直接放入function中,需要用函数指针明确指明或者lambda临时包装一下(这方法太经典了)。
7.显式类型转换符explicit:在c++11中,新增了显式类型转换符,用来阻止编译器利用定义好的类型转换符来进行隐式类型转换。增加了explicit的类型转换符必须显式调用类型转换才可以(static_cast等)。
class Foo {
public:
Foo (int i):data(i){}
explicit operator int () {return data;}
private:
int data;
};
Foo f(1);
int a = 2 + f; //隐式转换,错误
int b = 2 + static_cast<int>(f); //ok,显式转换
注意:当表达式被当做条件处理时(if/while/do-while/for/问号表达式/&&/||/!),编译器会隐式的执行显式类型转换。
8.类型转换的best practice:除了定义显式bool类型转换符之外,不要定义其他的类型转换。