运算符重载与友元
运算符重载
基本概念
在 C++ 中,当操作数包含类对象时,运算符操作本质是调用对应的函数(称为“运算符重载函数”)。
核心逻辑
- 示例:
A a, b; a + b;等价于a.operator+(b); - 特殊规则:赋值运算符(
=)是类的默认成员函数,无需手动定义即可使用;其余大多数运算符(+、<<、++等)需手动重载才能用于类对象,否则编译报错。 - 函数本质:运算符重载函数的函数名为
operator+(+为要重载的运算符),格式与普通函数一致,包含返回值类型、参数列表。
声明格式示例
class A {
public:
A& operator+(const A& r); // 加法运算符重载声明
// 返回值类型:A&(对象引用)
// 函数名:operator+
// 参数列表:const A& r(右侧操作数,左侧操作数为调用对象本身)
};
运算符重载分类与示例
运算符重载分为「类成员函数重载」和「非类成员函数重载」两类,核心区别在于函数归属与参数传递方式。
类成员运算符重载
重载函数作为类的成员函数,左侧操作数为调用函数的类对象(隐含 this 指针),仅需在参数列表中声明右侧操作数。
经典示例:时间类(Time)的加法重载
#include <iostream>
using namespace std;
class Time {
private:
int hour; // 小时
int minute; // 分钟
int second; // 秒
public:
// 构造函数
Time(int h = 0, int m = 0, int s = 0) : hour(h), minute(m), second(s) {}
// 加法运算符重载(类成员函数)
const Time operator+(const Time& r);
// 显示时间(辅助函数)
void show() const {
cout << hour << ":" << minute << ":" << second << endl;
}
};
// 加法运算符重载实现(处理时间进位逻辑)
const Time Time::operator+(const Time& r) {
Time tmp;
// 秒数相加,处理进位
tmp.second = this->second + r.second;
if (tmp.second >= 60) {
tmp.minute++;
tmp.second -= 60;
}
// 分钟相加,处理进位
tmp.minute = this->minute + r.minute;
if (tmp.minute >= 60) {
tmp.hour++;
tmp.minute -= 60;
}
// 小时相加,处理24小时制
tmp.hour = this->hour + r.hour;
tmp.hour %= 24;
return tmp;
}
// 测试代码
int main(void) {
Time t1(2, 10, 20);
Time t2(4, 12, 51);
Time t = t1 + t2; // 等价于 t1.operator+(t2)
t.show(); // 输出:6:23:11
return 0;
}
语法重点
- 左操作数隐含:
t1 + t2中,t1是调用对象(this指向t1),t2是参数列表中的右侧操作数。 - 等价转换:
t1 + t2↔t1.operator+(t2);t2 + t1↔t2.operator+(t1)。
非类成员运算符重载
重载函数作为普通全局函数,需显式声明所有操作数,且至少有一个操作数是自定义类型(否则编译报错)。
经典示例:重载 << 实现时间类的流输出
默认情况下 cout << t(t 为 Time 对象)不支持,需重载 << 运算符:
#include <iostream>
using namespace std;
class Time {
private:
int hour;
int minute;
int second;
public:
Time(int h = 0, int m = 0, int s = 0) : hour(h), minute(m), second(s) {}
// 提供公有接口,供全局重载函数访问私有成员
int h() const { return hour; }
int m() const { return minute; }
int s() const { return second; }
};
// 非类成员运算符重载:重载 <<
ostream& operator<<(ostream& out, const Time& t) {
out << t.h() << ":" << t.m() << ":" << t.s();
return out; // 返回ostream&以支持连续输出(如cout << t1 << t2)
}
// 测试代码
int main(void) {
Time t1(2, 10, 20);
Time t2(4, 12, 51);
cout << "t1: " << t1 << endl; // 输出:t1: 2:10:20
cout << "t1 + t2: " << t1 << " + " << t2 << " = " << Time(t1.h()+t2.h(), t1.m()+t2.m(), t1.s()+t2.s()) << endl;
return 0;
}
示例:重载 >> 实现时间类的流输入
// 非类成员运算符重载:重载 >>
istream& operator>>(istream& in, Time& t) {
int h, m, s;
in >> h >> m >> s;
// 可在此处添加参数合法性校验(如h∈[0,23], m∈[0,59], s∈[0,59])
t = Time(h, m, s); // 需确保Time类支持赋值操作(默认已支持)
return in; // 返回istream&以支持连续输入(如cin >> t1 >> t2)
}
// 测试代码
int main(void) {
Time t;
cout << "请输入时间(时 分 秒):";
cin >> t;
cout << "你输入的时间:" << t << endl;
return 0;
}
语法重点
- 参数顺序:运算符的操作数需按「左→右」顺序填入参数列表(如
<<的左操作数是ostream对象,右操作数是Time对象,参数列表为(ostream& out, const Time& t))。 - 返回值:流运算符(
<<、>>)返回流对象引用(ostream&/istream&),以支持连续输入输出。
自加自减运算符重载(特殊单目运算符)
自加(++)、自减(--)是特殊的单目运算符,需区分「前缀」(++a)和「后缀」(a++),逻辑不同。
核心规则
| 类型 | 函数声明格式 | 返回值类型 | 逻辑特点 |
|---|---|---|---|
| 前缀++ | Time& operator++() |
对象引用(Time&) | 先自增,再返回自身(支持链式赋值) |
| 后缀++ | const Time operator++(int) |
const 临时对象 | 先返回自身副本,再自增(不支持赋值) |
| 前缀-- | Time& operator--() |
对象引用(Time&) | 先自减,再返回自身 |
| 后缀-- | const Time operator--(int) |
const 临时对象 | 先返回自身副本,再自减 |
注:后缀运算符的
int参数仅为标记,无实际数据传递,编译器通过该参数区分前缀/后缀。
实现示例:Time 类的自加自减重载
class Time {
private:
int hour;
int minute;
int second;
public:
Time(int h = 0, int m = 0, int s = 0) : hour(h), minute(m), second(s) {}
// 前缀++:先自增,返回引用
Time& operator++() {
second++;
if (second >= 60) {
second = 0;
minute++;
if (minute >= 60) {
minute = 0;
hour++;
hour %= 24;
}
}
return *this;
}
// 后缀++:先返回副本,再自增(int为标记)
const Time operator++(int) {
Time tmp = *this; // 保存当前状态
// 自增逻辑(与前缀一致)
second++;
if (second >= 60) {
second = 0;
minute++;
if (minute >= 60) {
minute = 0;
hour++;
hour %= 24;
}
}
return tmp; // 返回自增前的副本
}
// 前缀--(类似前缀++)
Time& operator--() {
second--;
if (second < 0) {
second = 59;
minute--;
if (minute < 0) {
minute = 59;
hour--;
if (hour < 0) hour = 23;
}
}
return *this;
}
// 后缀--(类似后缀++)
const Time operator--(int) {
Time tmp = *this;
second--;
if (second < 0) {
second = 59;
minute--;
if (minute < 0) {
minute = 59;
hour--;
if (hour < 0) hour = 23;
}
}
return tmp;
}
// 友元函数:直接访问私有成员,简化输出(后续友元章节详解)
friend ostream& operator<<(ostream& out, const Time& t);
};
// 友元函数实现 << 重载
ostream& operator<<(ostream& out, const Time& t) {
out << t.hour << ":" << t.minute << ":" << t.second;
return out;
}
// 测试代码
int main(void) {
Time t(23, 59, 59);
cout << "初始时间:" << t << endl;
Time t1 = ++t; // 前缀++:t先自增为0:0:0,t1 = t
cout << "++t 后:t=" << t << ", t1=" << t1 << endl; // 0:0:0, 0:0:0
Time t2 = t++; // 后缀++:t2 = t(0:0:0),t再自增为0:0:1
cout << "t++ 后:t=" << t << ", t2=" << t2 << endl; // 0:0:1, 0:0:0
return 0;
}
运算符重载的限制
C++ 对运算符重载有严格约束,避免滥用导致逻辑混乱:
- 归属约束:重载函数要么是类成员,要么是非类成员(全局函数),且非类成员重载需至少有一个自定义类型参数。
- 操作数与优先级约束:不能改变运算符的「操作数个数」和「优先级」(如
+始终是双目运算符,优先级不变)。 - 运算符范围约束:只能重载 C++ 已有的运算符,不能创建新运算符(如不能自定义
@运算符)。 - 不可重载的运算符(涉及编译系统底层逻辑):
sizeof(求内存尺寸).(成员引用符)、.*(成员指针引用符)::(域解析符)?:(条件运算符)typeid(RTTI 运算符)- 四种强制类型转换运算符:
const_cast、static_cast、dynamic_cast、reinterpret_cast
- 只能作为类成员重载的运算符:
=(赋值运算符)()(函数调用运算符)[](下标运算符)->(成员访问运算符)
拓展知识
运算符重载的应用场景
- 自定义数据类型的运算:如字符串拼接(
string s1 + s2)、矩阵运算、日期计算。 - 容器类操作:如 STL 中的
vector重载[]实现下标访问,string重载==实现字符串比较。 - 简化代码逻辑:将复杂的类方法调用转化为直观的运算符操作(如
a + b替代a.add(b))。
常见错误与注意事项
- 后缀自增返回非
const对象:导致(a++) = b这样的非法赋值(C++ 标准不允许,编译报错),需返回const临时对象。 - 返回局部对象的引用:如
Time& operator+()中返回局部变量tmp的引用,会导致悬垂引用(局部变量销毁后引用失效)。 - 忽略参数的
const修饰:如operator+(Time& r)未加const,导致常量对象无法作为参数(如const Time t1; t1 + t2编译报错)。
友元(Friend)
基本概念
类的封装性要求:非类成员只能访问类的 public 成员,无法直接访问 private/protected 成员。但在某些场景下(如外部函数需要直接操作类的私有数据),需要打破这种限制——友元就是为解决此问题设计的。
友元的核心思想:将外部函数/类声明为当前类的「朋友」,朋友可以直接访问当前类的所有成员(包括 private/protected),无需通过公有接口。
生活类比
电视机(TV 类)的频道、音量是私有成员,遥控器(Remoter 类)需要直接修改这些数据,因此将 Remoter 类声明为 TV 类的友元。
友元的种类
友元函数
普通全局函数被声明为类的友元,可直接访问类的所有成员。
语法格式
#include <iostream>
using namespace std;
class Base {
private:
int a;
protected:
int b;
public:
int c;
// 声明友元函数:friend + 函数声明(无需属于类成员)
friend void show(Base& r);
};
// 友元函数实现:无需加 friend 关键字,与普通函数一致
void show(Base& r) {
// 直接访问 private/protected/public 成员
r.a = 123; // private 成员
r.b = 456; // protected 成员
r.c = 789; // public 成员
cout << "a: " << r.a << ", b: " << r.b << ", c: " << r.c << endl;
}
// 测试代码
int main(void) {
Base obj;
show(obj); // 输出:a: 123, b: 456, c: 789
return 0;
}
语法要点
- 友元函数与访问控制符(
public/protected/private)无关:无论声明在类的哪个区域,效果相同。 - 友元函数不是类成员:没有
this指针,需通过参数传递类对象才能访问其成员。
友元类
整个类被声明为另一个类的友元,友元类的所有成员函数都能直接访问当前类的所有成员。
经典示例:遥控器类(Remoter)作为电视机类(TV)的友元
#include <iostream>
using namespace std;
// 电视机类
class TV {
private:
int channel; // 频道(私有成员)
int volume; // 音量(私有成员)
public:
// 声明 Remoter 为友元类:Remoter 的所有成员函数都能访问 TV 的私有成员
friend class Remoter;
};
// 遥控器类
class Remoter {
public:
// 调整频道
void setChannel(TV& tv, int c) {
// 直接访问 TV 的私有成员 channel
if (c >= 1 && c <= 100) tv.channel = c;
else cout << "频道非法!" << endl;
}
// 调整音量
void setVolume(TV& tv, int v) {
// 直接访问 TV 的私有成员 volume
if (v >= 0 && v <= 100) tv.volume = v;
else cout << "音量非法!" << endl;
}
// 显示当前状态
void showStatus(TV& tv) {
cout << "当前频道:" << tv.channel << ", 当前音量:" << tv.volume << endl;
}
};
// 测试代码
int main(void) {
TV myTV;
Remoter myRemoter;
myRemoter.setChannel(myTV, 10);
myRemoter.setVolume(myTV, 30);
myRemoter.showStatus(myTV); // 输出:当前频道:10, 当前音量:30
myRemoter.setChannel(myTV, 101); // 输出:频道非法!
return 0;
}
友元成员函数
仅将友元类的特定成员函数声明为当前类的友元,避免友元类的所有成员都拥有访问权限(更安全)。
语法格式(以万能遥控器为例)
#include <iostream>
using namespace std;
// 提前声明遥控器类(因为 TV 类中要引用 Remoter 的成员函数)
class Remoter;
// 电视机类
class TV {
private:
int channel;
int volume;
public:
// 仅声明 Remoter 的 setChannel 和 setVolume 为友元成员函数
friend void Remoter::setChannel(TV& tv, int c);
friend void Remoter::setVolume(TV& tv, int v);
};
// 遥控器类(万能遥控器,可控制电视、灯光等)
class Remoter {
public:
// 控制电视的频道(友元成员函数)
void setChannel(TV& tv, int c) {
if (c >= 1 && c <= 100) tv.channel = c;
}
// 控制电视的音量(友元成员函数)
void setVolume(TV& tv, int v) {
if (v >= 0 && v <= 100) tv.volume = v;
}
// 控制灯光(与 TV 无关,无 TV 类的访问权限)
void turnOnLight() {
cout << "灯光开启" << endl;
}
void turnOffLight() {
cout << "灯光关闭" << endl;
}
// 显示电视状态(需通过友元成员函数间接访问)
void showTVStatus(TV& tv) {
// 直接访问 TV 的私有成员(因 setChannel/setVolume 是友元,但 showTVStatus 不是?不,此处需注意:友元声明是针对具体函数的,showTVStatus 未被声明为友元,无法直接访问 TV 的私有成员!)
// 修正方案:要么将 showTVStatus 也声明为友元,要么在 TV 类中提供公有接口。
cout << "当前频道:" << tv.channel << ", 当前音量:" << tv.volume << endl; // 编译报错!
}
};
// 修正后的 TV 类(添加 showTVStatus 为友元)
class TV {
private:
int channel;
int volume;
public:
friend void Remoter::setChannel(TV& tv, int c);
friend void Remoter::setVolume(TV& tv, int v);
friend void Remoter::showTVStatus(TV& tv); // 新增友元声明
};
语法要点
- 提前声明:若友元成员函数所在的类(如
Remoter)在当前类(如TV)之后定义,需提前声明友元类(class Remoter;)。 - 精准授权:仅授权必要的成员函数,最小化权限泄露,兼顾灵活性与安全性。
友元的特性与争议
核心特性
- 单向性:
A是B的友元,不代表B是A的友元(如遥控器能控制电视,电视不能控制遥控器)。 - 非传递性:
A是B的友元,B是C的友元,不代表A是C的友元。 - 非继承性:父类的友元不会自动成为子类的友元,子类需单独声明。
争议与使用原则
- 争议:友元打破了类的封装性,可能导致代码安全性降低(如友元函数误修改类的私有数据)。
- 使用原则:
- 最小权限原则:优先使用友元成员函数,而非友元类,仅授权必要的操作。
- 明确关系原则:仅在两个类存在明确的「使用关系」(use-a)时使用(如遥控器-电视、打印机-文档)。
- 替代方案优先:若能通过公有接口实现需求,尽量不使用友元(如通过
getter/setter访问私有成员)。
拓展知识
友元与运算符重载的结合
当重载非类成员运算符时,若需要访问类的私有成员,可将重载函数声明为类的友元(比提供 getter/setter 更简洁)。
示例:Time 类的 << 重载(友元版)
class Time {
private:
int hour;
int minute;
int second;
public:
Time(int h = 0, int m = 0, int s = 0) : hour(h), minute(m), second(s) {}
// 声明 << 重载函数为友元,直接访问私有成员
friend ostream& operator<<(ostream& out, const Time& t);
};
// 友元函数实现 << 重载,直接访问私有成员
ostream& operator<<(ostream& out, const Time& t) {
out << t.hour << ":" << t.minute << ":" << t.second;
return out;
}
友元在实际开发中的应用
- 运算符重载:如 STL 中的
string类,重载==、+等运算符时,通过友元函数访问私有字符数组。 - 测试代码:单元测试中,测试函数需访问类的私有成员以验证内部状态,可将测试函数声明为友元。
- 跨类协作:两个关系紧密的类(如
Buffer缓冲区类和Reader读取类),通过友元实现高效数据交互。
整体总结
| 知识点 | 核心作用 | 关键约束/原则 |
|---|---|---|
| 运算符重载 | 让自定义类型支持运算符操作 | 不改变操作数个数、优先级;有限定运算符范围 |
| 友元 | 打破封装,实现跨类数据访问 | 最小权限、明确关系、替代方案优先 |
| 两者结合 | 简化重载函数,访问私有成员 | 友元仅用于必要场景,避免滥用 |
核心思想:C++ 的运算符重载和友元都是为了「兼顾封装性与灵活性」——既通过类保护数据安全,又在必要时提供便捷的交互方式,让代码更直观、高效。

浙公网安备 33010602011771号