本文整理了 C 语言开发中的可能出现的代码问题,包括返回值处理、断言的使用、系统资源使用、内存释放、内存越界、空野指针和未初始化,并给出了必要的解释和错误示例代码。

返回值处理(TOP1)

  1. 被调函数执行结果对业务流程有影响时,调用者却 没有处理其返回值

    • 若返回值是指针类型,可能导致空指针访问——如被调函数申请内存失败;
    • 若返回值是多种返回值系列,可能导致缺少必要的回退——如中间某步异常退出导致的的资源泄漏。
  2. 调用者对被调函数的返回值处理不准确,导致有隐患或问题。

    • 返回值数据类型被错误转换——如返回值为 int 类型,却被强转为 bool 类型;
    • 返回值比较的目标不是该函数的返回值系列——如用函数 A 的返回值系列跟被调函数 B 的返回值作比较。

断言的使用(TOP2)

  1. 使用断言错误,包括:
    • 在 debug 断言中包含非逻辑表达式——如 DBGASSERT(0 == func(&a, &b)) 包含了业务逻辑;
    • 对程序运行中可能发生的情况使用了断言检查。
1
2
3
4
5
6
7
8
9
10
11
12
// gcc -D_DEBUG_VERSION xxx.c  or  gcc xxx.c
#ifdef _DEBUG_VERSION
#define DBGASSERT(expression) \
do { \
if (!(expression)) { \
fprintf(stderr, "Assertion failed: %s, file %s/%s:%d\n", #expression, __FILE__, __FUNCTION__, __LINE__); \
abort(); \
} \
} while (0)
#else
#define DBGASSERT(expression) ((void)0)
#endif

断言用于对程序运行过程中 不应该发生 的情况进行 检查

条件判断用于对程序运行过程中 可能发生 的情况进行 处理

系统资源使用(TOP3)

  1. 资源的申请释放不在同一层次或者不对称。
  2. 在成对的系统资源操作之间异常退出。
  3. 过早的申请资源,导致不必要的异常回退或资源泄漏等问题。
  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
32
33
34
35
typedef struct tag_student {
int age;
char* pcName;
char* pcCard;
} STUDENT_S;

void example1_wrong(...) {
STUDENT_S* pstStu = (STUDENT_S*)malloc(sizeof(STUDENT_S));
DBGASSERT(NULL != pstStu);
while (...) {
...;
if (...) {
free(pstStu); // 资源释放不在同一层次
return;
}
...;
}
...;
return;
}

void example1_correct(...) {
STUDENT_S* pstStu = (STUDENT_S*)malloc(sizeof(STUDENT_S));
DBGASSERT(NULL != pstStu);
while (...) {
...;
if (...) {
break;
}
...;
}
...;
free(pstStu); // 资源释放在同一层次
return;
}

上面的错误示例是申请资源在外层、释放资源在内层;另一种不在同一层是:定义在外层,申请资源在内层、释放资源在外层——可能导致资源泄漏或空指针问题。

例子(问题二)

在成对的系统资源操作之间异常退出,异常退出前忘记释放资源。

1
2
3
4
5
6
7
8
9
10
11
void example2_wrong(...) {
STUDENT_S* pstStu = (STUDENT_S*)malloc(sizeof(STUDENT_S));
DBGASSERT(NULL != pstStu);
...;
if (...) {
return; // 异常退出应释放资源
}
...;
free(pstStu);
return;
}

例子(问题三)

过早的申请资源,导致不必要的异常回退或资源泄漏等问题。资源申请应在合法性和前提条件满足之后再进行。

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 case1 = 10;

void example3_wrong(...) {
STUDENT_S* pstStu = (STUDENT_S*)malloc(sizeof(STUDENT_S));
DBGASSERT(NULL != pstStu);
if (10 == case1) {
return;
}
...;
free(pstStu);
return;
}

void example3_correct(...) {
STUDENT_S* pstStu = NULL;
if (10 == case1) {
return;
}
/* 合法性和前提条件已满足 */
pstStu = (STUDENT_S*)malloc(sizeof(STUDENT_S));
DBGASSERT(NULL != pstStu);
...;
free(pstStu);
return;
}

例子(问题四)

将申请的资源直接赋给间接变量(包括:数据结构字段、多级指针、全局变量),这可能会导致一些潜在的错误不能被检测工具检测到。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#define MAC_ADDR_LEN (6)
#define MAC_LEN (14)

void example4_wrong(...) {
*ppstData = (unsigned char*)malloc(MAC_ADDR_LEN * sizeof(unsigned char));
DBGASSERT(NULL != *ppstStu);
memcpy(*ppstData, aucMacAddr, MAC_LEN); // 复制的长度大于申请的长度
...;
}

/* 上述低级错误无法通过工具检测,因为资源被直接赋给了多级指针 */
void example4_correct(...) {
unsigned char* pcTempData = NULL;
pcTempData = (unsigned char*)malloc(MAC_ADDR_LEN * sizeof(unsigned char));
DBGASSERT(NULL != pcTempData);
/* 若这里长度书写错误,比如超长,pclint 可以触发 Warning 669 */
memcpy(pcTempData, aucMacAddr, MAC_LEN);
*ppstData = pcTempData; // 所有操作成功或结束后,挂接资源到相应的变量或数据结构上
...;
}

对于数据结构的子内存,如对学生的身份证 pstStu->card 申请资源时,也应先将申请资源挂到局部指针变量上,万事俱备,再赋给数据结构的子内存 pstStu->card = pcTempCardInfo

内存释放(TOP4)

  1. 用不匹配的内存释放函数释放内存资源。
  2. 释放非法地址、内存重复释放、释放后再使用。
  3. 释放内存前没有先从数据结构上摘除。
  4. 内存资源泄漏,没有第一时间释放资源。

解释(问题一):

  • 如用结构体 B 的内存释放函数,去释放结构体 A 的指针变量动态申请的内存资源。

解释(问题二):

  • 释放非法地址:试图使用 free() 函数释放一个未经 malloc() 或类似函数分配的内存地址;
  • 内存重复释放:如将内存资源传递给某接口(该接口负责内存资源的释放),这时不应再次释放内存资源;
  • 释放后再使用:释放内存后,该内存块的内容和所有权已经归还给系统,再次访问这块内存会导致不可预测的结果。因此,最好在释放内存资源后,将指针变量赋值为 NULL,这样再次使用会报空指针错误

解释(问题三):

  • 如删除链表中的节点时,先释放了对应节点的内存资源,再尝试从链表中剔除该节点。应该先剔除再释放对应节点的内存资源。

解释(问题四):

  • 如在网络收包接口中,申请了内存资源,但未正确释放,造成了内存泄漏。这样,在大流量的冲击下,短时间内会使系统可用资源耗尽,导致系统崩溃。

内存越界(TOP5)

  1. 字符串、内存拷贝或清零等操作越界。
  2. 缓冲区空间太小导致数据溢出。
  3. 非法参数没有检查导致访问越界。

例子(问题一)

字符串拷贝越界。如果以动态申请的内存资源大小为准,我们应该拷贝ulLen-1,但其实申请的内存资源大小是错的,应该是ulLen+1,要包含字符串末尾的\0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
unsigned long ulLen = strlen(pcWord);

void example1_wrong(...) {
unsigned char* pcTemp = (unsigned char*)malloc(ulLen * sizeof(unsigned char)); // 少 `\0` 的位置
DBGASSERT(NULL != pcTemp);
strncpy(pcTemp, pcWord, ulLen); // 拷贝的字节数错误
}

void example1_correct(...) {
unsigned char* pcTemp = (unsigned char*)malloc((ulLen + 1) * sizeof(unsigned char));
DBGASSERT(NULL != pcTemp);
strncpy(pcTemp, pcWord, ulLen);
pcTemp[ulLen] = '\0';
}

内存拷贝越界,内存拷贝应 以目的缓冲区的字节数为依据,才能实现防止访问越界

1
2
3
unsigned char* pcTemp = (unsigned char*)malloc(10 * sizeof(unsigned char));
strncpy(pcTemp, pcWord, (unsigned long)sizeof(pcWord)); // wrong
strncpy(pcTemp, pcWord, (unsigned long)sizeof(pcTemp)); // correct

内存清零错误。memset 函数的第三个参数应该是要设置的字节数,而不是数组的大小。

1
2
3
4
int* arr = (int*)malloc(10 * sizeof(int));
DBGASSERT(NULL != arr);
memset(arr, 0, sizeof(arr)); // wrong, sizeof(arr) i.e., sizeof(int*) == 4
memset(arr, 0, 10 * sizeof(int)); // correct

例子(问题二)

缓冲区空间太小、字符串拼装越界。推荐使用 n 系列函数,比如 snprintf。

1
2
3
4
5
6
7
8
9
void example2_wrong(...) {
char name[10] = {0};
sprintf(name, "name-%ld", ulId); // ulong 值填充后很可能超过缓冲区大小
}

void example2_correct(...) {
char name[10] = {0};
snprintf(name, 10, "name-%ld", ulId); // 限制最大填充大小
}

空野指针(TOP6)

  1. 释放全局变量上的资源后没有清零全局变量。
  2. 释放数据结构上挂接的内存后没有清零数据结构上的挂接字段
  3. 访问空指针。

例子(问题一)

释放全局变量上的资源后没有清零全局变量。

1
2
3
4
5
6
7
8
9
int* arr = (int*)malloc(10 * sizeof(int));
DBGASSERT(NULL != arr);
free(arr);
arr[0] = 100; // 不会报错,但 free 后 arr 地址已经是野指针

/* 正确做法 */
free(arr);
arr = NULL;
arr[0] = 100; // segment fault

例子二(问题二)

释放数据结构上挂接的内存后没有清零数据结构上的挂接字段。

1
2
3
4
5
6
7
8
9
10
11
12
STUDENT_S* pstStu = (STUDENT_S*)malloc(sizeof(STUDENT_S));

void example2_wrong(...) {
char* pcTempCard = (char*)malloc(18 * sizeof(char));
DBGASSERT(NULL != pcTempCard);
pstStu->pcCard = pcTempCard; // 动态资源挂接到数据结构的字段
...;
if (...) {
free(pcTempCard); // pstStu->pcCard 字段没有清零
return;
}
}

未初始化(TOP7)

  1. 导致访问未初始化的变量或数据结构字段或动态内存。
  2. 函数的入参未被初始化
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
void example1_wrong(...) {
STUDENT_S stStu;
if (...) {
stStu.age = 18; // 条件不成立时,该字段不会初始化
}
stStu.pcName = "lihua";
printf("%s: %d\n", stStu.pcName, stStu.age); // 可能使用未初始化的字段
...;
}

int nr;

void example2_wrong(int *nr) {
(*nr)++;
...;
}
example2_wrong(&nr); // 从几开始加?