友元
定义与作用
友元(friend)允许外部函数或类访问本类的私有(private)和受保护(protected)成员。它打破了常规的封装边界,但提供了精细的访问控制。
class Student {
friend void print_detail(const Student&); // 友元函数
friend class Registrar; // 友元类
friend void Course::enroll(Student&); // 友元成员函数
};
友元不是类的成员,不受访问控制符的约束——它声明在类内部的任何位置效果相同。
核心原理
友元关系的特点
友元 vs 成员函数 vs 自由函数
| 特性 | 友元 | 成员函数 | 普通自由函数 |
|---|---|---|---|
| 访问 private | 是 | 是 | 否 |
通过 . 调用 | 否 | 是 | 否 |
| 与类耦合度 | 高 | 最高 | 低 |
| 符号依赖操作 | 适合(== 等二元运算) | 可能非对称 | 必须通过公共接口 |
完整示例
示例一:友元函数的经典用法——重载流输出运算符
场景说明:为自定义类实现 operator<<,使其能直接输出到 std::cout。
#include <iostream>
#include <string>
#include <vector>
class Transcript {
public:
Transcript(std::string name, std::string major)
: name_(std::move(name)), major_(std::move(major)) {}
void addCourse(const std::string& course, double grade) {
courses_.push_back(course);
grades_.push_back(grade);
}
// 友元声明:允许 operator<< 访问私有成员
friend std::ostream& operator<<(std::ostream& os, const Transcript& t);
private:
std::string name_;
std::string major_;
std::vector<std::string> courses_;
std::vector<double> grades_;
double getGPA() const {
if (grades_.empty()) return 0.0;
double sum = 0.0;
for (double g : grades_) sum += g;
return sum / grades_.size();
}
};
// 友元函数的定义
std::ostream& operator<<(std::ostream& os, const Transcript& t) {
os << "===== 成绩单 =====" << std::endl;
os << "姓名: " << t.name_ << std::endl;
os << "专业: " << t.major_ << std::endl;
os << "GPA: " << t.getGPA() << std::endl;
os << "课程列表:" << std::endl;
for (size_t i = 0; i < t.courses_.size(); ++i) {
os << " " << t.courses_[i] << ": " << t.grades_[i] << std::endl;
}
return os;
}
int main() {
Transcript t("张三", "计算机科学");
t.addCourse("C++ 程序设计", 4.0);
t.addCourse("数据结构", 3.7);
t.addCourse("算法分析", 3.8);
// 直接用 std::cout << 对象
std::cout << t;
return 0;
}
预期输出:
===== 成绩单 =====
姓名: 张三
专业: 计算机科学
GPA: 3.83333
课程列表:
C++ 程序设计: 4
数据结构: 3.7
算法分析: 3.8
逐段分析:
operator<<是二元运算符,第一个参数是ostream而非本类——因此不能是成员函数- 声明为友元后,
operator<<可以访问name_、major_、courses_、grades_等私有成员 - 这是友元最优雅的使用场景——保留了私有成员的直接访问权,又不必暴露 getter
示例二:友元类和友元成员函数
场景说明:注册系统需要深入访问学生数据,但只授予特定类的临时权限。
#include <iostream>
#include <string>
class Student; // 前向声明
class Advisor {
public:
void reviewProfile(const Student& s);
};
class Student {
public:
Student(std::string name, int id) : name_(std::move(name)), id_(id) {}
// 友元成员函数:允许 Advisor::reviewProfile 访问
friend void Advisor::reviewProfile(const Student& s);
// 友元类:允许整个 Registrar 访问
friend class Registrar;
private:
std::string name_;
int id_;
int confidential_score_ = 0; // 机密评分
};
// Advisor 的成员函数定义(必须在 Student 完整定义后)
void Advisor::reviewProfile(const Student& s) {
std::cout << "[导师] 查看学生 " << s.name_ << " (" << s.id_ << ")" << std::endl;
// 导师可以看 name_, id_,但不能看 confidential_score_
}
class Registrar {
public:
void enroll(Student& s) {
std::cout << "[注册处] 为 " << s.name_ << " 注册" << std::endl;
s.confidential_score_ = 85; // 注册处可以修改机密评分
}
void printRecord(const Student& s) {
std::cout << "[注册处] " << s.name_
<< " | ID: " << s.id_
<< " | 机密评分: " << s.confidential_score_ << std::endl;
}
};
int main() {
std::cout << "===== 友元权限分层 =====" << std::endl;
Student alice("张三", 2024001);
// 导师:有限访问
Advisor advisor;
advisor.reviewProfile(alice);
// 注册处:完全访问
Registrar reg;
reg.enroll(alice);
reg.printRecord(alice);
return 0;
}
预期输出:
===== 友元权限分层 =====
[导师] 查看学生 张三 (2024001)
[注册处] 为 张三 注册
[注册处] 张三 | ID: 2024001 | 机密评分: 85
逐段分析:
friend void Advisor::reviewProfile(...)只授权一个成员函数——比授权整个类更精细friend class Registrar授权整个类——Registrar的所有成员函数都能访问Student的私有成员- 导师只能访问
name_和id_;注册处还能访问confidential_score_——权限分层 - 前向声明
class Student;是友元成员函数声明的前提——编译器需要知道类型存在
示例三:对称运算符的友元重载
#include <iostream>
class Rational {
public:
Rational(int num = 0, int den = 1) : num_(num), den_(den) {}
// 友元:对称比较
friend bool operator==(const Rational& a, const Rational& b);
friend Rational operator+(const Rational& a, const Rational& b);
void print() const {
std::cout << num_ << "/" << den_ << std::endl;
}
private:
int num_, den_;
};
bool operator==(const Rational& a, const Rational& b) {
return a.num_ * b.den_ == b.num_ * a.den_;
}
Rational operator+(const Rational& a, const Rational& b) {
return Rational(a.num_ * b.den_ + b.num_ * a.den_, a.den_ * b.den_);
}
int main() {
Rational r1(1, 2), r2(1, 3);
std::cout << "r1 == r2? " << (r1 == r2) << std::endl;
std::cout << "r1 == Rational(2,4)? " << (r1 == Rational(2, 4)) << std::endl;
// 对称性:左操作数的隐式转换
std::cout << "1 == r1? " << (1 == r1) << std::endl;
// 加法
Rational r3 = r1 + r2;
r3.print();
return 0;
}
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
| 友元声明和函数声明混淆 | 友元声明不一定是函数声明 | 在类外再写一个独立的函数声明 |
| 友元成员函数的前向声明顺序 | 编译错误 | 先声明类 → 声明友元 → 定义友元函数 |
| 认为友元关系是双向的 | 编译错误 | 每方单独声明 |
| 模板友元声明 | 语法复杂 | 注意 template<> 和特化 |
| 友元类过多 | 破坏封装 | 最小化友元范围(优先友元成员函数) |
常见面试问题
友元是什么?它有什么特性?——允许外部函数或类访问私有成员。特性:单向、不可传递、不可继承。声明位置不影响访问控制。
什么场景必须使用友元?——二元运算符重载(如
operator<<、operator==)需要对称隐式转换时;测试类访问被测试类的私有成员;工厂类构造复杂对象。友元函数和成员函数的选择原则?——二元运算符的第一个参数不是本类时(如
ostream& << T),必须用友元或自由函数。对称操作优先友元。友元声明是函数声明吗?——仅当该函数之前未被声明时是。如果函数已经声明过,友元声明只是授予访问权。最佳实践:类外再写一个独立的函数声明。
如何将模板函数声明为友元?——
template<typename T> friend void func(const T&);。模板友元的语法较复杂,需注意全特化、偏特化的区别。
小结
- 友元提供了精细的访问控制:按需授权函数/类/成员函数访问私有成员
- 友元关系是单向、不可传递、不可继承的
operator<<是友元的经典用法——第一个参数不是本类,必须是自由函数- 优先使用友元成员函数(而非友元类)——最小权限原则
- 友元是封装的一部分,不是封装的破坏——它让访问控制更精确而非更粗糙