15. 友元:友元函数、友元类、友元的应用场景
说到友元,很多初学者第一反应是:「这不破坏封装性吗?」
嗯,我刚开始学的时候也这么想。后来在项目中写了一个矩阵运算库,才真正体会到——有时候,封装性是要为性能让路的。
友元,说白了就是给「外人」开个后门。让某个函数或类,能访问你的私有成员。听起来有点危险?确实。但用对了地方,它就是一把利器。
15.1 为什么需要友元?
C++ 的类默认是「闭关锁国」的。私有成员只有自己人能碰。但现实世界没这么简单。
举个例子:你写了一个 Matrix 类,想重载 operator<< 来打印矩阵内容。这个运算符的左侧是 ostream,不是你的 Matrix 对象。所以它不能直接访问 Matrix 的私有数据。
怎么办?
- 把所有数据改成 public?—— 太粗暴,等于裸奔。
- 写一堆 getter?—— 可以,但每次调用都有函数调用开销,性能敏感场景下不划算。
- 用友元?—— 对,这就是它的典型场景。
核心思想:友元不是「破坏封装」,而是「在信任的前提下,打开一扇窗」。
15.2 友元函数
友元函数,就是一个不属于类的普通函数,但被授权访问类的私有成员。
15.2.1 声明方式
在类定义里,用 friend 关键字声明一下就行。位置无所谓,public、private、protected 区域都可以。
class Matrix {
private:
int data[4][4];
public:
Matrix() { /* 初始化 */ }
// 声明友元函数
friend void printMatrix(const Matrix& m);
};
// 普通函数,但可以访问 Matrix 的私有成员
void printMatrix(const Matrix& m) {
for (int i = 0; i < 4; ++i) {
for (int j = 0; j < 4; ++j) {
std::cout << m.data[i][j] << " ";
}
std::cout << std::endl;
}
}
注意:printMatrix 不是成员函数。它没有 this 指针,调用时也不需要 m.printMatrix() 这种形式。
15.2.2 运算符重载中的友元
我个人觉得,友元函数在运算符重载里用得最多。尤其是双目运算符,比如 +、-、<<、>>。
class Complex {
private:
double real, imag;
public:
Complex(double r = 0, double i = 0) : real(r), imag(i) {}
// 友元:重载 +
friend Complex operator+(const Complex& a, const Complex& b);
// 友元:重载 <<
friend std::ostream& operator<<(std::ostream& os, const Complex& c);
};
Complex operator+(const Complex& a, const Complex& b) {
return Complex(a.real + b.real, a.imag + b.imag);
}
std::ostream& operator<<(std::ostream& os, const Complex& c) {
os << "(" << c.real << ", " << c.imag << "i)";
return os;
}
小技巧:如果运算符的左侧操作数不是当前类对象(比如 << 左侧是 ostream),那就只能用友元函数,不能用成员函数重载。
15.3 友元类
友元类更「大方」—— 整个类都是你的朋友。A 类声明 B 类是它的友元,那 B 的所有成员函数都能访问 A 的私有成员。
class Engine {
private:
int horsepower;
double displacement;
friend class Mechanic; // Mechanic 是 Engine 的友元类
};
class Mechanic {
public:
void tune(Engine& e) {
e.horsepower += 50; // 可以访问私有成员
e.displacement *= 1.1; // 可以访问私有成员
}
};
我在项目中遇到过这样的场景:一个 Document 类和一个 DocumentSerializer 类。序列化器需要读取文档的内部数据结构,但又不希望把这些数据暴露给外部。用友元类就很合适。
注意:友元关系是单向的,不能继承。A 是 B 的友元,不代表 B 是 A 的友元。A 的友元类 C 也不能自动访问 B 的私有成员。
15.4 友元的应用场景
我总结了一下,友元主要用在以下几个地方:
| 场景 | 说明 | 示例 |
|---|---|---|
| 运算符重载 | 尤其是 <<、>>、双目运算符 | Complex 类的 +、<< |
| 迭代器模式 | 迭代器需要访问容器的内部结构 | 自定义容器的 begin()/end() |
| 工厂模式 | 工厂类需要创建对象,但构造函数是私有的 | 单例模式、对象池 |
| 测试工具 | 单元测试需要访问私有成员来验证状态 | Google Test 中的 FRIEND_TEST |
| 性能敏感代码 | 避免 getter/setter 的函数调用开销 | 矩阵运算、图形引擎 |
15.4.1 工厂模式中的友元
我曾经写过一个对象池,希望用户只能通过工厂创建对象,不能自己 new。把构造函数设为 private,然后把工厂类设为友元。
class Connection {
private:
Connection() {} // 私有构造函数
friend class ConnectionPool;
};
class ConnectionPool {
public:
Connection* acquire() {
return new Connection(); // 友元,可以调用私有构造函数
}
void release(Connection* conn) {
delete conn;
}
};
15.4.2 测试中的友元
嗯,这里要注意。很多人觉得测试应该只测 public 接口。但有时候,你想验证内部状态是否正确。比如一个缓存类,你想确认它有没有正确淘汰数据。
class Cache {
private:
std::map<int, std::string> data;
int maxSize;
void evictOldest(); // 私有方法
// 测试类可以访问私有成员
friend class CacheTest;
};
// 测试代码
class CacheTest {
public:
void testEviction() {
Cache cache;
cache.data[1] = "a"; // 直接操作私有数据,验证逻辑
cache.evictOldest();
assert(cache.data.empty());
}
};
我的建议:友元用于测试时,可以用条件编译包裹起来。发布版本里去掉测试友元,避免不必要的耦合。
15.5 友元的注意事项
说了这么多好处,也得泼点冷水。友元不是银弹,用不好会出问题。
- 不要滥用:能用 public 接口解决的问题,就别用友元。我见过一个项目,几乎每个类都声明了五六个友元,最后改一个类的内部实现,所有友元类都得跟着改——维护成本爆炸。
- 友元不传递:A 是 B 的友元,B 是 C 的友元,但 A 不是 C 的友元。别指望「朋友的朋友也是我的朋友」。
- 友元不继承:基类的友元不能访问派生类的私有成员。反过来,派生类的友元也不能访问基类的私有成员。
- 友元函数不是成员函数:它没有 this 指针,不能直接调用类的成员函数(除非通过对象)。
避坑指南:我曾经在代码里把友元声明写在了 private 区域,结果同事以为这是个私有成员函数,找了半天没找到定义。后来我养成了习惯——友元声明统一放在类定义的最前面,或者用注释标明。
15.6 知识体系总览
下面这张图,帮你把友元的知识点串起来:
15.7 小结
友元这个特性,用好了是神器,用砸了是灾难。我的经验是:
- 优先考虑 public 接口,实在绕不开再用友元
- 友元函数比友元类更可控,能用函数就别用类
- 友元关系要写在文档里,不然后来者一脸懵
- 记得友元是单向的、不继承的、不传递的
你想想看,C++ 给了你这么多控制权,就是让你在「封装」和「灵活」之间做权衡。友元就是那个平衡点。用对了,代码既安全又高效。