复合类型
# 第 5 章 C++ 复合类型
# 目录介绍
# 5.1 复合类型
# 5.1.1 复合数据类型
C++ 中的复合数据类型是由基本数据类型组合而成的更复杂的数据结构。它们允许将多个值组织在一起,以便更高效地管理和操作数据。
复合数据类型:
- array:数组类型,用于存储一组相同类型的元素。
- struct:结构体类型,用于定义自定义的复合数据类型,可以包含多个不同类型的成员变量。
- class:类类型,类似于结构体,但可以包含成员函数和访问控制。
# 5.1.2 综合案例与思考
综合案例:复合类型概览
#include <iostream>
#include <string>
#include <array>
using namespace std;
// 枚举:表示颜色
enum class Color { Red, Green, Blue };
// 结构体:存储坐标
struct Point {
double x, y;
};
// 联合体:节省内存
union Value {
int intVal;
float floatVal;
};
int main() {
// 数组:相同类型元素的集合
array<int, 5> scores = {90, 85, 78, 92, 88};
// 结构体:不同类型组合
Point p = {3.14, 2.71};
cout << "坐标: (" << p.x << ", " << p.y << ")" << endl;
// 枚举:命名常量
Color c = Color::Green;
cout << "颜色值: " << static_cast<int>(c) << endl;
// 联合体:共享内存
Value v;
v.intVal = 42;
cout << "整数值: " << v.intVal << endl;
v.floatVal = 3.14f;
cout << "浮点值: " << v.floatVal << endl;
return 0;
}
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
案例知识融合:这个案例在一个程序中展示了C++所有复合数据类型的基本用法——数组(存储同类型数据)、结构体(组合不同类型)、枚举(定义命名常量)、联合体(共享内存存储)。它们各有特点,共同构成了C++丰富的类型系统。
思考题:
- 复合类型和基本类型的本质区别是什么?编译器如何在内存中布局复合类型?
struct和class在C++中几乎相同(默认访问权限不同),为什么C++同时保留了这两个关键字?- 在实际项目中,你会在什么场景下选择使用结构体而不是类?
# 5.2 字符串
# 5.2.1 字符串表示
字符串 是一种用于存储和操作文本数据的数据类型。C++ 提供了两种主要的字符串表示方式:
- C 风格字符串:基于字符数组的字符串。
std::string类:C++ 标准库提供的字符串类,功能更强大、更安全。
# 5.2.2 C风格字符串
C风格字符串: char 变量名[] = "字符串值"
示例:
int main() {
char str1[] = "hello world";
cout << str1 << endl;
return 0;
}
2
3
4
5
注意:C风格的字符串要用双引号括起来
# 5.2.3 C++风格字符串
C++风格字符串: string 变量名 = "字符串值"
示例:
int main() {
string str = "hello world";
cout << str << endl;
return 0;
}
2
3
4
5
注意:C++风格字符串,需要加入头文件==#include<string>==
# 5.2.4 综合案例与思考
综合案例:C风格与C++风格字符串对比
#include <iostream>
#include <cstring> // C风格字符串函数
#include <string> // C++字符串类
using namespace std;
int main() {
// 1. C风格字符串操作(容易出错)
cout << "=== C风格字符串 ===" << endl;
char cstr1[20] = "Hello";
char cstr2[20] = " World";
strcat(cstr1, cstr2); // 拼接(需确保缓冲区足够大)
cout << "拼接: " << cstr1 << endl;
cout << "长度: " << strlen(cstr1) << endl;
cout << "比较: " << strcmp("abc", "abd") << endl; // 负数
// 2. C++风格字符串(安全方便)
cout << "\n=== C++风格字符串 ===" << endl;
string s1 = "Hello";
string s2 = " World";
string s3 = s1 + s2; // 直接用+拼接,自动管理内存
cout << "拼接: " << s3 << endl;
cout << "长度: " << s3.length() << endl;
cout << "比较: " << (s1 < s2) << endl; // 直接用运算符
// 3. 互相转换
cout << "\n=== 互相转换 ===" << endl;
const char* fromCpp = s3.c_str(); // C++ -> C
string fromC = cstr1; // C -> C++
cout << "C字符串: " << fromCpp << endl;
cout << "C++字符串: " << fromC << endl;
return 0;
}
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
案例知识融合:这个案例直观对比了C风格字符串(字符数组+函数操作)和C++风格字符串(string类+运算符)的差异。C风格需要手动管理缓冲区和使用strcat/strlen/strcmp等函数,而C++的string可以直接用+拼接、<比较,自动管理内存更安全。同时展示了两种风格的互转方法。
思考题:
- C风格字符串使用
\0作为结束符,如果忘记了这个结束符会发生什么问题? string s = "Hello"中,"Hello"本身是C风格字符串还是C++字符串?转换过程是怎样的?- 为什么某些底层API(如操作系统API)仍然使用C风格字符串?什么时候必须使用
c_str()转换?
# 5.3 string类
# 5.3.1 string类
#include <string>,需要导入字符串头文件。然后 using std::string;
std::string 是 C++ 标准库提供的字符串类,功能更强大、更安全。
# 5.3.2 声明和初始化
string str = "Hello";
string str2("World");
2
# 5.3.3 常用操作
std::string 提供了丰富的成员函数和操作符,如 length、append、substr 等。
#include <iostream>
#include <string>
using namespace std;
int main() {
string str1 = "Hello";
string str2 = "World";
// 获取字符串长度
cout << "Length of str1: " << str1.length() << endl;
// 连接字符串
string result = str1 + " " + str2;
cout << "Concatenated string: " << result << endl;
// 提取子字符串
string substr = result.substr(6, 5);
cout << "Substring: " << substr << endl;
// 查找子字符串
size_t pos = result.find("World");
if (pos != string::npos) {
cout << "Found 'World' at position: " << pos << endl;
}
return 0;
}
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
输出:
Length of str1: 5
Concatenated string: Hello World
Substring: World
Found 'World' at position: 6
2
3
4
# 5.3.4 字符串转换
1.std::string 转 C 风格字符串 使用 c_str() 方法。
string str = "Hello";
const char *cstr = str.c_str();
cout << "C-style string: " << cstr << endl;
2
3
2.C 风格字符串转 std::string 直接赋值即可。
const char *cstr = "Hello";
string str = cstr;
cout << "std::string: " << str << endl;
2
3
# 5.3.5 字符串输入
使用 cin 或 getline。
int main() {
string str;
cout << "Enter a string: ";
getline(cin, str); // 读取一行输入
cout << "You entered: " << str << endl;
return 0;
}
2
3
4
5
6
7
# 5.3.6 字符串输出
使用 cout。
int main() {
string str = "Hello World";
cout << "String: " << str << endl;
return 0;
}
2
3
4
5
# 5.3.7 综合案例与思考
综合案例:字符串处理实战——简易文本分析器
#include <iostream>
#include <string>
#include <algorithm>
using namespace std;
int main() {
string text = " Hello, World! This is a C++ string demo. ";
// 1. 去除首尾空格
size_t start = text.find_first_not_of(' ');
size_t end = text.find_last_not_of(' ');
string trimmed = text.substr(start, end - start + 1);
cout << "去空格: [" << trimmed << "]" << endl;
// 2. 转小写
string lower = trimmed;
transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
cout << "小写: " << lower << endl;
// 3. 统计单词数(以空格分隔)
int wordCount = 0;
bool inWord = false;
for (char c : trimmed) {
if (c == ' ') {
inWord = false;
} else if (!inWord) {
inWord = true;
wordCount++;
}
}
cout << "单词数: " << wordCount << endl;
// 4. 查找和替换
string replaced = trimmed;
size_t pos = replaced.find("World");
if (pos != string::npos) {
replaced.replace(pos, 5, "C++");
}
cout << "替换后: " << replaced << endl;
// 5. 字符串与数字互转
int num = 42;
string numStr = to_string(num); // int -> string
int back = stoi(numStr); // string -> int
double pi = stod("3.14159"); // string -> double
cout << "数字转换: " << numStr << " -> " << back << ", pi=" << pi << endl;
return 0;
}
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
44
45
46
47
48
49
案例知识融合:这个案例综合运用了string类的常用操作——find_first_not_of去空格、transform配合tolower转小写、遍历统计单词、find+replace查找替换、以及to_string/stoi/stod数字与字符串互转。涵盖了声明初始化、常用操作、字符串转换、输入输出等全部知识点。
思考题:
string::find返回string::npos表示未找到,npos的值是什么?为什么不返回-1?cin >> str和getline(cin, str)读取字符串有什么区别?在什么场景下必须用getline?string的底层是如何管理内存的?频繁的字符串拼接(如循环中s += "x")效率如何?有什么优化方法?
# 5.4 数组
# 5.4.1 数组基本概念
在 C++ 中,数组 是一种用于存储相同类型元素的连续内存数据结构。数组的大小在声明时固定,不能动态改变。
数组是一组相同类型元素的集合,这些元素在内存中连续存储。数组的每个元素可以通过索引访问。
# 5.4.2 数组的声明
数据类型 数组名[数组大小];
数据类型:数组中元素的类型(如int、double等)。数组名:数组的名称。数组大小:数组中元素的数量,必须是一个常量表达式。
示例
int arr[5]; // 声明一个包含 5 个整数的数组
# 5.4.3 数组初始化
数组可以在声明时初始化,也可以后续赋值。
1.声明时初始化
int arr1[5] = {1, 2, 3, 4, 5}; // 完全初始化
int arr2[5] = {1, 2}; // 部分初始化,未初始化的元素为 0
int arr3[] = {1, 2, 3, 4, 5}; // 自动推断数组大小
2
3
2.后续赋值
int arr[5];
arr[0] = 10;
arr[1] = 20;
2
3
# 5.4.4 访问数组元素
数组元素通过索引访问,索引从 0 开始。
示例
#include <iostream>
using namespace std;
int main() {
int arr[5] = {10, 20, 30, 40, 50};
cout << "First element: " << arr[0] << endl;
cout << "Third element: " << arr[2] << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
# 5.4.5 多维数组
数据类型 数组名[行数][列数];
案例如下:
#include <iostream>
using namespace std;
int main() {
int arr[2][3] = {{1, 2, 3}, {4, 5, 6}};
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
cout << "arr[" << i << "][" << j << "] = " << arr[i][j] << endl;
}
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
# 5.4.6 数组作为函数参数
数组可以作为函数参数传递,通常以指针的形式传递。示例
#include <iostream>
using namespace std;
void printArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
cout << arr[i] << " ";
}
cout << endl;
}
int main() {
int arr[5] = {1, 2, 3, 4, 5};
printArray(arr, 5); // 传递数组和大小
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 5.4.7 动态数组
C++ 使用 new 和 delete 运算符动态分配和释放数组。
动态分配数组
int *arr = new int[5]; // 动态分配一个包含 5 个整数的数组
for (int i = 0; i < 5; i++) {
arr[i] = i + 1;
}
2
3
4
释放数组
delete[] arr; // 释放动态数组
# 5.4.8 数组的总结
- 数组是存储相同类型元素的连续内存数据结构。
- 数组的大小在声明时固定,不能动态改变。
- 数组元素通过索引访问,索引从
0开始。 - 数组可以作为函数参数传递,通常以指针的形式传递。
- 动态数组使用
new和delete进行内存管理。 - 标准库中的
std::array和std::vector比原生数组更安全、更灵活。
# 5.4.9 综合案例与思考
综合案例:数组的多种操作实战
#include <iostream>
#include <algorithm>
using namespace std;
// 打印数组
void printArray(const int arr[], int size) {
for (int i = 0; i < size; ++i)
cout << arr[i] << " ";
cout << endl;
}
// 查找元素(返回索引,找不到返回-1)
int findElement(const int arr[], int size, int target) {
for (int i = 0; i < size; ++i) {
if (arr[i] == target) return i;
}
return -1;
}
int main() {
// 1. 一维数组基本操作
cout << "=== 一维数组 ===" << endl;
int arr[] = {64, 25, 12, 22, 11};
int n = sizeof(arr) / sizeof(arr[0]);
cout << "原数组: "; printArray(arr, n);
sort(arr, arr + n); // STL排序
cout << "排序后: "; printArray(arr, n);
int idx = findElement(arr, n, 22);
cout << "22的位置: " << idx << endl;
// 2. 二维数组:矩阵转置
cout << "\n=== 矩阵转置 ===" << endl;
int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};
int transposed[3][2];
for (int i = 0; i < 2; ++i)
for (int j = 0; j < 3; ++j)
transposed[j][i] = matrix[i][j];
cout << "转置后:" << endl;
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 2; ++j)
cout << transposed[i][j] << " ";
cout << endl;
}
// 3. 动态数组
cout << "\n=== 动态数组 ===" << endl;
int size = 5;
int* dynArr = new int[size];
for (int i = 0; i < size; ++i)
dynArr[i] = (i + 1) * 10;
cout << "动态数组: ";
for (int i = 0; i < size; ++i)
cout << dynArr[i] << " ";
cout << endl;
delete[] dynArr; // 必须释放!
return 0;
}
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
案例知识融合:这个案例综合了数组的核心操作——声明初始化、sizeof计算元素个数、数组作为函数参数传递、STL排序、线性查找、二维数组的矩阵转置、以及动态数组的分配和释放。
思考题:
- 数组名在大多数情况下会"退化"为指针,那为什么在
sizeof(arr)中数组名不退化?还有哪些场景下数组名不退化? - 动态数组用
new[]分配后必须用delete[]释放,如果误用delete(不带[])会怎样? - C++中有原生数组、
std::array、std::vector三种"数组",它们各自的优缺点和适用场景是什么?
# 5.5 联合体
# 5.5.1 什么是联合体
在 C++ 中,联合体(Union) 是一种特殊的数据类型,它允许在相同的内存位置存储不同的数据类型。
联合体的所有成员共享同一块内存空间,因此联合体的大小等于其最大成员的大小。
联合体的主要用途是节省内存,尤其是在需要存储多种类型的数据但同一时间只使用其中一种的情况下。
# 5.5.2 联合体定义
联合体的定义语法与结构体类似,使用关键字 union。语法
union 联合体名 {
数据类型 成员1;
数据类型 成员2;
// ...
};
2
3
4
5
示例
union MyUnion {
int i;
float f;
char c;
};
2
3
4
5
# 5.5.3 联合体特点
- 共享内存: 联合体的所有成员共享同一块内存空间,修改一个成员会影响其他成员的值。
- 大小等于最大成员: 联合体的大小等于其最大成员的大小。
- 同一时间只能使用一个成员: 联合体在同一时间只能存储一个成员的值。
# 5.5.4 联合体使用
#include <iostream>
using namespace std;
union MyUnion {
int i;
float f;
char c;
};
int main() {
MyUnion u;
u.i = 10;
cout << "u.i = " << u.i << endl; // 10
u.f = 3.14;
cout << "u.f = " << u.f << endl; // 3.14
cout << "u.i = " << u.i << endl; // 不确定的值(内存被覆盖)
u.c = 'A';
cout << "u.c = " << u.c << endl; // A
cout << "u.i = " << u.i << endl; // 不确定的值(内存被覆盖)
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 5.5.5 联合体与结构体
| 特性 | 联合体(Union) | 结构体(Struct) |
|---|---|---|
| 内存分配 | 所有成员共享同一块内存 | 每个成员有自己的内存空间 |
| 大小 | 等于最大成员的大小 | 等于所有成员大小之和(考虑对齐) |
| 使用场景 | 同一时间只使用一个成员 | 同时使用多个成员 |
# 5.5.6 匿名联合体
匿名联合体没有名称,可以直接访问其成员。示例
#include <iostream>
using namespace std;
struct MyStruct {
int type;
union {
int i;
float f;
char c;
};
};
int main() {
MyStruct s;
s.type = 1;
s.i = 10;
cout << "s.i = " << s.i << endl;
s.type = 2;
s.f = 3.14;
cout << "s.f = " << s.f << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# 5.5.7 联合体应用场景
- 节省内存: 当需要存储多种类型的数据但同一时间只使用其中一种时,可以使用联合体。
- 类型转换: 联合体可以用于将一种类型的数据解释为另一种类型。
- 硬件编程: 在嵌入式系统中,联合体常用于访问硬件寄存器或处理二进制数据。
# 5.5.8 注意事项
- 成员覆盖: 修改一个成员的值会覆盖其他成员的值,因此需要小心使用。
- 初始化: 联合体只能初始化第一个成员。
- 类型安全: 联合体不提供类型安全检查,容易导致错误。
# 5.5.9 综合案例与思考
综合案例:用联合体实现轻量级变体类型
#include <iostream>
#include <string>
using namespace std;
// 用枚举+联合体实现简单的Variant类型
enum class ValueType { INT, FLOAT, CHAR };
struct Variant {
ValueType type;
union {
int intVal;
float floatVal;
char charVal;
};
void print() const {
switch (type) {
case ValueType::INT:
cout << "Int: " << intVal << endl; break;
case ValueType::FLOAT:
cout << "Float: " << floatVal << endl; break;
case ValueType::CHAR:
cout << "Char: " << charVal << endl; break;
}
}
};
// 用联合体检测系统字节序
bool isLittleEndian() {
union {
int i;
char c;
} test;
test.i = 1;
return test.c == 1; // 小端:低字节在低地址
}
int main() {
// 1. Variant类型使用
cout << "=== Variant类型 ===" << endl;
Variant v1 = {ValueType::INT, {.intVal = 42}};
Variant v2 = {ValueType::FLOAT, {.floatVal = 3.14f}};
Variant v3 = {ValueType::CHAR, {.charVal = 'X'}};
v1.print();
v2.print();
v3.print();
// 2. 内存共享验证
cout << "\n=== 内存共享 ===" << endl;
union { int i; float f; char bytes[4]; } u;
u.i = 0x41424344;
cout << "int: 0x" << hex << u.i << endl;
cout << "字节: ";
for (int k = 0; k < 4; ++k)
cout << u.bytes[k] << " ";
cout << endl;
// 3. 字节序检测
cout << dec << "\n=== 字节序 ===" << endl;
cout << "系统字节序: " << (isLittleEndian() ? "小端" : "大端") << endl;
// 4. 联合体大小
cout << "\n=== 大小对比 ===" << endl;
cout << "Variant大小: " << sizeof(Variant) << " 字节" << endl;
cout << "union{int,float,char}大小: " << sizeof(u) << " 字节" << endl;
return 0;
}
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
案例知识融合:这个案例展示了联合体的实际应用——配合枚举实现轻量级变体类型(类似简化版std::variant)、利用内存共享特性查看数据的字节表示、以及用联合体检测系统字节序。涵盖了联合体的定义、使用、匿名联合体、内存共享特点和实际应用场景。
思考题:
- C++17引入了
std::variant作为类型安全的联合体替代品,它和传统联合体相比有什么优势? - 用联合体进行类型双关(type punning,如将int的字节解释为float)在C++中是否是未定义行为?有什么安全的替代方式?
- 联合体中可以包含有构造函数的类型吗(如
std::string)?C++11对此有什么变化?
# 5.6 枚举
枚举(Enumeration) 是一种用户定义的数据类型,用于表示一组命名的整数常量。枚举可以提高代码的可读性和可维护性,因为它允许使用有意义的名称代替硬编码的整数值。
C++ 支持两种枚举类型:
- 无作用域枚举(C-style enum)
- 有作用域枚举(C++11 引入的
enum class)
# 5.6.1 无作用域枚举
无作用域枚举是 C 语言风格的枚举,枚举常量在全局作用域中可见。语法
enum 枚举名 {
枚举常量1,
枚举常量2,
// ...
};
2
3
4
5
示例
#include <iostream>
using namespace std;
enum Color {
Red,
Green,
Blue
};
int main() {
Color c = Red;
cout << "c = " << c << endl; // 0
if (c == Red) {
cout << "The color is Red!" << endl;
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
显式指定值
enum Color {
Red = 1,
Green = 2,
Blue = 4
};
2
3
4
5
# 5.6.2 有作用域枚举
C++11 引入了有作用域枚举(enum class),枚举常量在枚举的作用域内可见,避免了命名冲突。
语法
enum class 枚举名 {
枚举常量1,
枚举常量2,
// ...
};
2
3
4
5
示例
#include <iostream>
using namespace std;
enum class Color {
Red,
Green,
Blue
};
int main() {
Color c = Color::Red;
cout << "c = " << static_cast<int>(c) << endl; // 0
if (c == Color::Red) {
cout << "The color is Red!" << endl;
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 5.6.3 枚举底层类型
默认情况下,枚举的底层类型是 int,但可以显式指定其他整数类型。示例
#include <iostream>
using namespace std;
enum class Color : char {
Red = 'R',
Green = 'G',
Blue = 'B'
};
int main() {
Color c = Color::Red;
cout << "c = " << static_cast<char>(c) << endl; // R
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 5.6.4 枚举应用场景
- 状态表示:使用枚举表示程序的状态或模式。
enum class State { Idle, Running, Paused, Stopped }; - 选项标志:使用枚举表示选项或标志。
enum class Options { None = 0, Read = 1, Write = 2, Execute = 4 }; - 提高可读性:使用枚举代替硬编码的整数值,提高代码的可读性。
# 5.6.5 枚举注意事项
- 命名冲突: 无作用域枚举的常量在全局作用域中可见,可能导致命名冲突。
- 类型安全: 有作用域枚举更安全,不会隐式转换为整数。
- 底层类型: 可以显式指定枚举的底层类型以节省内存。
| 特性 | enum class | 传统 enum |
|---|---|---|
| 作用域 | 限定在枚举类型内部 | 全局 |
| 类型安全性 | 强类型,不能隐式转换 | 弱类型,可以隐式转换 |
| 底层类型 | 可指定(默认 int) | 由编译器决定 |
| 代码可读性与维护性 | 更安全、更易维护 | 可能导致命名冲突和类型错误 |
# 5.6.6 类型安全性
enum class:是强类型的,不能隐式转换为整数或其他类型。需要显式类型转换才能与整数或其他类型交互。
int value = static_cast<int>(TimeUnit::NANOSECONDS); // 需要显式转换
传统 enum:是弱类型的,可以隐式转换为整数或其他类型。
int value = NANOSECONDS; // 隐式转换为整数
# 5.6.7 综合案例与思考
综合案例:用枚举实现状态机
#include <iostream>
#include <string>
using namespace std;
// 有作用域枚举:交通灯状态
enum class TrafficLight : int { Red = 0, Yellow = 1, Green = 2 };
// 枚举转字符串
string toString(TrafficLight light) {
switch (light) {
case TrafficLight::Red: return "红灯(停)";
case TrafficLight::Yellow: return "黄灯(注意)";
case TrafficLight::Green: return "绿灯(行)";
default: return "未知";
}
}
// 状态转换
TrafficLight nextState(TrafficLight current) {
int next = (static_cast<int>(current) + 1) % 3;
return static_cast<TrafficLight>(next);
}
// 无作用域枚举的命名冲突演示
namespace Direction {
enum Dir { Up, Down, Left, Right };
}
// 有作用域枚举不会冲突
enum class Color { Red, Green, Blue };
// TrafficLight::Red 和 Color::Red 不冲突!
int main() {
// 1. 状态机模拟
cout << "=== 交通灯状态机 ===" << endl;
TrafficLight light = TrafficLight::Red;
for (int i = 0; i < 6; ++i) {
cout << "当前: " << toString(light) << endl;
light = nextState(light);
}
// 2. 类型安全演示
cout << "\n=== 类型安全 ===" << endl;
Color c = Color::Red;
// if (c == TrafficLight::Red) {} // 编译错误!不同枚举类型不能比较
// int n = c; // 编译错误!不能隐式转int
int n = static_cast<int>(c); // 必须显式转换
cout << "Color::Red = " << n << endl;
// 3. 指定底层类型节省内存
cout << "\n=== 底层类型 ===" << endl;
cout << "TrafficLight大小: " << sizeof(TrafficLight) << " 字节" << endl;
return 0;
}
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
44
45
46
47
48
49
50
51
52
53
54
案例知识融合:这个案例用交通灯状态机展示了枚举的核心用法——有作用域枚举enum class的定义、枚举与整数的显式转换、状态转换逻辑、枚举转字符串、类型安全特性(不同枚举类型不能隐式比较)、以及指定底层类型。
思考题:
enum class不能隐式转换为整数,这带来了类型安全但有时也不方便。有没有优雅的方式让enum class支持位运算(如权限标志)?- 枚举的
switch语句如果漏掉了某个case,编译器能发出警告吗?如何利用这个特性? - C++中没有内置的"枚举转字符串"功能,你知道有哪些方法可以实现枚举和字符串的自动映射?
# 5.7 结构体
# 5.7.1 结构体定义
结构体(struct) 是一种用户定义的数据类型,用于将不同类型的数据组合在一起。以便更方便地管理和操作这些数据。
结构体的定义使用 struct 关键字,语法如下:
struct 结构体名 {
数据类型 成员1;
数据类型 成员2;
// ...
};
2
3
4
5
# 5.7.2 结构体创建
通过结构体创建变量的方式有三种:
- struct 结构体名 变量名
- struct 结构体名 变量名 = { 成员1值 , 成员2值...}
- 定义结构体时顺便创建变量
示例:
//结构体定义
struct student {
//成员列表
string name; //姓名
int age; //年龄
int score; //分数
} stu3; //结构体变量创建方式3
int main() {
//结构体变量创建方式1
struct student stu1; //struct 关键字可以省略
stu1.name = "张三";
stu1.age = 18;
stu1.score = 100;
cout << "姓名:" << stu1.name << " 年龄:" << stu1.age << " 分数:" << stu1.score << endl;
//结构体变量创建方式2
struct student stu2 = { "李四",19,60 };
cout << "姓名:" << stu2.name << " 年龄:" << stu2.age << " 分数:" << stu2.score << endl;
//结构体变量创建方式3
stu3.name = "王五";
stu3.age = 18;
stu3.score = 80;
cout << "姓名:" << stu3.name << " 年龄:" << stu3.age << " 分数:" << stu3.score << endl;
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
总结1:定义结构体时的关键字是struct,不可省略
总结2:创建结构体变量时,关键字struct可以省略
总结3:结构体变量利用操作符 ''.'' 访问成员
结构体的作用:
- 组织数据:结构体允许你将相关的数据项组合在一起,形成一个逻辑单元,便于管理和操作。
- 传递复杂数据:结构体可以作为函数参数传递,方便传递和操作复杂的数据结构。
- 定义自定义数据类型:通过结构体,你可以定义自己的数据类型,使代码更具可读性和可维护性。
# 5.7.3 结构体与函数
# 5.7.4 结构体数组
作用:将自定义的结构体放入到数组中方便维护。
语法:struct 结构体名 数组名[元素个数] = { {} , {} , ... {} }
结构体数组是一个数组,其中每个元素都是一个结构体实例。结构体数组允许你在一个数组中存储多个结构体实例,每个实例可以包含多个数据成员。示例:
#include <iostream>
#include <string>
using namespace std;
struct Student {
string name;
int score;
};
int main() {
Student students[3] = {
{"Alice", 90},
{"Bob", 85},
{"Charlie", 95}
};
for (int i = 0; i < 3; i++) {
cout << "Name: " << students[i].name << ", Score: " << students[i].score << endl;
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
输出:
Name: Alice, Score: 90
Name: Bob, Score: 85
Name: Charlie, Score: 95
2
3
# 5.7.5 结构体嵌套
作用: 结构体中的成员可以是另一个结构体
结构体嵌套是指在一个结构体中包含另一个结构体作为其成员。这种结构体嵌套的方式允许你创建更复杂的数据结构,将多个相关的数据项组合在一起,以便更方便地管理和操作这些数据。
例如:每个老师辅导一个学员,一个老师的结构体中,记录一个学生的结构体
示例:
//学生结构体定义
struct student {
//成员列表
string name; //姓名
int age; //年龄
int score; //分数
};
//教师结构体定义
struct teacher {
//成员列表
int id; //职工编号
string name; //教师姓名
int age; //教师年龄
struct student stu; //子结构体 学生
};
int main() {
struct teacher t1;
t1.id = 10000;
t1.name = "老王";
t1.age = 40;
t1.stu.name = "张三";
t1.stu.age = 18;
t1.stu.score = 100;
cout << "教师 职工编号: " << t1.id << " 姓名: " << t1.name << " 年龄: " << t1.age << endl;
cout << "辅导学员 姓名: " << t1.stu.name << " 年龄:" << t1.stu.age << " 考试分数: " << t1.stu.score << endl;
return 0;
}
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
总结:在结构体中可以定义另一个结构体作为成员,用来解决实际问题
# 5.7.6 结构体案例
打印学生成绩
学校正在做毕设项目,每名老师带领5个学生,总共有3名老师,需求如下
设计学生和老师的结构体,其中在老师的结构体中,有老师姓名和一个存放5名学生的数组作为成员
学生的成员有姓名、考试分数,创建数组存放3名老师,通过函数给每个老师及所带的学生赋值
最终打印出老师数据以及老师所带的学生数据。
示例:
struct Student {
string name;
int score;
};
struct Teacher {
string name;
Student sArray[5];
};
void allocateSpace(Teacher tArray[] , int len) {
string tName = "教师";
string sName = "学生";
string nameSeed = "ABCDE";
for (int i = 0; i < len; i++) {
tArray[i].name = tName + nameSeed[i];
for (int j = 0; j < 5; j++) {
tArray[i].sArray[j].name = sName + nameSeed[j];
tArray[i].sArray[j].score = rand() % 61 + 40;
}
}
}
void printTeachers(Teacher tArray[], int len) {
for (int i = 0; i < len; i++) {
cout << tArray[i].name << endl;
for (int j = 0; j < 5; j++) {
cout << "\t姓名:" << tArray[i].sArray[j].name << " 分数:" << tArray[i].sArray[j].score << endl;
}
}
}
int main() {
srand((unsigned int)time(NULL)); //随机数种子 头文件 #include <ctime>
Teacher tArray[3]; //老师数组
int len = sizeof(tArray) / sizeof(Teacher);
allocateSpace(tArray, len); //创建数据
printTeachers(tArray, len); //打印数据
return 0;
}
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
英雄排名
设计一个英雄的结构体,包括成员姓名,年龄,性别;创建结构体数组,数组中存放5名英雄。
通过冒泡排序的算法,将数组中的英雄按照年龄进行升序排序,最终打印排序后的结果。
五名英雄信息如下:
{"刘备",23,"男"},
{"关羽",22,"男"},
{"张飞",20,"男"},
{"赵云",21,"男"},
{"貂蝉",19,"女"},
2
3
4
5
示例:
//英雄结构体
struct hero
{
string name;
int age;
string sex;
};
//冒泡排序
void bubbleSort(hero arr[] , int len)
{
for (int i = 0; i < len - 1; i++)
{
for (int j = 0; j < len - 1 - i; j++)
{
if (arr[j].age > arr[j + 1].age)
{
hero temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}
//打印数组
void printHeros(hero arr[], int len) {
for (int i = 0; i < len; i++) {
cout << "姓名: " << arr[i].name << " 性别: " << arr[i].sex << " 年龄: " << arr[i].age << endl;
}
}
int main() {
struct hero arr[5] = {
{"刘备",23,"男"},
{"关羽",22,"男"},
{"张飞",20,"男"},
{"赵云",21,"男"},
{"貂蝉",19,"女"},
};
int len = sizeof(arr) / sizeof(hero); //获取数组元素个数
bubbleSort(arr, len); //排序
printHeros(arr, len); //打印
return 0;
}
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
# 5.7.7 综合案例与思考
综合案例:用结构体构建通讯录系统
#include <iostream>
#include <string>
using namespace std;
const int MAX_CONTACTS = 100;
struct Contact {
string name;
string phone;
int age;
};
struct AddressBook {
Contact contacts[MAX_CONTACTS];
int count = 0;
// 添加联系人
bool add(const string& name, const string& phone, int age) {
if (count >= MAX_CONTACTS) return false;
contacts[count++] = {name, phone, age};
return true;
}
// 查找联系人
int find(const string& name) const {
for (int i = 0; i < count; ++i) {
if (contacts[i].name == name) return i;
}
return -1;
}
// 删除联系人
bool remove(const string& name) {
int idx = find(name);
if (idx == -1) return false;
contacts[idx] = contacts[--count]; // 用最后一个填充
return true;
}
// 打印所有联系人
void printAll() const {
cout << "--- 通讯录 (" << count << "人) ---" << endl;
for (int i = 0; i < count; ++i) {
cout << " " << contacts[i].name
<< " | " << contacts[i].phone
<< " | " << contacts[i].age << "岁" << endl;
}
}
};
int main() {
AddressBook book;
book.add("张三", "13800000001", 25);
book.add("李四", "13800000002", 30);
book.add("王五", "13800000003", 28);
book.printAll();
cout << "\n查找李四: ";
int idx = book.find("李四");
if (idx >= 0)
cout << "找到!电话: " << book.contacts[idx].phone << endl;
cout << "\n删除李四后:" << endl;
book.remove("李四");
book.printAll();
return 0;
}
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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
案例知识融合:这个案例使用结构体嵌套(AddressBook包含Contact数组)构建了一个完整的通讯录系统,涵盖了结构体定义、结构体创建和初始化、结构体数组、结构体嵌套、结构体作为函数参数、以及在结构体中定义成员函数(C++特性)。
思考题:
- C++中
struct可以包含成员函数,这和class有什么区别?在什么情况下你会选择用struct而不是class? - 结构体作为函数参数时,值传递和引用传递(
const &)有什么性能差异?什么时候应该用引用传递? - 这个通讯录案例中,删除操作用"最后一个元素填充"的方式,这种方法有什么优缺点?有更好的删除策略吗?
# 5.8 容器
# 5.8.1 vector
动态数组,支持随机访问。
#include <iostream>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
vec.push_back(6); // 添加元素
for (int i : vec) {
std::cout << i << " "; // 输出: 1 2 3 4 5 6
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
# 5.8.2 list
双向链表,支持高效插入和删除。
#include <iostream>
#include <list>
int main() {
std::list<int> lst = {1, 2, 3, 4, 5};
lst.push_front(0); // 在头部添加元素
for (int i : lst) {
std::cout << i << " "; // 输出: 0 1 2 3 4 5
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
# 5.8.4 map
键值对容器,基于红黑树实现,按键排序。
std::map 的特点
- 键值对存储: 每个元素是一个
pair<const Key, Value>,其中Key是键,Value是值。 - 自动排序: 元素按照键的顺序自动排序(默认是升序)。
- 唯一键: 每个键在
std::map中只能出现一次。 - 高效查找: 基于红黑树实现,查找、插入和删除的时间复杂度为
O(log n)。
插入元素:使用 insert 方法或 [] 操作符插入元素。
mapName.insert(std::make_pair(key, value)); // 使用 insert
mapName[key] = value; // 使用 [] 操作符
2
访问元素:使用 [] 操作符或 at 方法访问元素。
ValueType value = mapName[key]; // 使用 [] 操作符
ValueType value = mapName.at(key); // 使用 at 方法
2
删除元素:使用 erase 方法删除元素。
mapName.erase(key); // 删除指定键的元素
查找元素:使用 find 方法查找元素。
auto it = mapName.find(key);
if (it != mapName.end()) {
// 找到元素
} else {
// 未找到元素
}
2
3
4
5
6
基本操作
#include <iostream>
#include <map>
using namespace std;
int main() {
// 定义 map
map<string, int> ageMap;
// 插入元素
ageMap["Alice"] = 25;
ageMap["Bob"] = 30;
ageMap.insert(make_pair("Charlie", 35));
// 访问元素
cout << "Alice's age: " << ageMap["Alice"] << endl;
cout << "Bob's age: " << ageMap.at("Bob") << endl;
// 查找元素
auto it = ageMap.find("Charlie");
if (it != ageMap.end()) {
cout << "Charlie's age: " << it->second << endl;
} else {
cout << "Charlie not found" << endl;
}
// 删除元素
ageMap.erase("Bob");
// 遍历 map
for (const auto& pair : ageMap) {
cout << "Key: " << pair.first << ", Value: " << pair.second << endl;
}
return 0;
}
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
# 5.8.4 unordered_map
基于哈希表的键值对容器,查找效率高。
#include <iostream>
#include <unordered_map>
int main() {
std::unordered_map<std::string, int> um;
um["Alice"] = 25;
um["Bob"] = 30;
for (const auto &pair : um) {
std::cout << pair.first << ": " << pair.second << std::endl;
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
# 5.8.5 array
#include <iostream>
#include <array>
using namespace std;
int main() {
array<int, 5> arr = {1, 2, 3, 4, 5};
for (int i = 0; i < arr.size(); i++) {
cout << arr[i] << " ";
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
# 5.8.6 综合案例与思考
综合案例:用STL容器实现学生成绩管理
#include <iostream>
#include <string>
#include <vector>
#include <map>
#include <list>
#include <array>
#include <algorithm>
using namespace std;
int main() {
// 1. vector:动态数组存储成绩
cout << "=== vector ===" << endl;
vector<int> scores = {85, 92, 78, 96, 88};
scores.push_back(90); // 添加
sort(scores.begin(), scores.end());
for (int s : scores) cout << s << " ";
cout << endl;
// 2. map:姓名->成绩的映射
cout << "\n=== map ===" << endl;
map<string, int> gradeBook;
gradeBook["张三"] = 92;
gradeBook["李四"] = 85;
gradeBook["王五"] = 96;
for (const auto& [name, score] : gradeBook) {
cout << name << ": " << score << endl;
}
// 3. list:频繁插入删除
cout << "\n=== list ===" << endl;
list<string> waitlist = {"学生A", "学生B", "学生C"};
waitlist.push_front("学生VIP"); // 头部插入O(1)
auto it = find(waitlist.begin(), waitlist.end(), "学生B");
waitlist.insert(it, "学生插队"); // 中间插入O(1)
for (const auto& s : waitlist) cout << s << " -> ";
cout << "end" << endl;
// 4. array:固定大小数组
cout << "\n=== array ===" << endl;
array<int, 5> fixed = {10, 20, 30, 40, 50};
cout << "大小: " << fixed.size() << ", 首元素: " << fixed.front() << endl;
// 5. 容器选择建议
cout << "\n=== 容器选择 ===" << endl;
cout << "随机访问多 -> vector" << endl;
cout << "频繁插删 -> list" << endl;
cout << "键值查找 -> map/unordered_map" << endl;
cout << "固定大小 -> array" << endl;
return 0;
}
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
44
45
46
47
48
49
50
51
案例知识融合:这个案例在一个学生管理场景中展示了四种常用容器的特点和用法——vector动态数组(支持排序和随机访问)、map有序键值对(按键自动排序)、list双向链表(高效的头部/中间插入)、array固定大小数组(编译时确定大小)。
思考题:
vector和list都能存储一组元素,但性能特点完全不同。在什么场景下list比vector更合适?map和unordered_map都是键值对容器,底层分别用红黑树和哈希表实现。它们各自的时间复杂度和适用场景是什么?- 为什么C++11推荐用
std::array替代原生数组?std::array有哪些原生数组不具备的安全特性?
# 5.9 练习题
# 5.9.1 数组冒泡排序
冒泡排序案例,作用: 最常用的排序算法,对数组内元素进行排序
- 比较相邻的元素。如果第一个比第二个大,就交换他们两个。
- 对每一对相邻元素做同样的工作,执行完毕后,找到第一个最大值。
- 重复以上的步骤,每次比较次数-1,直到不需要比较
示例: 将数组 { 4,2,8,0,5,7,1,3,9 } 进行升序排序
int main() {
int arr[9] = {4, 2, 8, 0, 5, 7, 1, 3, 9};
for (int i = 0; i < 9 - 1; i++) {
for (int j = 0; j < 9 - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
for (int i = 0; i < 9; i++) {
cout << arr[i] << endl;
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 5.9.2 数组成绩统计
案例描述:有三名同学(张三,李四,王五),在一次考试中的成绩分别如下表,请分别输出三名同学的总成绩
| 语文 | 数学 | 英语 | |
|---|---|---|---|
| 张三 | 100 | 100 | 100 |
| 李四 | 90 | 50 | 100 |
| 王五 | 60 | 70 | 80 |
参考答案:
int main() {
int scores[3][3] =
{
{100, 100, 100},
{90, 50, 100},
{60, 70, 80},
};
string names[3] = {"张三", "李四", "王五"};
for (int i = 0; i < 3; i++) {
int sum = 0;
for (int j = 0; j < 3; j++) {
sum += scores[i][j];
}
cout << names[i] << "同学总成绩为: " << sum << endl;
}
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# 5.X 卷一改造增补:用现代容器替代裸数组与裸 union
本节为卷一新增。下面这些「老写法」在新代码里基本不该再出现。
# 5.X.1 数组 → std::array / std::vector
// 老写法
int arr[10] = {0};
int (*p)[10] = &arr; // 数组指针折磨人
// 现代写法
std::array<int, 10> fixed{}; // 编译期固定大小,零开销
std::vector<int> dynamic(10); // 运行期可变长,自动管理
// 函数参数:永远用 span(C++20)
void process(std::span<const int> s) { /* 同时接受 array/vector/C 数组 */ }
2
3
4
5
6
7
8
9
10
# 5.X.2 字符串视图 std::string_view(C++17)
void log(std::string_view sv); // 零拷贝;接受 const char*、std::string、char[]
90% 的「只读字符串形参」都该改成 string_view——节省一次构造,参数兼容性还更好。
# 5.X.3 联合体 → std::variant(C++17)
// 老 union(不知道当前是哪个成员、不能放非平凡类型)
union Value { int i; double d; char* s; };
// 现代:variant 类型安全 + 支持 std::string 等
std::variant<int, double, std::string> v = "hello";
std::visit([](auto&& x){ std::cout << x; }, v);
2
3
4
5
6
# 5.X.4 强类型枚举 enum class(C++11)
enum class Color : uint8_t { Red, Green, Blue };
Color c = Color::Red; // 必须带类型名,杜绝命名冲突
// int x = c; // 编译错误:禁止隐式转 int
2
3
# 5.X.5 推荐阅读
- 卷一 §2.9
03.数据类型.md——byte/optional/variant/any - 卷一
18.特性图谱§C++17 / C++20 - 卷三《模板与泛型卷》—— 容器与算法的深度展开
# 5.Y 新手陷阱 Top 5
| # | 陷阱 | 说明 |
|---|---|---|
| 1 | C 字符串忘了 \0 | char s[5]="hello" 编译错(少一位);用 std::string |
| 2 | 函数参数接收数组 | void f(int a[10]) 实际退化成 int*,sizeof(a)==指针大小 |
| 3 | 局部数组当返回值 | 返回栈上数组指针 → 悬挂指针;返回 vector |
| 4 | union 放非平凡类型 | C++ 中 union {std::string s;} 必须手动管理生命周期 |
| 5 | 普通 enum 隐式转 int | 用 enum class |