15. 友元:友元函数、友元类、友元的应用场景

说到友元,很多初学者第一反应是:「这不破坏封装性吗?」

嗯,我刚开始学的时候也这么想。后来在项目中写了一个矩阵运算库,才真正体会到——有时候,封装性是要为性能让路的

友元,说白了就是给「外人」开个后门。让某个函数或类,能访问你的私有成员。听起来有点危险?确实。但用对了地方,它就是一把利器。

15.1 为什么需要友元?

C++ 的类默认是「闭关锁国」的。私有成员只有自己人能碰。但现实世界没这么简单。

举个例子:你写了一个 Matrix 类,想重载 operator<< 来打印矩阵内容。这个运算符的左侧是 ostream,不是你的 Matrix 对象。所以它不能直接访问 Matrix 的私有数据。

怎么办?

  • 把所有数据改成 public?—— 太粗暴,等于裸奔。
  • 写一堆 getter?—— 可以,但每次调用都有函数调用开销,性能敏感场景下不划算。
  • 用友元?—— 对,这就是它的典型场景。

核心思想:友元不是「破坏封装」,而是「在信任的前提下,打开一扇窗」。

15.2 友元函数

友元函数,就是一个不属于类的普通函数,但被授权访问类的私有成员。

15.2.1 声明方式

在类定义里,用 friend 关键字声明一下就行。位置无所谓,public、private、protected 区域都可以。

class Matrix {
private:
    int data[4][4];
public:
    Matrix() { /* 初始化 */ }
    
    // 声明友元函数
    friend void printMatrix(const Matrix& m);
};

// 普通函数,但可以访问 Matrix 的私有成员
void printMatrix(const Matrix& m) {
    for (int i = 0; i < 4; ++i) {
        for (int j = 0; j < 4; ++j) {
            std::cout << m.data[i][j] << " ";
        }
        std::cout << std::endl;
    }
}

注意:printMatrix 不是成员函数。它没有 this 指针,调用时也不需要 m.printMatrix() 这种形式。

15.2.2 运算符重载中的友元

我个人觉得,友元函数在运算符重载里用得最多。尤其是双目运算符,比如 +-<<>>

class Complex {
private:
    double real, imag;
public:
    Complex(double r = 0, double i = 0) : real(r), imag(i) {}
    
    // 友元:重载 +
    friend Complex operator+(const Complex& a, const Complex& b);
    
    // 友元:重载 <<
    friend std::ostream& operator<<(std::ostream& os, const Complex& c);
};

Complex operator+(const Complex& a, const Complex& b) {
    return Complex(a.real + b.real, a.imag + b.imag);
}

std::ostream& operator<<(std::ostream& os, const Complex& c) {
    os << "(" << c.real << ", " << c.imag << "i)";
    return os;
}

小技巧:如果运算符的左侧操作数不是当前类对象(比如 << 左侧是 ostream),那就只能用友元函数,不能用成员函数重载。

15.3 友元类

友元类更「大方」—— 整个类都是你的朋友。A 类声明 B 类是它的友元,那 B 的所有成员函数都能访问 A 的私有成员。

class Engine {
private:
    int horsepower;
    double displacement;
    
    friend class Mechanic;  // Mechanic 是 Engine 的友元类
};

class Mechanic {
public:
    void tune(Engine& e) {
        e.horsepower += 50;      // 可以访问私有成员
        e.displacement *= 1.1;   // 可以访问私有成员
    }
};

我在项目中遇到过这样的场景:一个 Document 类和一个 DocumentSerializer 类。序列化器需要读取文档的内部数据结构,但又不希望把这些数据暴露给外部。用友元类就很合适。

注意:友元关系是单向的,不能继承。A 是 B 的友元,不代表 B 是 A 的友元。A 的友元类 C 也不能自动访问 B 的私有成员。

15.4 友元的应用场景

我总结了一下,友元主要用在以下几个地方:

场景 说明 示例
运算符重载 尤其是 <<、>>、双目运算符 Complex 类的 +、<<
迭代器模式 迭代器需要访问容器的内部结构 自定义容器的 begin()/end()
工厂模式 工厂类需要创建对象,但构造函数是私有的 单例模式、对象池
测试工具 单元测试需要访问私有成员来验证状态 Google Test 中的 FRIEND_TEST
性能敏感代码 避免 getter/setter 的函数调用开销 矩阵运算、图形引擎

15.4.1 工厂模式中的友元

我曾经写过一个对象池,希望用户只能通过工厂创建对象,不能自己 new。把构造函数设为 private,然后把工厂类设为友元。

class Connection {
private:
    Connection() {}  // 私有构造函数
    
    friend class ConnectionPool;
};

class ConnectionPool {
public:
    Connection* acquire() {
        return new Connection();  // 友元,可以调用私有构造函数
    }
    
    void release(Connection* conn) {
        delete conn;
    }
};

15.4.2 测试中的友元

嗯,这里要注意。很多人觉得测试应该只测 public 接口。但有时候,你想验证内部状态是否正确。比如一个缓存类,你想确认它有没有正确淘汰数据。

class Cache {
private:
    std::map<int, std::string> data;
    int maxSize;
    
    void evictOldest();  // 私有方法
    
    // 测试类可以访问私有成员
    friend class CacheTest;
};

// 测试代码
class CacheTest {
public:
    void testEviction() {
        Cache cache;
        cache.data[1] = "a";  // 直接操作私有数据,验证逻辑
        cache.evictOldest();
        assert(cache.data.empty());
    }
};

我的建议:友元用于测试时,可以用条件编译包裹起来。发布版本里去掉测试友元,避免不必要的耦合。

15.5 友元的注意事项

说了这么多好处,也得泼点冷水。友元不是银弹,用不好会出问题。

  • 不要滥用:能用 public 接口解决的问题,就别用友元。我见过一个项目,几乎每个类都声明了五六个友元,最后改一个类的内部实现,所有友元类都得跟着改——维护成本爆炸。
  • 友元不传递:A 是 B 的友元,B 是 C 的友元,但 A 不是 C 的友元。别指望「朋友的朋友也是我的朋友」。
  • 友元不继承:基类的友元不能访问派生类的私有成员。反过来,派生类的友元也不能访问基类的私有成员。
  • 友元函数不是成员函数:它没有 this 指针,不能直接调用类的成员函数(除非通过对象)。

避坑指南:我曾经在代码里把友元声明写在了 private 区域,结果同事以为这是个私有成员函数,找了半天没找到定义。后来我养成了习惯——友元声明统一放在类定义的最前面,或者用注释标明。

15.6 知识体系总览

下面这张图,帮你把友元的知识点串起来:

友元知识体系 友元 (friend) 友元函数 友元类 普通函数 运算符重载函数 成员函数(另一个类) 整个类都是朋友 所有成员函数可访问 常用于工厂/迭代器 典型应用场景 运算符重载 | 迭代器模式 | 工厂模式 | 单元测试 | 性能优化

15.7 小结

友元这个特性,用好了是神器,用砸了是灾难。我的经验是:

  • 优先考虑 public 接口,实在绕不开再用友元
  • 友元函数比友元类更可控,能用函数就别用类
  • 友元关系要写在文档里,不然后来者一脸懵
  • 记得友元是单向的、不继承的、不传递的

你想想看,C++ 给了你这么多控制权,就是让你在「封装」和「灵活」之间做权衡。友元就是那个平衡点。用对了,代码既安全又高效。


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