23. 友元:友元函数、友元类、友元的优缺点

友元(friend)这个特性,在C++里一直是个争议话题。有人觉得它破坏了封装,有人觉得它必不可少。我个人觉得,它就像一把锋利的刀——用好了切菜顺手,用不好容易伤着自己。

说白了,友元就是让类外部的某个函数或某个类,能够访问本类的私有成员。正常情况下,私有成员只有类内部才能碰,但友元机制给了你一个「开后门」的权限。

为什么需要友元?

你想想看,有时候我们确实需要一些外部函数来操作类的内部数据。比如运算符重载,特别是双目运算符,像 operator<< 这种,它左边是 ostream,右边是你的类对象。这种情况下,你没法把它写成成员函数,因为左操作数不是你的类。

我在项目中遇到过这样一个场景:写一个矩阵类,需要重载 operator* 实现矩阵乘法。如果只写成员函数版本,那 matrix * 2 可以,但 2 * matrix 就不行。这时候友元函数就派上用场了。

友元函数

友元函数不是类的成员函数,但它可以访问类的私有成员。声明时在函数前面加个 friend 关键字就行。

class Matrix {
private:
    int data[3][3];
public:
    Matrix() { /* 初始化 */ }
    
    // 友元函数声明
    friend Matrix operator*(const Matrix& m, int scalar);
    friend Matrix operator*(int scalar, const Matrix& m);
    
    // 友元函数:打印矩阵
    friend void printMatrix(const Matrix& m);
};

// 友元函数定义
Matrix operator*(const Matrix& m, int scalar) {
    Matrix result;
    for (int i = 0; i < 3; ++i)
        for (int j = 0; j < 3; ++j)
            result.data[i][j] = m.data[i][j] * scalar;
    return result;
}

Matrix operator*(int scalar, const Matrix& m) {
    return m * scalar;  // 复用上面的实现
}

void printMatrix(const Matrix& m) {
    for (int i = 0; i < 3; ++i) {
        for (int j = 0; j < 3; ++j)
            std::cout << m.data[i][j] << ' ';
        std::cout << '\n';
    }
}

小提示:友元函数可以放在类的 public、private 或 protected 区域,效果都一样。我习惯放在 public 区域开头,这样一眼就能看到这个类有哪些「外部朋友」。

友元类

友元类就更直接了——整个类都是你的朋友,它的所有成员函数都能访问你的私有成员。

class Engine {
private:
    int horsepower;
    double displacement;
    
    friend class Mechanic;  // 声明 Mechanic 是友元类
    
public:
    Engine(int hp, double disp) : horsepower(hp), displacement(disp) {}
};

class Mechanic {
public:
    void tuneEngine(Engine& e) {
        // 可以访问 Engine 的私有成员
        e.horsepower += 50;
        e.displacement *= 1.1;
    }
    
    void printSpecs(const Engine& e) {
        std::cout << "马力: " << e.horsepower << '\n';
        std::cout << "排量: " << e.displacement << '\n';
    }
};

注意:友元关系是单向的,不能传递。A 是 B 的友元,B 是 C 的友元,不代表 A 能访问 C 的私有成员。我曾经在代码评审里看到有人踩了这个坑,以为友元能像继承一样传递下去。

友元的优缺点

优点 缺点
方便实现某些特定功能(如运算符重载) 破坏了封装性,增加了耦合
提高性能,避免频繁调用 getter/setter 维护困难,修改一个类的私有成员可能影响多个友元
让两个紧密相关的类可以协作 友元关系不能继承,容易造成设计混乱

避坑指南

我曾经接手过一个项目,里面有个类声明了十几个友元类和友元函数。每次改那个类的私有成员,都要去检查所有友元有没有受影响。后来我花了整整两天重构,把大部分友元都改成了接口调用。

所以我的建议是:

  • 能用公有接口解决的,就别用友元
  • 友元函数比友元类更可控,优先考虑
  • 友元类的数量控制在 1-2 个以内
  • 如果两个类需要频繁互访私有成员,考虑是不是应该合并成一个类

友元的核心逻辑

下面这张图帮你理清友元的关系:

类 A 私有成员: data_ 公有成员: getData() friend class B; 友元函数 friend void func(); 类 B(友元类) 可以访问 A::data_ 可以调用 A 的私有方法 单向访问,不可传递 声明友元 友元函数也可访问 友元关系:类 A 主动声明谁可以访问自己的私有成员 友元类 B 的所有成员函数都能访问 A 的私有成员

核心要点:友元是「被访问者主动授予」的权限,不是「访问者主动索取」的。类 A 说「B 是我的朋友」,B 才能访问 A 的私有成员。反过来不行。

实际项目中的使用建议

我个人的经验是,友元最适合用在两个场景:

  1. 运算符重载——特别是 <<>>* 这些需要对称性的运算符
  2. 工厂模式——工厂类需要访问产品类的私有构造函数

其他情况,能不用就不用。你想想看,如果每个类都随便开友元,那 private 还有什么意义?封装性一旦被破坏,后续维护的成本会直线上升。

嗯,关于友元就聊这么多。记住一句话:友元是特例,不是常规手段。用的时候多问自己一句——真的非用不可吗?


公众号:蓝海资料掘金营,微信 deep3321