C++运算符重载:怎样定义函数才“符合常理”

本文探讨了C++中运算符重载的关键问题,包括返回值类型的选择、参数类型的选择以及是否添加const修饰符等问题。通过具体示例,详细解释了如何正确地重载运算符,以确保代码符合C++的习惯。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

最近在尝试实现C++ STL库,遇到的一个很重要问题就是在容器、迭代器中的运算符重载。运算符重载看上去比较简单,但如果希望重载结果“符合常理”,即符合C++的运算习惯,也不是非常容易。这些问题通常出现在函数定义中:
- 重载函数的返回值,是原类型、引用类型,还是常引用类型?
- 重载函数的参数,是原类型、引用类型、常引用类型,还是右值引用类型?
- 重载函数是否需要const标记?
这篇博客主要讨论这些问题。

1 返回值

函数返回值有3种选择:
- 原类型T
- 引用类型T&
- 常引用类型const T&
首先应该区分原类型和引用类型。一个判决标准是:如果返回对象是在函数内部定义的,就一定要返回原类型,否则(返回值是this指向的对象,或返回值为某个函数参数),返回引用类型即可。
一个常见(且非常危险)的错误是:

class T{
public:
    T& operator+(const T& t){
        T tReturn;
        //TODO:计算tReturn
        return tReturn;
    }
};
T a,b,c;
c = a + b;

这段代码的问题在于,当返回tReturn的时候,只返回了其引用,而tReturn是函数内部定义的,在函数结束的时候即被销毁。因此,c指向的是一个无意义的空间(在堆栈中)。
而正确的代码为:

T operator+(const T& t){...}

使用原类型,函数返回时会调用复制构造函数,将返回值存储在安全的位置。
如果是返回本对象,例如return *this,则需要使用引用类型,此时需要区分常引用和非常引用类型。一般来说,如果你不希望返回值的接收者修改返回对象,则应使用常引用类型(const T&),否则应该使用普通引用类型(T&)。

例如,赋值运算符的重载应该返回什么类型呢?我们可以考虑如下运算的结果:

int a = 1, b = 2;
(a = b) = 3;
cout << a << " " << b;

执行完毕后,会得到3 2。注意到a = b运算后,应有a == 2, b == 2。但接下来得到a == 3的结果,说明a = b运算返回了a,而且是a的普通引用。这样,系统会继续执行a = 3语句,将a赋值给3。
由此可知,赋值运算符重载函数的返回值应为普通引用T&

再举一个例子:考虑自加运算符(++),其返回值应该如何设置?
如果是前置自加(++a),应该是先自加再返回,因此可以先把对象本身自加,再返回对象的引用。这时需要注意,一般约定++运算符的返回值不可被修改(即所谓的“右值”),我们重载运算符时也应遵循这一习惯。所以,前置自加的返回值为const T&

class T{
public:
    const T& operator++(){
        //TODO: 对象的自加操作
        return *this;
    }
};

而对于后置自加(a++),则应该返回原值,然后再自加。此时,一般的做法是先复制对象的一个副本,自加对象后,返回副本对象。由于副本对象是在函数内部定义的,所以返回值为原类型T

class T{
public:
    T operator++(int){//C++中约定int参数表示后置自加
        T tmp = *this;
        //TODO: this对象的自加
        return tmp;
    }
};

2 参数

所有双目运算符都需要再传入一个参数(一般与重载对象的类型相同),该参数的类型有如下几种选择:
- 原类型T
- 引用类型T&
- 常引用类型const T&
- 右值引用类型T&&
首先,原类型T一般不会使用,因为会调用复制构造函数,造成额外的成本。只要在函数中不会修改参数的值,均可用常引用类型const T&
如果在函数中会修改参数的值,则应该使用引用类型T&,但这种情况很少见,因为一般的双目运算符都不会修改其右侧运算数的值。唯一的特例是括号运算符operator(),这是因为括号运算符是用于函数对象,换句话说,它直接代表了一个函数,所以参数可以是任意类型。例如:

class T{
public:
    void operator()(int a, string b, T& c){...}
};
T t;
t(1, 2, t);//这就类似于调用一个名为t的函数

最后,值得注意的是右值引用T&&。参数为右值引用说明了该参数为临时对象,即将被销毁,因此其中的一些动态开辟的变量可以被重载函数回收利用。右值引用通常用于含复制操作的函数中,最典型的例子就是赋值运算符。

class T{
public:
    //operator=需要两个重载函数
    T& operator=(const T& t);//参数为左值,说明将来还会用到,因此只能复制里面的数据
    T& operator=(T&& t);//参数为右值,很快就会被销毁,因此可以直接把里面的数据“拿来”(该函数也可以不写,但效率相应会有影响)
}
T t1, t2, t3;
t1 = t2; //t2作为左值传入,运算结束后,t2的值不会改变。
t3 = T(); //这个新生成的T作为右值传入。
t3 = std::move(t2);//t2被强制转换成右值输入,在运算结束后,t2的值可能会改变。

3 const标记

const标记相对比较简单:如果函数内部不会修改本对象的值,则(建议)加上const标记。最简单的情况是:关系运算符重载函数都应该是const类型的,四则运算、位运算等不修改被运算数的运算符也是const类型的。
值得注意的是const函数的2个性质:
1. 即使两个函数的区别仅为一个有const标记,一个没有const标记,这两个函数也是不同的函数;
2. const函数只能调用const函数,const对象也只有const函数可以被访问。
因此,某些返回引用类型的函数不应该设置为const类型,否则,const对象也可以调用该函数得到引用类型,并修改该引用类型,造成不符合预期的结果。
举例来说,类似迭代器这样的类会重载operator*来模拟指针的“获取相应地址的对象”的行为,该函数应该返回所指向对象的引用。问题是,对于一般的对象,应该返回非const引用类型,对于const对象,则应该返回const引用。因此,可以定义如下的重载函数:

class T1{
public:
    T2& operator*(){
        return t;
    }
    const T2& operator*() const{
        return t;
    }
private:
    T2 t;
};
T t1;//(*t1)返回t1.t的引用
const T t2;//(*t2)返回t2.t的常引用

4 常见运算符的重载规则

综上,可以得出常见运算符的重载规则(并非全部),注意这些规则并不是绝对的(理论上你可以任意定义),只是所谓“符合常理”的。例外仍有很多,例如ostreamoperator<<定义的就不是按位左移操作,也不符合下面的规则。定义运算符的时候,应该注意思考,用户在使用该运算符时,所期待的结果是什么?

运算符返回值参数是否const
双目+, -, *, /, %, ^, |, &, <<, >>Tconst T&
>, <, >=, <=, ==, !=boolconst T&
~T
!bool
&&, ||, !boolconst T&
+=, -=, *=, /=, %=, ^=, |=, &=, <<=, >>=T&const T&
前置++, - -const T&
后置++, - -Tint
赋值运算符=T&const T&或T&&
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值