目录
前言
(高亮)
C语言是一门面向过程的、抽象化的通用程序设计语言,广泛应用于底层开发。它是目前最著名,最流行的语言,效率高、功能强、用法灵活。
在学习编程语言的过程中,最怕最难的就是找BUG,而找BUG又是必不可少的能力,甚至可能是编程中必经的一个环节。有时候找BUG的时间甚至可能比写代码的时间还要长。语法上的错误可以在编译器的帮助下较轻松的解决,但是一些逻辑上的错误不仅仅难以修正,还可能难以察觉,它可能突然出现,又神秘消失。
这篇微博整理了常见的C语言易出现的错误,以及见过的比较隐晦的错误。
一、运算类
(是什么 为什么 怎么做)
除法
- 除数不能为0,使用除法的时候一定要检查除数是否有可能取到零值。
- 当除法运算符两边是整数的时候,返回的结果也是整数,使用的时候需要注意判断 / 运算符 两边是什么类型的变量。
运算过程中的类型转换
- 在进行 加减乘除 以及 比较 等运算的时候,要注意参与运算的都是什么类型的数据, 典型的例子是 有符号数 与 无符号数 比较大小、 整形 与 浮点型 进行 相等判断 。
&& 与 ||
- && 运算符的优先级高于 ||,在实际使用的过程中,应当对判定条件加上括号便于阅读。
自増与自减
- ++p; 与 p++; ,前者返回增加后的值,后者返回增加前的值。单独写很容易判断,但遇上 *p++;时,很容易只考虑到 p++ 的优先级比 *p 高,却忽略了p++返回值并不是增加之后的。
逻辑短路
&& 和 || 运算符具有短路特性。
- 对于 condition1 && condition2 ,当condition1为false时,condition2不执行。
- 对于 condition1 || condition2 ,当condition1为true时,condition2不执行。
- 所以尤其注意在condition2内执行具有修改变量功能的语句,如 i++ > 10, x = getValue() 等等。
! 与 ~
- ! 运算符代表 整体逻辑非 ,零值运算后返回 1,非零值运算后返回 0。常用于做条件判断。
- ~ 运算符代表 按位取反,其返回值与原值对应的二进制位相反。常用于位操作。非零值经过~运算之后不一定是零值。
右移运算
- 对于有符号数,需要特别考虑对于符号位的处理情况。对于算术右移,最高位填充符号位。正数填充0,负数填充1。
二、变量类
静态局部变量
需要注意当函数被调用多次时,静态局部变量是具有记忆的、共享的。
- 函数调用次序不一样,可能处理过程、返回值不一样。
- 第二次调用函数时,可能会使得第一次调用的结果发生变化。比如函数返回值为静态变量指针的情况。
static全局变量
需要注意static关键字会限定变量的作用域。
- 静态局部变量所具有的缺点,static全局变量也有。
- 在头文件定义static变量,并不会起到多个源文件共享变量的结果。
- 由于static变量作用域不会超出文件,多个源文件内static变量可以重名。
volatile变量
volatile关键字可以用来提醒编译器它后面所定义的变量随时有可能改变,因此编译后的程序每次需要存储或读取这个变量的时候,都会直接从变量地址中读取数据。
- 使用volatile变量的时候,自己也要明白它是随时变化的,连续取值的结果可能不一样。比如连续用于条件判断的时候,可能会发生你不希望的跳转。连续用于赋值的时候,可能赋的值并不相同。
整形变量
- 整形变量需要考虑有无符号,尤其是参与算术运算以及比较判断的时候,需要考虑 不同类型的隐式转换 以及 无符号数的非负数特性 。
- 使用整形变量需要注意其表示范围,注意超出范围之后的情况。
浮点变量
- 浮点变量不是精确的,尤其在判断相等的时候需要注意,不要使用== 运算符,而是使用一个极小值 EPSION 来比较。即 (exp < EPSION && exp > -EPSION)。
三、数组类
数组长度
- 对数组进行操作时,要检查是否有可能存在 下标为负数、越界 等问题。
- sizeof 不是函数,其结果可能是在编译期得出的,所以在运行时它的值可能不会变化,需要注意。
- sizeof 得到的是长度字节数,而不是数组元素个数。
- 在数组作为函数参数的时候,数组在函数内部其实是指针,此时不可以用sizeof来获取数组长度。
多维数组
- 多维数组在内存上是连续的。
- 多维数组定义时除了最高维之外其他维的长度一定要明确。
字符串
- 字符数组的末尾需要有一个 ’\0‘,用于表示字符串结束,尤其在 使用字符数组 和 申请动态内存 的时候要注意。
- 字符数组记得要初始化,赋值 和 使用 时注意末尾是否有 ’\0’ 存在,许多库函数是根据 ‘\0’ 来判断字符串是否结束的。
- 尽量不要使用二维字符数组来储存多行字符串,因为多维字符数组在内存上是连续的,如果某一行没有 ‘\0’ 存在,很可能会把多行语句当做一整条语句处理。
四、指针类
内存泄漏
- malloc分配内存有可能会失败 此时返回NULL。
- 用malloc 分配的内存记得用free释放,尤其是malloc和free不在同一个函数内部时,很容易忘记。
- 单片机编程时,尽量不要使用malloc函数。
内存溢出
- 由于堆栈空间不是无限的,即使没有内存泄漏,也有可能发生内存溢出。当程序运行时间长,递归调用函数、或者程序占用内存大时需要注意。
野指针
- 指针一定要初始化,弃用之后要记得赋值为NULL(注意全部大写)。
- 注意函数不能返回指向局部变量(非静态)的指针,因为函数结束后其内部局部变量(非静态)就释放了。即使使用的是静态局部变量的指针,也可能会存在其他问题,具体请看 变量类-静态局部变量。
重复释放
- 一快内存重复释放可能会发生问题,因此不能这么做。尤其是free同时存在于多个函数内部时,很容易发生这类事件。
const对象
- 注意 顶层指针 与 底层指针 的区别。
- 定义时使用 int const *p 、 int * const p 与 int const * const p 都不能保证 *p这个值在使用过程中是不变的。它只代表你不能对 *p 或者 p 赋值。
文件操作
- 文件打开之后一定要关闭,且关闭之后才能再打开。
- 一定要明确"r" “w” “a” “r+” “w+” “a+” 的意思再使用。
- 打开文件的数目是有限制的,包括stdin stdout stderr。
五、预处理类
头文件包含
- 头文件循环包含是极其不规范的行为,必须避免。
- 使用 宏定义防止重复包含:
#ifndef FILENAME_H
#define FILENAME_H
/* code */
#endif
宏定义替换
- 替换的内容不包括字符串里的内容。
- 如果宏内部有运算,注意替换后运算优先级是否有影响。
- 如果宏用作条件编译的条件,那么宏内部不可以有类型转换操作。
带参数的宏
- 使用参数进行运算时,要把参数用括号括起来,防止运算优先级问题。
- 如果带参宏内部要定义新变量,要注意潜在的变量名冲突,即使在大括号内定义依旧有风险,因为传入的参数可能有与之同名的变量。
- 宏不传入指针也可以改变传入的参数, 这一点与普通C语言函数有区别。
- 因为宏不会对参数类型进行检查, 所以在使用带参宏的时候需要明白参数类型是否符合条件。
- 宏的参数不要使用执行后会改变变量值的语句,如i++,函数调用等,因为不能确定宏展开后该语句执行了多少次。
六、输入输出类
缓冲区溢出
- 注意读取键盘输入内容时,读取结果很可能比预想的长。
参数赋值
- 注意用scanf()函数对参数赋值有可能失败,且一个参数赋值失败后不会继续对下一个参数赋值。所里需要对其返回值(表示正确赋值的个数)进行判断。
输入缓冲区
- 多次获取输入内容时,注意可能会读取到上一次未读完的内容,比如换行符’\n‘等等。一些格式如"%d"会忽略空白符,但是有一些不会,如"%c"。
输出显示
- 输出时注意设置的显示策略,是什么时候会显示到屏幕上。并不是printf()函数执行完成就一定会立刻显示在屏幕上的。