目录
一、一个让人纠结的场景
二、友元的两种形式
1. 友元函数
2. 友元类
三、友元的典型应用场景
场景1:运算符重载(最常见)
场景2:两个紧密耦合的类
场景3:单元测试中访问私有状态
四、友元的缺点:为什么被“警告”
1. 破坏封装
2. 减少可读性
3. 容易被滥用
五、友元 vs public:怎么选择?
六、完整例子:有理数类
七、四个常见误区
1. 以为友元可以继承
2. 以为友元类可以访问派生类的私有成员
3. 把友元写在public/protected/private里
4. 过度使用友元导致设计腐化
八、这一篇的收获
一、一个让人纠结的场景
你写了一个Point类,表示二维坐标点:
cpp
class Point { private: double x, y; public: Point(double x, double y) : x(x), y(y) {} double getX() const { return x; } double getY() const { return y; } };然后你想写一个函数,计算两个点之间的距离:
cpp
double distance(const Point& p1, const Point& p2) { // 需要访问p1.x, p1.y, p2.x, p2.y // 但它们是private! return sqrt(pow(p1.getX() - p2.getX(), 2) + pow(p1.getY() - p2.getY(), 2)); }目前只能通过getX()/getY()来访问。这没问题,但有两个小烦恼:
性能:函数调用有开销(虽然很小)
语义:
distance和Point关系密切,把它写成成员函数不合适(距离不属于单个点),用public接口总觉得隔了一层
有没有办法让distance函数直接访问x和y?
有——把distance声明为Point的友元函数。
cpp
class Point { private: double x, y; public: Point(double x, double y) : x(x), y(y) {} // 声明友元函数:这个函数可以访问我的私有成员 friend double distance(const Point& p1, const Point& p2); }; // 实现友元函数 double distance(const Point& p1, const Point& p2) { return sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2)); }现在distance可以像成员函数一样直接访问x和y,但它仍然是一个普通函数(不是成员函数)。
二、友元的两种形式
1. 友元函数
全局函数或另一个类的成员函数可以成为友元。
cpp
class Box { private: int width; public: Box(int w) : width(w) {} // 全局函数作为友元 friend void printWidth(const Box& b); // 另一个类的成员函数作为友元(声明时需注明所属类) friend void OtherClass::setWidth(Box& b, int w); }; void printWidth(const Box& b) { cout << b.width; // 可以访问私有成员 }2. 友元类
整个类成为另一个类的友元,该类的所有成员函数都能访问对方的私有成员。
cpp
class Engine { private: int horsepower; void ignite() { cout << "引擎点火" << endl; } // 声明Car为友元类:Car可以访问Engine的所有私有成员 friend class Car; }; class Car { public: void start(Engine& e) { e.horsepower = 300; // ✅ 可以访问 e.ignite(); // ✅ 可以访问 } };注意:友元关系是单向的。Car是Engine的友元,不代表Engine是Car的友元。也不具有传递性:如果A是B的友元,B是C的友元,不代表A是C的友元。
三、友元的典型应用场景
场景1:运算符重载(最常见)
重载<<运算符让自定义类支持cout << obj时,通常需要友元。
cpp
class Complex { private: double real, imag; public: Complex(double r, double i) : real(r), imag(i) {} // 友元函数重载<< friend ostream& operator<<(ostream& os, const Complex& c); }; ostream& operator<<(ostream& os, const Complex& c) { os << c.real << "+" << c.imag << "i"; return os; }为什么必须是友元?因为operator<<的第一个参数是ostream&,不是Complex对象,所以不能写成成员函数。如果不想用友元,就只能写public getter,但这样会暴露太多接口。
场景2:两个紧密耦合的类
比如链表(List)和节点(Node),节点通常只让链表操作,其他类不应该直接碰节点内部。
cpp
class List; // 前置声明 class Node { private: int data; Node* next; friend class List; // 只有List能操作Node的内部指针 }; class List { private: Node* head; public: void insert(int val) { Node* newNode = new Node{val, head}; // 可以访问Node的私有成员 head = newNode; } };场景3:单元测试中访问私有状态
有时需要测试类的内部状态,可以把测试类或测试函数声明为友元。
cpp
class MyClass { private: int internalState; #ifdef UNIT_TEST friend class MyClassTest; // 只在测试编译时启用 #endif };四、友元的缺点:为什么被“警告”
1. 破坏封装
封装的意义在于:类的内部实现可以随时修改,只要保持public接口不变,外部代码不受影响。
有了友元之后,情况变了:
cpp
class Database { private: string connectionString; // 友元函数可以访问 friend void debugPrint(const Database& db); }; void debugPrint(const Database& db) { cout << db.connectionString; // 直接依赖内部细节 }如果某天你把connectionString改名成connStr,debugPrint会编译失败。友元代码和类内部产生了强耦合。
2. 减少可读性
看一个类的声明时,public接口已经够多了,再加上一堆friend声明,会让人困惑:“这个类到底暴露给多少外部实体?”
3. 容易被滥用
新手容易把友元当成“更方便的getter/setter”:
cpp
// ❌ 不好的做法:把无关的函数都声明为友元 class Data { private: int value; public: friend void func1(Data&); friend void func2(Data&); friend void func3(Data&); // 为什么不直接把value改成public??? };五、友元 vs public:怎么选择?
| 情况 | 推荐方案 |
|---|---|
| 外部功能可以通过public接口实现 | 用public,不用友元 |
| 功能与类紧密相关,但不是类的核心职责 | 考虑友元(如运算符重载) |
| 功能需要高性能,避免getter/setter开销 | 用友元(但先确认性能确实是瓶颈) |
| 一个辅助类专门服务于主类 | 把辅助类声明为友元 |
| 只是不想写getter | ❌ 不要用友元,要么写getter,要么把变量改成public |
一个判断标准:如果删除友元声明后,必须把多个成员变成public才能让外部正常工作,那友元可能是合理的。如果只需要开放一两个getter,那用getter更干净。
六、完整例子:有理数类
用友元重载算术运算符,展示友元在数学类中的典型用法:
cpp
#include <iostream> #include <numeric> // for gcd using namespace std; class Rational { private: int numerator; // 分子 int denominator; // 分母 void reduce() { int g = gcd(numerator, denominator); numerator /= g; denominator /= g; if (denominator < 0) { // 分母保持正数 numerator = -numerator; denominator = -denominator; } } public: Rational(int num = 0, int den = 1) : numerator(num), denominator(den) { if (denominator == 0) { throw invalid_argument("分母不能为0"); } reduce(); } // 友元函数声明 friend Rational operator+(const Rational& a, const Rational& b); friend Rational operator*(const Rational& a, const Rational& b); friend ostream& operator<<(ostream& os, const Rational& r); }; // 实现友元函数 Rational operator+(const Rational& a, const Rational& b) { int num = a.numerator * b.denominator + b.numerator * a.denominator; int den = a.denominator * b.denominator; return Rational(num, den); } Rational operator*(const Rational& a, const Rational& b) { return Rational(a.numerator * b.numerator, a.denominator * b.denominator); } ostream& operator<<(ostream& os, const Rational& r) { if (r.denominator == 1) { os << r.numerator; } else { os << r.numerator << "/" << r.denominator; } return os; } int main() { Rational a(1, 2); // 1/2 Rational b(2, 3); // 2/3 cout << a << " + " << b << " = " << a + b << endl; // 7/6 cout << a << " * " << b << " = " << a * b << endl; // 1/3 Rational c(4, 6); // 会自动约分为2/3 cout << "4/6 约分后: " << c << endl; return 0; }输出:
text
1/2 + 2/3 = 7/6 1/2 * 2/3 = 1/3 4/6 约分后: 2/3
这个例子中,友元函数直接访问numerator和denominator,让运算符重载的实现简洁自然。如果用getter,代码会变丑,而且没有本质的封装收益——因为这两个函数本身就是类接口的一部分。
七、四个常见误区
1. 以为友元可以继承
cpp
class Base { friend void func(Base& b); }; class Derived : public Base { // func(Derived&) 不能访问Derived的私有成员 // 友元不继承 };2. 以为友元类可以访问派生类的私有成员
友元关系只针对声明的那个类,不自动延伸到派生类。
3. 把友元写在public/protected/private里
友元声明不受访问修饰符影响,写在哪里都一样。但按惯例,通常写在类的最开始(public之前)或最后。
cpp
class Demo { friend class A; // ✅ 习惯放这里 public: // ... private: // ... friend class B; // ✅ 也可以,但没必要 };4. 过度使用友元导致设计腐化
如果发现你的类有5个以上的友元声明,或者友元类和主类的关系很松散,说明你的封装出了问题,该重新审视设计了。
八、这一篇的收获
你现在应该明白:
友元函数/类可以访问类的私有成员,是封装的一个“受控漏洞”
最合理的应用场景是运算符重载(特别是
<<和>>)和紧密耦合的辅助类友元破坏封装、增加耦合,应该谨慎使用,不要当成“方便版public”
友元不可继承、不传递、单向
💡 小作业:实现一个
Matrix类(矩阵),用友元重载*运算符实现矩阵乘法。两个矩阵相乘需要访问对方的私有数据(二维数组),用友元实现比用getter更自然。
下一篇预告:第10篇《类的组合与嵌套:一个类中包含另一个类的对象》——一个类作为另一个类的成员变量时,如何正确初始化?构造和析构的顺序是怎样的?下篇讲清楚组合关系(has-a)的实现细节。