【c++面向对象编程】第9篇:友元(friend):破坏封装的“特权”——真的有害吗?
2026/5/13 0:25:12 网站建设 项目流程

目录

一、一个让人纠结的场景

二、友元的两种形式

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()来访问。这没问题,但有两个小烦恼:

  1. 性能:函数调用有开销(虽然很小)

  2. 语义distancePoint关系密切,把它写成成员函数不合适(距离不属于单个点),用public接口总觉得隔了一层

有没有办法让distance函数直接访问xy

有——把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可以像成员函数一样直接访问xy,但它仍然是一个普通函数(不是成员函数)。


二、友元的两种形式

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(); // ✅ 可以访问 } };

注意:友元关系是单向的CarEngine的友元,不代表EngineCar的友元。也不具有传递性:如果AB的友元,BC的友元,不代表AC的友元。


三、友元的典型应用场景

场景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改名成connStrdebugPrint会编译失败。友元代码和类内部产生了强耦合

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

这个例子中,友元函数直接访问numeratordenominator,让运算符重载的实现简洁自然。如果用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)的实现细节。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询