目录
[1. 友元函数](#1. 友元函数)
[2. 友元类](#2. 友元类)
[1. 破坏封装](#1. 破坏封装)
[2. 减少可读性](#2. 减少可读性)
[3. 容易被滥用](#3. 容易被滥用)
[五、友元 vs public:怎么选择?](#五、友元 vs public:怎么选择?)
[1. 以为友元可以继承](#1. 以为友元可以继承)
[2. 以为友元类可以访问派生类的私有成员](#2. 以为友元类可以访问派生类的私有成员)
[3. 把友元写在public/protected/private里](#3. 把友元写在public/protected/private里)
[4. 过度使用友元导致设计腐化](#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)的实现细节。