一、friend
的基本概念
- 在 C++ 中,类的成员函数默认只能访问自身的
private
和protected
成员,外部函数或其他类无法直接访问这些私有成员。 - 通过在类内部声明某个函数或类为 友元,允许该函数/类突破访问控制,访问该类的私有(
private
)和保护(protected
)成员。 - 友元关系不是对等的:被声明为友元的函数/类,并不因此获得对其所属类的访问权;需要对方也显式声明其为友元才能互相访问。
二、友元的形式
- 友元函数(Friend Function)
- 友元类(Friend Class)
- 友元成员函数(Friend Member Function)
- 友元模板(Friend Template)
1. 友元函数
class A {
private:
int _x;
public:
A(int x): _x(x) {}
// 声明全局函数 foo 为 A 的友元
friend void foo(const A& a);
};
void foo(const A& a) {
// 可以访问 A::_x
std::cout << "A::_x = " << a._x << std::endl;
}
- 优点:方便外部函数直接操作类内部数据,不必通过公有接口。
- 缺点:破坏了封装性,需要谨慎使用。
2. 友元类
class B; // 前向声明
class A {
private:
int _x;
public:
A(int x): _x(x) {}
friend class B; // 声明 B 为友元类
};
class B {
public:
void show(const A& a) {
// 可以访问 A::_x
std::cout << "A::_x = " << a._x << std::endl;
}
};
- 友元类:整个类都可以访问另一个类的私有成员,相当于把该类所有成员函数都当作友元函数。
3. 友元成员函数
class A {
private:
int _x;
public:
A(int x): _x(x) {}
friend void B::show(const A& a); // 只声明 B::show 为友元
};
class B {
public:
void show(const A& a) {
std::cout << "A::_x = " << a._x << std::endl;
}
};
- 在
A
中声明B::show
为友元,仅限这一成员函数,而非整个类。
4. 友元模板
情况一:让整个模板类成为友元
template<typename T>
class Helper;
class A {
private:
int _x;
public:
A(int x): _x(x) {}
// 将模板 Helper<T> 全部实例化都设为友元
template<typename T>
friend class Helper;
};
template<typename T>
class Helper {
public:
void dump(const A& a) {
std::cout << "A::_x = " << a._x << std::endl;
}
};
情况二:让模板函数成为友元
class B;
template<typename T>
void printB(const B& b);
class B {
private:
int _y;
public:
B(int y): _y(y) {}
template<typename T>
friend void printB<>(const B& b); // printB<T> 是 B 的友元
};
template<typename T>
void printB(const B& b) {
std::cout << "B::_y = " << b._y << std::endl;
}
三、使用注意事项与最佳实践
-
封装性
- 友元打破封装,增加类间耦合,必须谨慎使用。
- 建议仅在确实需要访问内部实现细节时才用。
-
最小化友元范围
- 尽量仅声明必要的函数或类为友元,而非整个类。
- 比如优先使用“友元成员函数”而非“友元类”。
-
双向访问需双重声明
- 友元关系不是互相传递的。若需要互相访问,双方都要声明对方为友元。
-
命名空间与作用域
- 友元声明发生在类的作用域内,而真正定义在命名空间作用域。
- 确保友元函数定义与声明的签名、模板参数完全一致。
-
模板实例化
- 对于友元模板,要注意编译器对模板实例化顺序的处理。
- 有时需要在类定义前进行模板声明。
-
测试与维护
- 由于破坏了封装,友元代码容易随着内部实现变化而失效或引入隐式依赖。
- 添加单元测试覆盖友元接口,避免内部重构时遗漏。
-
替代方案
- 若仅需部分内部状态,可考虑提供受限的公有(或受保护)访问函数,比如
protected get()
、const_iterator
等。 - 或者使用 内部代理(Proxy),将访问逻辑放在专用的接口类中,并限定其可见性。
- 若仅需部分内部状态,可考虑提供受限的公有(或受保护)访问函数,比如
四、综合示例
下例演示一个类 Matrix
,并通过友元函数和友元类实现矩阵间运算及调试输出:
#include <iostream>
#include <vector>
class Matrix; // 前向声明
// 友元函数:矩阵相加
Matrix operator+(const Matrix& a, const Matrix& b);
// 友元类:调试工具
class MatrixPrinter;
class Matrix {
private:
int rows, cols;
std::vector<double> data;
public:
Matrix(int r, int c): rows(r), cols(c), data(r*c, 0.0) {}
double& at(int i, int j) { return data[i*cols + j]; }
double at(int i, int j) const { return data[i*cols + j]; }
// 声明友元
friend Matrix operator+(const Matrix& a, const Matrix& b);
friend class MatrixPrinter;
};
// 矩阵相加实现
Matrix operator+(const Matrix& a, const Matrix& b) {
if (a.rows != b.rows || a.cols != b.cols) {
throw std::invalid_argument("矩阵维度不匹配");
}
Matrix result(a.rows, a.cols);
for (int i = 0; i < a.rows * a.cols; ++i) {
// 直接访问私有 data
result.data[i] = a.data[i] + b.data[i];
}
return result;
}
// 友元类:专门用于打印 Matrix 的内部数据
class MatrixPrinter {
public:
static void print(const Matrix& m) {
for (int i = 0; i < m.rows; ++i) {
for (int j = 0; j < m.cols; ++j) {
std::cout << m.data[i * m.cols + j] << " ";
}
std::cout << "\n";
}
}
};
int main() {
Matrix A(2,2), B(2,2);
A.at(0,0)=1; A.at(0,1)=2; A.at(1,0)=3; A.at(1,1)=4;
B.at(0,0)=5; B.at(0,1)=6; B.at(1,0)=7; B.at(1,1)=8;
Matrix C = A + B;
std::cout << "Matrix C:\n";
MatrixPrinter::print(C);
return 0;
}
-
说明:
operator+
作为友元函数,可以直接操作Matrix::data
。MatrixPrinter
作为友元类,可以在调试或日志中无障碍地打印矩阵内部状态。
五、总结
- 友元 是 C++ 中打破访问控制的有力手段,但也容易增加耦合、降低维护性。
- 使用时应遵循最小化原则:仅声明真正需要的函数/类为友元。
- 了解各种友元形式(函数、类、成员、模板),以及它们的使用场景与限制。
- 通过替代方案(公有接口、代理模式等)权衡可维护性与灵活性。