C语言编程易错点总结

前言

(高亮)
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()函数执行完成就一定会立刻显示在屏幕上的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值