Skip to content

友元:把私有成员的访问权,主动给出去 ​

嘿!我的朋友!今天咱们介绍 friend!别误会,friend 实际上是 C++ 的关键字,哈哈!前面几章咱们一直在强调封装——private 成员藏在类内部,外部代码只能通过 public 接口操作对象。但偶尔您会碰到一种情况:某个外部函数或另一个类确实需要访问私有成员,而且这种访问是合理的、不可避免的。C++ 提供了专门的机制来处理这种场景,就是**friend(友元)**。

友元的本质是定向授权:类的作者主动声明"我信任这个函数/类,允许它看到我的私有成员"。它不是把封装彻底拆掉(那直接全写 public 就行了),而是把访问权限精确地授予指定的函数或类。接下来咱们把友元的三种形态(友元函数、友元类、友元成员函数)逐一拆开来看,最后讨论什么时候该用友元,什么时候不该用。

友元函数:授权一个外部函数 ​

友元函数是最基本的友元形态。咱们声明的方式是在类的内部用 friend 关键字加上一个普通函数的声明:

C++
class Vector3D {
private:
    float x, y, z;
public:
    Vector3D(float x, float y, float z) : x(x), y(y), z(z) {}
    // 声明 dot_product 为友元函数
    friend float dot_product(const Vector3D& a, const Vector3D& b);
};

// 友元函数定义——不是成员函数,不需要 Vector3D::
float dot_product(const Vector3D& a, const Vector3D& b)
{
    return a.x * b.x + a.y * b.y + a.z * b.z;
}

咱们这里有几个要点需要搞清楚。首先,friend 声明出现在类的内部,但 dot_product 不是 Vector3D 的成员函数,它是一个普通的全局函数,只不过获得了访问 Vector3D 私有成员的特权。调用时和普通函数一样:dot_product(v1, v2),而不是 v1.dot_product(v2)。

其次,friend 声明可以放在类的任何位置,public、private、protected 区域都无所谓,效果完全相同。通常咱们把它集中放在类的开头或末尾,和成员函数声明分开,一眼就能看出"哪些外部函数拥有特殊权限"。

友元函数最经典的应用场景是重载 operator<<,让自定义类型能直接输出到流。这个场景之所以需要友元,是因为 operator<< 的左操作数是 std::ostream&,而不是咱们的类本身,它不可能写成咱们的成员函数:

C++
class Point {
private:
    int x, y;

public:
    Point(int x, int y) : x(x), y(y) {}

    // 友元重载 operator<<
    friend std::ostream& operator<<(std::ostream& os, const Point& p);
};

std::ostream& operator<<(std::ostream& os, const Point& p)
{
    os << "(" << p.x << ", " << p.y << ")";
    return os;
}

// 现在可以这样用了
Point p(3, 4);
std::cout << p << std::endl;  // 输出: (3, 4)

operator<< 重载的细节咱们会在下一章展开,这里只需要理解它为什么必须是友元——第一个参数是 std::ostream&,不是 Point,所以这个函数没法写成 Point 的成员函数。

友元类:授权给整个类 ​

如果一个类的很多成员函数都需要访问另一个类的私有成员,逐个声明友元函数就太繁琐了。咱们这时可以用 friend class 一次性授权给整个类:

展开代码收起代码共 34 行
C++
class Matrix {
private:
    float data[3][3];

public:
    Matrix()  // 初始化为单位矩阵
    {
        for (int i = 0; i < 3; ++i) {
            for (int j = 0; j < 3; ++j) {
                data[i][j] = (i == j) ? 1.0f : 0.0f;
            }
        }
    }

    // Vector 是 Matrix 的友元类
    friend class Vector;
};

class Vector {
private:
    float x, y, z;

public:
    Vector(float x, float y, float z) : x(x), y(y), z(z) {}

    Vector transform(const Matrix& m)
    {
        // Vector 的成员函数可以直接访问 Matrix 的 private 成员
        float nx = m.data[0][0] * x + m.data[0][1] * y + m.data[0][2] * z;
        float ny = m.data[1][0] * x + m.data[1][1] * y + m.data[1][2] * z;
        float nz = m.data[2][0] * x + m.data[2][1] * y + m.data[2][2] * z;
        return Vector(nx, ny, nz);
    }
};

friend class Vector; 意味着 Vector 的所有成员函数都可以访问 Matrix 的私有成员。这是一种粗粒度的授权,咱们要慎重使用,但确实有一些场景下两个类关系足够紧密,值得这种级别的信任。典型的合理场景包括"容器 + 迭代器"模式、以及上面这种数学类型之间的紧密协作。共同特征是:两个类在逻辑上是一个整体,只是出于代码组织的原因被拆成了两个类。

友元成员函数:只授权一个成员函数 ​

您要是觉得"友元类授权太宽泛",C++ 还提供了更精细的控制:只授权另一个类的某一个成员函数:

C++
class Vector;  // 前向声明

class Matrix {
private:
    float data[3][3];
public:
    Matrix();
    // 只授权 Vector::transform 这一个成员函数
    friend Vector Vector::transform(const Matrix& m);
};

class Vector {
private:
    float x, y, z;

public:
    Vector(float x, float y, float z) : x(x), y(y), z(z) {}

    Vector transform(const Matrix& m);
};

理论上这种方式最安全——最小权限原则嘛。但实际使用中,友元成员函数有一个让人头疼的依赖问题:声明 friend Vector Vector::transform(const Matrix&) 的时候,编译器必须已经看到 Vector 类的完整定义,否则它不知道 transform 确实是 Vector 的成员函数。这就要求咱们仔细安排头文件包含顺序,弄不好就会陷入循环依赖。咱们要是需要授权的成员函数有三四个之多,不如直接用友元类来得干脆。

什么时候该用友元 ​

友元很容易被滥用,咱们有必要认真讨论一下使用边界。

合理使用友元的场景。咱们最典型的是运算符重载——前面说的 operator<< 就是最好的例子。紧耦合的实现搭档也是合理的,比如 Container 和它的 Iterator、Matrix 和 Vector。这些情况下两个类本来就共享实现细节,用友元只是把这个事实在代码层面显式表达出来。

不应该使用友元的场景。咱们要是只是想偷懒、不想设计合适的公共接口,随手加个 friend 让外部函数直接操作私有数据,这种友元就是有害的。大多数"需要友元"的场景其实可以通过提供恰当的访问接口来替代:

C++
// 不推荐:用友元绕过接口设计
class SensorData {
    friend void serialize(const SensorData& data, uint8_t* buffer);
private:
    float values[100];
    int count;
};

// 推荐:提供只读接口,封装完好
class SensorData {
private:
    float values[100];
    int count;
public:
    const float* data() const { return values; }
    int size() const { return count; }
};

咱们要记住友元关系的三个关键特性,它们经常被误解。友元不继承:如果 Base 是 X 的友元,Derived(继承自 Base)并不会自动成为 X 的友元。友元不传递:如果 A 是 B 的友元,B 是 C 的友元,A 并不会自动成为 C 的友元。友元是单向的:A 是 B 的友元,意味着 A 能访问 B 的私有成员,但 B 不能反过来访问 A 的私有成员——除非 A 也声明 B 为友元。这三条规则确保了友元权限不会越过声明无限扩散。

友元声明不是函数前向声明。在类的内部写 friend void foo(); 确实会使 foo 成为该类的友元,但咱们把友元函数定义在类外面的时候,要确保在调用点之前能找到它的普通声明(而非 friend 声明)。否则在某些编译器上可能出现"找不到函数定义"的链接错误,尤其是当友元函数定义在另一个 .cpp 文件中的时候。最稳妥的做法是在类的外面再加一行普通的函数声明。

实战——friend_demo.cpp ​

现在咱们来看一个完整的示例:Matrix 和 Vector 通过友元关系协作完成矩阵-向量乘法。

展开代码收起代码共 55 行
C++
// friend_demo.cpp
#include <array>
#include <cstdio>

class Vector;

class Matrix {
private:
    std::array<std::array<float, 3>, 3> data;
public:
    Matrix() : data{{{1, 0, 0}, {0, 1, 0}, {0, 0, 1}}} {}
    void set(int row, int col, float value) { data[row][col] = value; }
    void print() const
    {
        for (int i = 0; i < 3; ++i)
            std::printf("| %.2f %.2f %.2f |\n",
                        data[i][0], data[i][1], data[i][2]);
    }
    // 授权 Vector 访问私有成员
    friend class Vector;
};

class Vector {
private:
    std::array<float, 3> v;
public:
    Vector(float x, float y, float z) : v{x, y, z} {}
    // 友元权限:直接访问 Matrix 内部数组
    Vector transform(const Matrix& m) const
    {
        float nx = m.data[0][0] * v[0] + m.data[0][1] * v[1] + m.data[0][2] * v[2];
        float ny = m.data[1][0] * v[0] + m.data[1][1] * v[1] + m.data[1][2] * v[2];
        float nz = m.data[2][0] * v[0] + m.data[2][1] * v[1] + m.data[2][2] * v[2];
        return Vector(nx, ny, nz);
    }
    void print() const
    { std::printf("(%.2f, %.2f, %.2f)\n", v[0], v[1], v[2]); }
};

int main()
{
    Matrix m;
    m.set(0, 0, 2.0f);
    m.set(1, 1, 3.0f);
    m.set(2, 2, 0.5f);
    Vector v(1.0f, 2.0f, 4.0f);
    Vector result = v.transform(m);
    std::printf("Matrix:\n");
    m.print();
    std::printf("Vector:  ");
    v.print();
    std::printf("Result:  ");
    result.print();
    return 0;
}

编译运行:

bash
g++ -std=c++17 -Wall -Wextra -o friend_demo friend_demo.cpp
./friend_demo

预期输出:

text
Matrix:
| 2.00 0.00 0.00 |
| 0.00 3.00 0.00 |
| 0.00 0.00 0.50 |
Vector:  (1.00, 2.00, 4.00)
Result:  (2.00, 6.00, 2.00)

咱们这个例子里,Vector::transform 直接访问了 Matrix::data 这个私有数组。如果不用友元,就得提供一个 float get(int, int) const 的访问接口——不是不行,但在数学库这种对性能敏感的场景下,少一层间接访问就意味着更紧凑的循环和更友好的缓存行为。

练习 ​

练习 1:用友元实现 operator<< ​

请您为下面的 Student 类实现一个友元函数 operator<<,使得 std::cout << student; 能够直接输出学生的信息。

C++
class Student {
private:
    int id;
    float score;
    std::string name;

public:
    Student(int id, float score, const std::string& name)
        : id(id), score(score), name(name) {}

    // 在这里添加友元声明
};

// 在这里实现 operator<<

验证方式:请您创建几个 Student 对象,用 std::cout 输出它们的信息,确认格式正确。

参考答案
展开代码收起代码共 36 行
C++
#include <iostream>
#include <string>

class Student {
 private:
    int id;
    float score;
    std::string name;

 public:
    Student(int id, float score, const std::string& name)
        : id(id), score(score), name(name) {}

    friend std::ostream& operator<<(std::ostream& os, const Student& student);
};

std::ostream& operator<<(std::ostream& os, const Student& student) {
    os << "学生ID:" << student.id << ","
       << "姓名:" << student.name << ","
       << "成绩:" << student.score;
    return os;
}

int main() {
    // 创建多个 Student 对象
    Student student1(1, 95.5f, "小明");
    Student student2(2, 88.0f, "小红");
    Student student3(3, 76.5f, "杰");

    // 输出学生信息,验证 operator<<
    std::cout << student1 << std::endl;
    std::cout << student2 << std::endl;
    std::cout << student3 << std::endl;

    return 0;
}

编译运行:

bash
g++ -std=c++17 -Wall -Wextra main.cpp -o main && ./main

运行结果:

text
学生ID:1,姓名:小明,成绩:95.5
学生ID:2,姓名:小红,成绩:88
学生ID:3,姓名:杰,成绩:76.5

练习 2:设计 Container-Iterator 友元对 ​

请您实现一个 IntBuffer 容器和一个 IntBufferIterator 迭代器。IntBuffer 内部用固定大小的 int 数组存储数据,IntBufferIterator 通过友元权限访问该数组完成遍历。要求外部代码无法直接访问 IntBuffer 的内部数组。提示:IntBuffer 声明 friend class IntBufferIterator;,迭代器持有指向容器的指针。

参考答案
展开代码收起代码共 45 行
C++
#include <array>
#include <cstddef>
#include <iostream>

class IntBufferIterator;

class IntBuffer {
 private:
  std::array<int, 4> data{};

 public:
  IntBuffer() {
    for (std::size_t i = 0; i < data.size(); ++i) {
      data[i] = static_cast<int>(i * 2);
    }
  }

  friend class IntBufferIterator;
};

class IntBufferIterator {
 private:
  const IntBuffer* buffer = nullptr;
  std::size_t index = 0;

 public:
  explicit IntBufferIterator(const IntBuffer& buffer) : buffer(&buffer) {}

  bool hasNext() const { return index < buffer->data.size(); }

  int next() {
    int value = buffer->data[index];
    ++index;
    return value;
  }
};

int main() {
  IntBuffer buffer;
  IntBufferIterator iterator{buffer};

  while (iterator.hasNext()) {
    std::cout << iterator.next() << std::endl;
  }
}

编译运行:

bash
g++ -std=c++17 -Wall -Wextra main.cpp -o main && ./main

运行结果:

text
0
2
4
6

pdf-latest-4-g85128cc · 85128cc · 2026-10-05