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。
- 声明位置:友元函数的声明可以放在类的
public、private或protected区域,其效果是相同的,因为它不属于类的成员。 - 定义方式:友元函数本身是一个全局函数,它的定义不需要使用
类名::的作用域解析符。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;
}
匹配规则:
- 类型直接匹配优先:实参的
const属性和形参完全一致的版本,优先级最高。 const实参的限制:const类型的实参,只能匹配const版本的重载函数。- 非
const实参的降级匹配:非const类型的实参,优先匹配非const版本;如果没有非const版本,会隐式转换为const,匹配const版本的重载。
非const实参优先找非const版本,找不到就降级转const;const实参只能找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;
}