背景:访问控制的两难
在本章及前一章中,我们反复强调访问控制的价值:它决定谁能访问类的哪些成员。
private
成员仅供本类成员使用;public
成员则对所有代码开放。
第 14.6 课《访问函数》进一步指出:应将数据设为private
,再通过精心设计的public
接口供外部使用。
然而,某些场景下,这种安排并不理想:
- 职责分离:例如一个“存储类”负责管理数据,另有一个“显示类”负责复杂的数据显示。若将二者合并,接口臃肿;若保持独立,显示类又无法访问存储类的
private
成员。 - 语法偏好:有时我们更倾向于使用非成员函数(尤其是运算符重载,后续课程详述),但非成员函数同样受限于访问权限。
- 接口缺失:若现有公开接口不足以完成需求,而新增接口又会暴露实现细节或带来误用风险,如何在不破坏封装的前提下解决?
我们需要的,是一种按需求局部突破访问控制的机制。
友元机制:按需授权
答案正是 friend(友元)。
在类体内使用 friend
声明,可明确授予某个类或函数(成员或非成员)对该类所有 private
与 protected
成员的完全访问权。
这样,类可以有选择地让外部代码直接访问其实现细节,而不影响其余部分。
关键洞见
友元关系始终由被访问的类授予;被授权方无需任何额外声明。
通过“访问控制 + 友元”,类始终掌握访问权的最终决定权。
友元非成员函数
友元非成员函数 是指被类显式授予访问权的非成员函数。除此之外,它仍是一个普通函数。
示例:授予非成员函数访问权
#include <iostream>
class Accumulator
{
private:
int m_value{ 0 };
public:
void add(int value) { m_value += value; }
// 友元声明
friend void print(const Accumulator& accumulator);
};
// 非成员函数实现
void print(const Accumulator& accumulator)
{
// 因 print 为 Accumulator 的友元,可直接访问 m_value
std::cout << accumulator.m_value;
}
int main()
{
Accumulator acc{};
acc.add(5);
print(acc); // 调用非成员函数
}
在类内定义友元非成员函数
友元非成员函数也可在类体内直接定义:
#include <iostream>
class Accumulator
{
private:
int m_value{ 0 };
public:
void add(int value) { m_value += value; }
// 类内定义的友元非成员函数
friend void print(const Accumulator& accumulator)
{
std::cout << accumulator.m_value;
}
};
尽管写在类内,print
仍是非成员函数;其友元身份使其可访问 Accumulator
的私有成员。
语法层面偏好非成员函数
以下示例比较成员函数与非成员函数:
#include <iostream>
class Value
{
private:
int m_value{};
public:
explicit Value(int v) : m_value{ v } {}
bool isEqualToMember(const Value& v) const;
friend bool isEqualToNonmember(const Value& v1, const Value& v2);
};
bool Value::isEqualToMember(const Value& v) const
{
return m_value == v.m_value; // 隐式对象 vs 显式参数
}
bool isEqualToNonmember(const Value& v1, const Value& v2)
{
return v1.m_value == v2.m_value; // 显式对象,对称清晰
}
int main()
{
Value v1{ 5 }, v2{ 6 };
std::cout << v1.isEqualToMember(v2) << '\n';
std::cout << isEqualToNonmember(v1, v2) << '\n';
}
非成员函数参数对称,可读性更佳;后续运算符重载时,这一优势更为明显。
多类共享友元函数
一个函数可同时成为多个类的友元:
#include <iostream>
class Humidity; // 前向声明
class Temperature
{
private:
int m_temp{ 0 };
public:
explicit Temperature(int temp) : m_temp{ temp } {}
friend void printWeather(const Temperature& t, const Humidity& h);
};
class Humidity
{
private:
int m_humidity{ 0 };
public:
explicit Humidity(int h) : m_humidity{ h } {}
friend void printWeather(const Temperature& t, const Humidity& h);
};
void printWeather(const Temperature& t, const Humidity& h)
{
std::cout << "The temperature is " << t.m_temp
<< " and the humidity is " << h.m_humidity << '\n';
}
int main()
{
Humidity hum{ 10 };
Temperature temp{ 12 };
printWeather(temp, hum);
}
printWeather
需访问Temperature
与Humidity
的私有数据,故被二者共同授予友元身份。- 类前向声明
class Humidity;
确保编译器在解析Temperature
中的友元声明时已知Humidity
存在。
友元是否破坏封装?
否。
友元由被隐藏数据的类主动授予,相当于将友元视为类的扩展,访问行为在预期之内。合理使用友元可在不破坏封装的前提下,实现职责分离或语法优化。
注意
- 友元与类实现耦合度高,实现变更时需同步修改友元代码。
- 若友元过多或链式友元复杂,则改动影响面扩大。
- 实现友元函数时,优先使用类的公开接口,减少对私有成员的依赖,从而降低维护成本。
最佳实践
友元函数应尽可能通过公开接口完成工作,而非直接访问私有成员。
优先非友元函数
与第 14.8 课《数据隐藏(封装)的好处》所述“优先使用非成员函数”同理,我们也应优先将函数实现为非友元。
示例对比:
// 版本 1:友元直接访问
class Accumulator
{
private:
int m_value{ 0 };
public:
void add(int v) { m_value += v; }
friend void print(const Accumulator& a);
};
void print(const Accumulator& a)
{
std::cout << a.m_value; // 直接访问,耦合度高
}
// 版本 2:非友元通过公开接口
class Accumulator
{
private:
int m_value{ 0 };
public:
void add(int v) { m_value += v; }
int value() const { return m_value; } // 合理访问函数
};
void print(const Accumulator& a)
{
std::cout << a.value(); // 使用接口,降低耦合
}
若类实现改变(如 m_value
改名),版本 2 仅需修改类内部及 value()
实现,而无需改动 print
,维护成本更低。
最佳实践
在可行且合理的情况下,优先将函数设计为非友元。
谨慎新增公开接口,避免过度膨胀;若确需友元,则应在设计层面权衡利弊。