友元
嘿!我的朋友!今天我们介绍friend!别误会,friend实际上是C++的关键字,哈哈!前面几章我们一直在强调封装——private 成员藏在类内部,外部代码只能通过 public 接口操作对象。但偶尔你会碰到一种情况:某个外部函数或另一个类确实需要访问私有成员,而且这种访问是合理的、不可避免的。C++ 提供了专门的机制来处理这种场景——friend(友元)。
友元的本质是定向授权:类的作者主动声明"我信任这个函数/类,允许它看到我的私有成员"。它不是把封装彻底拆掉(那直接全写 public 就行了),而是开了一扇有控制的小门。接下来我们把友元的三种形态——友元函数、友元类、友元成员函数——逐一拆开来看,最后讨论什么时候该用友元,什么时候不该用。
友元函数——给外部函数一张通行证
友元函数是最基本的友元形态。声明方式是在类的内部用 friend 关键字加上一个普通函数的声明:
class Vector3D {
private:
float x, y, z;
public:
Vector3D(float x, float y, float z) : x(x), y(y), z(z) {}
// 声明 dot_product 为友元函数
friend float dot_product(const Vector3D& a, const Vector3D& b);
};
// 友元函数定义——不是成员函数,不需要 Vector3D::
float dot_product(const Vector3D& a, const Vector3D& b)
{
return a.x * b.x + a.y * b.y + a.z * b.z;
}这里有几个要点需要搞清楚。首先,friend 声明出现在类的内部,但 dot_product 不是 Vector3D 的成员函数——它是一个普通的全局函数,只不过获得了访问 Vector3D 私有成员的特权。调用时和普通函数一样:dot_product(v1, v2),而不是 v1.dot_product(v2)。
其次,friend 声明可以放在类的任何位置——public、private、protected 区域都无所谓,效果完全相同。通常我们把它集中放在类的开头或末尾,和成员函数声明分开,一眼就能看出"哪些外部函数拥有特殊权限"。
友元函数最经典的应用场景是重载 operator<<,让自定义类型能直接输出到流。这个场景之所以需要友元,是因为 operator<< 的左操作数是 std::ostream&,而不是你的类本身——它不可能成为你的成员函数:
class Point {
private:
int x, y;
public:
Point(int x, int y) : x(x), y(y) {}
// 友元重载 operator<<
friend std::ostream& operator<<(std::ostream& os, const Point& p);
};
std::ostream& operator<<(std::ostream& os, const Point& p)
{
os << "(" << p.x << ", " << p.y << ")";
return os;
}
// 现在可以这样用了
Point p(3, 4);
std::cout << p << std::endl; // 输出: (3, 4)operator<< 重载的细节我们会在下一章展开,这里只需要理解它为什么必须是友元——第一个参数是 std::ostream&,不是 Point,所以这个函数没法写成 Point 的成员函数。
友元类——让整个类都成为信任对象
如果一个类的很多成员函数都需要访问另一个类的私有成员,逐个声明友元函数就太繁琐了。这时可以用 friend class 一次性授权给整个类:
class Matrix {
private:
float data[3][3];
public:
Matrix() // 初始化为单位矩阵
{
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
data[i][j] = (i == j) ? 1.0f : 0.0f;
}
}
}
// Vector 是 Matrix 的友元类
friend class Vector;
};
class Vector {
private:
float x, y, z;
public:
Vector(float x, float y, float z) : x(x), y(y), z(z) {}
Vector transform(const Matrix& m)
{
// Vector 的成员函数可以直接访问 Matrix 的 private 成员
float nx = m.data[0][0] * x + m.data[0][1] * y + m.data[0][2] * z;
float ny = m.data[1][0] * x + m.data[1][1] * y + m.data[1][2] * z;
float nz = m.data[2][0] * x + m.data[2][1] * y + m.data[2][2] * z;
return Vector(nx, ny, nz);
}
};friend class Vector; 意味着 Vector 的所有成员函数都可以访问 Matrix 的私有成员。这是一种粗粒度的授权——要慎重使用,但确实有一些场景下两个类关系足够紧密,值得这种级别的信任。典型的合理场景包括"容器 + 迭代器"模式、以及上面这种数学类型之间的紧密协作。共同特征是:两个类在逻辑上是一个整体,只是出于代码组织的原因被拆成了两个类。
友元成员函数——精确制导的授权
如果你觉得"友元类授权太宽泛",C++ 还提供了更精细的控制:只授权另一个类的某一个成员函数:
class Vector; // 前向声明
class Matrix {
private:
float data[3][3];
public:
Matrix();
// 只授权 Vector::transform 这一个成员函数
friend Vector Vector::transform(const Matrix& m);
};
class Vector {
private:
float x, y, z;
public:
Vector(float x, float y, float z) : x(x), y(y), z(z) {}
Vector transform(const Matrix& m);
};理论上这种方式最安全——最小权限原则嘛。但实际使用中,友元成员函数有一个让人头疼的依赖问题:声明 friend Vector Vector::transform(const Matrix&) 的时候,编译器必须已经看到 Vector 类的完整定义,否则它不知道 transform 确实是 Vector 的成员函数。这就要求我们仔细安排头文件包含顺序,弄不好就会陷入循环依赖。如果需要授权的成员函数有三四个之多,不如直接用友元类来得干脆。
什么时候该用友元——一张决策清单
友元很容易被滥用,我们有必要认真讨论一下使用边界。
合理使用友元的场景。运算符重载是最典型的——前面说的 operator<< 就是最好的例子。紧耦合的实现搭档也是合理的,比如 Container 和它的 Iterator、Matrix 和 Vector。这些情况下两个类本来就共享实现细节,用友元只是把这个事实在代码层面显式表达出来。
不应该使用友元的场景。如果只是想偷懒、不想设计合适的公共接口,随手加个 friend 让外部函数直接操作私有数据——这种友元就是有害的。大多数"需要友元"的场景其实可以通过提供恰当的访问接口来替代:
// 不推荐:用友元绕过接口设计
class SensorData {
friend void serialize(const SensorData& data, uint8_t* buffer);
private:
float values[100];
int count;
};
// 推荐:提供只读接口,封装完好
class SensorData {
private:
float values[100];
int count;
public:
const float* data() const { return values; }
int size() const { return count; }
};⚠️ 踩坑预警:友元关系不继承、不传递 友元关系有三个关键特性经常被误解。第一,友元不继承:如果
Base是X的友元,Derived(继承自Base)并不会自动成为X的友元。第二,友元不传递:如果A是B的友元,B是C的友元,A并不会自动成为C的友元。第三,友元是单向的:A是B的友元,意味着A能访问B的私有成员,但B不能反过来访问A的私有成员——除非A也声明B为友元。这三条规则确保了友元权限不会像权限提升漏洞一样无限扩散。⚠️ 踩坑预警:友元声明不是函数前向声明 在类的内部写
friend void foo();确实会使foo成为该类的友元,但当你把友元函数定义在类外面的时候,要确保在调用点之前能找到它的普通声明(而非friend声明)。否则在某些编译器上可能出现"找不到函数定义"的链接错误,尤其是当友元函数定义在另一个.cpp文件中的时候。最稳妥的做法是在类的外面再加一行普通的函数声明。
实战——friend_demo.cpp
现在我们来看一个完整的示例:Matrix 和 Vector 通过友元关系协作完成矩阵-向量乘法。
// friend_demo.cpp
#include <array>
#include <cstdio>
class Vector;
class Matrix {
private:
std::array<std::array<float, 3>, 3> data;
public:
Matrix() : data{{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}} {}
void set(int row, int col, float value) { data[row][col] = value; }
void print() const
{
for (int i = 0; i < 3; ++i)
std::printf("| %.2f %.2f %.2f |\n",
data[i][0], data[i][1], data[i][2]);
}
// 授权 Vector 访问私有成员
friend class Vector;
};
class Vector {
private:
std::array<float, 3> v;
public:
Vector(float x, float y, float z) : v{x, y, z} {}
// 友元权限:直接访问 Matrix 内部数组
Vector transform(const Matrix& m) const
{
float nx = m.data[0][0] * v[0] + m.data[0][1] * v[1] + m.data[0][2] * v[2];
float ny = m.data[1][0] * v[0] + m.data[1][1] * v[1] + m.data[1][2] * v[2];
float nz = m.data[2][0] * v[0] + m.data[2][1] * v[1] + m.data[2][2] * v[2];
return Vector(nx, ny, nz);
}
void print() const
{ std::printf("(%.2f, %.2f, %.2f)\n", v[0], v[1], v[2]); }
};
int main()
{
Matrix m;
m.set(0, 0, 2.0f);
m.set(1, 1, 3.0f);
m.set(2, 2, 0.5f);
Vector v(1.0f, 2.0f, 4.0f);
Vector result = v.transform(m);
std::printf("Matrix:\n");
m.print();
std::printf("Vector: ");
v.print();
std::printf("Result: ");
result.print();
return 0;
}编译运行:
g++ -std=c++17 -Wall -Wextra -o friend_demo friend_demo.cpp
./friend_demo预期输出:
Matrix:
| 2.00 0.00 0.00 |
| 0.00 3.00 0.00 |
| 0.00 0.00 0.50 |
Vector: (1.00, 2.00, 4.00)
Result: (2.00, 6.00, 2.00)这个例子里 Vector::transform 直接访问了 Matrix::data 这个私有数组。如果不用友元,就得提供一个 float get(int, int) const 的访问接口——不是不行,但在数学库这种对性能敏感的场景下,少一层间接访问就意味着更紧凑的循环和更友好的缓存行为。
练习
练习 1:用友元实现 operator<<
为下面的 Student 类实现一个友元函数 operator<<,使得 std::cout << student; 能够直接输出学生的信息。
class Student {
private:
int id;
float score;
public:
Student(int id, float score) : id(id), score(score) {}
// 在这里添加友元声明
};
// 在这里实现 operator<<验证方式:创建几个 Student 对象,用 std::cout 输出它们的信息,确认格式正确。
练习 2:设计 Container-Iterator 友元对
实现一个 IntBuffer 容器和一个 IntBufferIterator 迭代器。IntBuffer 内部用固定大小的 int 数组存储数据,IntBufferIterator 通过友元权限访问该数组完成遍历。要求外部代码无法直接访问 IntBuffer 的内部数组。提示:IntBuffer 声明 friend class IntBufferIterator;,迭代器持有指向容器的指针。
小结
友元是 C++ 封装体系中一个经过审慎设计的"逃生舱"——在不完全放弃 private 保护的前提下,为特定的外部函数或类授予访问权限。友元函数适合运算符重载(尤其是 operator<<),友元类适合紧耦合的实现搭档(容器与迭代器、数学类型协作),友元成员函数则在需要最小权限授权时发挥作用。
但友元也是一把双刃剑——每多一个友元声明,封装就多一道裂缝。笔者的建议是:在写下 friend 之前,先问自己"有没有不破坏封装的替代方案?"如果有,用替代方案;如果没有,而且场景确实需要直接访问内部数据,再放心地用友元。
下一章我们会把目光转向 this 指针和级联调用——深入理解 this 在对象模型中扮演的角色,以及如何利用它写出更优雅的链式接口。