本文整理了 C 语言开发中的可能出现的代码问题,包括返回值处理、断言的使用、系统资源使用、内存释放、内存越界、空野指针和未初始化,并给出了必要的解释和错误示例代码。
返回值处理(TOP1)
-
被调函数执行结果对业务流程有影响时,调用者却 没有处理其返回值。
- 若返回值是指针类型,可能导致空指针访问——如被调函数申请内存失败;
- 若返回值是多种返回值系列,可能导致缺少必要的回退——如中间某步异常退出导致的的资源泄漏。
-
调用者对被调函数的返回值处理不准确,导致有隐患或问题。
- 返回值数据类型被错误转换——如返回值为 int 类型,却被强转为 bool 类型;
- 返回值比较的目标不是该函数的返回值系列——如用函数 A 的返回值系列跟被调函数 B 的返回值作比较。
断言的使用(TOP2)
- 使用断言错误,包括:
- 在 debug 断言中包含非逻辑表达式——如
DBGASSERT(0 == func(&a, &b))
包含了业务逻辑;
- 对程序运行中可能发生的情况使用了断言检查。
1 2 3 4 5 6 7 8 9 10 11 12
| #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 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); memcpy(pcTempData, aucMacAddr, MAC_LEN); *ppstData = pcTempData; ...; }
|
对于数据结构的子内存,如对学生的身份证 pstStu->card
申请资源时,也应先将申请资源挂到局部指针变量上,万事俱备,再赋给数据结构的子内存 pstStu->card = pcTempCardInfo
。
内存释放(TOP4)
- 用不匹配的内存释放函数释放内存资源。
- 释放非法地址、内存重复释放、释放后再使用。
- 释放内存前没有先从数据结构上摘除。
- 内存资源泄漏,没有第一时间释放资源。
解释(问题一):
- 如用结构体 B 的内存释放函数,去释放结构体 A 的指针变量动态申请的内存资源。
解释(问题二):
- 释放非法地址:试图使用
free()
函数释放一个未经 malloc()
或类似函数分配的内存地址;
- 内存重复释放:如将内存资源传递给某接口(该接口负责内存资源的释放),这时不应再次释放内存资源;
- 释放后再使用:释放内存后,该内存块的内容和所有权已经归还给系统,再次访问这块内存会导致不可预测的结果。因此,最好在释放内存资源后,将指针变量赋值为 NULL,这样再次使用会报空指针错误。
解释(问题三):
- 如删除链表中的节点时,先释放了对应节点的内存资源,再尝试从链表中剔除该节点。应该先剔除再释放对应节点的内存资源。
解释(问题四):
- 如在网络收包接口中,申请了内存资源,但未正确释放,造成了内存泄漏。这样,在大流量的冲击下,短时间内会使系统可用资源耗尽,导致系统崩溃。
内存越界(TOP5)
- 字符串、内存拷贝或清零等操作越界。
- 缓冲区空间太小导致数据溢出。
- 非法参数没有检查导致访问越界。
例子(问题一)
字符串拷贝越界。如果以动态申请的内存资源大小为准,我们应该拷贝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)); 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)); strncpy(pcTemp, pcWord, (unsigned long)sizeof(pcTemp));
|
内存清零错误。memset 函数的第三个参数应该是要设置的字节数,而不是数组的大小。
1 2 3 4
| int* arr = (int*)malloc(10 * sizeof(int)); DBGASSERT(NULL != arr); memset(arr, 0, sizeof(arr)); memset(arr, 0, 10 * sizeof(int));
|
例子(问题二)
缓冲区空间太小、字符串拼装越界。推荐使用 n 系列函数,比如 snprintf。
1 2 3 4 5 6 7 8 9
| void example2_wrong(...) { char name[10] = {0}; sprintf(name, "name-%ld", ulId); }
void example2_correct(...) { char name[10] = {0}; snprintf(name, 10, "name-%ld", ulId); }
|
空野指针(TOP6)
- 释放全局变量上的资源后没有清零全局变量。
- 释放数据结构上挂接的内存后没有清零数据结构上的挂接字段。
- 访问空指针。
例子(问题一)
释放全局变量上的资源后没有清零全局变量。
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); arr = NULL; arr[0] = 100;
|
例子二(问题二)
释放数据结构上挂接的内存后没有清零数据结构上的挂接字段。
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); return; } }
|
未初始化(TOP7)
- 导致访问未初始化的变量或数据结构字段或动态内存。
- 函数的入参未被初始化
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);
|