代码仓库shanchuann/CPP-Learninng

基本概念

在C++面向对象编程中,封装是核心特性之一。类通过privateprotected访问控制符,将内部数据和实现细节隐藏起来,仅暴露public接口与外部交互,以此保证数据安全和代码的可维护性。但在某些场景下,这种严格的封装可能带来不便——比如两个紧密协作的类需要互相访问对方的内部状态,或者某个外部函数需要直接操作类的私有成员以实现特定功能(如运算符重载)。此时,友元机制便提供了一种灵活的解决方案,它允许特定的外部实体临时获得访问类私有成员的权限,成为类的“特殊访客”。

友元可以是函数,也可以是类,甚至可以是另一个类的某个成员函数。它们的共同特点是,一旦被类声明为友元,就能够绕过访问控制符的限制,直接访问类中的privateprotected成员。这种机制并非破坏封装的设计,而是在保证整体封装性的前提下,为特殊场景提供的灵活接口。

外部函数的特殊访问权

普通函数可以被声明为友元函数。要将一个函数声明为类的友元,只需在类的内部(通常是public区域,也可在privateprotected区域,位置不影响权限)使用friend关键字声明该函数即可。

例如,假设有一个表示点的Point类,包含私有成员xy,若需要一个外部函数printPoint来打印这两个坐标,就可以将printPoint声明为Point的友元:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class Point {
private:
int x;
int y;
public:
Point(int x_, int y_) : x(x_), y(y_) {}
// 声明printPoint为友元函数
friend void printPoint(const Point& p);
};

// 友元函数定义,可直接访问Point的私有成员x和y
void printPoint(const Point& p) {
// 此处直接访问p.x和p.y,无需通过public接口
std::cout << "Point(" << p.x << ", " << p.y << ")" << std::endl;
}

在这个例子中,printPoint并非Point类的成员函数,但因为被声明为友元,所以能直接访问Point的私有成员xy。需要注意的是,友元函数的声明仅指定了它的访问权限,函数的定义仍需在类外部进行,且定义时不需要再添加friend关键字。

运算符重载

运算符重载是友元函数的常见应用场景。例如,重载输出运算符<<时,由于运算符的左操作数是ostream对象(如std::cout),而非当前类的对象,因此无法将其定义为类的成员函数(成员函数的左操作数默认是this指针指向的对象)。这时,将重载函数声明为类的友元,就能让它访问类的私有成员,从而完成输出操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Student {
private:
std::string name;
int age;
public:
Student(std::string n, int a) : name(n), age(a) {}
// 声明<<运算符重载为友元
friend std::ostream& operator<<(std::ostream& os, const Student& s);
};

// 友元函数实现<<重载,访问Student的私有成员
std::ostream& operator<<(std::ostream& os, const Student& s) {
os << "Name: " << s.name << ", Age: " << s.age;
return os;
}

// 使用示例
int main() {
Student s("Alice", 20);
std::cout << s << std::endl; // 输出:Name: Alice, Age: 20
return 0;
}

友元类

除了函数,一个类也可以被声明为另一个类的友元类。当类A被声明为类B的友元时,类A的所有成员函数都拥有访问类B的privateprotected成员的权限。这种机制适用于两个类关系非常紧密,需要互相深入访问内部状态的场景。

例如,一个Teacher类需要管理多个Student对象的成绩细节,而成绩可能是Student的私有成员,此时可将Teacher声明为Student的友元类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Student {
private:
std::string name;
int score; // 私有成员:成绩
// 声明Teacher为友元类
friend class Teacher;
public:
Student(std::string n, int s) : name(n), score(s) {}
};

class Teacher {
public:
// Teacher的成员函数可直接访问Student的私有成员score
void adjustScore(Student& s, int newScore) {
s.score = newScore; // 合法,因为Teacher是Student的友元
}
void printStudentScore(const Student& s) {
std::cout << s.name << "'s score: " << s.score << std::endl;
}
};
1
2
John's score: 85
John's score: 95

友元类的声明同样使用friend关键字,在目标类内部声明“friend class 类名;”即可。需要注意的是,友元关系是单向的——如果TeacherStudent的友元,并不意味着StudentTeacher的友元,Student无法访问Teacher的私有成员。同时,友元关系没有传递性,若类A是类B的友元,类B是类C的友元,类A不会自动成为类C的友元。

友元成员函数

还可以将某个类的特定成员函数声明为友元,而非整个类,这种方式称为友元成员函数。相比友元类,它能更精确地控制权限,避免过度开放访问权限。

例如,只需让Teacher类的adjustScore函数访问Student的私有成员,而printStudentScore函数不需要,则可单独声明adjustScore为友元:

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
28
29
30
31
// 提前声明Student类,因为Teacher的成员函数参数需要用到Student
class Student;

class Teacher {
public:
// 声明成员函数adjustScore,参数为Student
void adjustScore(Student& s, int newScore);
void printStudentScore(const Student& s); // 该函数不声明为友元
};

class Student {
private:
std::string name;
int score;
// 仅声明Teacher的adjustScore为友元成员函数
friend void Teacher::adjustScore(Student& s, int newScore);
public:
Student(std::string n, int s) : name(n), score(s) {}
std::string getName() const { return name; } // 提供public接口供print使用
int getScore() const { return score; }
};

// 实现Teacher的adjustScore,可访问Student的私有成员
void Teacher::adjustScore(Student& s, int newScore) {
s.score = newScore; // 合法,因为是友元成员函数
}

// printStudentScore未被声明为友元,只能通过public接口访问Student成员
void Teacher::printStudentScore(const Student& s) {
std::cout << s.getName() << "'s score: " << s.getScore() << std::endl;
}

使用友元成员函数时,需要注意类的声明顺序:由于友元成员函数属于另一个类,必须先声明该类及其成员函数,再在目标类中声明友元关系,否则编译器会无法识别函数所属的类。

特性与注意事项

不具有继承性:友元关系不能被继承,即如果类A是类B的友元,类B的子类C不会自动获得类A的友元权限,类A也不能访问C中新增的私有成员(除非C单独声明A为友元)。

不具有对称性:A是B的友元,并不意味着B是A的友元。

不具有传递性:A是B的友元,B是C的友元,但A不是C的友元。

友元声明不会影响访问控制符的原有作用,它只是额外授予权限,类的其他成员仍受privateprotectedpublic的限制。

此外,友元的声明位置在类内的publicprivateprotected区域均可,位置不影响友元的权限,通常为了代码清晰,会放在public区域。

一个常规的成员函数声明描述了三件在逻辑上互不相同的事情:

  • 该函数能够访问类声明的私有部分
  • 该函数位于类的作用域之中
  • 该函数必须经由一个对象去激活(有一个this指针)

将一个函数声明为友元可以使他具有第一种性质

将一个函数声明为static可以使他具有第一种和第二种性质