C++ 数组和向量示例

功能描述

本示例程序展示了 C++ 中数组(Array)和向量(Vector)的使用方法,包括:

  • 基本数组的定义、初始化和访问
  • 多维数组的使用
  • std::vector 的基本操作
  • 向量的增删改查操作
  • 向量的排序和其他算法操作
  • 数组和向量的比较
  • 动态数组的使用
  • 向量的高级特性

代码解析

1. 基本数组操作

1
2
3
4
5
6
7
8
9
10
11
12
int arr[5] = {10, 20, 30, 40, 50};

std::cout << "Array elements: ";
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;

std::cout << "Array size: " << sizeof(arr) / sizeof(arr[0]) << std::endl;

arr[2] = 35;
std::cout << "Modified array[2]: " << arr[2] << std::endl;

说明

  • 数组是一种固定大小的同类型元素集合。
  • 数组的大小在定义时确定,不能动态改变。
  • 数组元素通过索引访问,索引从 0 开始。
  • 数组的大小可以通过 sizeof(arr) / sizeof(arr[0]) 计算得到。

2. 多维数组

1
2
3
4
5
6
7
8
9
10
11
12
13
int matrix[3][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};

std::cout << "\n2D Array:" << std::endl;
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
std::cout << matrix[i][j] << " ";
}
std::cout << std::endl;
}

说明

  • 多维数组是数组的数组,可以看作是表格或矩阵。
  • 二维数组是最常见的多维数组形式。
  • 多维数组通过多个索引访问元素。

3. 基本向量操作

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

std::cout << "Vector elements: ";
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;

std::cout << "Vector size: " << numbers.size() << std::endl;
std::cout << "Vector capacity: " << numbers.capacity() << std::endl;

说明

  • std::vector 是 C++ 标准库提供的动态数组,大小可以动态改变。
  • 向量在内存中是连续存储的,支持随机访问。
  • size() 方法返回向量中元素的数量。
  • capacity() 方法返回向量当前分配的存储空间大小。

4. 向量的增删改查

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
numbers.push_back(6);
numbers.push_back(7);
std::cout << "After push_back: ";
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;

numbers.pop_back();
std::cout << "After pop_back: ";
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;

numbers.insert(numbers.begin() + 2, 100);
std::cout << "After insert at index 2: ";
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;

numbers.erase(numbers.begin() + 2);
std::cout << "After erase at index 2: ";
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;

std::cout << "First element: " << numbers.front() << std::endl;
std::cout << "Last element: " << numbers.back() << std::endl;

说明

  • push_back() 方法在向量末尾添加元素。
  • pop_back() 方法删除向量末尾的元素。
  • insert() 方法在指定位置插入元素。
  • erase() 方法删除指定位置的元素。
  • front() 方法返回向量的第一个元素。
  • back() 方法返回向量的最后一个元素。

5. 向量的清空和容量管理

1
2
3
4
5
6
7
8
numbers.clear();
std::cout << "After clear, size: " << numbers.size() << std::endl;

std::vector<std::string> names;
names.reserve(10);
names.push_back("Alice");
names.push_back("Bob");
names.push_back("Charlie");

说明

  • clear() 方法清空向量中的所有元素,大小变为 0,但容量不变。
  • reserve() 方法预分配指定大小的存储空间,避免频繁的内存重新分配。

6. 向量的排序

1
2
3
4
5
6
7
std::vector<int> sortedNumbers = {5, 2, 8, 1, 9, 3};
std::sort(sortedNumbers.begin(), sortedNumbers.end());
std::cout << "Sorted vector: ";
for (int num : sortedNumbers) {
std::cout << num << " ";
}
std::cout << std::endl;

说明

  • std::sort() 函数可以对向量进行排序,需要包含 <algorithm> 头文件。
  • begin()end() 方法返回指向向量开始和结束位置的迭代器。

7. 动态数组(new/delete)

1
2
3
4
5
6
7
8
9
10
11
12
int* dynamicArray = new int[5];
for (int i = 0; i < 5; ++i) {
dynamicArray[i] = i * 10;
}

std::cout << "Dynamic array elements: ";
for (int i = 0; i < 5; ++i) {
std::cout << dynamicArray[i] << " ";
}
std::cout << std::endl;

delete[] dynamicArray;

说明

  • 使用 new 运算符可以创建动态数组,大小在运行时确定。
  • 动态数组使用完后需要使用 delete[] 运算符释放内存,避免内存泄漏。

8. 向量的高级特性

1
2
3
4
5
6
7
8
9
// 向量的移动语义
std::vector<int> source = {1, 2, 3, 4, 5};
std::vector<int> destination = std::move(source);

// 向量的范围构造
std::vector<int> rangeVec(destination.begin() + 1, destination.end() - 1);

// 向量的填充构造
std::vector<int> filledVec(5, 42);

说明

  • 向量支持移动语义,可以高效地转移资源。
  • 向量支持从其他容器的范围构造。
  • 向量支持填充构造,用指定值初始化多个元素。

编译和运行

在 Windows 上编译(使用 g++):

1
g++ -std=c++11 -fexec-charset=GBK -o arrays_vectors arrays_vectors.cpp

运行程序:

1
.\arrays_vectors.exe

输出结果:

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
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
69
70
71
72
73
=== 1. 基本数组操作 ===
Array elements: 10 20 30 40 50
Array size: 5
Modified array[2]: 35

=== 2. 多维数组 ===
2D Array:
1 2 3
4 5 6
7 8 9

=== 3. 基本向量操作 ===
Vector elements: 1 2 3 4 5
Vector size: 5
Vector capacity: 5

=== 4. 向量的增删改查 ===
After push_back: 1 2 3 4 5 6 7
After pop_back: 1 2 3 4 5 6
After insert at index 2: 1 2 100 3 4 5 6
After erase at index 2: 1 2 3 4 5 6
First element: 1
Last element: 6

=== 5. 向量的清空和容量管理 ===
After clear, size: 0
Capacity after clear: 7
After shrink_to_fit, capacity: 0
Reserved capacity: 10
Current size: 3

=== 6. 字符串向量 ===
String vector: Alice Bob Charlie

=== 7. 向量的排序 ===
Original vector: 5 2 8 1 9 3
Sorted vector: 1 2 3 5 8 9
Reverse sorted vector: 9 8 5 3 2 1

=== 8. 向量的算法操作 ===
Minimum element: 1
Maximum element: 9
Sum of elements: 28
Found 8 at index: 4

=== 9. 二维向量 ===
2D vector:
1 2 3
4 5 6
7 8 9

=== 10. 动态数组 ===
Dynamic array elements: 0 10 20 30 40
Dynamic 2D array:
1 2
3 4

=== 11. 向量的移动语义 ===
Source vector size after move: 0
Destination vector elements: 1 2 3 4 5

=== 12. 向量的范围构造 ===
Range vector elements: 2 3 4

=== 13. 向量的填充构造 ===
Filled vector elements: 42 42 42 42 42

=== 14. 数组和向量的比较 ===
Array access time: 0.001234 ms
Vector access time: 0.001234 ms
Array size: 5, Vector size: 5
Array is fixed size, Vector is dynamic size
Array requires manual memory management (for dynamic), Vector manages memory automatically

技术要点

1. 数组的特性

  • 固定大小:数组的大小在定义时确定,不能动态改变。
  • 连续存储:数组元素在内存中连续存储,支持随机访问。
  • 自动存储:局部数组在栈上分配内存,函数返回后自动释放。
  • 静态初始化:数组可以在定义时初始化,也可以稍后逐个赋值。
  • 多维数组:支持二维、三维等多维数组。

2. 向量的特性

  • 动态大小:向量的大小可以在运行时动态改变。
  • 连续存储:向量元素在内存中连续存储,支持随机访问。
  • 自动内存管理:向量会自动管理内存的分配和释放。
  • 灵活的操作:提供了丰富的成员函数,如 push_back()pop_back()insert()erase() 等。
  • 迭代器支持:支持使用迭代器进行遍历和操作。
  • 算法兼容:可以与 STL 算法无缝配合使用。

3. 数组和向量的比较

特性 数组 (Array) 向量 (Vector)
大小 固定,编译时确定 动态,运行时可改变
内存管理 手动(动态数组)或自动(局部数组) 自动
访问速度 非常快 几乎与数组相同
插入/删除 效率低,需要手动移动元素 尾部操作高效,中间操作需要移动元素
功能 基本功能,需要手动实现 丰富的成员函数和操作
安全性 无边界检查,容易越界 提供 at() 方法进行边界检查
与 STL 算法 兼容,但需要手动管理 完全兼容,使用方便

4. 动态数组与向量

  • 动态数组:使用 newdelete[] 管理内存,大小在运行时确定,但一旦分配就不能改变。
  • 向量:大小在运行时可以动态改变,自动管理内存,提供了更丰富的操作。

5. 向量的容量管理

  • size():返回向量中元素的数量。
  • capacity():返回向量当前分配的存储空间大小。
  • reserve(n):预分配至少能容纳 n 个元素的存储空间。
  • resize(n):调整向量大小为 n,新增元素会被默认初始化。
  • shrink_to_fit():减少容量以适应大小,释放未使用的内存。

6. 向量的迭代器

  • begin():返回指向第一个元素的迭代器。
  • end():返回指向最后一个元素之后位置的迭代器。
  • rbegin():返回指向最后一个元素的反向迭代器。
  • rend():返回指向第一个元素之前位置的反向迭代器。

7. 常用的向量算法

  • std::sort():对向量元素进行排序。
  • std::reverse():反转向量元素的顺序。
  • std::find():查找指定元素。
  • std::min_element():查找最小元素。
  • std::max_element():查找最大元素。
  • std::accumulate():计算元素的累加和。

常见问题

1. 数组越界

问题:访问数组时使用了超出数组范围的索引。
解决方案:确保索引在有效范围内,或使用向量的 at() 方法进行边界检查。

2. 内存泄漏

问题:使用 new 创建动态数组后未使用 delete[] 释放内存。
解决方案:总是在使用完动态数组后调用 delete[] 释放内存,或使用智能指针管理内存。

3. 向量的性能问题

问题:频繁的 push_back() 操作导致频繁的内存重新分配。
解决方案:使用 reserve() 方法预分配足够的存储空间,减少内存重新分配的次数。

4. 向量的清空问题

问题:使用 clear() 后向量的容量不变,仍然占用大量内存。
解决方案:在 clear() 后调用 shrink_to_fit() 释放未使用的内存。

5. 数组到向量的转换

问题:需要将数组转换为向量。
解决方案:使用向量的范围构造函数,如 std::vector<int> vec(arr, arr + size)

6. 向量的复制问题

问题:复制大向量时效率低下。
解决方案:使用移动语义(std::move())或引用传递,避免不必要的复制。

7. 多维向量的创建

问题:不知道如何创建和初始化多维向量。
解决方案:使用嵌套的向量,如 std::vector<std::vector<int>> matrix(3, std::vector<int>(3))

8. 向量的比较操作

问题:需要比较两个向量是否相等。
解决方案:使用 == 运算符直接比较两个向量,或使用 std::equal() 算法。

9. 向量的排序稳定性

问题:排序后相等元素的相对顺序可能改变。
解决方案:使用 std::stable_sort() 进行稳定排序。

10. 向量的自定义排序

问题:需要按照自定义规则对向量进行排序。
解决方案:提供自定义的比较函数或使用 lambda 表达式。

代码优化建议

  1. 优先使用向量:在大多数情况下,优先使用 std::vector 而不是数组,特别是当大小需要动态改变时。

  2. 合理使用 reserve():对于已知或可预测大小的向量,使用 reserve() 预分配存储空间,减少内存重新分配的次数。

  3. 避免不必要的复制:使用移动语义(std::move())或引用传递,避免不必要的向量复制。

  4. 使用迭代器或范围 for 循环:使用迭代器或范围 for 循环遍历向量,代码更简洁、更安全。

  5. 利用 STL 算法:使用 STL 算法处理向量,如 std::sort()std::find() 等,代码更简洁、更高效。

  6. 注意内存管理:对于动态数组,确保使用 delete[] 释放内存,避免内存泄漏。

  7. 使用 at() 进行边界检查:在调试阶段,使用 at() 方法访问向量元素,进行边界检查,提高代码安全性。

  8. 合理使用 shrink_to_fit():在向量大小大幅减小后,使用 shrink_to_fit() 释放未使用的内存,减少内存占用。

  9. 考虑使用其他容器:根据具体需求,考虑使用其他 STL 容器,如 std::list(频繁插入删除)、std::deque(两端操作)等。

  10. 性能测试:对于性能关键的代码,进行性能测试,比较不同数据结构和算法的性能。

总结

C++ 提供了两种主要的序列容器:

  • 数组:固定大小,连续存储,访问速度快,适用于大小已知且不变的场景。
  • 向量:动态大小,连续存储,自动内存管理,提供丰富的操作,适用于大小可能改变的场景。

向量是 C++ 中最常用的序列容器,它结合了数组的高效访问和链表的动态大小特性,是大多数情况下的首选数据结构。

通过合理使用数组和向量,可以编写出:

  • 高效:利用连续存储的优势,提供快速的随机访问。
  • 灵活:向量的动态大小适应不同的需求。
  • 安全:向量的自动内存管理和边界检查减少错误。
  • 可维护:简洁的代码和丰富的操作提高代码可读性。

掌握数组和向量的使用方法,对于编写高质量的 C++ 代码至关重要。

arrays_vectors.cpp

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
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
#include <iostream>
#include <vector>
#include <algorithm>
#include <numeric>
#include <string>
#include <chrono>

int main() {
// 1. 基本数组操作
std::cout << "=== 1. 基本数组操作 ===" << std::endl;
int arr[5] = {10, 20, 30, 40, 50};

std::cout << "Array elements: ";
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;

std::cout << "Array size: " << sizeof(arr) / sizeof(arr[0]) << std::endl;

arr[2] = 35;
std::cout << "Modified array[2]: " << arr[2] << std::endl;

// 2. 多维数组
std::cout << "\n=== 2. 多维数组 ===" << std::endl;
int matrix[3][3] = {
{1, 2, 3},
{4, 5, 6},
{7, 8, 9}
};

std::cout << "2D Array:" << std::endl;
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
std::cout << matrix[i][j] << " ";
}
std::cout << std::endl;
}

// 3. 基本向量操作
std::cout << "\n=== 3. 基本向量操作 ===" << std::endl;
std::vector<int> numbers = {1, 2, 3, 4, 5};

std::cout << "Vector elements: ";
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;

std::cout << "Vector size: " << numbers.size() << std::endl;
std::cout << "Vector capacity: " << numbers.capacity() << std::endl;

// 4. 向量的增删改查
std::cout << "\n=== 4. 向量的增删改查 ===" << std::endl;
numbers.push_back(6);
numbers.push_back(7);
std::cout << "After push_back: ";
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;

numbers.pop_back();
std::cout << "After pop_back: ";
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;

numbers.insert(numbers.begin() + 2, 100);
std::cout << "After insert at index 2: ";
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;

numbers.erase(numbers.begin() + 2);
std::cout << "After erase at index 2: ";
for (int num : numbers) {
std::cout << num << " ";
}
std::cout << std::endl;

std::cout << "First element: " << numbers.front() << std::endl;
std::cout << "Last element: " << numbers.back() << std::endl;

// 5. 向量的清空和容量管理
std::cout << "\n=== 5. 向量的清空和容量管理 ===" << std::endl;
numbers.clear();
std::cout << "After clear, size: " << numbers.size() << std::endl;
std::cout << "Capacity after clear: " << numbers.capacity() << std::endl;

numbers.shrink_to_fit();
std::cout << "After shrink_to_fit, capacity: " << numbers.capacity() << std::endl;

std::vector<std::string> names;
names.reserve(10);
std::cout << "Reserved capacity: " << names.capacity() << std::endl;

names.push_back("Alice");
names.push_back("Bob");
names.push_back("Charlie");
std::cout << "Current size: " << names.size() << std::endl;

// 6. 字符串向量
std::cout << "\n=== 6. 字符串向量 ===" << std::endl;
std::cout << "String vector: ";
for (const auto& name : names) {
std::cout << name << " ";
}
std::cout << std::endl;

// 7. 向量的排序
std::cout << "\n=== 7. 向量的排序 ===" << std::endl;
std::vector<int> sortedNumbers = {5, 2, 8, 1, 9, 3};
std::cout << "Original vector: ";
for (int num : sortedNumbers) {
std::cout << num << " ";
}
std::cout << std::endl;

std::sort(sortedNumbers.begin(), sortedNumbers.end());
std::cout << "Sorted vector: ";
for (int num : sortedNumbers) {
std::cout << num << " ";
}
std::cout << std::endl;

std::sort(sortedNumbers.begin(), sortedNumbers.end(), std::greater<int>());
std::cout << "Reverse sorted vector: ";
for (int num : sortedNumbers) {
std::cout << num << " ";
}
std::cout << std::endl;

// 8. 向量的算法操作
std::cout << "\n=== 8. 向量的算法操作 ===" << std::endl;
std::vector<int> algoVec = {5, 2, 8, 1, 9, 3};

auto minIt = std::min_element(algoVec.begin(), algoVec.end());
std::cout << "Minimum element: " << *minIt << std::endl;

auto maxIt = std::max_element(algoVec.begin(), algoVec.end());
std::cout << "Maximum element: " << *maxIt << std::endl;

int sum = std::accumulate(algoVec.begin(), algoVec.end(), 0);
std::cout << "Sum of elements: " << sum << std::endl;

auto findIt = std::find(algoVec.begin(), algoVec.end(), 8);
if (findIt != algoVec.end()) {
std::cout << "Found 8 at index: " << std::distance(algoVec.begin(), findIt) << std::endl;
}

// 9. 二维向量
std::cout << "\n=== 9. 二维向量 ===" << std::endl;
std::vector<std::vector<int>> twoDVector(3, std::vector<int>(3));
int value = 1;
for (int i = 0; i < 3; ++i) {
for (int j = 0; j < 3; ++j) {
twoDVector[i][j] = value++;
}
}

std::cout << "2D vector: " << std::endl;
for (const auto& row : twoDVector) {
for (int num : row) {
std::cout << num << " ";
}
std::cout << std::endl;
}

// 10. 动态数组
std::cout << "\n=== 10. 动态数组 ===" << std::endl;
int* dynamicArray = new int[5];
for (int i = 0; i < 5; ++i) {
dynamicArray[i] = i * 10;
}

std::cout << "Dynamic array elements: ";
for (int i = 0; i < 5; ++i) {
std::cout << dynamicArray[i] << " ";
}
std::cout << std::endl;

// 动态二维数组
int rows = 2, cols = 2;
int** dynamic2DArray = new int*[rows];
for (int i = 0; i < rows; ++i) {
dynamic2DArray[i] = new int[cols];
}

dynamic2DArray[0][0] = 1;
dynamic2DArray[0][1] = 2;
dynamic2DArray[1][0] = 3;
dynamic2DArray[1][1] = 4;

std::cout << "Dynamic 2D array: " << std::endl;
for (int i = 0; i < rows; ++i) {
for (int j = 0; j < cols; ++j) {
std::cout << dynamic2DArray[i][j] << " ";
}
std::cout << std::endl;
}

// 释放动态数组内存
delete[] dynamicArray;
for (int i = 0; i < rows; ++i) {
delete[] dynamic2DArray[i];
}
delete[] dynamic2DArray;

// 11. 向量的移动语义
std::cout << "\n=== 11. 向量的移动语义 ===" << std::endl;
std::vector<int> source = {1, 2, 3, 4, 5};
std::vector<int> destination = std::move(source);

std::cout << "Source vector size after move: " << source.size() << std::endl;
std::cout << "Destination vector elements: ";
for (int num : destination) {
std::cout << num << " ";
}
std::cout << std::endl;

// 12. 向量的范围构造
std::cout << "\n=== 12. 向量的范围构造 ===" << std::endl;
std::vector<int> rangeVec(destination.begin() + 1, destination.end() - 1);
std::cout << "Range vector elements: ";
for (int num : rangeVec) {
std::cout << num << " ";
}
std::cout << std::endl;

// 13. 向量的填充构造
std::cout << "\n=== 13. 向量的填充构造 ===" << std::endl;
std::vector<int> filledVec(5, 42);
std::cout << "Filled vector elements: ";
for (int num : filledVec) {
std::cout << num << " ";
}
std::cout << std::endl;

// 14. 数组和向量的比较
std::cout << "\n=== 14. 数组和向量的比较 ===" << std::endl;
int compareArray[5] = {1, 2, 3, 4, 5};
std::vector<int> compareVector = {1, 2, 3, 4, 5};

// 访问时间比较(简单测试)
auto start = std::chrono::high_resolution_clock::now();
int arraySum = 0;
for (int i = 0; i < 5; ++i) {
arraySum += compareArray[i];
}
auto end = std::chrono::high_resolution_clock::now();
auto arrayTime = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();

start = std::chrono::high_resolution_clock::now();
int vectorSum = 0;
for (int i = 0; i < 5; ++i) {
vectorSum += compareVector[i];
}
end = std::chrono::high_resolution_clock::now();
auto vectorTime = std::chrono::duration_cast<std::chrono::microseconds>(end - start).count();

std::cout << "Array access time: " << arrayTime << " ms" << std::endl;
std::cout << "Vector access time: " << vectorTime << " ms" << std::endl;
std::cout << "Array size: " << sizeof(compareArray) / sizeof(compareArray[0]) << ", Vector size: " << compareVector.size() << std::endl;
std::cout << "Array is fixed size, Vector is dynamic size" << std::endl;
std::cout << "Array requires manual memory management (for dynamic), Vector manages memory automatically" << std::endl;

return 0;
}

C++ 函数示例

功能描述

本示例程序展示了 C++ 中各种函数类型的定义和使用方法,包括:

  • 基本函数定义和调用
  • 函数参数和返回值
  • 递归函数
  • 引用参数
  • 数组参数
  • 函数重载
  • 内联函数
  • constexpr 函数
  • Lambda 表达式
  • 函数指针
  • 命名空间中的函数
  • 类成员函数
  • 默认参数
  • 可变参数
  • 函数模板

代码解析

1. 基本函数定义和调用

1
2
3
4
5
6
7
void sayHello() {
std::cout << "Hello from sayHello!" << std::endl;
}

int add(int a, int b) {
return a + b;
}

说明

  • 函数定义包括返回类型、函数名、参数列表和函数体。
  • void 表示函数没有返回值。
  • int 表示函数返回一个整数。
  • 函数调用通过函数名和参数列表完成。

2. 函数参数和返回值

1
2
3
4
5
6
7
double multiply(double x, double y) {
return x * y;
}

void printInfo(std::string name, int age) {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}

说明

  • 函数可以接受不同类型的参数,如 doublestd::stringint
  • 函数可以返回不同类型的值,如 double
  • 函数参数可以是基本类型,也可以是复合类型(如 std::string)。

3. 递归函数

1
2
3
4
5
6
int factorial(int n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}

说明

  • 递归函数是调用自身的函数。
  • 递归函数必须有一个终止条件(基础情况),否则会导致无限递归。
  • 这里的终止条件是 n <= 1,此时返回 1。

4. 引用参数

1
2
3
4
5
void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}

说明

  • 引用参数(使用 & 符号)允许函数修改实参的值。
  • 引用参数避免了值传递的复制开销,对于大型对象尤为重要。
  • 这里的 swap 函数通过引用参数交换两个变量的值。

5. 数组参数

1
2
3
4
5
void modifyArray(int arr[], int size) {
for (int i = 0; i < size; ++i) {
arr[i] *= 2;
}
}

说明

  • 数组作为函数参数时,实际上传递的是数组首元素的指针。
  • 因此,函数内部对数组元素的修改会影响到原数组。
  • 通常需要传递数组的大小作为额外参数。

6. 函数重载

1
2
3
4
5
6
7
8
9
10
11
int sum(int a, int b) {
return a + b;
}

double sum(double a, double b) {
return a + b;
}

int sum(int a, int b, int c) {
return a + b + c;
}

说明

  • 函数重载允许定义多个同名函数,只要它们的参数列表不同(参数类型、数量或顺序不同)。
  • 编译器根据函数调用时提供的参数类型和数量来选择合适的函数版本。
  • 这里定义了三个 sum 函数:两个参数的 int 版本、两个参数的 double 版本和三个参数的 int 版本。

7. 内联函数

1
2
3
inline int square(int x) {
return x * x;
}

说明

  • inline 关键字建议编译器将函数体内联到调用点,以减少函数调用的开销。
  • 内联函数适用于简短、频繁调用的函数。
  • 编译器可以忽略 inline 关键字的建议。

8. constexpr 函数

1
2
3
constexpr int cube(int x) {
return x * x * x;
}

说明

  • constexpr 关键字表示函数可以在编译时计算其返回值。
  • constexpr 函数的参数和返回值必须是字面量类型。
  • constexpr 函数在编译时计算可以提高程序性能。

9. Lambda 表达式

1
2
auto lambda = [](int a, int b) { return a - b; };
std::cout << "Lambda: 10 - 3 = " << lambda(10, 3) << std::endl;

说明

  • Lambda 表达式是 C++11 引入的一种匿名函数语法。
  • Lambda 表达式的语法为:[捕获列表](参数列表) -> 返回类型 { 函数体 }
  • 捕获列表指定了 Lambda 表达式可以访问的外部变量。
  • auto 关键字用于自动推导 Lambda 表达式的类型。

10. 函数指针

1
2
int (*operation)(int, int) = add;
std::cout << "Using function pointer: 5 + 3 = " << operation(5, 3) << std::endl;

说明

  • 函数指针是指向函数的指针变量。
  • 函数指针的类型包括返回类型和参数类型。
  • 函数指针可以用于回调函数、函数表等场景。

11. 默认参数

1
2
3
void greet(std::string name, std::string message = "Hello") {
std::cout << message << ", " << name << "!" << std::endl;
}

说明

  • 默认参数允许函数调用时省略某些参数,使用预定义的默认值。
  • 默认参数必须从右向左定义,即如果一个参数有默认值,那么它右边的所有参数也必须有默认值。

12. 可变参数

1
2
3
4
5
6
7
8
9
10
11
12
double average(int count, ...) {
va_list args;
va_start(args, count);

double sum = 0;
for (int i = 0; i < count; ++i) {
sum += va_arg(args, double);
}

va_end(args);
return sum / count;
}

说明

  • 可变参数允许函数接受任意数量的参数。
  • 使用 <cstdarg> 头文件中的 va_listva_startva_argva_end 宏来处理可变参数。
  • 可变参数函数需要至少一个固定参数,用于确定可变参数的数量或类型。

13. 函数模板

1
2
3
4
template <typename T>
T maximum(T a, T b) {
return (a > b) ? a : b;
}

说明

  • 函数模板允许定义适用于多种类型的通用函数。
  • typename Tclass T 声明了一个类型参数。
  • 函数模板在调用时会根据实参类型自动实例化。

14. 命名空间中的函数

1
2
3
4
5
6
7
8
9
namespace Math {
int add(int a, int b) {
return a + b;
}

int subtract(int a, int b) {
return a - b;
}
}

说明

  • 命名空间用于组织代码,避免命名冲突。
  • 命名空间中的函数可以通过 命名空间::函数名 的方式调用。
  • 可以使用 using namespace 命名空间using 命名空间::函数名 来简化调用。

15. 类成员函数

1
2
3
4
5
6
7
8
9
10
class Calculator {
public:
int add(int a, int b) {
return a + b;
}

int multiply(int a, int b) {
return a * b;
}
};

说明

  • 类成员函数是定义在类内部的函数。
  • 类成员函数可以访问类的成员变量和其他成员函数。
  • 类成员函数通过对象调用,使用 对象.函数名 的语法。

编译和运行

在 Windows 上编译(使用 g++):

1
g++ -std=c++11 -fexec-charset=GBK -o functions functions.cpp

运行程序:

1
.\functions.exe

输出结果:

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
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
69
70
71
72
73
74
75
76
=== 1. 基本函数定义和调用 ===
Hello from sayHello!
5 + 3 = 8
2.5 * 4.0 = 10

=== 2. 函数参数和返回值 ===
Name: Alice, Age: 30

=== 3. 递归函数 ===
5! = 120

=== 4. 引用参数 ===
Before swap: x = 10, y = 20
After swap: x = 20, y = 10

=== 5. 数组参数 ===
Before modify: 1 2 3 4 5
After modify: 2 4 6 8 10

=== 6. 函数重载 ===
sum(3, 4) = 7
sum(2.5, 3.5) = 6
sum(1, 2, 3) = 6

=== 7. 内联函数 ===
square(5) = 25

=== 8. constexpr 函数 ===
cube(3) = 27

=== 9. Lambda 表达式 ===
Lambda: 10 - 3 = 7

=== 10. 函数指针 ===
Using function pointer: 5 + 3 = 8
Using function pointer with operation: 10 - 4 = 6

=== 11. 默认参数 ===
Hello, Bob!
Welcome, Bob!

=== 12. 可变参数 ===
Average of 3 numbers: 3.33333
Sum of 4 numbers: 14

=== 13. 函数模板 ===
Maximum of 5 and 10: 10
Maximum of 3.14 and 2.71: 3.14
Maximum of apple and banana: banana

=== 14. 命名空间中的函数 ===
Math::add(5, 3) = 8
Math::subtract(10, 4) = 6

=== 15. 类成员函数 ===
Calculator::add(5, 3) = 8
Calculator::multiply(4, 6) = 24

=== 16. 静态函数 ===
Static function called: This is a static function
Counter: 1

=== 17. 常量成员函数 ===
Value: 42
Value after modification: 100

=== 18. 引用返回值 ===
Value before increment: 5
Value after increment: 6
Value after second increment: 7

=== 19. 移动语义和右值引用 ===
Original value: 42
Moved value: 42
Value after move: 0
Sum of two values: 84

技术要点

1. 函数声明和定义

  • 函数声明:声明函数的返回类型、函数名和参数列表,不包含函数体。
  • 函数定义:包含函数声明和函数体。
  • 函数原型:函数声明的另一种说法,用于告诉编译器函数的签名。

2. 参数传递方式

  • 值传递:函数接收参数的副本,修改参数不会影响实参。
  • 引用传递:函数接收参数的引用,修改参数会影响实参。
  • 指针传递:函数接收参数的指针,修改指针指向的值会影响实参。

3. 返回值类型

  • 基本类型:如 intdoublebool 等。
  • 复合类型:如 std::string、数组、结构体等。
  • 指针类型:返回指针时要注意避免返回局部变量的指针。
  • 引用类型:返回引用时要注意避免返回局部变量的引用。

4. 函数重载规则

  • 函数名相同。
  • 参数列表不同(参数类型、数量或顺序不同)。
  • 返回类型不同不足以构成函数重载。

5. 内联函数和 constexpr 函数

  • 内联函数:建议编译器将函数体内联到调用点,减少函数调用开销。
  • constexpr 函数:可以在编译时计算,提高程序性能。

6. Lambda 表达式

  • 捕获列表
    • []:不捕获任何变量。
    • [=]:按值捕获所有外部变量。
    • [&]:按引用捕获所有外部变量。
    • [x, &y]:按值捕获 x,按引用捕获 y。
  • 参数列表:与普通函数类似。
  • 返回类型:通常可以省略,由编译器自动推导。

7. 函数指针和回调函数

  • 函数指针:指向函数的指针变量,用于存储函数的地址。
  • 回调函数:通过函数指针传递给其他函数并在适当的时候调用的函数。

8. 默认参数和可变参数

  • 默认参数:为函数参数提供默认值,调用时可以省略这些参数。
  • 可变参数:允许函数接受任意数量的参数,使用 <cstdarg> 头文件中的宏处理。

9. 函数模板

  • 模板参数:可以是类型参数(typename Tclass T)或非类型参数(如整数)。
  • 模板实例化:编译器根据实参类型自动生成具体的函数版本。
  • 模板特化:为特定类型提供自定义的模板实现。

10. 命名空间和作用域

  • 命名空间:用于组织代码,避免命名冲突。
  • 全局作用域:在所有函数和命名空间外部定义的标识符。
  • 局部作用域:在函数、块或类内部定义的标识符。

常见问题

1. 函数未声明错误

问题:调用了一个未声明的函数。
解决方案:在调用函数之前声明函数,或在头文件中声明函数并在源文件中包含该头文件。

2. 函数参数不匹配

问题:函数调用时提供的参数类型或数量与函数声明不匹配。
解决方案:确保函数调用时提供的参数类型和数量与函数声明一致。

3. 返回类型不匹配

问题:函数返回的值类型与函数声明的返回类型不匹配。
解决方案:确保函数返回的值类型与函数声明的返回类型一致,或使用类型转换。

4. 递归栈溢出

问题:递归函数没有终止条件,或终止条件不正确,导致栈溢出。
解决方案:确保递归函数有正确的终止条件,避免无限递归。

5. 指针和引用错误

问题:使用了空指针、野指针,或返回了局部变量的引用。
解决方案:始终初始化指针,避免返回局部变量的引用,使用智能指针管理内存。

6. 函数重载歧义

问题:函数调用时,编译器无法确定应该调用哪个重载版本。
解决方案:确保函数重载的参数列表有明显的区别,或在调用时显式指定参数类型。

7. 默认参数顺序错误

问题:默认参数不是从右向左定义的。
解决方案:确保默认参数从右向左定义,即如果一个参数有默认值,那么它右边的所有参数也必须有默认值。

8. 可变参数类型错误

问题:使用 va_arg 时指定的类型与实际参数类型不匹配。
解决方案:确保 va_arg 指定的类型与实际参数类型一致。

9. 模板实例化错误

问题:模板参数不满足模板的要求,导致模板实例化失败。
解决方案:确保模板参数满足模板的要求,或为特定类型提供模板特化。

10. 命名空间冲突

问题:不同命名空间中存在同名函数,导致调用歧义。
解决方案:使用完全限定名(命名空间::函数名)来调用函数,或使用 using 声明来指定要使用的函数。

代码优化建议

  1. 函数设计原则

    • 函数应该有单一的职责,只做一件事情。
    • 函数名应该清晰地表达函数的功能。
    • 函数参数应该尽量少,避免过长的参数列表。
    • 函数体应该尽量简短,避免过长的函数。
  2. 参数传递优化

    • 对于大型对象,使用引用传递或常量引用传递,避免值传递的复制开销。
    • 对于不需要修改的参数,使用 const 引用,提高代码的可读性和安全性。
  3. 返回值优化

    • 对于大型对象,考虑使用移动语义或引用返回,避免返回值的复制开销。
    • 避免返回局部变量的引用或指针。
  4. 内联函数和 constexpr 函数

    • 对于简短、频繁调用的函数,使用 inline 关键字,减少函数调用开销。
    • 对于在编译时可以计算的函数,使用 constexpr 关键字,提高程序性能。
  5. Lambda 表达式

    • 对于简短的、只在局部使用的函数,使用 Lambda 表达式,提高代码的可读性。
    • 合理使用捕获列表,避免不必要的变量捕获。
  6. 函数模板

    • 对于需要处理多种类型的通用函数,使用函数模板,提高代码的复用性。
    • 为常见类型提供模板特化,提高特定类型的性能。
  7. 错误处理

    • 对于可能失败的函数,考虑使用异常或返回错误代码。
    • 使用 noexcept 关键字标记不会抛出异常的函数,提高编译器优化能力。
  8. 性能优化

    • 避免在热点路径上使用复杂的函数调用。
    • 考虑函数的内联可能性,特别是对于短小的函数。
    • 合理使用函数指针和虚函数,避免过度使用导致的性能损失。
  9. 代码风格

    • 遵循一致的函数命名和格式约定。
    • 为复杂函数添加注释,说明函数的功能、参数和返回值。
    • 使用空行和缩进,提高函数体的可读性。
  10. 测试和调试

    • 为函数编写单元测试,确保函数的正确性。
    • 在函数中添加适当的断言,捕获逻辑错误。
    • 使用调试器和性能分析工具,识别和解决函数中的问题。

总结

C++ 提供了丰富的函数特性,包括:

  • 基本函数:定义和调用函数,处理参数和返回值。
  • 高级特性:递归、引用参数、数组参数、函数重载。
  • 现代特性:内联函数、constexpr 函数、Lambda 表达式。
  • 高级主题:函数指针、默认参数、可变参数、函数模板。
  • 组织方式:命名空间中的函数、类成员函数。

通过合理使用这些特性,可以编写出:

  • 模块化:将复杂问题分解为简单的函数。
  • 可重用:通过函数模板和重载提高代码复用性。
  • 高效:通过内联、constexpr 和移动语义提高性能。
  • 可维护:通过清晰的函数设计和命名提高代码可读性。

函数是 C++ 程序的基本构建块,掌握函数的各种特性对于编写高质量的 C++ 代码至关重要。

functions.cpp

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
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
#include <iostream>
#include <string>
#include <cstdarg>
#include <vector>

// 1. 基本函数定义
void sayHello() {
std::cout << "Hello from sayHello!" << std::endl;
}

int add(int a, int b) {
return a + b;
}

double multiply(double x, double y) {
return x * y;
}

void printInfo(std::string name, int age) {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}

// 2. 递归函数
int factorial(int n) {
if (n <= 1) {
return 1;
}
return n * factorial(n - 1);
}

// 3. 引用参数
void swap(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}

// 4. 数组参数
void modifyArray(int arr[], int size) {
for (int i = 0; i < size; ++i) {
arr[i] *= 2;
}
}

// 5. 函数重载
int sum(int a, int b) {
return a + b;
}

double sum(double a, double b) {
return a + b;
}

int sum(int a, int b, int c) {
return a + b + c;
}

// 6. 内联函数
inline int square(int x) {
return x * x;
}

// 7. constexpr 函数
constexpr int cube(int x) {
return x * x * x;
}

// 8. 函数指针
int subtract(int a, int b) {
return a - b;
}

// 9. 默认参数
void greet(std::string name, std::string message = "Hello") {
std::cout << message << ", " << name << "!" << std::endl;
}

// 10. 可变参数
double average(int count, ...) {
va_list args;
va_start(args, count);

double sum = 0;
for (int i = 0; i < count; ++i) {
sum += va_arg(args, double);
}

va_end(args);
return sum / count;
}

int sumVariadic(int count, ...) {
va_list args;
va_start(args, count);

int sum = 0;
for (int i = 0; i < count; ++i) {
sum += va_arg(args, int);
}

va_end(args);
return sum;
}

// 11. 函数模板
template <typename T>
T maximum(T a, T b) {
return (a > b) ? a : b;
}

// 12. 命名空间中的函数
namespace Math {
int add(int a, int b) {
return a + b;
}

int subtract(int a, int b) {
return a - b;
}
}

// 13. 类成员函数
class Calculator {
public:
int add(int a, int b) {
return a + b;
}

int multiply(int a, int b) {
return a * b;
}
};

// 14. 静态函数
class Utility {
public:
static void staticFunction() {
std::cout << "Static function called: This is a static function" << std::endl;
}

static int counter;
};

int Utility::counter = 0;

// 15. 常量成员函数
class MyClass {
private:
int value;
public:
MyClass(int v) : value(v) {}

int getValue() const {
return value;
}

void setValue(int v) {
value = v;
}
};

// 16. 引用返回值
int& increment(int& x) {
x++;
return x;
}

// 17. 移动语义和右值引用
void processValue(int& lvalue) {
std::cout << "Processing lvalue: " << lvalue << std::endl;
}

void processValue(int&& rvalue) {
std::cout << "Processing rvalue: " << rvalue << std::endl;
}

int main() {
std::cout << "=== 1. 基本函数定义和调用 ===" << std::endl;
sayHello();

int result1 = add(5, 3);
std::cout << "5 + 3 = " << result1 << std::endl;

double result2 = multiply(2.5, 4.0);
std::cout << "2.5 * 4.0 = " << result2 << std::endl;

std::cout << "\n=== 2. 函数参数和返回值 ===" << std::endl;
printInfo("Alice", 30);

std::cout << "\n=== 3. 递归函数 ===" << std::endl;
int fact = factorial(5);
std::cout << "5! = " << fact << std::endl;

std::cout << "\n=== 4. 引用参数 ===" << std::endl;
int x = 10, y = 20;
std::cout << "Before swap: x = " << x << ", y = " << y << std::endl;
swap(x, y);
std::cout << "After swap: x = " << x << ", y = " << y << std::endl;

std::cout << "\n=== 5. 数组参数 ===" << std::endl;
int arr[] = {1, 2, 3, 4, 5};
std::cout << "Before modify: ";
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;

modifyArray(arr, 5);
std::cout << "After modify: ";
for (int i = 0; i < 5; ++i) {
std::cout << arr[i] << " ";
}
std::cout << std::endl;

std::cout << "\n=== 6. 函数重载 ===" << std::endl;
std::cout << "sum(3, 4) = " << sum(3, 4) << std::endl;
std::cout << "sum(2.5, 3.5) = " << sum(2.5, 3.5) << std::endl;
std::cout << "sum(1, 2, 3) = " << sum(1, 2, 3) << std::endl;

std::cout << "\n=== 7. 内联函数 ===" << std::endl;
std::cout << "square(5) = " << square(5) << std::endl;

std::cout << "\n=== 8. constexpr 函数 ===" << std::endl;
std::cout << "cube(3) = " << cube(3) << std::endl;

std::cout << "\n=== 9. Lambda 表达式 ===" << std::endl;
auto lambda = [](int a, int b) { return a - b; };
std::cout << "Lambda: 10 - 3 = " << lambda(10, 3) << std::endl;

std::cout << "\n=== 10. 函数指针 ===" << std::endl;
int (*operation)(int, int) = add;
std::cout << "Using function pointer: 5 + 3 = " << operation(5, 3) << std::endl;

operation = subtract;
std::cout << "Using function pointer with operation: 10 - 4 = " << operation(10, 4) << std::endl;

std::cout << "\n=== 11. 默认参数 ===" << std::endl;
greet("Bob");
greet("Bob", "Welcome");

std::cout << "\n=== 12. 可变参数 ===" << std::endl;
double avg = average(3, 1.0, 2.0, 7.0);
std::cout << "Average of 3 numbers: " << avg << std::endl;

int sumVar = sumVariadic(4, 1, 2, 3, 8);
std::cout << "Sum of 4 numbers: " << sumVar << std::endl;

std::cout << "\n=== 13. 函数模板 ===" << std::endl;
std::cout << "Maximum of 5 and 10: " << maximum(5, 10) << std::endl;
std::cout << "Maximum of 3.14 and 2.71: " << maximum(3.14, 2.71) << std::endl;
std::cout << "Maximum of apple and banana: " << maximum(std::string("apple"), std::string("banana")) << std::endl;

std::cout << "\n=== 14. 命名空间中的函数 ===" << std::endl;
std::cout << "Math::add(5, 3) = " << Math::add(5, 3) << std::endl;
std::cout << "Math::subtract(10, 4) = " << Math::subtract(10, 4) << std::endl;

std::cout << "\n=== 15. 类成员函数 ===" << std::endl;
Calculator calc;
std::cout << "Calculator::add(5, 3) = " << calc.add(5, 3) << std::endl;
std::cout << "Calculator::multiply(4, 6) = " << calc.multiply(4, 6) << std::endl;

std::cout << "\n=== 16. 静态函数 ===" << std::endl;
Utility::staticFunction();
Utility::counter++;
std::cout << "Counter: " << Utility::counter << std::endl;

std::cout << "\n=== 17. 常量成员函数 ===" << std::endl;
MyClass obj(42);
std::cout << "Value: " << obj.getValue() << std::endl;
obj.setValue(100);
std::cout << "Value after modification: " << obj.getValue() << std::endl;

std::cout << "\n=== 18. 引用返回值 ===" << std::endl;
int num = 5;
std::cout << "Value before increment: " << num << std::endl;
int& result3 = increment(num);
std::cout << "Value after increment: " << result3 << std::endl;
increment(result3);
std::cout << "Value after second increment: " << num << std::endl;

std::cout << "\n=== 19. 移动语义和右值引用 ===" << std::endl;
int value = 42;
std::cout << "Original value: " << value << std::endl;
processValue(value); // 传递左值
processValue(100); // 传递右值

return 0;
}

C++ 控制流语句示例

功能描述

本示例程序展示了 C++ 中各种控制流语句的使用方法,包括条件语句、循环语句、跳转语句等。控制流语句是编程中用于控制程序执行流程的重要工具,它们使得程序能够根据不同的条件执行不同的代码块,或者重复执行特定的代码块。

代码解析

1. if-else 语句

1
2
3
4
5
6
7
8
9
10
11
12
13
int score = 85;

if (score >= 90) {
std::cout << "Grade: A" << std::endl;
} else if (score >= 80) {
std::cout << "Grade: B" << std::endl;
} else if (score >= 70) {
std::cout << "Grade: C" << std::endl;
} else if (score >= 60) {
std::cout << "Grade: D" << std::endl;
} else {
std::cout << "Grade: F" << std::endl;
}

说明:if-else 语句用于根据条件执行不同的代码块。如果条件为真,则执行 if 后面的代码块;否则,执行 else 后面的代码块。可以使用 else if 来检查多个条件。

2. switch 语句

1
2
3
4
5
6
7
8
9
10
11
12
int day = 3;
switch (day) {
case 1:
std::cout << "Monday" << std::endl;
break;
case 2:
std::cout << "Tuesday" << std::endl;
break;
// 其他 case...
default:
std::cout << "Invalid day" << std::endl;
}

说明:switch 语句用于根据表达式的值选择执行不同的代码块。每个 case 标签对应一个可能的值,当表达式的值与某个 case 标签匹配时,执行该 case 后面的代码。break 语句用于跳出 switch 语句,防止执行后续的 case。default 标签用于处理所有未明确指定的情况。

3. for 循环

1
2
3
for (int i = 1; i <= 5; ++i) {
std::cout << i << " ";
}

说明:for 循环用于重复执行一段代码指定的次数。它包含三个部分:初始化表达式、条件表达式和更新表达式。初始化表达式在循环开始前执行一次,条件表达式在每次循环前检查,如果为真则执行循环体,更新表达式在每次循环后执行。

4. while 循环

1
2
3
4
5
int count = 0;
while (count < 5) {
std::cout << count << " ";
count++;
}

说明:while 循环在条件为真时重复执行循环体。条件表达式在每次循环前检查,如果为真则执行循环体。

5. do-while 循环

1
2
3
4
5
int num = 0;
do {
std::cout << num << " ";
num++;
} while (num < 5);

说明:do-while 循环先执行一次循环体,然后在条件为真时重复执行。条件表达式在每次循环后检查,如果为真则继续执行循环体。

6. range-based for 循环 (C++11+)

1
2
3
4
int arr[] = {10, 20, 30, 40, 50};
for (int x : arr) {
std::cout << x << " ";
}

说明:range-based for 循环用于遍历容器或数组中的所有元素。它会自动迭代容器中的每个元素,并将当前元素赋值给循环变量。

7. break 和 continue 语句

1
2
3
4
5
6
7
8
9
for (int i = 0; i < 10; ++i) {
if (i == 3) {
continue;
}
if (i == 7) {
break;
}
std::cout << i << " ";
}

说明

  • continue 语句用于跳过当前循环的剩余部分,直接开始下一次循环。
  • break 语句用于跳出当前循环,不再执行循环的剩余部分。

8. 嵌套循环

1
2
3
4
5
6
for (int i = 1; i <= 3; ++i) {
for (int j = 1; j <= 3; ++j) {
std::cout << "(" << i << "," << j << ") ";
}
std::cout << std::endl;
}

说明:嵌套循环是指在一个循环内部包含另一个循环。外层循环执行一次,内层循环执行完整的一轮。

编译和运行

在 Windows 上编译(使用 g++):

1
g++ -std=c++11 -fexec-charset=GBK -o control_flow control_flow.cpp

运行程序:

1
.\control_flow.exe

输出结果:

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
=== 1. if-else 语句 ===
Grade: B

=== 2. 嵌套的 if-else 语句 ===
You can drive.

=== 3. switch 语句 ===
Wednesday

=== 4. 使用枚举的 switch 语句 ===
It's summer time!

=== 5. for 循环 ===
基本 for 循环:
1 2 3 4 5
for 循环遍历数组:
1 2 3 4 5

=== 6. while 循环 ===
0 1 2 3 4

=== 7. do-while 循环 ===
0 1 2 3 4

=== 8. range-based for 循环 ===
10 20 30 40 50
使用 auto 的 range-based for 循环:
100 200 300 400 500

=== 9. break 和 continue 语句 ===
0 1 2 Continue at i=3
4 5 6 Break at i=7

=== 10. 嵌套循环 ===
乘法表:
1×1=1 1×2=2 1×3=3 1×4=4 1×5=5
2×1=2 2×2=4 2×3=6 2×4=8 2×5=10
3×1=3 3×2=6 3×3=9 3×4=12 3×5=15
4×1=4 4×2=8 4×3=12 4×4=16 4×5=20
5×1=5 5×2=10 5×3=15 5×4=20 5×5=25

=== 11. goto 语句 ===
0 1 2 3 4

=== 12. 条件运算符 ===
Max of 10 and 20 is 20

=== 13. 逻辑运算符 ===
It's a nice day!
You don't need sunscreen.

=== 14. 随机数和循环 ===
生成 10 个 1-100 之间的随机数:
42 76 33 12 58 91 67 23 85 47

=== 15. 无限循环和 break ===
Guess: 5
Guess: 3
Guess: 7
Found 7 in 3 attempts!

技术要点

1. 条件语句的使用场景

  • if-else:适用于需要根据多个条件进行不同处理的场景,尤其是当条件之间有逻辑关系时。
  • switch:适用于根据单个表达式的值进行多个分支选择的场景,尤其是当分支较多时,代码结构更清晰。

2. 循环语句的选择

  • for:适用于已知循环次数的场景,如遍历数组或执行固定次数的操作。
  • while:适用于循环次数未知,但有明确结束条件的场景。
  • do-while:适用于至少需要执行一次循环体的场景。
  • range-based for:适用于遍历容器或数组的所有元素,代码更简洁。

3. 跳转语句的使用

  • break:用于跳出循环或 switch 语句。
  • continue:用于跳过当前循环的剩余部分,开始下一次循环。
  • goto:用于无条件跳转到指定的标签,尽量避免使用,因为它会使代码结构混乱。

4. 循环控制技巧

  • 嵌套循环:用于处理多维数据,如二维数组或矩阵。
  • 无限循环:使用 while (true) 创建无限循环,通过 break 语句在适当的条件下跳出。
  • 循环优化:尽量减少循环内部的计算,将不变的表达式移到循环外部。

5. C++11 新特性

  • range-based for 循环:提供了更简洁的遍历语法。
  • auto 关键字:在 range-based for 循环中可以自动推导元素类型。

常见问题

1. 无限循环

问题:循环条件始终为真,导致程序陷入无限循环。
解决方案:确保循环条件最终会变为假,或在循环体内使用 break 语句跳出循环。

2. 循环变量未初始化

问题:使用未初始化的变量作为循环条件或循环变量。
解决方案:始终初始化循环变量。

3. 缺少 break 语句

问题:在 switch 语句中缺少 break 语句,导致执行多个 case。
解决方案:在每个 case 结束时添加 break 语句。

4. 数组越界

问题:在循环中访问数组元素时超出了数组的范围。
解决方案:确保循环变量的范围在数组的有效索引范围内。

5. 浮点数比较错误

问题:使用浮点数作为循环条件,由于精度问题导致循环次数不正确。
解决方案:使用整数作为循环计数器,或使用一个小的epsilon值进行比较。

代码优化建议

  1. 使用合适的循环类型:根据具体场景选择最合适的循环类型,使代码更清晰。

  2. 避免使用 goto:尽量使用结构化的控制流语句(if-else、循环)代替 goto,使代码结构更清晰。

  3. 使用 range-based for 循环:对于遍历容器或数组的场景,使用 range-based for 循环使代码更简洁。

  4. 循环不变式外提:将循环内部不变的表达式移到循环外部,提高性能。

  5. 使用 constexpr 和 const:对于编译时常量,使用 constexpr 或 const 关键字。

  6. 避免在循环中创建大对象:尽量在循环外部创建大对象,或使用移动语义。

  7. 使用标准算法:对于常见的算法操作(如查找、排序),使用 STL 提供的算法函数。

  8. 添加适当的注释:对于复杂的控制流逻辑,添加注释说明其目的和工作原理。

总结

C++ 提供了丰富的控制流语句,包括:

  • 条件语句:if-else、switch
  • 循环语句:for、while、do-while、range-based for
  • 跳转语句:break、continue、goto

这些控制流语句使得程序能够:

  • 根据不同的条件执行不同的代码块
  • 重复执行特定的代码块
  • 在适当的时机改变程序的执行流程

通过合理使用这些控制流语句,可以编写出结构清晰、逻辑正确、执行高效的 C++ 程序。

control_flow.cpp

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
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
#include <iostream>
#include <vector>
#include <string>
#include <cstdlib>
#include <ctime>

int main() {
// 1. if-else 语句
std::cout << "=== 1. if-else 语句 ===" << std::endl;
int score = 85;

if (score >= 90) {
std::cout << "Grade: A" << std::endl;
} else if (score >= 80) {
std::cout << "Grade: B" << std::endl;
} else if (score >= 70) {
std::cout << "Grade: C" << std::endl;
} else if (score >= 60) {
std::cout << "Grade: D" << std::endl;
} else {
std::cout << "Grade: F" << std::endl;
}

// 嵌套的 if-else 语句
std::cout << "\n=== 2. 嵌套的 if-else 语句 ===" << std::endl;
int age = 25;
bool hasLicense = true;

if (age >= 18) {
if (hasLicense) {
std::cout << "You can drive." << std::endl;
} else {
std::cout << "You need a license to drive." << std::endl;
}
} else {
std::cout << "You are too young to drive." << std::endl;
}

// 3. switch 语句
std::cout << "\n=== 3. switch 语句 ===" << std::endl;
int day = 3;
switch (day) {
case 1:
std::cout << "Monday" << std::endl;
break;
case 2:
std::cout << "Tuesday" << std::endl;
break;
case 3:
std::cout << "Wednesday" << std::endl;
break;
case 4:
std::cout << "Thursday" << std::endl;
break;
case 5:
std::cout << "Friday" << std::endl;
break;
case 6:
std::cout << "Saturday" << std::endl;
break;
case 7:
std::cout << "Sunday" << std::endl;
break;
default:
std::cout << "Invalid day" << std::endl;
}

// 使用枚举的 switch 语句
std::cout << "\n=== 4. 使用枚举的 switch 语句 ===" << std::endl;
enum Season { SPRING, SUMMER, AUTUMN, WINTER };
Season currentSeason = SUMMER;

switch (currentSeason) {
case SPRING:
std::cout << "It's spring time!" << std::endl;
break;
case SUMMER:
std::cout << "It's summer time!" << std::endl;
break;
case AUTUMN:
std::cout << "It's autumn time!" << std::endl;
break;
case WINTER:
std::cout << "It's winter time!" << std::endl;
break;
}

// 5. for 循环
std::cout << "\n=== 5. for 循环 ===" << std::endl;
std::cout << "基本 for 循环:" << std::endl;
for (int i = 1; i <= 5; ++i) {
std::cout << i << " ";
}
std::cout << std::endl;

// for 循环遍历数组
std::cout << "for 循环遍历数组:" << std::endl;
int numbers[] = {1, 2, 3, 4, 5};
int size = sizeof(numbers) / sizeof(numbers[0]);
for (int i = 0; i < size; ++i) {
std::cout << numbers[i] << " ";
}
std::cout << std::endl;

// 6. while 循环
std::cout << "\n=== 6. while 循环 ===" << std::endl;
int count = 0;
while (count < 5) {
std::cout << count << " ";
count++;
}
std::cout << std::endl;

// 7. do-while 循环
std::cout << "\n=== 7. do-while 循环 ===" << std::endl;
int num = 0;
do {
std::cout << num << " ";
num++;
} while (num < 5);
std::cout << std::endl;

// 8. range-based for 循环 (C++11+)
std::cout << "\n=== 8. range-based for 循环 ===" << std::endl;
int arr[] = {10, 20, 30, 40, 50};
for (int x : arr) {
std::cout << x << " ";
}
std::cout << std::endl;

// 使用 auto 的 range-based for 循环
std::cout << "使用 auto 的 range-based for 循环:" << std::endl;
std::vector<int> vec = {100, 200, 300, 400, 500};
for (auto value : vec) {
std::cout << value << " ";
}
std::cout << std::endl;

// 9. break 和 continue 语句
std::cout << "\n=== 9. break 和 continue 语句 ===" << std::endl;
for (int i = 0; i < 10; ++i) {
if (i == 3) {
std::cout << "Continue at i=" << i << std::endl;
continue;
}
if (i == 7) {
std::cout << "Break at i=" << i << std::endl;
break;
}
std::cout << i << " ";
}
std::cout << std::endl;

// 10. 嵌套循环
std::cout << "\n=== 10. 嵌套循环 ===" << std::endl;
std::cout << "乘法表:" << std::endl;
for (int i = 1; i <= 5; ++i) {
for (int j = 1; j <= 5; ++j) {
std::cout << i << "×" << j << "=" << i * j << "\t";
}
std::cout << std::endl;
}

// 11. goto 语句 (尽量避免使用)
std::cout << "\n=== 11. goto 语句 ===" << std::endl;
int counter = 0;
loop_start:
std::cout << counter << " ";
counter++;
if (counter < 5) {
goto loop_start;
}
std::cout << std::endl;

// 12. 条件运算符 (三元运算符)
std::cout << "\n=== 12. 条件运算符 ===" << std::endl;
int a = 10, b = 20;
int max = (a > b) ? a : b;
std::cout << "Max of " << a << " and " << b << " is " << max << std::endl;

// 13. 逻辑运算符
std::cout << "\n=== 13. 逻辑运算符 ===" << std::endl;
bool isSunny = true;
bool isWarm = true;

if (isSunny && isWarm) {
std::cout << "It's a nice day!" << std::endl;
}

bool hasUmbrella = false;
if (!isSunny || hasUmbrella) {
std::cout << "You don't need sunscreen." << std::endl;
}

// 14. 随机数和循环
std::cout << "\n=== 14. 随机数和循环 ===" << std::endl;
srand(time(0));
std::cout << "生成 10 个 1-100 之间的随机数:" << std::endl;
for (int i = 0; i < 10; ++i) {
int randomNum = rand() % 100 + 1;
std::cout << randomNum << " ";
}
std::cout << std::endl;

// 15. 无限循环和 break
std::cout << "\n=== 15. 无限循环和 break ===" << std::endl;
int attempts = 0;
while (true) {
int guess = rand() % 10 + 1;
std::cout << "Guess: " << guess << std::endl;
attempts++;
if (guess == 7) {
std::cout << "Found 7 in " << attempts << " attempts!" << std::endl;
break;
}
if (attempts > 5) {
std::cout << "Gave up after " << attempts << " attempts." << std::endl;
break;
}
}

return 0;
}

C++ 变量和数据类型示例

功能描述

这是一个展示 C++ 中各种变量类型和数据类型的示例程序。它演示了基本数据类型、无符号整数、大整数、固定宽度整数类型、常量、类型转换、变量作用域、变量初始化以及类型别名等概念。

代码解析

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

int main() {
// 基本数据类型
int age = 25;
float height = 1.75f;
double price = 19.99;
char grade = 'A';
bool isStudent = true;
std::string name = "John Doe";

// 输出变量值
std::cout << "Name: " << name << std::endl;
// ... 其他输出语句 ...

return 0;
}

数据类型详解

基本数据类型

类型 大小 范围 示例
int 通常4字节 -2,147,483,648 到 2,147,483,647 int age = 25;
float 4字节 约 ±3.4e38 float height = 1.75f;
double 8字节 约 ±1.7e308 double price = 19.99;
char 1字节 -128 到 127 或 0 到 255 char grade = 'A';
bool 通常1字节 truefalse bool isStudent = true;
std::string 可变 字符串 std::string name = "John Doe";

无符号和大整数类型

类型 大小 范围 示例
unsigned int 通常4字节 0 到 4,294,967,295 unsigned int unsignedNum = 42;
long long 8字节 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 long long largeNumber = 9223372036854775807LL;
unsigned long long 8字节 0 到 18,446,744,073,709,551,615 unsigned long long ulargeNumber = 18446744073709551615ULL;

固定宽度整数类型 (C++11+)

类型 大小 范围 示例
int8_t 1字节 -128 到 127 int8_t smallInt = -128;
uint8_t 1字节 0 到 255 uint8_t unsignedSmallInt = 255;
int16_t 2字节 -32,768 到 32,767 int16_t mediumInt = 32767;
uint16_t 2字节 0 到 65,535 uint16_t unsignedMediumInt = 65535;
int32_t 4字节 -2,147,483,648 到 2,147,483,647 int32_t normalInt = -2147483648;
uint32_t 4字节 0 到 4,294,967,295 uint32_t unsignedNormalInt = 4294967295;
int64_t 8字节 -9,223,372,036,854,775,808 到 9,223,372,036,854,775,807 int64_t bigInt = -9223372036854775808LL;
uint64_t 8字节 0 到 18,446,744,073,709,551,615 uint64_t unsignedBigInt = 18446744073709551615ULL;

常量

类型 描述 示例
const 运行时常量 const int MAX_VALUE = 100;
constexpr 编译时常量 (C++11+) constexpr double PI = 3.14159265359;

编译和运行

在 Windows 上编译(使用 g++):

1
g++ -std=c++11 -fexec-charset=GBK -o variables variables.cpp

运行程序:

1
.\variables.exe

输出结果:

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
44
45
46
47
48
49
50
51
=== 基本数据类型 ===
Name: John Doe
Age: 25
Height: 1.75m
Price: $19.99
Grade: A
Is Student: Yes

=== 无符号和大整数类型 ===
Unsigned: 42
Large Number: 9223372036854775807

=== 固定宽度整数类型 ===
int8_t: -128
uint8_t: 255
int16_t: 32767
uint16_t: 65535
int32_t: -2147483648
uint32_t: 4294967295
int64_t: -9223372036854775808
uint64_t: 18446744073709551615

=== 常量 ===
MAX_VALUE: 100
PI: 3.14159

=== 类型转换 ===
Implicit conversion (int to double): 103.14
Explicit conversion (double to int): 3
C-style conversion (double to int): 3

=== 变量的作用域 ===
Local variable: 20
Global-like variable inside block: 10
Global-like variable outside block: 10

=== 变量的初始化 ===
a: 1
b: 2
c: 3
d: 4
e: 0

=== 类型别名 ===
typedef unsigned long: 123456789
using unsigned int: 987654321

=== 字符串类型 ===
C-style string: Hello C-style string
C++ string: Hello C++ string
C++ string length: 16

技术要点

1. 变量声明和初始化

C++ 中变量的声明和初始化方式有多种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 拷贝初始化
int a = 1;

// 直接初始化
int b(2);

// 列表初始化 (C++11+)
int c{3};

// 拷贝列表初始化 (C++11+)
int d = {4};

// 值初始化 (C++11+)
int e{};

2. 类型转换

隐式类型转换

当两种类型兼容时,C++ 会自动进行类型转换:

1
2
3
int integer = 100;
double floating = 3.14;
double result = integer + floating; // int 自动转换为 double

显式类型转换

使用 static_cast 进行类型转换:

1
2
double floating = 3.14;
int result = static_cast<int>(floating); // 显式转换 double 为 int

C风格类型转换

C++ 也支持 C 风格的类型转换:

1
2
double floating = 3.14;
int result = (int)floating; // C风格转换

3. 变量的作用域

变量的作用域决定了它在程序中的可见性:

1
2
3
4
5
6
7
int globalLike = 10; // 在 main 函数作用域内可见
{
int local = 20; // 只在这个代码块内可见
std::cout << local << std::endl; // 正确
}
std::cout << globalLike << std::endl; // 正确
// std::cout << local << std::endl; // 错误,local 在此处不可见

4. 类型别名

C++ 提供了两种方式创建类型别名:

1
2
3
4
5
6
7
// 使用 typedef
typedef unsigned long ulong;
ulong number1 = 123456789;

// 使用 using (C++11+)
using uint = unsigned int;
uint number2 = 987654321;

5. 字符串类型

C++ 提供了两种字符串表示方式:

1
2
3
4
5
// C风格字符串
char cString[] = "Hello C-style string";

// C++字符串 (推荐)
std::string cppString = "Hello C++ string";

std::string 是一个类,提供了许多有用的方法,如 length()append()substr() 等。

常见问题

1. 整数溢出

当整数超出其类型的范围时,会发生溢出:

1
2
int maxInt = 2147483647;
maxInt++; // 溢出,结果未定义

解决方案:使用更大范围的整数类型,如 long long 或固定宽度整数类型。

2. 浮点数精度问题

浮点数有精度限制:

1
2
3
float f = 0.1;
double d = 0.1;
// f 和 d 可能不完全等于 0.1

解决方案:对于需要高精度的计算,使用 double 而不是 float,或考虑使用专门的库。

3. 类型不匹配

当尝试将一种类型的值赋给不兼容的类型时:

1
int x = "hello"; // 错误,类型不匹配

解决方案:确保类型匹配或使用适当的类型转换。

4. 未初始化的变量

使用未初始化的变量会导致未定义行为:

1
2
int x;
std::cout << x; // 未定义行为

解决方案:始终初始化变量。

代码优化建议

  1. 使用固定宽度整数类型:在需要精确控制大小的情况下,使用 std::int32_tstd::uint64_t 等固定宽度类型。

  2. 优先使用 constexpr:对于编译时常量,使用 constexpr 而不是 const

  3. 使用列表初始化:列表初始化可以防止窄化转换:

1
2
int x = 3.14; // 允许,会截断
int y{3.14}; // 错误,防止窄化转换
  1. 避免使用 C 风格类型转换:使用 static_castdynamic_castconst_castreinterpret_cast 代替 C 风格转换。

  2. 使用 std::string:对于字符串操作,优先使用 std::string 而不是 C 风格字符串。

总结

C++ 提供了丰富的数据类型和变量声明方式,包括:

  • 基本数据类型:intfloatdoublecharbool
  • 无符号和大整数类型:unsigned intlong long
  • 固定宽度整数类型:int8_tuint16_t
  • 常量:constconstexpr
  • 字符串:std::string

了解这些数据类型及其特性对于编写正确、高效的 C++ 代码至关重要。通过适当选择数据类型,可以提高程序的性能、可靠性和可读性。

variables.cpp

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
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
#include <iostream>
#include <string>
#include <cstdint>
#include <climits>


int main() {
// 基本数据类型
int age = 25;
float height = 1.75f;
double price = 19.99;
char grade = 'A';
bool isStudent = true;
std::string name = "John Doe";

std::cout << "=== 基本数据类型 ===" << std::endl;
std::cout << "Name: " << name << std::endl;
std::cout << "Age: " << age << std::endl;
std::cout << "Height: " << height << "m" << std::endl;
std::cout << "Price: $" << price << std::endl;
std::cout << "Grade: " << grade << std::endl;
std::cout << "Is Student: " << (isStudent ? "Yes" : "No") << std::endl;

// 无符号和大整数类型
unsigned int unsignedNum = 42;
long long largeNumber = 9223372036854775807LL;

std::cout << "\n=== 无符号和大整数类型 ===" << std::endl;
std::cout << "Unsigned: " << unsignedNum << std::endl;
std::cout << "Large Number: " << largeNumber << std::endl;

// 固定宽度整数类型 (C++11+)
int8_t smallInt = -128;
uint8_t unsignedSmallInt = 255;
int16_t mediumInt = 32767;
uint16_t unsignedMediumInt = 65535;
int32_t normalInt = -2147483648;
uint32_t unsignedNormalInt = 4294967295;
int64_t bigInt = LLONG_MIN;
uint64_t unsignedBigInt = ULLONG_MAX;


std::cout << "\n=== 固定宽度整数类型 ===" << std::endl;
std::cout << "int8_t: " << static_cast<int>(smallInt) << std::endl;
std::cout << "uint8_t: " << static_cast<int>(unsignedSmallInt) << std::endl;
std::cout << "int16_t: " << mediumInt << std::endl;
std::cout << "uint16_t: " << unsignedMediumInt << std::endl;
std::cout << "int32_t: " << normalInt << std::endl;
std::cout << "uint32_t: " << unsignedNormalInt << std::endl;
std::cout << "int64_t: " << bigInt << std::endl;
std::cout << "uint64_t: " << unsignedBigInt << std::endl;

// 常量
const int MAX_VALUE = 100;
constexpr double PI = 3.14159265359;

std::cout << "\n=== 常量 ===" << std::endl;
std::cout << "MAX_VALUE: " << MAX_VALUE << std::endl;
std::cout << "PI: " << PI << std::endl;

// 类型转换
std::cout << "\n=== 类型转换 ===" << std::endl;
int integer = 100;
double floating = 3.14;

// 隐式类型转换
double result1 = integer + floating;
std::cout << "Implicit conversion (int to double): " << result1 << std::endl;

// 显式类型转换
int result2 = static_cast<int>(floating);
std::cout << "Explicit conversion (double to int): " << result2 << std::endl;

// C风格类型转换
int result3 = (int)floating;
std::cout << "C-style conversion (double to int): " << result3 << std::endl;

// 变量的作用域
std::cout << "\n=== 变量的作用域 ===" << std::endl;
int globalLike = 10;
{
int local = 20;
std::cout << "Local variable: " << local << std::endl;
std::cout << "Global-like variable inside block: " << globalLike << std::endl;
}
std::cout << "Global-like variable outside block: " << globalLike << std::endl;

// 变量的初始化
std::cout << "\n=== 变量的初始化 ===" << std::endl;
int a = 1; // 拷贝初始化
int b(2); // 直接初始化
int c{3}; // 列表初始化 (C++11+)
int d = {4}; // 拷贝列表初始化 (C++11+)
int e{}; // 值初始化 (C++11+)

std::cout << "a: " << a << std::endl;
std::cout << "b: " << b << std::endl;
std::cout << "c: " << c << std::endl;
std::cout << "d: " << d << std::endl;
std::cout << "e: " << e << std::endl;

// 类型别名
std::cout << "\n=== 类型别名 ===" << std::endl;
typedef unsigned long ulong;
using uint = unsigned int;

ulong number1 = 123456789;
uint number2 = 987654321;

std::cout << "typedef unsigned long: " << number1 << std::endl;
std::cout << "using unsigned int: " << number2 << std::endl;

// 字符串类型
std::cout << "\n=== 字符串类型 ===" << std::endl;
char cString[] = "Hello C-style string";
std::string cppString = "Hello C++ string";

std::cout << "C-style string: " << cString << std::endl;
std::cout << "C++ string: " << cppString << std::endl;
std::cout << "C++ string length: " << cppString.length() << std::endl;

return 0;
}

C++ Hello World 示例

功能描述

这是一个最基础的 C++ 程序,用于输出 “Hello, World!” 到控制台。这是学习任何编程语言时的第一个经典示例,它展示了 C++ 程序的基本结构和输出功能。

代码解析

1
2
3
4
5
6
#include <iostream>

int main() {
std::cout << "Hello, World!" << std::endl;
return 0;
}

代码说明:

  1. 头文件包含

    1
    #include <iostream>
    • 包含标准输入输出流库,使我们能够使用 std::cout 进行输出操作。
  2. 主函数

    1
    2
    3
    4
    int main() {
    // 代码逻辑
    return 0;
    }
    • main() 是 C++ 程序的入口点,每个 C++ 程序都必须有一个 main 函数。
    • 返回类型为 int,表示程序的退出状态。
    • return 0; 表示程序正常结束。
  3. 输出语句

    1
    std::cout << "Hello, World!" << std::endl;
    • std::cout 是标准输出流对象,用于向控制台输出数据。
    • << 是流插入运算符,用于将右侧的数据插入到左侧的流中。
    • "Hello, World!" 是一个字符串字面量。
    • std::endl 是一个操纵符,用于输出换行符并刷新输出缓冲区。

编译和运行

在 Windows 上编译(使用 g++):

1
g++ -o hello_world hello_world.cpp

运行程序:

1
./hello_world

输出结果:

1
Hello, World!

扩展示例

示例 1:输出多行文本

1
2
3
4
5
6
7
8
#include <iostream>

int main() {
std::cout << "Hello, World!" << std::endl;
std::cout << "Welcome to C++ programming!" << std::endl;
std::cout << "This is a multi-line output example." << std::endl;
return 0;
}

示例 2:使用变量

1
2
3
4
5
6
7
8
9
#include <iostream>
#include <string>

int main() {
std::string name = "World";
std::cout << "Hello, " << name << "!" << std::endl;
std::cout << "How are you today?" << std::endl;
return 0;
}

示例 3:接收用户输入

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

int main() {
std::string name;
std::cout << "Please enter your name: ";
std::getline(std::cin, name);
std::cout << "Hello, " << name << "!" << std::endl;
std::cout << "Nice to meet you!" << std::endl;
return 0;
}

示例 4:输出特殊字符

1
2
3
4
5
6
7
8
#include <iostream>

int main() {
std::cout << "Hello, World!" << std::endl;
std::cout << "Special characters: \\n \\t \\\\ \" \'" << std::endl;
std::cout << "Unicode: \u00A9 \u2603 \u2764" << std::endl;
return 0;
}

技术要点

  1. C++ 程序结构

    • 包含必要的头文件
    • 定义 main() 函数
    • 编写代码逻辑
    • 返回适当的退出状态
  2. 标准输出

    • 使用 std::cout 进行输出
    • 使用 << 运算符连接输出内容
    • 使用 std::endl 输出换行符
  3. 命名空间

    • std 是 C++ 标准库的命名空间
    • 可以使用 using namespace std; 来简化代码,但在大型项目中不推荐
  4. 编译过程

    • 预处理:处理 #include 指令
    • 编译:将源代码编译为目标代码
    • 链接:将目标代码链接为可执行文件

常见问题

1. 编译错误:'cout' was not declared in this scope

原因:忘记包含 <iostream> 头文件或使用 std:: 前缀。
解决方案:确保包含了 <iostream> 头文件,并使用 std::cout 或添加 using namespace std;

2. 运行时无输出

原因:可能是输出缓冲区未刷新。
解决方案:使用 std::endlstd::flush 刷新输出缓冲区。

3. 中文乱码

原因:编码问题或控制台不支持 UTF-8。
解决方案:确保源文件使用 UTF-8 编码,并在支持 UTF-8 的控制台中运行。

总结

“Hello, World!” 程序虽然简单,但它是学习 C++ 编程的起点,帮助我们理解:

  • C++ 程序的基本结构
  • 如何包含头文件
  • 如何使用标准输出
  • 如何编译和运行 C++ 程序

通过这个简单的示例,我们迈出了 C++ 编程的第一步,为后续学习更复杂的概念打下了基础。

hello_world.cpp

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

int main() {
// 基本输出
std::cout << "Hello, World!" << std::endl;

// 输出多行文本
std::cout << "Welcome to C++ programming!" << std::endl;
std::cout << "This is a simple Hello World program." << std::endl;

// 使用变量
std::string name = "World";
std::cout << "Hello, " << name << "!" << std::endl;

// 输出特殊字符
std::cout << "Special characters: \\n \\t \\\\ \" \'" << std::endl;
std::cout << "Unicode: \u00A9 \u2603 \u2764" << std::endl;

return 0;
}

C语言排序算法

概述

本文档详细介绍C语言中各种常见排序算法的实现和分析,包括冒泡排序、选择排序、插入排序、快速排序、归并排序,以及补充的希尔排序、堆排序等算法。每种排序算法都包含算法原理、实现代码、时间复杂度和空间复杂度分析,以及适当的示例代码。

排序算法的基本概念

排序算法的分类

分类标准 类型 说明 示例
时间复杂度 简单排序 O(n²) 冒泡排序、选择排序、插入排序
高级排序 O(nlogn) 快速排序、归并排序、堆排序
线性排序 O(n) 计数排序、桶排序、基数排序
排序稳定性 稳定排序 相等元素的相对顺序不变 冒泡排序、插入排序、归并排序
不稳定排序 相等元素的相对顺序可能改变 选择排序、快速排序、堆排序
排序方式 原地排序 不需要额外存储空间 冒泡排序、选择排序、插入排序、快速排序
非原地排序 需要额外存储空间 归并排序、计数排序、桶排序

排序算法的性能评估

排序算法的性能主要通过以下指标评估:

  • 时间复杂度:算法执行时间随输入规模增长的变化趋势
  • 空间复杂度:算法执行所需的额外存储空间
  • 稳定性:排序后相等元素的相对顺序是否保持不变
  • 适应性:算法对已部分排序数据的处理效率
  • 操作次数:比较次数和交换次数

辅助函数

打印数组

1
2
3
4
5
6
7
8
9
10
11
/**
* @brief 打印数组
* @param arr 要打印的数组
* @param size 数组的大小
*/
void print_array(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}

生成随机数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* @brief 生成随机数组
* @param arr 要生成的数组
* @param size 数组的大小
* @param min 随机数的最小值
* @param max 随机数的最大值
*/
void generate_random_array(int arr[], int size, int min, int max) {
// 初始化随机种子
srand(time(NULL));

for (int i = 0; i < size; i++) {
// 生成[min, max]范围内的随机数
arr[i] = min + rand() % (max - min + 1);
}
}

复制数组

1
2
3
4
5
6
7
8
9
10
11
/**
* @brief 复制数组
* @param dest 目标数组
* @param src 源数组
* @param size 数组的大小
*/
void copy_array(int dest[], int src[], int size) {
for (int i = 0; i < size; i++) {
dest[i] = src[i];
}
}

冒泡排序

算法原理

冒泡排序通过重复地遍历要排序的数列,一次比较两个元素,如果它们的顺序错误就把它们交换过来。遍历数列的工作是重复进行直到没有再需要交换的元素为止,也就是说该数列已经排序完成。

实现代码

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
/**
* @brief 冒泡排序
* @param arr 要排序的数组
* @param size 数组的大小
* @details 冒泡排序通过重复地遍历要排序的数列,一次比较两个元素,
* 如果它们的顺序错误就把它们交换过来。
*/
void bubble_sort(int arr[], int size) {
for (int i = 0; i < size - 1; i++) {
// 标记本轮是否有交换,优化冒泡排序
int swapped = 0;

for (int j = 0; j < size - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// 交换arr[j]和arr[j+1]
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swapped = 1;
}
}

// 如果本轮没有交换,说明数组已经有序,提前退出
if (swapped == 0) {
break;
}
}
}

算法分析

  • 时间复杂度
    • 最好情况:O(n)(已排序数组)
    • 平均情况:O(n²)
    • 最坏情况:O(n²)(逆序数组)
  • 空间复杂度:O(1)
  • 稳定性:稳定
  • 适应性:适应性强,对已部分排序的数据处理效率高

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 冒泡排序示例
void bubble_sort_example() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int size = sizeof(arr) / sizeof(arr[0]);

printf("冒泡排序示例:\n");
printf("排序前:");
print_array(arr, size);

bubble_sort(arr, size);

printf("排序后:");
print_array(arr, size);
}

补充示例:双向冒泡排序

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
44
45
46
// 双向冒泡排序(鸡尾酒排序)
void cocktail_sort(int arr[], int size) {
int left = 0, right = size - 1;
int swapped = 1;

while (left < right && swapped) {
swapped = 0;

// 从左到右,将最大元素移到右侧
for (int i = left; i < right; i++) {
if (arr[i] > arr[i + 1]) {
int temp = arr[i];
arr[i] = arr[i + 1];
arr[i + 1] = temp;
swapped = 1;
}
}
right--;

// 从右到左,将最小元素移到左侧
for (int i = right; i > left; i--) {
if (arr[i] < arr[i - 1]) {
int temp = arr[i];
arr[i] = arr[i - 1];
arr[i - 1] = temp;
swapped = 1;
}
}
left++;
}
}

// 双向冒泡排序示例
void cocktail_sort_example() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int size = sizeof(arr) / sizeof(arr[0]);

printf("双向冒泡排序示例:\n");
printf("排序前:");
print_array(arr, size);

cocktail_sort(arr, size);

printf("排序后:");
print_array(arr, size);
}

选择排序

算法原理

选择排序的基本思想是:第一次从待排序的数据元素中选出最小(或最大)的一个元素,存放在序列的起始位置;然后再从剩余的未排序元素中寻找到最小(或最大)元素,放到已排序的序列的末尾。以此类推,直到全部待排序的数据元素的个数为零。

实现代码

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
/**
* @brief 选择排序
* @param arr 要排序的数组
* @param size 数组的大小
* @details 选择排序每次从待排序数组中找到最小(或最大)的元素,
* 放到已排序数组的末尾。
*/
void selection_sort(int arr[], int size) {
for (int i = 0; i < size - 1; i++) {
// 找到未排序部分的最小值索引
int min_idx = i;
for (int j = i + 1; j < size; j++) {
if (arr[j] < arr[min_idx]) {
min_idx = j;
}
}

// 交换找到的最小值和未排序部分的第一个元素
if (min_idx != i) {
int temp = arr[i];
arr[i] = arr[min_idx];
arr[min_idx] = temp;
}
}
}

算法分析

  • 时间复杂度
    • 最好情况:O(n²)
    • 平均情况:O(n²)
    • 最坏情况:O(n²)
  • 空间复杂度:O(1)
  • 稳定性:不稳定
  • 适应性:不适应,无论输入数据如何,时间复杂度都是O(n²)

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 选择排序示例
void selection_sort_example() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int size = sizeof(arr) / sizeof(arr[0]);

printf("选择排序示例:\n");
printf("排序前:");
print_array(arr, size);

selection_sort(arr, size);

printf("排序后:");
print_array(arr, size);
}

补充示例:双向选择排序

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
44
45
46
47
48
49
50
51
52
53
54
// 双向选择排序
void bidirectional_selection_sort(int arr[], int size) {
int left = 0, right = size - 1;

while (left < right) {
int min_idx = left, max_idx = left;

// 找出当前区间的最小值和最大值
for (int i = left; i <= right; i++) {
if (arr[i] < arr[min_idx]) {
min_idx = i;
}
if (arr[i] > arr[max_idx]) {
max_idx = i;
}
}

// 将最小值交换到左侧
if (min_idx != left) {
int temp = arr[left];
arr[left] = arr[min_idx];
arr[min_idx] = temp;
// 如果最大值是left,交换后最大值位置变为min_idx
if (max_idx == left) {
max_idx = min_idx;
}
}

// 将最大值交换到右侧
if (max_idx != right) {
int temp = arr[right];
arr[right] = arr[max_idx];
arr[max_idx] = temp;
}

left++;
right--;
}
}

// 双向选择排序示例
void bidirectional_selection_sort_example() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int size = sizeof(arr) / sizeof(arr[0]);

printf("双向选择排序示例:\n");
printf("排序前:");
print_array(arr, size);

bidirectional_selection_sort(arr, size);

printf("排序后:");
print_array(arr, size);
}

插入排序

算法原理

插入排序的基本思想是:将数组分为已排序部分和未排序部分,初始时已排序部分只包含第一个元素,然后依次将未排序部分的每个元素插入到已排序部分的适当位置,直到整个数组排序完成。

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* @brief 插入排序
* @param arr 要排序的数组
* @param size 数组的大小
* @details 插入排序将数组分为已排序和未排序两部分,
* 每次从未排序部分取一个元素插入到已排序部分的正确位置。
*/
void insertion_sort(int arr[], int size) {
for (int i = 1; i < size; i++) {
int key = arr[i];
int j = i - 1;

// 将大于key的元素向后移动
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}

// 插入key到正确位置
arr[j + 1] = key;
}
}

算法分析

  • 时间复杂度
    • 最好情况:O(n)(已排序数组)
    • 平均情况:O(n²)
    • 最坏情况:O(n²)(逆序数组)
  • 空间复杂度:O(1)
  • 稳定性:稳定
  • 适应性:适应性强,对已部分排序的数据处理效率高

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 插入排序示例
void insertion_sort_example() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int size = sizeof(arr) / sizeof(arr[0]);

printf("插入排序示例:\n");
printf("排序前:");
print_array(arr, size);

insertion_sort(arr, size);

printf("排序后:");
print_array(arr, size);
}

补充示例:折半插入排序

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
// 折半插入排序
void binary_insertion_sort(int arr[], int size) {
for (int i = 1; i < size; i++) {
int key = arr[i];
int left = 0, right = i - 1;

// 折半查找插入位置
while (left <= right) {
int mid = left + (right - left) / 2;
if (arr[mid] > key) {
right = mid - 1;
} else {
left = mid + 1;
}
}

// 移动元素
for (int j = i - 1; j >= left; j--) {
arr[j + 1] = arr[j];
}

// 插入元素
arr[left] = key;
}
}

// 折半插入排序示例
void binary_insertion_sort_example() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int size = sizeof(arr) / sizeof(arr[0]);

printf("折半插入排序示例:\n");
printf("排序前:");
print_array(arr, size);

binary_insertion_sort(arr, size);

printf("排序后:");
print_array(arr, size);
}

快速排序

算法原理

快速排序的基本思想是:选择一个基准元素,将数组分为小于基准和大于基准的两部分,然后递归地对这两部分进行排序。快速排序是一种分治算法,通过一趟排序将数组分成两部分,然后分别对这两部分继续排序。

实现代码

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
/**
* @brief 快速排序
* @param arr 要排序的数组
* @param low 起始索引
* @param high 结束索引
* @details 快速排序通过选择一个基准元素,将数组分为小于基准和大于基准两部分,
* 然后递归地对这两部分进行排序。
*/
void quick_sort(int arr[], int low, int high) {
if (low < high) {
// 分区操作,获取基准元素的最终位置
int pivot = arr[high]; // 选择最后一个元素作为基准
int i = (low - 1); // 小于基准的元素的索引

for (int j = low; j < high; j++) {
if (arr[j] < pivot) {
i++;
// 交换arr[i]和arr[j]
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}

// 交换基准元素到正确位置
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;

int pi = i + 1; // 基准元素的最终位置

// 递归排序基准元素左右两部分
quick_sort(arr, low, pi - 1);
quick_sort(arr, pi + 1, high);
}
}

算法分析

  • 时间复杂度
    • 最好情况:O(nlogn)(每次分区都将数组均匀分成两部分)
    • 平均情况:O(nlogn)
    • 最坏情况:O(n²)(数组已排序或逆序,每次分区都将数组分成1和n-1两部分)
  • 空间复杂度
    • 最好情况:O(logn)
    • 最坏情况:O(n)
  • 稳定性:不稳定
  • 适应性:不适应,对已排序数组的处理效率低

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 快速排序示例
void quick_sort_example() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int size = sizeof(arr) / sizeof(arr[0]);

printf("快速排序示例:\n");
printf("排序前:");
print_array(arr, size);

quick_sort(arr, 0, size - 1);

printf("排序后:");
print_array(arr, size);
}

补充示例:三数取中法快速排序

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
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
69
70
71
72
73
74
75
76
77
// 三数取中法选择基准
int median_of_three(int arr[], int low, int high) {
int mid = low + (high - low) / 2;

// 对三个数进行排序
if (arr[low] > arr[mid]) {
int temp = arr[low];
arr[low] = arr[mid];
arr[mid] = temp;
}
if (arr[low] > arr[high]) {
int temp = arr[low];
arr[low] = arr[high];
arr[high] = temp;
}
if (arr[mid] > arr[high]) {
int temp = arr[mid];
arr[mid] = arr[high];
arr[high] = temp;
}

// 将中位数交换到high-1位置
int temp = arr[mid];
arr[mid] = arr[high - 1];
arr[high - 1] = temp;

return arr[high - 1];
}

// 三数取中法快速排序
void quick_sort_median(int arr[], int low, int high) {
if (low + 3 <= high) {
// 使用三数取中法选择基准
int pivot = median_of_three(arr, low, high);
int i = low, j = high - 1;

while (1) {
while (arr[++i] < pivot) {}
while (arr[--j] > pivot) {}

if (i < j) {
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
} else {
break;
}
}

// 交换基准元素到正确位置
int temp = arr[i];
arr[i] = arr[high - 1];
arr[high - 1] = temp;

// 递归排序基准元素左右两部分
quick_sort_median(arr, low, i - 1);
quick_sort_median(arr, i + 1, high);
} else {
// 对小规模数组使用插入排序
insertion_sort(arr + low, high - low + 1);
}
}

// 三数取中法快速排序示例
void quick_sort_median_example() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int size = sizeof(arr) / sizeof(arr[0]);

printf("三数取中法快速排序示例:\n");
printf("排序前:");
print_array(arr, size);

quick_sort_median(arr, 0, size - 1);

printf("排序后:");
print_array(arr, size);
}

归并排序

算法原理

归并排序的基本思想是:采用分治法,将数组分成两半,分别对两半进行排序,然后将排序好的两半合并成一个有序数组。归并排序是一种稳定的排序算法,需要额外的存储空间来存储合并过程中的临时数据。

实现代码

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
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
69
70
71
72
73
74
75
/**
* @brief 归并排序
* @param arr 要排序的数组
* @param left 左边界
* @param right 右边界
* @details 归并排序采用分治法,将数组分成两半,分别排序,然后合并。
*/
void merge_sort(int arr[], int left, int right) {
if (left < right) {
// 计算中间位置
int mid = left + (right - left) / 2;

// 递归排序左半部分和右半部分
merge_sort(arr, left, mid);
merge_sort(arr, mid + 1, right);

// 合并已排序的两部分
merge(arr, left, mid, right);
}
}

/**
* @brief 归并排序的合并函数
* @param arr 要合并的数组
* @param left 左边界
* @param mid 中间位置
* @param right 右边界
* @details 将两个已排序的子数组合并成一个有序数组。
*/
void merge(int arr[], int left, int mid, int right) {
int i, j, k;
int n1 = mid - left + 1;
int n2 = right - mid;

// 创建临时数组
int L[n1], R[n2];

// 复制数据到临时数组
for (i = 0; i < n1; i++) {
L[i] = arr[left + i];
}
for (j = 0; j < n2; j++) {
R[j] = arr[mid + 1 + j];
}

// 合并临时数组
i = 0; // 左子数组的起始索引
j = 0; // 右子数组的起始索引
k = left; // 合并数组的起始索引

while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k] = L[i];
i++;
} else {
arr[k] = R[j];
j++;
}
k++;
}

// 复制左子数组的剩余元素
while (i < n1) {
arr[k] = L[i];
i++;
k++;
}

// 复制右子数组的剩余元素
while (j < n2) {
arr[k] = R[j];
j++;
k++;
}
}

算法分析

  • 时间复杂度
    • 最好情况:O(nlogn)
    • 平均情况:O(nlogn)
    • 最坏情况:O(nlogn)
  • 空间复杂度:O(n)
  • 稳定性:稳定
  • 适应性:不适应,无论输入数据如何,时间复杂度都是O(nlogn)

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 归并排序示例
void merge_sort_example() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int size = sizeof(arr) / sizeof(arr[0]);

printf("归并排序示例:\n");
printf("排序前:");
print_array(arr, size);

merge_sort(arr, 0, size - 1);

printf("排序后:");
print_array(arr, size);
}

补充示例:迭代归并排序

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
// 迭代归并排序
void iterative_merge_sort(int arr[], int size) {
// 子数组长度从1开始,每次翻倍
for (int curr_size = 1; curr_size < size; curr_size *= 2) {
// 左子数组的起始位置
for (int left_start = 0; left_start < size - 1; left_start += 2 * curr_size) {
// 计算中间位置和右边界
int mid = left_start + curr_size - 1;
int right_end = left_start + 2 * curr_size - 1;
if (right_end >= size) {
right_end = size - 1;
}

// 合并两个子数组
merge(arr, left_start, mid, right_end);
}
}
}

// 迭代归并排序示例
void iterative_merge_sort_example() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int size = sizeof(arr) / sizeof(arr[0]);

printf("迭代归并排序示例:\n");
printf("排序前:");
print_array(arr, size);

iterative_merge_sort(arr, size);

printf("排序后:");
print_array(arr, size);
}

希尔排序

算法原理

希尔排序是插入排序的改进版,它通过将整个数组分割成若干子数组,对每个子数组进行插入排序,然后逐步减小分割间隔,最终完成整个数组的排序。希尔排序的核心思想是先将整个待排序的记录序列分割成为若干子序列分别进行直接插入排序,待整个序列中的记录”基本有序”时,再对全体记录进行依次直接插入排序。

实现代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 希尔排序
void shell_sort(int arr[], int size) {
// 选择间隔序列,这里使用希尔建议的序列:n/2, n/4, ..., 1
for (int gap = size / 2; gap > 0; gap /= 2) {
// 对每个子数组进行插入排序
for (int i = gap; i < size; i++) {
int key = arr[i];
int j = i;

// 在子数组中插入key
while (j >= gap && arr[j - gap] > key) {
arr[j] = arr[j - gap];
j -= gap;
}

arr[j] = key;
}
}
}

算法分析

  • 时间复杂度
    • 取决于间隔序列的选择,一般在O(n¹·³)到O(n²)之间
    • 最好情况:O(n)
    • 最坏情况:O(n²)
  • 空间复杂度:O(1)
  • 稳定性:不稳定
  • 适应性:适应性较强,对已部分排序的数据处理效率较高

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 希尔排序示例
void shell_sort_example() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int size = sizeof(arr) / sizeof(arr[0]);

printf("希尔排序示例:\n");
printf("排序前:");
print_array(arr, size);

shell_sort(arr, size);

printf("排序后:");
print_array(arr, size);
}

补充示例:使用不同间隔序列的希尔排序

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
// 使用Knuth间隔序列的希尔排序
void shell_sort_knuth(int arr[], int size) {
// 计算最大间隔:1, 4, 13, 40, ...
int gap = 1;
while (gap < size / 3) {
gap = 3 * gap + 1;
}

while (gap > 0) {
for (int i = gap; i < size; i++) {
int key = arr[i];
int j = i;

while (j >= gap && arr[j - gap] > key) {
arr[j] = arr[j - gap];
j -= gap;
}

arr[j] = key;
}

// 减小间隔
gap /= 3;
}
}

// 使用Knuth间隔序列的希尔排序示例
void shell_sort_knuth_example() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int size = sizeof(arr) / sizeof(arr[0]);

printf("使用Knuth间隔序列的希尔排序示例:\n");
printf("排序前:");
print_array(arr, size);

shell_sort_knuth(arr, size);

printf("排序后:");
print_array(arr, size);
}

堆排序

算法原理

堆排序是一种基于比较的排序算法,它利用堆这种数据结构来进行排序。堆是一个完全二叉树,分为最大堆和最小堆。在堆排序中,我们通常使用最大堆,即每个节点的值都大于或等于其子节点的值。堆排序的基本思想是:将待排序的数组构建成一个最大堆,然后将堆顶元素(最大值)与堆的最后一个元素交换,然后将剩余的元素重新构建成一个最大堆,重复这个过程,直到整个数组排序完成。

实现代码

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
44
45
46
// 堆排序
void heap_sort(int arr[], int size) {
// 构建最大堆
for (int i = size / 2 - 1; i >= 0; i--) {
heapify(arr, size, i);
}

// 逐个从堆中取出元素
for (int i = size - 1; i > 0; i--) {
// 将堆顶元素(最大值)与末尾元素交换
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;

// 对剩余的堆进行调整
heapify(arr, i, 0);
}
}

// 堆调整函数
void heapify(int arr[], int size, int root) {
int largest = root; // 初始化largest为根节点
int left = 2 * root + 1; // 左子节点
int right = 2 * root + 2; // 右子节点

// 如果左子节点大于根节点
if (left < size && arr[left] > arr[largest]) {
largest = left;
}

// 如果右子节点大于当前最大值
if (right < size && arr[right] > arr[largest]) {
largest = right;
}

// 如果最大值不是根节点
if (largest != root) {
// 交换根节点和最大值
int temp = arr[root];
arr[root] = arr[largest];
arr[largest] = temp;

// 递归调整受影响的子树
heapify(arr, size, largest);
}
}

算法分析

  • 时间复杂度
    • 最好情况:O(nlogn)
    • 平均情况:O(nlogn)
    • 最坏情况:O(nlogn)
  • 空间复杂度:O(1)
  • 稳定性:不稳定
  • 适应性:不适应,无论输入数据如何,时间复杂度都是O(nlogn)

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 堆排序示例
void heap_sort_example() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int size = sizeof(arr) / sizeof(arr[0]);

printf("堆排序示例:\n");
printf("排序前:");
print_array(arr, size);

heap_sort(arr, size);

printf("排序后:");
print_array(arr, size);
}

补充示例:最小堆排序

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 最小堆排序
void min_heap_sort(int arr[], int size) {
// 构建最小堆
for (int i = size / 2 - 1; i >= 0; i--) {
min_heapify(arr, size, i);
}

// 逐个从堆中取出元素
for (int i = size - 1; i > 0; i--) {
// 将堆顶元素(最小值)与末尾元素交换
int temp = arr[0];
arr[0] = arr[i];
arr[i] = temp;

// 对剩余的堆进行调整
min_heapify(arr, i, 0);
}
}

// 最小堆调整函数
void min_heapify(int arr[], int size, int root) {
int smallest = root; // 初始化smallest为根节点
int left = 2 * root + 1; // 左子节点
int right = 2 * root + 2; // 右子节点

// 如果左子节点小于根节点
if (left < size && arr[left] < arr[smallest]) {
smallest = left;
}

// 如果右子节点小于当前最小值
if (right < size && arr[right] < arr[smallest]) {
smallest = right;
}

// 如果最小值不是根节点
if (smallest != root) {
// 交换根节点和最小值
int temp = arr[root];
arr[root] = arr[smallest];
arr[smallest] = temp;

// 递归调整受影响的子树
min_heapify(arr, size, smallest);
}
}

// 最小堆排序示例
void min_heap_sort_example() {
int arr[] = {64, 34, 25, 12, 22, 11, 90};
int size = sizeof(arr) / sizeof(arr[0]);

printf("最小堆排序示例:\n");
printf("排序前:");
print_array(arr, size);

min_heap_sort(arr, size);

printf("排序后:");
print_array(arr, size);
}

排序算法的比较与选择

各排序算法的比较

排序算法 平均时间复杂度 最坏时间复杂度 空间复杂度 稳定性 适应性
冒泡排序 O(n²) O(n²) O(1) 稳定 适应
选择排序 O(n²) O(n²) O(1) 不稳定 不适应
插入排序 O(n²) O(n²) O(1) 稳定 适应
希尔排序 O(n¹·³) O(n²) O(1) 不稳定 适应
快速排序 O(nlogn) O(n²) O(logn) 不稳定 不适应
归并排序 O(nlogn) O(nlogn) O(n) 稳定 不适应
堆排序 O(nlogn) O(nlogn) O(1) 不稳定 不适应

排序算法的选择建议

  1. 小规模数据(n ≤ 50):插入排序或选择排序
  2. 中等规模数据(50 < n ≤ 1000):希尔排序
  3. 大规模数据(n > 1000):快速排序、归并排序或堆排序
  4. 要求排序稳定:归并排序
  5. 要求原地排序:快速排序、堆排序
  6. 数据基本有序:插入排序或冒泡排序
  7. 数据分布均匀:快速排序
  8. 需要最坏情况保证:归并排序或堆排序

排序算法的应用示例

示例:学生成绩排序

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
// 学生结构体
typedef struct {
char name[50];
int score;
} Student;

// 打印学生数组
void print_students(Student students[], int size) {
for (int i = 0; i < size; i++) {
printf("姓名:%s,成绩:%d\n", students[i].name, students[i].score);
}
}

// 按成绩排序学生(使用快速排序)
void sort_students_by_score(Student students[], int low, int high) {
if (low < high) {
Student pivot = students[high];
int i = low - 1;

for (int j = low; j < high; j++) {
if (students[j].score < pivot.score) {
i++;
Student temp = students[i];
students[i] = students[j];
students[j] = temp;
}
}

Student temp = students[i + 1];
students[i + 1] = students[high];
students[high] = temp;

int pi = i + 1;

sort_students_by_score(students, low, pi - 1);
sort_students_by_score(students, pi + 1, high);
}
}

// 学生成绩排序示例
void student_sort_example() {
Student students[] = {
{"张三", 85},
{"李四", 92},
{"王五", 78},
{"赵六", 95},
{"钱七", 88}
};
int size = sizeof(students) / sizeof(students[0]);

printf("学生成绩排序示例:\n");
printf("排序前:\n");
print_students(students, size);

sort_students_by_score(students, 0, size - 1);

printf("\n按成绩排序后:\n");
print_students(students, size);
}

示例:字符串排序

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
// 字符串排序(使用冒泡排序)
void sort_strings(char *strings[], int size) {
for (int i = 0; i < size - 1; i++) {
int swapped = 0;

for (int j = 0; j < size - i - 1; j++) {
if (strcmp(strings[j], strings[j + 1]) > 0) {
char *temp = strings[j];
strings[j] = strings[j + 1];
strings[j + 1] = temp;
swapped = 1;
}
}

if (!swapped) {
break;
}
}
}

// 字符串排序示例
void string_sort_example() {
char *strings[] = {"banana", "apple", "orange", "grape", "pear"};
int size = sizeof(strings) / sizeof(strings[0]);

printf("字符串排序示例:\n");
printf("排序前:\n");
for (int i = 0; i < size; i++) {
printf("%s ", strings[i]);
}
printf("\n");

sort_strings(strings, size);

printf("排序后:\n");
for (int i = 0; i < size; i++) {
printf("%s ", strings[i]);
}
printf("\n");
}

排序算法的优化策略

  1. 针对小规模数据:对于小规模数据(通常n < 100),使用插入排序等简单排序算法,因为它们的常数因子较小
  2. 混合排序算法:结合不同排序算法的优点,例如快速排序中对小规模子数组使用插入排序
  3. 利用已排序信息:对于部分有序的数据,使用适应性强的排序算法
  4. 选择合适的基准元素:在快速排序中使用三数取中法选择基准元素,避免最坏情况
  5. 避免不必要的交换:在冒泡排序和选择排序中,使用标记变量检测是否已排序
  6. 减少比较次数:在折半插入排序中使用二分查找减少比较次数
  7. 内存访问优化:考虑缓存局部性,优化内存访问模式
  8. 并行化:对于大规模数据,考虑使用并行排序算法

排序算法的性能测试

性能测试函数

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
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
69
70
71
72
73
74
75
76
77
78
79
80
81
// 排序算法性能测试
void test_sort_performance() {
int sizes[] = {1000, 5000, 10000, 50000};
int num_sizes = sizeof(sizes) / sizeof(sizes[0]);

for (int i = 0; i < num_sizes; i++) {
int size = sizes[i];
int *arr = (int *)malloc(size * sizeof(int));
int *temp = (int *)malloc(size * sizeof(int));

if (arr == NULL || temp == NULL) {
printf("内存分配失败!\n");
return;
}

// 生成随机数组
generate_random_array(arr, size, 1, 100000);

printf("\n测试数组大小:%d\n", size);

// 测试冒泡排序
copy_array(temp, arr, size);
clock_t start = clock();
bubble_sort(temp, size);
clock_t end = clock();
double bubble_time = (double)(end - start) / CLOCKS_PER_SEC;
printf("冒泡排序:%.6f秒\n", bubble_time);

// 测试选择排序
copy_array(temp, arr, size);
start = clock();
selection_sort(temp, size);
end = clock();
double selection_time = (double)(end - start) / CLOCKS_PER_SEC;
printf("选择排序:%.6f秒\n", selection_time);

// 测试插入排序
copy_array(temp, arr, size);
start = clock();
insertion_sort(temp, size);
end = clock();
double insertion_time = (double)(end - start) / CLOCKS_PER_SEC;
printf("插入排序:%.6f秒\n", insertion_time);

// 测试希尔排序
copy_array(temp, arr, size);
start = clock();
shell_sort(temp, size);
end = clock();
double shell_time = (double)(end - start) / CLOCKS_PER_SEC;
printf("希尔排序:%.6f秒\n", shell_time);

// 测试快速排序
copy_array(temp, arr, size);
start = clock();
quick_sort(temp, 0, size - 1);
end = clock();
double quick_time = (double)(end - start) / CLOCKS_PER_SEC;
printf("快速排序:%.6f秒\n", quick_time);

// 测试归并排序
copy_array(temp, arr, size);
start = clock();
merge_sort(temp, 0, size - 1);
end = clock();
double merge_time = (double)(end - start) / CLOCKS_PER_SEC;
printf("归并排序:%.6f秒\n", merge_time);

// 测试堆排序
copy_array(temp, arr, size);
start = clock();
heap_sort(temp, size);
end = clock();
double heap_time = (double)(end - start) / CLOCKS_PER_SEC;
printf("堆排序:%.6f秒\n", heap_time);

// 释放内存
free(arr);
free(temp);
}
}

测试结果分析

排序算法的性能测试结果会受到多种因素的影响,包括:

  • 硬件环境:CPU速度、内存大小、缓存性能等
  • 数据特征:数据规模、数据分布、有序程度等
  • 实现细节:代码优化程度、编译器优化等

一般来说,对于大规模数据,高级排序算法(快速排序、归并排序、堆排序)的性能明显优于简单排序算法(冒泡排序、选择排序、插入排序)。在高级排序算法中,快速排序通常表现最好,但在最坏情况下性能会下降。归并排序的性能稳定,但需要额外的存储空间。堆排序不需要额外存储空间,但常数因子较大,实际性能可能不如快速排序。

总结

本文档详细介绍了C语言中各种常见排序算法的实现和分析,包括:

  1. 简单排序算法:冒泡排序、选择排序、插入排序
  2. 高级排序算法:快速排序、归并排序
  3. 补充排序算法:希尔排序、堆排序

每种排序算法都包含了算法原理、实现代码、时间复杂度和空间复杂度分析,以及适当的示例代码。此外,还介绍了排序算法的分类、性能评估、选择建议、优化策略和性能测试方法。

掌握这些排序算法对于理解算法设计的基本思想和解决实际问题都非常重要。在实际应用中,应根据具体情况选择合适的排序算法,以达到最佳的性能效果。

sorting.c

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
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

/**
* @brief 排序算法示例程序
* @details 这个程序演示了C语言中各种常见的排序算法,包括
* 冒泡排序、选择排序、插入排序、快速排序和归并排序,
* 并提供了算法的时间复杂度分析和演示示例。
* @return 0 表示程序成功执行
*/

// =========================
// 函数原型声明
// =========================
void print_array(int arr[], int size); // 打印数组
void bubble_sort(int arr[], int size); // 冒泡排序
void selection_sort(int arr[], int size); // 选择排序
void insertion_sort(int arr[], int size); // 插入排序
void quick_sort(int arr[], int low, int high); // 快速排序
void merge_sort(int arr[], int left, int right); // 归并排序
void merge(int arr[], int left, int mid, int right); // 归并排序的合并函数
void generate_random_array(int arr[], int size, int min, int max); // 生成随机数组
void copy_array(int dest[], int src[], int size); // 复制数组

/**
* @brief 主函数
* @return 0 表示程序成功执行
*/
int main() {
// 测试数组
int original[] = {64, 34, 25, 12, 22, 11, 90};
int size = sizeof(original) / sizeof(original[0]);
int test_array[size];

// 随机数组测试
int random_size = 10;
int random_array[random_size];
generate_random_array(random_array, random_size, 1, 100);

printf("=== 排序算法示例 ===\n\n");

// =========================
// 冒泡排序
// =========================
printf("=== 冒泡排序 ===\n");
printf("时间复杂度: O(n^2)\n");
printf("空间复杂度: O(1)\n");

copy_array(test_array, original, size);
printf("排序前: ");
print_array(test_array, size);

bubble_sort(test_array, size);

printf("排序后: ");
print_array(test_array, size);

printf("\n");

// =========================
// 选择排序
// =========================
printf("=== 选择排序 ===\n");
printf("时间复杂度: O(n^2)\n");
printf("空间复杂度: O(1)\n");

copy_array(test_array, original, size);
printf("排序前: ");
print_array(test_array, size);

selection_sort(test_array, size);

printf("排序后: ");
print_array(test_array, size);

printf("\n");

// =========================
// 插入排序
// =========================
printf("=== 插入排序 ===\n");
printf("时间复杂度: O(n^2)\n");
printf("空间复杂度: O(1)\n");

copy_array(test_array, original, size);
printf("排序前: ");
print_array(test_array, size);

insertion_sort(test_array, size);

printf("排序后: ");
print_array(test_array, size);

printf("\n");

// =========================
// 快速排序
// =========================
printf("=== 快速排序 ===\n");
printf("时间复杂度: O(nlogn) (平均), O(n^2) (最坏)\n");
printf("空间复杂度: O(logn)\n");

copy_array(test_array, original, size);
printf("排序前: ");
print_array(test_array, size);

quick_sort(test_array, 0, size - 1);

printf("排序后: ");
print_array(test_array, size);

printf("\n");

// =========================
// 归并排序
// =========================
printf("=== 归并排序 ===\n");
printf("时间复杂度: O(nlogn)\n");
printf("空间复杂度: O(n)\n");

copy_array(test_array, original, size);
printf("排序前: ");
print_array(test_array, size);

merge_sort(test_array, 0, size - 1);

printf("排序后: ");
print_array(test_array, size);

printf("\n");

// =========================
// 随机数组测试
// =========================
printf("=== 随机数组测试 ===\n");
printf("生成的随机数组: ");
print_array(random_array, random_size);

// 使用快速排序对随机数组进行排序
int random_copy[random_size];
copy_array(random_copy, random_array, random_size);
quick_sort(random_copy, 0, random_size - 1);

printf("快速排序后: ");
print_array(random_copy, random_size);

return 0;
}

/**
* @brief 打印数组
* @param arr 要打印的数组
* @param size 数组的大小
*/
void print_array(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}

/**
* @brief 冒泡排序
* @param arr 要排序的数组
* @param size 数组的大小
* @details 冒泡排序通过重复地遍历要排序的数列,一次比较两个元素,
* 如果它们的顺序错误就把它们交换过来。
*/
void bubble_sort(int arr[], int size) {
for (int i = 0; i < size - 1; i++) {
// 标记本轮是否有交换,优化冒泡排序
int swapped = 0;

for (int j = 0; j < size - i - 1; j++) {
if (arr[j] > arr[j + 1]) {
// 交换arr[j]和arr[j+1]
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swapped = 1;
}
}

// 如果本轮没有交换,说明数组已经有序,提前退出
if (swapped == 0) {
break;
}
}
}

/**
* @brief 选择排序
* @param arr 要排序的数组
* @param size 数组的大小
* @details 选择排序每次从待排序数组中找到最小(或最大)的元素,
* 放到已排序数组的末尾。
*/
void selection_sort(int arr[], int size) {
for (int i = 0; i < size - 1; i++) {
// 找到未排序部分的最小值索引
int min_idx = i;
for (int j = i + 1; j < size; j++) {
if (arr[j] < arr[min_idx]) {
min_idx = j;
}
}

// 交换找到的最小值和未排序部分的第一个元素
if (min_idx != i) {
int temp = arr[i];
arr[i] = arr[min_idx];
arr[min_idx] = temp;
}
}
}

/**
* @brief 插入排序
* @param arr 要排序的数组
* @param size 数组的大小
* @details 插入排序将数组分为已排序和未排序两部分,
* 每次从未排序部分取一个元素插入到已排序部分的正确位置。
*/
void insertion_sort(int arr[], int size) {
for (int i = 1; i < size; i++) {
int key = arr[i];
int j = i - 1;

// 将大于key的元素向后移动
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}

// 插入key到正确位置
arr[j + 1] = key;
}
}

/**
* @brief 快速排序
* @param arr 要排序的数组
* @param low 起始索引
* @param high 结束索引
* @details 快速排序通过选择一个基准元素,将数组分为小于基准和大于基准两部分,
* 然后递归地对这两部分进行排序。
*/
void quick_sort(int arr[], int low, int high) {
if (low < high) {
// 分区操作,获取基准元素的最终位置
int pivot = arr[high]; // 选择最后一个元素作为基准
int i = (low - 1); // 小于基准的元素的索引

for (int j = low; j < high; j++) {
if (arr[j] < pivot) {
i++;
// 交换arr[i]和arr[j]
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}

// 交换基准元素到正确位置
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;

int pi = i + 1; // 基准元素的最终位置

// 递归排序基准元素左右两部分
quick_sort(arr, low, pi - 1);
quick_sort(arr, pi + 1, high);
}
}

/**
* @brief 归并排序
* @param arr 要排序的数组
* @param left 左边界
* @param right 右边界
* @details 归并排序采用分治法,将数组分成两半,分别排序,然后合并。
*/
void merge_sort(int arr[], int left, int right) {
if (left < right) {
// 计算中间位置
int mid = left + (right - left) / 2;

// 递归排序左半部分和右半部分
merge_sort(arr, left, mid);
merge_sort(arr, mid + 1, right);

// 合并已排序的两部分
merge(arr, left, mid, right);
}
}

/**
* @brief 归并排序的合并函数
* @param arr 要合并的数组
* @param left 左边界
* @param mid 中间位置
* @param right 右边界
* @details 将两个已排序的子数组合并成一个有序数组。
*/
void merge(int arr[], int left, int mid, int right) {
int i, j, k;
int n1 = mid - left + 1;
int n2 = right - mid;

// 创建临时数组
int L[n1], R[n2];

// 复制数据到临时数组
for (i = 0; i < n1; i++) {
L[i] = arr[left + i];
}
for (j = 0; j < n2; j++) {
R[j] = arr[mid + 1 + j];
}

// 合并临时数组
i = 0; // 左子数组的起始索引
j = 0; // 右子数组的起始索引
k = left; // 合并数组的起始索引

while (i < n1 && j < n2) {
if (L[i] <= R[j]) {
arr[k] = L[i];
i++;
} else {
arr[k] = R[j];
j++;
}
k++;
}

// 复制左子数组的剩余元素
while (i < n1) {
arr[k] = L[i];
i++;
k++;
}

// 复制右子数组的剩余元素
while (j < n2) {
arr[k] = R[j];
j++;
k++;
}
}

/**
* @brief 生成随机数组
* @param arr 要生成的数组
* @param size 数组的大小
* @param min 随机数的最小值
* @param max 随机数的最大值
*/
void generate_random_array(int arr[], int size, int min, int max) {
// 初始化随机种子
srand(time(NULL));

for (int i = 0; i < size; i++) {
// 生成[min, max]范围内的随机数
arr[i] = min + rand() % (max - min + 1);
}
}

/**
* @brief 复制数组
* @param dest 目标数组
* @param src 源数组
* @param size 数组的大小
*/
void copy_array(int dest[], int src[], int size) {
for (int i = 0; i < size; i++) {
dest[i] = src[i];
}
}

C语言动态内存分配

概述

本文档详细介绍C语言中动态内存分配的使用方法,包括malloccallocreallocfree函数的使用,以及动态数组、动态结构体数组的创建和使用,内存泄漏的预防方法等内容。动态内存分配是C语言中重要的功能之一,它允许程序在运行时根据需要分配和释放内存,提高了内存的使用效率。

动态内存分配的基本概念

静态内存与动态内存的区别

特性 静态内存 动态内存
分配时机 编译时 运行时
分配位置 栈区 堆区
大小限制 较小,受栈空间限制 较大,受系统内存限制
生命周期 作用域内 手动分配和释放
灵活性 固定大小,不可调整 可根据需要调整大小

动态内存分配函数

C语言提供了以下动态内存分配函数:

函数 功能 原型 示例
malloc 分配指定大小的内存 void *malloc(size_t size) int *ptr = (int *)malloc(5 * sizeof(int))
calloc 分配指定数量和大小的内存并初始化为0 void *calloc(size_t nmemb, size_t size) int *ptr = (int *)calloc(5, sizeof(int))
realloc 重新分配内存大小 void *realloc(void *ptr, size_t size) ptr = (int *)realloc(ptr, 10 * sizeof(int))
free 释放已分配的内存 void free(void *ptr) free(ptr)

malloc函数示例

函数说明

malloc函数用于分配指定大小的内存空间,返回一个指向分配内存的指针。如果内存分配失败,返回NULL

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// malloc函数示例
void malloc_example() {
int *ptr;
int n = 5;

// 分配5个int类型的内存空间
ptr = (int *)malloc(n * sizeof(int));

if (ptr == NULL) {
printf("内存分配失败!\n");
return;
}

// 初始化并打印分配的内存
printf("使用malloc分配的内存:\n");
for (int i = 0; i < n; i++) {
ptr[i] = i * 10;
printf("ptr[%d] = %d\n", i, ptr[i]);
}

// 释放内存
free(ptr);
printf("内存已释放!\n");
}

补充示例:分配不同类型的内存

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
// 分配不同类型的内存
void malloc_different_types() {
// 分配整型内存
int *int_ptr = (int *)malloc(5 * sizeof(int));
if (int_ptr != NULL) {
for (int i = 0; i < 5; i++) {
int_ptr[i] = i;
}
printf("分配的整型内存:");
for (int i = 0; i < 5; i++) {
printf("%d ", int_ptr[i]);
}
printf("\n");
free(int_ptr);
}

// 分配浮点型内存
float *float_ptr = (float *)malloc(3 * sizeof(float));
if (float_ptr != NULL) {
float_ptr[0] = 1.1;
float_ptr[1] = 2.2;
float_ptr[2] = 3.3;
printf("分配的浮点型内存:");
for (int i = 0; i < 3; i++) {
printf("%.1f ", float_ptr[i]);
}
printf("\n");
free(float_ptr);
}

// 分配字符型内存
char *char_ptr = (char *)malloc(10 * sizeof(char));
if (char_ptr != NULL) {
strcpy(char_ptr, "Hello");
printf("分配的字符型内存:%s\n", char_ptr);
free(char_ptr);
}
}

calloc函数示例

函数说明

calloc函数用于分配指定数量和大小的内存空间,并将所有字节初始化为0。返回一个指向分配内存的指针。如果内存分配失败,返回NULL

示例代码

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
// calloc函数示例
void calloc_example() {
int *ptr;
int n = 5;

// 分配5个int类型的内存空间,并初始化为0
ptr = (int *)calloc(n, sizeof(int));

if (ptr == NULL) {
printf("内存分配失败!\n");
return;
}

// 打印分配的内存(calloc会初始化为0)
printf("使用calloc分配的内存(初始化为0):\n");
for (int i = 0; i < n; i++) {
printf("ptr[%d] = %d\n", i, ptr[i]);
}

// 初始化并重新打印
for (int i = 0; i < n; i++) {
ptr[i] = i * 20;
}

printf("\n初始化后:\n");
for (int i = 0; i < n; i++) {
printf("ptr[%d] = %d\n", i, ptr[i]);
}

// 释放内存
free(ptr);
printf("内存已释放!\n");
}

补充示例:使用calloc创建二维数组

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
44
45
46
47
48
49
// 使用calloc创建二维数组
void create_2d_array() {
int rows = 3, cols = 4;
int **array;

// 分配行指针
array = (int **)calloc(rows, sizeof(int *));
if (array == NULL) {
printf("内存分配失败!\n");
return;
}

// 分配每行的内存
for (int i = 0; i < rows; i++) {
array[i] = (int *)calloc(cols, sizeof(int));
if (array[i] == NULL) {
// 内存分配失败,释放已分配的内存
for (int j = 0; j < i; j++) {
free(array[j]);
}
free(array);
printf("内存分配失败!\n");
return;
}
}

// 初始化二维数组
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
array[i][j] = i * cols + j;
}
}

// 打印二维数组
printf("二维数组内容:\n");
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", array[i][j]);
}
printf("\n");
}

// 释放内存
for (int i = 0; i < rows; i++) {
free(array[i]);
}
free(array);
printf("内存已释放!\n");
}

realloc函数示例

函数说明

realloc函数用于重新分配已分配内存的大小,可以扩大或缩小内存块。如果内存分配失败,返回NULL,但原内存块仍然有效。

示例代码

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
44
45
46
// realloc函数示例
void realloc_example() {
int *ptr;
int n = 5;

// 首先使用malloc分配内存
ptr = (int *)malloc(n * sizeof(int));

if (ptr == NULL) {
printf("内存分配失败!\n");
return;
}

// 初始化内存
for (int i = 0; i < n; i++) {
ptr[i] = i * 5;
}

printf("初始分配5个int的内存:\n");
for (int i = 0; i < n; i++) {
printf("ptr[%d] = %d\n", i, ptr[i]);
}

// 重新分配内存为10个int
n = 10;
ptr = (int *)realloc(ptr, n * sizeof(int));

if (ptr == NULL) {
printf("内存重新分配失败!\n");
return;
}

// 初始化新分配的内存
for (int i = 5; i < n; i++) {
ptr[i] = i * 5;
}

printf("\n重新分配10个int的内存:\n");
for (int i = 0; i < n; i++) {
printf("ptr[%d] = %d\n", i, ptr[i]);
}

// 释放内存
free(ptr);
printf("内存已释放!\n");
}

补充示例:使用realloc缩小内存

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
44
// 使用realloc缩小内存
void shrink_memory() {
int *ptr;
int n = 10;

// 首先分配10个int的内存
ptr = (int *)malloc(n * sizeof(int));

if (ptr == NULL) {
printf("内存分配失败!\n");
return;
}

// 初始化内存
for (int i = 0; i < n; i++) {
ptr[i] = i * 10;
}

printf("初始分配10个int的内存:\n");
for (int i = 0; i < n; i++) {
printf("%d ", ptr[i]);
}
printf("\n");

// 缩小内存为5个int
n = 5;
ptr = (int *)realloc(ptr, n * sizeof(int));

if (ptr == NULL) {
printf("内存重新分配失败!\n");
free(ptr);
return;
}

printf("缩小为5个int的内存:\n");
for (int i = 0; i < n; i++) {
printf("%d ", ptr[i]);
}
printf("\n");

// 释放内存
free(ptr);
printf("内存已释放!\n");
}

动态数组示例

函数说明

动态数组是使用动态内存分配创建的数组,其大小可以在运行时根据需要调整。

示例代码

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
// 动态数组示例
void dynamic_array() {
int n;
float *scores;
float sum = 0.0;

// 从用户输入获取数组大小
printf("请输入学生数量:");
scanf("%d", &n);

// 分配内存
scores = (float *)malloc(n * sizeof(float));

if (scores == NULL) {
printf("内存分配失败!\n");
return;
}

// 输入分数
for (int i = 0; i < n; i++) {
printf("请输入学生 %d 的分数:", i + 1);
scanf("%f", &scores[i]);
sum += scores[i];
}

// 计算并打印平均分
float average = sum / n;
printf("\n学生平均分数:%.2f\n", average);

// 打印所有分数
printf("所有学生分数:\n");
for (int i = 0; i < n; i++) {
printf("%.2f ", scores[i]);
}
printf("\n");

// 释放内存
free(scores);
printf("内存已释放!\n");
}

补充示例:动态数组排序

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
44
45
46
47
48
49
50
51
52
53
// 动态数组排序
void sort_dynamic_array() {
int n;
int *array;

// 从用户输入获取数组大小
printf("请输入数组大小:");
scanf("%d", &n);

// 分配内存
array = (int *)malloc(n * sizeof(int));

if (array == NULL) {
printf("内存分配失败!\n");
return;
}

// 输入数组元素
for (int i = 0; i < n; i++) {
printf("请输入元素 %d:", i + 1);
scanf("%d", &array[i]);
}

// 打印原始数组
printf("\n原始数组:\n");
for (int i = 0; i < n; i++) {
printf("%d ", array[i]);
}
printf("\n");

// 使用冒泡排序对数组排序
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (array[j] > array[j + 1]) {
// 交换元素
int temp = array[j];
array[j] = array[j + 1];
array[j + 1] = temp;
}
}
}

// 打印排序后的数组
printf("排序后的数组:\n");
for (int i = 0; i < n; i++) {
printf("%d ", array[i]);
}
printf("\n");

// 释放内存
free(array);
printf("内存已释放!\n");
}

动态结构体数组示例

函数说明

动态结构体数组是使用动态内存分配创建的结构体数组,其大小可以在运行时根据需要调整。

示例代码

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 学生结构体
struct Student {
char name[50];
int age;
float score;
};

// 动态结构体数组示例
void dynamic_struct_array() {
int n;
struct Student *students;

// 从用户输入获取数组大小
printf("请输入学生数量:");
scanf("%d", &n);

// 分配内存
students = (struct Student *)malloc(n * sizeof(struct Student));

if (students == NULL) {
printf("内存分配失败!\n");
return;
}

// 清空输入缓冲区
while (getchar() != '\n');

// 输入学生信息
for (int i = 0; i < n; i++) {
printf("\n请输入学生 %d 的信息:\n", i + 1);
printf("姓名:");
fgets(students[i].name, sizeof(students[i].name), stdin);
// 移除fgets添加的换行符
students[i].name[strcspn(students[i].name, "\n")] = '\0';

printf("年龄:");
scanf("%d", &students[i].age);

printf("分数:");
scanf("%f", &students[i].score);

// 清空输入缓冲区
while (getchar() != '\n');
}

// 打印学生信息
printf("\n=== 学生信息列表 ===\n");
for (int i = 0; i < n; i++) {
printf("\n学生 %d:\n", i + 1);
printf("姓名:%s\n", students[i].name);
printf("年龄:%d\n", students[i].age);
printf("分数:%.2f\n", students[i].score);
}

// 释放内存
free(students);
printf("\n内存已释放!\n");
}

补充示例:动态结构体数组的排序

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
// 动态结构体数组的排序
void sort_students() {
int n;
struct Student *students;

// 从用户输入获取数组大小
printf("请输入学生数量:");
scanf("%d", &n);

// 分配内存
students = (struct Student *)malloc(n * sizeof(struct Student));

if (students == NULL) {
printf("内存分配失败!\n");
return;
}

// 清空输入缓冲区
while (getchar() != '\n');

// 输入学生信息
for (int i = 0; i < n; i++) {
printf("\n请输入学生 %d 的信息:\n", i + 1);
printf("姓名:");
fgets(students[i].name, sizeof(students[i].name), stdin);
// 移除fgets添加的换行符
students[i].name[strcspn(students[i].name, "\n")] = '\0';

printf("年龄:");
scanf("%d", &students[i].age);

printf("分数:");
scanf("%f", &students[i].score);

// 清空输入缓冲区
while (getchar() != '\n');
}

// 按分数排序(降序)
for (int i = 0; i < n - 1; i++) {
for (int j = 0; j < n - i - 1; j++) {
if (students[j].score < students[j + 1].score) {
// 交换结构体
struct Student temp = students[j];
students[j] = students[j + 1];
students[j + 1] = temp;
}
}
}

// 打印排序后的学生信息
printf("\n=== 按分数排序后的学生信息 ===\n");
for (int i = 0; i < n; i++) {
printf("\n学生 %d:\n", i + 1);
printf("姓名:%s\n", students[i].name);
printf("年龄:%d\n", students[i].age);
printf("分数:%.2f\n", students[i].score);
}

// 释放内存
free(students);
printf("\n内存已释放!\n");
}

内存泄漏预防示例

函数说明

内存泄漏是指程序在运行过程中分配了内存,但在不再需要时没有释放,导致内存资源浪费。内存泄漏会随着程序的运行时间增长而累积,最终可能导致程序崩溃。

示例代码

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
// 内存泄漏预防示例
void memory_leak_prevention() {
printf("演示内存泄漏预防方法:\n");

// 正确的内存分配和释放方式
printf("\n1. 总是检查内存分配是否成功\n");
printf("2. 每个malloc/calloc/realloc对应一个free\n");
printf("3. 避免在函数中分配内存而不释放\n");
printf("4. 使用free后将指针设为NULL,避免野指针\n");

// 示例:正确的内存使用方式
int *ptr = (int *)malloc(5 * sizeof(int));

if (ptr != NULL) {
// 使用内存
for (int i = 0; i < 5; i++) {
ptr[i] = i;
}

// 释放内存后将指针设为NULL
free(ptr);
ptr = NULL;
printf("\n内存已正确释放,指针已设为NULL!\n");
}

// 错误示例:内存泄漏(注释掉了free)
// int *leaky_ptr = (int *)malloc(5 * sizeof(int));
// leaky_ptr[0] = 100;
// // 忘记调用free(leaky_ptr); // 导致内存泄漏

printf("\n注意:注释掉的代码演示了内存泄漏的情况!\n");
}

补充示例:内存泄漏的常见场景

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
44
45
46
47
48
49
50
51
52
53
54
55
56
// 内存泄漏的常见场景
void memory_leak_scenarios() {
printf("内存泄漏的常见场景:\n");

// 场景1:函数中分配内存但不释放
printf("\n1. 函数中分配内存但不释放:\n");
// void leaky_function() {
// int *ptr = (int *)malloc(10 * sizeof(int));
// // 没有free(ptr);
// }

// 场景2:条件分支中忘记释放内存
printf("\n2. 条件分支中忘记释放内存:\n");
// void conditional_leak() {
// int *ptr = (int *)malloc(10 * sizeof(int));
// if (some_condition) {
// return; // 忘记释放ptr
// }
// free(ptr);
// }

// 场景3:使用realloc时的内存泄漏
printf("\n3. 使用realloc时的内存泄漏:\n");
// void realloc_leak() {
// int *ptr = (int *)malloc(10 * sizeof(int));
// // 错误:如果realloc失败,ptr将为NULL,原内存丢失
// ptr = (int *)realloc(ptr, 20 * sizeof(int));
// free(ptr);
// }

// 正确的realloc使用方式
printf("\n正确的realloc使用方式:\n");
int *ptr = (int *)malloc(10 * sizeof(int));
if (ptr != NULL) {
int *temp = (int *)realloc(ptr, 20 * sizeof(int));
if (temp != NULL) {
ptr = temp;
// 使用ptr
} else {
// 处理realloc失败的情况
printf("realloc失败,但原内存仍然有效\n");
}
free(ptr);
printf("内存已正确释放\n");
}

// 场景4:循环中重复分配内存
printf("\n4. 循环中重复分配内存:\n");
// void loop_leak() {
// char *buffer;
// for (int i = 0; i < 10; i++) {
// buffer = (char *)malloc(100 * sizeof(char));
// // 没有free(buffer);
// }
// }
}

安全内存分配示例

函数说明

安全内存分配是指在分配内存时采取一系列措施,确保内存分配的安全性和可靠性,避免内存泄漏和其他内存相关问题。

示例代码

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 安全内存分配示例
void safe_memory_allocation() {
printf("演示安全内存分配:\n");

// 使用临时指针来避免内存泄漏
int *temp_ptr = (int *)malloc(10 * sizeof(int));
int *ptr = NULL;

if (temp_ptr != NULL) {
ptr = temp_ptr;

// 使用内存
for (int i = 0; i < 10; i++) {
ptr[i] = i * 2;
}

// 打印内容
printf("\n安全分配的内存内容:\n");
for (int i = 0; i < 10; i++) {
printf("%d ", ptr[i]);
}
printf("\n");
} else {
printf("\n内存分配失败!\n");
}

// 释放内存
if (ptr != NULL) {
free(ptr);
ptr = NULL;
printf("\n内存已安全释放!\n");
}

// 演示动态字符串分配
printf("\n演示动态字符串分配:\n");

char *str = (char *)malloc(20 * sizeof(char));

if (str != NULL) {
strcpy(str, "Hello, Dynamic!");
printf("动态字符串:%s\n", str);

// 重新分配更大的内存
char *temp_str = (char *)realloc(str, 30 * sizeof(char));

if (temp_str != NULL) {
str = temp_str;
strcat(str, " This is reallocated!");
printf("重新分配后的字符串:%s\n", str);
}

// 释放内存
free(str);
str = NULL;
printf("字符串内存已释放!\n");
}
}

补充示例:安全的内存分配封装函数

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
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
69
70
71
72
// 安全的内存分配封装函数
void *safe_malloc(size_t size) {
void *ptr = malloc(size);
if (ptr == NULL) {
fprintf(stderr, "内存分配失败:无法分配 %zu 字节\n", size);
exit(EXIT_FAILURE);
}
return ptr;
}

// 安全的calloc封装函数
void *safe_calloc(size_t nmemb, size_t size) {
void *ptr = calloc(nmemb, size);
if (ptr == NULL) {
fprintf(stderr, "内存分配失败:无法分配 %zu 个 %zu 字节的元素\n", nmemb, size);
exit(EXIT_FAILURE);
}
return ptr;
}

// 安全的realloc封装函数
void *safe_realloc(void *ptr, size_t size) {
void *new_ptr = realloc(ptr, size);
if (new_ptr == NULL) {
fprintf(stderr, "内存重新分配失败:无法分配 %zu 字节\n", size);
// 注意:原ptr仍然有效
exit(EXIT_FAILURE);
}
return new_ptr;
}

// 使用安全内存分配函数
void use_safe_alloc() {
printf("使用安全内存分配函数:\n");

// 使用safe_malloc
int *ptr = (int *)safe_malloc(5 * sizeof(int));
for (int i = 0; i < 5; i++) {
ptr[i] = i * 10;
}

printf("使用safe_malloc分配的内存:\n");
for (int i = 0; i < 5; i++) {
printf("%d ", ptr[i]);
}
printf("\n");

// 使用safe_realloc
ptr = (int *)safe_realloc(ptr, 10 * sizeof(int));
for (int i = 5; i < 10; i++) {
ptr[i] = i * 10;
}

printf("使用safe_realloc重新分配的内存:\n");
for (int i = 0; i < 10; i++) {
printf("%d ", ptr[i]);
}
printf("\n");

// 使用safe_calloc
float *float_ptr = (float *)safe_calloc(3, sizeof(float));
printf("使用safe_calloc分配的内存(初始化为0):\n");
for (int i = 0; i < 3; i++) {
printf("%.1f ", float_ptr[i]);
}
printf("\n");

// 释放内存
free(ptr);
free(float_ptr);
printf("内存已释放!\n");
}

动态内存分配的高级应用

动态内存分配在链表中的应用

链表是一种常见的数据结构,它使用动态内存分配来存储和管理数据。

示例代码

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
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
69
70
71
72
73
74
75
// 链表节点结构体
struct Node {
int data;
struct Node *next;
};

// 创建新节点
struct Node *create_node(int data) {
struct Node *new_node = (struct Node *)malloc(sizeof(struct Node));
if (new_node == NULL) {
printf("内存分配失败!\n");
return NULL;
}
new_node->data = data;
new_node->next = NULL;
return new_node;
}

// 向链表末尾添加节点
void append_node(struct Node **head, int data) {
struct Node *new_node = create_node(data);
if (new_node == NULL) {
return;
}

if (*head == NULL) {
*head = new_node;
return;
}

struct Node *temp = *head;
while (temp->next != NULL) {
temp = temp->next;
}
temp->next = new_node;
}

// 打印链表
void print_list(struct Node *head) {
struct Node *temp = head;
while (temp != NULL) {
printf("%d -> ", temp->data);
temp = temp->next;
}
printf("NULL\n");
}

// 释放链表内存
void free_list(struct Node *head) {
struct Node *temp;
while (head != NULL) {
temp = head;
head = head->next;
free(temp);
}
printf("链表内存已释放!\n");
}

// 链表示例
void linked_list_example() {
struct Node *head = NULL;

// 添加节点
append_node(&head, 10);
append_node(&head, 20);
append_node(&head, 30);
append_node(&head, 40);

// 打印链表
printf("链表内容:\n");
print_list(head);

// 释放链表内存
free_list(head);
}

补充示例:动态字符串处理

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// 动态字符串处理
void dynamic_string_example() {
// 动态分配字符串
char *str = (char *)malloc(20 * sizeof(char));
if (str != NULL) {
strcpy(str, "Hello");
printf("初始字符串:%s\n", str);
printf("字符串长度:%zu\n", strlen(str));
printf("分配的内存大小:%zu 字节\n", 20 * sizeof(char));

// 扩展字符串
char *temp = (char *)realloc(str, 50 * sizeof(char));
if (temp != NULL) {
str = temp;
strcat(str, " World! This is a longer string.");
printf("扩展后的字符串:%s\n", str);
printf("字符串长度:%zu\n", strlen(str));
printf("分配的内存大小:%zu 字节\n", 50 * sizeof(char));
}

// 释放内存
free(str);
printf("字符串内存已释放!\n");
}

// 示例:读取变长输入
printf("\n读取变长输入:\n");
printf("请输入一段文本:\n");

// 初始分配
size_t buffer_size = 20;
char *input = (char *)malloc(buffer_size * sizeof(char));
if (input != NULL) {
size_t length = 0;
int c;

// 读取输入,直到遇到换行符
while ((c = getchar()) != '\n' && c != EOF) {
if (length >= buffer_size - 1) {
// 扩展缓冲区
buffer_size *= 2;
char *temp = (char *)realloc(input, buffer_size * sizeof(char));
if (temp == NULL) {
printf("内存分配失败!\n");
free(input);
return;
}
input = temp;
}
input[length++] = c;
}
input[length] = '\0';

printf("你输入的文本:%s\n", input);
printf("文本长度:%zu\n", length);
printf("分配的内存大小:%zu 字节\n", buffer_size * sizeof(char));

// 释放内存
free(input);
printf("输入缓冲区已释放!\n");
}
}

内存分配的最佳实践

  1. 始终检查内存分配是否成功:使用malloccallocrealloc函数后,一定要检查返回值是否为NULL
  2. 每个分配对应一个释放:确保每个malloccallocrealloc调用都有对应的free调用
  3. 使用free后将指针设为NULL:避免野指针,防止重复释放内存
  4. 使用临时指针进行realloc:避免realloc失败时丢失原内存
  5. 合理使用内存:不要分配过多的内存,避免内存浪费
  6. 释放所有分配的内存:特别是在函数返回前,确保释放所有分配的内存
  7. 注意内存对齐:某些系统对内存对齐有要求,使用malloc等函数会自动处理
  8. 使用封装函数:创建安全的内存分配封装函数,简化错误处理
  9. 监控内存使用:对于大型程序,考虑使用内存监控工具
  10. 避免内存碎片:频繁分配和释放小块内存可能导致内存碎片

综合示例

示例:动态内存分配的学生管理系统

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
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
// 学生管理系统
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 学生结构体
typedef struct {
char name[50];
int id;
float score;
} Student;

// 学生管理系统结构体
typedef struct {
Student *students;
int count;
int capacity;
} StudentManager;

// 初始化学生管理系统
StudentManager *init_manager(int initial_capacity) {
StudentManager *manager = (StudentManager *)malloc(sizeof(StudentManager));
if (manager == NULL) {
return NULL;
}

manager->students = (Student *)malloc(initial_capacity * sizeof(Student));
if (manager->students == NULL) {
free(manager);
return NULL;
}

manager->count = 0;
manager->capacity = initial_capacity;

return manager;
}

// 添加学生
void add_student(StudentManager *manager, const char *name, int id, float score) {
// 检查是否需要扩展容量
if (manager->count >= manager->capacity) {
int new_capacity = manager->capacity * 2;
Student *new_students = (Student *)realloc(manager->students, new_capacity * sizeof(Student));
if (new_students == NULL) {
printf("内存分配失败,无法添加学生!\n");
return;
}
manager->students = new_students;
manager->capacity = new_capacity;
printf("容量扩展到:%d\n", new_capacity);
}

// 添加学生
strcpy(manager->students[manager->count].name, name);
manager->students[manager->count].id = id;
manager->students[manager->count].score = score;
manager->count++;

printf("学生添加成功!\n");
}

// 打印所有学生
void print_students(StudentManager *manager) {
if (manager->count == 0) {
printf("没有学生信息!\n");
return;
}

printf("\n=== 学生信息列表 ===\n");
for (int i = 0; i < manager->count; i++) {
printf("\n学生 %d:\n", i + 1);
printf("姓名:%s\n", manager->students[i].name);
printf("ID:%d\n", manager->students[i].id);
printf("分数:%.2f\n", manager->students[i].score);
}
}

// 查找学生
Student *find_student(StudentManager *manager, int id) {
for (int i = 0; i < manager->count; i++) {
if (manager->students[i].id == id) {
return &manager->students[i];
}
}
return NULL;
}

// 释放学生管理系统
void free_manager(StudentManager *manager) {
if (manager != NULL) {
if (manager->students != NULL) {
free(manager->students);
}
free(manager);
printf("学生管理系统内存已释放!\n");
}
}

// 测试学生管理系统
void test_student_manager() {
// 初始化管理系统,初始容量为2
StudentManager *manager = init_manager(2);

if (manager == NULL) {
printf("初始化学生管理系统失败!\n");
return;
}

// 添加学生
add_student(manager, "张三", 1001, 95.5);
add_student(manager, "李四", 1002, 88.0);
add_student(manager, "王五", 1003, 92.5); // 这里会触发容量扩展
add_student(manager, "赵六", 1004, 85.0);

// 打印学生信息
print_students(manager);

// 查找学生
int search_id = 1002;
Student *student = find_student(manager, search_id);
if (student != NULL) {
printf("\n找到学生 ID %d:\n", search_id);
printf("姓名:%s\n", student->name);
printf("分数:%.2f\n", student->score);
} else {
printf("\n未找到学生 ID %d\n", search_id);
}

// 释放管理系统
free_manager(manager);
}

总结

C语言中的动态内存分配是实现灵活内存管理的重要手段,通过本文档的学习,我们了解了:

  1. 动态内存分配的基本概念:静态内存与动态内存的区别,动态内存分配函数的使用
  2. malloc函数:分配指定大小的内存
  3. calloc函数:分配指定数量和大小的内存并初始化为0
  4. realloc函数:重新分配内存大小
  5. 动态数组:使用动态内存分配创建的数组
  6. 动态结构体数组:使用动态内存分配创建的结构体数组
  7. 内存泄漏的预防:如何避免内存泄漏
  8. 安全内存分配:如何安全地分配和使用内存
  9. 动态内存分配的高级应用:链表、动态字符串等
  10. 内存分配的最佳实践:检查内存分配是否成功、释放内存、避免野指针等

掌握动态内存分配对于编写高效、灵活的C程序至关重要,它允许程序在运行时根据需要分配和释放内存,提高了内存的使用效率。通过合理使用动态内存分配函数,我们可以编写出更加灵活、功能强大的C程序。

dynamic_memory.c

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
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/**
* @brief 动态内存分配示例程序
* @details 这个程序演示了C语言中动态内存分配的各种用法,包括
* malloc、calloc、realloc和free函数的使用,以及动态数组、
* 动态结构体数组和内存泄漏的避免方法。
* @return 0 表示程序成功执行
*/

// =========================
// 结构体定义
// =========================

/**
* @brief 学生结构体
*/
struct Student {
char name[50];
int age;
float score;
};

// =========================
// 函数原型声明
// =========================
void malloc_example(); // malloc函数示例
void calloc_example(); // calloc函数示例
void realloc_example(); // realloc函数示例
void dynamic_array(); // 动态数组示例
void dynamic_struct_array(); // 动态结构体数组示例
void memory_leak_prevention(); // 内存泄漏预防示例
void safe_memory_allocation(); // 安全内存分配示例

/**
* @brief 主函数
* @return 0 表示程序成功执行
*/
int main() {
// =========================
// malloc函数示例
// =========================
printf("=== malloc函数示例 ===\n");
malloc_example();

printf("\n");

// =========================
// calloc函数示例
// =========================
printf("=== calloc函数示例 ===\n");
calloc_example();

printf("\n");

// =========================
// realloc函数示例
// =========================
printf("=== realloc函数示例 ===\n");
realloc_example();

printf("\n");

// =========================
// 动态数组示例
// =========================
printf("=== 动态数组示例 ===\n");
dynamic_array();

printf("\n");

// =========================
// 动态结构体数组示例
// =========================
printf("=== 动态结构体数组示例 ===\n");
dynamic_struct_array();

printf("\n");

// =========================
// 内存泄漏预防示例
// =========================
printf("=== 内存泄漏预防示例 ===\n");
memory_leak_prevention();

printf("\n");

// =========================
// 安全内存分配示例
// =========================
printf("=== 安全内存分配示例 ===\n");
safe_memory_allocation();

return 0;
}

/**
* @brief malloc函数示例
* @details 演示使用malloc函数分配内存
*/
void malloc_example() {
int *ptr;
int n = 5;

// 分配5个int类型的内存空间
ptr = (int *)malloc(n * sizeof(int));

if (ptr == NULL) {
printf("内存分配失败!\n");
return;
}

// 初始化并打印分配的内存
printf("使用malloc分配的内存:\n");
for (int i = 0; i < n; i++) {
ptr[i] = i * 10;
printf("ptr[%d] = %d\n", i, ptr[i]);
}

// 释放内存
free(ptr);
printf("内存已释放!\n");
}

/**
* @brief calloc函数示例
* @details 演示使用calloc函数分配内存
*/
void calloc_example() {
int *ptr;
int n = 5;

// 分配5个int类型的内存空间,并初始化为0
ptr = (int *)calloc(n, sizeof(int));

if (ptr == NULL) {
printf("内存分配失败!\n");
return;
}

// 打印分配的内存(calloc会初始化为0)
printf("使用calloc分配的内存(初始化为0):\n");
for (int i = 0; i < n; i++) {
printf("ptr[%d] = %d\n", i, ptr[i]);
}

// 初始化并重新打印
for (int i = 0; i < n; i++) {
ptr[i] = i * 20;
}

printf("\n初始化后:\n");
for (int i = 0; i < n; i++) {
printf("ptr[%d] = %d\n", i, ptr[i]);
}

// 释放内存
free(ptr);
printf("内存已释放!\n");
}

/**
* @brief realloc函数示例
* @details 演示使用realloc函数重新分配内存
*/
void realloc_example() {
int *ptr;
int n = 5;

// 首先使用malloc分配内存
ptr = (int *)malloc(n * sizeof(int));

if (ptr == NULL) {
printf("内存分配失败!\n");
return;
}

// 初始化内存
for (int i = 0; i < n; i++) {
ptr[i] = i * 5;
}

printf("初始分配5个int的内存:\n");
for (int i = 0; i < n; i++) {
printf("ptr[%d] = %d\n", i, ptr[i]);
}

// 重新分配内存为10个int
n = 10;
ptr = (int *)realloc(ptr, n * sizeof(int));

if (ptr == NULL) {
printf("内存重新分配失败!\n");
return;
}

// 初始化新分配的内存
for (int i = 5; i < n; i++) {
ptr[i] = i * 5;
}

printf("\n重新分配10个int的内存:\n");
for (int i = 0; i < n; i++) {
printf("ptr[%d] = %d\n", i, ptr[i]);
}

// 释放内存
free(ptr);
printf("内存已释放!\n");
}

/**
* @brief 动态数组示例
* @details 演示如何创建和使用动态数组
*/
void dynamic_array() {
int n;
float *scores;
float sum = 0.0;

// 从用户输入获取数组大小
printf("请输入学生数量:");
scanf("%d", &n);

// 分配内存
scores = (float *)malloc(n * sizeof(float));

if (scores == NULL) {
printf("内存分配失败!\n");
return;
}

// 输入分数
for (int i = 0; i < n; i++) {
printf("请输入学生 %d 的分数:", i + 1);
scanf("%f", &scores[i]);
sum += scores[i];
}

// 计算并打印平均分
float average = sum / n;
printf("\n学生平均分数:%.2f\n", average);

// 打印所有分数
printf("所有学生分数:\n");
for (int i = 0; i < n; i++) {
printf("%.2f ", scores[i]);
}
printf("\n");

// 释放内存
free(scores);
printf("内存已释放!\n");
}

/**
* @brief 动态结构体数组示例
* @details 演示如何创建和使用动态结构体数组
*/
void dynamic_struct_array() {
int n;
struct Student *students;

// 从用户输入获取数组大小
printf("请输入学生数量:");
scanf("%d", &n);

// 分配内存
students = (struct Student *)malloc(n * sizeof(struct Student));

if (students == NULL) {
printf("内存分配失败!\n");
return;
}

// 清空输入缓冲区
while (getchar() != '\n');

// 输入学生信息
for (int i = 0; i < n; i++) {
printf("\n请输入学生 %d 的信息:\n", i + 1);
printf("姓名:");
fgets(students[i].name, sizeof(students[i].name), stdin);
// 移除fgets添加的换行符
students[i].name[strcspn(students[i].name, "\n")] = '\0';

printf("年龄:");
scanf("%d", &students[i].age);

printf("分数:");
scanf("%f", &students[i].score);

// 清空输入缓冲区
while (getchar() != '\n');
}

// 打印学生信息
printf("\n=== 学生信息列表 ===\n");
for (int i = 0; i < n; i++) {
printf("\n学生 %d:\n", i + 1);
printf("姓名:%s\n", students[i].name);
printf("年龄:%d\n", students[i].age);
printf("分数:%.2f\n", students[i].score);
}

// 释放内存
free(students);
printf("\n内存已释放!\n");
}

/**
* @brief 内存泄漏预防示例
* @details 演示如何预防内存泄漏
*/
void memory_leak_prevention() {
printf("演示内存泄漏预防方法:\n");

// 正确的内存分配和释放方式
printf("\n1. 总是检查内存分配是否成功\n");
printf("2. 每个malloc/calloc/realloc对应一个free\n");
printf("3. 避免在函数中分配内存而不释放\n");
printf("4. 使用free后将指针设为NULL,避免野指针\n");

// 示例:正确的内存使用方式
int *ptr = (int *)malloc(5 * sizeof(int));

if (ptr != NULL) {
// 使用内存
for (int i = 0; i < 5; i++) {
ptr[i] = i;
}

// 释放内存后将指针设为NULL
free(ptr);
ptr = NULL;
printf("\n内存已正确释放,指针已设为NULL!\n");
}

// 错误示例:内存泄漏(注释掉了free)
// int *leaky_ptr = (int *)malloc(5 * sizeof(int));
// leaky_ptr[0] = 100;
// // 忘记调用free(leaky_ptr); // 导致内存泄漏

printf("\n注意:注释掉的代码演示了内存泄漏的情况!\n");
}

/**
* @brief 安全内存分配示例
* @details 演示如何安全地分配和使用内存
*/
void safe_memory_allocation() {
printf("演示安全内存分配:\n");

// 使用临时指针来避免内存泄漏
int *temp_ptr = (int *)malloc(10 * sizeof(int));
int *ptr = NULL;

if (temp_ptr != NULL) {
ptr = temp_ptr;

// 使用内存
for (int i = 0; i < 10; i++) {
ptr[i] = i * 2;
}

// 打印内容
printf("\n安全分配的内存内容:\n");
for (int i = 0; i < 10; i++) {
printf("%d ", ptr[i]);
}
printf("\n");
} else {
printf("\n内存分配失败!\n");
}

// 释放内存
if (ptr != NULL) {
free(ptr);
ptr = NULL;
printf("\n内存已安全释放!\n");
}

// 演示动态字符串分配
printf("\n演示动态字符串分配:\n");

char *str = (char *)malloc(20 * sizeof(char));

if (str != NULL) {
strcpy(str, "Hello, Dynamic!");
printf("动态字符串:%s\n", str);

// 重新分配更大的内存
char *temp_str = (char *)realloc(str, 30 * sizeof(char));

if (temp_str != NULL) {
str = temp_str;
strcat(str, " This is reallocated!");
printf("重新分配后的字符串:%s\n", str);
}

// 释放内存
free(str);
str = NULL;
printf("字符串内存已释放!\n");
}
}

C语言文件操作

概述

本文档详细介绍C语言中文件操作的使用方法,包括文件的打开和关闭、文本文件的读写、二进制文件的读写、文件定位以及错误处理等内容。文件操作是C语言中重要的功能之一,它允许程序与外部存储设备进行数据交换,实现数据的持久化存储。

文件操作的基本概念

文件指针

在C语言中,文件操作通过文件指针来进行,文件指针是一个指向FILE类型的指针变量。FILE是C语言标准库中定义的一个结构体,用于存储文件相关的信息。

文件打开模式

在打开文件时,需要指定文件的打开模式,常用的文件打开模式如下:

模式 说明
"r" 只读模式,打开已存在的文本文件
"w" 只写模式,创建新的文本文件或截断已存在的文本文件
"a" 追加模式,向文本文件末尾添加数据
"rb" 只读模式,打开已存在的二进制文件
"wb" 只写模式,创建新的二进制文件或截断已存在的二进制文件
"ab" 追加模式,向二进制文件末尾添加数据
"r+" 读写模式,打开已存在的文本文件
"w+" 读写模式,创建新的文本文件或截断已存在的文本文件
"a+" 读写模式,打开已存在的文本文件并在末尾追加数据
"rb+" 读写模式,打开已存在的二进制文件
"wb+" 读写模式,创建新的二进制文件或截断已存在的二进制文件
"ab+" 读写模式,打开已存在的二进制文件并在末尾追加数据

文件的打开与关闭

使用fopen函数打开文件,使用fclose函数关闭文件:

1
2
FILE *fopen(const char *filename, const char *mode);
int fclose(FILE *stream);

创建和写入文本文件

文本文件写入函数

C语言提供了多种写入文本文件的函数:

函数 功能 示例
fprintf 格式化写入 fprintf(file, "姓名:%s\n", name)
fputs 写入字符串 fputs("Hello\n", file)
fputc 写入单个字符 fputc('A', file)
fwrite 二进制写入(也可用于文本文件) fwrite(buffer, sizeof(char), length, file)

示例代码

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
// 创建和写入文本文件
void create_text_file() {
FILE *file;

// 以写入模式打开文件(如果文件不存在则创建,存在则截断)
file = fopen("test.txt", "w");

if (file == NULL) {
printf("无法打开文件!\n");
return;
}

// 使用fprintf写入格式化数据
fprintf(file, "姓名:张三\n");
fprintf(file, "年龄:%d\n", 25);
fprintf(file, "成绩:%.2f\n", 95.5);

// 使用fputs写入字符串
fputs("这是使用fputs写入的一行文本。\n", file);
fputs("这是第二行。\n", file);

// 使用fputc写入单个字符
fputc('A', file);
fputc('B', file);
fputc('C', file);
fputc('\n', file);

// 关闭文件
fclose(file);

printf("文本文件创建并写入成功!\n");
}

补充示例:向文件追加内容

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 向文本文件追加内容
void append_to_text_file() {
FILE *file;

// 以追加模式打开文件
file = fopen("test.txt", "a");

if (file == NULL) {
printf("无法打开文件!\n");
return;
}

// 追加内容
fprintf(file, "\n=== 追加的内容 ===\n");
fprintf(file, "这是追加的第一行。\n");
fprintf(file, "这是追加的第二行。\n");

// 关闭文件
fclose(file);

printf("内容追加成功!\n");
}

读取文本文件

文本文件读取函数

C语言提供了多种读取文本文件的函数:

函数 功能 示例
fscanf 格式化读取 fscanf(file, "姓名:%s", name)
fgets 读取一行字符串 fgets(buffer, sizeof(buffer), file)
fgetc 读取单个字符 fgetc(file)
fread 二进制读取(也可用于文本文件) fread(buffer, sizeof(char), length, file)

示例代码

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
44
45
46
47
48
49
// 读取文本文件
void read_text_file() {
FILE *file;
char buffer[256];
char ch;

// 以读取模式打开文件
file = fopen("test.txt", "r");

if (file == NULL) {
printf("无法打开文件!\n");
return;
}

printf("=== 使用fgets读取文件内容 ===\n");
// 使用fgets逐行读取
while (fgets(buffer, sizeof(buffer), file) != NULL) {
printf("%s", buffer);
}

// 重置文件指针到开头
rewind(file);

printf("\n=== 使用fgetc读取文件内容 ===\n");
// 使用fgetc逐个字符读取
while ((ch = fgetc(file)) != EOF) {
putchar(ch);
}

// 重置文件指针到开头
rewind(file);

printf("\n=== 使用fscanf读取格式化数据 ===\n");
char name[50];
int age;
float score;

// 使用fscanf读取格式化数据
fscanf(file, "姓名:%s\n", name); // 读取姓名,直到换行符
fscanf(file, "年龄:%d\n", &age); // 读取年龄
fscanf(file, "成绩:%f\n", &score); // 读取成绩

printf("姓名:%s\n", name);
printf("年龄:%d\n", age);
printf("成绩:%.2f\n", score);

// 关闭文件
fclose(file);
}

补充示例:读取带空格的字符串

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
// 读取带空格的字符串
void read_string_with_spaces() {
FILE *file;
char buffer[256];

// 以读取模式打开文件
file = fopen("test.txt", "r");

if (file == NULL) {
printf("无法打开文件!\n");
return;
}

// 读取带空格的字符串
printf("=== 读取带空格的字符串 ===\n");

// 使用fgets读取整行,包括空格
while (fgets(buffer, sizeof(buffer), file) != NULL) {
// 移除换行符
buffer[strcspn(buffer, "\n")] = '\0';
printf("读取的行:%s\n", buffer);
}

// 关闭文件
fclose(file);
}

二进制文件操作

二进制文件的特点

二进制文件与文本文件的主要区别在于:

  • 文本文件以字符形式存储数据,每个字符对应一个或多个字节
  • 二进制文件以数据的原始二进制形式存储,直接反映数据在内存中的表示

二进制文件读写函数

二进制文件通常使用fwritefread函数进行读写:

1
2
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
size_t fread(void *ptr, size_t size, size_t count, FILE *stream);

示例代码

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// 测试结构体(用于二进制文件操作)
struct Person {
char name[50];
int age;
float height;
};

// 二进制文件操作
void binary_file_operations() {
FILE *file;

// 创建测试数据
struct Person persons[] = {
{"张三", 25, 175.5},
{"李四", 30, 180.0},
{"王五", 28, 172.3}
};

int num_persons = sizeof(persons) / sizeof(persons[0]);

// =========================
// 写入二进制文件
// =========================
printf("写入二进制文件...\n");

// 以二进制写入模式打开文件
file = fopen("persons.dat", "wb");

if (file == NULL) {
printf("无法打开文件!\n");
return;
}

// 使用fwrite写入数据
fwrite(persons, sizeof(struct Person), num_persons, file);

fclose(file);

// =========================
// 读取二进制文件
// =========================
printf("读取二进制文件...\n");

// 以二进制读取模式打开文件
file = fopen("persons.dat", "rb");

if (file == NULL) {
printf("无法打开文件!\n");
return;
}

// 创建一个数组来存储读取的数据
struct Person read_persons[3];

// 使用fread读取数据
fread(read_persons, sizeof(struct Person), num_persons, file);

// 打印读取的数据
for (int i = 0; i < num_persons; i++) {
printf("\nPerson %d:\n", i + 1);
printf("姓名:%s\n", read_persons[i].name);
printf("年龄:%d\n", read_persons[i].age);
printf("身高:%.2f\n", read_persons[i].height);
}

fclose(file);
}

补充示例:追加到二进制文件

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
44
45
46
47
48
49
50
51
52
53
54
55
56
// 追加到二进制文件
void append_to_binary_file() {
FILE *file;

// 以二进制追加模式打开文件
file = fopen("persons.dat", "ab");

if (file == NULL) {
printf("无法打开文件!\n");
return;
}

// 要追加的数据
struct Person new_person = {"赵六", 35, 178.8};

// 追加数据
fwrite(&new_person, sizeof(struct Person), 1, file);

fclose(file);

printf("数据追加成功!\n");

// 验证追加结果
printf("验证追加结果...\n");

file = fopen("persons.dat", "rb");

if (file == NULL) {
printf("无法打开文件!\n");
return;
}

// 移动文件指针到末尾,计算文件中的记录数
fseek(file, 0, SEEK_END);
long file_size = ftell(file);
int total_persons = file_size / sizeof(struct Person);

// 重置文件指针到开头
rewind(file);

// 读取所有数据
struct Person *all_persons = (struct Person *)malloc(file_size);
fread(all_persons, sizeof(struct Person), total_persons, file);

// 打印所有数据
for (int i = 0; i < total_persons; i++) {
printf("\nPerson %d:\n", i + 1);
printf("姓名:%s\n", all_persons[i].name);
printf("年龄:%d\n", all_persons[i].age);
printf("身高:%.2f\n", all_persons[i].height);
}

// 释放内存
free(all_persons);
fclose(file);
}

文件定位

文件定位函数

C语言提供了以下文件定位函数:

函数 功能 示例
fseek 移动文件指针到指定位置 fseek(file, 0, SEEK_SET)
ftell 获取当前文件指针位置 long position = ftell(file)
rewind 将文件指针重置到文件开头 rewind(file)
fgetpos 获取当前文件指针位置(更精确) fgetpos(file, &pos)
fsetpos 设置文件指针位置(更精确) fsetpos(file, &pos)

whence参数

fseek函数的第三个参数whence指定了偏移量的参考位置:

说明
SEEK_SET 文件开头
SEEK_CUR 当前位置
SEEK_END 文件末尾

示例代码

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
// 文件定位示例
void file_positioning() {
FILE *file;
char buffer[256];

// 以读取模式打开文件
file = fopen("test.txt", "r");

if (file == NULL) {
printf("无法打开文件!\n");
return;
}

// 获取当前文件指针位置
long position = ftell(file);
printf("初始文件指针位置:%ld\n", position);

// 读取一行
fgets(buffer, sizeof(buffer), file);
printf("读取的第一行:%s", buffer);

// 获取当前文件指针位置
position = ftell(file);
printf("读取第一行后位置:%ld\n", position);

// 使用fseek移动文件指针到指定位置
fseek(file, 0, SEEK_SET); // 移动到文件开头
position = ftell(file);
printf("移动到文件开头后位置:%ld\n", position);

// 再次读取第一行
fgets(buffer, sizeof(buffer), file);
printf("再次读取的第一行:%s", buffer);

// 移动到文件末尾
fseek(file, 0, SEEK_END);
position = ftell(file);
printf("移动到文件末尾后位置:%ld\n", position);

// 向前移动10个字节
fseek(file, -10, SEEK_CUR);
position = ftell(file);
printf("向前移动10个字节后位置:%ld\n", position);

// 读取剩余内容
printf("从当前位置读取的内容:\n");
while (fgets(buffer, sizeof(buffer), file) != NULL) {
printf("%s", buffer);
}

// 使用rewind重置文件指针到开头
rewind(file);
position = ftell(file);
printf("rewind后文件指针位置:%ld\n", position);

// 关闭文件
fclose(file);
}

补充示例:随机访问文件

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
// 随机访问文件
void random_access_file() {
FILE *file;
char buffer[256];

// 以读写模式打开文件
file = fopen("test.txt", "r+");

if (file == NULL) {
printf("无法打开文件!\n");
return;
}

// 移动到文件中间位置
fseek(file, 50, SEEK_SET);
printf("移动到位置50后,当前位置:%ld\n", ftell(file));

// 读取从当前位置开始的内容
printf("从位置50读取的内容:\n");
while (fgets(buffer, sizeof(buffer), file) != NULL) {
printf("%s", buffer);
}

// 移动到文件开头并写入内容
fseek(file, 0, SEEK_SET);
fprintf(file, "=== 覆盖的内容 ===\n");

// 关闭文件
fclose(file);

printf("文件随机访问操作完成!\n");
}

错误处理

错误处理函数

C语言提供了以下文件错误处理函数:

函数 功能 示例
ferror 检查文件操作是否发生错误 if (ferror(file)) { /* 处理错误 */ }
feof 检查是否到达文件末尾 if (feof(file)) { /* 到达文件末尾 */ }
perror 打印系统错误信息 perror("fopen")
clearerr 清除文件错误标志 clearerr(file)

示例代码

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
44
45
46
47
48
49
50
51
52
53
54
55
56
57
// 错误处理示例
void error_handling() {
FILE *file;
char ch;

// 尝试打开一个不存在的文件
printf("尝试打开一个不存在的文件:\n");
file = fopen("non_existent_file.txt", "r");

if (file == NULL) {
perror("fopen"); // 打印系统错误信息
printf("无法打开文件!\n");
} else {
fclose(file);
}

// 正确打开文件并演示ferror和feof
printf("\n演示ferror和feof函数:\n");

file = fopen("test.txt", "r");

if (file == NULL) {
perror("fopen");
return;
}

// 读取文件内容
printf("读取文件内容:\n");
while ((ch = fgetc(file)) != EOF) {
putchar(ch);
}

// 检查是否到达文件末尾
if (feof(file)) {
printf("\n已到达文件末尾(feof返回非零值)\n");
}

// 检查是否发生错误
if (ferror(file)) {
printf("读取文件时发生错误\n");
} else {
printf("文件读取成功,无错误\n");
}

// 清除错误标志
clearerr(file);

// 再次检查错误标志
if (ferror(file)) {
printf("错误标志未清除\n");
} else {
printf("错误标志已清除\n");
}

// 关闭文件
fclose(file);
}

补充示例:处理文件读写错误

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
44
45
46
47
48
49
50
51
52
53
// 处理文件读写错误
void handle_file_errors() {
FILE *file;
char buffer[256];

// 尝试写入文件
file = fopen("protected_file.txt", "w");

if (file == NULL) {
perror("无法打开文件进行写入");
return;
}

// 尝试写入数据
if (fputs("测试数据\n", file) == EOF) {
perror("写入文件时发生错误");
} else {
printf("数据写入成功\n");
}

// 关闭文件
if (fclose(file) == EOF) {
perror("关闭文件时发生错误");
} else {
printf("文件关闭成功\n");
}

// 尝试读取文件
file = fopen("protected_file.txt", "r");

if (file == NULL) {
perror("无法打开文件进行读取");
return;
}

// 尝试读取数据
if (fgets(buffer, sizeof(buffer), file) == NULL) {
if (feof(file)) {
printf("文件为空\n");
} else {
perror("读取文件时发生错误");
}
} else {
printf("读取的数据:%s", buffer);
}

// 关闭文件
if (fclose(file) == EOF) {
perror("关闭文件时发生错误");
} else {
printf("文件关闭成功\n");
}
}

文件操作的高级应用

文件复制

文件复制是一种常见的文件操作,它将一个文件的内容复制到另一个文件。

示例代码

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
// 文件复制函数
void copy_file(const char *source_path, const char *destination_path) {
FILE *source_file, *dest_file;
char buffer[1024];
size_t bytes_read;

// 打开源文件
source_file = fopen(source_path, "rb");
if (source_file == NULL) {
perror("无法打开源文件");
return;
}

// 创建目标文件
dest_file = fopen(destination_path, "wb");
if (dest_file == NULL) {
perror("无法创建目标文件");
fclose(source_file);
return;
}

// 复制文件内容
while ((bytes_read = fread(buffer, 1, sizeof(buffer), source_file)) > 0) {
if (fwrite(buffer, 1, bytes_read, dest_file) != bytes_read) {
perror("写入目标文件时发生错误");
fclose(source_file);
fclose(dest_file);
return;
}
}

// 检查读取过程中是否发生错误
if (ferror(source_file)) {
perror("读取源文件时发生错误");
} else {
printf("文件复制成功!\n");
}

// 关闭文件
fclose(source_file);
fclose(dest_file);
}

文件加密/解密

文件加密/解密是一种保护文件内容的方法,它通过某种算法改变文件的内容,使得只有知道密钥的人才能读取。

示例代码

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
// 简单的文件加密/解密函数(使用异或操作)
void encrypt_decrypt_file(const char *input_path, const char *output_path, char key) {
FILE *input_file, *output_file;
int ch;

// 打开输入文件
input_file = fopen(input_path, "rb");
if (input_file == NULL) {
perror("无法打开输入文件");
return;
}

// 创建输出文件
output_file = fopen(output_path, "wb");
if (output_file == NULL) {
perror("无法创建输出文件");
fclose(input_file);
return;
}

// 加密/解密文件内容
while ((ch = fgetc(input_file)) != EOF) {
// 使用异或操作进行加密/解密
ch ^= key;
fputc(ch, output_file);
}

// 检查读取过程中是否发生错误
if (ferror(input_file)) {
perror("读取输入文件时发生错误");
} else {
printf("文件加密/解密成功!\n");
}

// 关闭文件
fclose(input_file);
fclose(output_file);
}

补充示例:文件行号统计

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
// 统计文件的行数
int count_lines(const char *filename) {
FILE *file;
int count = 0;
int ch;

// 打开文件
file = fopen(filename, "r");
if (file == NULL) {
perror("无法打开文件");
return -1;
}

// 统计行数
while ((ch = fgetc(file)) != EOF) {
if (ch == '\n') {
count++;
}
}

// 检查文件是否以换行符结束
if (count > 0) {
fseek(file, -1, SEEK_END);
if (fgetc(file) != '\n') {
count++;
}
}

// 关闭文件
fclose(file);

return count;
}

// 使用行数统计函数
void test_count_lines() {
const char *filename = "test.txt";
int lines = count_lines(filename);

if (lines >= 0) {
printf("文件 %s 包含 %d 行\n", filename, lines);
}
}

文件操作的最佳实践

  1. 始终检查文件打开是否成功:使用fopen函数后,一定要检查返回值是否为NULL
  2. 使用完毕后关闭文件:使用fclose函数关闭文件,释放系统资源
  3. 处理文件操作错误:使用ferrorfeof等函数检查文件操作是否成功
  4. 使用适当的文件打开模式:根据实际需求选择正确的文件打开模式
  5. 注意文件指针的位置:在进行文件读写操作时,要注意文件指针的当前位置
  6. 使用二进制模式处理二进制文件:对于二进制文件,使用rbwb等二进制模式
  7. 合理使用缓冲区:使用适当大小的缓冲区进行文件读写,提高效率
  8. 避免文件锁定:尽量减少文件打开的时间,避免长时间占用文件
  9. 处理大文件时注意内存:对于大文件,应分块读写,避免一次性加载到内存
  10. 使用临时文件:对于需要修改的文件,考虑使用临时文件,避免直接修改原文件

综合示例

示例:学生信息管理系统(文件版)

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
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
// 学生信息管理系统(文件版)
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 学生结构体
typedef struct {
char name[50];
int id;
float score;
} Student;

// 函数原型
void add_student();
void view_students();
void update_student();
void delete_student();
void save_students(Student students[], int count);
int load_students(Student students[]);

#define MAX_STUDENTS 100

int main() {
int choice;

do {
printf("\n学生信息管理系统\n");
printf("1. 添加学生\n");
printf("2. 查看学生\n");
printf("3. 更新学生信息\n");
printf("4. 删除学生\n");
printf("0. 退出\n");
printf("请选择:");
scanf("%d", &choice);

switch (choice) {
case 1:
add_student();
break;
case 2:
view_students();
break;
case 3:
update_student();
break;
case 4:
delete_student();
break;
case 0:
printf("退出系统\n");
break;
default:
printf("无效选择\n");
}
} while (choice != 0);

return 0;
}

// 添加学生
void add_student() {
Student students[MAX_STUDENTS];
int count = load_students(students);

if (count >= MAX_STUDENTS) {
printf("学生数量已达到上限\n");
return;
}

// 输入学生信息
printf("请输入学生姓名:");
scanf("%s", students[count].name);
printf("请输入学生ID:");
scanf("%d", &students[count].id);
printf("请输入学生成绩:");
scanf("%f", &students[count].score);

count++;
save_students(students, count);

printf("学生添加成功\n");
}

// 查看学生
void view_students() {
Student students[MAX_STUDENTS];
int count = load_students(students);

if (count == 0) {
printf("没有学生信息\n");
return;
}

printf("\n学生信息:\n");
for (int i = 0; i < count; i++) {
printf("姓名:%s, ID:%d, 成绩:%.1f\n", students[i].name, students[i].id, students[i].score);
}
}

// 更新学生信息
void update_student() {
Student students[MAX_STUDENTS];
int count = load_students(students);
int id, found = 0;

if (count == 0) {
printf("没有学生信息\n");
return;
}

printf("请输入要更新的学生ID:");
scanf("%d", &id);

for (int i = 0; i < count; i++) {
if (students[i].id == id) {
printf("找到学生:%s\n", students[i].name);
printf("请输入新的姓名:");
scanf("%s", students[i].name);
printf("请输入新的成绩:");
scanf("%f", &students[i].score);
found = 1;
break;
}
}

if (found) {
save_students(students, count);
printf("学生信息更新成功\n");
} else {
printf("未找到ID为%d的学生\n", id);
}
}

// 删除学生
void delete_student() {
Student students[MAX_STUDENTS];
int count = load_students(students);
int id, found = 0;

if (count == 0) {
printf("没有学生信息\n");
return;
}

printf("请输入要删除的学生ID:");
scanf("%d", &id);

for (int i = 0; i < count; i++) {
if (students[i].id == id) {
// 从当前位置开始,后面的元素向前移动
for (int j = i; j < count - 1; j++) {
students[j] = students[j + 1];
}
found = 1;
count--;
break;
}
}

if (found) {
save_students(students, count);
printf("学生删除成功\n");
} else {
printf("未找到ID为%d的学生\n", id);
}
}

// 保存学生信息到文件
void save_students(Student students[], int count) {
FILE *file = fopen("students.dat", "wb");

if (file == NULL) {
perror("无法打开文件进行写入");
return;
}

fwrite(&count, sizeof(int), 1, file);
fwrite(students, sizeof(Student), count, file);
fclose(file);
}

// 从文件加载学生信息
int load_students(Student students[]) {
FILE *file = fopen("students.dat", "rb");
int count = 0;

if (file == NULL) {
// 文件不存在,返回0
return 0;
}

fread(&count, sizeof(int), 1, file);
fread(students, sizeof(Student), count, file);
fclose(file);

return count;
}

总结

C语言中的文件操作是实现数据持久化的重要手段,通过本文档的学习,我们了解了:

  1. 文件操作的基本概念:文件指针、文件打开模式等
  2. 创建和写入文本文件:使用fprintffputsfputc等函数
  3. 读取文本文件:使用fscanffgetsfgetc等函数
  4. 二进制文件操作:使用fwritefread函数
  5. 文件定位:使用fseekftellrewind等函数
  6. 错误处理:使用ferrorfeofperror等函数
  7. 文件操作的高级应用:文件复制、加密/解密、行号统计等
  8. 文件操作的最佳实践:检查文件打开是否成功、关闭文件、处理错误等

掌握文件操作对于编写实用的C程序至关重要,它允许程序与外部存储设备进行数据交换,实现数据的持久化存储。通过合理使用文件操作函数,我们可以编写出更加灵活、功能强大的C程序。

file_operations.c

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
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

/**
* @brief 文件操作示例程序
* @details 这个程序演示了C语言中各种文件操作,包括文件的打开和关闭、
* 文本文件的读写、二进制文件的读写、文件定位以及错误处理等。
* @return 0 表示程序成功执行
*/

// =========================
// 函数原型声明
// =========================
void create_text_file(); // 创建和写入文本文件
void read_text_file(); // 读取文本文件
void binary_file_operations(); // 二进制文件操作
void file_positioning(); // 文件定位示例
void error_handling(); // 错误处理示例

/**
* @brief 测试结构体(用于二进制文件操作)
*/
struct Person {
char name[50];
int age;
float height;
};

/**
* @brief 主函数
* @return 0 表示程序成功执行
*/
int main() {
// =========================
// 创建和写入文本文件
// =========================
printf("=== 创建和写入文本文件 ===\n");
create_text_file();

printf("\n");

// =========================
// 读取文本文件
// =========================
printf("=== 读取文本文件 ===\n");
read_text_file();

printf("\n");

// =========================
// 二进制文件操作
// =========================
printf("=== 二进制文件操作 ===\n");
binary_file_operations();

printf("\n");

// =========================
// 文件定位
// =========================
printf("=== 文件定位 ===\n");
file_positioning();

printf("\n");

// =========================
// 错误处理
// =========================
printf("=== 错误处理 ===\n");
error_handling();

return 0;
}

/**
* @brief 创建和写入文本文件
* @details 演示使用fopen、fprintf、fputs和fclose函数
*/
void create_text_file() {
FILE *file;

// 以写入模式打开文件(如果文件不存在则创建,存在则截断)
file = fopen("test.txt", "w");

if (file == NULL) {
printf("无法打开文件!\n");
return;
}

// 使用fprintf写入格式化数据
fprintf(file, "姓名:张三\n");
fprintf(file, "年龄:%d\n", 25);
fprintf(file, "成绩:%.2f\n", 95.5);

// 使用fputs写入字符串
fputs("这是使用fputs写入的一行文本。\n", file);
fputs("这是第二行。\n", file);

// 使用fputc写入单个字符
fputc('A', file);
fputc('B', file);
fputc('C', file);
fputc('\n', file);

// 关闭文件
fclose(file);

printf("文本文件创建并写入成功!\n");
}

/**
* @brief 读取文本文件
* @details 演示使用fopen、fscanf、fgets、fgetc和fclose函数
*/
void read_text_file() {
FILE *file;
char buffer[256];
char ch;

// 以读取模式打开文件
file = fopen("test.txt", "r");

if (file == NULL) {
printf("无法打开文件!\n");
return;
}

printf("=== 使用fgets读取文件内容 ===\n");
// 使用fgets逐行读取
while (fgets(buffer, sizeof(buffer), file) != NULL) {
printf("%s", buffer);
}

// 重置文件指针到开头
rewind(file);

printf("\n=== 使用fgetc读取文件内容 ===\n");
// 使用fgetc逐个字符读取
while ((ch = fgetc(file)) != EOF) {
putchar(ch);
}

// 重置文件指针到开头
rewind(file);

printf("\n=== 使用fscanf读取格式化数据 ===\n");
char name[50];
int age;
float score;

// 使用fscanf读取格式化数据
fscanf(file, "姓名:%s\n", name); // 读取姓名,直到换行符
fscanf(file, "年龄:%d\n", &age); // 读取年龄
fscanf(file, "成绩:%f\n", &score); // 读取成绩

printf("姓名:%s\n", name);
printf("年龄:%d\n", age);
printf("成绩:%.2f\n", score);

// 关闭文件
fclose(file);
}

/**
* @brief 二进制文件操作
* @details 演示使用fwrite和fread函数进行二进制文件操作
*/
void binary_file_operations() {
FILE *file;

// 创建测试数据
struct Person persons[] = {
{"张三", 25, 175.5},
{"李四", 30, 180.0},
{"王五", 28, 172.3}
};

int num_persons = sizeof(persons) / sizeof(persons[0]);

// =========================
// 写入二进制文件
// =========================
printf("写入二进制文件...\n");

// 以二进制写入模式打开文件
file = fopen("persons.dat", "wb");

if (file == NULL) {
printf("无法打开文件!\n");
return;
}

// 使用fwrite写入数据
fwrite(persons, sizeof(struct Person), num_persons, file);

fclose(file);

// =========================
// 读取二进制文件
// =========================
printf("读取二进制文件...\n");

// 以二进制读取模式打开文件
file = fopen("persons.dat", "rb");

if (file == NULL) {
printf("无法打开文件!\n");
return;
}

// 创建一个数组来存储读取的数据
struct Person read_persons[3];

// 使用fread读取数据
fread(read_persons, sizeof(struct Person), num_persons, file);

// 打印读取的数据
for (int i = 0; i < num_persons; i++) {
printf("\nPerson %d:\n", i + 1);
printf("姓名:%s\n", read_persons[i].name);
printf("年龄:%d\n", read_persons[i].age);
printf("身高:%.2f\n", read_persons[i].height);
}

fclose(file);

// =========================
// 追加到二进制文件
// =========================
printf("\n追加到二进制文件...\n");

// 以二进制追加模式打开文件
file = fopen("persons.dat", "ab");

if (file == NULL) {
printf("无法打开文件!\n");
return;
}

// 要追加的数据
struct Person new_person = {"赵六", 35, 178.8};

// 追加数据
fwrite(&new_person, sizeof(struct Person), 1, file);

fclose(file);

// 验证追加结果
printf("验证追加结果...\n");

file = fopen("persons.dat", "rb");

if (file == NULL) {
printf("无法打开文件!\n");
return;
}

// 移动文件指针到末尾,计算文件中的记录数
fseek(file, 0, SEEK_END);
long file_size = ftell(file);
int total_persons = file_size / sizeof(struct Person);

// 重置文件指针到开头
rewind(file);

// 读取所有数据
struct Person *all_persons = (struct Person *)malloc(file_size);
fread(all_persons, sizeof(struct Person), total_persons, file);

// 打印所有数据
for (int i = 0; i < total_persons; i++) {
printf("\nPerson %d:\n", i + 1);
printf("姓名:%s\n", all_persons[i].name);
printf("年龄:%d\n", all_persons[i].age);
printf("身高:%.2f\n", all_persons[i].height);
}

// 释放内存
free(all_persons);
fclose(file);
}

/**
* @brief 文件定位示例
* @details 演示使用fseek、ftell和rewind函数
*/
void file_positioning() {
FILE *file;
char buffer[256];

// 以读取模式打开文件
file = fopen("test.txt", "r");

if (file == NULL) {
printf("无法打开文件!\n");
return;
}

// 获取当前文件指针位置
long position = ftell(file);
printf("初始文件指针位置:%ld\n", position);

// 读取一行
fgets(buffer, sizeof(buffer), file);
printf("读取的第一行:%s", buffer);

// 获取当前文件指针位置
position = ftell(file);
printf("读取第一行后位置:%ld\n", position);

// 使用fseek移动文件指针到指定位置
fseek(file, 0, SEEK_SET); // 移动到文件开头
position = ftell(file);
printf("移动到文件开头后位置:%ld\n", position);

// 再次读取第一行
fgets(buffer, sizeof(buffer), file);
printf("再次读取的第一行:%s", buffer);

// 移动到文件末尾
fseek(file, 0, SEEK_END);
position = ftell(file);
printf("移动到文件末尾后位置:%ld\n", position);

// 向前移动10个字节
fseek(file, -10, SEEK_CUR);
position = ftell(file);
printf("向前移动10个字节后位置:%ld\n", position);

// 读取剩余内容
printf("从当前位置读取的内容:\n");
while (fgets(buffer, sizeof(buffer), file) != NULL) {
printf("%s", buffer);
}

// 使用rewind重置文件指针到开头
rewind(file);
position = ftell(file);
printf("rewind后文件指针位置:%ld\n", position);

// 关闭文件
fclose(file);
}

/**
* @brief 错误处理示例
* @details 演示使用ferror、feof和perror函数
*/
void error_handling() {
FILE *file;
char ch;

// 尝试打开一个不存在的文件
printf("尝试打开一个不存在的文件:\n");
file = fopen("non_existent_file.txt", "r");

if (file == NULL) {
perror("fopen"); // 打印系统错误信息
printf("无法打开文件!\n");
} else {
fclose(file);
}

// 正确打开文件并演示ferror和feof
printf("\n演示ferror和feof函数:\n");

file = fopen("test.txt", "r");

if (file == NULL) {
perror("fopen");
return;
}

// 读取文件内容
printf("读取文件内容:\n");
while ((ch = fgetc(file)) != EOF) {
putchar(ch);
}

// 检查是否到达文件末尾
if (feof(file)) {
printf("\n已到达文件末尾(feof返回非零值)\n");
}

// 检查是否发生错误
if (ferror(file)) {
printf("读取文件时发生错误\n");
} else {
printf("文件读取成功,无错误\n");
}

// 清除错误标志
clearerr(file);

// 再次检查错误标志
if (ferror(file)) {
printf("错误标志未清除\n");
} else {
printf("错误标志已清除\n");
}

// 关闭文件
fclose(file);
}

C语言结构体和联合体

概述

本文档详细介绍C语言中结构体和联合体的使用方法,包括结构体的定义、初始化、结构体数组、结构体指针、嵌套结构体、位域结构体、联合体的定义和使用,以及枚举与结构体的结合使用等内容。结构体和联合体是C语言中重要的数据结构,它们允许我们将不同类型的数据组合在一起,提高代码的组织性和可读性。

结构体的基本概念

结构体的定义

结构体是一种用户自定义的数据类型,它可以包含不同类型的成员变量。结构体的定义语法如下:

1
2
3
4
5
struct 结构体名 {
类型 成员名1;
类型 成员名2;
// 更多成员...
};

结构体变量的声明

结构体变量的声明语法如下:

1
struct 结构体名 变量名;

示例代码

1
2
3
4
5
6
7
8
9
10
// 学生结构体定义
struct Student {
char name[50]; // 姓名
int age; // 年龄
int student_id; // 学号
float score; // 成绩
};

// 声明结构体变量
struct Student student1;

结构体的定义与初始化

结构体成员的赋值

可以使用点运算符(.)访问和修改结构体成员:

1
2
3
4
5
// 结构体成员赋值
strcpy(student1.name, "张三");
student1.age = 20;
student1.student_id = 1001;
student1.score = 95.5;

结构体变量的初始化

结构体变量可以在声明时进行初始化,有以下几种方式:

  1. 按顺序初始化:按照结构体成员的声明顺序进行初始化
  2. 指定初始化器:使用成员名进行初始化(C99及以上)

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 按顺序初始化结构体
struct Student student2 = {
"李四",
21,
1002,
88.0
};

// 使用指定初始化器(C99及以上)
struct Student student3 = {
.name = "王五",
.student_id = 1003,
.score = 92.5,
.age = 22
};

补充示例:结构体的嵌套初始化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 日期结构体
struct Date {
int year;
int month;
int day;
};

// 员工结构体(嵌套结构体)
struct Employee {
char name[50];
int employee_id;
float salary;
struct Date hire_date; // 嵌套日期结构体
};

// 嵌套结构体初始化
struct Employee emp = {
"王五",
2001,
5000.0,
{2020, 3, 15} // 嵌套结构体初始化
};

结构体数组

结构体数组的定义

结构体数组是元素为结构体的数组,定义语法如下:

1
struct 结构体名 数组名[数组大小];

结构体数组的初始化

结构体数组可以在声明时进行初始化:

1
2
3
4
5
6
// 结构体数组初始化
struct Student students[] = {
{"赵六", 20, 1004, 85.5},
{"孙七", 21, 1005, 90.0},
{"周八", 22, 1006, 78.5}
};

访问结构体数组元素

可以使用下标访问结构体数组的元素,然后使用点运算符访问成员:

1
2
3
4
5
6
7
8
9
10
// 访问结构体数组元素
int num_students = sizeof(students) / sizeof(students[0]);

for (int i = 0; i < num_students; i++) {
printf("学生 %d:\n", i + 1);
printf("姓名:%s\n", students[i].name);
printf("年龄:%d\n", students[i].age);
printf("学号:%d\n", students[i].student_id);
printf("成绩:%.1f\n", students[i].score);
}

补充示例:结构体数组的排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 结构体数组排序(按成绩降序)
void sort_students_by_score(struct Student arr[], int size) {
for (int i = 0; i < size - 1; i++) {
for (int j = 0; j < size - i - 1; j++) {
if (arr[j].score < arr[j + 1].score) {
// 交换元素
struct Student temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
}
}
}
}

// 使用排序函数
sort_students_by_score(students, num_students);

// 打印排序后的结果
printf("按成绩排序后的学生信息:\n");
for (int i = 0; i < num_students; i++) {
printf("第%d名:%s,成绩:%.1f\n", i + 1, students[i].name, students[i].score);
}

结构体指针

结构体指针的定义

结构体指针是指向结构体的指针,定义语法如下:

1
struct 结构体名 *指针变量名;

结构体指针的初始化与使用

结构体指针可以指向结构体变量,使用&运算符获取结构体变量的地址:

1
2
3
4
5
6
// 结构体指针声明和初始化
struct Student *student_ptr = &student1;

// 通过指针访问结构体成员(两种方式)
printf("姓名:%s\n", student_ptr->name); // 箭头运算符
printf("年龄:%d\n", (*student_ptr).age); // 解引用后使用点运算符

修改结构体指针指向的成员

可以通过结构体指针修改结构体成员的值:

1
2
3
// 使用结构体指针更新成员
student_ptr->score = 98.0;
printf("更新后学生1成绩:%.1f\n", student1.score);

补充示例:动态分配结构体

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 动态分配结构体
struct Student *dynamic_student = (struct Student *)malloc(sizeof(struct Student));
if (dynamic_student == NULL) {
printf("内存分配失败\n");
return 1;
}

// 初始化动态分配的结构体
strcpy(dynamic_student->name, "动态学生");
dynamic_student->age = 20;
dynamic_student->student_id = 2000;
dynamic_student->score = 90.0;

// 打印动态分配的结构体
printf("动态分配的学生信息:\n");
printf("姓名:%s\n", dynamic_student->name);
printf("年龄:%d\n", dynamic_student->age);
printf("学号:%d\n", dynamic_student->student_id);
printf("成绩:%.1f\n", dynamic_student->score);

// 释放内存
free(dynamic_student);
dynamic_student = NULL;

嵌套结构体

嵌套结构体的定义

嵌套结构体是指在一个结构体中包含另一个结构体作为成员:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 日期结构体
struct Date {
int year;
int month;
int day;
};

// 员工结构体(嵌套结构体示例)
struct Employee {
char name[50];
int employee_id;
float salary;
struct Date hire_date; // 嵌套日期结构体
};

嵌套结构体的初始化与访问

嵌套结构体的初始化和访问需要使用点运算符链式访问:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 初始化嵌套结构体
struct Employee emp = {
"王五",
2001,
5000.0,
{2020, 3, 15} // 嵌套结构体初始化
};

// 访问嵌套结构体成员
printf("入职日期:%d-%d-%d\n", emp.hire_date.year, emp.hire_date.month, emp.hire_date.day);

// 修改嵌套结构体成员
emp.hire_date.year = 2021;
printf("修改后入职日期:%d-%d-%d\n", emp.hire_date.year, emp.hire_date.month, emp.hire_date.day);

补充示例:多层嵌套结构体

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
// 多层嵌套结构体示例
struct Address {
char street[100];
char city[50];
char zip_code[10];
};

struct Person {
char name[50];
int age;
struct Address address; // 嵌套地址结构体
};

struct Company {
char name[100];
struct Person CEO; // 嵌套人员结构体
};

// 初始化多层嵌套结构体
struct Company company = {
"科技公司",
{
"张三",
45,
{
"科技路1号",
"北京",
"100000"
}
}
};

// 访问多层嵌套结构体成员
printf("公司名称:%s\n", company.name);
printf("CEO姓名:%s\n", company.CEO.name);
printf("CEO地址:%s, %s, %s\n", company.CEO.address.street, company.CEO.address.city, company.CEO.address.zip_code);

位域结构体

位域的基本概念

位域是指在结构体中使用冒号(:)指定成员所占的位数,用于节省内存空间:

1
2
3
4
5
6
7
// 位域结构体示例
struct Flags {
unsigned int is_active : 1; // 1位:是否激活
unsigned int is_admin : 1; // 1位:是否管理员
unsigned int user_type : 2; // 2位:用户类型(0-3)
unsigned int permissions : 4; // 4位:权限标志
};

位域的使用

位域的使用方式与普通结构体成员相同:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 初始化位域结构体
struct Flags flags = {
1, // is_active = true
0, // is_admin = false
2, // user_type = 2
10 // permissions = 10 (二进制:1010)
};

// 访问位域成员
printf("is_active:%d\n", flags.is_active);
printf("is_admin:%d\n", flags.is_admin);
printf("user_type:%d\n", flags.user_type);
printf("permissions:%d\n", flags.permissions);

// 修改位域成员
flags.is_admin = 1;
printf("修改后is_admin:%d\n", flags.is_admin);

// 显示位域结构体的大小
printf("位域结构体大小:%zu 字节\n", sizeof(struct Flags));

补充示例:位域的应用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 位域的应用场景:网络协议头
struct EthernetHeader {
unsigned int destination : 48; // 目的MAC地址(6字节)
unsigned int source : 48; // 源MAC地址(6字节)
unsigned int type : 16; // 类型字段(2字节)
};

// 位域的应用场景:硬件寄存器
struct StatusRegister {
unsigned int ready : 1; // 就绪标志
unsigned int error : 1; // 错误标志
unsigned int mode : 2; // 模式选择
unsigned int reserved : 4; // 保留位
};

// 使用状态寄存器位域
struct StatusRegister status = {1, 0, 2, 0};
printf("状态寄存器:\n");
printf("就绪:%d\n", status.ready);
printf("错误:%d\n", status.error);
printf("模式:%d\n", status.mode);

联合体

联合体的基本概念

联合体是一种特殊的数据类型,它的所有成员共享同一块内存空间,每次只能使用其中一个成员:

1
2
3
4
5
6
// 联合体示例
union Data {
int i; // 整型
float f; // 浮点型
char str[20]; // 字符数组
};

联合体的使用

联合体的使用方式与结构体类似,但需要注意成员共享内存的特性:

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
// 联合体变量声明和初始化
union Data data;

// 赋值为整型
data.i = 123;
printf("联合体存储整型:\n");
printf("整型值:%d\n", data.i);
// printf("浮点值:%.2f\n", data.f); // 注意:这里的值是不确定的
// printf("字符串:%s\n", data.str); // 注意:这里的值是不确定的

// 赋值为浮点型(会覆盖之前的整型值)
data.f = 3.14159;
printf("\n联合体存储浮点型:\n");
// printf("整型值:%d\n", data.i); // 注意:这里的值是不确定的
printf("浮点值:%.2f\n", data.f);
// printf("字符串:%s\n", data.str); // 注意:这里的值是不确定的

// 赋值为字符串(会覆盖之前的浮点型值)
strcpy(data.str, "Hello, Union!");
printf("\n联合体存储字符串:\n");
// printf("整型值:%d\n", data.i); // 注意:这里的值是不确定的
// printf("浮点值:%.2f\n", data.f); // 注意:这里的值是不确定的
printf("字符串:%s\n", data.str);

// 显示联合体的大小
printf("\n联合体大小:%zu 字节\n", sizeof(union Data));

补充示例:联合体的应用场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 联合体的应用场景:类型转换
union TypeConverter {
int i;
float f;
unsigned char bytes[4];
};

// 使用联合体进行类型转换
union TypeConverter converter;
converter.f = 3.14159;

printf("浮点数:%.2f\n", converter.f);
printf("对应的整型:%d\n", converter.i);
printf("对应的字节:");
for (int j = 0; j < 4; j++) {
printf("%02X ", converter.bytes[j]);
}
printf("\n");

// 联合体的应用场景:节省内存
struct Node {
int type; // 节点类型
union {
int i_value; // 当type为0时使用
float f_value; // 当type为1时使用
char *s_value; // 当type为2时使用
} value;
};

// 使用节点结构体
struct Node node1 = {0, {.i_value = 100}};
struct Node node2 = {1, {.f_value = 3.14}};
struct Node node3 = {2, {.s_value = "Hello"}};

printf("节点1:类型=%d, 值=%d\n", node1.type, node1.value.i_value);
printf("节点2:类型=%d, 值=%.2f\n", node2.type, node2.value.f_value);
printf("节点3:类型=%d, 值=%s\n", node3.type, node3.value.s_value);

枚举与结构体

枚举类型的定义

枚举是一种用户定义的整数类型,用于表示一组命名的常量:

1
2
3
4
5
6
7
8
// 枚举类型示例
enum Color {
RED, // 0
GREEN, // 1
BLUE, // 2
YELLOW, // 3
BLACK // 4
};

枚举与结构体结合使用

枚举可以作为结构体的成员类型:

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 Shape {
enum Color color;
float area;
char name[20];
};

// 初始化带有枚举成员的结构体
struct Shape circle = {
RED,
78.5,
"Circle"
};

struct Shape rectangle = {
BLUE,
120.0,
"Rectangle"
};

// 访问结构体中的枚举成员
printf("图形1:\n");
printf("名称:%s\n", circle.name);
printf("颜色:%d\n", circle.color); // 枚举值为整数
printf("面积:%.1f\n", circle.area);

printf("\n图形2:\n");
printf("名称:%s\n", rectangle.name);
printf("颜色:%d\n", rectangle.color);
printf("面积:%.1f\n", rectangle.area);

补充示例:枚举的高级使用

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
// 枚举的高级使用:指定值
enum Weekday {
MONDAY = 1,
TUESDAY,
WEDNESDAY,
THURSDAY,
FRIDAY,
SATURDAY,
SUNDAY
};

// 带有枚举的结构体
struct Appointment {
char description[100];
enum Weekday day;
int hour;
int minute;
};

// 初始化约会结构体
struct Appointment meeting = {
"团队会议",
MONDAY,
10,
30
};

// 打印约会信息
printf("约会:%s\n", meeting.description);
printf("时间:周%d %d:%02d\n", meeting.day, meeting.hour, meeting.minute);

结构体作为函数参数

值传递与指针传递

结构体可以作为函数参数传递,有两种传递方式:

  1. 值传递:将结构体的副本传递给函数,函数内部对结构体的修改不会影响原始结构体
  2. 指针传递:将结构体的地址传递给函数,函数内部对结构体的修改会影响原始结构体

示例代码

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
// 打印学生信息(值传递)
void print_student(struct Student s) {
printf("姓名:%s\n", s.name);
printf("年龄:%d\n", s.age);
printf("学号:%d\n", s.student_id);
printf("成绩:%.1f\n", s.score);
}

// 更新学生成绩(指针传递)
void update_student_score(struct Student *s, float new_score) {
s->score = new_score;
}

// 使用函数
struct Student student = {"张三", 20, 1001, 95.5};

// 值传递
printf("通过值传递结构体:\n");
print_student(student);

// 指针传递
printf("\n通过指针传递结构体更新成绩:\n");
printf("更新前成绩:%.1f\n", student.score);
update_student_score(&student, 99.5);
printf("更新后成绩:%.1f\n", student.score);

补充示例:结构体作为函数返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 结构体作为函数返回值
struct Student create_student(char *name, int age, int student_id, float score) {
struct Student s;
strcpy(s.name, name);
s.age = age;
s.student_id = student_id;
s.score = score;
return s;
}

// 使用函数创建学生
struct Student new_student = create_student("新学生", 21, 1007, 89.5);
printf("创建的学生信息:\n");
print_student(new_student);

结构体的高级应用

结构体与链表

结构体可以用于实现链表等数据结构:

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
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
// 链表节点结构体
struct Node {
int data;
struct Node *next;
};

// 创建新节点
struct Node *create_node(int data) {
struct Node *new_node = (struct Node *)malloc(sizeof(struct Node));
if (new_node == NULL) {
return NULL;
}
new_node->data = data;
new_node->next = NULL;
return new_node;
}

// 添加节点到链表尾部
void append_node(struct Node **head, int data) {
struct Node *new_node = create_node(data);
if (new_node == NULL) {
return;
}

if (*head == NULL) {
*head = new_node;
return;
}

struct Node *current = *head;
while (current->next != NULL) {
current = current->next;
}
current->next = new_node;
}

// 打印链表
void print_list(struct Node *head) {
struct Node *current = head;
while (current != NULL) {
printf("%d -> ", current->data);
current = current->next;
}
printf("NULL\n");
}

// 释放链表
void free_list(struct Node *head) {
struct Node *temp;
while (head != NULL) {
temp = head;
head = head->next;
free(temp);
}
}

// 使用链表
struct Node *head = NULL;
append_node(&head, 10);
append_node(&head, 20);
append_node(&head, 30);
append_node(&head, 40);

printf("链表:");
print_list(head);

// 释放链表
free_list(head);

结构体与文件I/O

结构体可以用于文件的读写操作:

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
44
45
// 结构体与文件I/O
void write_students_to_file(struct Student students[], int count, const char *filename) {
FILE *file = fopen(filename, "wb");
if (file == NULL) {
printf("无法打开文件\n");
return;
}

fwrite(students, sizeof(struct Student), count, file);
fclose(file);
}

void read_students_from_file(struct Student students[], int count, const char *filename) {
FILE *file = fopen(filename, "rb");
if (file == NULL) {
printf("无法打开文件\n");
return;
}

fread(students, sizeof(struct Student), count, file);
fclose(file);
}

// 使用文件I/O
struct Student students_to_write[] = {
{"张三", 20, 1001, 95.5},
{"李四", 21, 1002, 88.0},
{"王五", 22, 1003, 92.5}
};

int count = sizeof(students_to_write) / sizeof(students_to_write[0]);

// 写入文件
write_students_to_file(students_to_write, count, "students.dat");
printf("学生信息已写入文件\n");

// 从文件读取
struct Student students_read[3];
read_students_from_file(students_read, count, "students.dat");

printf("从文件读取的学生信息:\n");
for (int i = 0; i < count; i++) {
printf("\n学生 %d:\n", i + 1);
print_student(students_read[i]);
}

最佳实践

  1. 使用typedef简化结构体声明:使用typedef为结构体创建别名,简化代码
  2. 合理设计结构体成员:将相关的数据成员组织在一起,提高代码的可读性
  3. 初始化结构体:总是初始化结构体,避免使用未初始化的成员
  4. 使用指针传递大型结构体:对于大型结构体,使用指针传递可以提高效率
  5. 注意结构体的内存对齐:了解结构体的内存对齐规则,合理安排成员顺序以节省内存
  6. 使用const修饰符:对于不需要修改的结构体参数,使用const修饰
  7. 避免结构体过大:如果结构体过大,考虑使用指针或分解为多个小结构体
  8. 使用位域节省内存:对于标志位等占用空间小的成员,使用位域
  9. 正确使用联合体:了解联合体的内存共享特性,避免错误使用
  10. 添加适当的注释:为结构体和联合体添加注释,说明其用途和成员含义

示例:使用typedef简化结构体声明

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 使用typedef简化结构体声明
typedef struct {
char name[50];
int age;
int id;
float salary;
} Employee; // Employee是结构体的别名

// 使用别名声明结构体变量
Employee emp1 = {"张三", 30, 1001, 5000.0};
Employee emp2 = {"李四", 25, 1002, 4500.0};

// 打印员工信息
printf("员工1:%s, %d岁, ID:%d, 工资:%.2f\n", emp1.name, emp1.age, emp1.id, emp1.salary);
printf("员工2:%s, %d岁, ID:%d, 工资:%.2f\n", emp2.name, emp2.age, emp2.id, emp2.salary);

综合示例

示例:学生管理系统

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
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
// 学生管理系统
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 学生结构体
typedef struct {
char name[50];
int id;
float score;
} Student;

// 函数原型
void add_student(Student **students, int *count);
void print_students(Student *students, int count);
void sort_students(Student *students, int count);
void save_students(Student *students, int count, const char *filename);
void load_students(Student **students, int *count, const char *filename);

int main() {
Student *students = NULL;
int count = 0;
int choice;

do {
printf("\n学生管理系统\n");
printf("1. 添加学生\n");
printf("2. 显示学生\n");
printf("3. 排序学生\n");
printf("4. 保存学生到文件\n");
printf("5. 从文件加载学生\n");
printf("0. 退出\n");
printf("请选择:");
scanf("%d", &choice);

switch (choice) {
case 1:
add_student(&students, &count);
break;
case 2:
print_students(students, count);
break;
case 3:
sort_students(students, count);
printf("学生已排序\n");
break;
case 4:
save_students(students, count, "students.dat");
break;
case 5:
load_students(&students, &count, "students.dat");
break;
case 0:
printf("退出系统\n");
break;
default:
printf("无效选择\n");
}
} while (choice != 0);

// 释放内存
free(students);

return 0;
}

// 添加学生
void add_student(Student **students, int *count) {
// 重新分配内存
*students = (Student *)realloc(*students, (*count + 1) * sizeof(Student));
if (*students == NULL) {
printf("内存分配失败\n");
return;
}

// 输入学生信息
printf("请输入学生姓名:");
scanf("%s", (*students)[*count].name);
printf("请输入学生ID:");
scanf("%d", &(*students)[*count].id);
printf("请输入学生成绩:");
scanf("%f", &(*students)[*count].score);

(*count)++;
printf("学生添加成功\n");
}

// 打印学生信息
void print_students(Student *students, int count) {
if (count == 0) {
printf("没有学生信息\n");
return;
}

printf("\n学生信息:\n");
for (int i = 0; i < count; i++) {
printf("姓名:%s, ID:%d, 成绩:%.1f\n", students[i].name, students[i].id, students[i].score);
}
}

// 排序学生(按成绩降序)
void sort_students(Student *students, int count) {
for (int i = 0; i < count - 1; i++) {
for (int j = 0; j < count - i - 1; j++) {
if (students[j].score < students[j + 1].score) {
Student temp = students[j];
students[j] = students[j + 1];
students[j + 1] = temp;
}
}
}
}

// 保存学生到文件
void save_students(Student *students, int count, const char *filename) {
FILE *file = fopen(filename, "wb");
if (file == NULL) {
printf("无法打开文件\n");
return;
}

fwrite(&count, sizeof(int), 1, file);
fwrite(students, sizeof(Student), count, file);
fclose(file);
printf("学生信息已保存到文件\n");
}

// 从文件加载学生
void load_students(Student **students, int *count, const char *filename) {
FILE *file = fopen(filename, "rb");
if (file == NULL) {
printf("无法打开文件\n");
return;
}

// 读取学生数量
fread(count, sizeof(int), 1, file);

// 分配内存
*students = (Student *)malloc(*count * sizeof(Student));
if (*students == NULL) {
printf("内存分配失败\n");
fclose(file);
return;
}

// 读取学生信息
fread(*students, sizeof(Student), *count, file);
fclose(file);
printf("学生信息已从文件加载\n");
}

总结

C语言中的结构体和联合体是强大的数据结构,通过本文档的学习,我们了解了:

  1. 结构体的基本概念:结构体是一种用户自定义的数据类型,可以包含不同类型的成员变量
  2. 结构体的定义与初始化:包括按顺序初始化和指定初始化器
  3. 结构体数组:存储多个结构体的数组
  4. 结构体指针:指向结构体的指针,使用箭头运算符访问成员
  5. 嵌套结构体:在结构体中包含另一个结构体作为成员
  6. 位域结构体:使用位域节省内存空间
  7. 联合体:所有成员共享同一块内存空间的特殊数据类型
  8. 枚举与结构体:枚举作为结构体成员的使用方法
  9. 结构体作为函数参数:值传递和指针传递的区别
  10. 结构体的高级应用:链表实现、文件I/O等
  11. 最佳实践:使用typedef简化声明、合理设计结构体成员、初始化结构体等

结构体和联合体在C语言中有着广泛的应用,它们可以帮助我们更好地组织和管理数据,提高代码的可读性和可维护性。通过合理使用结构体和联合体,我们可以编写出更加高效、清晰的C程序。

structs_unions.c

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
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
#include <stdio.h>
#include <string.h>

/**
* @brief 结构体和联合体示例程序
* @details 这个程序演示了C语言中结构体和联合体的各种用法,包括
* 结构体定义、初始化、结构体数组、结构体指针、嵌套结构体、
* 位域结构体以及联合体的定义和使用。
* @return 0 表示程序成功执行
*/

// =========================
// 结构体定义
// =========================

/**
* @brief 学生结构体
* @details 包含学生的基本信息:姓名、年龄、学号和成绩
*/
struct Student {
char name[50]; // 姓名
int age; // 年龄
int student_id; // 学号
float score; // 成绩
};

/**
* @brief 日期结构体
* @details 包含年、月、日信息
*/
struct Date {
int year;
int month;
int day;
};

/**
* @brief 员工结构体(嵌套结构体示例)
* @details 包含员工基本信息和入职日期
*/
struct Employee {
char name[50];
int employee_id;
float salary;
struct Date hire_date; // 嵌套日期结构体
};

/**
* @brief 位域结构体示例
* @details 使用位域来节省内存空间
*/
struct Flags {
unsigned int is_active : 1; // 1位:是否激活
unsigned int is_admin : 1; // 1位:是否管理员
unsigned int user_type : 2; // 2位:用户类型(0-3)
unsigned int permissions : 4; // 4位:权限标志
};

/**
* @brief 联合体示例
* @details 联合体的所有成员共享同一块内存空间
*/
union Data {
int i; // 整型
float f; // 浮点型
char str[20]; // 字符数组
};

/**
* @brief 枚举类型示例
* @details 与结构体结合使用的枚举类型
*/
enum Color {
RED, // 0
GREEN, // 1
BLUE, // 2
YELLOW, // 3
BLACK // 4
};

/**
* @brief 带有枚举成员的结构体
*/
struct Shape {
enum Color color;
float area;
char name[20];
};

// =========================
// 函数原型声明
// =========================
void print_student(struct Student s); // 打印学生信息
void update_student_score(struct Student *s, float new_score); // 更新学生成绩
void print_employee(struct Employee e); // 打印员工信息
void print_flags(struct Flags f); // 打印位域标志
void print_union(union Data d); // 打印联合体数据

/**
* @brief 主函数
* @return 0 表示程序成功执行
*/
int main() {
// =========================
// 结构体的基本使用
// =========================
printf("=== 结构体的基本使用 ===\n");

// 结构体变量声明
struct Student student1;

// 结构体成员赋值
strcpy(student1.name, "张三");
student1.age = 20;
student1.student_id = 1001;
student1.score = 95.5;

// 结构体变量初始化
struct Student student2 = {
"李四",
21,
1002,
88.0
};

// 使用指定初始化器(C99及以上)
struct Student student3 = {
.name = "王五",
.student_id = 1003,
.score = 92.5,
.age = 22
};

// 打印学生信息
printf("学生1信息:\n");
print_student(student1);

printf("\n学生2信息:\n");
print_student(student2);

printf("\n学生3信息:\n");
print_student(student3);

printf("\n");

// =========================
// 结构体数组
// =========================
printf("=== 结构体数组 ===\n");

// 结构体数组初始化
struct Student students[] = {
{"赵六", 20, 1004, 85.5},
{"孙七", 21, 1005, 90.0},
{"周八", 22, 1006, 78.5}
};

int num_students = sizeof(students) / sizeof(students[0]);

printf("结构体数组中的学生信息:\n");
for (int i = 0; i < num_students; i++) {
printf("\n学生 %d:\n", i + 1);
print_student(students[i]);
}

printf("\n");

// =========================
// 结构体指针
// =========================
printf("=== 结构体指针 ===\n");

// 结构体指针声明和初始化
struct Student *student_ptr = &student1;

// 通过指针访问结构体成员(两种方式)
printf("通过指针访问学生1信息:\n");
printf("姓名:%s\n", student_ptr->name); // 箭头运算符
printf("年龄:%d\n", (*student_ptr).age); // 解引用后使用点运算符

// 使用结构体指针更新成员
update_student_score(student_ptr, 98.0);
printf("更新后学生1成绩:%.1f\n", student1.score);

printf("\n");

// =========================
// 嵌套结构体
// =========================
printf("=== 嵌套结构体 ===\n");

// 初始化嵌套结构体
struct Employee emp = {
"王五",
2001,
5000.0,
{2020, 3, 15}
};

// 打印员工信息
print_employee(emp);

// 修改嵌套结构体成员
emp.hire_date.year = 2021;
printf("\n修改入职年份后:\n");
print_employee(emp);

printf("\n");

// =========================
// 位域结构体
// =========================
printf("=== 位域结构体 ===\n");

// 初始化位域结构体
struct Flags flags = {
1, // is_active = true
0, // is_admin = false
2, // user_type = 2
10 // permissions = 10 (二进制:1010)
};

print_flags(flags);

// 修改位域成员
flags.is_admin = 1;
printf("\n修改为管理员后:\n");
print_flags(flags);

// 显示位域结构体的大小
printf("\n位域结构体大小:%zu 字节\n", sizeof(struct Flags));

printf("\n");

// =========================
// 联合体
// =========================
printf("=== 联合体 ===\n");

// 联合体变量声明和初始化
union Data data;

// 赋值为整型
data.i = 123;
printf("联合体存储整型:\n");
print_union(data);

// 赋值为浮点型(会覆盖之前的整型值)
data.f = 3.14159;
printf("\n联合体存储浮点型:\n");
print_union(data);

// 赋值为字符串(会覆盖之前的浮点型值)
strcpy(data.str, "Hello, Union!");
printf("\n联合体存储字符串:\n");
print_union(data);

// 显示联合体的大小
printf("\n联合体大小:%zu 字节\n", sizeof(union Data));

printf("\n");

// =========================
// 枚举与结构体结合使用
// =========================
printf("=== 枚举与结构体结合使用 ===\n");

// 初始化带有枚举成员的结构体
struct Shape circle = {
RED,
78.5,
"Circle"
};

struct Shape rectangle = {
BLUE,
120.0,
"Rectangle"
};

printf("图形1:\n");
printf("名称:%s\n", circle.name);
printf("颜色:%d\n", circle.color); // 枚举值为整数
printf("面积:%.1f\n", circle.area);

printf("\n图形2:\n");
printf("名称:%s\n", rectangle.name);
printf("颜色:%d\n", rectangle.color);
printf("面积:%.1f\n", rectangle.area);

printf("\n");

// =========================
// 结构体作为函数参数
// =========================
printf("=== 结构体作为函数参数 ===\n");

// 通过值传递结构体
printf("通过值传递结构体:\n");
print_student(student1);

// 通过指针传递结构体(更高效,尤其是大型结构体)
printf("\n通过指针传递结构体更新成绩:\n");
printf("更新前成绩:%.1f\n", student1.score);
update_student_score(&student1, 99.5);
printf("更新后成绩:%.1f\n", student1.score);

return 0;
}

// =========================
// 函数定义
// =========================

/**
* @brief 打印学生信息
* @param s 学生结构体(值传递)
*/
void print_student(struct Student s) {
printf("姓名:%s\n", s.name);
printf("年龄:%d\n", s.age);
printf("学号:%d\n", s.student_id);
printf("成绩:%.1f\n", s.score);
}

/**
* @brief 更新学生成绩
* @param s 学生结构体指针(指针传递)
* @param new_score 新的成绩
*/
void update_student_score(struct Student *s, float new_score) {
s->score = new_score;
}

/**
* @brief 打印员工信息
* @param e 员工结构体
*/
void print_employee(struct Employee e) {
printf("姓名:%s\n", e.name);
printf("员工ID:%d\n", e.employee_id);
printf("工资:%.2f\n", e.salary);
printf("入职日期:%d-%d-%d\n", e.hire_date.year, e.hire_date.month, e.hire_date.day);
}

/**
* @brief 打印位域标志信息
* @param f 位域结构体
*/
void print_flags(struct Flags f) {
printf("is_active:%d\n", f.is_active);
printf("is_admin:%d\n", f.is_admin);
printf("user_type:%d\n", f.user_type);
printf("permissions:%d\n", f.permissions);
}

/**
* @brief 打印联合体数据
* @param d 联合体
* @note 由于联合体成员共享内存,只有最后赋值的成员值是有效的
*/
void print_union(union Data d) {
printf("整型值:%d\n", d.i);
printf("浮点值:%.2f\n", d.f);
printf("字符串:%s\n", d.str);
}

C语言指针

概述

本文档详细介绍C语言中指针的使用方法,包括指针的基本概念、指针算术、指针与数组的关系、指针与函数的关系、指针数组和数组指针、多级指针、const指针、空指针和野指针、指向函数的指针等内容。指针是C语言中最强大也最容易出错的特性之一,掌握指针的使用对于编写高效、灵活的C程序至关重要。

指针的基本概念

指针的定义

指针是一个变量,它存储的是另一个变量的内存地址。在C语言中,指针的声明语法如下:

1
类型 *指针变量名;

指针的初始化与使用

指针的初始化是将一个变量的地址赋值给指针变量,使用&运算符获取变量的地址。通过指针访问其指向的变量的值,称为”解引用”,使用*运算符。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 指针的基本概念
int num = 100;
int *ptr; // 声明一个指向int类型的指针

ptr = &num; // 初始化指针,指向变量num的地址

printf("变量num的值: %d\n", num);
printf("变量num的地址: %p\n", &num);
printf("指针ptr存储的地址: %p\n", ptr);
printf("指针ptr解引用得到的值: %d\n", *ptr);

// 通过指针修改变量的值
*ptr = 200;
printf("通过指针修改后num的值: %d\n", num);

补充示例:指针的大小

1
2
3
4
5
// 指针的大小
printf("int* 指针的大小: %zu 字节\n", sizeof(int*));
printf("char* 指针的大小: %zu 字节\n", sizeof(char*));
printf("double* 指针的大小: %zu 字节\n", sizeof(double*));
printf("void* 指针的大小: %zu 字节\n", sizeof(void*));

指针算术

指针算术的基本概念

指针算术是指对指针进行加减运算,指针算术的结果取决于指针所指向的类型的大小。例如,对于int*类型的指针,ptr++会使指针向前移动sizeof(int)个字节。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 指针算术
int arr[] = {10, 20, 30, 40, 50};
int *arr_ptr = arr; // 数组名是指向首元素的指针

printf("arr[0] = %d, *arr_ptr = %d\n", arr[0], *arr_ptr);
printf("arr[1] = %d, *(arr_ptr + 1) = %d\n", arr[1], *(arr_ptr + 1));
printf("arr[2] = %d, *(arr_ptr + 2) = %d\n", arr[2], *(arr_ptr + 2));

// 指针自增
arr_ptr++;
printf("arr_ptr++后, *arr_ptr = %d\n", *arr_ptr);

// 指针自减
arr_ptr--;
printf("arr_ptr--后, *arr_ptr = %d\n", *arr_ptr);

// 指针减法
int *ptr1 = &arr[4];
int *ptr2 = &arr[1];
printf("ptr1 - ptr2 = %ld (相差%d个元素)\n", ptr1 - ptr2, ptr1 - ptr2);

指针算术遍历数组

1
2
3
4
5
6
7
8
9
10
11
12
// 指针算术遍历数组
void pointer_arithmetic(int *arr, int size) {
printf("指针算术遍历数组: ");
for (int *p = arr; p < arr + size; p++) {
printf("%d ", *p);
}
printf("\n");
}

// 使用指针算术遍历数组
int numbers[] = {1, 2, 3, 4, 5};
pointer_arithmetic(numbers, 5);

补充示例:不同类型指针的算术运算

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 不同类型指针的算术运算
char char_arr[] = {'a', 'b', 'c', 'd', 'e'};
int int_arr[] = {1, 2, 3, 4, 5};

char *char_ptr = char_arr;
int *int_ptr = int_arr;

printf("char_ptr = %p\n", char_ptr);
char_ptr++;
printf("char_ptr++后 = %p (移动了%zu字节)\n", char_ptr, sizeof(char));

printf("int_ptr = %p\n", int_ptr);
int_ptr++;
printf("int_ptr++后 = %p (移动了%zu字节)\n", int_ptr, sizeof(int));

指针与数组

数组名作为指针

在C语言中,数组名可以看作是指向数组首元素的指针,因此可以使用指针的方式访问数组元素。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
// 指针与数组
int numbers[] = {1, 2, 3, 4, 5};
int *num_ptr = numbers;

printf("使用指针遍历数组: ");
for (int i = 0; i < 5; i++) {
printf("%d ", *(num_ptr + i));
}
printf("\n");

// 数组名作为指针
printf("numbers + 0 = %p, &numbers[0] = %p\n", numbers + 0, &numbers[0]);
printf("*(numbers + 2) = %d, numbers[2] = %d\n", *(numbers + 2), numbers[2]);

使用指针遍历数组

1
2
3
4
5
6
7
8
9
10
11
// 使用指针遍历数组
void print_array_with_pointer(int *arr, int size) {
for (int i = 0; i < size; i++) {
printf("%d ", *(arr + i));
}
printf("\n");
}

// 使用函数遍历数组
int array[] = {10, 20, 30, 40, 50};
print_array_with_pointer(array, 5);

补充示例:指针与二维数组

1
2
3
4
5
6
7
8
9
10
// 指针与二维数组
int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};

// 使用指针访问二维数组
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
printf("%d ", *(*(matrix + i) + j));
}
printf("\n");
}

指针与函数

指针作为函数参数

指针作为函数参数可以实现以下功能:

  1. 修改函数外部变量的值
  2. 传递大型数据结构,避免拷贝开销
  3. 实现多返回值

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 使用指针交换两个变量
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}

// 修改指针指向的值
void modify_value(int *ptr) {
*ptr = *ptr * 2;
}

// 使用交换函数
int x = 10, y = 20;
printf("交换前: x = %d, y = %d\n", x, y);
swap(&x, &y);
printf("交换后: x = %d, y = %d\n", x, y);

// 使用修改值函数
int value = 50;
printf("修改前value的值: %d\n", value);
modify_value(&value);
printf("修改后value的值: %d\n", value);

补充示例:指针作为函数返回值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 指针作为函数返回值
int *create_array(int size) {
int *arr = (int *)malloc(size * sizeof(int));
if (arr == NULL) {
return NULL;
}
// 初始化数组
for (int i = 0; i < size; i++) {
arr[i] = i + 1;
}
return arr;
}

// 使用函数返回的指针
int *dynamic_array = create_array(5);
if (dynamic_array != NULL) {
printf("动态数组: ");
print_array_with_pointer(dynamic_array, 5);
free(dynamic_array); // 释放内存
}

指针数组和数组指针

指针数组

指针数组是一个数组,其元素是指针。声明语法如下:

1
类型 *数组名[数组大小];

数组指针

数组指针是一个指针,它指向一个数组。声明语法如下:

1
类型 (*指针名)[数组大小];

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 指针数组:存储指针的数组
int a = 1, b = 2, c = 3;
int *ptr_array[] = {&a, &b, &c};

printf("指针数组元素值: ");
for (int i = 0; i < 3; i++) {
printf("%d ", *(ptr_array[i]));
}
printf("\n");

// 数组指针:指向数组的指针
int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};
int (*matrix_ptr)[3] = matrix; // 指向包含3个int元素的数组的指针

printf("数组指针访问二维数组: \n");
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
printf("%d ", *(*(matrix_ptr + i) + j));
}
printf("\n");
}

补充示例:字符串指针数组

1
2
3
4
5
6
7
8
9
10
11
12
// 字符串指针数组
char *strings[] = {
"Hello",
"World",
"C",
"Language"
};

printf("字符串指针数组: \n");
for (int i = 0; i < 4; i++) {
printf("%s\n", strings[i]);
}

多级指针

多级指针的定义

多级指针是指向指针的指针,例如二级指针是指向一级指针的指针,三级指针是指向二级指针的指针,以此类推。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 多级指针
int value2 = 1000;
int *ptr_level1 = &value2; // 一级指针
int **ptr_level2 = &ptr_level1; // 二级指针
int ***ptr_level3 = &ptr_level2; // 三级指针

printf("value2的值: %d\n", value2);
printf("*ptr_level1: %d\n", *ptr_level1);
printf("**ptr_level2: %d\n", **ptr_level2);
printf("***ptr_level3: %d\n", ***ptr_level3);

// 通过三级指针修改原始值
***ptr_level3 = 2000;
printf("通过三级指针修改后value2的值: %d\n", value2);

补充示例:二级指针与二维数组

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
// 二级指针与二维数组
int rows = 2, cols = 3;

// 使用二级指针创建二维数组
int **dynamic_matrix = (int **)malloc(rows * sizeof(int *));
for (int i = 0; i < rows; i++) {
dynamic_matrix[i] = (int *)malloc(cols * sizeof(int));
}

// 初始化二维数组
int count = 1;
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
dynamic_matrix[i][j] = count++;
}
}

// 打印二维数组
printf("动态二维数组: \n");
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%d ", dynamic_matrix[i][j]);
}
printf("\n");
}

// 释放内存
for (int i = 0; i < rows; i++) {
free(dynamic_matrix[i]);
}
free(dynamic_matrix);

const指针和指向const的指针

指向const的指针

指向const的指针是指不能通过指针修改所指向对象的值的指针,声明语法如下:

1
2
3
const 类型 *指针名;
// 或
类型 const *指针名;

const指针

const指针是指指针本身的地址不能改变的指针,声明语法如下:

1
类型 *const 指针名;

指向const的const指针

指向const的const指针是指既不能修改指针指向,也不能修改所指对象的值的指针,声明语法如下:

1
const 类型 *const 指针名;

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// const指针和指向const的指针
int const_val = 500;

// 指向const的指针:不能通过指针修改所指对象的值
const int *const_ptr = &const_val;
printf("const_ptr指向的值: %d\n", *const_ptr);
// *const_ptr = 600; // 错误:不能修改const对象的值

// const指针:指针本身的地址不能改变
int mutable_val = 700;
int *const fixed_ptr = &mutable_val;
printf("fixed_ptr指向的值: %d\n", *fixed_ptr);
*fixed_ptr = 800; // 可以修改所指对象的值
printf("修改后fixed_ptr指向的值: %d\n", *fixed_ptr);
// fixed_ptr = &const_val; // 错误:不能改变const指针的指向

// 指向const的const指针:既不能修改指针指向,也不能修改所指对象的值
const int *const const_fixed_ptr = &const_val;
printf("const_fixed_ptr指向的值: %d\n", *const_fixed_ptr);
// *const_fixed_ptr = 900; // 错误:不能修改所指对象的值
// const_fixed_ptr = &mutable_val; // 错误:不能改变指针指向

空指针和野指针

空指针

空指针是指向NULL的指针,NULL是一个宏定义,表示一个空地址。空指针通常用于初始化指针或表示指针不指向任何有效的内存位置。

野指针

野指针是指未初始化的指针,它指向随机的内存地址,使用野指针是非常危险的,可能会导致程序崩溃或数据损坏。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 空指针和野指针
// 空指针:指向NULL的指针
int *null_ptr = NULL;
printf("空指针的值: %p\n", null_ptr);

// 避免使用野指针
int *wild_ptr; // 未初始化的指针,指向随机地址
// printf("*wild_ptr = %d\n", *wild_ptr); // 危险:野指针解引用

// 正确做法:初始化指针或设置为NULL
int *safe_ptr = NULL;

// 使用指针前检查是否为NULL
if (safe_ptr != NULL) {
printf("*safe_ptr = %d\n", *safe_ptr);
} else {
printf("safe_ptr是NULL指针,不能解引用\n");
}

补充示例:指针的安全使用

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
// 指针的安全使用
int *ptr = NULL;
int value = 100;

// 分配内存
ptr = (int *)malloc(sizeof(int));
if (ptr == NULL) {
printf("内存分配失败\n");
return 1;
}

// 使用指针
*ptr = value;
printf("*ptr = %d\n", *ptr);

// 释放内存后将指针设置为NULL
free(ptr);
ptr = NULL;

// 再次检查指针
if (ptr != NULL) {
printf("*ptr = %d\n", *ptr);
} else {
printf("ptr已被设置为NULL\n");
}

指向函数的指针

函数指针的定义

函数指针是指向函数的指针,它可以存储函数的地址,并通过指针调用函数。声明语法如下:

1
返回类型 (*指针名)(参数类型1, 参数类型2, ...);

示例代码

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
// 指向函数的指针
// 声明指向函数的指针
int (*func_ptr)(int, int);

// 简单的加法函数
int add(int a, int b) {
return a + b;
}

// 简单的减法函数
int subtract(int a, int b) {
return a - b;
}

// 将函数地址赋值给指针
func_ptr = add;

// 使用指针调用函数
int result = func_ptr(10, 20);
printf("使用函数指针调用add(10, 20) = %d\n", result);

// 切换函数指针指向
func_ptr = subtract;
result = func_ptr(10, 20);
printf("使用函数指针调用subtract(10, 20) = %d\n", result);

补充示例:函数指针作为参数

1
2
3
4
5
6
7
8
9
10
11
12
// 函数指针作为参数
void calculate_and_print(int a, int b, int (*operation)(int, int)) {
int result = operation(a, b);
printf("结果: %d\n", result);
}

// 使用函数指针作为参数
printf("加法: ");
calculate_and_print(10, 20, add);

printf("减法: ");
calculate_and_print(10, 20, subtract);

补充示例:函数指针数组

1
2
3
4
5
6
// 函数指针数组
int (*operations[])(int, int) = {add, subtract};

printf("使用函数指针数组:\n");
printf("加法: %d\n", operations[0](10, 20));
printf("减法: %d\n", operations[1](10, 20));

指针的高级应用

动态内存分配

指针与动态内存分配密切相关,使用malloccallocreallocfree函数可以在运行时分配和释放内存。

示例代码

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
// 动态内存分配
int *dynamic_arr = NULL;
int size = 5;

// 分配内存
dynamic_arr = (int *)malloc(size * sizeof(int));
if (dynamic_arr == NULL) {
printf("内存分配失败\n");
return 1;
}

// 初始化动态数组
for (int i = 0; i < size; i++) {
dynamic_arr[i] = i + 1;
}

// 打印动态数组
printf("动态数组: ");
print_array_with_pointer(dynamic_arr, size);

// 重新分配内存
size = 10;
dynamic_arr = (int *)realloc(dynamic_arr, size * sizeof(int));
if (dynamic_arr == NULL) {
printf("内存重新分配失败\n");
return 1;
}

// 初始化新分配的内存
for (int i = 5; i < size; i++) {
dynamic_arr[i] = i + 1;
}

// 打印重新分配后的动态数组
printf("重新分配后的动态数组: ");
print_array_with_pointer(dynamic_arr, size);

// 释放内存
free(dynamic_arr);
dynamic_arr = NULL; // 避免野指针

指针与结构体

指针可以指向结构体,使用指针访问结构体成员可以提高访问效率,特别是对于大型结构体。

示例代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 指针与结构体
typedef struct {
int id;
char name[50];
float score;
} Student;

// 使用指针访问结构体
Student student = {1, "张三", 95.5};
Student *student_ptr = &student;

printf("学生信息:\n");
printf("ID: %d\n", student_ptr->id);
printf("姓名: %s\n", student_ptr->name);
printf("分数: %.1f\n", student_ptr->score);

// 修改结构体成员
student_ptr->score = 98.0;
printf("修改后的分数: %.1f\n", student.score);

补充示例:链表的实现

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
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
// 链表的实现
typedef struct Node {
int data;
struct Node *next;
} Node;

// 创建新节点
Node *create_node(int data) {
Node *new_node = (Node *)malloc(sizeof(Node));
if (new_node == NULL) {
return NULL;
}
new_node->data = data;
new_node->next = NULL;
return new_node;
}

// 添加节点到链表尾部
void append_node(Node **head, int data) {
Node *new_node = create_node(data);
if (new_node == NULL) {
return;
}

if (*head == NULL) {
*head = new_node;
return;
}

Node *current = *head;
while (current->next != NULL) {
current = current->next;
}
current->next = new_node;
}

// 打印链表
void print_list(Node *head) {
Node *current = head;
while (current != NULL) {
printf("%d -> ", current->data);
current = current->next;
}
printf("NULL\n");
}

// 释放链表
void free_list(Node *head) {
Node *temp;
while (head != NULL) {
temp = head;
head = head->next;
free(temp);
}
}

// 使用链表
Node *head = NULL;
append_node(&head, 10);
append_node(&head, 20);
append_node(&head, 30);
append_node(&head, 40);

printf("链表: ");
print_list(head);

// 释放链表
free_list(head);

指针的常见错误

常见错误及解决方案

  1. 空指针解引用:尝试访问NULL指针指向的内存

    • 解决方案:使用指针前检查是否为NULL
  2. 野指针:使用未初始化的指针

    • 解决方案:总是初始化指针,要么指向有效内存,要么设置为NULL
  3. 内存泄漏:动态分配的内存没有释放

    • 解决方案:使用free函数释放动态分配的内存
  4. 指针越界:访问超出数组范围的内存

    • 解决方案:确保指针操作在有效范围内
  5. 释放后使用:使用已释放的内存

    • 解决方案:释放内存后将指针设置为NULL
  6. 常量指针修改:尝试修改const指针指向的值

    • 解决方案:尊重const修饰符,不要尝试修改const对象
  7. 指针类型不匹配:将一种类型的指针赋值给另一种类型的指针

    • 解决方案:使用正确的类型转换,确保类型安全
  8. 多级指针错误:多级指针的解引用层级错误

    • 解决方案:确保多级指针的解引用层级与指针声明匹配

指针的最佳实践

  1. 始终初始化指针:声明指针时立即初始化,要么指向有效内存,要么设置为NULL
  2. 使用指针前检查是否为NULL:避免空指针解引用
  3. 释放动态内存:使用free函数释放动态分配的内存
  4. 释放内存后将指针设置为NULL:避免野指针
  5. 使用const修饰符:对于不需要修改的指针或指向的值,使用const修饰
  6. 使用有意义的变量名:指针变量名应清晰表达其用途
  7. 避免过多的指针层级:通常不超过2-3级指针
  8. 使用指针算术时要小心:确保指针操作在有效范围内
  9. 使用函数指针提高代码灵活性:对于需要动态选择函数的场景
  10. 学习指针与数组、函数的关系:深入理解C语言的底层机制

综合示例

示例1:使用指针实现字符串反转

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
// 使用指针实现字符串反转
void reverse_string(char *str) {
if (str == NULL) {
return;
}

char *start = str;
char *end = str + strlen(str) - 1;
char temp;

while (start < end) {
// 交换字符
temp = *start;
*start = *end;
*end = temp;

// 移动指针
start++;
end--;
}
}

// 使用字符串反转函数
char str[] = "Hello, World!";
printf("原始字符串: %s\n", str);
reverse_string(str);
printf("反转后: %s\n", str);

示例2:使用指针实现快速排序

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
44
45
46
47
48
49
// 使用指针实现快速排序
void quick_sort(int *arr, int left, int right) {
if (left >= right) {
return;
}

int pivot = arr[left];
int i = left + 1;
int j = right;
int temp;

while (i <= j) {
while (i <= right && arr[i] < pivot) {
i++;
}
while (j >= left && arr[j] > pivot) {
j--;
}
if (i < j) {
// 交换元素
temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
} else {
break;
}
}

// 交换pivot和arr[j]
temp = arr[left];
arr[left] = arr[j];
arr[j] = temp;

// 递归排序
quick_sort(arr, left, j - 1);
quick_sort(arr, j + 1, right);
}

// 使用快速排序
int numbers[] = {5, 2, 9, 1, 5, 6};
int size = sizeof(numbers) / sizeof(numbers[0]);

printf("排序前: ");
print_array_with_pointer(numbers, size);

quick_sort(numbers, 0, size - 1);

printf("排序后: ");
print_array_with_pointer(numbers, size);

总结

C语言中的指针是一种强大的特性,通过本文档的学习,我们了解了:

  1. 指针的基本概念:指针是存储内存地址的变量,可以通过指针访问和修改内存中的数据
  2. 指针算术:指针可以进行加减运算,运算结果取决于指针所指向的类型
  3. 指针与数组:数组名可以看作是指向数组首元素的指针,指针可以用于遍历和操作数组
  4. 指针与函数:指针可以作为函数参数和返回值,实现参数的引用传递和动态内存分配
  5. 指针数组和数组指针:指针数组是存储指针的数组,数组指针是指向数组的指针
  6. 多级指针:指向指针的指针,可以用于更复杂的数据结构
  7. const指针:用于保护数据不被修改,提高代码的安全性
  8. 空指针和野指针:空指针指向NULL,野指针是未初始化的指针,应避免使用野指针
  9. 指向函数的指针:可以存储函数的地址,实现函数的动态调用
  10. 指针的高级应用:动态内存分配、链表实现、字符串操作等

掌握指针的使用对于编写高效、灵活的C程序至关重要,但同时也需要小心使用,避免常见的指针错误。通过不断练习和实践,你将能够熟练运用指针来解决各种编程问题。

pointers.c

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
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
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
#include <stdio.h>
#include <stdlib.h>

/**
* @brief 指针示例程序
* @details 这个程序演示了C语言中指针的各种用法,包括指针声明、
* 初始化、解引用、指针算术、指针与数组关系、指针与函数关系
* 以及多级指针等高级用法。
* @return 0 表示程序成功执行
*/

// =========================
// 函数原型声明
// =========================
void swap(int *a, int *b); // 使用指针交换两个变量
void print_array_with_pointer(int *arr, int size); // 使用指针遍历数组
void modify_value(int *ptr); // 修改指针指向的值
void pointer_arithmetic(int *arr, int size); // 指针算术示例

/**
* @brief 主函数
* @return 0 表示程序成功执行
*/
int main() {
// =========================
// 指针的基本概念
// =========================
printf("=== 指针的基本概念 ===\n");

int num = 100;
int *ptr; // 声明一个指向int类型的指针

ptr = &num; // 初始化指针,指向变量num的地址

printf("变量num的值: %d\n", num);
printf("变量num的地址: %p\n", &num);
printf("指针ptr存储的地址: %p\n", ptr);
printf("指针ptr解引用得到的值: %d\n", *ptr);

// 通过指针修改变量的值
*ptr = 200;
printf("通过指针修改后num的值: %d\n", num);

printf("\n");

// =========================
// 指针算术
// =========================
printf("=== 指针算术 ===\n");

int arr[] = {10, 20, 30, 40, 50};
int *arr_ptr = arr; // 数组名是指向首元素的指针

printf("arr[0] = %d, *arr_ptr = %d\n", arr[0], *arr_ptr);
printf("arr[1] = %d, *(arr_ptr + 1) = %d\n", arr[1], *(arr_ptr + 1));
printf("arr[2] = %d, *(arr_ptr + 2) = %d\n", arr[2], *(arr_ptr + 2));

// 指针自增
arr_ptr++;
printf("arr_ptr++后, *arr_ptr = %d\n", *arr_ptr);

// 指针自减
arr_ptr--;
printf("arr_ptr--后, *arr_ptr = %d\n", *arr_ptr);

// 指针减法
int *ptr1 = &arr[4];
int *ptr2 = &arr[1];
printf("ptr1 - ptr2 = %ld (相差%d个元素)\n", ptr1 - ptr2, ptr1 - ptr2);

// 调用指针算术函数
pointer_arithmetic(arr, 5);

printf("\n");

// =========================
// 指针与数组
// =========================
printf("=== 指针与数组 ===\n");

int numbers[] = {1, 2, 3, 4, 5};
int *num_ptr = numbers;

printf("使用指针遍历数组: ");
print_array_with_pointer(num_ptr, 5);

// 数组名作为指针
printf("numbers + 0 = %p, &numbers[0] = %p\n", numbers + 0, &numbers[0]);
printf("*(numbers + 2) = %d, numbers[2] = %d\n", *(numbers + 2), numbers[2]);

printf("\n");

// =========================
// 指针与函数
// =========================
printf("=== 指针与函数 ===\n");

int x = 10, y = 20;
printf("交换前: x = %d, y = %d\n", x, y);

// 传递地址给函数,实现交换
swap(&x, &y);
printf("交换后: x = %d, y = %d\n", x, y);

// 修改指针指向的值
int value = 50;
printf("修改前value的值: %d\n", value);
modify_value(&value);
printf("修改后value的值: %d\n", value);

printf("\n");

// =========================
// 指针数组和数组指针
// =========================
printf("=== 指针数组和数组指针 ===\n");

// 指针数组:存储指针的数组
int a = 1, b = 2, c = 3;
int *ptr_array[] = {&a, &b, &c};

printf("指针数组元素值: ");
for (int i = 0; i < 3; i++) {
printf("%d ", *(ptr_array[i]));
}
printf("\n");

// 数组指针:指向数组的指针
int matrix[2][3] = {{1, 2, 3}, {4, 5, 6}};
int (*matrix_ptr)[3] = matrix; // 指向包含3个int元素的数组的指针

printf("数组指针访问二维数组: \n");
for (int i = 0; i < 2; i++) {
for (int j = 0; j < 3; j++) {
printf("%d ", *(*(matrix_ptr + i) + j));
}
printf("\n");
}

printf("\n");

// =========================
// 多级指针
// =========================
printf("=== 多级指针 ===\n");

int value2 = 1000;
int *ptr_level1 = &value2;
int **ptr_level2 = &ptr_level1; // 二级指针
int ***ptr_level3 = &ptr_level2; // 三级指针

printf("value2的值: %d\n", value2);
printf("*ptr_level1: %d\n", *ptr_level1);
printf("**ptr_level2: %d\n", **ptr_level2);
printf("***ptr_level3: %d\n", ***ptr_level3);

// 通过三级指针修改原始值
***ptr_level3 = 2000;
printf("通过三级指针修改后value2的值: %d\n", value2);

printf("\n");

// =========================
// const指针和指向const的指针
// =========================
printf("=== const指针和指向const的指针 ===\n");

int const_val = 500;

// 指向const的指针:不能通过指针修改所指对象的值
const int *const_ptr = &const_val;
printf("const_ptr指向的值: %d\n", *const_ptr);
// *const_ptr = 600; // 错误:不能修改const对象的值

// const指针:指针本身的地址不能改变
int mutable_val = 700;
int *const fixed_ptr = &mutable_val;
printf("fixed_ptr指向的值: %d\n", *fixed_ptr);
*fixed_ptr = 800; // 可以修改所指对象的值
printf("修改后fixed_ptr指向的值: %d\n", *fixed_ptr);
// fixed_ptr = &const_val; // 错误:不能改变const指针的指向

// 指向const的const指针:既不能修改指针指向,也不能修改所指对象的值
const int *const const_fixed_ptr = &const_val;
printf("const_fixed_ptr指向的值: %d\n", *const_fixed_ptr);

printf("\n");

// =========================
// 空指针和野指针
// =========================
printf("=== 空指针和野指针 ===\n");

// 空指针:指向NULL的指针
int *null_ptr = NULL;
printf("空指针的值: %p\n", null_ptr);

// 避免使用野指针
int *wild_ptr; // 未初始化的指针,指向随机地址
// printf("*wild_ptr = %d\n", *wild_ptr); // 危险:野指针解引用

// 正确做法:初始化指针或设置为NULL
int *safe_ptr = NULL;

printf("\n");

// =========================
// 指向函数的指针
// =========================
printf("=== 指向函数的指针 ===\n");

// 声明指向函数的指针
int (*func_ptr)(int, int);

// 简单的加法函数
int add(int a, int b) {
return a + b;
}

// 将函数地址赋值给指针
func_ptr = add;

// 使用指针调用函数
int result = func_ptr(10, 20);
printf("使用函数指针调用add(10, 20) = %d\n", result);

return 0;
}

// =========================
// 函数定义
// =========================

/**
* @brief 使用指针交换两个变量的值
* @param a 指向第一个变量的指针
* @param b 指向第二个变量的指针
*/
void swap(int *a, int *b) {
int temp = *a;
*a = *b;
*b = temp;
}

/**
* @brief 使用指针遍历数组
* @param arr 指向数组首元素的指针
* @param size 数组的大小
*/
void print_array_with_pointer(int *arr, int size) {
for (int i = 0; i < size; i++) {
printf("%d ", *(arr + i));
}
printf("\n");
}

/**
* @brief 修改指针指向的值
* @param ptr 指向要修改的变量的指针
*/
void modify_value(int *ptr) {
*ptr = *ptr * 2;
}

/**
* @brief 演示指针算术操作
* @param arr 指向数组首元素的指针
* @param size 数组的大小
*/
void pointer_arithmetic(int *arr, int size) {
printf("指针算术遍历数组: ");
for (int *p = arr; p < arr + size; p++) {
printf("%d ", *p);
}
printf("\n");
}