C++ 友元非成员函数:深入理解与应用

背景:访问控制的两难

在本章及前一章中,我们反复强调访问控制的价值:它决定谁能访问类的哪些成员。

  • private 成员仅供本类成员使用;
  • public 成员则对所有代码开放。
    第 14.6 课《访问函数》进一步指出:应将数据设为 private,再通过精心设计的 public 接口供外部使用。

然而,某些场景下,这种安排并不理想:

  • 职责分离:例如一个“存储类”负责管理数据,另有一个“显示类”负责复杂的数据显示。若将二者合并,接口臃肿;若保持独立,显示类又无法访问存储类的 private 成员。
  • 语法偏好:有时我们更倾向于使用非成员函数(尤其是运算符重载,后续课程详述),但非成员函数同样受限于访问权限。
  • 接口缺失:若现有公开接口不足以完成需求,而新增接口又会暴露实现细节或带来误用风险,如何在不破坏封装的前提下解决?

我们需要的,是一种按需求局部突破访问控制的机制。


友元机制:按需授权

答案正是 friend(友元)

在类体内使用 friend 声明,可明确授予某个函数(成员或非成员)对该类所有 privateprotected 成员的完全访问权。
这样,类可以有选择地让外部代码直接访问其实现细节,而不影响其余部分。

关键洞见
友元关系始终由被访问的类授予;被授权方无需任何额外声明。
通过“访问控制 + 友元”,类始终掌握访问权的最终决定权。


友元非成员函数

友元非成员函数 是指被类显式授予访问权的非成员函数。除此之外,它仍是一个普通函数。

示例:授予非成员函数访问权

#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 需访问 TemperatureHumidity 的私有数据,故被二者共同授予友元身份。
  • 类前向声明 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,维护成本更低。

最佳实践
在可行且合理的情况下,优先将函数设计为非友元。
谨慎新增公开接口,避免过度膨胀;若确需友元,则应在设计层面权衡利弊。

关注公众号,回复"cpp-tutorial"

可领取价值199元的C++学习资料

公众号二维码

扫描上方二维码或搜索"cpp-tutorial"