8.3 友元
private 成员受到保护,外部代码不能直接访问。这种保护很重要:它让对象能自己维护自己的规则。但 C++ 还有一个叫 friend 的功能,可以让类主动授权某些外部函数或外部类访问自己的 private 和 protected 成员。
友元不是“我懒得写 getter”的快捷方式。它是一种明确的设计表达:这个外部函数虽然不是成员函数,但它和这个类型关系非常紧密,应该和这个类的接口放在一起。
为什么需要友元
假设 Point 隐藏自己的坐标。普通外部函数不能读取 x_ 和 y_。
class Point {
private:
double x_;
double y_;
public:
Point(double x, double y) : x_(x), y_(y) {}
};
double distance(const Point& a, const Point& b) {
// 错误:x_ 和 y_ 是 private。
// return std::sqrt((a.x_ - b.x_) * (a.x_ - b.x_) + ...);
}你可以添加 getter,很多时候这确实是正确做法。但两点之间的距离是一个对称操作:两个点没有谁更像“主角”。自由函数读起来更自然:
double d = distance(start, finish);这时,Point 可以授权这个自由函数。
#include <cmath>
class Point {
private:
double x_;
double y_;
public:
Point(double x, double y) : x_(x), y_(y) {}
friend double distance(const Point& a, const Point& b);
};
double distance(const Point& a, const Point& b) {
double dx = a.x_ - b.x_;
double dy = a.y_ - b.y_;
return std::sqrt(dx * dx + dy * dy);
}distance 仍然不是成员函数。它没有 this 指针,调用方式是 distance(a, b),不是 a.distance(b)。类只是说:这个函数可以读取我的私有数据。
友元是具体的、单向的、不会继承的
友元比很多初学者想象得更精确:
- 如果
Point声明distance是友元,只有这个准确的函数获得访问权。 - 如果
Point把Inspector设为友元,不代表Inspector自动把Point设为友元。 - 如果基类声明了某个友元,派生类不会自动把自己的 private 成员也开放给它。
这也是友元强大但仍然可读的原因:授权写在类定义里。
class SecretBox {
private:
int code_;
public:
explicit SecretBox(int code) : code_(code) {}
friend class SecurityAudit;
};
class SecurityAudit {
public:
void inspect(const SecretBox& box) const {
std::cout << "internal code: " << box.code_ << std::endl;
}
};这里 SecurityAudit 可以读取 SecretBox::code_,普通代码仍然不行。如果程序中到处都需要这个 code,那它应该成为公共查询接口。如果只有一个狭窄的审计工具需要它,friend 就能表达这种狭窄关系。
友元函数和公共 getter 的取舍
写 friend 之前,先问清楚你真正想要的接口是什么。
class Wallet {
private:
int cents_;
public:
int cents() const { return cents_; }
};当一个值本来就是类型的公共含义时,getter 很合适。例如 Wallet 暴露 cents 可能很合理。
当某个操作需要内部细节,但你又不想把细节开放给所有代码时,友元更合适。
class Box {
private:
int width_;
int height_;
int depth_;
public:
Box(int width, int height, int depth)
: width_(width), height_(height), depth_(depth) {}
friend bool same_volume(const Box& left, const Box& right);
};
bool same_volume(const Box& left, const Box& right) {
return left.width_ * left.height_ * left.depth_
== right.width_ * right.height_ * right.depth_;
}这样既能保持尺寸私有,又能允许一个被精确选择的比较函数。代价是耦合:友元知道类的内部结构,所以 private 表示方式改变时,友元也可能需要跟着改。