一、前言
由于C语言的灵活性,用C语言开发出来的程序容易造成内存泄漏、运行异常、运行结果不可预期等程序质量问题,在用C语言开发程序的过程中,必须高度重视程序质量问题,应当把提高程序稳定性的方法加入到项目管理和开发过程中,最大限度地提高程序的稳定性,保证项目的成功开发。在这里总结多年来的C语言开发经验,拿出来共享以期在这方面能够得到更多的指教。
二、影响程序稳定性的因素
1、内存泄漏。造成内存泄漏的原因有:
1)、程序有多个出口,但不能保证在每一个出口能够完全释放掉所有的动态内存,如函数内有多个“return”,但没有在每一个“return”前释放掉在原已申请但必须释放的动态内存;
2)、对于“struct”数据结构,没有完全释放掉每一个指向动态内存的指针,如只释放指向“struct”数据结构指针没有释放“struct”体内的指针或某些指针被漏释放;
3)、对于用动态内存建立的链表在释放时没有一个一个结点去释放;
4)、一段动态内存空间原来只被一个指针引用,但在这个指针引用另外一段内存空间的时候,该段内存没有被释放;
5)、对于在函数内申请但必须在函数外释放的动态内存,在对该内存使用后忽略该动态内存的释放;
6)、用户强行退出程序,程序在退出前不能完全释放掉所有的动态内存;
7)、程序运行过程中发生了异常导致动态内存未被释放。
2、程序运行发生异常。造成异常产生的原因有:
1)、释放指针时该指针为空或是一个已被释放但释放后未被置空的指针;
2)、对于C库中的函数,如字符串操作函数,在调用该类函数时实参为空指针或者改指针没有指向可用的内存地址空间或者所指向的内存空间大小不足以用来实现当前的字符串操作;
3)、对于指向一个“struct”数据结构的指针,当指针为空时使用“struct”的分体数据;
4)、数组或指针发生越界操作;
5)、指针指向一个已被释放但释放后未被置空的指针,如一个全局变量的指针,在一个地方被释放后,但指针值未被置空,这时在另一个地方引用该指针的值时会发生异常;
6)、更改定义为常量的值;
7)、动态申请完一个内存后,未检查是否申请成功就调用了该指针;
8)、对于一块连续的内存块和“struct”数据结构在第一次使用时没有做初始化操作。
9)、在用非ASCII(如中文字符、Unicode)编码时,若使用char*来申请空间,在用C库中的字符串操作函数来操作,会因无法判断字符串结束位置而产生异常。
10)、指针类型强制转换时,当强制转换后指针指向的内存空间大于原来指针指向的内存空间时可能会出现异常(取决于堆或栈空间的结构和大小),如把“INT12*”强制转换成“INT32*”,应当尽量避免指针类型的强制转换;
11)、更改了数据结构,但代码没有相应更新或整个工程中相关文件没有做相应更新;
12)、申请的栈空间或堆空间超出了系统的容量限制;
13)、栈溢出,当函数中定义一个太大的数组时容易造成栈溢出,递归调用太深也容易造成栈举出;
14)、全局变量使用混乱,造成程序错乱;
16)、内存碎片太多,造成内存分配失败而导致程序异常,如建立一个太长的链表容易造成大量内存碎片;
17)、文件操作过于频繁(特别是写操作),系统应付不过来容易造成程序出现异常,这个在嵌入式系统中较常见。
三、内存泄漏预防措施
1、在代码审查时,检查函数体内的每一个“return”前是否有没有释放必须要释放的指针;
2、设计“struct”数据结构时,应当设计相应的释放“struct”指针的函数,并确保所有的“struct”体内的指针都被释放;
3、对于用动态内存建立的链表在释放时要一个一个结点去释放,对于每一个链表也要有相应的链表内存管理函数,如链表的释放函数;
4、当一个指针变量要指向另一个动态内存地址时先检查一下该指针是否有指向另一个动态内存地址,如果有则应当考虑是否要先释放掉原先的指向的动态内存;
5、在调用一个函数时,对于函数的输出值要确认值的内存空间是否是在函数内部动态申请,如果是则应当考虑是适当的时候把它释放掉;
6、减少程序的出口的数目,最好是一个出口,在出口处理函数中确保释放所有的动态内存;
7、当用户强行退出时,要考虑在每一个退出点是否能够释放所有的动态内存;
8、释放掉一个指针所指的内存空间后,就立即把改指针置为空;
9、少用动态申请内存,能用数组代替的就用数组的形式;
10、尽量减少全局变量的使用,避免指针指向的混乱;
11、封装动态内存申请和释放的底层函数,便于检查内存泄漏问题;
12、把内存泄漏的检查方法放进设计代码中,便于发现内存泄漏。
四、程序运行异常预防措施
1、在释放指针前先检查指针是否为空;
2、当把指针作为参数传入C库函数中的参数时,先检查指针是否为空;
3、在函数体内,当要调用指针参数时,先判断该指针是否为空;
4、当要调用“struct”指针数据结构中的分体时要先判断该指针是否为空;
5、当做指针移动操作时要考虑指针是否会发生越界;
6、当一个函数体内可能会改变参数中的值时,要避免传入常量形式的值,在设计函数时要尽量避免试图去改变参数中的值;
7、动态申请完一个内存后要先检查是否申请成功;
8、对于一块连续的内存块和“struct”数据结构在第一次使用时要做初始化操作,如申请完内存后,记得用memset清空内存;
9、备案所有的全局变量,考虑全局变量对程序可能产生的影响,尽量少用全局变量。对于全局变量的定义最好使用“static”来申明,不让其它模块直接访问该全局变量,并且设计好相应的操作该全局变量的方法函数,在定义全局变量时要充分考虑好全局变量的初始化方法和程序结束时的处理方法,对于整个工程中的全局变量要进行登记管理,登记内容包括变量名、类型名、定义位置、使用范围、使用目的、初始化方法、程序结束时的处理方法及其它注意事项。
10、在用非ASCII(如中文字符、Unicode)编码时,要使用unsigned char*来申请空间,并记住申请空间大小,不要用C库中的字符串操作函数来操作。
11、记得申请足够的内存,比如,储存年份应该是5个空间而不是4个,记得保留‘/0’的空间;
12、在函数中最好不要定义占用内存太大的局部变量,否则容易造成栈溢出,对于较大内存的使用最好是使用堆内存空间的方法。由于栈溢出这种情况比较不常见,容易被人忽视,所以在发生因栈溢出而产生问题时往往不容易被发现原因所在;
13、尽量不频繁分配小块的内存;
14、在设计递归调用时要考虑递归调用可能的深度,防止出现栈溢出;
15、不要定义太多的局部变量,如果要定义一个数组类型的局部变量,数组不要太长,以防止出现栈溢出;
16、减少读写文件的次数,优化文件的读写方法。
五、其它提高程序的稳定性的方法
1、严格执行代码审查制度,在做代码审查前要先制定好审查方法和审查细节;
2、在编写代码前先做好设计流程图,清晰化整个设计意图;
3、提高代码的可阅读性,改善编码风格,给函数添加注释和使用说明,函数的作用,入口参数和出口参数,作者、修改时间都要说明,代码中添加足够的注释性文字。