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

    • 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++ 专业术语

引用与指针

定义与作用

指针和引用是 C++ 中间接访问对象的两种机制:

特性指针 T*左值引用 T&
本质存储地址的对象已有对象的别名
可否为空可为 nullptr不可为空(必须绑定有效对象)
可否重绑定可指向不同对象一旦绑定不可更改
可否有空值检查有 if (ptr)无条件安全
可否做算术运算有 ptr++ / ptr+1不可
语法*ptr 解引用直接使用

核心原则:优先使用引用——它更安全、语法更简洁;需要"可能为空、需要重定向或做地址运算"时才用指针。

核心原理

引用的底层实现

引用在底层通常实现为 T* const(常量指针),但这是编译器实现细节,标准不强制。

指针寻址层级

指向成员的指针

指向成员的指针是一种特殊的指针类型,它不指向具体对象,而是指向类的某个成员:

int MyClass::* p_member;          // 指向 MyClass 的 int 成员
void (MyClass::* p_method)(int);  // 指向 MyClass 的成员函数

需要通过对象或对象指针来调用:

obj.*p_member;      // 通过对象访问
ptr->*p_method(42); // 通过指针访问

完整示例

示例一:引用作为函数参数的效率优势

场景说明:对比传值、传指针、传引用三种参数传递方式的开销和行为差异。

#include <iostream>
#include <string>
#include <chrono>

struct LargeData {
    std::string content;
    LargeData() : content(10000, 'A') {}  // 大对象(约 10KB)
};

// 传值:发生完整拷贝
void process_by_value(LargeData data) {
    // data 是副本,修改不影响原对象
    data.content[0] = 'B';
}

// 传指针:拷贝 8 字节地址,但需要判空
void process_by_pointer(LargeData* data) {
    if (data == nullptr) {
        std::cout << "空指针,跳过处理" << std::endl;
        return;
    }
    data->content[0] = 'C';
}

// 传引用:拷贝 8 字节地址,无判空开销,语法简洁
void process_by_reference(LargeData& data) {
    data.content[0] = 'D';
}

// 只读引用:承诺不修改,可接收临时对象
void read_only(const LargeData& data) {
    std::cout << "数据大小: " << data.content.size() << " 字节" << std::endl;
    // data.content[0] = 'X';  // 编译错误:const 引用不可修改
}

int main() {
    LargeData obj;

    // 传递方式对比
    process_by_value(obj);          // 完整拷贝,约 10KB 开销
    process_by_pointer(&obj);       // 拷贝 8 字节,需取地址
    process_by_reference(obj);      // 拷贝 8 字节,直接用对象名

    // const 引用可接受临时对象
    read_only(obj);                 // 可接受左值
    read_only(LargeData());         // 可接受右值(临时对象)

    // 引用 vs 空安全
    LargeData* ptr = nullptr;
    process_by_pointer(ptr);        // 内部判空,安全
    // LargeData& ref = *ptr;      // 未定义行为!引用不能绑定到空

    std::cout << "obj.content[0] = " << obj.content[0]
              << " (原为 'A',被 reference 版本改为 'D')" << std::endl;

    return 0;
}

预期输出:

空指针,跳过处理
数据大小: 10000 字节
数据大小: 10000 字节
obj.content[0] = D (原为 'A',被 reference 版本改为 'D')

逐段分析:

  • 传值 process_by_value:对大对象产生完整拷贝,性能最差。函数内修改不影响原对象
  • 传指针 process_by_pointer:仅拷贝 8 字节地址,但调用处需取地址 &obj,且函数内必须判空
  • 传引用 process_by_reference:性能同指针,但调用语法自然(直接传对象名),且无需判空
  • const T& 的独特优势:可接受临时对象(右值),延长其生命周期至函数调用结束
  • 引用不能重新绑定是安全保证——int& ref = a; ref = b; 是把 b 的值赋给 a,而非让 ref 绑定到 b

示例二:二级指针与引用参数实现链表节点删除

场景说明:用二级指针和指针引用两种方式删除链表中的目标节点,对比可读性。

#include <iostream>

struct Node {
    int value;
    Node* next;
    Node(int v, Node* n = nullptr) : value(v), next(n) {}
};

// 方式 A:二级指针
void remove_v1(Node** head, int target) {
    Node** indirect = head;
    while (*indirect && (*indirect)->value != target) {
        indirect = &((*indirect)->next);
    }
    if (*indirect) {
        Node* to_delete = *indirect;
        *indirect = (*indirect)->next;
        delete to_delete;
    }
}

// 方式 B:指针引用(更清晰的写法)
void remove_v2(Node*& head, int target) {
    Node** indirect = &head;
    while (*indirect && (*indirect)->value != target) {
        indirect = &((*indirect)->next);
    }
    if (*indirect) {
        Node* to_delete = *indirect;
        *indirect = (*indirect)->next;
        delete to_delete;
    }
}

void print_list(const Node* head) {
    while (head) {
        std::cout << head->value << " -> ";
        head = head->next;
    }
    std::cout << "null" << std::endl;
}

int main() {
    Node* list = new Node(1, new Node(2, new Node(3)));

    std::cout << "原链表: ";
    print_list(list);

    remove_v1(&list, 2);
    std::cout << "删除 2 后 (v1): ";
    print_list(list);

    remove_v2(list, 1);
    std::cout << "删除 1 后 (v2): ";
    print_list(list);

    // 清理
    while (list) {
        Node* tmp = list;
        list = list->next;
        delete tmp;
    }

    return 0;
}

预期输出:

原链表: 1 -> 2 -> 3 -> null
删除 2 后 (v1): 1 -> 3 -> null
删除 1 后 (v2): 3 -> null

逐段分析:

  • remove_v1 接收 Node**,调用时需 &list。语义是"指向链表头指针的指针"
  • remove_v2 接收 Node*&,调用时直接传 list。语义是"链表头指针的引用",意图更清晰
  • 指针引用在需要修改外部指针值(如删除头节点)时,比二级指针更可读
  • print_list 接收 const Node*——只读遍历,不修改链表

示例三:指向成员的指针实现通用排序框架

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

struct Student {
    std::string name;
    int id;
    double gpa;

    void print() const {
        std::cout << id << " | " << name << " | GPA " << gpa << std::endl;
    }
};

// 通用排序:使用指向成员数据指针
void sort_by(std::vector<Student>& students,
             double Student::* member,
             bool ascending = true) {
    if (ascending) {
        std::sort(students.begin(), students.end(),
            [member](const Student& a, const Student& b) {
                return a.*member < b.*member;
            });
    } else {
        std::sort(students.begin(), students.end(),
            [member](const Student& a, const Student& b) {
                return a.*member > b.*member;
            });
    }
}

// 使用成员函数指针批量执行操作
void batch_operation(std::vector<Student>& students,
                     void (Student::* op)() const) {
    for (auto& s : students) {
        (s.*op)();
    }
}

int main() {
    std::vector<Student> students = {
        {"张三", 1003, 3.8},
        {"李四", 1001, 3.5},
        {"王五", 1002, 4.0}
    };

    std::cout << "---- 按 GPA 升序 ----" << std::endl;
    sort_by(students, &Student::gpa, true);
    batch_operation(students, &Student::print);

    std::cout << "\n---- 按 ID 降序 ----" << std::endl;
    sort_by(students, &Student::id, false);
    batch_operation(students, &Student::print);

    return 0;
}

预期输出:

---- 按 GPA 升序 ----
1001 | 李四 | GPA 3.5
1003 | 张三 | GPA 3.8
1002 | 王五 | GPA 4

---- 按 ID 降序 ----
1003 | 张三 | GPA 3.8
1002 | 王五 | GPA 4
1001 | 李四 | GPA 3.5

逐段分析:

  • double Student::* member 是指向 Student 类中 double 类型成员的指针
  • a.*member 通过对象访问成员指针指向的成员
  • &Student::gpa 获取成员的地址,不绑定到具体对象
  • void (Student::* op)() const 是指向 const 成员函数的指针
  • 成员指针的核心价值:将"访问哪个成员"参数化,实现通用框架

易错场景与面试考点

易错场景

场景错误表现正确做法
返回局部变量的引用悬挂引用,未定义行为返回值类型而非引用
解引用空指针作引用int& r = *nullptr; 是 UB引用必须绑定到有效对象
引用重新绑定误解ref = b; 是赋值而非重绑定引用不可重绑定,用指针替代
int& const语法不合法(引用本身不可变)不需要 const
T* 转为 T& 未判空间接引入 UB转换前判空

常见面试问题

  1. 引用和指针的区别(至少 4 点)?——引用必须初始化、不可为空、不可重绑定、语法更简洁;指针可为空、可重定向、支持算术运算、需要显式解引用。

  2. 为什么没有"引用的引用"?C++11 中的引用折叠是什么?——引用本身不是对象,不能有引用。但在模板中 T&& + 引用叠加会触发引用折叠规则(& + & = &、&& + & = &、& + && = &、&& + && = &&)。

  3. 何时用指针,何时用引用?——可能为空或需要重定向用指针;必须有效、不需要重新绑定时用引用。函数参数优先 const T&。

  4. int* const p 和 const int* p 的区别?——前者是指针本身不可修改(指向固定地址),后者是指针指向的内容不可修改。用从右向左读法区分。

  5. 什么是悬挂引用?如何避免?——引用绑定的对象已销毁。绝不返回局部对象的引用;注意容器重新分配导致的引用失效。

  6. 成员指针有什么实际应用场景?——通用排序框架、ORM 映射、回调机制、序列化/反序列化中按字段操作。

小结

  • 引用 = 别名,底层近似 T* const;不可为空、不可重绑定
  • 函数参数优先 const T&:零拷贝 + 安全只读 + 可接受临时对象
  • 指针在需要可选(nullptr)、可重定向、或地址运算时使用
  • 指针引用 T*& 是修改外部指针的清晰写法
  • 成员指针 T C::* 将"访问哪个成员"参数化
上一页
枚举类型
下一页
数组与 stdarray