C++操作符重载深度解析

1. 操作符重载

  1. 函数重载
    1. 名同、参数不同返回值不同没有用的:参数顺序、参数类型匹配(找到最佳匹配)
    2. 静态绑定
  2. 歧义控制:
    1. 顺序:
    2. 最佳匹配:
      1. 原则一:这个匹配每一个参数不比其他的匹配更差
      2. 原则二:这个匹配有一个参数更精确匹配
    3. 整形提升:更好的,标准转换(标准转换都是一视同仁的)
    4. 窄转换?允许的,大->小
  3. 操作符重载(变为一种函数)
    1. 动机:操作符语义
      • built_in 类型
      • 自定义数据类型
    2. 作用:
      1. 提高可读性
      2. 提供可扩充性

1.1 + 操作符重载

  • 一种设计思想是: c++不希望区分内置类型和用户自定义类型的操作, 希望用户自定义类型也能使用类似内置类型的操作符进行操作.
cpp 复制代码
class Complex {
    double real, imag;
    public:
        Complex() {real = 0; imag = 0;}
        Complex(double r, double i) { real = r; imag = i; }
        Complex add(Complex& x);
};
Complex a(1,2),b(3,4),c;
c=a.add(b);//想要写成 a + b
  • 为了实现上述目标, C++ 提供了操作符重载的机制, 允许用户为自定义类型重新定义操作符的含义.
cpp 复制代码
class Complex {
    double real, imag;
    public:
        Complex() { real = 0; imag = 0; }
        Complex(double r, double i) { real = r; imag = i; }
        Complex operator + (Complex& x) {  
            Complex temp;
            temp.real = real + x.real;
            temp.imag = imag + x.imag;
            return temp;
        }
};
Complex a(1,2),b(3,4),c;
c = a.operator + (b);
  • 注意, 我们这里重载的是 + 操作符, 而且是成员函数形式的重载;
    同样, 也可以使用友元函数+全局函数的形式进行重载.
cpp 复制代码
class Complex {
    double real, imag ;
    public :
        Complex() { real = 0 ; imag = 0 ; }
        Complex(double r, double i) {
            real = r;
            imag = i;
        }
        friend Complex operator+(Complex& c1 , Complex& c2);//这个是已经预定义好的,我们这样子写就是重载
};
//全局函数
Complex operator+ (Complex& c1 , Complex& c2 ) {//全局函数重载至少包含一个用户自定义类型
    Complex temp;
    temp.real = c1.real + c2.real;
    temp.imag = c1.imag + c2.imag;
    return temp;
}//一般返回临时变量

Complex a(1,2),b(3,4),c;
c = a + b;//自动进行翻译
  • 注意 : 友元函数和全局函数的重载至少包含一个用户自定义类型的参数 , 否则会和内置类型的操作符冲突

1.2 自增自减的重载

cpp 复制代码
enum Day { SUN, MON, TUE, WED, THU, FRI, SAT};
// 我们希望 Day 类型可以进行自增操作, 也希望 Day 可以直接输出
Day& operator++(Day& d){  
    return d= (d==SAT)? SUN: Day(d+1); 
}

ostream& operator<<(ostream& os, Day d){
    const char* dayName[] = {"SUN", "MON", "TUE", "WED", "THU", "FRI", "SAT"};
    os << dayName[d];
    return os;
}

int main(){
    Day today = WED;
    cout << "Today is " << today << endl;
    Day tomorrow = ++today;
    cout << "Tomorrow is " << tomorrow << endl;
}
  • ps: 上面的 + 操作符重载, 返回值是值类型, 这里的 ++/<< 操作符重载, 返回值是引用类型, 我们希望确实修改了原来的对象

1.3 可以重载的操作符

  1. 不可以重载的操作符 :.(成员访问操作符)、.*(成员指针访问运算符/解引用访问,如下)、::(域操作符)、?:(条件操作符)、sizeof:也不重载
  • 三目, 实际上编译器看成是 if else, 但是两个参数需要优先计算, 所以不可以重载
  1. 重载基本原则:
    1. 方法:(大多数都支持,但是有的不支持)
      1. 要么类成员函数
      2. 要么带有类参数的全局函数
    2. 遵循原有语法
      1. 单目/双目:一一对应
      2. 优先级, 结合性等等都不变
  2. 限制: = () [] -> 必须是类成员函数重载, 不可以是全局函数重载!
  • 顺序是固定的? 因为第一个参数一定是用户自定义类型
  • 对于=赋值操作符, 如果不重载, 编译器会提供一个默认的赋值操作符 ,
    成员函数优先级更高, 所以你定义在全局的赋值操作符是没有意义的!
全局函数作为补充的情况/双目运算符一般友元函数, 单目运算符一般成员函数
  • obj + 1010 + obj 这两种情况同时支持, 那么就可以直接友元函数
  • 双目运算, 交换律
cpp 复制代码
class Box {
    double width;
    public:
        Box(double w) { width = w; }
        friend Box operator+(Box b, double val); // 友元函数
        friend Box operator+(double val, Box b); // 友元函数
};
不建议重载 ||&&
cpp 复制代码
if (p != 0 && strlen(p) > 0) { ... }
  • 注意, 当 p==0 的时候, 第二个条件不会被计算, 但是如果重载了 && 操作符, 那么两个条件都会被计算, 可能会引发错误,
  • 短路原则被破坏了!(逻辑与和逻辑或, 一般不可以重载)

1.4 双目运算符返回值总结

  1. 返回类型的问题: 如果没有&的时候,第一个return出现了对象拷贝,避免:临时变量不能返回拷贝
  2. 对于加减乘除等操作符, 一般返回值是值类型, 而不是引用类型

这里的例子是用来说明, 假如我们对于加减乘除等操作符, 返回值是引用类型, 会出现什么问题:

cpp 复制代码
class Rational { 
	public:
		Rational(int,int);
            const Rational& operator *(const Rational& r) const;//const写不写都行,写了更好
	private:
		int n, d;
};
// operator * 的函数体
return Rational(n * r.n, d * r.d);

Rational *result  = new Rational(n*r.n, d*r.d);
return *result;//返回引用的问题?
// w = x * y * z出现问题:出现内存泄露的问题

static Rational result;//声明为static
result.n = n * r.n;
result.d = d * r.d;
return result;//static是全局的,可以吗?不可以,同时出现两个的结果会出现问题
//if((a * b) == (c * d)) ->永真式

1.5 单目运算符重载

  • a++ vs ++a
  • prefix ++ 返回的是左值, postfix ++ 返回的是右值(加一之前的临时变量)
  • 实际上我们要区分两种情况, 方法是改变参数列表方便重载
cpp 复制代码
class Counter{
    int value;
    public:
        Counter() { value = 0; }
        Counter& operator ++() // ++a, 效率更高, 返回引用, 参数列表为空
        {   value++;
            return *this;
        }

        Counter operator ++(int) //a++
        {   Counter temp=*this; //这里的int值是什么意义?区分两个函数,dummy argument,哑元变量
            value++;
            return temp;
        }
}
  • 这里的返回值, ++a 返回的是引用类型, a++ 返回的是值类型
  • 这里也可以发现, 一般来说 ++a 效率更高, 因为没有创建临时变量

1.6 操作符 = 的重载

cpp 复制代码
A a;
A b = a; // 这里调用的是拷贝构造函数

A c, d;
c = d;  // 这里调用的一定不是拷贝构造, 而是赋值操作符重载函数
  • 如果不提供赋值操作符重载, 编译器会提供一个默认的赋值操作符, 它会将对象的每个成员逐一进行赋值
  • 赋值操作符重载不能继承? 为什么:
    • 因为拷贝构造,派生出来的类有一些新的部分. 编译器会为每个类自动生成赋值操作符,无法继承。子类需要根据自身成员重新定义赋值操作符。
  • 返回值的要求? 默认返回的是*this的引用, 但是没有强制要求
    • 支持链式赋值: a = b = c;
深拷贝的良好实践
  • 希望解决问题: 自我赋值问题, 异常安全问题
cpp 复制代码
class A{
    public:
    A& operator = (A& a) {
            //赋值
            x = a.x;
            y = a.y;
            delete []p;
            p = new char[strlen(a.p)+1];
            strcpy(p,a.p);
            return *this;//也会出现悬垂
    }//还有问题,就是赋值自身会出现问题
}; // 方法一, 如果 new 失败, 那么抛出异常, 对象被破坏! p 成为悬垂指针

class A{
    public:
    A& operator = (A& a) {
        //更安全的拷贝,先new再delete
        char *pOrig = p;
        p = new char ...
        strcpy();
        delete pOrig;
        return *this;
    }
}; 
自赋值问题
cpp 复制代码
class A{
    public:
    A& operator = (A& a) {
        if (this == &a) //检测自赋值
            return *this;
        //赋值
        ...
    }
};

1.7 操作符 [ ] 的重载

  • 当你的类内部维护一个数组, 希望通过 obj[index] 的形式访问数组元素 时, 可以重载 [] 操作符
  • 下面是一个 string 的类
cpp 复制代码
class string {
    char *p;
    public :
        string(char *p1){
            p = new char [strlen(p1)+ 1];
            strcpy(p,p1);//#pragma warning(disable:4996)来屏蔽问题
        }
        char& operator[] (int i) {
            return p[i];
        }
        const char operator[](int i) const {
            return p[i];
        }
        //可以用两个重载函数吗?是可以的
        virtual ~string() { delete[] p ; }
};

int main() {
    string s("aabbcc");
    s[0] = 'z'; // 调用 char& operator[](int i)
    const string cs("const");
    char ch = cs[1]; // 调用 const char operator[](int i) const
    cs[0] = 'x'; // 错误, 不能修改 const 对象
}
  • 这里的返回值是不是需要加引用?
    • 如果我们希望对他进行赋值, 那么需要加引用
  • char & operator[](int i) const 这是一个奇怪的现象, 虽然理论上这是一 const, 但是返回值是引用, 所以可以进行赋值操作, 修改了 const 的语义!
  • 所以我们需要提供两个重载版本, 一个是 const 版本(返回左值), 一个是非 const 版本(返回右值), 对于可变对象, 调用非 const 版本; 对于不可变对象, 调用 const 版本 (醍醐灌顶, 这个时候, 我们人为保证了 const 的语义安全性!)
    • 这里的参数列表实质上不同, this 指针的类型不同, 所以可以重载成功
    • 下标操作符的重载都是成对出现的
多维数组 + []
  • 如果是一个二维数组的话, 我们怎么重载
  • 如果是一个三维数组 arr[i][j][k], 我们期望中的是第一次, 偏移 i * (n2 * n3), 第二次偏移 j * n3, 第三次偏移 k;
    但是, 如果第一次返回的是 int * ,第二次就做不到运算符重载, 因为是内置类型;
    我们可以使用 自定义类型(代理类)!
cpp 复制代码
class Array2D{
public:
    class Array1D{
    public:
        Array1D(int *p) {this->p = p;}
        int& operator[](int j) {
            return p[j];
        }
        const int& operator[](int j) const {
            return p[j];
        }
    private:
        int *p;
    };
    Array2D(int rows, int cols) {
        this->rows = rows;
        this->cols = cols;
        data = new int[rows * cols];
    }
    ~Array2D() {
        delete[] data;
    }
    Array1D operator[](int i) {
        return Array1D(data + i * cols);
    }
    const Array1D operator[](int i) const {
        return Array1D(data + i * cols);
    }
private:
    int rows;
    int cols;
    int *data;
};

1.9 操作符 () 的重载

  • 函数调用操作符重载, 我们可以传入一个函数对象
cpp 复制代码
class Func{
    double para;
    int lowerBound, upperBound;
public:
    double operator()(double, int, int); 
};

Func f;
f(3.14, 1, 10); // 调用 operator()
  • 比如我们传入一个 class errorlogger, 对象已经绑定到了日志文件, 我们将这个对象当成函数来调用, 传入错误信息, 这样就可以实现日志记录功能
  • 再比如, cpp 中的 sort,
    可以传入一个函数指针(但是不能保存对象的状态);
    可以传入函数对象, 定义一个类, 并且重载 () 操作符(可以保存状态);
    可以传入匿名函数, lambda 函数;

函数指针 vs 函数对象:

  • 函数指针
    • 不能保存状态,
    • 编译器无法内联优化, 效率较低
  • 函数对象
    • 对象, 可以携带成员变量
    • 是类, 编译器可以内联优化, 效率较高
lambda 函数

可以捕获作用域之内的变量, 是创建匿名函数对象的方式, 编译器会自动生成一个类, 并且重载 () 操作符

语法:

  • 捕获:
    • [], 不捕获任何变量
    • [=], 按值捕获所有变量
    • [&], 按引用捕获所有变量
    • [x, &y], 按值捕获 x, 按引用捕获 y
      举例:
cpp 复制代码
vector<string> str_filter(vector<string> &vec, function<bool(const string&)> matched) {
    vector<string> result;
    for (string tmp: vec) {
        if (matched(tmp)) {
            result.push_back(tmp);
        }
    }
    return result;
}

int main() {
    vector<string> vec = {"www.example.com", "test.org", "sample.com", "hello.net"};
    string pattern = ".com";
    // 使用 lambda 函数作为过滤条件
    vector<string> filtered = str_filter(vec, 
    [&](const string &s) -> bool {
        return s.find(pattern) != string::npos;
    }
    );
}
  • std::function 灵活的封装函数, 可以保存函数指针, 函数对象, lambda 函数等, 作为参数传递给其他函数

1.10 类型转换 操作符重载

  • 类型转换操作符重载, 允许我们定义如何将一个自定义类型转换为另一个类型
  • 基本类型 / 用户自定义类型
  • 可以减少混合计算中需要定义的重载操作符的数量
cpp 复制代码
class Rational {
private: int n, d;
public:
    Rational(int n, int d) : n(n), d(d) {}
    operator double() const {
        return static_cast<double>(n) / d;
    }
};
  • 比如:ostream f("abc.txt"); if (f) {...}, 这就是调用了 operator bool()

问题:

  • 如果我们定义了 operator double(), 那么 Rational 对象可以隐式转换为 double, 可能会引发意外的类型转换, 需要谨慎使用;
  • 如果有多个类型转换操作符, 可能不能确定调用哪个, 需要避免歧义; explicit 关键字可以避免隐式转换引发的问题
cpp 复制代码
class A {
public:
    operator int() { return 1; }
    explicit operator double() { return 2.0; }
};
A a;
cout << a + 1; // 如果没有 explicit, 那么错误, 不确定调用哪个转换操作符
// 如果有 explicit, 那么只能调用 operator int(), double 需要显式转换
cout << static_cast<double>(a) + 1.0; // 显式调用

1.11 操作符 -> 的重载

  • -> 实际上是二元运算符, 但是重载的时候按照一元操作符重载描述;
  • 一个非常重要的应用是实现智能指针
cpp 复制代码
A a;
a->f();
a.operator->(f);
a.operator->()->f(); //重载时按一元操作符重载描述,这时,a.operator->()返回一个指针(或者是已经重定义过->的对象)

这里的两个 -> 不需要都重载, 只需要重载第一个 -> 即可, 返回一个指针类型 或者是一个已经重载过 -> 的对象

Prevent memory Leak, 避免内存泄漏

问题描述代码:

cpp 复制代码
class A{
    public:
    void f();
    int g(double);
    void h(char);
};
void test(){
    A *p = new A;
    p->f();//如果出错不能顺序执行, 会内存泄漏
    p->g(1.1);//返回值
    p->h('A');
    delete p;
}

使用智能指针的方案:

cpp 复制代码
// 使用智能指针
class AWrapper{//不包含逻辑
    A* p;// ? T p; 支持多个类型
    public:
        AWrapper(A *p) { this->p = p;}
        ~AWrapper() { delete p;}
        A*operator->() { return p;}
};

void test(){
    AWrapper p(new A);
    p->f();// 自动调用AWrapper::operator->()
    p->g(1.1);
    p->h('A');
} // 退出的时候自动调用AWrapper::~AWrapper(), 按照编译器控制的生命周期
// RAII 资源获取即初始化
  • 智能指针的局限性, 只能按照栈对象的生命周期进行管理, 不能跨函数传递

1.12 一个空的类包含哪些函数

cpp 复制代码
class Empty{
    Empty() {} // 默认构造函数
    Empty(const Empty& ) {} // 拷贝构造函数
    Empty(Empty&& ) noexcept {} // 移动构造函数
    ~Empty() {} // 析构函数
    Empty& operator=(const Empty& ) { return *this; } // 拷贝赋值
    Empty& operator=(Empty&& ) noexcept { return *this; } // 移动赋值
    Empty *operator &(); // 取地址
    const Empty* operator &() const; // 取地址 const 版本
}

智能指针的重要例子(考试重点)

  1. 异常处理 / 析构函数
  2. 异常处理 可以解决的问题
  3. 析构函数 可以解决的问题
cpp 复制代码
template <typename T>
class auto_ptr{
private:
    T* ptr;
public:
    auto_ptr(T *p = 0): ptr(p) {}
    ~auto_ptr() { delete ptr;}
    T* operator->() const {return ptr;} // 当作指针使用
    T& operator *() const {return *ptr;} // 指针解引用
};
  • 一个类似的实际应用,在GUI中

    cpp 复制代码
    void displayInfo(const Information& info) {
        WINDOW_HANDLE w(createWindow());
        // display info in window corresponding to w
        destroyWindow(w); // 如果中间代码抛出异常, 这里不会被执行, 导致内存泄漏
    }

    解决方案:(智能指针, 但是没有重载 ->, 而是重载类型转换, 也是一种解决方案)

    cpp 复制代码
    class WindowHandleWrapper {
    public:
        WindowHandleWrapper(WINDOW_HANDLE handle) : w(handle) {}
        ~WindowHandleWrapper() { destroyWindow(w); }
        operator WINDOW_HANDLE() const { return w; }
    private:
        WINDOW_HANDLE w;
        WindowHandleWrapper(const WindowHandleWrapper&); // 禁止拷贝
        WindowHandleWrapper& operator=(const WindowHandleWrapper&); // 禁止赋值
    };
    
    void displayInfo(const Information& info) {
        WindowHandleWrapper w(createWindow());
        // display info in window corresponding to w
    } // 退出时自动调用析构函数, 释放资源

1.14 new/delete 操作符重载

  • 频繁调用系统的存储管理, 影响效率
  • 程序自身管理内存, 提高效率
  • 方法:
    • 调用系统存储分配, 申请一块大内存, 自行管理
    • 针对该内存, 自己管理存储分配, 去配
    • 通过重载new和delete操作符实现
    • 重载的 new 和 delete 必须是静态成员函数或者全局函数
    • 重载的 new 和 delete 遵循类的访问控制, 可以继承
1.14.1 重载 new
  1. void *operator new (size_t size, ...), 可以有多个参数
  2. 名:operator new
  3. 返回类型:void *
  4. 第一个参数:size_t(unsigned int)
  • 系统自动计算对象的大小, 并且传递给 new 操作符重载函数
  1. 其他参数:用户自定义
  • A *p = new (arg1, arg2, ...) A;
  1. 如果重载一个 new, 那么不再调用系统默认的 new, 而是调用用户自定义的 new
  2. 重载了 new, 但是不代表 new [] 也重载了, 需要单独重载 new []

一些特殊的new:

  1. void *operator new (size_t) throw (std::bad_alloc); 标准版本的 operator new, 如果内存分配失败, 抛出异常 std::bad_alloc
  2. void *operator new (size_t, void* p);
  • placement new, 定位 new , 允许在指定的内存位置创建对象, 不进行内存分配;适合我们实现内存池等高级内存管理技术
  • 直接调用构造函数在已有内存地址上构造对象
  1. void *operator new (size_t, std::nothrow_t&) noexcept;
  • 不抛出异常的 new, 如果内存分配失败, 返回nullptr
1.14.2 重载 delete
  • void operator delete (void *p, size_t size);, 第二个参数可有可无
  • delete 的重载只能有一个
  • 如果重载了 delete, 那么不再调用系统默认的 delete, 而是调用用户自定义的 delete
相关推荐
武子康1 小时前
Java-205 RabbitMQ 工作模式实战:Work Queue 负载均衡 + fanout 发布订阅(手动ACK/QoS/临时队列)
java·性能优化·消息队列·系统架构·rabbitmq·java-rabbitmq·mq
CodeCraft Studio1 小时前
Vaadin 25 正式发布:回归标准Java Web,让企业级开发更简单、更高效
java·开发语言·前端·vaadin·java web 框架·纯java前端框架·企业级java ui框架
阿拉斯攀登1 小时前
电子签名:笔迹特征比对核心算法详解
人工智能·算法·机器学习·电子签名·汉王
ytttr8732 小时前
matlab进行利用遗传算法对天线阵列进行优化
开发语言·算法·matlab
十五年专注C++开发2 小时前
QTableWidget和QTableView插入数据比较
c++·qt·qtablewidget·qtableview
一招定胜负2 小时前
机器学习算法三:决策树
算法·决策树·机器学习
无限进步_2 小时前
【C语言】队列(Queue)数据结构的实现与分析
c语言·开发语言·数据结构·c++·算法·链表·visual studio
Haoea!2 小时前
JDK21新特性-序列集合
java
特立独行的猫a2 小时前
Google C++ 编码规范核心要点总结 (2025精简版)
开发语言·c++·编码规范