第一部分:C++语言核心与基础

1. C++程序基本框架与结构

  • 基本框架

    1
    2
    3
    4
    5
    6
    7
    #include <iostream> // 预处理指令

    int main(int argc, char* argv[]) { // 程序入口点
    // 你的代码
    std::cout << "Hello, World!" << std::endl;
    return 0; // 返回值给操作系统
    }
    • 原理深度剖析
      • #include:在编译的预处理阶段,预处理器会找到指定的头文件,并将其内容原封不动地复制#include指令所在的位置。这本质是一种文本替换。
      • main函数:是操作系统加载程序后执行的起点。argcargv由操作系统填充,argc是参数个数,argv是一个指向字符串指针的数组,存储着具体的参数。返回值return 0;被传递给操作系统,表示程序正常退出(非0值通常表示错误)。
  • 模块化与头文件保护

    • 模块化:将声明(.h/.hpp)与定义(.cpp)分离。.cpp文件是独立的编译单元。

    • 头文件保护

      1
      2
      3
      4
      #ifndef MY_HEADER_H
      #define MY_HEADER_H
      // ... 头文件内容 ...
      #endif // MY_HEADER_H
      • 底层原理:防止由于一个头文件被多个源文件包含而导致的重复定义错误。预处理器维护一个已定义宏的集合。当第一次遇到#ifndef MY_HEADER_H时,宏未定义,条件为真,于是定义宏并包含内容。后续再遇到同一个头文件时,因为宏已定义,条件为假,整个内容都会被跳过。
      • 现代替代品#pragma once。这是一个编译器指令(非标准,但被广泛支持),功能相同但更简洁。编译器内部会记录每个文件的唯一标识,遇到重复的#pragma once文件则直接跳过。其效率通常比#ifndef更高,因为它不需要解析整个文件内容。

2. C++现代词汇

  • nullptr (C++11)

    • 解决的问题:C++中NULL通常被定义为0。在函数重载时,func(int)func(char*),调用func(NULL)会歧义地调用func(int),而不是预期的指针版本。
    • 原理与底层nullptrstd::nullptr_t类型的字面量,它可以隐式转换为任何指针类型,但不能转换为整数类型。这从类型系统层面消除了歧义。底层上,它通常就是一个表示空地址的值(如0),但其类型是关键。
  • constexpr (C++11)

    • 核心思想:将计算从运行时转移到编译时

    • 底层原理

      • 声明为constexpr的变量或函数,编译器会在编译期间验证并(如果可能)计算其值。
      • 对于constexpr变量,其值必须由编译器已知的常量表达式初始化。它进入了C++的常量表达式世界。
      • 对于constexpr函数,当其参数是常量表达式时,它会在编译期被计算;否则,它就像一个普通函数一样在运行时被调用。
      • C++14/C++17/C++20 极大地放宽了constexpr函数内部的限制(允许循环、局部变量等)。
    • 示例与优势

      1
      2
      3
      4
      5
      6
      7
      constexpr int factorial(int n) { // 编译期阶乘
      return n <= 1 ? 1 : (n * factorial(n - 1));
      }
      int main() {
      int arr[factorial(5)]; // 数组大小在编译期确定,栈上分配
      // 等价于 int arr[120];
      }

      这避免了运行时计算,提升了性能,并允许在需要常量表达式的上下文中(如数组大小、模板参数)使用函数。

  • noexcept (C++11)

    • 作用
      1. 异常规范:声明函数不会抛出异常。
      2. 运算符:在编译期检查一个表达式是否声明为不抛异常。
    • 底层原理与优化
      • 从语言机制上,如果noexcept函数抛出了异常,程序会直接调用std::terminate终止,而不是正常的栈展开。
      • 关键优化std::vector在重新分配内存(realloc)时,需要将旧元素移动或复制到新内存。如果元素的移动构造函数是noexcept的,vector会优先使用更高效的移动操作;否则,为了强异常安全保证,它必须使用可能更慢的复制操作。这是因为移动操作可能会“掏空”源对象,如果中途抛出异常,源对象已损坏,无法恢复。因此,为移动操作标记noexcept是重要的性能优化手段。
  • 类型推导 (auto & decltype)

    • auto

      • 原理:编译器根据初始化器(等号右边的表达式)来推导变量的类型。它遵循模板参数推导的规则。

      • 底层:这纯粹是编译期的行为,不会产生任何运行时开销。生成的代码与您手动写出类型完全一样。

      • 注意auto会忽略引用和顶层const,如果需要,需手动加上&const

        1
        2
        3
        const int ci = 0;
        auto b = ci; // b 是 int (const被忽略)
        auto& c = ci; // c 是 const int&
    • decltype

      • 原理:查询一个实体或表达式的声明类型。它严格返回表达式的确切类型,包括引用和const限定。

      • 规则

        • decltype(entity):直接返回实体的声明类型。
        • decltype(expression):如果表达式是左值,返回T&;如果是右值,返回T
      • 应用:常用于模板库编写、函数返回类型推导(C++14后置返回类型)

        1
        2
        3
        int i = 0;
        decltype(i) a; // a 是 int
        decltype((i)) b = i; // b 是 int& (因为(i)是一个左值表达式)
  • 结构化绑定 (C++17)

    • 作用:从数组、结构体或元组等聚合类型中一次性解包多个变量。

    • 示例

      1
      2
      3
      4
      5
      6
      std::pair<int, std::string> getPair() { return {42, "hello"}; }
      auto [id, name] = getPair(); // id是int, name是std::string

      struct Point { double x, y; };
      Point p{1.0, 2.0};
      auto& [x, y] = p; // x和y是double&, 绑定到p的成员
    • 底层实现:编译器会生成一个匿名对象。对于auto [a, b] = expr;,其行为类似于:

      1
      2
      3
      auto __anonymous = expr; // 创建一个临时对象
      auto& a = __anonymous.<member0>; // 绑定到第一个成员
      auto& b = __anonymous.<member1>; // 绑定到第二个成员
      • 注意,ab是这个匿名对象的成员的别名,而不是直接绑定到expr的成员,除非你使用auto&

第二部分:数据类型、表达式和基本运算

1. C++类型系统

  • 基本类型
    • 底层内存表示
      • int, char, bool等的大小由实现定义,但C++标准规定了最小尺寸(如int至少16位)。sizeof运算符可以获取其字节数。
      • 浮点数float/double通常遵循IEEE 754标准,在内存中以符号位、指数位、尾数位的形式存储。
  • 指针
    • 本质:一个存储内存地址的整数变量。其类型决定了编译器如何解释该地址开始的内存数据(“步长”和“视野”)。
    • 底层:在32位系统上通常是4字节,64位系统上是8字节。
    • **void***:无类型指针,仅存储地址。必须强制转换后才能解引用。
  • std::string
    • 底层实现原理(常见)
      • 短字符串优化(SSO):这是现代std::string实现的关键。对象内部有一个固定大小的缓冲区(例如16字节)。如果字符串很短,直接存储在这个缓冲区里,避免了堆分配。如果字符串过长,则会在堆上分配内存,内部指针指向堆内存。
      • 内存布局:通常包含:大小(size)、容量(capacity)和一个指向数据的指针(或对于短字符串,直接利用这些字节存储数据本身)。SSO使得在栈上创建短字符串或在容器中存放大量短字符串时,性能极高。
  • std::byte (C++17)
    • 目的:表示内存的原始字节,而非字符。它使代码的意图更清晰。
    • 本质:一个枚举类,只有按位运算符被重载。不能进行算术运算(如+, -),强制程序员在操作前将其转换为适当的类型,提高了类型安全。

2. 常量与变量

  • 作用域规则
    • 全局/局部:由编译器通过符号表管理。局部变量通常在栈帧上,生命周期随栈帧的创建和销毁而开始和结束。全局/静态变量在程序的静态存储区,生命周期贯穿整个程序。
    • thread_local
      • 原理:每个线程都拥有该变量的一个独立实例
      • 底层实现:操作系统或运行时库提供了线程局部存储(TLS) 机制。编译器会为每个thread_local变量生成一个密钥,访问时通过一个内部函数(如pthread_getspecific或Windows的TlsGetValue)来获取当前线程对应的变量地址。这比访问普通全局变量慢,但实现了线程间的数据隔离。
  • 统一初始化 ({}语法)
    • 形式T obj{arg1, arg2, ...};
    • 优势
      1. 防止窄化转换int x{5.0}; 会编译错误,而int x(5.0);只会警告。
      2. 避免“最令人烦恼的解析”TimeKeeper tk(Timer()); 可能被解析为函数声明,而TimeKeeper tk{Timer{}};则明确是对象初始化。
      3. 对所有类型一致:可以初始化数组、容器、聚合类等。
    • 底层:编译器会尝试匹配构造函数,包括std::initializer_list构造函数。如果存在std::initializer_list参数的构造函数,且参数能转换,编译器会强烈优先匹配它。

3. 表达式与运算符

  • 表达式求值顺序

    • 重要规则:除了少数运算符(如&&, ||, ,, ?:),C++标准未规定函数参数和子表达式的求值顺序。

      1
      2
      f(++i, ++i); // 行为未定义!
      std::cout << i << i++; // 行为未定义!
    • 底层原因:给予编译器最大的优化自由,根据平台特性生成最高效的代码。

  • 短路求值 (&&||)

    • 原理:对于a && b,只有当a为真时才对b求值。对于a || b,只有当a为假时才对b求值。
    • 底层实现:编译器通过条件跳转指令实现。这不仅是性能优化,更是逻辑上的常用技巧(如if (p && p->isValid()))。
  • 位运算

    • 底层:直接对整数的二进制位进行操作。编译器会生成对应的CPU位操作指令(如AND(按位与), OR(按位或), XOR(按位异或), SHL(左移), SHR(右移))。
    • 应用:标志位处理、掩码、底层协议、优化(如用移位代替乘除2的幂)。
  • 条件运算符 (?:)

    • 底层:编译为条件跳转指令,类似于if-else
  • 指针运算

    • p + n:计算地址 (char*)p + n * sizeof(*p)。类型决定了步长。
    • p1 - p2:计算两个指针之间的元素个数,((char*)p1 - (char*)p2) / sizeof(*p1)

第三部分:C++的基本语句

1. 分支与循环结构

  • ifswitch 的底层实现

    • if-else:编译器生成一系列的比较和条件跳转指令。
    • switch语句
      • 跳转表:当case标签密集且值较小时,编译器会创建一个静态数组(跳转表),每个索引对应一个目标地址。switch的值直接作为索引进行跳转,时间复杂度O(1),非常高效。
      • 二分查找/if-else链:当case标签稀疏时,编译器可能生成二分查找逻辑或退化为if-else链,时间复杂度O(log n)O(n)
  • 范围for循环

    • 语法for (declaration : range) { ... }

    • 底层实现:编译器将其展开为基于迭代器的普通for循环。

      1
      2
      3
      4
      5
      6
      for (auto& x : vec) { ... }
      // 等价于:
      for (auto it = vec.begin(), end = vec.end(); it != end; ++it) {
      auto& x = *it;
      // ...
      }
    • 要求range必须能调用begin()end(),返回的迭代器支持!=, ++, *操作。

2. 转向语句

  • break:直接跳出当前一层循环或switch
  • continue:跳过本次循环剩余部分,直接进入下一轮循环的条件判断。
  • return:结束当前函数,并返回一个值(如果有)。
  • 底层实现:所有这些语句都编译为无条件跳转指令,跳转到程序代码的特定位置。

3. 异常处理

  • try/catch 底层机制(概念模型)

    • 这是一个复杂的、与编译器ABI紧密相关的特性。其核心是栈展开
    1. throw发生时,异常对象被创建在一个特殊区域。
    2. 运行时系统接管控制权,它从当前函数开始,沿调用链向上查找匹配的catch块。这个过程就是栈展开
    3. 在栈展开过程中,所有局部对象的析构函数会被自动调用。这是RAII和异常安全的基础。
    4. 找到匹配的catch后,栈展开停止,控制权转移到catch块。
    5. 如果未找到匹配的catch,调用std::terminate
  • 实现成本:为了支持栈展开,编译器需要生成额外的“栈映射”数据,记录每个函数调用点上哪些局部对象需要析构,以及catch块的位置。这增加了二进制文件的大小,并在正常路径(无异常抛出时)可能带来微小的性能开销。因此,异常通常只用于真正的、不可预期的错误情况。

  • 异常安全保证

    1. 基本保证:操作失败时,程序仍处于有效状态,无资源泄漏。所有不变量均保持。(例如,一个容器操作失败后,容器本身仍是可用的)。
    2. 强保证:操作要么完全成功,要么失败后程序状态回滚到操作调用前的状态。具有提交或回滚的语义。通常通过“拷贝-交换”惯用法实现。
    3. 无抛出保证:操作承诺永远不会抛出异常。所有内置类型和POD类型的操作都是无抛出的。如前所述,移动操作标记为noexcept是提供强异常安全的关键。

第四部分:数组、指针与引用

1. 数组的底层原理

  • 内存布局与地址计算
    • 一维数组:在内存中是连续的线性序列。arr[i]被编译器翻译为 *(arr + i),其中arr是数组首元素的地址( decays to pointer)。
    • 二维数组:本质是”数组的数组”。int arr[M][N]在内存中按行优先顺序排列。arr[i][j]的地址计算为:(char*)arr + i * (N * sizeof(int)) + j * sizeof(int)。编译器在编译时就知道N,所以能直接计算偏移量。
  • 数组与指针的微妙关系
    • 数组到指针的衰减:在大多数表达式中,数组名会退化为指向其首元素的指针。这是为什么int arr[10]; int* p = arr;是合法的。
    • 关键区别
      • sizeof(arr):返回整个数组的字节大小。
      • sizeof(p):返回指针变量的大小。
      • &arr:产生一个指向整个数组的指针,类型是int(*)[10]&arr + 1会跳过整个数组。
      • &p:产生一个指向指针变量的指针,类型是int**

2. 字符串:C风格 vs. C++风格

  • C风格字符串(字符数组)

    • 本质:以空字符'\0'结尾的字符数组。
    • 函数原理
      • strlen:从给定指针开始,线性扫描内存直到遇到'\0',计数。时间复杂度O(n)。
      • strcpy_s/strcat_s:安全版本,要求传入目标缓冲区大小,防止缓冲区溢出。内部在拷贝前后会检查边界。
      • strcmp:按字节比较ASCII值,直到遇到不同的字符或'\0'
      • strstr:实现子串查找,朴素算法复杂度O(n*m),但标准库实现可能使用KMP或Boyer-Moore等高效算法。
  • std::string(重温与深化)

    • SSO的典型实现:一个std::string对象内部可能包含:

      1
      2
      3
      4
      5
      6
      union {
      char* _ptr; // 指向堆内存(长字符串)
      char _local[16]; // 本地缓冲区(短字符串)
      };
      size_t _size; // 当前字符串长度
      size_t _capacity; // 已分配容量(对于长字符串)
      • 通过_size_local的某个字节来判断当前是短字符串模式还是长字符串模式。

3. 引用:带语法糖的安全指针

  • 本质:在底层,引用几乎总是通过指针来实现。编译器会为引用变量分配存储空间(存储所引用的地址)。
  • 与指针的关键区别(语言层面)
    1. 必须初始化:引用在诞生时必须绑定到一个对象,不存在空引用。
    2. 不可重新绑定:一旦初始化,不能再指向其他对象。
    3. 语法便利:使用引用不需要解引用操作符*,编译器自动处理。
  • 底层视角int& r = x; 生成的汇编代码与 int* const p = &x; 非常相似。r = 5; 会被编译为 *p = 5;

4. 数据结构的构建(链表、树、栈)

  • 指针单链表

    1
    2
    3
    4
    5
    struct ListNode {
    int val;
    ListNode* next; // 自引用结构,存储下一个节点的地址
    ListNode(int x) : val(x), next(nullptr) {} // 构造函数初始化
    };
    • 内存模型:节点在堆上非连续分配,通过指针”链”在一起。插入/删除操作只需修改指针,时间复杂度O(1)(已知前驱节点),但随机访问需要遍历,O(n)。
  • 二叉树

    1
    2
    3
    4
    5
    6
    struct TreeNode {
    int val;
    TreeNode* left;
    TreeNode* right;
    TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
    };
    • 递归结构:树的定义是递归的,因此遍历(前序、中序、后序)最自然的实现方式是递归。递归调用会使用调用栈,深度过大时可能导致栈溢出。
  • 栈的实现

    • 基于数组

      1
      2
      3
      4
      5
      6
      7
      8
      9
      template<typename T>
      class Stack {
      private:
      std::vector<T> elems; // 动态数组,自动管理内存
      public:
      void push(const T& e) { elems.push_back(e); }
      void pop() { elems.pop_back(); }
      T& top() { return elems.back(); }
      };
      • 原理:利用vector的尾部作为栈顶,push_backpop_back都是摊销常数时间复杂度。
    • 基于链表

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      template<typename T>
      class Stack {
      private:
      struct Node { T data; Node* next; };
      Node* top_ptr;
      public:
      void push(const T& e) {
      Node* new_node = new Node{e, top_ptr}; // 堆分配,在头部插入
      top_ptr = new_node;
      }
      // ... 需要手动管理Node的内存
      };
      • 原理:每个push都需要在堆上分配新节点,操作本身是O(1),但涉及系统调用,常数因子可能比数组版大。

第五部分:函数的有关使用

1. 函数调用机制与栈帧

  • 栈帧(活动记录)
    • 构成:每次函数调用,编译器都会在调用栈上压入一个栈帧,通常包含:
      1. 返回地址(调用结束后回到哪里)
      2. 调用者的栈帧指针
      3. 函数参数
      4. 局部变量
      5. 保存的寄存器
    • 原理:通过栈指针(SP)帧指针(FP) 寄存器来管理。FP指向当前栈帧的基址,通过固定的偏移量访问参数和局部变量。
  • std::function 与类型擦除
    • 问题:如何存储任意可调用对象(函数指针、Lambda、函数对象)?
    • 原理std::function使用类型擦除技术。
      1. 包装:它内部有一个模板构造函数,接受任意可调用对象。
      2. 多态:它创建一个派生自某个基类(如__base_func)的模板类(如__derived_func<F>),该派生类重写了operator()clone等虚函数。
      3. 存储std::function对象内部存储一个指向基类的指针(通常是unique_ptr)。调用时,通过这个指针进行动态分发(虚函数调用)。
    • 开销:相比直接调用,有一次虚函数调用可能的一次堆分配(对于小对象,优化实现可能使用小对象优化)。

2. 参数传递的深度解析

  • 传值 (void func(T t))

    • 底层:在栈上创建实参的完整副本。调用拷贝构造函数(对于类类型)。成本高。
  • 传常量引用 (void func(const T& t))

    • 底层:传递一个指针(引用的实现)到原对象。无拷贝。由于是const,编译器保证函数内不会修改原对象。
  • 传引用 (void func(T& t))

    • 底层:同样传递指针,但允许修改原对象。用于输出参数。
  • 移动语义 + 传值 (void func(T t)) 的现代用法

    • 场景:当函数内部无论如何都需要一个副本时(例如,存储到成员变量中)。

    • 优势

      • 如果传入左值,会调用一次拷贝构造,与以前一样。
      • 如果传入右值,会调用移动构造,将资源”偷”过来,成本极低。
    • 示例

      1
      2
      3
      4
      5
      void setData(std::string data) { m_data = std::move(data); } // 注意最后的move!
      // 调用:
      setData("Hello"); // 传入字符串字面量(右值)-> 移动构造
      std::string s = "World";
      setData(s); // 传入左值 -> 拷贝构造
      • 这提供了一个”拷贝或移动”的统一接口,有时比提供setData(const string&)setData(string&&)两个重载更简洁。

3. 函数高级特性

  • 函数重载

    • 原理:C++使用名称修饰 来在编译器层面区分不同重载。编译器将函数名、参数类型、命名空间等信息编码成一个唯一的链接器符号。例如,void func(int)void func(double)会被修饰成像_Z4funci_Z4funcd这样的符号。
  • 内联函数

    • 原理:编译器将函数体直接展开到调用处,消除函数调用的开销(压栈、跳转、返回)。
    • 代价与权衡:以代码膨胀为代价换取性能。适用于短小且频繁调用的函数。
    • inline关键字:在现代C++中,它主要是一个链接指令,表示允许在多个编译单元中定义相同的函数(通常放在头文件中),链接器会选取一个。
  • 操作符重载

    • 本质:就是特殊的成员函数或非成员函数,函数名是operator@

    • 实现为成员函数a @ b 等价于 a.operator@(b)。左操作数是this

    • 实现为非成员函数a @ b 等价于 operator@(a, b)。通常需要声明为friend以访问私有成员。

    • 示例:<<重载

      1
      2
      3
      4
      // 非成员函数,用于输出自定义类型
      std::ostream& operator<<(std::ostream& os, const MyClass& obj) {
      return os << obj.data_; // 需要friend或public访问
      }

4. Lambda表达式

  • 编译器魔法:生成匿名类

    • 原理:对于每个Lambda,编译器会在当前作用域生成一个唯一的、匿名的类类型(闭包类型)。
    • 捕获列表的映射
      • [=]:所有捕获的变量成为该匿名类的值类型成员。编译器在构造函数中按值拷贝这些变量。
      • [&]:所有捕获的变量成为该匿名类的引用类型成员。编译器存储这些变量的引用
      • [var] / [&var]:指定特定变量的捕获方式。
    • 函数调用运算符:Lambda的函数体成为这个匿名类的operator()
  • mutable的作用

    • 默认行为:默认情况下,Lambda的operator()const的,这意味着你不能修改按值捕获的变量(它们被视为const成员)。
    • mutable:加上后,operator()变为非const,允许你修改按值捕获的变量(修改的是Lambda对象内部的副本,不影响外部变量)。
  • 泛型Lambda (C++14)

    1
    auto lambda = [](auto x, auto y) { return x + y; };
    • 原理:编译器为它生成一个模板化的operator()

      1
      2
      3
      4
      5
      class AnonymousLambda {
      public:
      template<typename T1, typename T2>
      auto operator()(T1 x, T2 y) const { return x + y; }
      };

第六部分:类与对象

1. 封装与访问控制

  • public/private/protected
    • 编译期检查:访问控制是编译期概念。编译器在解析代码时,根据上下文检查对成员的访问是否合法。违反规则会直接报错。运行时没有任何开销

2. 特殊成员函数与=default/=delete

  • 规则 of Three/Five/Zero

    • Rule of Three:如果你需要自定义析构函数、拷贝构造函数或拷贝赋值运算符中的任何一个,你很可能需要全部三个。
    • Rule of Five (C++11):加上移动构造函数和移动赋值运算符。
    • Rule of Zero:理想情况是,让编译器生成的默认行为处理资源管理,你将资源管理交给像vectorstringunique_ptr这样的成员对象。
  • =default:显式要求编译器生成默认实现。即使你定义了其他构造函数,也可以用=default来获取编译器生成的默认构造函数。

  • =delete:禁止编译器生成某些函数,或禁止某些不希望的转换。

    1
    2
    3
    4
    5
    6
    class NonCopyable {
    public:
    NonCopyable() = default;
    NonCopyable(const NonCopyable&) = delete; // 禁止拷贝
    NonCopyable& operator=(const NonCopyable&) = delete;
    };
  • explicit

    • 作用:阻止构造函数的隐式转换

    • 示例

      1
      2
      3
      4
      5
      6
      7
      class String {
      public:
      explicit String(int size); // 禁止从int隐式构造String
      };
      void f(const String& s);
      f(10); // 错误!不能隐式将int转换为String
      f(String(10)); // 正确,显式构造

3. 构造函数与析构函数家族

  • 拷贝 vs. 移动

    • 拷贝语义T(const T&)T& operator=(const T&)不改变源对象,创建资源的完整副本(深拷贝)。
    • 移动语义T(T&&)T& operator=(T&&)。将源对象的资源”偷”过来,并将源对象置于有效但未定义的状态(通常是空)。必须标记为noexcept,以便被标准库高效使用。
  • 委托构造函数

    1
    2
    3
    4
    5
    6
    class Foo {
    int a, b, c;
    public:
    Foo(int x) : a(x), b(0), c(0) {}
    Foo(int x, int y) : Foo(x) { b = y; } // 委托给第一个构造函数
    };
    • 原理:在一个构造函数的初始化列表中调用另一个构造函数,避免代码重复。
  • 虚析构函数
    • 核心原理:当delete一个指向派生类对象的基类指针时,如果基类析构函数不是virtual,则行为是未定义的(通常只调用基类析构函数,导致派生部分资源泄漏)。
    • 多态析构:通过虚函数表,delete base_ptr;会正确调用派生类的析构函数,然后自动调用基类的析构函数。

4. 成员函数限定符

  • const成员函数

    • 底层:在成员函数声明的末尾加const,实质是给this指针加上了const限定。即T* const this 变成了 const T* const this。这保证了该函数不会修改对象的任何非mutable成员。
  • 引用限定符 (C++11)

    • 问题:对于operator=,我们希望区分左值对象和右值对象。

    • 解决方案

      1
      2
      3
      4
      5
      class String {
      public:
      String& operator=(const String&) &; // 只能被左值对象调用
      String operator=(const String&) &&; // 只能被右值对象调用
      };
    • 应用:防止对右值进行无意义的赋值,如getTempString() = otherString;

5. 友元与静态成员

  • 友元

    • 本质:打破了封装,是编译期的白名单机制。它授予非成员函数或其他类访问本类privateprotected成员的权力。
    • 注意:友元关系不可传递,也不继承
  • 静态成员

    • 内存与生命周期:存储在程序的静态存储区,不依赖于任何对象实例。生命周期从程序开始到结束。

    • 线程安全实现

      1
      2
      3
      4
      5
      6
      7
      8
      9
      class Singleton {
      public:
      static Singleton& getInstance() {
      static Singleton instance; // C++11保证这是线程安全的
      return instance;
      }
      private:
      Singleton() = default;
      };
      • C++11标准规定,局部静态变量的初始化是线程安全的。编译器会插入底层同步代码(如原子操作或锁)来保证只初始化一次。

6. 设计原则与UML

  • 关键设计原则
    • 单一职责原则:一个类应该只有一个引起它变化的原因。
    • 开放-封闭原则:对扩展开放,对修改封闭。
    • 依赖倒置原则:依赖于抽象(接口),而非具体实现。
  • UML关联
    • 组合A包含BB的生命周期由A管理(A析构,B也析构)。用实心菱形表示。代码中通常表现为:class A { B b; };
    • 聚合A使用B,但B的生命周期独立于A。用空心菱形表示。代码中通常表现为:class A { B* b; };

第七部分:类的继承与派生、多态性知识

1. 继承的内存布局与访问控制

  • 内存布局模型

    • 单继承:派生类对象包含一个完整的基类子对象,后跟派生类自己的数据成员。

      1
      2
      class Base { int b_data; };
      class Derived : public Base { int d_data; };
      • 内存布局[Base::b_data | Derived::d_data]
    • 原理:基类子对象在派生类对象中拥有固定的偏移量(通常为0)。这保证了将Derived*隐式转换为Base*时,不需要调整指针值——它们指向同一个地址。
  • 访问控制的实际效果

    • public继承:is-a关系,基类的接口在派生类中保持原样。
    • protected/private继承:is-implemented-in-terms-of关系,本质是组合的语法变体。
    • 底层实质:访问控制是在编译期由编译器检查的。无论何种继承方式,基类子对象在内存中都存在,只是通过派生类对象访问基类成员的权限不同。

2. 虚函数机制与vtable/vptr

  • vtable(虚函数表)

    • 本质:每个包含虚函数的类(或从包含虚函数的类派生)都有一个编译期生成的静态函数指针数组,存储在只读数据段。

    • 内容

      • 指向该类各个虚函数实现的指针
      • 可选的RTTI信息(用于typeiddynamic_cast
    • 继承链中的vtable构建

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      class Base {
      public:
      virtual void f1() { ... }
      virtual void f2() { ... }
      };
      class Derived : public Base {
      public:
      void f1() override { ... } // 重写
      virtual void f3() { ... } // 新虚函数
      };
      • Base的vtable[&Base::f1, &Base::f2]
      • Derived的vtable[&Derived::f1, &Base::f2, &Derived::f3]
  • vptr(虚函数表指针)

    • 本质:每个对象实例中编译器自动添加的隐藏指针,指向该对象所属类的vtable。
    • 生命周期:在构造函数中初始化,在析构函数中调整。
    • 内存位置:通常位于对象起始处(保证与C的兼容性和最高访问效率)。
  • 虚函数调用的完整过程

    1
    2
    Base* ptr = new Derived;
    ptr->f1(); // 多态调用
    1. 通过ptr找到对象的vptr
    2. 通过vptr找到vtable
    3. 在vtable的固定偏移处(如索引0)找到函数地址
    4. 通过该地址调用函数,并传入this指针
  • 性能开销
    • 空间开销:每个对象一个vptr(通常4/8字节),每个类一个vtable
    • 时间开销:一次指针解引用(vptr) + 一次数组索引(vtable) + 可能的一次缓存未命中

3. 纯虚函数与抽象基类

  • 纯虚函数声明virtual void func() = 0;
  • 抽象基类效果:不能实例化,只能作为接口定义。
  • vtable中的表示:纯虚函数在vtable中通常填充为一个特殊的纯虚调用句柄,如果被调用会终止程序(或抛出异常)。

4. 重写控制(override/final)

  • override:编译期检查,确保函数确实重写了基类的虚函数。防止因签名不匹配导致的意外隐藏。
  • final
    • 用于类:禁止进一步派生
    • 用于虚函数:禁止在派生类中重写
    • 优化机会:标记为final的虚函数调用,在某些情况下编译器可以进行去虚拟化优化,直接进行静态调用。

5. 多重继承与虚继承

  • 普通多重继承的内存布局

    1
    2
    3
    class Base1 { int b1_data; };
    class Base2 { int b2_data; };
    class Derived : public Base1, public Base2 { int d_data; };
    • 内存布局[Base1::b1_data | Base2::b2_data | Derived::d_data]
      • Base1*Derived*地址相同
      • Base2*需要偏移:(char*)derived + sizeof(Base1)
  • 菱形问题与虚继承

    1
    2
    3
    4
    class A { int a_data; };
    class B : virtual public A { int b_data; };
    class C : virtual public A { int c_data; };
    class D : public B, public C { int d_data; };
  • 虚继承的实现机制
    • 虚基类表(vbtable):类似vtable,存储虚基类相对于当前子对象的偏移量。
    • 虚基类指针(vbptr):在包含虚基类的类对象中,指向vbtable。
    • 内存布局:虚基类子对象通常放在对象末尾,通过vbptr间接访问。
    • 访问成本:通过虚基类指针多一次间接访问,比普通成员访问慢。

6. RTTI成本分析与替代方案

  • dynamic_cast实现原理

    1. 检查源类型和目标类型是否在同一个继承层次
    2. 遍历继承树,检查转换是否合法
    3. 需要时调整指针偏移量
  • 性能成本

    • 涉及类型信息查询和继承树遍历
    • 在复杂继承层次中可能很昂贵
    • 依赖vtable,因此只能用于多态类(有虚函数的类)
  • 替代方案

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 方案1:使用枚举+static_cast(编译期)
    enum class Type { TYPE_BASE, TYPE_DERIVED };
    class Base {
    protected:
    virtual Type getType() const { return Type::TYPE_BASE; }
    };

    // 方案2:使用自定义类型标识
    class Derived : public Base {
    protected:
    Type getType() const override { return Type::TYPE_DERIVED; }
    public:
    static bool classof(const Base* b) {
    return b->getType() == Type::TYPE_DERIVED;
    }
    };

第八部分:模板与元编程

1. 模板实例化机制

  • 两阶段编译
    1. 定义期:检查模板本身的语法
    2. 实例化期:用具体类型替换模板参数,生成具体代码,检查类型相关操作
  • 代码膨胀问题:每个不同的模板参数组合都会生成一份独立的代码。可通过提取公共部分到非模板基类来缓解。

2. 变参模板

  • 参数包展开模式

    1
    2
    3
    4
    5
    6
    template<typename... Args>
    void print(Args... args) {
    // 递归展开(C++11/14风格)
    // 或使用折叠表达式(C++17)
    (std::cout << ... << args);
    }
  • 完美转发参数包

    1
    2
    3
    4
    template<typename... Args>
    void forwarder(Args&&... args) {
    target(std::forward<Args>(args)...); // 保持值类别
    }

3. SFINAE与std::enable_if

  • SFINAE原理:在模板重载决议中,如果某个模板的立即上下文中的替换失败,编译器会 silently 丢弃该特化,而不是报错。

  • std::enable_if应用

    1
    2
    3
    4
    5
    6
    template<typename T>
    typename std::enable_if<std::is_integral<T>::value, T>::type
    process(T value) {
    // 只对整数类型有效
    return value * 2;
    }
  • 现代替代品:C++20的Concepts提供了更清晰的语法替代SFINAE。

4. 编译期分支if constexpr

  • 与运行时if的关键区别if constexpr编译期确定分支,未选择的分支不会实例化

    1
    2
    3
    4
    5
    6
    7
    8
    template<typename T>
    auto get_value(T t) {
    if constexpr (std::is_pointer_v<T>) {
    return *t; // 对指针类型:解引用
    } else {
    return t; // 对非指针类型:直接返回
    }
    }
    • 如果用int实例化,return *t;这行代码根本不存在
    • 避免了编译错误(对非指针类型解引用)

5. 类型萃取

  • std::is_same:编译期类型比较

    1
    2
    static_assert(std::is_same_v<int, int>);    // 通过
    static_assert(std::is_same_v<int, float>); // 失败
  • std::decay:模拟按值传递的类型转换

    • 移除引用和cv限定符

    • 数组退化为指针

    • 函数退化为函数指针

      1
      2
      3
      std::decay_t<const int&>    // → int
      std::decay_t<int[5]> // → int*
      std::decay_t<int(int)> // → int(*)(int)
  • 类型萃取的实现原理

    1
    2
    3
    4
    5
    6
    7
    8
    9
    template<typename T>
    struct is_pointer {
    static constexpr bool value = false;
    };

    template<typename T>
    struct is_pointer<T*> {
    static constexpr bool value = true; // 偏特化匹配指针
    };

第九部分:输入输出流

1. 流缓冲区与格式化

  • 流的三层架构
    1. 格式化层ostream/istream,处理数据类型转换和格式化
    2. 缓冲层streambuf,管理字符序列的缓冲
    3. 设备层:文件、内存、字符串等具体设备
  • 缓冲区刷新策略
    • unitbuf:每个操作后刷新
    • nounitbuf:缓冲区满时刷新
    • 手动刷新:flush, endl(刷新+换行)

2. 文件I/O高级特性

  • 二进制I/O与内存对齐

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    struct Data {
    int id;
    double value;
    char name[32];
    };

    Data data;
    // 写入:直接内存拷贝
    file.write(reinterpret_cast<const char*>(&data), sizeof(data));
    // 读取
    file.read(reinterpret_cast<char*>(&data), sizeof(data));
    • 注意事项
      • 平台字节序问题(大端/小端)
      • 结构体填充字节导致的大小不一致
      • 版本兼容性问题
  • 内存映射文件原理

    • 将文件直接映射到进程的虚拟地址空间
    • 通过页错误机制实现按需加载
    • 优点:避免用户态-内核态数据拷贝,适合大文件随机访问
    • 实现:Unix的mmap,Windows的CreateFileMapping
  • 异步I/O与std::async

    1
    2
    3
    4
    5
    6
    auto future = std::async(std::launch::async, [file_path]() {
    return read_large_file(file_path); // 在独立线程中执行
    });

    // 主线程继续其他工作
    auto result = future.get(); // 等待结果
  • 文件锁的实现级别

    • 劝告锁std::filebuf::lock()(C++17),进程间协作
    • 强制锁:操作系统级别强制实施
    • 范围锁:锁定文件的特定区域

3. 时间库深度使用

  • 时钟类型区别

    • system_clock:系统实时时钟,可调整,可转换为日历时间
    • steady_clock:单调时钟,保证始终递增,适合性能测量
    • high_resolution_clock:最高精度的时钟(通常是steady_clock的别名)
  • 高精度计时实现

    1
    2
    3
    4
    5
    6
    7
    8
    9
    auto start = std::chrono::high_resolution_clock::now();

    // 被测代码
    std::this_thread::sleep_for(std::chrono::milliseconds(100));

    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::nanoseconds>(end - start);

    std::cout << "耗时: " << duration.count() << " 纳秒" << std::endl;
  • 时间点算术

    1
    2
    3
    4
    using namespace std::chrono;
    auto now = system_clock::now();
    auto one_hour_later = now + hours(1); // 时间点 + 时长 = 时间点
    auto diff = one_hour_later - now; // 时间点 - 时间点 = 时长

第十部分:标准模板库和高级特性应用

1. 多线程应用编程

  • 线程创建与生命周期管理

    1
    2
    std::thread t1([](){ /* 任务 */ });
    std::thread t2(func, arg1, arg2);
    • 底层原理std::thread 封装了操作系统原生线程(pthread, Windows Thread)
    • 资源管理:线程对象析构前必须调用join()(等待结束)或detach()(分离管理)
    • 线程局部存储thread_local变量每个线程有独立副本
  • 同步原语深度解析

    • 互斥量 (std::mutex)

      1
      2
      3
      4
      5
      std::mutex mtx;
      {
      std::lock_guard<std::mutex> lock(mtx); // RAII管理
      // 临界区
      }
      • 底层实现:通常使用原子操作+系统调用(futex)实现
      • 性能代价:锁竞争导致线程阻塞、上下文切换
    • 条件变量 (std::condition_variable)

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      std::condition_variable cv;
      std::mutex mtx;
      bool ready = false;

      // 等待方
      std::unique_lock<std::mutex> lock(mtx);
      cv.wait(lock, []{ return ready; });

      // 通知方
      {
      std::lock_guard<std::mutex> lock(mtx);
      ready = true;
      }
      cv.notify_one();
      • 避免虚假唤醒:使用谓词参数的wait版本
      • 底层机制:线程进入等待队列,释放锁,被唤醒后重新获取锁
    • 原子操作 (std::atomic)

      1
      2
      std::atomic<int> counter{0};
      counter.fetch_add(1, std::memory_order_relaxed);
      • 无锁编程:使用CPU原子指令,避免锁开销
      • 内存顺序
        • memory_order_relaxed:只保证原子性
        • memory_order_acquire/release:保证同步
        • memory_order_seq_cst:最强一致性(默认)

2. 容器深度分析

  • 序列容器内存布局

    • vector:单块连续内存,随机访问O(1),尾部操作摊销O(1)
      • 扩容策略:通常2倍增长,size() + 1可能触发重新分配
      • 迭代器失效:插入/删除可能使所有迭代器失效
    • list:双向链表,插入删除O(1),随机访问O(n)
      • 内存开销:每个节点包含两个指针开销
    • deque:分块连续内存,头尾操作O(1)
      • 实现机制:中央map + 多个固定大小块
  • 关联容器实现原理

    • 红黑树特性

      • 自平衡二叉搜索树
      • 从根到叶子的最长路径不超过最短路径的2倍
      • 插入、删除、查找都是O(log n)
    • map vs unordered_map

      1
      2
      std::map<int, string> tree_map;        // 红黑树,有序
      std::unordered_map<int, string> hash_map; // 哈希表,无序但更快
  • 无序容器(哈希表)实现

    • 哈希冲突解决
      • 链地址法:每个桶是链表(STL实现)
      • 开放地址法:线性探测、二次探测
    • 性能参数
      • 负载因子 = 元素数量 / 桶数量
      • 重新哈希:负载因子超过阈值时重建哈希表

3. 迭代器类别与概念

  • 迭代器层次体系

    1
    2
    3
    输入迭代器 → 前向迭代器 → 双向迭代器 → 随机访问迭代器

    输出迭代器
    • 随机访问迭代器:支持+, -, []vector, deque, array
    • 双向迭代器:支持++, --list, set, map
    • 前向迭代器:仅支持++forward_list, unordered_containers

4. C++17现代组件

  • std::optional:类型安全的可空值

    1
    2
    3
    4
    5
    6
    7
    8
    std::optional<int> find(int key) {
    if (exists(key)) return compute(key);
    return std::nullopt; // 无值
    }

    if (auto result = find(42)) {
    use(*result); // 安全解引用
    }
  • std::variant:类型安全的联合体

    1
    2
    3
    4
    5
    std::variant<int, double, string> v = 3.14;
    std::visit([](auto&& arg) {
    using T = std::decay_t<decltype(arg)>;
    if constexpr (std::is_same_v<T, int>) { /* ... */ }
    }, v);
  • std::string_view:字符串的非拥有视图

    1
    2
    3
    void process(std::string_view sv) { // 接受string, char*, 字面量
    auto substr = sv.substr(0, 5); // O(1)操作
    }
    • 性能优势:避免不必要的字符串拷贝
    • 生命周期风险:不管理内存,必须保证原字符串存活
  • std::filesystem:现代文件系统操作

    1
    2
    3
    4
    5
    6
    namespace fs = std::filesystem;
    for (auto& entry : fs::directory_iterator(".")) {
    if (entry.is_regular_file()) {
    std::cout << entry.file_size() << " " << entry.path() << "\n";
    }
    }

5. 并行STL算法

1
2
3
4
5
6
7
8
9
10
#include <execution>
std::vector<int> data = {3, 1, 4, 1, 5, 9, 2, 6};

// 并行排序
std::sort(std::execution::par, data.begin(), data.end());

// 并行变换
std::transform(std::execution::par_unseq,
data.begin(), data.end(), data.begin(),
[](int x) { return x * 2; });
  • 执行策略
    • seq:顺序执行
    • par:并行执行
    • par_unseq:并行+向量化

第十一部分:内存管理

1. 动态内存分配机制

  • new/delete vs malloc/free

    1
    2
    3
    4
    5
    6
    7
    // C++方式:调用构造函数/析构函数
    MyClass* obj = new MyClass(args);
    delete obj;

    // C方式:仅分配原始内存
    void* mem = malloc(sizeof(MyClass));
    free(mem);
    • operator new重载:可自定义内存分配行为

    • placement new:在已分配内存上构造对象

      1
      2
      3
      4
      5
      cpp
      void* buffer = malloc(sizeof(MyClass));
      MyClass* obj = new (buffer) MyClass(args); // placement new
      obj->~MyClass(); // 必须显式调用析构函数
      free(buffer);

2. 智能指针实现原理

  • unique_ptr独占所有权,零开销

    1
    2
    std::unique_ptr<MyClass> ptr(new MyClass);
    // 编译为裸指针操作,无额外开销
  • shared_ptr控制块机制

    1
    2
    std::shared_ptr<MyClass> sp1(new MyClass);
    auto sp2 = sp1; // 共享所有权
    • 内存布局

      1
      2
      3
      [sp1] → [控制块] → [对象数据]
      [sp2] ↗
      控制块包含:引用计数、弱引用计数、删除器、分配器
    • make_shared优化:对象和控制块单次分配

  • weak_ptr解决循环引用

    1
    2
    3
    4
    5
    6
    class B; class A {
    std::shared_ptr<B> b_ptr;
    };
    class B {
    std::weak_ptr<A> a_ptr; // 使用weak_ptr打破循环
    };

3. 内存对齐控制

  • 硬件对齐要求:现代CPU要求数据在自然边界对齐

  • 对齐控制

    1
    2
    3
    4
    5
    6
    struct alignas(16) AlignedData { // 16字节对齐
    int data[4];
    };

    static_assert(alignof(AlignedData) == 16);
    static_assert(sizeof(AlignedData) == 16); // 无填充

4. 移动语义与完美转发

  • 右值引用T&& 绑定到临时对象

  • 引用折叠规则

    • T& &T&
    • T& &&T&
    • T&& &T&
    • T&& &&T&&
  • 完美转发实现

    1
    2
    3
    4
    template<typename T>
    void wrapper(T&& arg) {
    target(std::forward<T>(arg)); // 保持值类别
    }

5. 内存问题检测工具

  • Valgrind Memcheck

    1
    valgrind --leak-check=full ./my_program
    • 检测能力:内存泄漏、使用未初始化内存、越界访问
  • AddressSanitizer (ASan)

    1
    g++ -fsanitize=address -g program.cpp
    • 原理:编译时插桩,影子内存跟踪每个字节状态
    • 优势:比Valgrind快,适合开发期持续检测

第十二部分:工具、测试与系统接口

1. C++与C语言互操作

  • extern "C"链接规范

    1
    2
    3
    4
    5
    extern "C" {
    #include "c_library.h"

    int c_function(int); // C风格名称修饰
    }
    • 名称修饰差异:C++支持重载,名称包含类型信息;C名称简单
  • ABI兼容性:确保数据结构布局、调用约定一致

2. GDB高级调试技巧

  • 核心转储分析

    1
    2
    3
    4
    gdb ./my_program core.dump
    (gdb) bt full # 完整调用栈
    (gdb) info registers # 寄存器状态
    (gdb) x/10x $sp # 检查栈内存
  • 条件断点与观察点

    1
    2
    (gdb) break file.cpp:100 if condition
    (gdb) watch variable # 数据断点

3. 单元测试实践

  • Google Test框架

    1
    2
    3
    4
    5
    6
    7
    8
    TEST(MyTestSuite, SpecificTest) {
    EXPECT_EQ(actual, expected);
    ASSERT_TRUE(condition); // 失败则终止当前测试
    }

    TEST_F(TestFixture, UsingFixture) {
    // 使用SetUp()初始化的环境
    }
  • 测试替身:Mock对象模拟依赖组件

4. UML建模与设计模式

  • UML类图关系

    • 继承:空心三角形箭头
    • 组合:实心菱形,整体控制部分生命周期
    • 聚合:空心菱形,整体不控制部分生命周期
    • 依赖:虚线箭头
  • 常用设计模式实现

    • RAII模式:资源获取即初始化

      1
      2
      3
      4
      5
      6
      7
      class FileHandle {
      FILE* file;
      public:
      FileHandle(const char* name) : file(fopen(name, "r")) {}
      ~FileHandle() { if (file) fclose(file); }
      // 禁用拷贝,允许移动
      };
    • 工厂模式:创建对象而不指定具体类

      1
      2
      3
      4
      5
      6
      std::unique_ptr<Shape> create_shape(ShapeType type) {
      switch (type) {
      case CIRCLE: return std::make_unique<Circle>();
      case SQUARE: return std::make_unique<Square>();
      }
      }
    • 观察者模式:对象间依赖通知

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      class Observer {
      public:
      virtual void update(const Event&) = 0;
      };

      class Subject {
      std::vector<Observer*> observers;
      public:
      void notify(const Event& e) {
      for (auto obs : observers) obs->update(e);
      }
      };