跟我学C++中级篇——重载问题分析之运算符重载的问题分析

一、运算符重载

在函数的重载中,运算符的重载可以说是一个较为特殊的存在。毕竟对于很多开发者来说,虽然可能经常用到,但真正自己写这些重载的还是较为少见的。运算符的重载分为两种方式,即类成员函数重载和类外普通函数重载。而类外普通函数重载一般会用到友元,而友元在多数的开发者眼中也算是一个比较鸡肋的存在。
C++中可以重载的运算符有四十多个,需要注意的是,运算符的重载还是要符合普通的认知,而不能胡乱重载。比如把+重载为*法,这就过分了。

二、主要的问题

面对运算符的重载,可能遇到的问题主要有以下几种(具体的例子在下面的解决方法中一并给出):

  1. 运算符的可重载性问题
    最基础的就是要注意哪些运算符可以重载哪些不可以重载。哪些严格限制重载,哪些可以任意重载。不可重载的主要有成员访问运算符“.”(对象.成员),作用域解析符“::”,条件运算符“?:”,类型转换“sizeof”,类型标识“typeid”,内存对齐“alignof”,强制类型转换“static_cast、dynamic_cast等”。
  2. 重载 operator->引发的无限递归问题
    这种属于对指针的操作符的理解问题,但它的不正常的操作会引发无限递归,从而导致程序崩溃
  3. 重载&&或||的短路问题
    这个其实更多的是对&&或||运算符的错误应用,只不过是在重载中又犯一次罢了
  4. 重载=未进行自赋值检查的问题
    这个也一个比较常见的问题,一般在面试的时候儿经常会遇到
  5. 重载new/delete问题
    一般来说是不推荐对这两个运算符进行重载的,因为它们会导致原内存处理链路的中断。而开发者手动介入的结果往往会因为一些特殊情况导致程序的崩溃。特别是有的时候忘记同时对new[]/delete[]的重载,导致严重的问题
  6. 重载的对称性问题
    这个非常重要,在提供一些运算符时,是否满足交换律。说得简单一些,前后的顺序颠倒可能引起程序的异常
  7. 重载++运算符的问题
    此运算符的重载重点在于语义的兼顾,很多开发者往往是按照自己的想法就写了出来,往往出现意想不到的问题。

其实在运算符的重载中还有各种问题,但基本已经属于代码本身的问题而不是重载运算符导致的。比如返回一些空值、临时变量的引用之类的问题。开发者还是要搞明白它们与运算符重载已经基本没有关系了。

三、解决方法

上面说明虽然简单,但有可能引起一些理解上的问题,下面就着代码分析,然后给出相应的解决方案:

  1. 运算符的可重载性问题
    属于强制性的问题,不举例分析了
  2. 重载 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->()”,然后不断重复这个步骤,然后就没有然后了。
上面的错误代码如果返回引用也是一个道理,所以就不再展开说明了。
  1. 重载&&或||的短路问题
    看下面的例子:
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; 
  } 
}
  1. 重载=未进行自赋值检查的问题
    看下面的例子:
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_; }
};
  1. 重载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要慎之又慎的原因。

  1. 重载的对称性问题
    看下面的例子:
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);
  1. 重载++运算符的问题
    看下面的例子:
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++有很多的技术细节需要掌握,上千页的文档不好看啊。其实不用每个细节都掌握,毕竟每个开发者的精力也有限。强干密枝,空隙不可能全面笼罩。真正用到的时候再认真学习相关的技术点就可以了,不必为此徒增烦恼。

评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值