Skip to content

8 运算符重载

成员访问运算符

运算符名 语法 可重载 类定义内(成员函数原型) 类定义外(友元/全局函数原型)
下标 a[b]
a[...](C++23起)
R& T::operator[](S b);
R& T::operator[](...);
不适用
间接寻址(解引用) *a R& T::operator*(); R& operator*(T a);
取地址 &a R* T::operator&(); R* operator&(T a);
对象的成员访问 a.b 不适用 不适用
指针的成员访问 a->b R* T::operator->(); 不适用
指向对象的成员的指针 a.*b 不适用 不适用
指向指针的成员的指针 a->*b R& T::operator->*(S b); R& operator->*(T a, S b);

下标运算符

C++ 规定,下标运算符 [] 必须以成员函数的形式重载。为了同时支持普通对象和 const 对象,我们需要提供两个重载版本

  • const 版本:支持「读+写」元素,
    原型R& T::operator[](S b);
  • const 版本:只支持「读」元素,不允许修改,
    原型const R& T::operator[](S b) const;

其他运算符

运算符名 语法 可重载 类内定义(成员函数原型) 类外定义(友元/全局函数原型)
函数调用 a(a1, a2) R T::operator()(Arg1 &a1, Arg2 &a2, ...); 不适用
逗号 a, b T2& T::operator,(T2 &b); T2& operator,(const T &a, T2 &b);
条件(三目) a ? b : c 不适用 不适用

函数调用运算符

  • 参数可以是任意个、任意类型(0 个、1 个、多个都可以,支持重载多个版本)。返回值类型也可以自定义,根据需求返回结果。
  • 当一个类重载了operator(),它的对象就可以像函数一样被调用,这种类叫可调用(callable)类,它的对象叫函数对象。
  • 可以持有状态(比如计数器、配置参数),普通函数只能靠全局 / 静态变量实现,不安全且不灵活。
  • 函数对象在 C++ 标准模板库(STL)中扮演着核心角色。STL 的算法(如 sort, for_each)经常接受函数对象作为参数,以实现自定义的比较逻辑或操作。
//函数对象
class GreaterThan {
    int baseline;
    public:
        GreaterThan(int x):baseline(x) {}
        bool operator () (const int& x) {
            return x>baseline;
        }
};
//普通函数
bool GreaterThan10(const int& x)   {
    return x>10;
}
bool GreaterThan20(const int& x)   {
    return x>20;
}
  • C++ 标准库(STL)中许多算法(如 find_if)需要传入“谓词函数”作为参数,这种参数既支持普通函数,也支持函数对象以及 lambda 函数

逗号运算符

  • 语法:a, b,原生逗号表达式的语义是「从左到右依次求值,返回右边的值」(比如i++, j++)。
  • 重载规则:既可以类内成员函数重载,也可以类外友元/全局函数重载
  • 避坑提醒:重载逗号运算符会破坏原生逗号的「顺序求值」特性,比如重载后a, b可能先算b再算a,和原生行为不一致。因此实际开发中几乎不会重载逗号运算符,除非有特殊场景需求。

友元

友元函数

在某些特定场景下,我们需要让外部的某个函数直接访问这些私有数据,这时就需要用到友元。它允许一个非成员函数访问该类的私有(private)和保护(protected)成员。

要在一个类中声明友元函数,只需在函数声明前加上关键字 friend

  • 声明位置:友元函数的声明可以放在类的 publicprivateprotected 区域,其效果是相同的,因为它不属于类的成员。
  • 定义方式:友元函数本身是一个全局函数,它的定义不需要使用 类名:: 的作用域解析符。
    class Integer {
        int x;
        friend istream& operator >> (istream& is,Integer& Int);
        friend ostream& operator << (ostream& os,const Integer& Int);
    };
    
    istream& operator >> (istream& is,Integer& Int) {
        is>>Int.x;
        return is;
    }
    
    ostream& operator << (ostream& os,const Integer& Int) {
        os<<Int.x;
        return os;
    }
    

友元类

有些时候,其他类的成员函数可能会很多,一个一个的声明为友元函数会比较麻烦。所以我们就可以直接声明友元类。

class Integer {
    int x;
    friend class IntegerFriend;
};

class IntegerFriend {
    public:
        int getX() const {return x;}
};

隐式类型转换

重载协议

如果重载的函数参数一样,可以通过转换到某个重载函数,编译会如何哪个版本的选择?

class Integer {
    int x;
public:
    Integer(int x = 0) : x(x) {}
    friend ostream& operator<<(ostream& o, const Integer& hs) {
        o << "const " << hs.x;
        return o;
    }
    friend ostream& operator<<(ostream& o, Integer& hs) {
        o << "no_const " << hs.x;
        return o;
    }
};

int main() {
    Integer i1(1);
    const Integer i2(2);
    cout << i1 << "," << 3 << "," << i2 << endl;
}

匹配规则:

  1. 类型直接匹配优先:实参的const属性和形参完全一致的版本,优先级最高。
  2. const实参的限制const类型的实参,只能匹配const版本的重载函数。
  3. const实参的降级匹配:非const类型的实参,优先匹配非const版本;如果没有非const版本,会隐式转换为const,匹配const版本的重载。

const实参优先找非const版本,找不到就降级转constconst实参只能找const版本,找不到直接报错。

空指针

class Integer {
    int x;
public:
    Integer(int x=0):x(x) {}
    friend Integer operator+(const Integer &lhs, Integer rhs){
        return lhs.x + rhs.x;
    } 
    friend Integer operator+(const Integer &lhs, Integer *rhs){
        if (rhs) return lhs.x + rhs->x + 2000;
        else return lhs.x + 1000;
    }
    friend ostream& operator<<(ostream& o, const Integer &hs) {
        o << hs.x; return o;
    } 
};

class Girlfriend {/* ... */};
void kissGirlfriend(Girlfriend* gf) {cout << "pointer"<<endl;};
void kissGirlfriend(int gfID){cout << "int"<<endl;};

int main() {   
    kissGirlfriend(nullptr);  // 指针类型字面量(void *)0 
    kissGirlfriend(0); // 尝试 NULL 取代 0 

    Integer i1(1), i2;
    i2 = i1 + 0;  // 尝试 1,nullptr,NULL,&i1 取代 0 
    cout << i2 << endl;
}
在运行i2 = i1 + 0;时,会调用operator+(const Integer &lhs, Integer *rhs)
因为0 做 标准转换:可以直接变成 空指针 Integer*(C++ 天生支持整数 0 转任何指针类型)→ 这是编译器自带的标准转换,速度最快、优先级最高。
0 做 自定义转换:需要调用构造函数 Integer(0) 才能变成 Integer 这是用户写的转换,优先级更低。

explicit 关键字

为了避免这种情况,我们可以使用explicit关键字。用于关闭这种自动类型转化的特性。
即被 explicit 关键字修饰的类构造函数,不能进行自动地隐式类型转换,只能显式地进行类型转换。

class Integer;
struct Cmp {
    bool operator () (const Integer& a, const Integer& b);
};

class Integer {
    int x;
public:
    // atoi is a stl function that convert a c-string to int
    explicit Integer(const char* s):x(atoi(s))   {}
    Integer(int x=0):x(x)   {}
    friend Cmp;
};

bool Cmp::operator () (const Integer& a, const Integer& b)   {
    return a.x<b.x;
}

int main()  {
    Integer a("3"), b("4");
    Cmp compare;
    cout<<compare(a,b)<<endl; // of course ok
    // some codes
    cout<<compare(Integer("1"),Integer("2"))<<endl; // explicit construction, ok
    cout<<compare((Integer)"1",(Integer)"2")<<endl; // explicit conversion, ok
    cout<<compare("1","2")<<endl; // error
    cout<<compare("a","b")<<endl; // error
    return 0;
}