一、运算符重载
在函数的重载中,运算符的重载可以说是一个较为特殊的存在。毕竟对于很多开发者来说,虽然可能经常用到,但真正自己写这些重载的还是较为少见的。运算符的重载分为两种方式,即类成员函数重载和类外普通函数重载。而类外普通函数重载一般会用到友元,而友元在多数的开发者眼中也算是一个比较鸡肋的存在。
C++中可以重载的运算符有四十多个,需要注意的是,运算符的重载还是要符合普通的认知,而不能胡乱重载。比如把+重载为*法,这就过分了。
二、主要的问题
面对运算符的重载,可能遇到的问题主要有以下几种(具体的例子在下面的解决方法中一并给出):
- 运算符的可重载性问题
最基础的就是要注意哪些运算符可以重载哪些不可以重载。哪些严格限制重载,哪些可以任意重载。不可重载的主要有成员访问运算符“.”(对象.成员),作用域解析符“::”,条件运算符“?:”,类型转换“sizeof”,类型标识“typeid”,内存对齐“alignof”,强制类型转换“static_cast、dynamic_cast等”。 - 重载 operator->引发的无限递归问题
这种属于对指针的操作符的理解问题,但它的不正常的操作会引发无限递归,从而导致程序崩溃 - 重载&&或||的短路问题
这个其实更多的是对&&或||运算符的错误应用,只不过是在重载中又犯一次罢了 - 重载=未进行自赋值检查的问题
这个也一个比较常见的问题,一般在面试的时候儿经常会遇到 - 重载new/delete问题
一般来说是不推荐对这两个运算符进行重载的,因为它们会导致原内存处理链路的中断。而开发者手动介入的结果往往会因为一些特殊情况导致程序的崩溃。特别是有的时候忘记同时对new[]/delete[]的重载,导致严重的问题 - 重载的对称性问题
这个非常重要,在提供一些运算符时,是否满足交换律。说得简单一些,前后的顺序颠倒可能引起程序的异常 - 重载++运算符的问题
此运算符的重载重点在于语义的兼顾,很多开发者往往是按照自己的想法就写了出来,往往出现意想不到的问题。
其实在运算符的重载中还有各种问题,但基本已经属于代码本身的问题而不是重载运算符导致的。比如返回一些空值、临时变量的引用之类的问题。开发者还是要搞明白它们与运算符重载已经基本没有关系了。
三、解决方法
上面说明虽然简单,但有可能引起一些理解上的问题,下面就着代码分析,然后给出相应的解决方案:
- 运算符的可重载性问题
属于强制性的问题,不举例分析了 - 重载 operator->引发的无限递归问题
看下面的例子:
#include <iostream>
class DemoPointer {
private:
int* p_;
public:
DemoPointer(int* p = nullptr) : p_(p) {}
// 错误重载:返回自身
DemoPointer& operator->() {
std::cout << "called DemoPointer::operator->() err!" << std::endl;
return *this; // 无限递归
}
//正确重载
//int* operator->() {
// std::cout << "called DemoPointer::operator->() ok!" << std::endl;
// return p_; // 返回原始指针
//}
void testFunc() {
std::cout << "called testFunc() called" << std::endl;
}
~DemoPointer() {
delete p_;
}
};
int main() {
DemoPointer p_(new int(100));
// 无限递归导致栈溢出
// p_->testFunc();
return 0;
}
简单说明一下为什么会导致无限递归,代码中“p_->testFunc() 调用 p_.operator->()”,然后返回的DemoPointer继续调用“operator->()”,然后不断重复这个步骤,然后就没有然后了。
上面的错误代码如果返回引用也是一个道理,所以就不再展开说明了。
- 重载&&或||的短路问题
看下面的例子:
class Demo {
public:
Demo(bool b) : value_(b) {}
operator bool() const { return value_; }
Demo operator&&(const Demo& other) const {
return value_ && other.value_;
}
private:
bool value_;
};
int main(){
Demo d1(true);
Demo d2(false);
//短路
if (d1 && d2) {
std::cout<<"result is:"<<d1&&d2<<std::endl;
}
}
- 重载=未进行自赋值检查的问题
看下面的例子:
class OwnerString {
char* data_;
public:
OwnerString(const char* s) {
data_ = new char[strlen(s) + 1];
strcpy(data_, s);
}
// 注释部分代码后:未处理自赋值
OwnerString& operator=(const OwnerString& other) {
//if (this == &other) {//自赋值检测
//return *this;
//}
delete[] data_;
data_ = new char[strlen(other.data_) + 1];
strcpy(data_, other.data_);
return *this;
}
~OwnerString() { delete[] data_; }
};
- 重载new/delete问题
看下面的例子:
class AllocDemo {
public:
void* operator new(size_t size) {
return malloc(size);
}
void operator delete(void* ptr) {
free(ptr);
}
};
上面的代码看上去是没有问题的,分配和释放成对的出现么。但实际上这有一个关键的问题,开发者能不能够保证所有的delete传入的ptr都是自定义分配的(malloc),如果无法保证,结果就只有一个,崩溃。这也是为什么反复强调重载new/delete要慎之又慎的原因。
- 重载的对称性问题
看下面的例子:
class Complex {
double real_, imag_;
public:
Complex(double r, double i = 0) : real_(r), imag_(i) {}
// 提供左操作数为 Complex重载
Complex operator+(const Complex& other) const {
return Complex(real_ + other.real_, imag_ + other.imag_);
}
};
int main(){
Complex c(3, 6);
Complex r1 = c + 3.0; // OK
Complex r2 = 3.0 + c; //err:无此重载
return 0;
}
}
这种问题就是考虑不周,只要增加相应的匹配重载即可:
// 如下面的非成员函数重载
Complex operator+(double lhs, const Complex& rhs) {
return Complex(lhs + rhs.real, rhs.imag);
- 重载++运算符的问题
看下面的例子:
class CalcAdd {
int value_;
public:
CalcAdd(int v = 0) : value_(v) {}
CalcAdd& operator++() {//返回一个自身的引用,支持链式调用
++value_;
return *this;
}
CalcAdd operator++(int) { //必需一个int占位符
CalcAdd temp = *this;
++value_;
return temp; //返回副本
}
};
int main(){
CalcAdd ca(5);
++ca;
ca++;
return 0;
}
一定要注意的是,前置++返回引用,后置++返回副本。不要弄错了。
侯捷老师说过,代码之前,了无秘密。看代码就一切都明白了。
四、总结
C++有很多的技术细节需要掌握,上千页的文档不好看啊。其实不用每个细节都掌握,毕竟每个开发者的精力也有限。强干密枝,空隙不可能全面笼罩。真正用到的时候再认真学习相关的技术点就可以了,不必为此徒增烦恼。

251





