C++

语法树

graph LR
    A["C++ 语法概览"]

    %% 1. 基础语法特性 (C++98/03 及之前)
    A --> B["基础语法特性<br>C++98/03"]
    B --> B1["基本数据类型"]
    B1 --> B1a["整型: int, short, long"]
    B1 --> B1b["浮点型: float, double"]
    B1 --> B1c["字符型: char, wchar_t"]
    B1 --> B1d["布尔型: bool"]
    B1 --> B1e["修饰符: signed, unsigned"]
    
    B --> B2["变量与常量"]
    B2 --> B2a["变量声明与定义"]
    B2 --> B2b["常量: const"]
    
    B --> B3["运算符"]
    B3 --> B3a["算术运算符: +, -, *, /, %"]
    B3 --> B3b["关系运算符: ==, !=, &lt;, &gt;"]
    B3 --> B3c["逻辑运算符: &amp;&amp;, ||, !"]
    B3 --> B3d["位运算符: &amp;, |, ^, ~, &lt;&lt;, &gt;&gt;"]
    B3 --> B3e["赋值运算符: =, +=, -="]
    B3 --> B3f["其他: sizeof, typeid"]
         
    B --> B4["控制结构"]
    B4 --> B4a["条件语句: if, else"]
    B4 --> B4b["开关语句: switch, case"]
    B4 --> B4c["循环: for, while, do-while"]
    B4 --> B4d["跳转: break, continue, return, goto"]
    
    B --> B5["函数"]
    B5 --> B5a["函数声明与定义"]
    B5 --> B5b["参数传递: 值, 引用"]
    B5 --> B5c["默认参数"]
    B5 --> B5d["函数重载"]
    B5 --> B5e["内联函数: inline"]
    
    B --> B6["指针与引用"]
    B6 --> B6a["指针: *"]
    B6 --> B6b["引用: &amp;"]
    B6 --> B6c["空指针: NULL"]
    
    B --> B7["类与对象"]
    B7 --> B7a["类定义: class, struct"]
    B7 --> B7b["访问控制: public, private"]
    B7 --> B7c["构造函数与析构函数"]
    B7 --> B7d["拷贝构造函数"]
    B7 --> B7e["静态成员: static"]
    B7 --> B7f["友元: friend"]
    B7 --> B7g["继承"]
    B7 --> B7h["多态性: virtual, =0"]
    
    B --> B8["模板"]
    B8 --> B8a["函数模板"]
    B8 --> B8b["类模板"]
    
    B --> B9["异常处理"]
    B9 --> B9a["try, catch, throw"]
    B9 --> B9b["标准异常类"]
    
    B --> B10["命名空间"]
    B10 --> B10a["定义与使用"]
    
    B --> B11["动态内存管理"]
    B11 --> B11a["new, delete"]
    
    B --> B12["预处理器"]
    B12 --> B12a["宏定义: #define"]
    B12 --> B12b["条件编译: #ifdef"]
    B12 --> B12c["文件包含: #include"]

    %% 2. 现代 C++ 新特性
    A --> C["现代 C++ 新特性"]
    
    C --> C1["C++11"]
    C1 --> C1a["自动类型推导: auto, decltype"]
    C1 --> C1b["范围 for 循环"]
    C1 --> C1c["nullptr"]
    C1 --> C1d["智能指针: unique_ptr, shared_ptr"]
    C1 --> C1e["移动语义: &amp;&amp;, std::move"]
    C1 --> C1f["完美转发: std::forward"]
    C1 --> C1g["Lambda 表达式"]
    C1 --> C1h["模板改进: 可变参数模板"]
    C1 --> C1i["初始化改进: {}"]
    C1 --> C1j["并发支持: std::thread"]
    C1 --> C1k["新容器: array, unordered_map"]
    C1 --> C1l["constexpr, static_assert"]
    
    C --> C2["C++14"]
    C2 --> C2a["泛型 Lambda"]
    C2 --> C2b["返回类型推导: auto"]
    C2 --> C2c["constexpr 扩展"]
    C2 --> C2d["变量模板"]
    
    C --> C3["C++17"]
    C3 --> C3a["结构化绑定"]
    C3 --> C3b["if/switch 初始化"]
    C3 --> C3c["折叠表达式"]
    C3 --> C3d["std::optional, variant, any"]
    C3 --> C3e["文件系统库: std::filesystem"]
    C3 --> C3f["并行算法"]
    
    C --> C4["C++20"]
    C4 --> C4a["概念: Concepts"]
    C4 --> C4b["Ranges 库"]
    C4 --> C4c["协程: co_await, co_yield"]
    C4 --> C4d["模块: import"]
    C4 --> C4e["三路比较: &lt;=&gt;"]
    C4 --> C4f["consteval, constinit"]
    C4 --> C4g["std::span, bit_cast"]

    %% 3. 其他特性
    A --> D["其他特性"]
    D --> D1["标准库扩展<br>C++98+"]
    D1 --> D1a["STL: vector, map"]
    D1 --> D1b["输入输出: iostream"]
    D1 --> D1c["字符串: string, string_view"]
    D1 --> D1d["正则表达式: regex"]
    
    D --> D2["编译器特性<br>C++11+"]
    D2 --> D2a["属性: [[nodiscard]]"]
    D2 --> D2b["对齐控制: alignas"]

详解

1. 基础语法特性(C++98/03及之前)

基本数据类型

C++ 的基本数据类型是语言的核心,用于定义变量以存储不同种类的数据。这些类型的具体大小和范围依赖于编译器和硬件平台,但 C++ 标准提供了一些基本保证。以下是对每种数据类型的详细讲解。

整型

整型用于存储整数值,根据大小和符号性分为以下几种:

  • int
    • 功能:表示基本的整数类型,通常是平台上最自然的大小(一般 4 字节,32 位)。
    • 范围:有符号时,范围通常为 -2,147,483,6482,147,483,647-2^312^31-1)。
    • 使用场景:适用于大多数整数计算,如循环计数器、数组索引等。
    • 底层原理:存储为二进制补码形式,符号位决定正负。
    • 注意事项:溢出时行为未定义(例如 INT_MAX + 1)。
    • 示例代码
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      #include <iostream>
      #include <limits>
      int main() {
      int i = 42; // 普通整数
      std::cout << "int value: " << i << "\n";
      std::cout << "int min: " << std::numeric_limits<int>::min() << "\n"; // -2147483648
      std::cout << "int max: " << std::numeric_limits<int>::max() << "\n"; // 2147483647
      i = INT_MAX; // 使用 limits 中的宏定义
      std::cout << "Max int: " << i << "\n";
      // i = i + 1; // 未定义行为,溢出
      return 0;
      }
  • short
    • 功能:短整型,比 int 小,通常 2 字节(16 位)。
    • 范围:有符号时,范围为 -32,76832,767-2^152^15-1)。
    • 使用场景:适合存储较小的整数,节省内存,如小型计数器。
    • 底层原理:同样使用二进制补码表示。
    • 注意事项:范围较小,需注意溢出。
    • 示例代码
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      #include <iostream>
      #include <limits>
      int main() {
      short s = 32767; // 短整型最大值
      std::cout << "short value: " << s << "\n";
      std::cout << "short min: " << std::numeric_limits<short>::min() << "\n"; // -32768
      std::cout << "short max: " << std::numeric_limits<short>::max() << "\n"; // 32767
      // s = 32768; // 溢出,未定义行为
      return 0;
      }
  • long
    • 功能:长整型,至少与 int 大小相同,通常 4 字节或 8 字节(视平台)。
    • 范围:32 位系统上有符号时为 -2^312^31-1,64 位系统上可能更大。
    • 使用场景:需要更大范围的整数,如文件大小。
    • 底层原理:补码表示,长度由编译器定义。
    • 注意事项:建议使用 long long 以确保 64 位支持。
    • 示例代码
      1
      2
      3
      4
      5
      6
      7
      8
      9
      #include <iostream>
      #include <limits>
      int main() {
      long l = 123456L; // 长整型,带 L 后缀
      std::cout << "long value: " << l << "\n";
      std::cout << "long min: " << std::numeric_limits<long>::min() << "\n";
      std::cout << "long max: " << std::numeric_limits<long>::max() << "\n";
      return 0;
      }
  • long long
    • 功能:超长整型,至少 8 字节(64 位),C++11 正式标准化。
    • 范围:有符号时为 -2^632^63-1(约 ±9.2×10^18)。
    • 使用场景:非常大的整数,如科学计算或时间戳。
    • 底层原理:补码表示,保证 64 位。
    • 注意事项:较老的编译器可能不支持。
    • 示例代码
      1
      2
      3
      4
      5
      6
      7
      8
      9
      #include <iostream>
      #include <limits>
      int main() {
      long long ll = 123456789LL; // 超长整型,带 LL 后缀
      std::cout << "long long value: " << ll << "\n";
      std::cout << "long long min: " << std::numeric_limits<long long>::min() << "\n";
      std::cout << "long long max: " << std::numeric_limits<long long>::max() << "\n";
      return 0;
      }
  • unsigned 修饰符
    • 功能:将整型变为无符号,范围从 0 开始,最大值翻倍。
    • 范围:如 unsigned int 为 0 到 4,294,967,295(2^32-1)。
    • 使用场景:非负数场景,如计数器、数组大小。
    • 底层原理:直接存储二进制值,无符号位。
    • 注意事项:与有符号类型混合运算需小心(如比较时可能出错)。
    • 示例代码
      1
      2
      3
      4
      5
      6
      7
      8
      9
      #include <iostream>
      #include <limits>
      int main() {
      unsigned int ui = 4294967295U; // 无符号整数,带 U 后缀
      std::cout << "unsigned int value: " << ui << "\n";
      std::cout << "unsigned int min: " << std::numeric_limits<unsigned int>::min() << "\n"; // 0
      std::cout << "unsigned int max: " << std::numeric_limits<unsigned int>::max() << "\n"; // 4294967295
      return 0;
      }

浮点型

浮点型用于存储小数,基于 IEEE 754 标准。

  • float
    • 功能:单精度浮点数,4 字节,约 7 位有效数字。
    • 范围:大约 ±3.4×10^38,精度有限。
    • 使用场景:需要小数但精度要求不高的场景,如图形计算。
    • 底层原理:分为符号位、指数和尾数,遵循 IEEE 754。
    • 注意事项:浮点运算可能有精度误差(如 0.1 + 0.2 != 0.3)。
    • 示例代码
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      #include <iostream>
      #include <limits>
      int main() {
      float f = 3.14f; // 单精度,带 f 后缀
      std::cout << "float value: " << f << "\n";
      std::cout << "float min: " << std::numeric_limits<float>::min() << "\n"; // 最小正值
      std::cout << "float max: " << std::numeric_limits<float>::max() << "\n";
      std::cout << "0.1 + 0.2: " << (0.1f + 0.2f) << "\n"; // 0.30000001(精度误差)
      return 0;
      }
  • double
    • 功能:双精度浮点数,8 字节,约 15 位有效数字。
    • 范围:大约 ±1.8×10^308,精度更高。
    • 使用场景:需要较高精度的浮点计算,如科学计算。
    • 底层原理:IEEE 754,双倍尾数位。
    • 注意事项:仍可能有精度误差,但比 float 小。
    • 示例代码
      1
      2
      3
      4
      5
      6
      7
      8
      9
      #include <iostream>
      #include <limits>
      int main() {
      double d = 3.1415926535; // 双精度
      std::cout << "double value: " << d << "\n";
      std::cout << "double min: " << std::numeric_limits<double>::min() << "\n";
      std::cout << "double max: " << std::numeric_limits<double>::max() << "\n";
      return 0;
      }
  • long double
    • 功能:扩展精度浮点数,大小和精度视平台而定(常 10 或 16 字节)。
    • 范围:通常比 double 大,精度更高。
    • 使用场景:需要极高精度的计算,如数学库。
    • 底层原理:依赖编译器实现,可能非 IEEE 754。
    • 注意事项:移植性差,不同平台行为不同。
    • 示例代码
      1
      2
      3
      4
      5
      6
      7
      8
      9
      #include <iostream>
      #include <limits>
      int main() {
      long double ld = 3.141592653589793238L; // 扩展精度,带 L 后缀
      std::cout << "long double value: " << ld << "\n";
      std::cout << "long double min: " << std::numeric_limits<long double>::min() << "\n";
      std::cout << "long double max: " << std::numeric_limits<long double>::max() << "\n";
      return 0;
      }

字符型

字符型用于存储单个字符或宽字符。

  • char
    • 功能:存储单字节字符,1 字节(8 位)。
    • 范围:有符号时 -128127,无符号时 0255
    • 使用场景:表示 ASCII 字符,如字母、数字。
    • 底层原理:直接存储字符的编码值(如 ASCII)。
    • 注意事项:默认是否带符号由编译器决定。
    • 示例代码
      1
      2
      3
      4
      5
      6
      7
      #include <iostream>
      int main() {
      char c = 'A'; // ASCII 值为 65
      std::cout << "char: " << c << "\n";
      std::cout << "ASCII value: " << (int)c << "\n"; // 65
      return 0;
      }
  • wchar_t
    • 功能:宽字符,用于存储多字节字符(如 Unicode),大小视平台(常 2 或 4 字节)。
    • 范围:依赖实现,通常支持更大的字符集。
    • 使用场景:国际化程序中支持非 ASCII 字符。
    • 底层原理:存储 Unicode 或其他宽字符编码。
    • 注意事项:需要宽字符流(如 wcout)输出。
    • 示例代码
      1
      2
      3
      4
      5
      6
      #include <iostream>
      int main() {
      wchar_t wc = L'中'; // 宽字符,带 L 前缀
      std::wcout << "wchar_t: " << wc << "\n";
      return 0;
      }

布尔型

  • bool
    • 功能:表示逻辑值 true(1)或 false(0),通常 1 字节。
    • 使用场景:条件判断、标志位。
    • 底层原理:存储为整数,0 表示假,非 0 表示真。
    • 注意事项:可以隐式转换为整数。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      #include <iostream>
      int main() {
      bool b1 = true;
      bool b2 = false;
      std::cout << "true: " << b1 << ", false: " << b2 << "\n"; // 1 0
      int i = b1; // 隐式转换
      std::cout << "Converted to int: " << i << "\n"; // 1
      return 0;
      }

变量与常量

变量是程序中可修改的数据存储单元,常量则是不可修改的固定值。

  • 变量
    • 功能:通过类型声明创建变量,可随时修改其值。
    • 使用场景:存储临时数据、计算中间结果。
    • 底层原理:分配内存空间,变量名映射到地址。
    • 注意事项:未初始化变量的值未定义。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      #include <iostream>
      int main() {
      int x; // 声明,未初始化
      x = 10; // 赋值
      std::cout << "x: " << x << "\n"; // 10
      x = 20; // 修改
      std::cout << "Modified x: " << x << "\n"; // 20
      return 0;
      }
  • 常量
    • 功能:使用 const 修饰,值在初始化后不可修改。
    • 使用场景:定义不变的值,如数学常数 PI。
    • 底层原理:编译器确保常量不可写,可能优化为内联值。
    • 注意事项:必须初始化,否则编译错误。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      #include <iostream>
      int main() {
      const int y = 30; // 常量,必须初始化
      std::cout << "y: " << y << "\n"; // 30
      // y = 40; // 错误:常量不可修改
      return 0;
      }

运算符

C++ 提供了丰富的运算符,用于执行算术、逻辑、位操作等。以下逐一详细讲解。

算术运算符

用于基本的数学运算。

  • +(加法)
    • 功能:将两个操作数相加。
    • 使用场景:数值计算。
    • 注意事项:整数溢出未定义,浮点数可能有精度误差。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      #include <iostream>
      int main() {
      int a = 5, b = 3;
      int sum = a + b;
      std::cout << "5 + 3 = " << sum << "\n"; // 8
      return 0;
      }
  • -(减法)
    • 功能:从第一个操作数减去第二个。
    • 使用场景:计算差值。
    • 注意事项:同加法,注意溢出。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      #include <iostream>
      int main() {
      int a = 5, b = 3;
      int diff = a - b;
      std::cout << "5 - 3 = " << diff << "\n"; // 2
      return 0;
      }
  • *(乘法)
    • 功能:两个操作数相乘。
    • 使用场景:面积、体积计算等。
    • 注意事项:溢出风险更大。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      #include <iostream>
      int main() {
      int a = 5, b = 3;
      int prod = a * b;
      std::cout << "5 * 3 = " << prod << "\n"; // 15
      return 0;
      }
  • /(除法)
    • 功能:第一个操作数除以第二个。
    • 使用场景:平均值计算。
    • 注意事项:整数除法结果截断,除以 0 未定义。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      #include <iostream>
      int main() {
      int a = 10, b = 3;
      int div = a / b;
      std::cout << "10 / 3 = " << div << "\n"; // 3(截断)
      double d = static_cast<double>(a) / b;
      std::cout << "10.0 / 3 = " << d << "\n"; // 3.33333
      return 0;
      }
  • %(取模)
    • 功能:返回除法后的余数,仅适用于整数。
    • 使用场景:判断奇偶、循环计数。
    • 注意事项:除以 0 未定义。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      #include <iostream>
      int main() {
      int a = 10, b = 3;
      int mod = a % b;
      std::cout << "10 % 3 = " << mod << "\n"; // 1
      return 0;
      }

关系运算符

用于比较两个值,返回布尔结果。

  • ==(等于)
    • 功能:检查两个操作数是否相等。
    • 使用场景:条件判断。
    • 注意事项:浮点数比较需注意精度。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      #include <iostream>
      int main() {
      int a = 5, b = 5;
      bool eq = (a == b);
      std::cout << "5 == 5: " << eq << "\n"; // 1
      return 0;
      }
  • !=(不等于)
    • 功能:检查两个操作数是否不相等。
    • 使用场景:排除特定值。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      #include <iostream>
      int main() {
      int a = 5, b = 3;
      bool neq = (a != b);
      std::cout << "5 != 3: " << neq << "\n"; // 1
      return 0;
      }
  • <(小于)
    • 功能:检查第一个操作数是否小于第二个。
    • 使用场景:排序、循环条件。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      #include <iostream>
      int main() {
      int a = 3, b = 5;
      bool lt = (a < b);
      std::cout << "3 < 5: " << lt << "\n"; // 1
      return 0;
      }
  • >(大于)
    • 功能:检查第一个操作数是否大于第二个。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      #include <iostream>
      int main() {
      int a = 5, b = 3;
      bool gt = (a > b);
      std::cout << "5 > 3: " << gt << "\n"; // 1
      return 0;
      }
  • <=(小于等于)
    • 功能:检查第一个操作数是否小于或等于第二个。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      #include <iostream>
      int main() {
      int a = 5, b = 5;
      bool le = (a <= b);
      std::cout << "5 <= 5: " << le << "\n"; // 1
      return 0;
      }
  • >=(大于等于)
    • 功能:检查第一个操作数是否大于或等于第二个。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      #include <iostream>
      int main() {
      int a = 5, b = 3;
      bool ge = (a >= b);
      std::cout << "5 >= 3: " << ge << "\n"; // 1
      return 0;
      }

逻辑运算符

用于组合布尔表达式。

  • &&(与)
    • 功能:两个操作数都为真时返回真。
    • 使用场景:多条件判断。
    • 注意事项:短路求值(左边为假不计算右边)。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      #include <iostream>
      int main() {
      bool a = true, b = false;
      bool result = (a && b);
      std::cout << "true && false: " << result << "\n"; // 0
      return 0;
      }
  • ||(或)
    • 功能:任一操作数为真时返回真。
    • 注意事项:短路求值(左边为真不计算右边)。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      #include <iostream>
      int main() {
      bool a = true, b = false;
      bool result = (a || b);
      std::cout << "true || false: " << result << "\n"; // 1
      return 0;
      }
  • !(非)
    • 功能:反转布尔值。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      #include <iostream>
      int main() {
      bool a = true;
      bool result = !a;
      std::cout << "!true: " << result << "\n"; // 0
      return 0;
      }

位运算符

按位操作,直接操作二进制位。

  • &(按位与)
    • 功能:逐位进行与运算。
    • 使用场景:提取特定位、掩码操作。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      #include <iostream>
      int main() {
      int a = 5; // 0101
      int b = 3; // 0011
      int result = a & b; // 0001
      std::cout << "5 & 3 = " << result << "\n"; // 1
      return 0;
      }
  • |(按位或)
    • 功能:逐位进行或运算。
    • 使用场景:设置特定位。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      #include <iostream>
      int main() {
      int a = 5; // 0101
      int b = 3; // 0011
      int result = a | b; // 0111
      std::cout << "5 | 3 = " << result << "\n"; // 7
      return 0;
      }
  • ^(按位异或)
    • 功能:逐位异或,相同为 0,不同为 1。
    • 使用场景:交换值、检测差异。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      #include <iostream>
      int main() {
      int a = 5; // 0101
      int b = 3; // 0011
      int result = a ^ b; // 0110
      std::cout << "5 ^ 3 = " << result << "\n"; // 6
      return 0;
      }
  • ~(按位取反)
    • 功能:将所有位取反。
    • 注意事项:结果为补码形式。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      #include <iostream>
      int main() {
      int a = 5; // 0101
      int result = ~a; // 1010(补码表示为 -6)
      std::cout << "~5 = " << result << "\n"; // -6
      return 0;
      }
  • <<(左移)
    • 功能:将位向左移动,低位补 0。
    • 使用场景:快速乘以 2 的幂。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      #include <iostream>
      int main() {
      int a = 5; // 0101
      int result = a << 1; // 1010
      std::cout << "5 << 1 = " << result << "\n"; // 10
      return 0;
      }
  • >>(右移)
    • 功能:将位向右移动,符号位决定高位补 0 或 1。
    • 使用场景:快速除以 2 的幂。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      #include <iostream>
      int main() {
      int a = 5; // 0101
      int result = a >> 1; // 0010
      std::cout << "5 >> 1 = " << result << "\n"; // 2
      return 0;
      }

赋值运算符

用于修改变量值。

  • =(赋值)
    • 功能:将右值赋给左值。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      #include <iostream>
      int main() {
      int a = 10;
      std::cout << "a = " << a << "\n"; // 10
      return 0;
      }
  • +=(加赋值)
    • 功能:加后赋值。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      #include <iostream>
      int main() {
      int a = 10;
      a += 5; // a = a + 5
      std::cout << "a += 5: " << a << "\n"; // 15
      return 0;
      }
  • -=(减赋值)
    • 功能:减后赋值。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      #include <iostream>
      int main() {
      int a = 10;
      a -= 3; // a = a - 3
      std::cout << "a -= 3: " << a << "\n"; // 7
      return 0;
      }
  • 其他赋值运算符*=/=%= 等类似)
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      #include <iostream>
      int main() {
      int a = 10;
      a *= 2; // a = a * 2
      std::cout << "a *= 2: " << a << "\n"; // 20
      return 0;
      }

其他运算符

一些特殊运算符。

  • sizeof
    • 功能:返回类型或变量的字节大小。
    • 使用场景:内存分配、调试。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      #include <iostream>
      int main() {
      int x = 10;
      std::cout << "sizeof(int): " << sizeof(int) << "\n"; // 通常 4
      std::cout << "sizeof(x): " << sizeof(x) << "\n"; // 4
      return 0;
      }
  • typeid
    • 功能:返回类型信息,需包含 <typeinfo>
    • 使用场景:运行时类型检查。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      #include <iostream>
      #include <typeinfo>
      int main() {
      int x = 10;
      std::cout << "Type of x: " << typeid(x).name() << "\n"; // "i" (int)
      return 0;
      }
  • dynamic_cast
    • 功能:运行时类型转换,用于多态类型。
    • 使用场景:安全的向下转型。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      #include <iostream>
      class Base { virtual void f() {} };
      class Derived : public Base {};
      int main() {
      Base* b = new Derived();
      Derived* d = dynamic_cast<Derived*>(b);
      if (d) std::cout << "Cast successful\n";
      delete b;
      return 0;
      }

控制结构

控制结构用于管理程序的执行流程。

条件语句

  • ifelse ifelse
    • 功能:根据条件执行不同代码块。
    • 使用场景:分支逻辑。
    • 底层原理:条件表达式求值为真(非 0)时执行。
    • 注意事项:避免悬垂 else 问题。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      #include <iostream>
      int main() {
      int x = 10;
      if (x > 0) {
      std::cout << "Positive\n";
      } else if (x == 0) {
      std::cout << "Zero\n";
      } else {
      std::cout << "Negative\n";
      }
      return 0;
      }

开关语句

  • switchcasedefault
    • 功能:根据整数值跳转到对应分支。
    • 使用场景:多条件选择。
    • 底层原理:编译为跳转表或条件分支。
    • 注意事项:需要 break,否则会贯穿。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      #include <iostream>
      int main() {
      int x = 2;
      switch (x) {
      case 1: std::cout << "One\n"; break;
      case 2: std::cout << "Two\n"; break;
      default: std::cout << "Other\n";
      }
      return 0;
      }

循环

  • for
    • 功能:固定次数循环,包含初始化、条件和增量。
    • 使用场景:数组遍历、计数。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      #include <iostream>
      int main() {
      for (int i = 0; i < 3; i++) {
      std::cout << i << " "; // 0 1 2
      }
      std::cout << "\n";
      return 0;
      }
  • while
    • 功能:条件为真时重复执行。
    • 使用场景:不确定次数的循环。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      #include <iostream>
      int main() {
      int i = 0;
      while (i < 3) {
      std::cout << i << " "; // 0 1 2
      i++;
      }
      std::cout << "\n";
      return 0;
      }
  • do-while
    • 功能:至少执行一次,后检查条件。
    • 使用场景:需要至少运行一次的循环。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      #include <iostream>
      int main() {
      int i = 0;
      do {
      std::cout << i << " "; // 0 1 2
      i++;
      } while (i < 3);
      std::cout << "\n";
      return 0;
      }

跳转

  • break
    • 功能:跳出当前循环或 switch。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      #include <iostream>
      int main() {
      for (int i = 0; i < 5; i++) {
      if (i == 3) break;
      std::cout << i << " "; // 0 1 2
      }
      std::cout << "\n";
      return 0;
      }
  • continue
    • 功能:跳过本次循环,继续下一次。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      #include <iostream>
      int main() {
      for (int i = 0; i < 5; i++) {
      if (i == 2) continue;
      std::cout << i << " "; // 0 1 3 4
      }
      std::cout << "\n";
      return 0;
      }
  • return
    • 功能:退出函数并返回值。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      #include <iostream>
      int add(int a, int b) {
      return a + b; // 返回并退出
      }
      int main() {
      std::cout << add(2, 3) << "\n"; // 5
      return 0;
      }
  • goto
    • 功能:跳转到指定标签。
    • 使用场景:复杂控制流(不推荐)。
    • 注意事项:易导致代码混乱。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      #include <iostream>
      int main() {
      int x = 0;
      goto label;
      x = 1; // 被跳过
      label:
      std::cout << "x: " << x << "\n"; // 0
      return 0;
      }

函数

函数是可重用的代码块,支持多种特性。

  • 函数声明与定义

    • 功能:声明指定函数签名,定义实现具体逻辑。
    • 使用场景:模块化编程。
    • 底层原理:声明生成符号,定义分配代码段。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      #include <iostream>
      void func(); // 声明
      int main() {
      func();
      return 0;
      }
      void func() { // 定义
      std::cout << "Function called\n";
      }
  • 参数传递

    • 值传递:传递副本,修改不影响原值。
      • 示例代码:
        1
        2
        3
        4
        5
        6
        7
        8
        #include <iostream>
        void byValue(int x) { x = 20; }
        int main() {
        int a = 10;
        byValue(a);
        std::cout << "a: " << a << "\n"; // 10
        return 0;
        }
    • 引用传递:传递别名,修改影响原值。
      • 示例代码:
        1
        2
        3
        4
        5
        6
        7
        8
        #include <iostream>
        void byReference(int& x) { x = 20; }
        int main() {
        int a = 10;
        byReference(a);
        std::cout << "a: " << a << "\n"; // 20
        return 0;
        }
  • 默认参数

    • 功能:为参数提供默认值,未传参时使用。
    • 注意事项:默认参数必须从右向左定义。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      #include <iostream>
      void func(int x = 10, int y = 20) {
      std::cout << "x: " << x << ", y: " << y << "\n";
      }
      int main() {
      func(); // 10, 20
      func(5); // 5, 20
      func(5, 15); // 5, 15
      return 0;
      }
  • 函数重载

    • 功能:同名函数根据参数不同区分。
    • 底层原理:编译器通过名字修饰区分。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      #include <iostream>
      int add(int a, int b) { return a + b; }
      double add(double a, double b) { return a + b; }
      int main() {
      std::cout << add(1, 2) << "\n"; // 3
      std::cout << add(1.5, 2.5) << "\n"; // 4
      return 0;
      }
  • 内联函数

    • 功能:使用 inline 建议编译器内联展开,减少调用开销。
    • 使用场景:小型函数提升性能。
    • 注意事项:仅建议,编译器可能忽略。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      #include <iostream>
      inline int square(int x) { return x * x; }
      int main() {
      std::cout << square(5) << "\n"; // 25
      return 0;
      }

指针

C++ 中的指针(Pointer)是一个非常核心且强大的概念,它允许直接操作内存地址。理解指针对于编写高效的 C++ 程序、动态内存管理以及底层操作至关重要。下面我会从基础到进阶,详细讲解 C++ 指针的方方面面。


1. 什么是指针?

  • 定义:指针是一个变量,它存储的是另一个变量的内存地址。
  • 内存地址:在计算机中,每个变量都有一个内存地址,指针通过存储这个地址来间接访问变量。
  • 用途
    • 动态内存分配(new/delete)。
    • 数组操作。
    • 函数参数传递(避免拷贝大对象)。
    • 实现数据结构(如链表、树)。
指针的声明

指针通过 * 符号声明,格式为:

1
类型 *指针名;
- 类型:指针指向的数据类型。 - *:表示这是一个指针。

示例

1
int *ptr; // 声明一个指向 int 类型的指针


2. 指针的基本操作

(1)获取变量地址

使用 & 操作符获取变量的地址:

1
2
int x = 10;
int *ptr = &x; // ptr 存储 x 的地址
- &x:获取变量 x 的内存地址。 - ptr:存储 x 的地址。

(2)解引用(访问指针指向的值)

使用 * 操作符访问指针指向的值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

int main() {
int x = 10;
int *ptr = &x;

cout << "x 的值: " << x << endl; // 输出: 10
cout << "x 的地址: " << &x << endl; // 输出: x 的地址(例如 0x7ffee4c0a4ac)
cout << "ptr 存储的地址: " << ptr << endl; // 输出: x 的地址
cout << "ptr 指向的值: " << *ptr << endl; // 输出: 10

// 修改指针指向的值
*ptr = 20;
cout << "修改后 x 的值: " << x << endl; // 输出: 20

return 0;
}
- *ptr:解引用,获取指针 ptr 指向的值。 - 修改 *ptr 会直接改变 x 的值,因为它们指向同一块内存。

(3)指针的初始化

未初始化的指针是危险的(野指针),可能指向随机内存。建议总是初始化指针:

1
int *ptr = nullptr; // C++11 推荐,初始化为空指针
- nullptr 表示指针不指向任何地址,避免野指针问题。


3. 指针与数组

在 C++ 中,数组名本质上是一个指向数组第一个元素的指针。

数组与指针的关系
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <iostream>
using namespace std;

int main() {
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr; // arr 是一个指针,指向数组第一个元素

cout << "数组第一个元素: " << *ptr << endl; // 输出: 1
cout << "数组第二个元素: " << *(ptr + 1) << endl; // 输出: 2

// 使用指针遍历数组
for (int i = 0; i < 5; i++) {
cout << "arr[" << i << "] = " << *(ptr + i) << endl;
}

return 0;
}
  • arr 等价于 &arr[0],是一个指针。
  • ptr + 1:指针加 1,移动到下一个元素(偏移一个 int 的大小,通常是 4 字节)。
  • *(ptr + i):访问第 i 个元素。
数组下标与指针

数组下标操作 arr[i] 等价于 *(arr + i)

1
2
3
int arr[5] = {1, 2, 3, 4, 5};
cout << arr[2] << endl; // 输出: 3
cout << *(arr + 2) << endl; // 输出: 3


4. 动态内存分配

C++ 中可以使用 newdelete 动态分配和释放内存,指针是动态内存管理的核心。

动态分配内存
1
2
3
4
5
6
int *ptr = new int; // 分配一个 int 大小的内存
*ptr = 42;
cout << *ptr << endl; // 输出: 42

delete ptr; // 释放内存
ptr = nullptr; // 避免野指针
  • new:分配内存并返回地址。
  • delete:释放内存,避免内存泄漏。
动态分配数组
1
2
3
4
5
6
7
8
int *arr = new int[5]; // 分配一个包含 5 个 int 的数组
for (int i = 0; i < 5; i++) {
arr[i] = i + 1;
cout << arr[i] << endl; // 输出: 1 2 3 4 5
}

delete[] arr; // 释放数组内存
arr = nullptr;
  • new int[5]:分配数组。
  • delete[]:释放数组内存(注意与 delete 的区别)。

5. 指针与函数

指针常用于函数参数传递,特别是在需要修改原始数据或避免拷贝大对象时。

(1)通过指针修改变量
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}

int main() {
int x = 10, y = 20;
cout << "Before swap: x = " << x << ", y = " << y << endl;

swap(&x, &y);
cout << "After swap: x = " << x << ", y = " << y << endl;

return 0;
}
  • 输出:
    1
    2
    Before swap: x = 10, y = 20
    After swap: x = 20, y = 10
  • 传递地址给函数,通过解引用修改原始变量。
(2)指针作为函数返回值

函数可以返回指针,但需要小心返回局部变量的地址(会导致未定义行为):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
int* createArray(int size) {
int *arr = new int[size]; // 动态分配内存
for (int i = 0; i < size; i++) {
arr[i] = i;
}
return arr;
}

int main() {
int *arr = createArray(5);
for (int i = 0; i < 5; i++) {
cout << arr[i] << endl; // 输出: 0 1 2 3 4
}
delete[] arr; // 释放内存
return 0;
}
- 返回动态分配的内存,调用者负责释放。

(3)避免返回局部变量地址
1
2
3
4
int* badFunction() {
int x = 10;
return &x; // 错误!x 是局部变量,函数返回后 x 被销毁
}
  • 局部变量的地址在函数返回后无效,访问会导致未定义行为。

6. 指针的进阶用法

(1)指针的指针(多级指针)

指针可以指向另一个指针,形成多级指针:

1
2
3
4
5
int x = 10;
int *ptr = &x;
int **pptr = &ptr; // 指向指针的指针

cout << **pptr << endl; // 输出: 10
- **pptr:解引用两次,访问 x 的值。

(2)常量指针与指针常量
  • 常量指针const int *ptr):指针指向的值是常量,不能修改。
    1
    2
    3
    int x = 10;
    const int *ptr = &x;
    // *ptr = 20; // 错误,不能修改值
  • 指针常量int *const ptr):指针本身是常量,不能指向其他地址。
    1
    2
    3
    4
    int x = 10;
    int *const ptr = &x;
    *ptr = 20; // 可以修改值
    // ptr = nullptr; // 错误,不能修改指针地址
  • 常量指针常量const int *const ptr):既不能修改值,也不能修改地址。
(3)函数指针

函数指针指向函数的地址,用于回调或动态调用:

1
2
3
4
5
6
7
8
9
10
11
12
#include <iostream>
using namespace std;

void sayHello() {
cout << "Hello!" << endl;
}

int main() {
void (*funcPtr)() = sayHello; // 函数指针
funcPtr(); // 调用函数,输出: Hello!
return 0;
}


7. 常见问题与注意事项

  • 野指针:未初始化或已释放的指针,访问会导致未定义行为。
    • 解决:始终初始化指针,释放后置为 nullptr
  • 内存泄漏:动态分配的内存未释放。
    • 解决:使用 delete 释放内存,或者使用智能指针(如 std::unique_ptrstd::shared_ptr)。
  • 悬空指针:指针指向的内存已被释放。
    • 解决:释放后立即置为 nullptr
  • 指针越界:访问数组时超出范围。
    • 解决:确保指针操作不越界,使用 std::vector 等容器更安全。

8. 智能指针(C++11 及以上)

C++11 引入了智能指针,简化了内存管理,推荐使用: - std::unique_ptr:独占所有权,自动释放。 - std::shared_ptr:共享所有权,引用计数。 - std::weak_ptr:解决 shared_ptr 循环引用问题。

示例:使用 std::unique_ptr

1
2
3
4
5
6
7
8
9
10
#include <iostream>
#include <memory>
using namespace std;

int main() {
unique_ptr<int> ptr(new int(42));
cout << *ptr << endl; // 输出: 42
// 离开作用域时,ptr 自动释放内存
return 0;
}


9. 总结

  • 指针基础:存储地址,使用 * 解引用,& 取地址。
  • 指针与数组:数组名是指针,指针可以遍历数组。
  • 动态内存:使用 new 分配,delete 释放。
  • 函数指针:传递地址,避免拷贝或实现回调。
  • 注意事项:避免野指针、内存泄漏,使用智能指针更安全。

引用

C++ 中的引用(Reference)是一种非常重要的特性,它为变量提供了一个别名(alias),允许通过这个别名直接访问和操作原始变量。引用在 C++ 中广泛用于函数参数传递、避免拷贝开销、以及简化代码。以下我会详细讲解 C++ 引用的概念、用法、特点以及与指针的对比。


1. 什么是引用?

  • 定义:引用是某个变量的别名,操作引用等价于操作原始变量。
  • 语法:通过 & 符号声明引用。
    1
    类型 &引用名 = 变量名;
  • 本质:引用并不是一个独立的变量,它只是已有变量的别名,编译器会将对引用的操作直接映射到原始变量上。
基本示例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
using namespace std;

int main() {
int x = 10;
int &ref = x; // ref 是 x 的引用

cout << "x = " << x << endl; // 输出: 10
cout << "ref = " << ref << endl; // 输出: 10

ref = 20; // 修改引用,实际修改 x
cout << "x = " << x << endl; // 输出: 20
cout << "ref = " << ref << endl; // 输出: 20

return 0;
}
  • int &ref = xrefx 的引用。
  • 修改 ref 会直接修改 x,因为它们指向同一个内存地址。

2. 引用的特点

(1)必须初始化

引用在声明时必须初始化,且不能改变指向(不能重新绑定到另一个变量)。

1
2
3
4
5
6
7
int x = 10;
// int &ref; // 错误!引用必须初始化
int &ref = x;

int y = 20;
ref = y; // 不是让 ref 指向 y,而是将 y 的值赋给 ref(即 x)
cout << x << endl; // 输出: 20
- ref = y:将 y 的值赋给 x,而不是让 ref 指向 y

(2)引用不是独立对象

引用不占用额外的内存,它只是变量的别名。refx 的地址相同:

1
2
cout << &x << endl;   // 输出: x 的地址
cout << &ref << endl; // 输出: 同一个地址

(3)不能创建引用的引用

引用本身不是对象,因此不能创建引用的引用:

1
2
3
int x = 10;
int &ref = x;
// int &&ref2 = ref; // 错误!不能创建引用的引用
- 注:&& 在 C++11 中表示右值引用(稍后会讲到)。

(4)不能创建数组的引用

数组的引用是合法的,但不能创建引用的数组:

1
2
3
int arr[3] = {1, 2, 3};
int (&refArr)[3] = arr; // 合法,引用一个数组
// int &refArr[3] = arr; // 错误,不能创建引用的数组


3. 引用的主要用途

(1)函数参数传递

引用常用于函数参数,避免拷贝大对象,提高效率。

示例:按值传递 vs 按引用传递

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
#include <iostream>
using namespace std;

// 按值传递(会拷贝)
void swapByValue(int a, int b) {
int temp = a;
a = b;
b = temp;
}

// 按引用传递(直接操作原始变量)
void swapByReference(int &a, int &b) {
int temp = a;
a = b;
b = temp;
}

int main() {
int x = 10, y = 20;

cout << "Before swapByValue: x = " << x << ", y = " << y << endl;
swapByValue(x, y);
cout << "After swapByValue: x = " << x << ", y = " << y << endl;

cout << "Before swapByReference: x = " << x << ", y = " << y << endl;
swapByReference(x, y);
cout << "After swapByReference: x = " << x << ", y = " << y << endl;

return 0;
}
- 输出:
1
2
3
4
Before swapByValue: x = 10, y = 20
After swapByValue: x = 10, y = 20
Before swapByReference: x = 10, y = 20
After swapByReference: x = 20, y = 10
- swapByValue:值传递,函数内的修改不影响原始变量。 - swapByReference:引用传递,函数内的修改直接影响原始变量。

效率优势: 对于大对象(如结构体、类对象),引用传递避免了拷贝开销:

1
2
3
4
5
6
7
8
#include <string>
void printByValue(string s) { // 拷贝字符串,效率低
cout << s << endl;
}

void printByReference(const string &s) { // 引用传递,无拷贝
cout << s << endl;
}

(2)函数返回值

引用可以作为函数返回值,避免拷贝大对象:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>
using namespace std;

int& getElement(int arr[], int index) {
return arr[index]; // 返回数组元素的引用
}

int main() {
int arr[5] = {1, 2, 3, 4, 5};
getElement(arr, 2) = 10; // 修改 arr[2]
cout << arr[2] << endl; // 输出: 10
return 0;
}
- 返回引用允许直接修改数组元素。

注意:不要返回局部变量的引用:

1
2
3
4
int& badFunction() {
int x = 10;
return x; // 错误!x 是局部变量,函数返回后 x 被销毁
}

(3)简化代码

引用可以让代码更简洁,避免频繁使用指针和解引用:

1
2
3
4
int x = 10;
int &ref = x;
ref++; // 直接操作引用,等价于 x++
cout << x << endl; // 输出: 11


4. 常量引用(const Reference)

常量引用常用于函数参数,防止意外修改原始数据,同时允许传递临时对象。

基本用法
1
2
3
int x = 10;
const int &ref = x; // 常量引用
// ref = 20; // 错误!不能通过常量引用修改 x
常量引用与临时对象

常量引用可以绑定到临时对象,而普通引用不行:

1
2
3
int x = 10;
const int &ref = x + 1; // 合法,绑定到临时对象
// int &ref2 = x + 1; // 错误,非 const 引用不能绑定临时对象

函数参数中的常量引用
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <iostream>
using namespace std;

void print(const int &value) { // 常量引用,避免拷贝且防止修改
cout << value << endl;
}

int main() {
int x = 10;
print(x); // 传递变量
print(20); // 传递临时对象
print(x + 30); // 传递表达式
return 0;
}
  • const int &value:既避免拷贝,又允许传递临时对象。

5. C++11 引入的右值引用(Rvalue Reference)

C++11 引入了右值引用(&&),用于支持移动语义和完美转发,优化性能。

左值与右值

  • 左值(Lvalue):有固定内存地址,可以取地址的对象(比如变量)。
  • 右值(Rvalue):临时对象或字面量,没有固定地址(比如 x + 1、字面量 42)。

右值引用

右值引用可以绑定到右值,用于实现移动语义(避免拷贝):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <iostream>
using namespace std;

void print(int &lvalue) {
cout << "左值: " << lvalue << endl;
}

void print(int &&rvalue) {
cout << "右值: " << rvalue << endl;
}

int main() {
int x = 10;
print(x); // 调用左值版本
print(20); // 调用右值版本
print(x + 30); // 调用右值版本
return 0;
}
- 输出:
1
2
3
左值: 10
右值: 20
右值: 40

移动语义

右值引用常用于移动构造和移动赋值,避免深拷贝:

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
32
33
#include <iostream>
#include <string>
using namespace std;

class MyString {
private:
char *data;
public:
MyString(const char *str) {
data = new char[strlen(str) + 1];
strcpy(data, str);
cout << "构造: " << data << endl;
}

// 移动构造函数
MyString(MyString &&other) noexcept : data(other.data) {
other.data = nullptr; // 转移资源
cout << "移动构造" << endl;
}

~MyString() {
if (data) {
cout << "析构: " << data << endl;
delete[] data;
}
}
};

int main() {
MyString s1("Hello");
MyString s2 = move(s1); // 移动构造
return 0;
}
- 输出:
1
2
3
构造: Hello
移动构造
析构: Hello
- move:将 s1 转换为右值,触发移动构造,避免拷贝。

移动构造函数和移动语义

这段代码展示了 C++11 引入的右值引用和移动语义,用于优化资源管理,避免不必要的拷贝。


  1. 代码整体结构
  • 头文件

    1
    2
    3
    #include <iostream>
    #include <string>
    using namespace std;
    • <iostream>:用于输入输出(cout)。
    • <string>:虽然代码中没有直接使用 std::string,但可能是为了后续扩展。
    • using namespace std;:避免每次使用 std::cout 时写 std::
  • MyString

    • 这是一个自定义字符串类,内部使用 char* 管理动态分配的字符数组。
    • 包含构造函数、移动构造函数和析构函数。
  • 主函数 main

    • 创建一个 MyString 对象 s1,然后通过 move 将其资源移动到 s2

  1. 逐部分分析

(1)类 MyString 的定义

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
class MyString {
private:
char *data;
public:
// 构造函数
MyString(const char *str) {
data = new char[strlen(str) + 1];
strcpy(data, str);
cout << "构造: " << data << endl;
}

// 移动构造函数
MyString(MyString &&other) noexcept : data(other.data) {
other.data = nullptr; // 转移资源
cout << "移动构造" << endl;
}

// 析构函数
~MyString() {
if (data) {
cout << "析构: " << data << endl;
delete[] data;
}
}
};

成员变量

  • char *data:一个指针,指向动态分配的字符数组,用于存储字符串。

构造函数

1
2
3
4
5
MyString(const char *str) {
data = new char[strlen(str) + 1];
strcpy(data, str);
cout << "构造: " << data << endl;
}
  • 作用:从一个 C 风格字符串(const char*)构造 MyString 对象。
  • 细节
    • strlen(str) + 1:计算字符串长度(包括结尾的 \0)。
    • new char[strlen(str) + 1]:动态分配内存,存储字符串。
    • strcpy(data, str):将输入字符串复制到 data 中。
    • cout << "构造: " << data << endl:打印构造信息。

移动构造函数

1
2
3
4
MyString(MyString &&other) noexcept : data(other.data) {
other.data = nullptr; // 转移资源
cout << "移动构造" << endl;
}
  • 作用:这是 C++11 引入的移动构造函数,用于从一个右值(临时对象或被 move 的对象)转移资源。
  • 参数
    • MyString &&other:右值引用,表示 other 是一个右值(临时对象或即将销毁的对象)。
  • 细节
    • : data(other.data):初始化列表,将 otherdata 指针直接赋值给当前对象的 data
    • other.data = nullptr:将 otherdata 置为空,避免 other 析构时释放同一块内存。
    • noexcept:表示这个函数不会抛出异常,移动构造函数通常需要是 noexcept 的,以便在标准库(如 std::vector)中使用时更安全。
    • cout << "移动构造" << endl:打印移动构造信息。

析构函数

1
2
3
4
5
6
~MyString() {
if (data) {
cout << "析构: " << data << endl;
delete[] data;
}
}
  • 作用:在对象销毁时释放动态分配的内存。
  • 细节
    • if (data):检查 data 是否为空指针(避免释放空指针)。
    • cout << "析构: " << data << endl:打印析构信息。
    • delete[] data:释放 data 指向的内存。

(2)主函数 main

1
2
3
4
5
int main() {
MyString s1("Hello");
MyString s2 = move(s1); // 移动构造
return 0;
}

第一行:创建 s1

1
MyString s1("Hello");
  • 调用普通构造函数,构造一个 MyString 对象 s1

  • 内部:

    • 分配内存,存储字符串 “Hello”。

    • 输出:

      1
      构造: Hello

第二行:创建 s2 并移动 s1

1
MyString s2 = move(s1); // 移动构造
  • 作用:通过 std::moves1 转换为右值,触发移动构造函数。

  • 细节

    • std::move(s1):将 s1 标记为右值(尽管 s1 是一个左值),告诉编译器可以“偷”它的资源。

    • 调用移动构造函数:

      • s2data 接管 s1data(指向 “Hello” 的内存)。
      • s1data 被置为 nullptr
    • 输出:

      1
      移动构造

程序结束:析构

  • main 函数结束时,s1s2 离开作用域,调用析构函数。

  • s2data 指向 “Hello”,正常析构:

    1
    析构: Hello
  • s1datanullptr,不会释放内存(避免重复释放)。

完整输出

1
2
3
构造: Hello
移动构造
析构: Hello

  1. 为什么需要移动构造函数?

(1)问题:拷贝的开销

如果没有移动构造函数,MyString s2 = s1 会调用拷贝构造函数(这里没有定义,默认会执行深拷贝):

  • 深拷贝会重新分配内存,复制 “Hello”,然后 s1s2 各自管理自己的内存。
  • 问题:如果 s1 即将销毁(比如临时对象),这种拷贝是浪费的。

(2)移动语义的优势

  • 移动构造函数通过“偷”资源(将 s1data 直接给 s2),避免了深拷贝。
  • s1data 被置为 nullptr,确保它不会重复释放内存。
  • 效率:移动操作(指针赋值)比拷贝(内存分配和复制)快得多。

(3)std::move 的作用

  • std::move 并不真正“移动”数据,它只是将对象转换为右值,告诉编译器可以调用移动构造函数。
  • 移动后,s1 进入“可销毁”状态(datanullptr),但仍然可以安全析构。

  1. 代码中需要注意的点

(1)内存管理

  • MyString 使用动态内存(newdelete[]),需要小心管理。
  • 移动构造函数通过将 other.data 置为 nullptr,避免了重复释放内存(double-free)的问题。

(2)缺少拷贝构造函数

  • 这段代码没有定义拷贝构造函数和拷贝赋值运算符。
  • 如果直接拷贝(MyString s2 = s1),编译器会生成默认的拷贝构造函数,导致浅拷贝(s1s2 共享 data),析构时会重复释放内存,引发崩溃。
  • 改进:需要实现拷贝构造函数和拷贝赋值运算符(遵循“规则五”)。

改进后的代码

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
32
33
34
35
36
37
38
39
40
41
42
#include <iostream>
#include <cstring>
using namespace std;

class MyString {
private:
char *data;
public:
// 构造函数
MyString(const char *str) {
data = new char[strlen(str) + 1];
strcpy(data, str);
cout << "构造: " << data << endl;
}

// 拷贝构造函数
MyString(const MyString &other) {
data = new char[strlen(other.data) + 1];
strcpy(data, other.data);
cout << "拷贝构造: " << data << endl;
}

// 移动构造函数
MyString(MyString &&other) noexcept : data(other.data) {
other.data = nullptr;
cout << "移动构造" << endl;
}

// 析构函数
~MyString() {
if (data) {
cout << "析构: " << data << endl;
delete[] data;
}
}
};

int main() {
MyString s1("Hello");
MyString s2 = move(s1); // 移动构造
return 0;
}

(3)noexcept 的重要性

  • 移动构造函数标记为 noexcept,告诉编译器它不会抛出异常。
  • 标准库(如 std::vector)在移动元素时会优先选择 noexcept 的移动构造函数,否则可能退回到拷贝。

  1. 移动语义的核心思想
  • 移动语义:将资源从一个对象“转移”到另一个对象,而不是拷贝。
  • 适用场景
    • 临时对象(右值)即将销毁时。
    • 需要高效转移资源(如动态内存、文件句柄)。
  • 好处
    • 避免不必要的深拷贝,提高性能。
    • 资源管理更安全(避免重复释放)。

  1. 总结
  • 代码功能
    • MyString 是一个管理动态字符串的类。
    • 移动构造函数通过右值引用(&&)实现资源转移,避免深拷贝。
    • std::moves1 转换为右值,触发移动构造。
  • 输出解释
    • 构造 s1:分配内存,存储 “Hello”。
    • 移动构造 s2:将 s1data 转移给 s2s1data 置为 nullptr
    • 析构:s2 释放 “Hello”,s1datanullptr,不释放。
  • 改进建议
    • 添加拷贝构造函数和拷贝赋值运算符,避免浅拷贝问题。
    • 考虑使用 std::string 替代 char*,更安全且方便。

如果你还有不明白的地方(比如右值引用的细节或拷贝构造函数的实现),可以告诉我,我会进一步讲解!


6. 引用与指针的对比

特性 引用 指针
初始化 必须初始化,不能改变指向 可以不初始化,可改变指向
内存 不占用额外内存(只是别名) 占用内存(存储地址)
语法 直接使用(无需解引用) 需要 * 解引用
安全性 更安全,不会指向无效内存 可能出现野指针、悬空指针
用途 函数参数、简化代码 动态内存、复杂数据结构

选择建议: - 优先使用引用,除非需要动态内存分配或改变指向(用指针)。 - 函数参数传递时,引用比指针更简洁且安全。


7. 注意事项

  • 避免返回局部变量的引用:局部变量在函数返回后销毁,返回其引用会导致未定义行为。
  • 引用与指针混用:引用可以绑定到指针解引用后的值:
    1
    2
    3
    4
    5
    int x = 10;
    int *ptr = &x;
    int &ref = *ptr; // ref 绑定到 ptr 指向的值
    ref = 20;
    cout << x << endl; // 输出: 20
  • 性能优化:对于大对象,使用引用传递(尤其是 const 引用)可以避免拷贝。

8. 总结

  • 引用基础:引用是变量的别名,操作引用等价于操作原始变量。
  • 用途
    • 函数参数传递:避免拷贝,提高效率。
    • 函数返回值:允许直接修改。
    • 简化代码:无需解引用。
  • 常量引用:防止修改,绑定临时对象。
  • 右值引用:支持移动语义,优化性能。
  • 与指针对比:引用更安全、简洁,但灵活性不如指针。

类与对象(面向对象特性)

C++ 的面向对象特性是其区别于 C 的重要部分,支持封装、继承和多态。以下是对每个子特性的详细讲解。

类定义(classstruct

  • 功能:类是用户定义的类型,封装数据和行为;class 默认访问权限为 privatestruct 默认为 public
  • 使用场景:建模现实世界的实体,如人、车等。
  • 底层原理:类是编译器的蓝图,实例化后分配内存,成员按声明顺序排列。
  • 注意事项:注意内存对齐可能增加对象大小;classstruct 在语法上等价,仅默认访问权限不同。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    #include <iostream>
    class MyClass { // class 定义
    private:
    int x;
    public:
    MyClass(int val) : x(val) {} // 构造函数
    int getX() { return x; }
    };
    struct MyStruct { // struct 定义
    int y = 20; // 默认 public
    };
    int main() {
    MyClass obj(10);
    std::cout << "MyClass x: " << obj.getX() << "\n"; // 10
    MyStruct s;
    std::cout << "MyStruct y: " << s.y << "\n"; // 20
    return 0;
    }

访问控制(publicprivateprotected

  • 功能:控制类成员的访问权限:
    • public:任何地方可访问。
    • private:仅类内部和友元可访问。
    • protected:类内部和派生类可访问。
  • 使用场景:封装数据,隐藏实现细节。
  • 底层原理:访问控制由编译器在编译时检查,不影响运行时。
  • 注意事项:合理设计访问权限以保护数据一致性。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #include <iostream>
    class MyClass {
    private:
    int x = 10; // 私有
    protected:
    int y = 20; // 受保护
    public:
    int z = 30; // 公有
    int getX() { return x; }
    int getY() { return y; }
    };
    int main() {
    MyClass obj;
    // std::cout << obj.x << "\n"; // 错误:私有
    // std::cout << obj.y << "\n"; // 错误:受保护
    std::cout << "z: " << obj.z << "\n"; // 30
    std::cout << "x: " << obj.getX() << "\n"; // 10
    std::cout << "y: " << obj.getY() << "\n"; // 20
    return 0;
    }

构造函数与析构函数

  • 功能
    • 构造函数:初始化对象,名称与类名相同,无返回值。构造顺序:基类 -> 成员对象 -> 派生类。
    • 析构函数:清理资源,名称为 ~类名,自动调用。析构顺序:派生类 -> 成员对象 -> 基类(与构造相反)。
    • 局部对象遵循“后构造先析构”,全局/静态对象和数组元素也有类似规律。虚析构函数确保多态场景下析构顺序正确。
  • 使用场景:管理对象生命周期,如分配/释放动态内存。
  • 底层原理:构造函数在对象创建时由编译器调用,析构函数在对象销毁时调用(栈上自动,堆上需 delete)。
  • 注意事项:若未定义,编译器提供默认版本;动态内存需手动释放。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #include <iostream>
    class MyClass {
    private:
    int* ptr;
    public:
    MyClass(int val) { // 构造函数
    ptr = new int(val);
    std::cout << "Constructor called, value: " << *ptr << "\n";
    }
    ~MyClass() { // 析构函数
    delete ptr;
    std::cout << "Destructor called\n";
    }
    int get() { return *ptr; }
    };
    int main() {
    MyClass obj(42); // 栈上对象,自动析构
    std::cout << "Value: " << obj.get() << "\n"; // 42
    return 0; // 输出 Constructor -> Value -> Destructor
    }

拷贝构造函数

  • 功能:以现有对象初始化新对象,形式为 Class(const Class&)
  • 使用场景:对象复制,如函数参数传递。
  • 底层原理:默认执行浅拷贝,自定义可实现深拷贝。
  • 注意事项:若有动态资源,需深拷贝以避免双重释放。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #include <iostream>
    class MyClass {
    private:
    int* ptr;
    public:
    MyClass(int val) : ptr(new int(val)) {}
    MyClass(const MyClass& other) { // 拷贝构造函数
    ptr = new int(*other.ptr); // 深拷贝
    std::cout << "Copy constructor called\n";
    }
    ~MyClass() { delete ptr; }
    int get() { return *ptr; }
    };
    int main() {
    MyClass a(10);
    MyClass b = a; // 调用拷贝构造函数
    std::cout << "a: " << a.get() << ", b: " << b.get() << "\n"; // 10, 10
    return 0;
    }

成员函数与数据成员

  • 功能
    • 数据成员:类中的变量,存储对象状态。
    • 成员函数:类中的函数,操作数据成员。
  • 使用场景:实现类的行为和属性。
  • 底层原理:成员函数共享,所有对象调用同一代码;数据成员每个对象独立存储。
  • 注意事项:避免成员函数修改意外数据。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include <iostream>
    class MyClass {
    public:
    int x = 10; // 数据成员
    void increment() { x++; } // 成员函数
    int getX() { return x; }
    };
    int main() {
    MyClass obj;
    std::cout << "Initial x: " << obj.getX() << "\n"; // 10
    obj.increment();
    std::cout << "After increment: " << obj.getX() << "\n"; // 11
    return 0;
    }

静态成员(static

  • 功能
    • 静态数据成员:类级别共享,所有对象共用。
    • 静态成员函数:无需对象即可调用,仅访问静态成员。
  • 使用场景:计数对象数量、工具函数。
  • 底层原理:静态成员存储在全局数据区,生命周期与程序相同。
  • 注意事项:静态数据成员需在类外定义。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include <iostream>
    class MyClass {
    public:
    static int count; // 静态数据成员
    MyClass() { count++; }
    static int getCount() { return count; } // 静态成员函数
    };
    int MyClass::count = 0; // 类外定义
    int main() {
    MyClass a, b;
    std::cout << "Object count: " << MyClass::getCount() << "\n"; // 2
    return 0;
    }

友元(friend

  • 功能:允许外部函数或类访问私有成员。
  • 使用场景:实现紧密相关的类或函数。
  • 底层原理:编译器放宽访问限制。
  • 注意事项:过度使用破坏封装。
  • 示例代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include <iostream>
    class MyClass {
    private:
    int x = 10;
    friend void print(MyClass& obj); // 友元函数
    };
    void print(MyClass& obj) {
    std::cout << "x: " << obj.x << "\n";
    }
    int main() {
    MyClass obj;
    print(obj); // 10
    return 0;
    }

继承(单继承、多继承)

  • 功能
    • 单继承:一个基类派生一个子类。
    • 多继承:多个基类派生一个子类。
  • 使用场景:代码复用、建模层次关系。
  • 底层原理:子类对象包含基类子对象,多继承可能导致内存布局复杂。
  • 注意事项:多继承可能引发菱形继承问题。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #include <iostream>
    class Base {
    public:
    int x = 10;
    };
    class Derived : public Base { // 单继承
    public:
    int y = 20;
    };
    class Base2 {
    public:
    int z = 30;
    };
    class Multi : public Base, public Base2 { // 多继承
    };
    int main() {
    Derived d;
    std::cout << "x: " << d.x << ", y: " << d.y << "\n"; // 10, 20
    Multi m;
    std::cout << "x: " << m.x << ", z: " << m.z << "\n"; // 10, 30
    return 0;
    }

多态性(虚函数 virtual,纯虚函数 = 0

  • 功能
    • 虚函数:通过基类指针调用派生类实现。
    • 纯虚函数:定义抽象接口,派生类必须实现。
  • 使用场景:运行时多态,如插件系统。
  • 底层原理:虚函数通过虚表(vtable)实现,每个类一个虚表指针。
  • 注意事项:虚函数有运行时开销;纯虚函数使类抽象。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    #include <iostream>
    class Base {
    public:
    virtual void speak() { std::cout << "Base\n"; } // 虚函数
    virtual void pure() = 0; // 纯虚函数
    virtual ~Base() {} // 虚析构函数,当基类指针指向派生类对象时,如果基类的析构函数不是虚函数,delete 操作只会调用基类的析构函数,而不会调用派生类的析构函数。这可能导致派生类中分配的资源(如动态内存、文件句柄等)无法释放,造成内存泄漏或资源未清理。
    };
    class Derived : public Base {
    public:
    void speak() override { std::cout << "Derived\n"; }
    void pure() override { std::cout << "Pure implemented\n"; }
    };
    int main() {
    Base* ptr = new Derived();
    ptr->speak(); // Derived
    ptr->pure(); // Pure implemented
    delete ptr;
    return 0;
    }

抽象类与接口

  • 功能:含纯虚函数的类为抽象类,不能实例化;全纯虚函数类可作为接口。
  • 使用场景:定义通用接口,如策略模式。
  • 底层原理:抽象类阻止实例化,强制子类实现。
  • 注意事项:析构函数应为虚函数。
  • 示例代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include <iostream>
    class Interface {
    public:
    virtual void action() = 0; // 接口
    virtual ~Interface() {}
    };
    class Impl : public Interface {
    public:
    void action() override { std::cout << "Action\n"; }
    };
    int main() {
    Interface* ptr = new Impl();
    ptr->action(); // Action
    delete ptr;
    return 0;
    }

模板

模板支持泛型编程,使代码类型无关。

函数模板

  • 功能:定义泛型函数,适用于多种类型。
  • 使用场景:通用算法,如最大值计算。
  • 底层原理:编译器为每种类型生成具体函数。
  • 注意事项:类型需支持函数中使用的操作。
  • 示例代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <iostream>
    namespace my_utils {
    template<typename T>
    T max(T a, T b) {
    return (a > b) ? a : b;
    }
    }
    int main() {
    std::cout << "Max int: " << my_utils::max(5, 3) << "\n"; // 5,会和 std::max 冲突, <algorithm> 库下的,或者使用 ::max ,表示使用全局命名空间中的 max
    std::cout << "Max double: " << my_utils::max(1.5, 2.5) << "\n"; // 2.5
    return 0;
    }

类模板

  • 功能:定义泛型类,成员类型可变。
  • 使用场景:容器类,如向量、列表。
  • 底层原理:编译器为每种类型实例化类。
  • 注意事项:模板定义和实现通常放在头文件中。
  • 示例代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #include <iostream>
    template<typename T>
    class Box {
    private:
    T value;
    public:
    Box(T v) : value(v) {}
    T get() { return value; }
    void set(T v) { value = v; }
    };
    int main() {
    Box<int> intBox(42);
    std::cout << "Int box: " << intBox.get() << "\n"; // 42
    Box<double> doubleBox(3.14);
    std::cout << "Double box: " << doubleBox.get() << "\n"; // 3.14
    return 0;
    }

异常处理

异常处理机制用于管理运行时错误。

trycatchthrow

  • 功能
    • throw:抛出异常。
    • try:包裹可能抛异常的代码。
    • catch:捕获并处理异常。
  • 使用场景:错误处理,如文件操作失败。
  • 底层原理:异常通过栈展开传递,调用析构函数。
  • 注意事项:未捕获的异常终止程序。
  • 示例代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include <iostream>
    #include <stdexcept>
    int main() {
    try {
    int x = -1;
    if (x < 0) {
    throw std::runtime_error("Negative value");
    }
    } catch (const std::exception& e) {
    std::cout << "Exception: " << e.what() << "\n"; // Negative value
    }
    return 0;
    }

标准异常类(如 std::exception

  • 功能:提供标准化的异常类型。
  • 使用场景:一致性错误处理。
  • 示例代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <iostream>
    #include <stdexcept>
    int main() {
    try {
    throw std::out_of_range("Index out of bounds");
    } catch (const std::out_of_range& e) {
    std::cout << "Out of range: " << e.what() << "\n";
    } catch (const std::exception& e) {
    std::cout << "General exception: " << e.what() << "\n";
    }
    return 0;
    }

命名空间

命名空间用于组织代码,避免命名冲突。

定义(namespace

  • 功能:将标识符分组,限定作用域。
  • 使用场景:库开发、避免全局污染。
  • 底层原理:编译器通过命名空间修饰符号名。
  • 注意事项:嵌套命名空间增加复杂度。
  • 示例代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <iostream>
    namespace MySpace {
    int x = 42;
    namespace Inner {
    int y = 10;
    }
    }
    int main() {
    std::cout << "x: " << MySpace::x << "\n"; // 42
    std::cout << "y: " << MySpace::Inner::y << "\n"; // 10
    return 0;
    }

使用(using

  • 功能:引入命名空间或特定符号。
  • 注意事项using namespace 可能导致冲突。
  • 示例代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include <iostream>
    namespace MySpace {
    int x = 42;
    }
    int main() {
    using MySpace::x; // 引入 x
    std::cout << "x: " << x << "\n"; // 42
    // using namespace MySpace; // 引入整个命名空间
    return 0;
    }

动态内存管理

C++ 支持手动管理堆内存。

newdelete

  • 功能
    • new:分配堆内存并构造对象。
    • delete:析构对象并释放内存。
  • 使用场景:动态对象生命周期管理。
  • 底层原理:调用内存分配器(如 malloc)和构造函数。
  • 注意事项:成对使用,避免内存泄漏。
  • 示例代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include <iostream>
    class MyClass {
    public:
    int x;
    MyClass(int val) : x(val) { std::cout << "Constructed\n"; }
    ~MyClass() { std::cout << "Destroyed\n"; }
    };
    int main() {
    MyClass* ptr = new MyClass(10);
    std::cout << "x: " << ptr->x << "\n"; // 10
    delete ptr; // 释放
    return 0;
    }

数组(new[]delete[]

  • 功能:分配和释放连续内存块。
  • 注意事项new[] 需用 delete[] 释放。
  • 示例代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include <iostream>
    int main() {
    int* arr = new int[3]{1, 2, 3}; // 分配并初始化
    for (int i = 0; i < 3; i++) {
    std::cout << arr[i] << " "; // 1 2 3
    }
    std::cout << "\n";
    delete[] arr; // 释放数组
    return 0;
    }

预处理器

预处理器在编译前处理代码。

宏定义(#define

  • 功能:定义常量或简单函数。
  • 使用场景:常量、调试开关。
  • 底层原理:文本替换,无类型检查。
  • 注意事项:避免复杂宏,易出错。
  • 示例代码:
    1
    2
    3
    4
    5
    6
    7
    8
    #include <iostream>
    #define PI 3.14
    #define SQUARE(x) ((x) * (x)) // 注意括号
    int main() {
    std::cout << "PI: " << PI << "\n"; // 3.14
    std::cout << "Square(5): " << SQUARE(5) << "\n"; // 25
    return 0;
    }

条件编译(#ifdef#ifndef#endif

  • 功能:根据条件包含或排除代码。
  • 使用场景:跨平台代码、调试。
  • 示例代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include <iostream>
    #define DEBUG
    int main() {
    #ifdef DEBUG
    std::cout << "Debug mode\n";
    #else
    std::cout << "Release mode\n";
    #endif
    #ifndef RELEASE
    std::cout << "Not release\n";
    #endif
    return 0;
    }

文件包含(#include

  • 功能:引入头文件或源代码。
  • 注意事项:使用 <> 表示标准库,"" 表示用户文件。
  • 示例代码:
    1
    2
    3
    4
    5
    6
    #include <iostream>  // 标准库
    #include "myheader.h" // 用户头文件(假设存在)
    int main() {
    std::cout << "Hello\n";
    return 0;
    }

2. 现代 C++ 新特性(C++11 及之后)

从这里开始,我将详细讲解现代 C++ 的特性,从 C++11 开始。

C++11

RAII

RAII(Resource Acquisition Is Initialization,资源获取即初始化)是 C++ 编程中的一种核心设计理念,用于管理资源的分配和释放。它通过将资源的生命周期绑定到对象的生命周期,利用 C++ 的自动对象管理机制(主要是栈对象的构造和析构),确保资源在使用完毕后被正确释放,避免资源泄漏。以下是对 RAII 的详细讲解,包括其原理、实现方式、优点和典型应用。


什么是 RAII?

RAII 的核心思想是: - 资源获取(如内存、文件句柄、锁、网络连接等)在对象构造时完成。 - 资源释放在对象析构时自动完成。 - 利用 C++ 的栈对象生命周期(当对象离开作用域时,析构函数自动调用),RAII 确保资源在不再需要时被正确清理,即使发生异常也能保证释放。

RAII 是 C++ 异常安全性和资源管理的基础,广泛用于标准库和现代 C++ 编程。


RAII 的工作原理
  1. 资源与对象绑定
    • 在对象的构造函数中获取资源(例如分配内存、打开文件、加锁)。
    • 资源的释放逻辑放在析构函数中。
  2. 自动管理
    • C++ 保证当对象离开作用域(无论是正常退出还是抛出异常),其析构函数都会被调用。
    • 因此,资源的释放是自动的,无需程序员手动干预。
  3. 异常安全
    • 即使代码抛出异常,栈上的对象仍会按逆序析构(栈解退,stack unwinding),确保资源不泄漏。

RAII 的代码示例

以下是一个简单的 RAII 示例,用于管理动态分配的内存:

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
#include <iostream>

class Resource {
int* data; // 动态分配的资源
public:
Resource() {
data = new int(42); // 构造函数中获取资源
std::cout << "Resource acquired: " << *data << std::endl;
}

~Resource() {
delete data; // 析构函数中释放资源
std::cout << "Resource released" << std::endl;
}

int getValue() const { return *data; }
};

void useResource() {
Resource r; // 栈上对象,自动管理
std::cout << "Using resource: " << r.getValue() << std::endl;
// 即使这里抛出异常,r 的析构函数也会被调用
} // r 离开作用域,自动调用析构函数释放资源

int main() {
useResource();
return 0;
}

输出

1
2
3
Resource acquired: 42
Using resource: 42
Resource released

在这个例子中: - Resource 对象的构造函数分配内存(new int)。 - 析构函数释放内存(delete data)。 - r 是栈对象,离开作用域时自动调用析构函数,确保内存不泄漏。


RAII 的典型应用

RAII 在 C++ 中无处不在,以下是几个常见场景:

  1. 动态内存管理
    • 标准库的智能指针(如 std::unique_ptrstd::shared_ptr)是 RAII 的经典实现。
    • 示例:
      1
      2
      3
      4
      5
      #include <memory>
      void example() {
      std::unique_ptr<int> ptr = std::make_unique<int>(10);
      // 使用 ptr
      } // ptr 离开作用域,内存自动释放
    • unique_ptr 在析构时自动调用 delete,无需手动释放。
  2. 文件管理
    • std::fstream(如 std::ifstreamstd::ofstream)使用 RAII 管理文件句柄。
    • 示例:
      1
      2
      3
      4
      5
      #include <fstream>
      void writeFile() {
      std::ofstream file("example.txt");
      file << "Hello, RAII!";
      } // file 离开作用域,自动关闭文件
  3. 互斥锁管理
    • std::lock_guardstd::unique_lock 使用 RAII 管理线程同步中的锁。
    • 示例(结合你的线程代码):
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      #include <mutex>
      std::mutex mtx;
      int counter = 0;

      void increment() {
      for (int i = 0; i < 1000; ++i) {
      std::lock_guard<std::mutex> lock(mtx); // RAII 管理锁
      ++counter;
      } // lock 离开作用域,自动解锁
      }
    • lock_guard 在构造时加锁,析构时解锁,保证即使抛出异常,锁也会释放。
  4. 其他资源
    • 网络连接(如 std::socket 的封装)。
    • 数据库连接。
    • 图形资源(如 OpenGL 上下文)。

RAII 的优点
  1. 自动资源管理
    • 资源释放由析构函数自动完成,避免手动调用 deleteclose 等。
  2. 异常安全
    • 即使抛出异常,栈解退机制确保析构函数被调用,防止资源泄漏。
  3. 代码简洁
    • 减少手动管理资源的代码,降低出错概率。
  4. 确定性释放
    • 资源在对象离开作用域时立即释放,行为可预测。

RAII 的注意事项
  1. 避免手动管理
    • 不要在 RAII 对象之外手动释放资源(如 delete ptr.get()),否则可能导致未定义行为。
  2. 析构函数不抛异常
    • RAII 依赖析构函数的调用,析构函数应保证不抛出异常(通常标记为 noexcept)。
    • 如果析构函数抛出异常,可能导致程序终止(std::terminate)。
  3. 拷贝和移动
    • RAII 对象管理独占资源时(如 std::unique_ptr),通常禁用拷贝,允许移动。
    • 如果需要共享资源(如 std::shared_ptr),需明确定义拷贝语义。
  4. 性能开销
    • RAII 对象的构造和析构可能引入少量开销,但通常被其安全性和简洁性抵消。

RAII 与你的线程代码

在你的原始代码中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <thread>
int counter = 0;
void increment() {
for (int i = 0; i < 1000; ++i) {
++counter;
}
}
int main() {
std::thread t1(increment);
std::thread t2(increment);
t1.join();
t2.join();
// ...
}

  • std::thread 本身不是严格的 RAII 对象,因为它不会自动调用 joindetach。如果 t1 未被 joindetach 就离开作用域,程序会调用 std::terminate
  • 改进方式:使用 RAII 封装 std::thread,确保线程总是被正确管理:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    class ThreadRAII {
    std::thread t;
    public:
    explicit ThreadRAII(std::thread&& thread) : t(std::move(thread)) {}
    ~ThreadRAII() {
    if (t.joinable()) t.join(); // 自动 join
    }
    ThreadRAII(const.ConcurrentThreadRAII&) = delete; // 禁用拷贝
    ThreadRAII& operator=(const ThreadRAII&) = delete;
    };

    int main() {
    ThreadRAII t1(std::thread(increment));
    ThreadRAII t2(std::thread(increment));
    // 离开作用域时,t1 和 t2 自动 join
    std::cout << counter << std::endl;
    return 0;
    }
    • ThreadRAII 确保线程在析构时被 join,符合 RAII 原则。

此外,修复数据竞争时使用的 std::lock_guard 是 RAII 的典型应用,确保锁的自动释放。


总结
  • RAII 是 C++ 的核心 idiom,通过将资源管理绑定到对象的构造和析构,实现自动、异常安全的资源管理。
  • 它广泛应用于内存(智能指针)、文件、锁等场景,简化代码并提高可靠性。
  • 关键点:资源在构造函数中获取,析构函数中释放;利用栈解退保证释放。
  • 在你的线程代码中,RAII 可用于管理锁(如 std::lock_guard)或封装线程(如 ThreadRAII),解决数据竞争和线程管理问题。
  • RAII 是现代 C++(如 C++11 及以后)的基石,体现了“用对象管理资源”的哲学。

如果需要更深入的讲解或特定示例,请告诉我!

自动类型推导

  • auto
    • 功能:让编译器根据初始化表达式推导变量类型。
    • 使用场景:简化复杂类型声明,如迭代器。
    • 底层原理:编译时类型推导,不影响运行时。
    • 注意事项:需初始化;不改变类型安全。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      #include <iostream>
      #include <vector>
      int main() {
      auto i = 10; // int
      auto d = 3.14; // double
      std::vector<int> v = {1, 2, 3};
      for (auto it = v.begin(); it != v.end(); ++it) {
      std::cout << *it << " "; // 1 2 3
      }
      std::cout << "\ni: " << i << ", d: " << d << "\n";
      return 0;
      }
  • decltype
    • 功能:提取表达式的类型,用于声明变量。
    • 使用场景:模板编程、类型推导。
    • 底层原理:编译时分析表达式类型。
    • 注意事项:可与 auto 结合使用。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      #include <iostream>
      int main() {
      int x = 10;
      decltype(x) y = 20; // y 是 int
      std::cout << "y: " << y << "\n"; // 20
      decltype(x + 3.14) z = 5.5; // z 是 double
      std::cout << "z: " << z << "\n"; // 5.5
      return 0;
      }

范围 for 循环

  • 功能:基于范围的循环,简化容器遍历。
  • 使用场景:数组、STL 容器遍历。
  • 底层原理:编译器将其转换为迭代器循环。
  • 注意事项:容器需支持 begin()end()
  • 示例代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include <iostream>
    #include <vector>
    int main() {
    std::vector<int> v = {1, 2, 3};
    for (int x : v) { // 按值
    std::cout << x << " "; // 1 2 3
    }
    std::cout << "\n";
    for (int& x : v) { // 按引用修改
    x *= 2;
    }
    for (int x : v) {
    std::cout << x << " "; // 2 4 6
    }
    return 0;
    }

nullptr

  • 功能:替代 NULL,明确表示空指针。
  • 使用场景:初始化指针、检查有效性。
  • 底层原理nullptrnullptr_t 类型,避免整数转换问题。
  • 注意事项:比 NULL(0)更类型安全。
  • 示例代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <iostream>
    void func(int) { std::cout << "int\n"; }
    void func(int*) { std::cout << "pointer\n"; }
    int main() {
    int* ptr = nullptr;
    if (ptr == nullptr) {
    std::cout << "Pointer is null\n";
    }
    // func(NULL); // 歧义,可能调用 int
    func(nullptr); // 明确调用 pointer
    return 0;
    }

智能指针

  • std::unique_ptr
    • 功能:独占所有权的智能指针,自动释放内存。
    • 使用场景:管理单一所有权的动态资源。
    • 底层原理:RAII 封装,析构时调用 delete
    • 注意事项:不可复制,只能移动。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      #include <iostream>
      #include <memory>
      int main() {
      std::unique_ptr<int> uptr = std::make_unique<int>(10);
      std::cout << "Value: " << *uptr << "\n"; // 10
      // std::unique_ptr<int> uptr2 = uptr; // 错误:不可复制
      std::unique_ptr<int> uptr2 = std::move(uptr); // 移动
      if (!uptr) std::cout << "uptr is null\n";
      std::cout << "uptr2: " << *uptr2 << "\n"; // 10
      return 0; // 自动释放
      }
  • std::shared_ptr
    • 功能:共享所有权的智能指针,引用计数管理。
    • 使用场景:多个对象共享资源。
    • 底层原理:引用计数为 0 时释放内存。
    • 注意事项:避免循环引用。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      #include <iostream>
      #include <memory>
      int main() {
      std::shared_ptr<int> sptr1 = std::make_shared<int>(20);
      std::cout << "sptr1: " << *sptr1 << "\n"; // 20
      {
      std::shared_ptr<int> sptr2 = sptr1; // 共享
      std::cout << "sptr2: " << *sptr2 << "\n"; // 20
      std::cout << "Use count: " << sptr1.use_count() << "\n"; // 2
      }
      std::cout << "After scope, sptr1: " << *sptr1 << "\n"; // 20
      return 0; // 引用计数为 0,释放
      }
  • std::weak_ptr
    • 功能:弱引用指针,解决 shared_ptr 循环引用。
    • 使用场景:配合 shared_ptr 管理复杂关系。
    • 底层原理:不增加引用计数,需通过 lock() 获取 shared_ptr
    • 注意事项:需检查有效性。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      #include <iostream>
      #include <memory>
      // struct 在 C++ 中可以像 class 一样使用,包含成员变量、构造函数、析构函数等,struct 默认 public,class 默认 private
      struct Node {
      std::shared_ptr<Node> next;
      std::weak_ptr<Node> prev; // 避免循环引用
      ~Node() { std::cout << "Node destroyed\n"; }
      };
      int main() {
      auto n1 = std::make_shared<Node>();
      auto n2 = std::make_shared<Node>();
      n1->next = n2;
      n2->prev = n1;
      return 0; // 正常销毁
      }

移动语义

  • 右值引用(T&&
    • 功能:绑定到右值(如临时对象),支持移动语义。
    • 使用场景:优化资源转移,避免拷贝。
    • 底层原理:右值引用延长临时对象生命周期。
    • 注意事项:区分左值和右值。
  • 移动构造函数和 std::move
    • 功能:转移资源所有权,减少深拷贝。
    • 底层原理:将资源指针转移,原对象置为可销毁状态。
    • 注意事项:移动后原对象状态需定义。
    • 示例代码:
      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
      #include <iostream>
      #include <utility>
      class MyClass {
      private:
      int* data;
      public:
      MyClass(int val = 0) : data(new int(val)) {
      std::cout << "Constructor\n";
      }
      MyClass(MyClass&& other) noexcept : data(other.data) { // 移动构造函数
      other.data = nullptr;
      std::cout << "Move constructor\n";
      }
      ~MyClass() {
      delete data;
      std::cout << "Destructor\n";
      }
      int get() const { return data ? *data : 0; }
      };
      int main() {
      MyClass a(10);
      MyClass b = std::move(a); // 移动
      std::cout << "a: " << a.get() << ", b: " << b.get() << "\n"; // 0, 10
      return 0;
      }

完美转发

  • 功能:通过 std::forward 和右值引用,保持参数的值类别(左值或右值)。
  • 使用场景:模板函数转发参数。
  • 底层原理:利用引用折叠规则(T&& 可绑定左值或右值)。
  • 注意事项:需与模板配合。
  • 示例代码:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include <iostream>
    #include <utility>
    void process(int& x) { std::cout << "Lvalue: " << x << "\n"; }
    void process(int&& x) { std::cout << "Rvalue: " << x << "\n"; }
    template<typename T>
    void forward(T&& arg) {
    process(std::forward<T>(arg));
    }
    int main() {
    int x = 10;
    forward(x); // Lvalue: 10
    forward(20); // Rvalue: 20
    return 0;
    }

Lambda 表达式

C++ 的 Lambda 表达式是一种便捷的方式,用于在代码中定义匿名函数对象。它在 C++11 中引入,广泛用于简化回调、函数式编程以及需要临时函数的场景。

  • 功能:定义匿名函数,支持捕获外部变量。

  • 使用场景:回调、局部逻辑。

  • 底层原理:编译器生成闭包类。

  • 注意事项:捕获方式影响变量生命周期。

  • 示例代码:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include <iostream>
    int main() {
    int x = 10;
    auto func = [x]() { return x * 2; }; // 按值捕获
    std::cout << "By value: " << func() << "\n"; // 20
    auto refFunc = [&x]() { return x * 2; }; // 按引用捕获
    x = 20;
    std::cout << "By reference: " << refFunc() << "\n"; // 40
    return 0;
    }

1. Lambda 表达式的基本语法

Lambda 表达式的完整语法如下:

1
[capture](parameters) mutable -> return_type { body }
  • [capture](捕获列表):指定外部变量如何被 Lambda 表达式捕获(按值或按引用)。
  • (parameters)(参数列表):类似普通函数的参数,定义 Lambda 接受的输入。
  • mutable(可选):允许在按值捕获时修改捕获的变量(默认按值捕获是只读的)。
  • -> return_type(返回类型,可选):显式指定返回类型,通常由编译器推导。
  • { body }(函数体):Lambda 的实现逻辑。

2. 捕获列表详解

捕获列表决定了 Lambda 如何访问外部作用域的变量。捕获方式有以下几种:

(1) 按值捕获 [x]
  • 外部变量被复制到 Lambda 内部,Lambda 持有该变量的副本。
  • 默认情况下,按值捕获的变量是只读的,不能修改。
  • 如果需要修改副本,可以使用 mutable 关键字,但不会影响外部变量。

示例:

1
2
3
4
5
int x = 10;
auto func = [x]() { return x * 2; }; // x 是副本
std::cout << func() << "\n"; // 输出 20
x = 20;
std::cout << func() << "\n"; // 依然输出 20,因为 func 内部的 x 是副本

(2) 按引用捕获 [&x]
  • Lambda 直接引用外部变量,修改 Lambda 内部的变量会影响外部变量。
  • 如果外部变量被销毁(例如离开作用域),Lambda 引用它会导致未定义行为(悬垂引用)。

示例:

1
2
3
4
int x = 10;
auto refFunc = [&x]() { return x * 2; }; // x 是引用
x = 20;
std::cout << refFunc() << "\n"; // 输出 40,因为 refFunc 引用了修改后的 x

(3) 全局捕获
  • [=]:按值捕获所有外部变量的副本。
  • [&]:按引用捕获所有外部变量。
  • 混合捕获:可以组合,例如 [=, &x] 表示默认按值捕获,但 x 按引用捕获。

示例:

1
2
3
4
int x = 10, y = 5;
auto mixed = [=, &x]() { return x + y; }; // y 按值,x 按引用
x = 20;
std::cout << mixed() << "\n"; // 输出 25(x=20, y=5 的副本)

(4) 捕获 this
  • 在类成员函数中,[this] 捕获当前对象的指针,[*this](C++17 起)捕获当前对象的副本。
  • 按引用捕获 [&] 隐式包含 this

示例:

1
2
3
4
5
6
7
struct Example {
int x = 10;
void func() {
auto lambda = [this]() { return x * 2; };
std::cout << lambda() << "\n"; // 输出 20
}
};

(5) 空捕获 []

示例:

1
auto callback = [](int x) { std::cout << "Callback: " << x << "\n"; };

Lambda 表达式的捕获列表是 [],表示空捕获,即不捕获任何外部变量,既不是按值捕获也不是按引用捕获。

空捕获列表 ([]):[] 表示 Lambda 表达式不从外部作用域捕获任何变量。

无外部变量引用:Lambda 的函数体 { std::cout << “Callback:” << x << “”; } 只使用了参数 x(通过函数调用传入)和全局对象 std::cout。std::cout 是全局的,不需要捕获,而 x 是 Lambda 的参数,不是外部作用域的变量。

捕获(按值 [=] 或按引用 [&])只有在 Lambda 访问外部作用域的变量时才起作用。例如,如果 Lambda 使用了外部的 int y,才会涉及捕获方式。


3. Lambda 的工作原理

Lambda 表达式实际上是编译器生成的匿名类的实例(称为闭包对象)。例如:

1
auto func = [x]() { return x * 2; };

编译器会生成类似以下的类:

1
2
3
4
5
6
class Lambda {
int x; // 捕获的变量
public:
Lambda(int x_) : x(x_) {}
int operator()() const { return x * 2; } // 重载函数调用操作符,operator() 是重载的函数调用操作符,() 表示这个操作符不接受参数(空参数列表),int 是返回值类型,表示调用这个操作符会返回一个整数,const 表示这个成员函数不会修改对象的状态(x 不会被改变)。
};

调用 func() 实际上是调用这个类的 operator()。这解释了 Lambda 为什么可以像函数一样使用。


4. Lambda 的常见用途
  1. 标准库算法:与 <algorithm> 配合,例如 std::sortstd::for_each

    1
    2
    std::vector<int> vec = {3, 1, 4, 1, 5};
    std::sort(vec.begin(), vec.end(), [](int a, int b) { return a < b; });
  2. 异步编程:与 std::async 或线程配合。

    1
    2
    auto task = []() { std::cout << "Running task\n"; };
    std::async(std::launch::async, task);

  3. 回调函数:传递给需要回调的函数。

    1
    2
    auto callback = [](int x) { std::cout << "Callback: " << x << "\n"; };// 
    someFunction(callback);

  4. 立即执行(IIFE,Immediately Invoked Function Expression):

    1
    int result = []() { return 42; }();  // 立即调用,result = 42


5. 注意事项
  1. 生命周期问题

    • 按引用捕获时,确保捕获的变量在 Lambda 使用时仍然有效。
    • 示例(错误用法):
      1
      2
      3
      4
      auto createLambda() {
      int x = 10;
      return [&x]() { return x; }; // 悬垂引用,x 在函数返回后销毁
      }
  2. 性能开销

    • 按值捕获会复制变量,可能会增加内存开销。
    • 对于大对象,考虑按引用捕获或使用 std::move(C++11 起支持移动捕获,C++14 增强)。
  3. C++14/17 增强

    • C++14:支持泛型 Lambda(auto 参数)和初始化捕获。

      1
      auto lambda = [y = 10](auto x) { return x + y; };  // 初始化捕获
    • C++17:支持 [*this] 捕获对象副本。

      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
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      #include <iostream>
      #include <functional>

      struct Example {
      int x = 10;

      void createLambda() {
      // 使用 [*this] 捕获对象副本
      auto lambda = [*this]() {
      std::cout << "Lambda: x = " << x << "\n";
      // 修改 x 不影响原始对象
      x = 20;
      std::cout << "Lambda modified: x = " << x << "\n";
      };

      // 调用 Lambda
      lambda();

      // 原始对象的 x 未改变
      std::cout << "Original: x = " << x << "\n";
      }

      // 模拟异步回调
      std::function<void()> createAsyncCallback() {
      // 返回 Lambda,捕获 [*this]
      return [*this]() {
      std::cout << "Async callback: x = " << x << "\n";
      };
      }
      };

      int main() {
      // 测试 [*this] 捕获
      Example obj;
      obj.createLambda();

      // 测试异步场景
      auto callback = obj.createAsyncCallback();
      // obj 销毁后,callback 仍然有效,因为它持有 obj 的副本
      callback();

      return 0;
      }
    • C++20:支持无状态 Lambda 的默认构造和赋值。

      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
      32
      #include <iostream>
      #include <vector>
      #include <optional>

      int main() {
      // 定义一个无状态 Lambda,lambda 不捕获任何变量([]),因此是无状态的,行为完全由函数体 { return 42; } 定义。
      auto lambda = []() { return 42; };

      // 默认构造无状态 Lambda(C++20),decltype(lambda) defaultLambda; 创建一个默认构造的闭包对象,行为与 lambda 相同。
      decltype(lambda) defaultLambda; // 默认构造
      std::cout << "Default constructed Lambda: " << defaultLambda() << "\n";

      // 赋值(C++20),assignedLambda = lambda; 将 lambda 的行为复制到 assignedLambda,这是 C++20 新增的功能。
      decltype(lambda) assignedLambda;
      assignedLambda = lambda; // 赋值操作
      std::cout << "Assigned Lambda: " << assignedLambda() << "\n";

      // 存储在容器中,std::vector 和 std::optional 可以存储无状态 Lambda,因为它们支持默认构造和赋值。
      std::vector<decltype(lambda)> lambdaVector(3); // 默认构造 3 个 Lambda
      for (const auto& l : lambdaVector) {
      std::cout << "Vector Lambda: " << l() << "\n";
      }

      // 使用 std::optional
      std::optional<decltype(lambda)> optionalLambda;
      optionalLambda = lambda; // 赋值
      if (optionalLambda) {
      std::cout << "Optional Lambda: " << optionalLambda.value()() << "\n";
      }

      return 0;
      }

      如果 Lambda 捕获变量(有状态 Lambda),则无法使用默认构造或赋值,因为它们的行为依赖捕获的变量。

      compiling
      1
      2
      3
      4
      int x = 10;
      auto statefulLambda = [x]() { return x; };
      decltype(statefulLambda) defaultLambda; // 错误:无默认构造函数
      statefulLambda = statefulLambda; // 错误:无赋值操作符
  4. mutable 关键字

    • 按值捕获默认只读,使用 mutable 允许修改副本。
      1
      2
      3
      4
      int x = 10;
      auto func = [x]() mutable { x += 1; return x; };
      std::cout << func() << "\n"; // 输出 11
      std::cout << x << "\n"; // 输出 10,外部 x 不变

模板改进

  • 可变参数模板
    • 功能:支持不定数量的模板参数。
    • 使用场景:通用函数,如打印。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      #include <iostream>
      template<typename... Args>
      void print(Args... args) {
      (std::cout << ... << args); // C++17 折叠表达式
      }
      int main() {
      print(1, " ", 2.5, " ", "hello"); // 1 2.5 hello
      std::cout << "\n";
      return 0;
      }
  • 模板别名
    • 功能:使用 using 定义模板类型别名。
    • 使用场景:简化复杂类型。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      #include <iostream>
      #include <vector>
      template<typename T>
      using Vec = std::vector<T>;
      int main() {
      Vec<int> v = {1, 2, 3};
      for (auto x : v) std::cout << x << " "; // 1 2 3
      std::cout << "\n";
      return 0;
      }

初始化改进

  • 统一初始化
    • 功能:使用 {} 初始化所有类型。
    • 使用场景:一致性初始化。
    • 注意事项:窄化转换(如 double 到 int)被禁止。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      #include <iostream>
      int main() {
      int x{10};
      double d{3.14};
      std::cout << "x: " << x << ", d: " << d << "\n";
      // int y{3.14}; // 错误:窄化转换
      return 0;
      }
  • 初始化列表
    • 功能:用 {} 初始化容器或对象。
    • 示例代码:
      1
      2
      3
      4
      5
      6
      7
      8
      #include <iostream>
      #include <vector>
      int main() {
      std::vector<int> v{1, 2, 3};
      for (auto x : v) std::cout << x << " "; // 1 2 3
      std::cout << "\n";
      return 0;
      }

并发支持

C++ 的并发控制是现代 C++(特别是 C++11 及之后)的一个重要特性,旨在支持多线程编程并确保线程安全。并发控制涉及管理多个线程对共享资源的访问,避免数据竞争(data race)、死锁(deadlock)等问题,同时最大化性能。


1. 并发控制的核心概念
(1) 线程(Threads)
  • C++11 引入了 <thread> 头文件,支持原生线程管理。
  • 线程表示并行执行的独立控制流,多个线程可能同时访问共享资源。
  • 问题:未经同步的共享资源访问可能导致数据竞争。
(2) 数据竞争(Data Race)
  • 当多个线程同时访问共享资源(例如变量),且至少一个线程是写操作时,可能导致未定义行为。
  • 示例:
    1
    2
    3
    4
    5
    6
    7
    8
    9
    int counter = 0;
    void increment() {
    for (int i = 0; i < 1000; ++i)
    ++counter; // counter 可能不是 2000,因为 ++counter 不是原子操作
    }
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();

std::thread 构造函数接受一个可调用对象(如函数、函数指针、lambda 表达式等)以及可选的参数。语法:std::thread variable_name(callable, args...)。在 thread t1(increment); 中,increment 是一个无参函数,因此没有额外参数。

构造 t1 时,std::thread 会分配一个新的线程(由操作系统管理),并在该线程中调用 increment()。新线程立即开始运行,除非系统资源受限。

t1 的生命周期从构造开始,直到线程执行完成或程序结束。如果 t1 未被 joindetach,程序在 t1 析构时(例如离开作用域)会调用 std::terminate,导致程序崩溃。t1.join() 确保主线程等待 t1 完成,避免此问题。

thread t1(increment); 而不是 thread t1(increment()); 是因为后者可能被解析为函数声明(称为“最令人头痛的解析”,most vexing parse)。thread t1(increment); 明确表示创建一个线程对象,调用 increment 函数。

increment 可以是任何可调用对象,例如:

  • 函数指针:void (*func)() = increment; thread t1(func);

  • Lambda 表达式:thread t1([](){ /* 代码 */ });

  • 函数对象(functor):thread t1(MyFunctor());

  • 如果 increment 需要参数,需在构造时提供:

    1
    2
    void increment(int n) { /* ... */ }
    thread t1(increment, 42); // 传递参数 42

如果 increment 抛出异常,线程会终止,异常不会传播到主线程。需在 increment 内部捕获异常,或使用其他机制(如 std::future)处理。

counter 是全局变量,被 t1t2 共享。++counter 不是原子操作,即可能被其他线程中断,它实际上涉及三个步骤:

  1. 读取 counter 的当前值。
  2. 将值加 1。
  3. 将新值写回 counter

要确保 counter 总是 2000,需要消除数据竞争。常见方法:

  1. 使用互斥锁(Mutex)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    #include <mutex>
    mutex mtx;
    void increment() {
    for (int i = 0; i < 1000; ++i) {
    mtx.lock();
    ++counter;
    mtx.unlock();
    }
    }
    • mutex 确保每次只有 一个线程能访问 counter,避免竞争。
    • 缺点:频繁加锁解锁可能降低性能。
  2. 使用原子操作

    1
    2
    3
    4
    5
    6
    7
    #include <atomic>
    atomic<int> counter = 0;
    void increment() {
    for (int i = 0; i < 1000; ++i) {
    ++counter;
    }
    }
    • std::atomic<int> 提供原子操作,保证 ++counter 不被中断。
    • 更高效,适合简单计数器场景。
  3. 减少锁的粒度

    • 将整个循环放在锁内(而不是每次迭代都加锁)可以减少加锁开销:
      1
      2
      3
      4
      5
      6
      7
      void increment() {
      mtx.lock();
      for (int i = 0; i < 1000; ++i) {
      ++counter;
      }
      mtx.unlock();
      }
(3) 同步原语(Synchronization Primitives)

C++ 提供了多种工具来控制并发:

  • 互斥锁(Mutex):防止多个线程同时访问共享资源。
  • 条件变量(Condition Variable):协调线程间的等待和通知。
  • 原子操作(Atomic Operations):无锁的线程安全操作。
  • 未来(Future)和承诺(Promise):异步任务的结果传递。
  • 线程局部存储(Thread-Local Storage):每个线程独立的数据。

2. C++ 并发控制的主要工具
(1) 互斥锁(<mutex>

互斥锁用于保护共享资源,确保一次只有一个线程访问临界区。

  • 常用类型

    • std::mutex:基本互斥锁。
    • std::recursive_mutex:允许同一线程多次锁定。
    • std::timed_mutex:支持超时。
    • std::shared_mutex(C++17):支持读写锁(多个读线程或单个写线程)。
  • 锁管理工具

    • std::lock_guard:RAII 风格的锁,自动解锁。
    • std::unique_lock:更灵活的锁,支持延迟锁定或转移。
    • std::scoped_lock(C++17):简化多锁管理,避免死锁。
  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    #include <iostream>
    #include <thread>
    #include <mutex>

    std::mutex mtx;
    int counter = 0;

    void increment() {
    for (int i = 0; i < 1000; ++i) {
    std::lock_guard<std::mutex> lock(mtx); // 自动锁定和解锁
    ++counter;
    }
    }

    int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    std::cout << "Counter: " << counter << "\n"; // 输出 2000
    return 0;
    }

  • 注意

    • 避免死锁:使用 std::scoped_lock 或按固定顺序加锁。
    • 最小化锁的范围以提高性能。
(2) 条件变量(<condition_variable>

条件变量用于线程间的同步,允许一个线程等待特定条件,另一个线程通知条件满足。

  • 常用类型

    • std::condition_variable:与 std::mutex 配合使用。
    • std::condition_variable_any:支持任意锁类型。
  • 示例(生产者-消费者模型):

    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
    32
    33
    34
    35
    36
    37
    38
    #include <iostream>
    #include <queue>
    #include <thread>
    #include <mutex>
    #include <condition_variable>

    std::queue<int> q;
    std::mutex mtx;
    std::condition_variable cv;

    void producer() {
    for (int i = 1; i <= 5; ++i) {
    std::lock_guard<std::mutex> lock(mtx);
    q.push(i);
    std::cout << "Produced: " << i << "\n";
    cv.notify_one(); // 通知消费者
    }
    }

    void consumer() {
    while (true) {
    std::unique_lock<std::mutex> lock(mtx);
    cv.wait(lock, [] { return !q.empty(); }); // 等待队列非空
    int value = q.front();
    q.pop();
    lock.unlock();
    std::cout << "Consumed: " << value << "\n";
    if (value == 5) break;
    }
    }

    int main() {
    std::thread t1(producer);
    std::thread t2(consumer);
    t1.join();
    t2.join();
    return 0;
    }

  • 注意

    • 使用 std::unique_lock 与条件变量配合。
    • 避免虚假唤醒(spurious wakeup),使用条件检查(如 [] { return !q.empty(); })。
(3) 原子操作(<atomic>

原子操作提供无锁的线程安全操作,适合简单数据类型(如计数器)。

  • 常用类型

    • std::atomic<T>:支持整数、指针等类型的原子操作。
    • std::atomic_flag:最简单的原子标志。
  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    #include <iostream>
    #include <thread>
    #include <atomic>

    std::atomic<int> counter(0);

    void increment() {
    for (int i = 0; i < 1000; ++i) {
    counter.fetch_add(1, std::memory_order_relaxed); // 原子递增
    }
    }

    int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    std::cout << "Counter: " << counter << "\n"; // 输出 2000
    return 0;
    }

  • 内存序(Memory Order)

    • std::memory_order_relaxed:最低性能约束。
    • std::memory_order_seq_cst:默认,强一致性。
    • 选择合适的内存序以平衡性能和正确性。
(4) 未来和承诺(<future>

std::futurestd::promise 用于异步任务的结果传递。

  • 组件

    • std::promise:设置任务结果。
    • std::future:获取任务结果。
    • std::async:异步执行函数。
  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include <iostream>
    #include <future>

    int compute(int x) {
    return x * 2;
    }

    int main() {
    // 异步执行
    std::future<int> result = std::async(std::launch::async, compute, 10);
    std::cout << "Result: " << result.get() << "\n"; // 输出 20
    return 0;
    }

  • 注意

    • std::async 的启动策略(std::launch::asyncstd::launch::deferred)影响执行时机。
    • std::future::get() 阻塞直到结果可用。
(5) 线程局部存储(<thread>

thread_local 变量为每个线程提供独立副本,避免共享资源竞争。

  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    #include <iostream>
    #include <thread>

    thread_local int tls_counter = 0;

    void increment() {
    ++tls_counter;
    std::cout << "Thread " << std::this_thread::get_id() << ": " << tls_counter << "\n";
    }

    int main() {
    std::thread t1(increment);
    std::thread t2(increment);
    t1.join();
    t2.join();
    return 0;
    }

  • 输出(可能):

    1
    2
    Thread 1234: 1
    Thread 5678: 1


3. C++17 和 C++20 的并发改进
C++17
  • 并行算法<algorithm>):
    • 支持并行执行标准算法,例如 std::sort
      1
      2
      3
      4
      #include <algorithm>
      #include <execution>
      std::vector<int> vec = {3, 1, 4, 1, 5};
      std::sort(std::execution::par, vec.begin(), vec.end());
    • 执行策略:std::execution::seq(顺序)、par(并行)、par_unseq(并行无序)。
  • 共享锁std::shared_mutex):
    • 支持读写锁,允许多个读线程同时访问:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      std::shared_mutex smtx;
      int data = 0;
      void reader() {
      std::shared_lock lock(smtx); // 读锁
      std::cout << "Read: " << data << "\n";
      }
      void writer() {
      std::unique_lock lock(smtx); // 写锁
      ++data;
      }
C++20
  • 信号量(<semaphore>
    • 提供轻量级同步,例如 std::counting_semaphore
      1
      2
      3
      4
      5
      6
      7
      #include <semaphore>
      std::counting_semaphore<1> sem(1);
      void task() {
      sem.acquire();
      std::cout << "Task running\n";
      sem.release();
      }
  • 屏障(<barrier>
    • 协调多个线程到达同步点:
      1
      2
      3
      4
      5
      6
      7
      #include <barrier>
      std::barrier barrier(2);
      void task(int id) {
      std::cout << "Thread " << id << " phase 1\n";
      barrier.arrive_and_wait();
      std::cout << "Thread " << id << " phase 2\n";
      }
  • 锁存器(<latch>
    • 单次同步,线程等待计数归零:
      1
      2
      3
      4
      5
      6
      #include <latch>
      std::latch latch(2);
      void task(int id) {
      std::cout << "Thread " << id << " done\n";
      latch.count_down();
      }
  • 协作中断(<jthread>
    • std::jthread 自动加入线程,支持中断:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      #include <jthread>
      void task(std::stop_token token) {
      while (!token.stop_requested()) {
      std::cout << "Running...\n";
      std::this_thread::sleep_for(std::chrono::milliseconds(100));
      }
      }
      int main() {
      std::jthread t(task);
      std::this_thread::sleep_for(std::chrono::seconds(1));
      t.request_stop();
      return 0;
      }

4. 注意事项
  1. 死锁:多锁时使用 std::scoped_lock 或固定加锁顺序。
  2. 性能:优先考虑原子操作或无锁结构,减少锁争用。
  3. 异常安全:使用 RAII(如 std::lock_guard)确保锁在异常时释放。
  4. 调试:使用工具(如 ThreadSanitizer)检测数据竞争。
  5. C++ 版本:确保编译器支持目标特性(例如 -std=c++20)。

5. 总结
  • 核心工具:互斥锁、条件变量、原子操作、未来/承诺、线程局部存储。
  • C++17 改进:并行算法、std::shared_mutex
  • C++20 增强:信号量、屏障、锁存器、std::jthread

constexpr

  • 功能:定义编译时常量或函数。
  • 使用场景:优化性能、静态断言。
  • 示例代码:
    1
    2
    3
    4
    5
    6
    7
    #include <iostream>
    constexpr int square(int x) { return x * x; }
    int main() {
    int arr[square(3)]; // 编译时计算 9
    std::cout << "Array size: " << sizeof(arr) / sizeof(int) << "\n"; // 9
    return 0;
    }

C++14

C++14 是 C++11 的增量更新,增强了语言的易用性和表达能力。

泛型 Lambda

  • 功能:允许 Lambda 表达式的参数使用 auto,使其支持泛型。
  • 使用场景:需要处理多种类型的匿名函数,如通用回调。
  • 底层原理:编译器为 Lambda 生成一个模板化的闭包类,每个类型实例化一个具体函数。
  • 注意事项:提高了代码灵活性,但可能增加编译时间;需确保参数类型支持 Lambda 体内的操作。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include <iostream>
    int main() {
    auto genericLambda = [](auto x) { // 参数 x 是泛型的
    return x + 1;
    };
    std::cout << "Int: " << genericLambda(5) << "\n"; // 6
    std::cout << "Double: " << genericLambda(3.14) << "\n"; // 4.14
    std::cout << "Char: " << genericLambda('A') << "\n"; // 'B' (ASCII 66)
    return 0;
    }

返回类型推导

  • 功能:允许函数使用 auto 作为返回类型,由函数体推导。
  • 使用场景:简化函数声明,尤其在返回值类型复杂时。
  • 底层原理:编译器根据 return 语句推导类型,所有返回路径必须一致。
  • 注意事项:不能用于声明(需定义);递归函数需显式返回类型。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <iostream>
    auto add(int a, int b) { // 返回类型推导为 int
    return a + b;
    }
    auto multiply(double a, double b) { // 返回类型推导为 double
    return a * b;
    }
    int main() {
    std::cout << "Add: " << add(2, 3) << "\n"; // 5
    std::cout << "Multiply: " << multiply(2.5, 3.0) << "\n"; // 7.5
    return 0;
    }

constexpr 扩展

  • 功能:扩展 constexpr 函数,支持更复杂的编译时计算(如循环、条件语句)。
  • 使用场景:需要编译时计算复杂表达式,如数学函数。
  • 底层原理:编译器在编译时执行函数,确保结果为常量。
  • 注意事项:函数体内限制放宽,但仍需满足常量表达式要求(如无动态内存分配)。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include <iostream>
    constexpr int factorial(int n) { // 编译时计算阶乘
    int result = 1;
    for (int i = 1; i <= n; ++i) { // C++14 允许循环
    result *= i;
    }
    return result;
    }
    int main() {
    int arr[factorial(4)]; // 编译时计算 24
    std::cout << "Array size: " << sizeof(arr) / sizeof(int) << "\n"; // 24
    std::cout << "Factorial(4): " << factorial(4) << "\n"; // 24
    return 0;
    }

变量模板

  • 功能:允许定义模板化的变量,提供类型参数化的常量。
  • 使用场景:泛型常量,如类型相关的数学常数。
  • 底层原理:编译器为每种类型实例化变量。
  • 注意事项:需显式指定类型或推导。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    #include <iostream>
    template<typename T>
    constexpr T pi = T(3.1415926535); // 变量模板
    int main() {
    std::cout << "Float pi: " << pi<float> << "\n"; // 3.14159
    std::cout << "Double pi: " << pi<double> << "\n"; // 3.14159
    return 0;
    }

C++17

C++17 引入了更多实用特性,提升了语言的现代化程度。

结构化绑定

  • 功能:解构赋值,将聚合类型(如 pairtuple、结构体)的成员绑定到变量。
  • 使用场景:简化多返回值函数的使用。
  • 底层原理:编译器生成临时对象并解构,绑定到新变量。
  • 注意事项:需支持结构化绑定的类型(如 std::pair 或含 std::tuple_size 的类型)。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <iostream>
    #include <utility>
    int main() {
    std::pair<int, double> p(1, 2.5);
    auto [x, y] = p; // 结构化绑定
    std::cout << "x: " << x << ", y: " << y << "\n"; // 1, 2.5
    struct Point { int a; double b; };
    Point pt{3, 4.5};
    auto [a, b] = pt;
    std::cout << "a: " << a << ", b: " << b << "\n"; // 3, 4.5
    return 0;
    }

if 和 switch 初始化

  • 功能:允许在 ifswitch 语句中初始化变量,限制作用域。
  • 使用场景:临时变量仅用于条件判断。
  • 底层原理:编译器将初始化和条件组合为单一语句。
  • 注意事项:变量作用域限于 ifswitch 块。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <iostream>
    int main() {
    if (int x = 10; x > 0) { // 初始化并判断
    std::cout << "x is positive: " << x << "\n"; // 10
    }
    // std::cout << x << "\n"; // 错误:x 超出作用域
    switch (int y = 2; y) { // 初始化并切换
    case 2: std::cout << "y is 2\n"; break;
    default: std::cout << "Other\n";
    }
    return 0;
    }

折叠表达式

  • 功能:简化可变参数模板的处理,允许对参数包进行一元或二元操作。
  • 使用场景:批量操作参数,如求和、打印。
  • 底层原理:编译器展开参数包并应用操作符。
  • 注意事项:支持常见运算符(如 +*&& 等)。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include <iostream>
    template<typename... Args>
    auto sum(Args... args) {
    return (args + ...); // 二元左折叠
    }
    template<typename... Args>
    void print(Args... args) {
    (std::cout << ... << args); // 打印所有参数
    }
    int main() {
    std::cout << "Sum: " << sum(1, 2, 3, 4) << "\n"; // 10
    print("Hello", " ", 42, "\n"); // Hello 42
    return 0;
    }

std::optional

  • 功能:表示可能为空的值,提供类型安全的可选值。
  • 使用场景:函数可能无返回值,或值可选。
  • 底层原理:封装值和状态,析构时自动清理。
  • 注意事项:需检查是否有值(has_value()*)。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    #include <iostream>
    #include <optional>
    std::optional<int> maybeInt(int x) {
    if (x > 0) return x;
    return std::nullopt; // 无值
    }
    int main() {
    auto opt1 = maybeInt(5);
    if (opt1) std::cout << "Value: " << *opt1 << "\n"; // 5
    auto opt2 = maybeInt(-1);
    if (!opt2) std::cout << "No value\n"; // No value
    return 0;
    }

std::variant

  • 功能:类型安全的联合类型,可存储多种类型之一。
  • 使用场景:替代 union,如状态机。
  • 底层原理:存储当前类型索引和值,析构时调用正确析构函数。
  • 注意事项:访问需使用 std::getstd::visit,错误访问抛异常。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <iostream>
    #include <variant>
    int main() {
    std::variant<int, double, std::string> v = 42;
    std::cout << "Int: " << std::get<int>(v) << "\n"; // 42
    v = 3.14;
    std::cout << "Double: " << std::get<double>(v) << "\n"; // 3.14
    v = "Hello";
    std::cout << "String: " << std::get<std::string>(v) << "\n"; // Hello
    // std::get<int>(v); // 抛出 std::bad_variant_access
    return 0;
    }

std::any

  • 功能:存储任意类型的值,提供类型擦除。
  • 使用场景:需要动态类型的场景,如脚本引擎。
  • 底层原理:使用类型擦除技术,存储值和类型信息。
  • 注意事项:访问需使用 std::any_cast,类型错误抛异常。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    #include <iostream>
    #include <any>
    int main() {
    std::any a = 42;
    std::cout << "Int: " << std::any_cast<int>(a) << "\n"; // 42
    a = 3.14;
    std::cout << "Double: " << std::any_cast<double>(a) << "\n"; // 3.14
    a = std::string("Hello");
    std::cout << "String: " << std::any_cast<std::string>(a) << "\n"; // Hello
    try {
    std::any_cast<int>(a); // 抛出 std::bad_any_cast
    } catch (const std::bad_any_cast& e) {
    std::cout << "Bad cast: " << e.what() << "\n";
    }
    return 0;
    }

文件系统库(std::filesystem

  • 功能:提供文件和目录操作的标准化接口。
  • 使用场景:文件管理、路径处理。
  • 底层原理:封装操作系统文件 API。
  • 注意事项:需链接文件系统库(如 -lstdc++fs 在某些编译器)。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    #include <iostream>
    #include <filesystem>
    namespace fs = std::filesystem;
    int main() {
    fs::path p = "example.txt";
    if (fs::exists(p)) {
    std::cout << p << " exists\n";
    } else {
    std::cout << p << " does not exist\n";
    }
    for (const auto& entry : fs::directory_iterator(".")) {
    std::cout << entry.path() << "\n"; // 列出当前目录
    }
    return 0;
    }

并行算法

  • 功能:STL 算法支持并行执行,优化多核性能。
  • 使用场景:大数据排序、变换。
  • 底层原理:依赖线程池或底层并发支持。
  • 注意事项:需编译器支持(如 -ltbb)。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include <iostream>
    #include <vector>
    #include <algorithm>
    #include <execution>
    int main() {
    std::vector<int> v = {3, 1, 4, 1, 5};
    std::sort(std::execution::par, v.begin(), v.end()); // 并行排序
    for (auto x : v) std::cout << x << " "; // 1 1 3 4 5
    std::cout << "\n";
    return 0;
    }

inline 变量

  • 功能:允许在头文件中定义 inline 静态变量,避免多重定义。
  • 使用场景:头文件中的全局常量。
  • 底层原理:编译器保证单一实例。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // header.h
    #ifndef HEADER_H
    #define HEADER_H
    inline int globalVar = 42;
    #endif
    // main.cpp
    #include <iostream>
    #include "header.h"
    int main() {
    std::cout << "globalVar: " << globalVar << "\n"; // 42
    return 0;
    }

C++20

C++20 是近年来最大的更新,引入了许多革新特性。

概念(Concepts)

  • 功能:约束模板参数,提供类型检查。
  • 使用场景:泛型编程中确保类型满足要求。
  • 底层原理:编译时验证类型特性。
  • 注意事项:需支持 C++20 的编译器。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <iostream>
    #include <concepts>
    template<typename T>
    requires std::integral<T> // 约束 T 为整数类型
    T add(T a, T b) {
    return a + b;
    }
    int main() {
    std::cout << "Int: " << add(2, 3) << "\n"; // 5
    // add(2.5, 3.5); // 错误:double 不满足 integral
    return 0;
    }

Ranges 库

  • 功能:提供范围操作,增强 STL 的功能性。
  • 使用场景:链式处理容器数据。
  • 底层原理:基于迭代器,新增视图概念。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    #include <iostream>
    #include <vector>
    #include <ranges>
    int main() {
    std::vector<int> v = {3, 1, 4, 1, 5};
    auto even = v | std::views::filter([](int x) { return x % 2 == 0; });
    std::ranges::sort(v);
    std::cout << "Sorted: ";
    for (auto x : v) std::cout << x << " "; // 1 1 3 4 5
    std::cout << "\nEven: ";
    for (auto x : even) std::cout << x << " "; // 4
    std::cout << "\n";
    return 0;
    }

协程(Coroutines)

  • 功能:支持异步编程,允许函数暂停和恢复。
  • 使用场景:异步 I/O、生成器。
  • 底层原理:编译器生成状态机,依赖协程框架。
  • 注意事项:需库支持(如 cppcoro),示例简化。
  • 示例代码
    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
    #include <iostream>
    #include <coroutine>
    struct Generator {
    struct promise_type {
    int value;
    std::suspend_always initial_suspend() { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    void unhandled_exception() {}
    Generator get_return_object() { return {this}; }
    std::suspend_always yield_value(int v) { value = v; return {}; }
    void return_void() {}
    };
    std::coroutine_handle<promise_type> coro;
    Generator(promise_type* p) : coro(std::coroutine_handle<promise_type>::from_promise(*p)) {}
    ~Generator() { if (coro) coro.destroy(); }
    int next() { coro.resume(); return coro.promise().value; }
    };
    Generator generate() {
    for (int i = 0; i < 3; ++i) {
    co_yield i; // 暂停并返回值
    }
    }
    int main() {
    auto g = generate();
    for (int i = 0; i < 3; ++i) {
    std::cout << g.next() << " "; // 0 1 2
    }
    std::cout << "\n";
    return 0;
    }

模块(Modules)

  • 功能:替代头文件,提供模块化编程。
  • 使用场景:大型项目,减少编译依赖。
  • 底层原理:编译器生成模块单元,优化编译。
  • 注意事项:需 C++20 支持,主流编译器支持尚不完善。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // hello.cppm
    export module hello;
    export void say_hello() {
    std::cout << "Hello from module\n";
    }
    // main.cpp
    import hello;
    int main() {
    say_hello(); // Hello from module
    return 0;
    }

三路比较运算符(<=>

  • 功能:提供默认比较运算符,返回比较结果。
  • 使用场景:简化自定义类型的比较。
  • 底层原理:返回 std::strong_ordering 等类型。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    #include <iostream>
    #include <compare>
    struct Point {
    int x;
    auto operator<=>(const Point& other) const = default; // 默认比较
    };
    int main() {
    Point p1{1}, p2{2};
    std::cout << "p1 < p2: " << (p1 < p2) << "\n"; // 1
    std::cout << "p1 == p2: " << (p1 == p2) << "\n"; // 0
    return 0;
    }

consteval 和 constinit

  • 功能
    • consteval:强制编译时计算。
    • constinit:控制常量初始化。
  • 使用场景:编译时优化、初始化控制。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    #include <iostream>
    consteval int square(int x) { return x * x; } // 必须编译时计算
    constinit int global = square(5); // 确保编译时初始化
    int main() {
    std::cout << "Square(3): " << square(3) << "\n"; // 9
    std::cout << "Global: " << global << "\n"; // 25
    return 0;
    }

新工具

  • std::span
    • 功能:提供连续内存的视图。
    • 使用场景:操作数组或容器片段。
    • 示例代码
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      #include <iostream>
      #include <span>
      void print(std::span<int> s) {
      for (auto x : s) std::cout << x << " ";
      std::cout << "\n";
      }
      int main() {
      int arr[] = {1, 2, 3, 4, 5};
      std::span<int> s(arr, 3); // 前 3 个元素
      print(s); // 1 2 3
      return 0;
      }
  • std::bit_cast
    • 功能:类型安全的位转换。
    • 使用场景:低级数据处理。
    • 示例代码
      1
      2
      3
      4
      5
      6
      7
      8
      #include <iostream>
      #include <bit>
      int main() {
      float f = 1.0f;
      uint32_t i = std::bit_cast<uint32_t>(f);
      std::cout << "Float as uint32_t: " << i << "\n"; // 1065353216
      return 0;
      }

日历和时区支持(std::chrono 扩展)

  • 功能:支持日期和时区操作。
  • 使用场景:时间相关应用。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    #include <iostream>
    #include <chrono>
    int main() {
    using namespace std::chrono;
    auto now = system_clock::now();
    std::cout << "Now: " << now.time_since_epoch().count() << "\n";
    auto today = floor<days>(now);
    std::cout << "Year: " << year_month_day{today}.year() << "\n";
    return 0;
    }

3. 其他特性总结

标准库扩展

  • 功能:STL 提供容器(如 vector)、算法、I/O 等。
  • 使用场景:日常编程。
  • 示例代码
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    #include <iostream>
    #include <vector>
    #include <string>
    int main() {
    std::vector<int> v = {1, 2, 3};
    std::string s = "Hello";
    std::cout << "Vector: ";
    for (auto x : v) std::cout << x << " "; // 1 2 3
    std::cout << "\nString: " << s << "\n"; // Hello
    return 0;
    }

编译器特性

  • 属性([[nodiscard]] 等)
    • 功能:提供编译器提示。

    • 示例代码

      1
      2
      3
      4
      5
      6
      7
      #include <iostream>
      [[nodiscard]] int func() { return 42; }
      int main() {
      // func(); // 警告:忽略返回值
      std::cout << func() << "\n"; // 42
      return 0;
      }

问题研究

C++ 如何实现多态

在 C++ 中,多态(Polymorphism)是面向对象编程的核心特性之一,它允许在运行时根据对象的实际类型执行不同的行为。C++ 主要通过 虚函数(Virtual Functions)继承(Inheritance) 实现多态,尤其是 运行时多态(Runtime Polymorphism),也称为动态多态。下面我将详细讲解 C++ 如何实现多态及其底层原理,包括实现机制、使用场景、示例代码和注意事项。


1. C++ 多态的分类

C++ 中的多态分为两种:

  • 编译时多态(Compile-time Polymorphism)
    • 通过 函数重载(Function Overloading)运算符重载(Operator Overloading) 实现。
    • 在编译时根据参数类型或数量确定调用哪个函数。
    • 原理:编译器通过名称修饰(Name Mangling)生成唯一的函数签名。
    • 示例(不展开,因为问题聚焦运行时多态):
      1
      2
      3
      4
      5
      6
      7
      8
      #include <iostream>
      void print(int x) { std::cout << "Int: " << x << "\n"; }
      void print(double x) { std::cout << "Double: " << x << "\n"; }
      int main() {
      print(5); // Int: 5
      print(3.14); // Double: 3.14
      return 0;
      }
  • 运行时多态(Runtime Polymorphism)
    • 通过 虚函数基类指针/引用 实现。
    • 在运行时根据对象的实际类型决定调用哪个函数。
    • 这是本文重点讲解的内容。

2. 运行时多态的实现方式

C++ 通过以下机制实现运行时多态: - 继承:定义基类和派生类,形成层次结构。 - 虚函数:在基类中使用 virtual 关键字声明函数,派生类可以重写(Override)。 - 基类指针或引用:通过基类类型的指针或引用调用虚函数。

关键字和语法

  • virtual:标记函数为虚函数,启用动态分派。
  • override(C++11):显式声明派生类重写基类虚函数(可选,但推荐)。
  • final(C++11):禁止进一步重写虚函数或继承类(可选)。

基本示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#include <iostream>
class Base {
public:
virtual void speak() { // 虚函数
std::cout << "Base speaking\n";
}
virtual ~Base() {} // 虚析构函数(重要)
};

class Derived : public Base {
public:
void speak() override { // 重写虚函数
std::cout << "Derived speaking\n";
}
};

int main() {
Base* ptr = new Derived(); // 基类指针指向派生类对象
ptr->speak(); // 输出 "Derived speaking"
delete ptr; // 正确调用 Derived 的析构函数
return 0;
}

运行结果:

1
Derived speaking

在这个例子中,尽管 ptrBase* 类型,但调用 speak() 时执行了 Derived 的版本,这就是运行时多态。


3. 多态的底层原理:虚函数表(vtable)

C++ 使用 虚函数表(Virtual Function Table,简称 vtable)虚表指针(vptr) 来实现运行时多态。以下是详细原理:

3.1 虚函数表的概念

  • 定义:每个包含虚函数的类在编译时会生成一个虚函数表(vtable),这是一个函数指针数组,存储该类所有虚函数的地址。
  • 内容:vtable 中按声明顺序存储虚函数的地址,派生类重写虚函数时会替换对应位置的地址。
  • 唯一性:每个类(而不是每个对象)拥有一个唯一的 vtable,共享给所有该类实例。

3.2 虚表指针(vptr)

  • 定义:每个包含虚函数的对象在内存中额外存储一个指向其类 vtable 的指针(vptr)。
  • 位置:vptr 通常存储在对象内存布局的开头(具体位置由编译器决定)。
  • 初始化:在对象构造时,构造函数会将 vptr 设置为指向该类的 vtable。

3.3 运行时分派的流程

  1. 对象创建
    • 创建 Derived 对象时,Derived 的构造函数将 vptr 设置为指向 Derived 的 vtable。
    • Derived 的 vtable 中,speak() 的地址是 Derived::speak 的实现。
  2. 虚函数调用
    • 通过 Base* ptr 调用 ptr->speak()
    • 编译器生成代码,访问 ptr 指向对象的 vptr。
    • 根据 vptr 找到 Derived 的 vtable。
    • 从 vtable 中提取 speak() 的函数地址(偏移固定,由编译器确定)。
    • 调用该地址对应的函数(即 Derived::speak)。
  3. 销毁对象
    • 删除对象时,虚析构函数确保按正确顺序调用析构函数(从派生类到基类)。

内存布局示意图

假设 BaseDerived 的定义如上: - Base 类 vtable

1
2
[0]: Base::speak
[1]: Base::~Base
- Derived 类 vtable
1
2
[0]: Derived::speak  // 重写
[1]: Derived::~Derived
- 对象内存
1
2
3
Derived 对象:
| vptr (指向 Derived 的 vtable) |
| 数据成员(若有) |

3.4 编译器如何处理

  • 静态代码:编译器为每个虚函数调用生成间接调用指令(如 call [vptr + offset])。
  • 动态分派:运行时通过 vptr 和 vtable 确定实际函数地址。

4. 详细示例与验证

以下代码展示多态的实现,并通过调试手段验证 vtable 的存在:

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
32
33
34
35
#include <iostream>
class Base {
public:
virtual void func1() { std::cout << "Base::func1\n"; }
virtual void func2() { std::cout << "Base::func2\n"; }
virtual ~Base() { std::cout << "Base destroyed\n"; }
};

class Derived : public Base {
public:
void func1() override { std::cout << "Derived::func1\n"; }
void func2() override { std::cout << "Derived::func2\n"; }
~Derived() override { std::cout << "Derived destroyed\n"; }
};

int main() {
Base* b1 = new Base();
Base* b2 = new Derived();

std::cout << "Calling through Base pointer:\n";
b1->func1(); // Base::func1
b1->func2(); // Base::func2
b2->func1(); // Derived::func1
b2->func2(); // Derived::func2

delete b1; // Base destroyed
delete b2; // Derived destroyed -> Base destroyed

// 使用引用
Derived d;
Base& ref = d;
ref.func1(); // Derived::func1

return 0;
}

运行结果:

1
2
3
4
5
6
7
8
9
Calling through Base pointer:
Base::func1
Base::func2
Derived::func1
Derived::func2
Base destroyed
Derived destroyed
Base destroyed
Derived::func1

  • 验证 vtable:在调试器(如 GDB 或 Visual Studio)中检查对象地址,会发现额外指针(vptr),其值指向 vtable。可以用 sizeof 检查对象大小:
    1
    2
    std::cout << "Size of Base: " << sizeof(Base) << "\n";    // 通常 8(vptr)
    std::cout << "Size of Derived: " << sizeof(Derived) << "\n"; // 通常 8(共享 vptr)

5. 注意事项与限制

  • 虚函数的开销
    • 空间开销:每个对象增加一个 vptr(通常 4 或 8 字节),每个类一个 vtable。
    • 时间开销:虚函数调用需要间接寻址,比直接调用慢。
  • 虚析构函数
    • 如果基类析构函数不是虚函数,通过基类指针删除派生类对象只会调用基类析构函数,导致资源泄漏。
    • 示例(错误情况):
      1
      2
      3
      4
      5
      6
      7
      class Base { public: ~Base() {} };  // 非虚析构
      class Derived : public Base { public: ~Derived() { std::cout << "Derived\n"; } };
      int main() {
      Base* ptr = new Derived();
      delete ptr; // 只调用 Base::~Base,Derived::~Derived 被跳过
      return 0;
      }
  • 纯虚函数(= 0
    • 定义抽象类,强制派生类实现。
    • 示例:
      1
      2
      3
      4
      class Abstract {
      public:
      virtual void mustImplement() = 0; // 纯虚函数
      };
  • 非虚函数:通过基类指针调用时,始终执行基类版本,不实现多态。
  • 多重继承
    • 多继承下,每个基类的 vtable 独立,对象可能有多个 vptr。

6. 多态的使用场景

  • 插件系统:基类定义接口,派生类实现具体功能。
  • 策略模式:运行时切换行为。
  • 框架设计:如 GUI 框架中,基类 Widget,派生类 ButtonTextBox

7. 总结

  • 如何实现:C++ 通过虚函数和基类指针/引用实现运行时多态。
  • 原理:依赖虚函数表(vtable)和虚表指针(vptr),编译器生成间接调用,运行时根据对象类型分派。
  • 关键点:需要 virtual 关键字、继承体系和动态绑定。
  • 优势:灵活性高,支持扩展。
  • 代价:增加内存和性能开销。

如果需要更深入探讨(如 vtable 的内存布局、多继承下的实现,或性能分析),请告诉我,我可以进一步扩展!

8. 示意图

好的,我将使用 Mermaid 语法为你绘制 C++ 多态底层原理的图示,展示 虚函数表(vtable)虚表指针(vptr) 的工作机制。由于 Mermaid 是一种基于文本的绘图工具,我会尽量清晰地表达多态的内存布局和调用流程。

以下是两种图: 1. 类层次结构和 vtable 的关系图:展示基类和派生类的虚函数表。 2. 对象内存布局和调用流程图:展示对象中的 vptr 如何指向 vtable 并实现动态分派。


图 1:类层次结构和虚函数表的关系

这个图展示了 BaseDerived 类的继承关系,以及它们的 vtable 如何存储虚函数地址。

classDiagram
    class Base {
        +speak()
        +~Base()
        -vptr
    }
    class Derived {
        +speak()
        +~Derived()
        -vptr
    }
    Derived --|> Base : 继承

    class Base_vtable {
        +speak
        +~Base
    }
    class Derived_vtable {
        +speak
        +~Derived
    }

    Base --> Base_vtable
    Derived --> Derived_vtable
解释
  • Base 和 Derived 类Base 是基类,Derived 是派生类,通过继承关系连接。
  • 虚函数表(vtable)
    • Base_vtable:存储 Base::speakBase::~Base 的地址。
    • Derived_vtable:存储 Derived::speakDerived::~Derived 的地址,speak 被重写。
  • vptr:每个类实例包含一个虚表指针,指向其对应的 vtable。

图 2:对象内存布局和调用流程

这个图展示了 Base* ptr = new Derived() 的内存布局和运行时调用 ptr->speak() 的流程。

sequenceDiagram
    participant Main as main()
    participant Ptr as Base* ptr
    participant Obj as Derived 对象
    participant Vptr as vptr
    participant Vtable as Derived_vtable

    Main->>Ptr: ptr = new Derived()
    Note right of Ptr: 分配 Derived 对象
    Ptr->>Obj: 构造 Derived
    Obj->>Vptr: 设置 vptr 指向 Derived_vtable
    Vptr->>Vtable: 关联

    Main->>Ptr: ptr->speak()
    Ptr->>Obj: 访问对象
    Obj->>Vptr: 获取 vptr
    Vptr->>Vtable: 查找 vtable
    Vtable-->>Main: 调用 Derived::speak()

    Main->>Ptr: delete ptr
    Ptr->>Obj: 析构
    Obj->>Vptr: 获取 vptr
    Vptr->>Vtable: 调用 Derived::~Derived()
    Vtable-->>Obj: 完成析构
内存布局示意图

由于 Mermaid 不直接支持内存布局图,我用文本描述并结合上图解释:

1
2
3
4
5
6
7
8
9
10
11
12
Derived 对象内存布局:
+-------------------+
| vptr (指向 Derived_vtable) | <- 虚表指针
+-------------------+
| 数据成员(若有) |
+-------------------+

Derived_vtable:
+-------------------+
| [0] Derived::speak | <- 虚函数地址
| [1] Derived::~Derived |
+-------------------+

解释
  • 对象创建
    • new Derived() 创建对象,构造函数设置 vptr 指向 Derived_vtable
  • 虚函数调用
    • ptr->speak():通过 ptr 访问对象,找到 vptr
    • vptr 指向 Derived_vtable,表中 [0]Derived::speak 的地址。
    • 调用该地址,执行 Derived::speak
  • 对象销毁
    • delete ptr:通过 vptr 找到 Derived_vtable,调用 Derived::~Derived

代码验证

以下代码与图对应,供参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <iostream>
class Base {
public:
virtual void speak() { std::cout << "Base speaking\n"; }
virtual ~Base() { std::cout << "Base destroyed\n"; }
};

class Derived : public Base {
public:
void speak() override { std::cout << "Derived speaking\n"; }
~Derived() override { std::cout << "Derived destroyed\n"; }
};

int main() {
Base* ptr = new Derived();
ptr->speak(); // 通过 vtable 调用 Derived::speak
delete ptr; // 通过 vtable 调用 Derived::~Derived
return 0;
}

运行结果:

1
2
3
Derived speaking
Derived destroyed
Base destroyed


总结

  • 图 1 展示了类与 vtable 的静态关系,说明多态依赖继承和虚函数重写。
  • 图 2 展示了运行时动态分派的过程,强调 vptr 和 vtable 的作用。
  • 原理:C++ 通过虚函数表和虚表指针实现运行时多态,确保基类指针能正确调用派生类的函数。

如果需要调整图表内容、增加细节(如多继承的 vtable)或进一步解释,请告诉我!

C++ 的模板和 Java 的泛型比较

在编程语言中,C++ 的模板(Templates)和 Java 的泛型(Generics)都是为了实现代码的通用性和类型安全而设计的机制,但它们的实现方式、设计理念和使用场景有显著差异。以下从多个方面对它们进行比较,用中文详细讲解。


1. 基本概念

  • C++ 模板

    • C++ 的模板是一种编译时(compile-time)的机制,允许开发者编写与类型无关的代码。
    • 它是基于“代码生成”的思想:编译器在编译时根据模板参数生成具体的代码(称为“模板实例化”)。
    • 模板可以用于类(类模板)、函数(函数模板)以及变量(C++14 引入变量模板)。

    示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    template <typename T>
    T add(T a, T b) {
    return a + b;
    }
    int main() {
    int x = add(1, 2); // 实例化为 add<int>
    double y = add(1.5, 2.5); // 实例化为 add<double>
    return 0;
    }

  • Java 泛型

    • Java 的泛型是一种运行时(runtime)支持的机制,引入于 Java 5,主要用于提高类型安全性,避免运行时类型转换错误。
    • 它是基于“类型擦除”(type erasure)的实现:在编译时,泛型信息会被擦除,生成的字节码中只保留原始类型(raw type),通过类型检查确保安全性。
    • 泛型主要用于类、接口和方法。

    示例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public class Box<T> {
    private T value;
    public void set(T value) { this.value = value; }
    public T get() { return value; }
    }
    public static void main(String[] args) {
    Box<Integer> intBox = new Box<>();
    intBox.set(10);
    System.out.println(intBox.get());
    }


2. 实现机制

  • C++ 模板
    • 编译时实例化:每次使用不同的类型调用模板时,编译器会生成一份新的代码。例如,add<int>add<double> 会生成两份独立的函数。
    • 无类型擦除:模板保留所有类型信息,编译器完全知道每个实例的具体类型。
    • 鸭子类型(Duck Typing):模板不要求类型实现特定接口,只要代码在语法上有效(比如支持 + 操作),编译就能通过。这也可能导致晦涩的错误信息。
  • Java 泛型
    • 类型擦除:编译后,泛型类型信息被擦除,Box<Integer>Box<String> 在字节码中都是 Box,只不过编译器插入了必要的类型转换(如 (Integer))。
    • 运行时统一:由于擦除,运行时无法直接获取泛型参数的类型(需要通过反射绕过)。
    • 类型约束:泛型通常需要通过 extendssuper 指定类型边界,要求类型实现特定接口或继承特定类。

3. 灵活性与约束

  • C++ 模板
    • 灵活性极高:可以用于任何类型,甚至包括基本类型(如 intdouble),无需显式约束。
    • 支持非类型参数:模板不仅支持类型参数,还支持整数、指针等非类型参数。 示例:
      1
      2
      3
      4
      5
      template <int N>
      struct Array {
      int data[N];
      };
      Array<5> arr; // 固定大小数组
    • 缺点:缺乏运行时类型检查,错误通常在编译时暴露,且错误信息可能复杂难懂。
  • Java 泛型
    • 约束较多:不支持基本类型(如 intdouble),必须使用包装类(如 IntegerDouble),因为泛型基于对象。
    • 不支持非类型参数:只能使用类型参数,无法像 C++ 那样用常量值作为模板参数。
    • 优点:通过类型擦除和编译时检查,保证了运行时的类型安全,且错误信息通常更直观。

4. 性能

  • C++ 模板
    • 零运行时开销:由于模板在编译时生成具体代码,运行时没有额外的类型检查或转换开销,性能几乎等同于手写特定类型的代码。
    • 代码膨胀:每个类型实例化都会生成新代码,可能导致二进制文件变大。
  • Java 泛型
    • 运行时开销:由于类型擦除和潜在的自动装箱/拆箱(如 intInteger),可能引入性能损耗。
    • 代码复用:字节码中只有一个类定义,不会因为泛型参数不同而重复生成代码,二进制文件更小。

5. 使用场景

  • C++ 模板
    • 高性能通用库:如 STL(标准模板库)中的容器(vectormap)和算法(sort),充分利用编译时优化。
    • 元编程:C++ 模板支持模板元编程(TMP),可以实现复杂的编译时计算。 示例:
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      template <int N>
      struct Factorial {
      static const int value = N * Factorial<N - 1>::value;
      };
      template <>
      struct Factorial<0> {
      static const int value = 1;
      };
      int main() {
      std::cout << Factorial<5>::value << std::endl; // 输出 120
      return 0;
      }
  • Java 泛型
    • 类型安全容器:如 List<T>Map<K, V>,避免运行时类型转换错误。
    • API 设计:泛型广泛用于框架和库(如 Java Collections Framework),提高代码可读性和安全性。
    • 不支持元编程:由于类型擦除和运行时限制,泛型无法实现像 C++ 那样的编译时计算。

6. 优缺点总结

特性 C++ 模板 Java 泛型
实现时机 编译时实例化 编译时检查,运行时擦除
类型支持 支持基本类型和对象类型 仅支持对象类型(需包装类)
灵活性 高,支持非类型参数和元编程 较低,约束较多
性能 无运行时开销,可能代码膨胀 有装箱/拆箱开销,代码复用好
错误检测 编译时,可能信息复杂 编译时,信息较清晰
运行时类型信息 保留 擦除,需反射获取

7. 实际例子对比

  • C++ 模板

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    template <typename T>
    class Container {
    public:
    T value;
    Container(T v) : value(v) {}
    void print() { std::cout << value << std::endl; }
    };
    int main() {
    Container<int> c1(42); // 实例化为 Container<int>
    Container<double> c2(3.14); // 实例化为 Container<double>
    c1.print(); // 输出 42
    c2.print(); // 输出 3.14
    return 0;
    }

  • Java 泛型

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public class Container<T> {
    private T value;
    public Container(T v) { this.value = v; }
    public void print() { System.out.println(value); }
    }
    public static void main(String[] args) {
    Container<Integer> c1 = new Container<>(42);
    Container<Double> c2 = new Container<>(3.14);
    c1.print(); // 输出 42
    c2.print(); // 输出 3.14
    }

    • 区别:C++ 生成两个不同类(Container<int>Container<double>),Java 生成单一字节码,运行时靠类型检查。

8. 结论

  • C++ 模板更适合追求性能和灵活性的场景,尤其是嵌入式系统或高性能计算。它提供了强大的编译时能力,但需要开发者处理复杂的编译错误。
  • Java 泛型更适合注重类型安全和代码简洁性的场景,适用于企业级应用和需要运行时一致性的环境,但牺牲了一些灵活性和性能。

两者各有千秋,选择哪种取决于项目需求和语言生态。如果你有具体的应用场景想讨论,可以告诉我,我再深入分析!