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["关系运算符: ==, !=, <, >"]
B3 --> B3c["逻辑运算符: &&, ||, !"]
B3 --> B3d["位运算符: &, |, ^, ~, <<, >>"]
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["引用: &"]
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["移动语义: &&, 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["三路比较: <=>"]
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,648
到2,147,483,647
(-2^31
到2^31-1
)。 - 使用场景:适用于大多数整数计算,如循环计数器、数组索引等。
- 底层原理:存储为二进制补码形式,符号位决定正负。
- 注意事项:溢出时行为未定义(例如
INT_MAX + 1
)。 - 示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
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,768
到32,767
(-2^15
到2^15-1
)。 - 使用场景:适合存储较小的整数,节省内存,如小型计数器。
- 底层原理:同样使用二进制补码表示。
- 注意事项:范围较小,需注意溢出。
- 示例代码:
1
2
3
4
5
6
7
8
9
10
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^31
到2^31-1
,64 位系统上可能更大。 - 使用场景:需要更大范围的整数,如文件大小。
- 底层原理:补码表示,长度由编译器定义。
- 注意事项:建议使用
long long
以确保 64 位支持。 - 示例代码:
1
2
3
4
5
6
7
8
9
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^63
到2^63-1
(约 ±9.2×10^18)。 - 使用场景:非常大的整数,如科学计算或时间戳。
- 底层原理:补码表示,保证 64 位。
- 注意事项:较老的编译器可能不支持。
- 示例代码:
1
2
3
4
5
6
7
8
9
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
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
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
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
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 位)。
- 范围:有符号时
-128
到127
,无符号时0
到255
。 - 使用场景:表示 ASCII 字符,如字母、数字。
- 底层原理:直接存储字符的编码值(如 ASCII)。
- 注意事项:默认是否带符号由编译器决定。
- 示例代码:
1
2
3
4
5
6
7
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
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
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
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
int main() {
const int y = 30; // 常量,必须初始化
std::cout << "y: " << y << "\n"; // 30
// y = 40; // 错误:常量不可修改
return 0;
}
- 功能:使用
运算符
C++ 提供了丰富的运算符,用于执行算术、逻辑、位操作等。以下逐一详细讲解。
算术运算符
用于基本的数学运算。
+
(加法)- 功能:将两个操作数相加。
- 使用场景:数值计算。
- 注意事项:整数溢出未定义,浮点数可能有精度误差。
- 示例代码:
1
2
3
4
5
6
7
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
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
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
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
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
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
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
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
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
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
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
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
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
int main() {
bool a = true;
bool result = !a;
std::cout << "!true: " << result << "\n"; // 0
return 0;
}
位运算符
按位操作,直接操作二进制位。
&
(按位与)- 功能:逐位进行与运算。
- 使用场景:提取特定位、掩码操作。
- 示例代码:
1
2
3
4
5
6
7
8
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
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
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
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
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
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
int main() {
int a = 10;
std::cout << "a = " << a << "\n"; // 10
return 0;
}
+=
(加赋值)- 功能:加后赋值。
- 示例代码:
1
2
3
4
5
6
7
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
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
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
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
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
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;
}
控制结构
控制结构用于管理程序的执行流程。
条件语句
if
、else if
、else
- 功能:根据条件执行不同代码块。
- 使用场景:分支逻辑。
- 底层原理:条件表达式求值为真(非 0)时执行。
- 注意事项:避免悬垂 else 问题。
- 示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
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;
}
开关语句
switch
、case
、default
- 功能:根据整数值跳转到对应分支。
- 使用场景:多条件选择。
- 底层原理:编译为跳转表或条件分支。
- 注意事项:需要
break
,否则会贯穿。 - 示例代码:
1
2
3
4
5
6
7
8
9
10
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
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
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
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
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
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
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
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
void func(); // 声明
int main() {
func();
return 0;
}
void func() { // 定义
std::cout << "Function called\n";
}
参数传递
- 值传递:传递副本,修改不影响原值。
- 示例代码:
1
2
3
4
5
6
7
8
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
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
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
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
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
2int 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
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 |
|
arr
等价于&arr[0]
,是一个指针。ptr + 1
:指针加 1,移动到下一个元素(偏移一个int
的大小,通常是 4 字节)。*(ptr + i)
:访问第i
个元素。
数组下标与指针
数组下标操作 arr[i]
等价于 *(arr + i)
:
1
2
3int arr[5] = {1, 2, 3, 4, 5};
cout << arr[2] << endl; // 输出: 3
cout << *(arr + 2) << endl; // 输出: 3
4. 动态内存分配
C++ 中可以使用 new
和 delete
动态分配和释放内存,指针是动态内存管理的核心。
动态分配内存
1 | int *ptr = new int; // 分配一个 int 大小的内存 |
new
:分配内存并返回地址。delete
:释放内存,避免内存泄漏。
动态分配数组
1 | int *arr = new int[5]; // 分配一个包含 5 个 int 的数组 |
new int[5]
:分配数组。delete[]
:释放数组内存(注意与delete
的区别)。
5. 指针与函数
指针常用于函数参数传递,特别是在需要修改原始数据或避免拷贝大对象时。
(1)通过指针修改变量
1 |
|
- 输出:
1
2Before 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
16int* 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 | int* badFunction() { |
- 局部变量的地址在函数返回后无效,访问会导致未定义行为。
6. 指针的进阶用法
(1)指针的指针(多级指针)
指针可以指向另一个指针,形成多级指针: 1
2
3
4
5int x = 10;
int *ptr = &x;
int **pptr = &ptr; // 指向指针的指针
cout << **pptr << endl; // 输出: 10**pptr
:解引用两次,访问 x
的值。
(2)常量指针与指针常量
- 常量指针(
const int *ptr
):指针指向的值是常量,不能修改。1
2
3int x = 10;
const int *ptr = &x;
// *ptr = 20; // 错误,不能修改值 - 指针常量(
int *const ptr
):指针本身是常量,不能指向其他地址。1
2
3
4int 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
using namespace std;
void sayHello() {
cout << "Hello!" << endl;
}
int main() {
void (*funcPtr)() = sayHello; // 函数指针
funcPtr(); // 调用函数,输出: Hello!
return 0;
}
7. 常见问题与注意事项
- 野指针:未初始化或已释放的指针,访问会导致未定义行为。
- 解决:始终初始化指针,释放后置为
nullptr
。
- 解决:始终初始化指针,释放后置为
- 内存泄漏:动态分配的内存未释放。
- 解决:使用
delete
释放内存,或者使用智能指针(如std::unique_ptr
、std::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
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 |
|
int &ref = x
:ref
是x
的引用。- 修改
ref
会直接修改x
,因为它们指向同一个内存地址。
2. 引用的特点
(1)必须初始化
引用在声明时必须初始化,且不能改变指向(不能重新绑定到另一个变量)。
1
2
3
4
5
6
7int x = 10;
// int &ref; // 错误!引用必须初始化
int &ref = x;
int y = 20;
ref = y; // 不是让 ref 指向 y,而是将 y 的值赋给 ref(即 x)
cout << x << endl; // 输出: 20ref = y
:将 y
的值赋给
x
,而不是让 ref
指向 y
。
(2)引用不是独立对象
引用不占用额外的内存,它只是变量的别名。ref
和
x
的地址相同: 1
2cout << &x << endl; // 输出: x 的地址
cout << &ref << endl; // 输出: 同一个地址
(3)不能创建引用的引用
引用本身不是对象,因此不能创建引用的引用: 1
2
3int x = 10;
int &ref = x;
// int &&ref2 = ref; // 错误!不能创建引用的引用&&
在 C++11 中表示右值引用(稍后会讲到)。
(4)不能创建数组的引用
数组的引用是合法的,但不能创建引用的数组: 1
2
3int 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
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
4Before swapByValue: x = 10, y = 20
After swapByValue: x = 10, y = 20
Before swapByReference: x = 10, y = 20
After swapByReference: x = 20, y = 10swapByValue
:值传递,函数内的修改不影响原始变量。 -
swapByReference
:引用传递,函数内的修改直接影响原始变量。
效率优势:
对于大对象(如结构体、类对象),引用传递避免了拷贝开销:
1
2
3
4
5
6
7
8
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
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
4int& badFunction() {
int x = 10;
return x; // 错误!x 是局部变量,函数返回后 x 被销毁
}
(3)简化代码
引用可以让代码更简洁,避免频繁使用指针和解引用: 1
2
3
4int x = 10;
int &ref = x;
ref++; // 直接操作引用,等价于 x++
cout << x << endl; // 输出: 11
4. 常量引用(const Reference)
常量引用常用于函数参数,防止意外修改原始数据,同时允许传递临时对象。
基本用法
1 | int x = 10; |
常量引用与临时对象
常量引用可以绑定到临时对象,而普通引用不行: 1
2
3int x = 10;
const int &ref = x + 1; // 合法,绑定到临时对象
// int &ref2 = x + 1; // 错误,非 const 引用不能绑定临时对象
函数参数中的常量引用
1 |
|
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
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
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
移动构造
析构: Hellomove
:将 s1
转换为右值,触发移动构造,避免拷贝。
移动构造函数和移动语义
这段代码展示了 C++11 引入的右值引用和移动语义,用于优化资源管理,避免不必要的拷贝。
- 代码整体结构
头文件:
1
2
3
using namespace std;<iostream>
:用于输入输出(cout
)。<string>
:虽然代码中没有直接使用std::string
,但可能是为了后续扩展。using namespace std;
:避免每次使用std::cout
时写std::
。
类
MyString
:- 这是一个自定义字符串类,内部使用
char*
管理动态分配的字符数组。 - 包含构造函数、移动构造函数和析构函数。
- 这是一个自定义字符串类,内部使用
主函数
main
:- 创建一个
MyString
对象s1
,然后通过move
将其资源移动到s2
。
- 创建一个
- 逐部分分析
(1)类 MyString
的定义
1 | class MyString { |
成员变量
char *data
:一个指针,指向动态分配的字符数组,用于存储字符串。
构造函数
1 | MyString(const char *str) { |
- 作用:从一个 C
风格字符串(
const char*
)构造MyString
对象。 - 细节:
strlen(str) + 1
:计算字符串长度(包括结尾的\0
)。new char[strlen(str) + 1]
:动态分配内存,存储字符串。strcpy(data, str)
:将输入字符串复制到data
中。cout << "构造: " << data << endl
:打印构造信息。
移动构造函数
1 | MyString(MyString &&other) noexcept : data(other.data) { |
- 作用:这是 C++11
引入的移动构造函数,用于从一个右值(临时对象或被
move
的对象)转移资源。 - 参数:
MyString &&other
:右值引用,表示other
是一个右值(临时对象或即将销毁的对象)。
- 细节:
: data(other.data)
:初始化列表,将other
的data
指针直接赋值给当前对象的data
。other.data = nullptr
:将other
的data
置为空,避免other
析构时释放同一块内存。noexcept
:表示这个函数不会抛出异常,移动构造函数通常需要是noexcept
的,以便在标准库(如std::vector
)中使用时更安全。cout << "移动构造" << endl
:打印移动构造信息。
析构函数
1 | ~MyString() { |
- 作用:在对象销毁时释放动态分配的内存。
- 细节:
if (data)
:检查data
是否为空指针(避免释放空指针)。cout << "析构: " << data << endl
:打印析构信息。delete[] data
:释放data
指向的内存。
(2)主函数 main
1 | int main() { |
第一行:创建 s1
1 | MyString s1("Hello"); |
调用普通构造函数,构造一个
MyString
对象s1
。内部:
分配内存,存储字符串 “Hello”。
输出:
1
构造: Hello
第二行:创建 s2
并移动
s1
1 | MyString s2 = move(s1); // 移动构造 |
作用:通过
std::move
将s1
转换为右值,触发移动构造函数。细节:
std::move(s1)
:将s1
标记为右值(尽管s1
是一个左值),告诉编译器可以“偷”它的资源。调用移动构造函数:
s2
的data
接管s1
的data
(指向 “Hello” 的内存)。s1
的data
被置为nullptr
。
输出:
1
移动构造
程序结束:析构
main
函数结束时,s1
和s2
离开作用域,调用析构函数。s2
的data
指向 “Hello”,正常析构:1
析构: Hello
s1
的data
是nullptr
,不会释放内存(避免重复释放)。
完整输出
1 | 构造: Hello |
- 为什么需要移动构造函数?
(1)问题:拷贝的开销
如果没有移动构造函数,MyString s2 = s1
会调用拷贝构造函数(这里没有定义,默认会执行深拷贝):
- 深拷贝会重新分配内存,复制 “Hello”,然后
s1
和s2
各自管理自己的内存。 - 问题:如果
s1
即将销毁(比如临时对象),这种拷贝是浪费的。
(2)移动语义的优势
- 移动构造函数通过“偷”资源(将
s1
的data
直接给s2
),避免了深拷贝。 s1
的data
被置为nullptr
,确保它不会重复释放内存。- 效率:移动操作(指针赋值)比拷贝(内存分配和复制)快得多。
(3)std::move
的作用
std::move
并不真正“移动”数据,它只是将对象转换为右值,告诉编译器可以调用移动构造函数。- 移动后,
s1
进入“可销毁”状态(data
为nullptr
),但仍然可以安全析构。
- 代码中需要注意的点
(1)内存管理
MyString
使用动态内存(new
和delete[]
),需要小心管理。- 移动构造函数通过将
other.data
置为nullptr
,避免了重复释放内存(double-free)的问题。
(2)缺少拷贝构造函数
- 这段代码没有定义拷贝构造函数和拷贝赋值运算符。
- 如果直接拷贝(
MyString s2 = s1
),编译器会生成默认的拷贝构造函数,导致浅拷贝(s1
和s2
共享data
),析构时会重复释放内存,引发崩溃。 - 改进:需要实现拷贝构造函数和拷贝赋值运算符(遵循“规则五”)。
改进后的代码:
1 |
|
(3)noexcept
的重要性
- 移动构造函数标记为
noexcept
,告诉编译器它不会抛出异常。 - 标准库(如
std::vector
)在移动元素时会优先选择noexcept
的移动构造函数,否则可能退回到拷贝。
- 移动语义的核心思想
- 移动语义:将资源从一个对象“转移”到另一个对象,而不是拷贝。
- 适用场景:
- 临时对象(右值)即将销毁时。
- 需要高效转移资源(如动态内存、文件句柄)。
- 好处:
- 避免不必要的深拷贝,提高性能。
- 资源管理更安全(避免重复释放)。
- 总结
- 代码功能:
MyString
是一个管理动态字符串的类。- 移动构造函数通过右值引用(
&&
)实现资源转移,避免深拷贝。 std::move
将s1
转换为右值,触发移动构造。
- 输出解释:
- 构造
s1
:分配内存,存储 “Hello”。 - 移动构造
s2
:将s1
的data
转移给s2
,s1
的data
置为nullptr
。 - 析构:
s2
释放 “Hello”,s1
的data
是nullptr
,不释放。
- 构造
- 改进建议:
- 添加拷贝构造函数和拷贝赋值运算符,避免浅拷贝问题。
- 考虑使用
std::string
替代char*
,更安全且方便。
如果你还有不明白的地方(比如右值引用的细节或拷贝构造函数的实现),可以告诉我,我会进一步讲解!
6. 引用与指针的对比
特性 | 引用 | 指针 |
---|---|---|
初始化 | 必须初始化,不能改变指向 | 可以不初始化,可改变指向 |
内存 | 不占用额外内存(只是别名) | 占用内存(存储地址) |
语法 | 直接使用(无需解引用) | 需要 * 解引用 |
安全性 | 更安全,不会指向无效内存 | 可能出现野指针、悬空指针 |
用途 | 函数参数、简化代码 | 动态内存、复杂数据结构 |
选择建议: - 优先使用引用,除非需要动态内存分配或改变指向(用指针)。 - 函数参数传递时,引用比指针更简洁且安全。
7. 注意事项
- 避免返回局部变量的引用:局部变量在函数返回后销毁,返回其引用会导致未定义行为。
- 引用与指针混用:引用可以绑定到指针解引用后的值:
1
2
3
4
5int x = 10;
int *ptr = &x;
int &ref = *ptr; // ref 绑定到 ptr 指向的值
ref = 20;
cout << x << endl; // 输出: 20 - 性能优化:对于大对象,使用引用传递(尤其是
const
引用)可以避免拷贝。
8. 总结
- 引用基础:引用是变量的别名,操作引用等价于操作原始变量。
- 用途:
- 函数参数传递:避免拷贝,提高效率。
- 函数返回值:允许直接修改。
- 简化代码:无需解引用。
- 常量引用:防止修改,绑定临时对象。
- 右值引用:支持移动语义,优化性能。
- 与指针对比:引用更安全、简洁,但灵活性不如指针。
类与对象(面向对象特性)
C++ 的面向对象特性是其区别于 C 的重要部分,支持封装、继承和多态。以下是对每个子特性的详细讲解。
类定义(class
和
struct
)
- 功能:类是用户定义的类型,封装数据和行为;
class
默认访问权限为private
,struct
默认为public
。 - 使用场景:建模现实世界的实体,如人、车等。
- 底层原理:类是编译器的蓝图,实例化后分配内存,成员按声明顺序排列。
- 注意事项:注意内存对齐可能增加对象大小;
class
和struct
在语法上等价,仅默认访问权限不同。 - 示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
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;
}
访问控制(public
、private
、protected
)
- 功能:控制类成员的访问权限:
public
:任何地方可访问。private
:仅类内部和友元可访问。protected
:类内部和派生类可访问。
- 使用场景:封装数据,隐藏实现细节。
- 底层原理:访问控制由编译器在编译时检查,不影响运行时。
- 注意事项:合理设计访问权限以保护数据一致性。
- 示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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
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
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
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
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
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
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
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
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
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
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;
}
异常处理
异常处理机制用于管理运行时错误。
try
、catch
、throw
- 功能:
throw
:抛出异常。try
:包裹可能抛异常的代码。catch
:捕获并处理异常。
- 使用场景:错误处理,如文件操作失败。
- 底层原理:异常通过栈展开传递,调用析构函数。
- 注意事项:未捕获的异常终止程序。
- 示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
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
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
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
namespace MySpace {
int x = 42;
}
int main() {
using MySpace::x; // 引入 x
std::cout << "x: " << x << "\n"; // 42
// using namespace MySpace; // 引入整个命名空间
return 0;
}
动态内存管理
C++ 支持手动管理堆内存。
new
和
delete
- 功能:
new
:分配堆内存并构造对象。delete
:析构对象并释放内存。
- 使用场景:动态对象生命周期管理。
- 底层原理:调用内存分配器(如
malloc
)和构造函数。 - 注意事项:成对使用,避免内存泄漏。
- 示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
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
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
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
int main() {
std::cout << "Debug mode\n";
std::cout << "Release mode\n";
std::cout << "Not release\n";
return 0;
}
文件包含(#include
)
- 功能:引入头文件或源代码。
- 注意事项:使用
<>
表示标准库,""
表示用户文件。 - 示例代码:
1
2
3
4
5
6
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 的工作原理
- 资源与对象绑定:
- 在对象的构造函数中获取资源(例如分配内存、打开文件、加锁)。
- 资源的释放逻辑放在析构函数中。
- 自动管理:
- C++ 保证当对象离开作用域(无论是正常退出还是抛出异常),其析构函数都会被调用。
- 因此,资源的释放是自动的,无需程序员手动干预。
- 异常安全:
- 即使代码抛出异常,栈上的对象仍会按逆序析构(栈解退,stack unwinding),确保资源不泄漏。
RAII 的代码示例
以下是一个简单的 RAII 示例,用于管理动态分配的内存:
1 |
|
输出: 1
2
3Resource acquired: 42
Using resource: 42
Resource released
在这个例子中: - Resource
对象的构造函数分配内存(new int
)。 -
析构函数释放内存(delete data
)。 - r
是栈对象,离开作用域时自动调用析构函数,确保内存不泄漏。
RAII 的典型应用
RAII 在 C++ 中无处不在,以下是几个常见场景:
- 动态内存管理:
- 标准库的智能指针(如
std::unique_ptr
和std::shared_ptr
)是 RAII 的经典实现。 - 示例:
1
2
3
4
5
void example() {
std::unique_ptr<int> ptr = std::make_unique<int>(10);
// 使用 ptr
} // ptr 离开作用域,内存自动释放 unique_ptr
在析构时自动调用delete
,无需手动释放。
- 标准库的智能指针(如
- 文件管理:
std::fstream
(如std::ifstream
、std::ofstream
)使用 RAII 管理文件句柄。- 示例:
1
2
3
4
5
void writeFile() {
std::ofstream file("example.txt");
file << "Hello, RAII!";
} // file 离开作用域,自动关闭文件
- 互斥锁管理:
std::lock_guard
和std::unique_lock
使用 RAII 管理线程同步中的锁。- 示例(结合你的线程代码):
1
2
3
4
5
6
7
8
9
10
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
在构造时加锁,析构时解锁,保证即使抛出异常,锁也会释放。
- 其他资源:
- 网络连接(如
std::socket
的封装)。 - 数据库连接。
- 图形资源(如 OpenGL 上下文)。
- 网络连接(如
RAII 的优点
- 自动资源管理:
- 资源释放由析构函数自动完成,避免手动调用
delete
、close
等。
- 资源释放由析构函数自动完成,避免手动调用
- 异常安全:
- 即使抛出异常,栈解退机制确保析构函数被调用,防止资源泄漏。
- 代码简洁:
- 减少手动管理资源的代码,降低出错概率。
- 确定性释放:
- 资源在对象离开作用域时立即释放,行为可预测。
RAII 的注意事项
- 避免手动管理:
- 不要在 RAII 对象之外手动释放资源(如
delete ptr.get()
),否则可能导致未定义行为。
- 不要在 RAII 对象之外手动释放资源(如
- 析构函数不抛异常:
- RAII 依赖析构函数的调用,析构函数应保证不抛出异常(通常标记为
noexcept
)。 - 如果析构函数抛出异常,可能导致程序终止(
std::terminate
)。
- RAII 依赖析构函数的调用,析构函数应保证不抛出异常(通常标记为
- 拷贝和移动:
- RAII 对象管理独占资源时(如
std::unique_ptr
),通常禁用拷贝,允许移动。 - 如果需要共享资源(如
std::shared_ptr
),需明确定义拷贝语义。
- RAII 对象管理独占资源时(如
- 性能开销:
- RAII 对象的构造和析构可能引入少量开销,但通常被其安全性和简洁性抵消。
RAII 与你的线程代码
在你的原始代码中: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 对象,因为它不会自动调用join
或detach
。如果t1
未被join
或detach
就离开作用域,程序会调用std::terminate
。- 改进方式:使用 RAII 封装
std::thread
,确保线程总是被正确管理:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18class 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
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
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
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
,明确表示空指针。 - 使用场景:初始化指针、检查有效性。
- 底层原理:
nullptr
是nullptr_t
类型,避免整数转换问题。 - 注意事项:比
NULL
(0)更类型安全。 - 示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
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
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
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
// 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
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
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
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
5int 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
4int x = 10;
auto refFunc = [&x]() { return x * 2; }; // x 是引用
x = 20;
std::cout << refFunc() << "\n"; // 输出 40,因为 refFunc 引用了修改后的 x
(3) 全局捕获
[=]
:按值捕获所有外部变量的副本。[&]
:按引用捕获所有外部变量。- 混合捕获:可以组合,例如
[=, &x]
表示默认按值捕获,但x
按引用捕获。
示例: 1
2
3
4int 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
7struct 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
6class Lambda {
int x; // 捕获的变量
public:
Lambda(int x_) : x(x_) {}
int operator()() const { return x * 2; } // 重载函数调用操作符,operator() 是重载的函数调用操作符,() 表示这个操作符不接受参数(空参数列表),int 是返回值类型,表示调用这个操作符会返回一个整数,const 表示这个成员函数不会修改对象的状态(x 不会被改变)。
};
调用 func()
实际上是调用这个类的
operator()
。这解释了 Lambda 为什么可以像函数一样使用。
4. Lambda 的常见用途
标准库算法:与
<algorithm>
配合,例如std::sort
、std::for_each
。1
2std::vector<int> vec = {3, 1, 4, 1, 5};
std::sort(vec.begin(), vec.end(), [](int a, int b) { return a < b; });异步编程:与
std::async
或线程配合。1
2auto task = []() { std::cout << "Running task\n"; };
std::async(std::launch::async, task);回调函数:传递给需要回调的函数。
1
2auto callback = [](int x) { std::cout << "Callback: " << x << "\n"; };//
someFunction(callback);立即执行(IIFE,Immediately Invoked Function Expression):
1
int result = []() { return 42; }(); // 立即调用,result = 42
5. 注意事项
生命周期问题:
- 按引用捕获时,确保捕获的变量在 Lambda 使用时仍然有效。
- 示例(错误用法):
1
2
3
4auto createLambda() {
int x = 10;
return [&x]() { return x; }; // 悬垂引用,x 在函数返回后销毁
}
性能开销:
- 按值捕获会复制变量,可能会增加内存开销。
- 对于大对象,考虑按引用捕获或使用
std::move
(C++11 起支持移动捕获,C++14 增强)。
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
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
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
4int x = 10;
auto statefulLambda = [x]() { return x; };
decltype(statefulLambda) defaultLambda; // 错误:无默认构造函数
statefulLambda = statefulLambda; // 错误:无赋值操作符
mutable 关键字:
- 按值捕获默认只读,使用
mutable
允许修改副本。1
2
3
4int 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
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
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
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
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
9int 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
未被 join
或 detach
,程序在 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
2void increment(int n) { /* ... */ }
thread t1(increment, 42); // 传递参数 42
如果 increment
抛出异常,线程会终止,异常不会传播到主线程。需在 increment
内部捕获异常,或使用其他机制(如 std::future
)处理。
counter
是全局变量,被 t1
和
t2
共享。++counter
不是原子操作,即可能被其他线程中断,它实际上涉及三个步骤:
- 读取
counter
的当前值。 - 将值加 1。
- 将新值写回
counter
。
要确保 counter
总是
2000,需要消除数据竞争。常见方法:
使用互斥锁(Mutex):
1
2
3
4
5
6
7
8
9
mutex mtx;
void increment() {
for (int i = 0; i < 1000; ++i) {
mtx.lock();
++counter;
mtx.unlock();
}
}mutex
确保每次只有 一个线程能访问counter
,避免竞争。- 缺点:频繁加锁解锁可能降低性能。
使用原子操作:
1
2
3
4
5
6
7
atomic<int> counter = 0;
void increment() {
for (int i = 0; i < 1000; ++i) {
++counter;
}
}std::atomic<int>
提供原子操作,保证++counter
不被中断。- 更高效,适合简单计数器场景。
减少锁的粒度:
- 将整个循环放在锁内(而不是每次迭代都加锁)可以减少加锁开销:
1
2
3
4
5
6
7void 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
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
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
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::future
和 std::promise
用于异步任务的结果传递。
组件:
std::promise
:设置任务结果。std::future
:获取任务结果。std::async
:异步执行函数。
示例:
1
2
3
4
5
6
7
8
9
10
11
12
13
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::async
或std::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
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
2Thread 1234: 1
Thread 5678: 1
3. C++17 和 C++20 的并发改进
C++17
- 并行算法(
<algorithm>
):- 支持并行执行标准算法,例如
std::sort
:1
2
3
4
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
10std::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
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
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
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
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. 注意事项
- 死锁:多锁时使用
std::scoped_lock
或固定加锁顺序。 - 性能:优先考虑原子操作或无锁结构,减少锁争用。
- 异常安全:使用 RAII(如
std::lock_guard
)确保锁在异常时释放。 - 调试:使用工具(如 ThreadSanitizer)检测数据竞争。
- C++ 版本:确保编译器支持目标特性(例如
-std=c++20
)。
5. 总结
- 核心工具:互斥锁、条件变量、原子操作、未来/承诺、线程局部存储。
- C++17
改进:并行算法、
std::shared_mutex
。 - C++20
增强:信号量、屏障、锁存器、
std::jthread
。
constexpr
- 功能:定义编译时常量或函数。
- 使用场景:优化性能、静态断言。
- 示例代码:
1
2
3
4
5
6
7
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
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
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
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
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 引入了更多实用特性,提升了语言的现代化程度。
结构化绑定
- 功能:解构赋值,将聚合类型(如
pair
、tuple
、结构体)的成员绑定到变量。 - 使用场景:简化多返回值函数的使用。
- 底层原理:编译器生成临时对象并解构,绑定到新变量。
- 注意事项:需支持结构化绑定的类型(如
std::pair
或含std::tuple_size
的类型)。 - 示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
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 初始化
- 功能:允许在
if
和switch
语句中初始化变量,限制作用域。 - 使用场景:临时变量仅用于条件判断。
- 底层原理:编译器将初始化和条件组合为单一语句。
- 注意事项:变量作用域限于
if
或switch
块。 - 示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
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
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
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::get
或std::visit
,错误访问抛异常。 - 示例代码:
1
2
3
4
5
6
7
8
9
10
11
12
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
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
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
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
inline int globalVar = 42;
// main.cpp
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
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
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
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
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
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
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
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
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
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
[[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
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 |
|
运行结果: 1
Derived speaking
在这个例子中,尽管 ptr
是 Base*
类型,但调用 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 运行时分派的流程
- 对象创建:
- 创建
Derived
对象时,Derived
的构造函数将 vptr 设置为指向Derived
的 vtable。 Derived
的 vtable 中,speak()
的地址是Derived::speak
的实现。
- 创建
- 虚函数调用:
- 通过
Base* ptr
调用ptr->speak()
。 - 编译器生成代码,访问
ptr
指向对象的 vptr。 - 根据 vptr 找到
Derived
的 vtable。 - 从 vtable 中提取
speak()
的函数地址(偏移固定,由编译器确定)。 - 调用该地址对应的函数(即
Derived::speak
)。
- 通过
- 销毁对象:
- 删除对象时,虚析构函数确保按正确顺序调用析构函数(从派生类到基类)。
内存布局示意图
假设 Base
和 Derived
的定义如上: -
Base 类 vtable: 1
2[0]: Base::speak
[1]: Base::~Base1
2[0]: Derived::speak // 重写
[1]: Derived::~Derived1
2
3Derived 对象:
| vptr (指向 Derived 的 vtable) |
| 数据成员(若有) |
3.4 编译器如何处理
- 静态代码:编译器为每个虚函数调用生成间接调用指令(如
call [vptr + offset]
)。 - 动态分派:运行时通过 vptr 和 vtable 确定实际函数地址。
4. 详细示例与验证
以下代码展示多态的实现,并通过调试手段验证 vtable 的存在:
1 |
|
运行结果: 1
2
3
4
5
6
7
8
9Calling 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
2std::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
7class 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
4class Abstract {
public:
virtual void mustImplement() = 0; // 纯虚函数
};
- 非虚函数:通过基类指针调用时,始终执行基类版本,不实现多态。
- 多重继承:
- 多继承下,每个基类的 vtable 独立,对象可能有多个 vptr。
6. 多态的使用场景
- 插件系统:基类定义接口,派生类实现具体功能。
- 策略模式:运行时切换行为。
- 框架设计:如 GUI 框架中,基类
Widget
,派生类Button
、TextBox
。
7. 总结
- 如何实现:C++ 通过虚函数和基类指针/引用实现运行时多态。
- 原理:依赖虚函数表(vtable)和虚表指针(vptr),编译器生成间接调用,运行时根据对象类型分派。
- 关键点:需要
virtual
关键字、继承体系和动态绑定。 - 优势:灵活性高,支持扩展。
- 代价:增加内存和性能开销。
如果需要更深入探讨(如 vtable 的内存布局、多继承下的实现,或性能分析),请告诉我,我可以进一步扩展!
8. 示意图
好的,我将使用 Mermaid 语法为你绘制 C++ 多态底层原理的图示,展示 虚函数表(vtable) 和 虚表指针(vptr) 的工作机制。由于 Mermaid 是一种基于文本的绘图工具,我会尽量清晰地表达多态的内存布局和调用流程。
以下是两种图: 1. 类层次结构和 vtable 的关系图:展示基类和派生类的虚函数表。 2. 对象内存布局和调用流程图:展示对象中的 vptr 如何指向 vtable 并实现动态分派。
图 1:类层次结构和虚函数表的关系
这个图展示了 Base
和 Derived
类的继承关系,以及它们的 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::speak
和Base::~Base
的地址。Derived_vtable
:存储Derived::speak
和Derived::~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
12Derived 对象内存布局:
+-------------------+
| 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
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
3Derived 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
9template <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
10public 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)
)。 - 运行时统一:由于擦除,运行时无法直接获取泛型参数的类型(需要通过反射绕过)。
- 类型约束:泛型通常需要通过
extends
或super
指定类型边界,要求类型实现特定接口或继承特定类。
- 类型擦除:编译后,泛型类型信息被擦除,
3. 灵活性与约束
- C++ 模板:
- 灵活性极高:可以用于任何类型,甚至包括基本类型(如
int
、double
),无需显式约束。 - 支持非类型参数:模板不仅支持类型参数,还支持整数、指针等非类型参数。
示例:
1
2
3
4
5template <int N>
struct Array {
int data[N];
};
Array<5> arr; // 固定大小数组 - 缺点:缺乏运行时类型检查,错误通常在编译时暴露,且错误信息可能复杂难懂。
- 灵活性极高:可以用于任何类型,甚至包括基本类型(如
- Java 泛型:
- 约束较多:不支持基本类型(如
int
、double
),必须使用包装类(如Integer
、Double
),因为泛型基于对象。 - 不支持非类型参数:只能使用类型参数,无法像 C++ 那样用常量值作为模板参数。
- 优点:通过类型擦除和编译时检查,保证了运行时的类型安全,且错误信息通常更直观。
- 约束较多:不支持基本类型(如
4. 性能
- C++ 模板:
- 零运行时开销:由于模板在编译时生成具体代码,运行时没有额外的类型检查或转换开销,性能几乎等同于手写特定类型的代码。
- 代码膨胀:每个类型实例化都会生成新代码,可能导致二进制文件变大。
- Java 泛型:
- 运行时开销:由于类型擦除和潜在的自动装箱/拆箱(如
int
到Integer
),可能引入性能损耗。 - 代码复用:字节码中只有一个类定义,不会因为泛型参数不同而重复生成代码,二进制文件更小。
- 运行时开销:由于类型擦除和潜在的自动装箱/拆箱(如
5. 使用场景
- C++ 模板:
- 高性能通用库:如
STL(标准模板库)中的容器(
vector
、map
)和算法(sort
),充分利用编译时优化。 - 元编程:C++
模板支持模板元编程(TMP),可以实现复杂的编译时计算。 示例:
1
2
3
4
5
6
7
8
9
10
11
12template <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;
}
- 高性能通用库:如
STL(标准模板库)中的容器(
- 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
14template <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
11public 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 生成单一字节码,运行时靠类型检查。
- 区别:C++
生成两个不同类(
8. 结论
- C++ 模板更适合追求性能和灵活性的场景,尤其是嵌入式系统或高性能计算。它提供了强大的编译时能力,但需要开发者处理复杂的编译错误。
- Java 泛型更适合注重类型安全和代码简洁性的场景,适用于企业级应用和需要运行时一致性的环境,但牺牲了一些灵活性和性能。
两者各有千秋,选择哪种取决于项目需求和语言生态。如果你有具体的应用场景想讨论,可以告诉我,我再深入分析!