友元(friend)是 C++ 提供的一种「定向开权限」机制:允许类外部的函数或另一个类访问本类的 private / protected 成员。本文介绍友元的三种形式、为什么需要它,以及使用时的注意事项。

一、友元是什么

友元函数不是类的成员函数,却可以访问类的 private / protected 成员。

三个关键认知:

  • 不是成员函数:它属于全局作用域(或另一个类),不在本类作用域内;
  • 没有 this 指针:因为不属于任何对象,想访问某个对象的成员就必须把它作为参数传进来;
  • 权限是「授予」的:由被访问的类自己用 friend 声明,主动权在被访问者手里,而不是想访问的人自己宣布。

二、友元函数

语法是类内声明 + 类外定义:在类里加 friend 声明,函数体写在类外面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
using namespace std;

class A {
private:
int num;
public:
A(int x) : num(x) {}
friend void show(A obj); // 类内声明:授予 show 访问权限
};

// 定义在类外,它不属于 A 类,是普通的全局函数
void show(A obj) {
cout << obj.num << endl; // 可以直接访问私有成员 num,普通函数做不到
}

int main() {
A a(100);
show(a); // 100
return 0;
}

注意 show 是靠参数 obj 拿到对象的——它没有 this,所以这个参数不能省。

三、为什么需要友元:重载 <<

友元最典型的用途是重载运算符,尤其是 <<。

如果把它写成成员函数,<< 的左操作数就必须是本类对象;但输出语句 cout << obj 的左操作数 cout 是 ostream,根本不是我们的类——成员函数形式表达不了,只能写成全局函数。而全局函数若要读类的私有成员,就得声明为友元:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>
using namespace std;

class Complex {
private:
double r, i;
public:
Complex(double real, double imag) : r(real), i(imag) {}

// 声明为友元,函数体里才能读 r、i
friend ostream& operator<<(ostream& os, const Complex& c);
};

ostream& operator<<(ostream& os, const Complex& c) {
os << c.r << " + " << c.i << "i";
return os; // 返回 os,才能支持 cout << a << b 链式输出
}

int main() {
Complex c(1, 2);
cout << c << endl; // 1 + 2i
return 0;
}

operator<< 为什么必须返回 ostream&,见 C++ 运算符重载详解。

四、友元类

授予另一个类访问权限,写法是 friend class 类名;:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
using namespace std;

class A {
private:
int n;
public:
A() : n(0) {}
friend class B; // B 的所有成员函数都能访问 A 的私有成员
};

class B {
public:
void add(A& obj) { obj.n++; } // 合法:B 是 A 的友元
};

int main() {
A a;
B b;
b.add(a);
return 0;
}

注意方向:是 A 把 B 声明为友元,而不是 B 宣布自己能访问 A。这再次体现了主动权在被访问的类手里。

五、友元成员函数

如果只想开放另一个类的某一个成员函数(颗粒度更细),可以这样写:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <iostream>
using namespace std;

class A; // 前置声明:B 的成员函数参数要用到 A

class B {
public:
void show(const A& a); // 先声明,定义放到最后
};

class A {
private:
int n = 10;
friend void B::show(const A& a); // 只给 B::show 开权限
};

// 定义放最后:此时 A 已是完整类型,才能访问 a.n
void B::show(const A& a) {
cout << a.n << endl;
}

int main() {
A a;
B b;
b.show(a); // 10
return 0;
}

这里的书写顺序有讲究:friend void B::show(const A& a); 要求 B 已经完整定义,而 B::show 的函数体又要求 A 已经完整定义,所以只能「前置声明 A → 定义 B → 定义 A → 定义 B::show」。

六、友元的三条性质

  1. 单向:A 声明 B 是友元,只意味着 B 能访问 A;反过来不行,除非 A 也被 B 声明为友元。
  2. 不能被继承:基类的友元不会自动成为派生类的友元,也不会自动获得访问派生类成员的权限。
  3. 不具有传递性:B 是 A 的友元、C 是 B 的友元,不代表 C 能访问 A。

七、友元与封装

友元确实破坏了封装——它把 private 撕开一个口子。一旦外部代码依赖了这些私有成员,以后想改内部结构就被绑住了。所以:

  • 能不用就不用。能用公有成员函数(getter)解决的,就别开友元。
  • 少数确实合理的情况:
    • 重载 operator<< / operator>>(成员函数形式表达不了);
    • 两个类需要紧密协作(如容器与迭代器);
    • 逻辑上属于本类、但语法上不能作为成员的工厂函数。

八、小结

  • 友元函数不是成员函数,没有 this 指针,访问对象成员要靠参数传;
  • 语法:类内 friend 声明,定义写在类外;
  • 三种形式:友元函数 friend void f();、友元类 friend class B;、友元成员函数 friend void B::f();;
  • 友元是单向、不可继承、不可传递的;
  • 最典型的用途是重载 operator<<;友元破坏封装,能不用就不用。