青江的个人站

“保持热爱,奔赴星海”

  • 主页
  • 目录
  • 图床
  • 留言板
  • -关于我-
友链 搜索文章 >>

青江的个人站

“保持热爱,奔赴星海”

  • 主页
  • 目录
  • 图床
  • 留言板
  • -关于我-

【C语言学习笔记】十六、泛型与综合实践


阅读数: 0次    2026-03-25
字数:16.5k字 | 预计阅读时长:72分钟

1. 再谈头文件与编译

头文件类似于一个索引,当在一个文件中引入一个头文件时,这个文件就可以直接调用头文件中定义过的函数。

头文件编写需要注意:

  1. 组织性:通过头文件可以清晰组织其他文件,一个头文件应该专注处理特定部分的内容;
  2. 重用性:一个头文件中的函数功能,在其他项目或功能中也可以引入并使用;
  3. 编译效率:文件非常多时,应当注意把声明函数放在头文件中,把实现函数放在源文件中,提高增量编译效率;
  4. 避免重复包含:避免出现重定义函数。

2. 编写头文件:函数声明和函数实现

编译时,会先编译头文件,如果头文件没有发生变化,则跳过其编译,可以节省资源和时间。

函数在.h文件中声明,在.c文件中定义。在Visual Studio中,可以在“Header Files”中添加.h头,在“Source Files”中添加.c文件。

规范的头文件需要前后写#ifndef与#endif等宏定义,例如math_operations.h文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#ifndef MATH_OPERATIONS_H
#define MATH_OPERATIONS_H

#include <stdint.h>

// 基本数学运算函数声明(供外部源文件调用)
//
// 参数:
// - a: 第一个操作数
// - b: 第二个操作数
//
// 返回:
// - add/subtract/multiply 返回 int32_t 结果
// - divide 返回 double 结果(用于保留小数)
int32_t add(int32_t a, int32_t b); // 加法:a + b
int32_t subtract(int32_t a, int32_t b); // 减法:a - b
int32_t multiply(int32_t a, int32_t b); // 乘法:a * b
double divide(int32_t a, int32_t b); // 除法:a / b

#endif // !MATH_OPERATIONS_H

写标准宏定义的目的是做头文件保护(include guard),防止math_operations.h被重复包含导致重定义错误。

作用流程:

  1. 第一次包含时,MATH_OPERATIONS_H未定义,进入文件内容并定义它。
  2. 再次包含时,MATH_OPERATIONS_H已定义,整个头文件内容被跳过。

在Visual Studio里也可用#pragma once达到同类目的。

通常来说,标准的头文件前面需要有规范的注释,包括头文件名、公司名、头文件作用等。

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
/**
* @file math_operations.h
* @brief 提供基础数学运算函数声明。
* @author HC
* @version 1.0
* @date 2026-03-18
*/

#pragma once

#include <stdint.h>

/**
* @brief 计算两个整数的和。
* @param a 第一个操作数。
* @param b 第二个操作数。
* @return a + b。
*/
int32_t add(int32_t a, int32_t b);

/**
* @brief 计算两个整数的差。
* @param a 第一个操作数。
* @param b 第二个操作数。
* @return a - b。
*/
int32_t subtract(int32_t a, int32_t b);

/**
* @brief 计算两个整数的积。
* @param a 第一个操作数。
* @param b 第二个操作数。
* @return a * b。
*/
int32_t multiply(int32_t a, int32_t b);

/**
* @brief 计算两个整数相除的结果。
* @param a 第一个操作数(被除数)。
* @param b 第二个操作数(除数)。
* @return a / b 的浮点结果。
*/
double divide(int32_t a, int32_t b);

在同名.c文件中,需要写被定义的函数的具体实现,此时需要先引入对应的头文件,引入自定义编写的头文件时要用双引号。例如math_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
#include "math_operations.h"

/**
* @brief 计算两个整数的和。
* @param a 第一个操作数。
* @param b 第二个操作数。
* @return a + b。
*/
int32_t add(int32_t a, int32_t b) {
return a + b;
}

/**
* @brief 计算两个整数的差。
* @param a 第一个操作数。
* @param b 第二个操作数。
* @return a - b。
*/
int32_t subtract(int32_t a, int32_t b) {
return a - b;
}

/**
* @brief 计算两个整数的积。
* @param a 第一个操作数。
* @param b 第二个操作数。
* @return a * b。
*/
int32_t multiply(int32_t a, int32_t b) {
return a * b;
}

/**
* @brief 计算两个整数相除的结果。
* @param a 第一个操作数(被除数)。
* @param b 第二个操作数(除数)。
* @return 若 b 为 0,返回 0.0;否则返回 (double)a / (double)b。
*/
double divide(int32_t a, int32_t b) {
if (b == 0) {
return 0.0;
}
return (double)a / (double)b;
}

在其他文件中想要调用这些函数,需要在对应的文件中包含头文件,同样使用双引号。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdlib.h>

// 包含数学运算函数的头文件
#include "math_operations.h"

int main() {
int32_t a = 10;
int32_t b = 5;

printf("add: %" PRId32 "\n", add(a, b));
printf("subtract: %" PRId32 "\n", subtract(a, b));
printf("multiply: %" PRId32 "\n", multiply(a, b));
printf("divide: %f\n", divide(a, b));

return EXIT_SUCCESS;
}

3. 泛型编程:比较与排序

设计概述:

  1. 抽象层
  2. 策略模式
  3. 模块化
  4. 安全性和跨平台

泛型编程可以编写完全一般化并可重复使用的算法,其效率与针对某特定数据类型而设计的算法相同。

例如要编写具有比较两个数大小功能的函数,按照以前的写法,比较整型和浮点型的函数需要分别写,这样两个函数实现的功能有重复,效率低下。这时使用泛型编程可以解决这个问题,使代码具有高可用性。

在泛型编程中,将参数写成无类型指针void*类型,代表此参数可以为任意类型。

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
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdlib.h>

// 比较函数声明:按 int32_t 解释 a、b,返回 -1/0/1
int compare_value(const void* a, const void* b);

// 输出比较结果声明
void print_compare_result(int result);

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

puts("比较结果:");
print_compare_result(compare_value(&x, &y));

return EXIT_SUCCESS;
}

// 以 int32_t 形式为例,强制转换 void* 参数为 int32_t*,解引用后比较值大小
// 比较两个 int32_t 值:a < b 返回 -1,a > b 返回 1,相等返回 0
int compare_value(const void* a, const void* b) {
const int32_t va = *(const int32_t*)a;
const int32_t vb = *(const int32_t*)b;

if (va < vb) {
return -1;
}
if (va > vb) {
return 1;
}
return 0;
}

// 根据 compare_value 的返回值输出可读结果
void print_compare_result(int result) {
if (result < 0) {
puts("第一个数 < 第二个数");
}
else if (result > 0) {
puts("第一个数 > 第二个数");
}
else {
puts("第一个数 == 第二个数");
}
}

可以使用泛型编程来实现对任意类型的数组进行排序,排序函数传入泛型数组、元素大小、元素个数与指向与比较函数类型相同的函数指针。

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
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdlib.h>

// 比较函数声明:按 int32_t 解释 a、b,返回 -1/0/1
int compare_value(const void* a, const void* b);

// 通用排序函数声明:接收数组起始地址、元素大小、元素个数和比较函数
void generic_sort(void* array, size_t element_size, size_t element_count, int (*compare)(const void*, const void*));

// 输出比较结果声明
void print_compare_result(int result);

int main() {
// 两个值比较示例
int32_t x = 10;
int32_t y = 20;

// 待排序数组
int32_t array[] = { 5, 2, 9, 1, 5, 6 };

puts("比较结果:");
print_compare_result(compare_value(&x, &y));

puts("排序前的数组:");
for (size_t i = 0; i < sizeof(array) / sizeof(array[0]); i++) {
printf("%" PRId32 " ", array[i]);
}

// 通过通用排序函数 + 比较函数完成排序
generic_sort(array, sizeof(int32_t), sizeof(array) / sizeof(array[0]), compare_value);

puts("\n排序后的数组:");
for (size_t i = 0; i < sizeof(array) / sizeof(array[0]); i++) {
printf("%" PRId32 " ", array[i]);
}

return EXIT_SUCCESS;
}

// 以 int32_t 形式为例:将 void* 转为 int32_t* 后解引用并比较
// 约定返回值:a < b 返回 -1,a > b 返回 1,相等返回 0
int compare_value(const void* a, const void* b) {
const int32_t va = *(const int32_t*)a;
const int32_t vb = *(const int32_t*)b;

if (va < vb) {
return -1;
}
if (va > vb) {
return 1;
}
return 0;
}

// 通用排序函数:内部调用标准库 qsort
void generic_sort(void* array, size_t element_size, size_t element_count, int (*compare)(const void*, const void*)) {
qsort(array, element_count, element_size, compare);
}

// 根据 compare_value 的返回值输出可读结果
void print_compare_result(int result) {
if (result < 0) {
puts("第一个数 < 第二个数");
}
else if (result > 0) {
puts("第一个数 > 第二个数");
}
else {
puts("第一个数 == 第二个数");
}
}

qsort是C标准库提供的通用排序函数,可对任意类型数组排序,它通过“比较函数”决定排序规则(升序、降序、按某字段排序等)。名字来自“quick sort”,但标准并不要求必须使用快速排序实现。

函数原型:

1
2
3
4
5
6
void qsort(
void* base,
size_t nmemb,
size_t size,
int (*compar)(const void*, const void*)
);
  • base:数组首地址
  • nmemb:元素个数
  • size:每个元素大小(字节)
  • compar:比较函数

比较函数返回规则(比较函数必须返回):

  • <0:前者排在后者前面
  • =0:两者相等
  • >0:前者排在后者后面

4. 企业案例:自定义函数处理比较器

这个案例将创建几个自定义的比较函数,用来根据一定的规则对输入的数组进行排序。

使用泛型编程可以实现一个排序函数接收多种不同类型的数组,提高了代码的重用性。

某些时候架构师会预留一些参数,例如比较函数指针定义中,除默认的两个比较值外,可以多写一个泛型参数context,作为用户自定义的上下文,可以在比较函数中使用,在需要时提供额外的信息或参数。

如果不需要使用context参数,可以直接传递NULL。

在sort.h文件中,定义比较函数指针与声明排序函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#pragma once

// 比较函数类型:
// - a、b 指向待比较的两个元素
// - context 为可选上下文,可传入排序所需的额外参数
// 返回值约定:
// - < 0:a 排在 b 前
// - = 0:a 与 b 等价
// - > 0:a 排在 b 后
typedef int (*CompareFunc)(const void*, const void*, void* context);

// 通用排序函数声明:
// - array: 数组首地址
// - length: 元素个数
// - size: 单个元素大小(字节)
// - compare: 比较函数
// - context: 透传给 compare 的上下文参数
void genericSort(void* array, size_t length, size_t size, CompareFunc compare, void* context);

在person.h中,定义需要排序的Person结构体:

1
2
3
4
5
6
7
8
9
#pragma once

// 人员信息结构体
// - name: 姓名(最多存储 49 个可见字符,最后一位为字符串结束符 '\0')
// - age: 年龄
typedef struct {
char name[50];
int age;
} Person;

在compare.h中,声明各种需要用到的比较函数,函数结构要和sort.h文件中定义的函数指针类型匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#pragma once

// 整数比较函数:按 int 值升序比较
// 返回值约定:< 0 表示 a 在前,= 0 表示相等,> 0 表示 b 在前
int intCompare(const void* a, const void* b, void* context);

// 字符串比较函数:按字典序升序比较
// a、b 通常指向字符串元素(如 char* 或字符数组元素)
int stringCompare(const void* a, const void* b, void* context);

// Person 比较函数:按 name 字段升序比较
int personCompareByName(const void* a, const void* b, void* context);

// Person 比较函数:按 age 字段升序比较
int personCompareByAge(const void* a, const void* b, void* context);

在compare.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
#include "compare.h"
#include "person.h"

// 提供 NULL 定义(用于未使用 context 时传入空指针)
#include <stddef.h>
// 提供 strcmp,用于字符串字典序比较
#include <string.h>

// 比较两个 int 值,返回 -1 / 0 / 1
int intCompare(const void* a, const void* b, void* context) {
(void)context; // 未使用的参数
// 强制类型转换并解引用,获取 int 值
int intA = *(const int*)a;
int intB = *(const int*)b;
return (intA > intB) - (intA < intB); // 返回 -1、0 或 1
}

// 比较两个字符串(字典序)
// 这里 a、b 是“指向字符串指针”的指针(如 const char* 数组元素)
int stringCompare(const void* a, const void* b, void* context) {
(void)context; // 未使用的参数
// 解引用两层:先得到字符串指针,再交给 strcmp 按字典序比较
const char* strA = *(const char**)a;
const char* strB = *(const char**)b;
return strcmp(strA, strB); // 返回 <0、0 或 >0
}

// 按 Person.name 比较(直接使用 strcmp)
int personCompareByName(const void* a, const void* b, void* context) {
(void)context; // 未使用的参数
const Person* personA = (const Person*)a;
const Person* personB = (const Person*)b;
return strcmp(personA->name, personB->name); // 按 name 字段比较
}

// 按 Person.age 比较
int personCompareByAge(const void* a, const void* b, void* context) {
(void)context; // 未使用的参数
const Person* personA = (const Person*)a;
const Person* personB = (const Person*)b;
return intCompare(&personA->age, &personB->age, NULL); // 按 age 字段比较
}

在使用NULL时,需要包含stddef.h头文件。

最后,在sort.c文件中实现通用排序函数,需要考虑跨平台运行的问题,Windows平台有排序函数qsort_s而其他平台需要用qsort_r函数进行排序。

qsort_s是带“用户上下文”的通用排序函数,相比qsort多了一个context参数,便于比较函数拿到额外信息。

函数签名为:

1
void qsort_s(void* base, rsize_t num, rsize_t size, compare_fn cmp, void* context);

参数含义:

  • base:数组首地址(void*,可排序任意类型)
  • num:元素个数
  • size:每个元素字节数
  • cmp:比较函数
  • context:透传给比较函数的用户上下文

比较函数的签名是:

1
int cmp(void* context, const void* a, const void* b);

返回值约定:

  • <0:a 在前
  • =0:等价
  • >0:b 在前

工作原理:

  1. qsort_s接收base/num/size/cmp/context
  2. 排序算法内部不断挑两个元素比较
  3. 每次比较时,调用:cmp(context, elemA, elemB)
  4. elemA/elemB是指向数组元素的地址(const void*)
  5. 比较函数里把它们强转回真实类型再比较

因此,完整的排序实现为:

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
#include "sort.h"

#include <stdlib.h>

// 内部上下文:将比较函数与用户上下文一起传递给 qsort_s 的回调。
typedef struct {
CompareFunc compare;
void* userContext;
} SortContext;

// 适配器函数(供 qsort_s 使用):
// 将 sortContext 中保存的 compare 与 userContext 还原后转调。
static int compareWrapper(void* context, const void* a, const void* b) {
SortContext* ctx = (SortContext*)context;
return ctx->compare(a, b, ctx->userContext);
}

// 通用排序实现:
// - Windows 下使用 qsort_s(通过 compareWrapper 适配回调签名)
// - 其他平台使用 qsort_r(直接传入 compare 与 context)
void genericSort(void* array, size_t length, size_t size, CompareFunc compare, void* context) {
// 前置条件(卫语句):
// - array 不能为空
// - compare 不能为空
// - size 必须大于 0
// - length 小于 2 时无需排序
if (array == NULL || compare == NULL || size == 0 || length < 2) {
return;
}

// 将 compare 与 context 打包,供 compareWrapper 在回调时取出使用。
SortContext sortContext = { compare, context };

#ifdef _WIN32
qsort_s(array, length, size, compareWrapper, &sortContext);
#else
qsort_r(array, length, size, compare, context);
#endif
}

在main函数中,包含需要的头文件后即可使用排序函数对多种类型的数组进行排序:

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
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

#include "sort.h"
#include "compare.h"
#include "person.h"

int main() {
// 1) 整数数组排序示例
int intArray[] = { 5, 2, 9, 1, 5, 6 };
size_t intLength = sizeof(intArray) / sizeof(intArray[0]);

printf("[int] 排序前: ");
for (size_t i = 0; i < intLength; i++) {
printf("%d ", intArray[i]);
}
printf("\n");

genericSort(intArray, intLength, sizeof(intArray[0]), intCompare, NULL);

printf("[int] 排序后: ");
for (size_t i = 0; i < intLength; i++) {
printf("%d ", intArray[i]);
}
printf("\n");

// 2) 字符串数组排序示例(字典序)
const char* stringArray[] = { "banana", "apple", "cherry", "date" };
size_t stringLength = sizeof(stringArray) / sizeof(stringArray[0]);

printf("[string] 排序前: ");
for (size_t i = 0; i < stringLength; i++) {
printf("%s ", stringArray[i]);
}
printf("\n");

genericSort(stringArray, stringLength, sizeof(stringArray[0]), stringCompare, NULL);

printf("[string] 排序后: ");
for (size_t i = 0; i < stringLength; i++) {
printf("%s ", stringArray[i]);
}
printf("\n");

// 3) 结构体数组排序示例(按姓名或年龄)
Person people[] = {
{ "Alice", 30 },
{ "Bob", 25 },
{ "Charlie", 35 }
};
size_t peopleLength = sizeof(people) / sizeof(people[0]);
CompareFunc compareFunc[] = { personCompareByName, personCompareByAge };

bool sortByName = false; // 切换排序方式
printf("[person] 当前排序字段: %s\n", sortByName ? "name" : "age");

printf("[person] 排序前: ");
for (size_t i = 0; i < peopleLength; i++) {
printf("%s (%d) ", people[i].name, people[i].age);
}
printf("\n");

genericSort(people, peopleLength, sizeof(people[0]), sortByName ? compareFunc[0] : compareFunc[1], NULL);

printf("[person] 排序后: ");
for (size_t i = 0; i < peopleLength; i++) {
printf("%s (%d) ", people[i].name, people[i].age);
}
printf("\n");

return EXIT_SUCCESS;
}

这样,就实现了一个比较规范的自定义函数处理比较器。

5. 指针的作用域和生命周期

在函数中定义的是局部指针,只能在函数内部使用,这个函数运行结束后,这个指针就会被销毁。

在文件开始,函数之外声明的指针为全局指针,可以在整个文件中被赋值与使用。

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
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdlib.h>

// 全局指针变量:其作用域覆盖整个文件,生命周期贯穿程序运行期
uint32_t* global_ptr;

// 演示函数:包含局部变量、局部指针和动态内存分配
void function() {
// 局部变量:只在 function 内有效
uint32_t local_var = 42;
// 局部指针:指向 local_var,随函数结束一起失效
uint32_t* local_ptr = &local_var;
printf("Local variable value: %" PRIu32 "\n", *local_ptr);

// 在堆上分配一个 uint32_t,并把地址保存到全局指针
global_ptr = (uint32_t*)malloc(sizeof(uint32_t));
if (global_ptr != NULL) {
// 通过全局指针写入堆内存中的值
*global_ptr = 100;
}
else {
fprintf(stderr, "Memory allocation failed\n");
}
}

int main() {
// 调用 function,初始化 global_ptr 指向的堆内存
function();

// 在 main 中访问全局指针指向的数据
if (global_ptr != NULL) {
printf("Global pointer value in main: %" PRIu32 "\n", *global_ptr);
// 释放动态分配的堆内存,避免内存泄漏
free(global_ptr);
global_ptr = NULL;
}

return EXIT_SUCCESS;
}

6. 悬挂指针(Dangling pointer)

在使用free释放在堆上的内存区域时,指向这块内存区域的指针将变成悬挂指针,如果此时再次访问悬挂指针,非常危险,会出现未定义行为,不应该在实际编程中出现。

因此,为避免悬挂指针的出现,在free后应当将指针设置为NULL。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdlib.h>

// 悬挂指针
int main() {
uint32_t* ptr = (uint32_t*)malloc(sizeof(uint32_t));
if (ptr == NULL) {
fprintf(stderr, "Memory allocation failed\n");
return EXIT_FAILURE;
}
*ptr = 42; // 使用指针
printf("Value: %" PRIu32 "\n", *ptr);
free(ptr); // 释放动态分配的内存
//printf("Value: %" PRIu32 "\n", *ptr); // ⚠️ 访问已释放的内存,导致未定义行为
ptr = NULL; // 避免悬挂指针

return EXIT_SUCCESS;
}

7. 可变参数(Variadic function final)

C语言中的参数可以设置以为动态可变,用于向函数中传递不固定数量的多个参数,典型的例子是printf函数。

可变参数依赖头文件stdarg.h,有四个核心宏定义,其中参数访问变量ap必须声明为va_list类型,并配合以下接口使用:

  • va_list:保存参数遍历状态
  • va_start(ap, last):从最后一个固定参数last后开始取可变参数
  • va_arg(ap, type):按指定类型取下一个参数
  • va_en(ap):结束清理

可选的还有va_copy(dst, src),用于复制一个参数遍历状态。

例如一个接收可变参数的计算多个数的平均值函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdlib.h>
#include <stdarg.h>

// 计算一组整数的平均值(使用可变参数)
// count 表示后续可变参数中整数的个数
double average(int count, ...);

int main() {
// 传入 4 个整数:2, 3, 4, 5
printf("Average of 2, 3, 4, 5: %.2f\n", average(4, 2, 3, 4, 5));

return EXIT_SUCCESS;
}

double average(int count, ...) {
va_list args; // 可变参数访问句柄(必须是 va_list 类型)
va_start(args, count); // 从最后一个固定参数 count 之后开始取参
double sum = 0.0; // 用于累加整数的和

for (size_t i = 0; i < count; ++i) {
// 逐个按 int 类型读取参数并累加
sum += va_arg(args, int);
}

va_end(args); // 结束可变参数访问并清理状态

// 防止 count 为 0 时除零
return count > 0 ? sum / count : 0.0;
}

常用到可变参数的函数包括格式化输出(例如printf),字符串拼接(例如snprintf),数学和统计中的一些函数(例如平均值),图形编程中的一些函数,一些输出log日志的函数,错误处理、异常捕获相关的函数以及自定义API框架等。

8. 练习:自定义日志函数

可以自定义一个日志函数来练习可变参数的使用。

日志函数需要包含日志级别、格式化字符串、自动添加时间戳等功能。

可变参数中,可以使用vprintf(format, args)函数将接收到的可变参数格式化并打印出来。

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
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdlib.h>
#include <stdarg.h>
#include <time.h>

typedef enum {
LOG_LEVEL_DEBUG,
LOG_LEVEL_INFO,
LOG_LEVEL_WARNING,
LOG_LEVEL_ERROR
} LogLevel;

// 获取当前本地时间字符串,格式:YYYY-MM-DD HH:MM:SS
// 返回静态缓冲区指针(后续调用会覆盖旧内容)
const char* get_current_time();

// 自定义日志函数:支持可变参数格式化输出
void log_message(LogLevel level, const char* format, ...);

// 练习:自定义日志函数
int main() {
// 基础日志输出示例
log_message(LOG_LEVEL_INFO, "This is an info message.");
log_message(LOG_LEVEL_WARNING, "This is a warning message.");
// 带格式化参数的日志输出示例
log_message(LOG_LEVEL_ERROR, "This is an error message with a number: %" PRId32, 42);

return EXIT_SUCCESS;
}

const char* get_current_time() {
static char buffer[20];
time_t current_time = time(NULL);
struct tm local_time;
// 将时间转换为本地时间结构体
localtime_s(&local_time, &current_time);
// 格式化为可读时间字符串
strftime(buffer, sizeof(buffer), "%Y-%m-%d %H:%M:%S", &local_time);
return buffer;
}

void log_message(LogLevel level, const char* format, ...) {
// 日志级别与字符串映射表
const char* level_strings[] = { "DEBUG", "INFO", "WARNING", "ERROR" };
// va_list 必须用于接收和传递可变参数
va_list args;
// 从最后一个固定参数 format 之后开始读取可变参数
va_start(args, format);
// 先输出日志前缀(时间 + 级别)
printf("[%s] [%s] ", get_current_time(), level_strings[level]);
// 使用 vprintf 输出可变参数正文
vprintf(format, args);
printf("\n");
// 结束可变参数访问
va_end(args);
}

9. assert断言

assert是C标准库assert.h提供的运行时断言,用于检查“程序在此处必须成立的条件”。

当条件为假时,assert会输出失败信息(表达式、文件名、行号等)并终止程序;条件为真时不做任何事。

常见用途:

  • 校验函数前置条件(如指针非空)
  • 校验不应该被破坏的不变量
  • 在开发/调试阶段快速暴露逻辑错误

需要注意的是,assert主要用于开发期的检查,不是用户输入的错误处理机制。

断言仅在“Debug”模式下生效,在将项目生成的版本改为“Release”后,断言将不再生效。

在“Debug”模式下,在文件开头定义宏#define NDEBUG后,assert(...)会被编译为空操作,断言也会失效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdlib.h>
#include <assert.h> // 包含assert.h头文件以使用assert断言功能

// assert断言
int main() {
int num = 5;
assert(num == 5); // 断言条件为真,程序继续执行
num = 3; // 修改num的值
assert(num == 5); // 断言条件为假,程序将终止并输出错误信息
puts("Hello, World!"); // 这行代码将不会被执行,因为前面的断言失败了

return EXIT_SUCCESS;
}

在第二次断言时,断言失败,程序运行后直接弹窗报错,并输入断言失败的相关信息:

1
Assertion failed: num == 5, file E:\HC\Practice\C\learn\main.c, line 12

10. 断言的debug与练习

练习:在一个给定整数数组中寻找到元素最大值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
#include <stdlib.h>
#include <assert.h> // 包含assert.h头文件以使用assert断言功能

// 在整型数组中查找最大值
// 前置条件:arr != NULL 且 size > 0
int findMaximum(int* arr, int size);

// assert断言
int main() {
int numbers[] = { 3, 5, 2, 8, 1 };
// 计算数组元素个数并查找最大值
int max = findMaximum(numbers, sizeof(numbers) / sizeof(numbers[0]));
printf("The maximum value is: %d\n", max);

// 演示断言失败场景:传入非法参数(调试模式下会中止程序)
findMaximum(NULL, 0); // 传递NULL指针,触发assert断言失败

return EXIT_SUCCESS;
}

int findMaximum(int* arr, int size) {
assert(arr != NULL); // 断言:确保数组指针不为NULL
assert(size > 0); // 断言:确保数组大小大于0
int max = arr[0]; // 初始化:先将第一个元素作为当前最大值
// 从第二个元素开始遍历,逐步更新最大值
for (int i = 1; i < size; i++) {
if (arr[i] > max) {
max = arr[i]; // 更新最大值
}
}
return max; // 返回找到的最大值
}

由此可见,在开发过程中判断问题出现的位置时,有时候断言比打印输出更有用,更容易找到问题出现的位置。

11. 企业案例:日志系统与指针问题处理的架构设计

企业案例:日志错误系统

包含模块:

  • 跨平台的基础数据类型
  • 日志模块
  • 日志级别模块
  • 封装内存分配和释放安全操作
  • 统一错误处理策略,记录错误、警告、致命错误
  • 检查指针,根据指针错误类型不同记录不同的日志
  • 包含主要运行逻辑

在开始之前,对于跨平台的基础数据类型,可以写一个types.h头文件重新定义一下,方便后续程序中提升代码可读性与书写效率。

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
#pragma once

/**
* @file types.h
* @brief 项目通用基础类型定义头文件。
* @author HC
*
* 说明:
* - 统一使用固定宽度整数类型,避免不同平台下 `int/long` 位宽差异。
* - 通过短别名(`i32/u32/i64/u64`)提升代码可读性与书写效率。
* - 本文件仅提供类型别名,不包含业务逻辑。
*/

#include <stdint.h>

// 引入 C 标准固定宽度整数定义:int32_t / uint32_t / int64_t / uint64_t
// 参考:C99 `<stdint.h>`

// 32 位有符号整数(int32_t 的别名)
typedef int32_t i32;
// 32 位无符号整数(uint32_t 的别名)
typedef uint32_t u32;
// 64 位有符号整数(int64_t 的别名)
typedef int64_t i64;
// 64 位无符号整数(uint64_t 的别名)
typedef uint64_t u64;

11.1 logger

接下来,就可以开始写日志系统的核心逻辑。

先创建声明日志相关函数的头文件logger.h:

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
#pragma once

/**
* @file logger.h
* @brief 轻量级日志模块公共接口声明。
* @author HC
*
* 功能概览:
* - 支持多级别日志输出(DEBUG/INFO/WARNING/ERROR/FATAL)。
* - 支持日志模块初始化与资源释放。
* - 提供可变参数日志函数,调用方式与 printf 类似。
*/

#include <stdarg.h>
#include <stdbool.h>

/**
* @brief 日志级别枚举(按严重程度递增)。
*/
typedef enum {
LOG_DEBUG, /**< 调试信息:用于开发调试,发布环境通常可关闭。 */
LOG_INFO, /**< 普通信息:记录程序正常运行状态。 */
LOG_WARNING, /**< 警告信息:出现异常征兆但不影响流程继续。 */
LOG_ERROR, /**< 错误信息:出现可恢复错误,当前操作可能失败。 */
LOG_FATAL /**< 致命错误:出现不可恢复错误,程序可能需要终止。 */
} LogLevel;

/**
* @brief 初始化日志模块。
*
* 打开并准备日志输出目标,为后续 `log_message` 调用提供运行环境。
*
* @param logFilePath 日志文件路径;传入 `NULL` 时默认输出到标准输出。
* @return bool
* @retval true 初始化成功,可正常写日志。
* @retval false 初始化失败,日志模块不可用。
*
* @note 建议在程序启动阶段调用一次,并在退出前调用 `logger_close`。
*/
bool logger_init(const char* logFilePath);

/**
* @brief 终止日志模块并释放相关资源。
*
* 用于执行清理动作,如刷新缓冲区、关闭文件句柄等。
* 该函数通常在程序退出前调用。
*/
void logger_close(void);

/**
* @brief 写入一条日志消息(支持可变参数)。
*
* 调用方式与 `printf` 类似:`format` 为格式字符串,后续参数按格式占位符依次传入。
*
* @param level 日志级别,决定日志严重程度。
* @param format 格式字符串(`printf` 风格)。
* @param ... 与 `format` 匹配的可变参数列表。
*
* @note 用法示例:`log_message(LOG_INFO, "value=%d", 42);`
*/
void log_message(LogLevel level, const char* format, ...);

/**
* @brief 使用 `va_list` 写入一条日志消息。
*
* 该接口用于可变参数转发场景(例如在包装函数中先 `va_start`,再转发到日志模块)。
*
* @param level 日志级别,决定日志严重程度。
* @param format 格式字符串(`printf` 风格)。
* @param args 已初始化的可变参数列表(`va_list`)。
*/
void vlog_message(LogLevel level, const char* format, va_list args);

/**
* @brief 设置日志模块的最小输出级别。
*
* 仅当日志级别大于或等于当前阈值时,日志才会被实际输出。
* 可用于在运行时动态控制日志详细程度。
*
* @param level 新的日志级别阈值。
*/
void set_log_level(LogLevel level);

在logger.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
/**
* @file logger.c
* @brief 轻量级日志模块实现。
* @author HC
*/

#include "logger.h"
#include <stdio.h>
#include <time.h>
#include <stddef.h>

/** @brief 当前日志输出目标(文件或标准输出)。 */
static FILE* logFile = NULL;
/** @brief 当前日志阈值:低于该级别的日志将被忽略。 */
static LogLevel currentLogLevel = LOG_INFO;
/** @brief 日志级别到字符串的映射表(下标与 `LogLevel` 枚举值对应)。 */
static const char* logLevelStrings[] = { "DEBUG", "INFO", "WARNING", "ERROR", "FATAL" };

/**
* @brief 初始化日志模块。
*
* 当 `logFilePath` 非空时,尝试以追加模式打开日志文件;
* 当 `logFilePath` 为空时,默认输出到标准输出 `stdout`。
*
* @param logFilePath 日志文件路径;可为 `NULL`。
* @return bool
* @retval true 初始化成功。
* @retval false 初始化失败(如文件无法打开)。
*/
bool logger_init(const char* logFilePath) {
if (logFilePath) {
errno_t err = fopen_s(&logFile, logFilePath, "a");
if (err != 0 || logFile == NULL) {
return false; // 文件打开失败
}
}
else {
logFile = stdout; // 默认输出到标准输出
}
return true;
}

/**
* @brief 关闭日志模块并释放资源。
*
* 若当前输出目标为文件,则关闭文件句柄;
* 若当前输出目标为 `stdout`,则不执行关闭操作。
*/
void logger_close(void) {
if (logFile && logFile != stdout) {
fclose(logFile);
}

logFile = NULL;
}

/**
* @brief 写入一条日志消息。
*
* 输出格式为:`[YYYY-MM-DD HH:MM:SS] [LEVEL] message`。
* 当日志级别低于当前阈值,或日志模块未初始化时,本函数直接返回。
*
* @param level 日志级别。
* @param format 格式字符串(`printf` 风格)。
* @param ... 与 `format` 匹配的可变参数。
*/
void log_message(LogLevel level, const char* format, ...) {
va_list args;
va_start(args, format);
vlog_message(level, format, args);
va_end(args);
}

/**
* @brief 输出日志公共实现(`va_list` 版本)。
*
* @param level 日志级别。
* @param format 格式字符串(`printf` 风格)。
* @param args 已初始化的可变参数列表。
*/
void vlog_message(LogLevel level, const char* format, va_list args) {
if (level < currentLogLevel || !logFile) {
return;
}

/* 生成本地时间并格式化为 [YYYY-MM-DD HH:MM:SS] */
time_t currentTime = time(NULL);
struct tm localTime;
localtime_s(&localTime, &currentTime);
fprintf(logFile, "[%04d-%02d-%02d %02d:%02d:%02d] ",
localTime.tm_year + 1900, localTime.tm_mon + 1, localTime.tm_mday,
localTime.tm_hour, localTime.tm_min, localTime.tm_sec);

/* 输出日志级别前缀 */
fprintf(logFile, "[%s] ", logLevelStrings[level]);

/* 输出用户传入的可变参数日志正文 */
vfprintf(logFile, format, args);
fprintf(logFile, "\n");
fflush(logFile);
}

/**
* @brief 设置日志输出阈值。
*
* 仅当日志级别大于或等于当前阈值时,日志才会被输出。
*
* @param level 新的日志级别阈值。
*/
void set_log_level(LogLevel level) {
currentLogLevel = level;
}

11.2 内存管理

在用于内存管理的memory_manager.h头文件中声明相关函数:

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
#pragma once

/**
* @file memory_manager.h
* @brief 内存管理模块公共接口声明。
* @author HC
*
* 功能概览:
* - 提供安全内存分配、重分配与释放接口。
* - 统一内存操作失败时的错误处理与日志行为。
*/

#include <stddef.h>

/**
* @brief 安全分配指定大小的内存。
*
* 在调试阶段通过断言保证 `size > 0`。
* 分配失败时输出错误信息,并触发断言以便尽早暴露问题。
*
* @param size 需要分配的字节数,必须大于 0。
* @return void* 分配成功时返回可用内存指针;失败时通常在断言处中止。
*/
void* safe_malloc(size_t size);

/**
* @brief 安全地调整已分配内存块大小。
*
* 在调试阶段通过断言保证 `newSize > 0`。
* 失败时记录错误日志并返回 `NULL`,原始指针保持有效。
*
* @param ptr 需要调整大小的原始内存指针,可为 `NULL`(等价于分配新内存)。
* @param newSize 调整后的新字节数,必须大于 0。
* @return void* 成功时返回新内存指针;失败时返回 `NULL`。
*/
void* safe_remalloc(void* ptr, size_t newSize);

/**
* @brief 安全释放内存并将外部指针置空。
*
* 通过二级指针避免释放后悬挂指针问题。
* 当传入空指针或已为空时,记录警告日志。
*
* @param ptr 指向待释放指针变量的地址。
*/
void safe_free(void** ptr);

在memory_manager.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
/**
* @file memory_manager.c
* @brief 内存管理模块实现。
* @author HC
*/

#include "memory_manager.h"
#include "logger.h"
#include <assert.h>
#include <stddef.h>
#include <stdio.h>
#include <stdlib.h>

/**
* @brief 安全分配指定大小的内存。
*
* C11 环境优先使用 `aligned_alloc` 进行对齐分配,
* 非 C11 环境回退到 `malloc`。
*
* @param size 需要分配的字节数,必须大于 0。
* @return void* 分配成功时返回内存指针。
*/
void* safe_malloc(size_t size) {
assert(size > 0); // 确保请求的内存大小有效,避免分配零字节或负数字节。

void* ptr = NULL;
// 在 C11 标准中,`aligned_alloc` 可用于分配对齐内存;在 C89 中只能使用 `malloc`。
#if defined(__STDC_VERSION__) && __STDC_VERSION__ >= 201112L
ptr = aligned_alloc(alignof(max_align_t), size);
#else
ptr = malloc(size);
#endif
if (ptr == NULL) {
// 在实际应用中,可能不希望在内存分配失败时直接终止程序
// 这里使用断言是为了在开发阶段快速发现内存分配失败的问题
// 模拟错误处理:在生产环境中,应该记录错误日志并采取适当的恢复措施,而不是直接断言失败。
fprintf(stderr, "Error: Memory allocation of %zu bytes failed.\n", size);
assert(ptr != NULL);
}
return ptr;
}

/**
* @brief 安全调整已分配内存块大小。
*
* 若调整失败,记录错误日志并返回 `NULL`。
*
* @param ptr 原始内存指针。
* @param newSize 新内存大小,必须大于 0。
* @return void* 成功时返回新指针,失败返回 `NULL`。
*/
void* safe_remalloc(void* ptr, size_t newSize) {
assert(newSize > 0); // 确保新的内存大小有效。
void* newPtr = realloc(ptr, newSize);
if (newPtr == NULL) {
log_message(LOG_ERROR, "Error: Memory reallocation to %zu bytes failed.", newSize);
}
return newPtr;
}

/**
* @brief 安全释放内存并将外部指针置空。
*
* @param ptr 指向待释放指针变量的地址。
*/
void safe_free(void** ptr) {
if (ptr && *ptr) {
free(*ptr);
*ptr = NULL; // 将指针置空,避免悬挂指针
}
else {
log_message(LOG_WARNING, "Warning: Attempted to free a NULL or already freed pointer.");
}
}

11.3 error_handling

头文件error_handling.h:

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
#pragma once

/**
* @file error_handling.h
* @brief 错误处理模块公共接口声明。
* @author HC
*
* 功能概览:
* - 提供统一的警告、错误与致命错误日志入口。
* - 基于日志模块输出格式化错误信息。
*/

/**
* @brief 记录一条警告级别日志。
*
* @param format 格式字符串(`printf` 风格)。
* @param ... 与 `format` 匹配的可变参数列表。
*/
void log_warning(const char* format, ...);

/**
* @brief 记录一条错误级别日志。
*
* @param format 格式字符串(`printf` 风格)。
* @param ... 与 `format` 匹配的可变参数列表。
*/
void log_error(const char* format, ...);

/**
* @brief 记录一条致命错误日志并终止程序。
*
* @param format 格式字符串(`printf` 风格)。
* @param ... 与 `format` 匹配的可变参数列表。
*
* @note 调用后会执行 `exit(EXIT_FAILURE)`。
*/
void log_fatal(const char* format, ...);

函数实现文件error_handling.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
/**
* @file error_handling.c
* @brief 错误处理模块实现。
* @author HC
*/

#include "logger.h"
#include "error_handling.h"
#include <stdarg.h>
#include <stdlib.h>

/**
* @brief 记录警告级别日志。
*
* @param format 格式字符串(`printf` 风格)。
* @param ... 与 `format` 匹配的可变参数列表。
*/
void log_warning(const char* format, ...) {
va_list args;
va_start(args, format);
vlog_message(LOG_WARNING, format, args);
va_end(args);
}

/**
* @brief 记录错误级别日志。
*
* @param format 格式字符串(`printf` 风格)。
* @param ... 与 `format` 匹配的可变参数列表。
*/
void log_error(const char* format, ...) {
va_list args;
va_start(args, format);
vlog_message(LOG_ERROR, format, args);
va_end(args);
}

/**
* @brief 记录致命错误日志并终止程序。
*
* @param format 格式字符串(`printf` 风格)。
* @param ... 与 `format` 匹配的可变参数列表。
*
* @note 该函数在记录日志后调用 `exit(EXIT_FAILURE)`,不会返回。
*/
void log_fatal(const char* format, ...) {
va_list args;
va_start(args, format);
vlog_message(LOG_FATAL, format, args);
va_end(args);
exit(EXIT_FAILURE); // 致命错误后终止程序
}

11.4 pointer_safety空指针、野指针、悬挂指针的处理

此处的头文件pointer_safety.h用到了函数式宏,具有以下优点:

  • 零调用开销:预处理阶段直接展开,没有函数调用栈开销(尤其在小逻辑里常用)。
  • 类型泛化:不依赖固定参数类型,同一个宏可用于多种指针类型(int、float等)。
  • 可嵌入表达式:可以像普通表达式一样写在赋值、返回、条件中,使用灵活。

不过也有代价:可读性和调试性较差、可能重复求值,带副作用参数要特别小心。

如果宏太长,一个单行容纳不下,则使用宏延续运算符\。

需要把一个宏的参数转换为字符串常量时,使用字符串常量化运算符#。

例如:

1
2
#define  message_for(a, b)  \
printf(#a "和" #b "是好朋友!\n")

在main函数中:

1
message_for(Bob, Alice);

输出:

1
Bob和Alice是好朋友!
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
#pragma once

/**
* @file pointer_safety.h
* @brief 指针安全检查辅助接口与宏定义。
* @author HC
*/

#include "error_handling.h"
#include <stdbool.h>

/**
* @brief 判断给定指针是否为 `NULL`。
*
* 当检测到空指针时会记录错误日志,便于快速定位问题。
*
* @param ptr 待检查的指针。
* @return bool
* @retval true 指针为空。
* @retval false 指针非空。
*/
bool is_nullptr(const void* ptr);

/**
* @brief 安全解引用宏。
*
* 若 `ptr` 为空:记录错误日志并返回 `defaultValue`;
* 若 `ptr` 非空:返回 `*ptr` 的值。
*
* @param ptr 需要解引用的指针表达式。
* @param defaultValue 当 `ptr` 为空时返回的默认值。
*
* @details 宏中使用了三元运算符与逗号运算符:
* `is_nullptr(ptr) ? (log_error(...), defaultValue) : *(ptr)`。
* 其中 `(a, b)` 表示先执行 `a`(记录日志),再返回 `b` 的值(`defaultValue`)。
*
* @note 该宏会对 `ptr` 求值两次(一次检查、一次解引用),
* 请避免传入带副作用的表达式(如 `p++`、函数调用等)。
*/
#define SAFE_DEREF(ptr, defaultValue) \
(is_nullptr(ptr) ? \
(log_error("Attempt to dereference a NULL pointer at %s:%d", __FILE__, __LINE__), (defaultValue)) : \
*(ptr))

在pointer_safety.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
/**
* @file pointer_safety.c
* @brief 指针安全检查模块实现。
* @author HC
*/

#include "pointer_safety.h"
#include <stddef.h>

/**
* @brief 判断给定指针是否为空,并在为空时记录错误。
*
* @param ptr 待检查的指针。
* @return bool
* @retval true 指针为空。
* @retval false 指针非空。
*/
bool is_nullptr(const void* ptr) {
if (ptr == NULL) {
log_error("Detected NULL pointer at %s:%d", __FILE__, __LINE__);
return true;
}
return false;
}

11.5 application_logic模块

一般比较规范的程序的启动逻辑不会都放在main函数中,会有专门的启动逻辑函数。

头文件application_logic.h:

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
#pragma once

/**
* @file application_logic.h
* @brief 应用逻辑模块公共接口声明。
* @author HC
*
* 功能概览:
* - 提供应用生命周期接口:初始化、执行、清理。
* - 对业务流程进行统一编排,并调用日志与内存管理模块。
*/

#include <stdbool.h>

/**
* @brief 初始化应用运行环境。
*
* 负责初始化日志模块并设置日志级别。
*
* @return bool
* @retval true 初始化成功。
* @retval false 初始化失败。
*/
bool app_init(void);

/**
* @brief 执行应用核心逻辑。
*
* 该函数演示典型业务流程:记录日志、申请内存、写入数据并释放资源。
*
* @return bool
* @retval true 执行成功。
* @retval false 执行失败。
*/
bool app_execute(void);

/**
* @brief 清理应用资源并关闭运行环境。
*
* 负责输出清理日志并关闭日志模块。
*/
void app_cleanup(void);

在函数实现application_logic.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
/**
* @file application_logic.c
* @brief 应用逻辑模块实现。
* @author HC
*/

#include "application_logic.h"
#include "logger.h"
#include "memory_manager.h"
#include <stdio.h>

/**
* @brief 初始化应用运行环境。
*
* @return bool
* @retval true 初始化成功。
* @retval false 初始化失败。
*/
bool app_init(void) {
if (!logger_init(NULL)) {
fprintf(stderr, "Failed to initialize logger.\n");
return false;
}
set_log_level(LOG_DEBUG); // 设置日志级别为 DEBUG,记录所有级别的日志
log_message(LOG_INFO, "Application initialized successfully.");
return true;
}

/**
* @brief 执行应用核心逻辑。
*
* @return bool
* @retval true 执行成功。
* @retval false 执行失败。
*/
bool app_execute(void) {
log_message(LOG_INFO, "Application execution started.");
// 模拟执行过程中的一些操作
int* data = (int*)safe_malloc(sizeof(int));
if (data == NULL) {
log_error("Failed to allocate memory for data.");
return false;
}
log_message(LOG_DEBUG, "Memory allocated successfully , now setting value.");
*data = 42; // 设置数据值
log_message(LOG_INFO, "Data value set to %d.", *data);
safe_free((void**)&data); // 释放内存并置空指针
log_message(LOG_DEBUG, "Memory freed and pointer set to NULL.");
log_message(LOG_DEBUG, "Finished executing application logic.");
return true;
}

/**
* @brief 清理应用资源并关闭运行环境。
*/
void app_cleanup(void) {
log_message(LOG_INFO, "Cleaning up application resources.");
logger_close(); // 关闭日志模块,释放资源
log_message(LOG_INFO, "Application cleanup completed.");
}

11.6 测试

所有工具函数库都编写完成后,即可对程序功能进行一个简单的测试。

在main函数中:

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
#include <stdio.h>
#include <stdlib.h>

#include "application_logic.h"
#include "logger.h"
#include "error_handling.h"

int main() {
// 第一步:初始化应用运行环境
if (!app_init()) {
fprintf(stderr, "Application initialization failed.\n");
logger_close(); // 确保在初始化失败时也能正确关闭日志模块
return EXIT_FAILURE;
}

// 第二步:执行应用核心逻辑
if (!app_execute()) {
log_error("Application execution failed.\n");
app_cleanup(); // 在执行失败时进行清理,确保资源正确释放
return EXIT_FAILURE;
}

// 第三步:正常结束前执行资源清理
app_cleanup(); // 正常执行完成后进行清理

return EXIT_SUCCESS;
}

测试成功,程序正常运行,控制台打印出了正确的日志。

11.7 写入文件

在application_logic.c中,为文件输出路径写宏定义:#define PATH "application.log",使用相对路径时,如果只写文件名,则log文件会在项目的根目录生成。

修改app_init()函数,将PATH传入logger_init函数,即可将日志输出到文件中。

运行程序进行测试,检查项目根目录,发现成功生成application.log文件,文件中成功输出了正确的日志。

12. 环境变量的读写

环境变量的读取需要用到getenv_s函数,包含在标准库头文件stdlib.h中。

函数原型:

1
2
3
4
5
6
errno_t getenv_s(
size_t* pReturnValue,
char* buffer,
size_t numberOfElements,
const char* varname
);

参数说明:

  • pReturnValue:返回写入所需字符数(包含结尾\0)
  • buffer:输出缓冲区。可传NULL(用于只查询长度)
  • numberOfElements:buffer的容量(字符数,不是字节数;char下通常等同字节数)
  • varname:环境变量名,如"PATH"

返回值和含义:

返回errno_t:

  • 0:调用成功(不代表变量一定存在)
  • 常见非0:
    • ERANGE(常见值34):缓冲区太小
    • EINVAL:参数非法

结合*pReturnValue判断状态:

  • err == 0 && required > 0:变量存在,已读取
  • err == 0 && required == 0:变量不存在或为空(按实现语义处理)

例如读取读取常见的环境变量PATH:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#include <stdio.h>
#include <stdlib.h>

int main() {
// 固定大小缓冲区,用于接收 PATH 环境变量内容
char buffer[512];
// 保存 getenv_s 返回的所需字符数(包含结尾 '\0')
size_t required = 0;

// 读取 PATH 环境变量到 buffer
errno_t err = getenv_s(&required, buffer, sizeof(buffer), "PATH");

// err == 0 且 required > 0:读取成功
if (err == 0 && required > 0) {
printf("PATH: %s\n", buffer);
printf("PATH length: %zu\n", required);
}
// err == 0 且 required == 0:变量未设置或为空
else if (err == 0 && required == 0) {
printf("PATH is not set.\n");
}
// 其他情况:读取失败(如缓冲区过小)
else {
fprintf(stderr, "Failed to get PATH, err=%d\n", (int)err);
}

return EXIT_SUCCESS;
}

需要注意的是,此时的缓冲区buffer为固定值,当环境变量的长度大于buffer的长度时,则报错误码34,超出缓冲区,无法读取。

因此,正确的用法应该是先传递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
#include <stdio.h>
#include <stdlib.h>

int main() {
// 第一步:先查询环境变量 PATH 需要的缓冲区长度(包含结尾 '\0')
size_t required = 0;

errno_t err = getenv_s(&required, NULL, 0, "PATH");
if (err != 0) {
fprintf(stderr, "Failed to query PATH size, err=%d\n", (int)err);
return EXIT_FAILURE;
}

// required == 0 表示 PATH 未设置(或为空)
if (required == 0) {
printf("PATH is not set.\n");
return EXIT_SUCCESS;
}

// 第二步:按查询到的长度动态分配缓冲区
char* buffer = (char*)malloc(required * sizeof(char));
if (buffer == NULL) {
fprintf(stderr, "Failed to allocate memory for PATH.\n");
return EXIT_FAILURE;
}

// 第三步:读取 PATH 到 buffer
err = getenv_s(&required, buffer, required, "PATH");
if (err != 0) {
fprintf(stderr, "Failed to get PATH, err=%d\n", (int)err);
free(buffer);
return EXIT_FAILURE;
}

// 输出读取结果
printf("PATH: %s\n", buffer);

// 释放动态分配的内存,避免内存泄漏
free(buffer);

return EXIT_SUCCESS;
}

写入或删除环境变量时,可以使用_putenv_s函数,也包含在标准库头文件stdlib.h中。

函数原型:

1
errno_t _putenv_s(const char* name, const char* value);

参数语义:

  • name:变量名(如“MY_KEY”)
  • value:变量值
    • 普通字符串:设置/覆盖变量
    • 空字符串“”:删除变量

返回值为0时,成功,否则失败。

作用范围:

  • 只影响当前进程的环境变量表
  • 对当前进程后续创建的子进程可见
  • 不会永久写入系统/用户环境(程序结束后通常失效)
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
#include <stdio.h>
#include <stdlib.h>

int main(void) {
// 设置环境变量:MY_APP_MODE=debug(仅当前进程及其子进程可见)
errno_t err = _putenv_s("MY_APP_MODE", "debug");
if (err != 0) {
fprintf(stderr, "_putenv_s failed, err=%d\n", (int)err);
return EXIT_FAILURE;
}

// 读取环境变量:required 返回所需字符数(包含结尾 '\0')
size_t required = 0;
char buf[64];
err = getenv_s(&required, buf, sizeof(buf), "MY_APP_MODE");

// 读取成功时输出变量值
if (err == 0 && required > 0) {
printf("MY_APP_MODE=%s\n", buf);
}

// 删除变量:value 传空字符串表示移除该变量
_putenv_s("MY_APP_MODE", "");

return EXIT_SUCCESS;
}

13. 命令行参数

在命令行中使用一些命令,例如ipconfig时,可以通过-向软件传递一些参数,为用户提供相应的功能。

C语言命令行参数通过main函数的参数接收,常见写法:

1
int main(int argc, char* argv[])
  • argc:参数个数(至少为1)
  • argv:参数字符串数组
    • argv[0]:程序名/路径
    • argv[1]...argv[argc-1]:传入的自定义参数

一个简单的打印所有输入参数的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <stdio.h>
#include <stdlib.h>

// 命令行参数
int main(int argc, char* argv[]) {
printf("Program name: %s\n", argv[0]);
printf("User have entered %d arguments.\n", argc - 1);

for (int i = 1; i < argc; i++) {
printf("Argument %zu: %s\n", i, argv[i]);
}

return EXIT_SUCCESS;
}

程序编译完成后,在命令行中向exe程序传递参数后运行,即可将传递的参数打印出来。

14. 小案例:命令行程序的编写

案例:一个命令行程序,当输入参数-a时,程序会把后面输入的所有数字加起来并输出它们的和。

atoi是C标准库函数,用于把字符串转换成int类型的数据。

函数原型:int atoi(const char* str);

行为:

  • 跳过前导空白
  • 识别可选正负号+/-
  • 读取后续数字并转换
  • 遇到第一个非数字字符停止

用例:

  • “123”->123
  • “-42abc”->-42
  • “abc”->0
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
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// 处理 -a 选项:将后续所有数字参数累加并输出结果
void processAdd(int argc, char* argv[]);

int main(int argc, char* argv[]) {
// 用法:program -a num1 num2 ...
// argc >= 3 表示至少包含:程序名、-a、1 个数字
if (argc >= 3 && strcmp(argv[1], "-a") == 0) { // 检查是否有足够的参数并且第一个参数是"-a"
processAdd(argc, argv);
}
else {
printf("Usage: %s -a num1 num2 ...\n", argv[0]); // 提示用户正确的使用方法
}

return EXIT_SUCCESS;
}

// 从 argv[2] 开始解析数字参数并求和
// argv[0] 是程序名,argv[1] 是选项 "-a"
void processAdd(int argc, char* argv[]) {
int sum = 0;
for (int i = 2; i < argc; i++) {
sum += atoi(argv[i]); // 将字符串转换为整数并累加到sum中
}
printf("The sum is %d\n", sum);
}

在命令行中测试成功。

15. 案例:自定义泛型队列

前面学过,如果函数返回或传入类型为void*类型的参数,称之为泛型。

除此之外,结构体也属于自己定义的类型,因此如果一个函数返回一个结构体,也可以称之为泛型。

15.1 节点结构模块

可以先写一个类型定义头文件type_definitions.h,定义一些常用数据类型的别名以及数据类型枚举和联合体。

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
/**
* @file TypeDefinitions.h
* @brief 定义项目中使用的基础类型别名、数据类型枚举和通用联合体。
* @author HC
*/

#pragma once

#include <inttypes.h>

// 有符号/无符号整数类型别名
typedef int32_t i32;
typedef uint32_t u32;
typedef int64_t i64;
typedef uint64_t u64;

// 浮点类型别名
typedef float f32;
typedef double f64;

// 通用数据类型标识
typedef enum {
TYPE_I32,
TYPE_U32,
TYPE_I64,
TYPE_U64,
TYPE_F32,
TYPE_F64,
TYPE_CHAR,
TYPE_PTR
} DataType;

// 通用值联合体:根据 DataType 解释对应字段
typedef union {
i32 i32Value;
u32 u32Value;
i64 i64Value;
u64 u64Value;
f32 f32Value;
f64 f64Value;
char charValue;
void* ptrValue;
} GenericValue;

在generic_queue.h中声明泛型队列结构体与公共接口函数:

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
#pragma once

/**
* @file GenericQueue.h
* @brief 泛型队列模块公共接口声明。
* @author HC
*/

#include "TypeDefinitions.h"
#include <stdbool.h>
#include <stddef.h>

/**
* @brief 泛型队列节点结构体。
*/
typedef struct GenericQueueNode {
GenericValue value; /**< 节点存储的数据值。 */
DataType type; /**< 节点存储的数据类型。 */
struct GenericQueueNode* next; /**< 指向下一个节点。 */
} GenericQueueNode;

/**
* @brief 泛型队列结构体。
*/
typedef struct {
GenericQueueNode* head; /**< 队列头节点。 */
GenericQueueNode* tail; /**< 队列尾节点。 */
size_t size; /**< 当前队列元素数量。 */
} GenericQueue;

/**
* @brief 创建并初始化一个空泛型队列。
*
* @return GenericQueue* 成功时返回队列指针;失败时返回 `NULL`。
*/
GenericQueue* createQueue(void);

/**
* @brief 销毁泛型队列并释放所有节点内存。
*
* @param queue 待销毁的队列指针,可为 `NULL`。
*/
void destroyQueue(GenericQueue* queue);

/**
* @brief 向队列尾部入队一个元素。
*
* @param queue 目标队列指针。
* @param value 待入队的数据值。
* @param type 待入队数据的类型标记。
* @return bool
* @retval true 入队成功。
* @retval false 入队失败(如队列为空指针或内存分配失败)。
*/
bool enqueueQueue(GenericQueue* queue, GenericValue value, DataType type);

/**
* @brief 从队列头部出队一个元素。
*
* 当 `outValue` 或 `outType` 为 `NULL` 时,对应输出会被忽略。
*
* @param queue 目标队列指针。
* @param outValue 用于接收出队数据值的输出指针,可为 `NULL`。
* @param outType 用于接收出队数据类型的输出指针,可为 `NULL`。
* @return bool
* @retval true 出队成功。
* @retval false 出队失败(如队列为空指针或队列为空)。
*/
bool dequeueQueue(GenericQueue* queue, GenericValue* outValue, DataType* outType);

/**
* @brief 获取队列当前元素数量。
*
* @param queue 目标队列指针。
* @return size_t 队列大小;当 `queue` 为 `NULL` 时返回 0。
*/
size_t getQueueSize(const GenericQueue* queue);

/**
* @brief 判断队列是否为空。
*
* @param queue 目标队列指针。
* @return bool
* @retval true 队列为空,或 `queue` 为 `NULL`。
* @retval false 队列非空。
*/
bool isQueueEmpty(const GenericQueue* queue);

在generic_queue.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
/**
* @file GenericQueue.c
* @brief 泛型队列模块实现。
* @author HC
*/

#include "GenericQueue.h"
#include <stdlib.h>
#include <stdbool.h>
#include <stddef.h>

/**
* @brief 创建并初始化一个空泛型队列。
*
* @return GenericQueue* 成功时返回队列指针;失败时返回 `NULL`。
*/
GenericQueue* createQueue(void) {
GenericQueue* queue = (GenericQueue*)malloc(sizeof(GenericQueue));
if (queue) {
queue->head = NULL;
queue->tail = NULL;
queue->size = 0;
}
return queue;
}

/**
* @brief 销毁泛型队列并释放所有节点内存。
*
* @param queue 待销毁的队列指针,可为 `NULL`。
*/
void destroyQueue(GenericQueue* queue) {
if (queue) {
GenericQueueNode* current = queue->head;
while (current) {
GenericQueueNode* next = current->next;
free(current);
current = next;
}
free(queue);
}
}

/**
* @brief 向队列尾部入队一个元素。
*
* @param queue 目标队列指针。
* @param value 待入队的数据值。
* @param type 待入队数据的类型标记。
* @return bool
* @retval true 入队成功。
* @retval false 入队失败(如队列为空指针或内存分配失败)。
*/
bool enqueueQueue(GenericQueue* queue, GenericValue value, DataType type) {
if (queue) {
GenericQueueNode* newNode = (GenericQueueNode*)malloc(sizeof(GenericQueueNode));
if (newNode) {
newNode->value = value;
newNode->type = type;
newNode->next = NULL;
if (queue->tail) {
queue->tail->next = newNode;
}
else {
queue->head = newNode;
}
queue->tail = newNode;
queue->size++;
return true;
}
}
return false;
}

/**
* @brief 从队列头部出队一个元素。
*
* 当 `outValue` 或 `outType` 为 `NULL` 时,对应输出会被忽略。
*
* @param queue 目标队列指针。
* @param outValue 用于接收出队数据值的输出指针,可为 `NULL`。
* @param outType 用于接收出队数据类型的输出指针,可为 `NULL`。
* @return bool
* @retval true 出队成功。
* @retval false 出队失败(如队列为空指针或队列为空)。
*/
bool dequeueQueue(GenericQueue* queue, GenericValue* outValue, DataType* outType) {
if (queue && queue->head) {
GenericQueueNode* temp = queue->head;
if (outValue) {
*outValue = temp->value;
}
if (outType) {
*outType = temp->type;
}
queue->head = temp->next;
if (!queue->head) {
queue->tail = NULL;
}
free(temp);
queue->size--;
return true;
}
return false;
}

/**
* @brief 获取队列当前元素数量。
*
* @param queue 目标队列指针。
* @return size_t 队列大小;当 `queue` 为 `NULL` 时返回 0。
*/
size_t getQueueSize(const GenericQueue* queue) {
return queue ? queue->size : 0;
}

/**
* @brief 判断队列是否为空。
*
* @param queue 目标队列指针。
* @return bool
* @retval true 队列为空,或 `queue` 为 `NULL`。
* @retval false 队列非空。
*/
bool isQueueEmpty(const GenericQueue* queue) {
return queue ? (queue->size == 0) : true;
}

这些函数已经覆盖了队列的核心操作(创建、销毁、入队、出队、判空、获取长度),理解它们的实现逻辑有助于掌握队列这种数据结构及其常见使用方式。

15.2 安全内存处理模块

在memory_management.h中进行内存管理模块公共接口声明。

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
#pragma once

/**
* @file memory_management.h
* @brief 内存管理模块公共接口声明。
* @author HC
*
* 功能概览:
* - 提供安全内存分配、重分配与释放接口。
* - 维护并输出当前内存使用统计信息。
*/

#include <stddef.h>

/**
* @brief 安全分配指定字节数内存。
*
* @param size 需要分配的字节数。
* @return void* 分配成功返回内存指针;失败时函数内部终止程序。
*/
void* safeMalloc(size_t size);

/**
* @brief 安全重分配内存块大小。
*
* @param ptr 原内存块指针,可为 `NULL`。
* @param newSize 新的内存大小(字节)。
* @return void* 重分配成功返回新指针;失败时函数内部终止程序。
*/
void* safeRealloc(void* ptr, size_t newSize);

/**
* @brief 安全释放内存并将外部指针置空。
*
* @param ptr 指向待释放指针变量的地址。
*/
void safeFree(void** ptr);

/**
* @brief 打印当前内存使用量。
*/
void printMemoryUsage();

/**
* @brief 将当前内存使用信息写入文件。
*
* @param filename 输出文件路径。
*/
void memoryManagementDumpToFile(const char* filename);

在memory_management.c中进行内存管理模块实现。

在Windows平台下,可以使用_msize检查内存块的大小。

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
/**
* @file memory_management.c
* @brief 内存管理模块实现。
* @author HC
*/

#include "memory_management.h"
#include <stdio.h>
#include <stdlib.h>

// _msize 可以检查内存块的大小
#ifdef _WIN32
#include <malloc.h>
#endif // _WIN32

static size_t currentMemoryUsage = 0;

/**
* @brief 安全分配指定字节数内存并更新内存统计。
*
* @param size 需要分配的字节数。
* @return void* 分配成功返回内存指针;失败时终止程序。
*/
void* safeMalloc(size_t size) {
void* ptr = malloc(size);
if (ptr) {
currentMemoryUsage += size;
}
else {
printf("Memory allocation failed for size %zu bytes\n", size);
exit(EXIT_FAILURE);
}
return ptr;
}

/**
* @brief 安全重分配内存并更新内存统计。
*
* @param ptr 原内存块指针,可为 `NULL`。
* @param newSize 新的内存大小(字节)。
* @return void* 重分配成功返回新指针;失败时终止程序。
*/
void* safeRealloc(void* ptr, size_t newSize) {
size_t oldSize = 0;
if (ptr) {
#ifdef _WIN32
oldSize = _msize(ptr);
#else
// 在非 Windows 平台上,无法直接获取内存块大小,无法准确更新 currentMemoryUsage
#endif // _WIN32
}
void* newPtr = realloc(ptr, newSize);
if (newPtr) {
if (ptr) {
currentMemoryUsage -= oldSize;
}
currentMemoryUsage += newSize;
}
else {
fprintf(stderr, "Memory reallocation failed for new size %zu bytes\n", newSize);
exit(EXIT_FAILURE);
}
return newPtr;
}

/**
* @brief 安全释放内存并将外部指针置空。
*
* @param ptr 指向待释放指针变量的地址。
*/
void safeFree(void** ptr) {
size_t oldSize = 0;
if (ptr && *ptr) {
#ifdef _WIN32
oldSize = _msize(*ptr);
#else
// 在非 Windows 平台上,无法直接获取内存块大小,无法准确更新 currentMemoryUsage
#endif // _WIN32
currentMemoryUsage -= oldSize;
free(*ptr);
*ptr = NULL;
}
}

/**
* @brief 打印当前内存使用量。
*/
void printMemoryUsage() {
printf("Current memory usage: %zu bytes\n", currentMemoryUsage);
}

/**
* @brief 将当前内存使用信息写入文件。
*
* @param filename 输出文件路径;为空时输出错误并返回。
*/
void memoryManagementDumpToFile(const char* filename) {
if (!filename) {
fprintf(stderr, "Invalid filename for memory dump\n");
return;
}
FILE* file;
errno_t err = fopen_s(&file, filename, "w");
if (err == 0 && file) {
fprintf(file, "Current memory usage: %zu bytes\n", currentMemoryUsage);
fclose(file);
}
else {
fprintf(stderr, "Failed to open file '%s' for writing memory dump\n", filename);
}
}

在type_safety_and_error_handling.h中进行类型安全与错误处理模块公共接口声明。

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
#pragma once

/**
* @file type_safety_and_error_handling.h
* @brief 类型安全与错误处理模块公共接口声明。
* @author HC
*/

#include <stddef.h>
#include <stdbool.h>
#include <stdio.h>

/**
* @brief 错误码枚举定义。
*/
typedef enum {
ERROR_NONE = 0, /**< 无错误 */
ERROR_MEMORY_ALLOCATION_FAILED, /**< 内存分配失败 */
ERROR_INVALID_OPERATION, /**< 无效操作 */
ERROR_BOUNDS, /**< 越界访问 */
// 可以根据需要添加更多错误类型
} ErrorCode;

/**
* @brief 设置当前错误码。
*
* @param code 要设置的错误码。
*/
void setErrorCode(ErrorCode code);

/**
* @brief 获取当前错误码。
*
* @return ErrorCode 当前错误码。
*/
ErrorCode getErrorCode(void);

/**
* @brief 清除当前错误状态。
*
* 将错误码重置为 `ERROR_NONE`。
*/
void clearErrorCode(void);

/**
* @brief 判断当前是否存在错误。
*
* @return bool
* @retval true 当前存在错误。
* @retval false 当前无错误。
*/
bool hasError(void);

/**
* @brief 设置错误码并记录上下文信息。
*
* @param code 错误码。
* @param file 触发错误的文件名,可为 `NULL`。
* @param line 触发错误的行号。
*/
void setErrorCodeWithContext(ErrorCode code, const char* file, int line);

/**
* @brief 获取最近一次错误上下文。
*
* @return const char* 上下文字符串。
*/
const char* getLastErrorContext(void);

/**
* @brief 统一输出最近一次错误信息。
*
* @param out 输出流;为 `NULL` 时默认输出到 `stderr`。
*/
void printLastError(FILE* out);

/**
* @brief 边界检查辅助函数。
*
* @param index 待访问索引。
* @param size 有效范围大小。
* @return bool
* @retval true 索引合法。
* @retval false 索引越界(并设置 `ERROR_BOUNDS`)。
*/
bool checkBounds(size_t index, size_t size);

/**
* @brief 带位置信息的错误设置宏。
*
* 等价于调用 `setErrorCodeWithContext(code, __FILE__, __LINE__)`。
*/
#define SET_ERROR(code) setErrorCodeWithContext((code), __FILE__, __LINE__)

/**
* @brief 获取错误码对应的可读错误信息。
*
* @param code 错误码。
* @return const char* 错误描述字符串。
*/
const char* getErrorMessage(ErrorCode code);

/**
* @brief 检查指针是否为空。
*
* @param ptr 待检查指针。
* @return ErrorCode
* @retval ERROR_NONE 指针非空。
* @retval ERROR_INVALID_OPERATION 指针为空。
*/
ErrorCode checkNotNull(const void* ptr);

/**
* @brief 统一处理错误码。
*
* @param code 需要处理的错误码。
*/
void handleError(ErrorCode code);

在type_safety_and_error_handling.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
/**
* @file type_safety_and_error_handling.c
* @brief 类型安全与错误处理模块实现。
* @author HC
*/

#include "type_safety_and_error_handling.h"
#include <stdio.h>
#include <stdlib.h>

/** @brief 当前错误码。 */
static ErrorCode g_currentErrorCode = ERROR_NONE;
/** @brief 最近一次错误上下文(文件与行号)。 */
static char g_lastErrorContext[128] = "";

/**
* @brief 设置当前错误码。
*
* @param code 要设置的错误码。
*/
void setErrorCode(ErrorCode code) {
g_currentErrorCode = code;
}

/**
* @brief 获取当前错误码。
*
* @return ErrorCode 当前错误码。
*/
ErrorCode getErrorCode(void) {
return g_currentErrorCode;
}

/**
* @brief 清除当前错误状态。
*
* 将错误码重置为 `ERROR_NONE`,并清空错误上下文。
*/
void clearErrorCode(void) {
g_currentErrorCode = ERROR_NONE;
g_lastErrorContext[0] = '\0';
}

/**
* @brief 判断当前是否存在错误。
*
* @return bool
* @retval true 当前存在错误。
* @retval false 当前无错误。
*/
bool hasError(void) {
return g_currentErrorCode != ERROR_NONE;
}

/**
* @brief 设置错误码并记录上下文信息。
*
* @param code 错误码。
* @param file 触发错误的文件名,可为 `NULL`。
* @param line 触发错误的行号。
*/
void setErrorCodeWithContext(ErrorCode code, const char* file, int line) {
g_currentErrorCode = code;
if (file) {
snprintf(g_lastErrorContext, sizeof(g_lastErrorContext), "%s:%d", file, line);
}
else {
snprintf(g_lastErrorContext, sizeof(g_lastErrorContext), "<unknown>:%d", line);
}
}

/**
* @brief 获取最近一次错误上下文字符串。
*
* @return const char* 上下文字符串;若无上下文则返回 "(no context)"。
*/
const char* getLastErrorContext(void) {
return g_lastErrorContext[0] ? g_lastErrorContext : "(no context)";
}

/**
* @brief 统一输出最近一次错误信息。
*
* @param out 输出流;为 `NULL` 时默认输出到 `stderr`。
*/
void printLastError(FILE* out) {
FILE* stream = out ? out : stderr;
fprintf(stream, "ErrorCode=%d, Message=%s, Context=%s\n",
(int)g_currentErrorCode,
getErrorMessage(g_currentErrorCode),
getLastErrorContext());
}

/**
* @brief 边界检查。
*
* @param index 待访问索引。
* @param size 有效范围大小。
* @return bool
* @retval true 索引合法。
* @retval false 索引越界(并设置 `ERROR_BOUNDS`)。
*/
bool checkBounds(size_t index, size_t size) {
if (index < size) {
return true;
}
setErrorCodeWithContext(ERROR_BOUNDS, __FILE__, __LINE__);
return false;
}

/**
* @brief 空指针检查。
*
* @param ptr 待检查指针。
* @return ErrorCode
* @retval ERROR_NONE 指针非空。
* @retval ERROR_INVALID_OPERATION 指针为空(并记录上下文)。
*/
ErrorCode checkNotNull(const void* ptr) {
if (ptr != NULL) {
return ERROR_NONE;
}

setErrorCodeWithContext(ERROR_INVALID_OPERATION, __FILE__, __LINE__);
return ERROR_INVALID_OPERATION;
}

/**
* @brief 统一处理错误。
*
* @param code 需要处理的错误码。
*/
void handleError(ErrorCode code) {
if (code == ERROR_NONE) {
exit(EXIT_FAILURE);
}

setErrorCode(code);
fprintf(stderr, "ErrorCode=%d, Message=%s, Context=%s\n",
(int)code,
getErrorMessage(code),
getLastErrorContext());
}

/**
* @brief 获取错误码对应的可读错误信息。
*
* @param code 错误码。
* @return const char* 错误描述字符串。
*/
const char* getErrorMessage(ErrorCode code) {
switch (code) {
case ERROR_NONE:
return "No error";
case ERROR_MEMORY_ALLOCATION_FAILED:
return "Memory allocation failed";
case ERROR_INVALID_OPERATION:
return "Invalid operation";
case ERROR_BOUNDS:
return "Out of bounds access";
default:
return "Unknown error";
}
}

15.3 测试运行模块

实际开发中,进行一个功能的测试,需要写专门的测试用例,用可重复、可验证的方式证明代码行为正确。

在test_and_validation.h中进行测试与验证模块公共接口声明。

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
#pragma once

/**
* @file test_and_validation.h
* @brief 测试与验证模块公共接口声明。
* @author HC
*
* 功能概览:
* - 提供统一测试用例描述结构与测试函数类型。
* - 提供测试套件执行入口与断言辅助宏。
* - 声明各模块测试函数(队列、内存管理、类型安全)。
*/

#include <stddef.h>
#include <stdio.h>

/**
* @brief 测试结果枚举。
*/
typedef enum TestResult {
TEST_SUCCESS, /**< 测试通过。 */
TEST_FAILURE /**< 测试失败。 */
} TestResult;

/**
* @brief 测试函数类型定义。
*
* 每个测试函数无参数,返回 `TestResult`。
*/
typedef TestResult(*TestFunction)(void);

/**
* @brief 测试用例描述结构体。
*/
typedef struct TestCase {
const char* name; /**< 测试用例名称。 */
TestFunction function; /**< 测试函数指针。 */
} TestCase;

/**
* @brief 运行测试用例集合并输出结果。
*
* @param testCases 测试用例数组首地址。
* @param testCount 测试用例数量。
*/
void runTestSuite(const TestCase* testCases, size_t testCount);

/**
* @brief 测试断言宏。
*
* 当表达式为假时,输出失败信息并立即返回 `TEST_FAILURE`。
*
* @param expression 需要验证的布尔表达式。
* @note 该宏设计用于返回类型为 `TestResult` 的测试函数中。
*/
#define VERIFY(expression) \
if (!(expression)) { \
fprintf(stderr, "Test failed: %s, at %s:%d\n", #expression, __FILE__, __LINE__); \
return TEST_FAILURE; \
}

/**
* @brief 队列模块测试。
*
* @return TestResult 测试结果。
*/
TestResult testQueueOperations(void);

/**
* @brief 内存管理模块测试。
*
* @return TestResult 测试结果。
*/
TestResult testMemoryManagement(void);

/**
* @brief 类型安全与错误处理模块测试。
*
* @return TestResult 测试结果。
*/
TestResult testTypeSafety(void);

在test_and_validation.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
/**
* @file test_and_validation.c
* @brief 测试与验证模块实现。
* @author HC
*/

#include "test_and_validation.h"
#include "generic_queue.h"
#include "memory_management.h"
#include "type_safety_and_error_handling.h"

#include <inttypes.h>
#include <stdio.h>

/**
* @brief 运行测试用例集合并输出统计结果。
*
* @param testCases 测试用例数组首地址。
* @param testCount 测试用例数量。
*/
void runTestSuite(const TestCase* testCases, size_t testCount) {
if (testCases == NULL || testCount == 0) {
fprintf(stderr, "No test cases to run.\n");
return;
}

size_t passed = 0;
for (size_t i = 0; i < testCount; ++i) {
TestResult result = testCases[i].function();
if (result == TEST_SUCCESS) {
printf("[PASS] %s\n", testCases[i].name);
passed++;
}
else {
printf("[FAIL] %s\n", testCases[i].name);
}
}

printf("Test summary: %zu/%zu passed.\n", passed, testCount);
}

/**
* @brief 测试泛型队列核心操作。
*
* 覆盖创建、入队、出队、判空和长度校验。
*
* @return TestResult
* @retval TEST_SUCCESS 测试通过。
* @retval TEST_FAILURE 测试失败。
*/
TestResult testQueueOperations(void) {
GenericQueue* queue = createQueue();
VERIFY(queue != NULL);
VERIFY(isQueueEmpty(queue) == true);
VERIFY(getQueueSize(queue) == 0);

GenericValue v1;
v1.i32Value = 42;
VERIFY(enqueueQueue(queue, v1, TYPE_I32) == true);

GenericValue v2;
v2.charValue = 'A';
VERIFY(enqueueQueue(queue, v2, TYPE_CHAR) == true);
VERIFY(getQueueSize(queue) == 2);

GenericValue outValue;
DataType outType;
VERIFY(dequeueQueue(queue, &outValue, &outType) == true);
VERIFY(outType == TYPE_I32);
VERIFY(outValue.i32Value == 42);

VERIFY(dequeueQueue(queue, &outValue, &outType) == true);
VERIFY(outType == TYPE_CHAR);
VERIFY(outValue.charValue == 'A');
VERIFY(isQueueEmpty(queue) == true);

destroyQueue(queue);
return TEST_SUCCESS;
}

/**
* @brief 测试内存管理核心操作。
*
* 覆盖分配、重分配、释放和内存信息导出。
*
* @return TestResult
* @retval TEST_SUCCESS 测试通过。
* @retval TEST_FAILURE 测试失败。
*/
TestResult testMemoryManagement(void) {
int* buffer = (int*)safeMalloc(sizeof(int));
VERIFY(buffer != NULL);
*buffer = 123;

buffer = (int*)safeRealloc(buffer, sizeof(int) * 2);
VERIFY(buffer != NULL);
VERIFY(buffer[0] == 123);
memoryManagementDumpToFile("memory_dump.txt");

safeFree((void**)&buffer);
VERIFY(buffer == NULL);

//memoryManagementDumpToFile("memory_dump.txt");
return TEST_SUCCESS;
}

/**
* @brief 测试类型安全与错误处理功能。
*
* 覆盖错误状态清理、边界检查、空指针检查及错误信息输出。
*
* @return TestResult
* @retval TEST_SUCCESS 测试通过。
* @retval TEST_FAILURE 测试失败。
*/
TestResult testTypeSafety(void) {
clearErrorCode();
VERIFY(hasError() == false);
VERIFY(getErrorCode() == ERROR_NONE);

VERIFY(checkBounds(1, 3) == true);
VERIFY(checkBounds(3, 3) == false);
VERIFY(getErrorCode() == ERROR_BOUNDS);

int value = 7;
VERIFY(checkNotNull(&value) == ERROR_NONE);
VERIFY(checkNotNull(NULL) == ERROR_INVALID_OPERATION);
VERIFY(getErrorCode() == ERROR_INVALID_OPERATION);

VERIFY(getErrorMessage(ERROR_MEMORY_ALLOCATION_FAILED) != NULL);
printLastError(stdout);
return TEST_SUCCESS;
}

最后,在main函数中调用测试函数,并将需要测试的测试用例传入,即可对程序功能进行完整测试。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#include <stdio.h>
#include <stdlib.h>
#include "test_and_validation.h"

int main() {
const TestCase testCases[] = {
{ "Queue Operations", testQueueOperations },
{ "Memory Management", testMemoryManagement },
{ "Type Safety", testTypeSafety }
};

runTestSuite(testCases, sizeof(testCases) / sizeof(testCases[0]));

return EXIT_SUCCESS;
}
本文来源: 青江的个人站
本文链接: https://hanqingjiang.com/2026/03/25/20260325_C_genericAndComprehensive/
版权声明: 本作品采用 CC BY-NC-SA 4.0 进行许可。转载请注明出处!
知识共享许可协议
赏

谢谢你请我喝可乐~

支付宝
微信
  • Notes
  • C

扫一扫,分享到微信

微信分享二维码
自托管统一认证:Pocket ID + Tinyauth 完整部署指南
  1. 1. 1. 再谈头文件与编译
  2. 2. 2. 编写头文件:函数声明和函数实现
  3. 3. 3. 泛型编程:比较与排序
  4. 4. 4. 企业案例:自定义函数处理比较器
  5. 5. 5. 指针的作用域和生命周期
  6. 6. 6. 悬挂指针(Dangling pointer)
  7. 7. 7. 可变参数(Variadic function final)
  8. 8. 8. 练习:自定义日志函数
  9. 9. 9. assert断言
  10. 10. 10. 断言的debug与练习
  11. 11. 11. 企业案例:日志系统与指针问题处理的架构设计
    1. 11.1. 11.1 logger
    2. 11.2. 11.2 内存管理
    3. 11.3. 11.3 error_handling
    4. 11.4. 11.4 pointer_safety空指针、野指针、悬挂指针的处理
    5. 11.5. 11.5 application_logic模块
    6. 11.6. 11.6 测试
    7. 11.7. 11.7 写入文件
  12. 12. 12. 环境变量的读写
  13. 13. 13. 命令行参数
  14. 14. 14. 小案例:命令行程序的编写
  15. 15. 15. 案例:自定义泛型队列
    1. 15.1. 15.1 节点结构模块
    2. 15.2. 15.2 安全内存处理模块
    3. 15.3. 15.3 测试运行模块
© 2021-2026 青江的个人站
晋ICP备2024051277号-1
powered by Hexo & Yilia
  • 友链
  • 搜索文章 >>

tag:

  • 生日快乐🎂
  • 新年快乐!
  • 小技巧
  • Linux
  • 命令
  • 语录
  • 复刻
  • Blog
  • Notes
  • Android
  • FPGA
  • Homework
  • MATLAB
  • C
  • Server
  • Vivado
  • Git

  • 引路人-稚晖
  • Bilibili-稚晖君
  • 超有趣讲师-Frank
  • Bilibili-Frank