乐途乐途
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
主页
  • 计算机基础

    • TCP/IP
    • Linux
    • HTTP
  • 数据库

    • SQL
    • MySQL 5.7
  • 编程语言

    • C
    • C++
    • Java SE
    • Python2
    • Python3
  • 数据格式

    • JSON
    • XML
  • 认证与安全

    • JWT
  • 工具

    • Markdown
  • Git

    • GitFlow
  • Quartz

    • Quartz
  • Java

    • Maven 入门
    • Maven 进阶
    • MyBatis
    • Spring
    • Spring MVC
  • Java

    • Spring Boot
    • Spring Cloud
    • Spring Cloud Alibaba
    • Spring Security
    • Spring AI
    • Spring Batch
    • Kafka
    • Java 设计模式
  • 缓存

    • Redis
  • 搜索引擎

    • Elasticsearch
  • 分布式协调

    • ZooKeeper
联系
阿里云
  • C++ 学习路径
  • 第1章 C++概述与开发环境

    • C++ 语言概述与编译模型
    • 第一个 C++ 程序与规范
    • 开发环境配置
    • 预处理指令详解
  • 第2章 基本语法与类型系统

    • 变量与基本类型
    • 类型转换
    • 枚举类型
    • 引用与指针
    • 数组与 stdarray
    • 字符串与原始字符串字面量
    • const 与 constexpr
    • nullptr 与空指针
    • 类型推导 auto 与 decltype
    • 基于范围的 for 循环
    • 值类别全面解析
    • char16_t 与 char32_t
    • static_assert 编译期断言
  • 第3章 函数与重载

    • 函数声明与定义
    • 函数重载
    • 默认参数与内联函数
    • Lambda 表达式
    • 函数对象与 stdfunction
    • 后置返回类型与 noexcept
  • 第4章 类与对象

    • 类的基本定义
    • 构造函数与析构函数
    • 拷贝控制
    • 移动构造函数与移动赋值
    • 列表初始化与类内初始化器
    • 静态成员与嵌套类
    • 友元
    • =default 与 =delete
  • 第5章 继承与多态

    • 继承基础
    • 虚函数与多态
    • 虚函数表与动态绑定原理
    • 虚析构函数
    • 抽象类与纯虚函数
    • 多重继承与虚继承
    • 继承构造函数
  • 第6章 运算符重载

    • 运算符重载基础
    • 算术与关系运算符重载
    • 赋值与移动运算符重载
    • 特殊运算符重载
  • 第7章 模板与泛型编程

    • 函数模板
    • 类模板
    • 模板特化与偏特化
    • 可变参数模板
    • 别名模板与模板模板参数
    • SFINAE 与类型萃取
    • 依赖名与 typename/template 关键字
  • 第8章 异常处理

    • 异常处理机制
    • noexcept 与异常安全
    • RAII 与异常安全实践
  • 第9章 内存管理与智能指针

    • 动态内存与内存分区
    • RAII 惯用法
    • unique_ptr
    • shared_ptr 与 weak_ptr
    • 内存管理最佳实践
  • 第10章 右值引用与移动语义

    • 右值引用与值类别深度解析
    • std::move 原理与使用
    • 完美转发与 std::forward
    • 移动语义性能对比与最佳实践
  • 第11章 STL容器

    • vector 深度剖析
    • deque 内部机制
    • list 与 forward_list
    • map 与 set 深度解析
    • unordered 容器与哈希原理
    • array 与 tuple
    • 容器适配器
    • 容器选择全景指南
  • 第12章 STL算法与迭代器

    • 迭代器体系全解
    • 非变异算法
    • 变异算法
    • 排序与二分算法
    • Lambda 与算法组合
    • std::random 随机数库
    • 自定义迭代器开发
  • 第13章 IO流与文件

    • 标准 IO 流
    • 格式化输出控制
    • 文件流操作
    • 字符串流
    • std::regex 正则表达式
  • 第14章 并发与多线程

    • thread 基础与线程管理
    • mutex 与 lock_guard
    • unique_lock 与 condition_variable
    • thread_local 线程局部存储
    • atomic 与内存序
    • future 与 async 异步编程
    • std::chrono 时间库
  • 第15章 现代C++新特性

    • 从 C++11 到 C++20 演进路线
    • C++14 关键新特性
    • C++17 关键新特性
    • C++20 核心特性速览
  • 第16章 面试考点与最佳实践

    • C++ 综合最佳实践清单
    • 高频面试题精讲
    • 多线程面试题与实战
    • 内存管理常见陷阱与排查
  • 附录

    • C++ 核心知识点
    • C++ 专业术语

友元

定义与作用

友元(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<> 和特化
友元类过多破坏封装最小化友元范围(优先友元成员函数)

常见面试问题

  1. 友元是什么?它有什么特性?——允许外部函数或类访问私有成员。特性:单向、不可传递、不可继承。声明位置不影响访问控制。

  2. 什么场景必须使用友元?——二元运算符重载(如 operator<<、operator==)需要对称隐式转换时;测试类访问被测试类的私有成员;工厂类构造复杂对象。

  3. 友元函数和成员函数的选择原则?——二元运算符的第一个参数不是本类时(如 ostream& << T),必须用友元或自由函数。对称操作优先友元。

  4. 友元声明是函数声明吗?——仅当该函数之前未被声明时是。如果函数已经声明过,友元声明只是授予访问权。最佳实践:类外再写一个独立的函数声明。

  5. 如何将模板函数声明为友元?——template<typename T> friend void func(const T&);。模板友元的语法较复杂,需注意全特化、偏特化的区别。

小结

  • 友元提供了精细的访问控制:按需授权函数/类/成员函数访问私有成员
  • 友元关系是单向、不可传递、不可继承的
  • operator<< 是友元的经典用法——第一个参数不是本类,必须是自由函数
  • 优先使用友元成员函数(而非友元类)——最小权限原则
  • 友元是封装的一部分,不是封装的破坏——它让访问控制更精确而非更粗糙
上一页
静态成员与嵌套类
下一页
=default 与 =delete