总述
C 陷阱与缺陷读书笔记,批判阅读
C 陷阱与缺陷
第1章 词法陷阱
1.1 =不同于==
不应该关闭警告选项,而应进行显式比较
1 | // improper |
赋值误写为比较
1 | if ((filedsc == open(argv[i], 0)) < 0) |
1.2 &和|不同于&&和||
见后
1.3 词法分析中的“贪心法”
1 | a---b; |
1 | y = x/*p; // is regarded as MULTI |
1 | y = x / *p; // x point to p |
1.4 整型常量
1-4.c
不允许出现9
1.5 字符和字符串
双引号引起的字符串代表指向无名数组起始字符的指针,无名数组用引号之间的字符以及二进制为0的字符
\0
初始化
练习题
第2章 语法陷阱
2.1 理解函数声明
C 变量的声明由两部分构成:类型以及一组类似表达式的声明符(declarator)
声明符与表达式类似,对它求值返回一个声明中给定类型的结果
1 | float ((f)); |
这个声明的含义是,当对其求值时,((f))
的类型为浮点类型,由此推知 f
的类型也是浮点类型
且 ()
结合优先级高于 *
1 | float *g(), (*h)(); |
知道如何声明一个类型的变量后就很容易得到该类型的类型转换符了:去掉声明中的变量名和末尾分号
1 | (float (*)()) |
调用函数
1 | (*fp)(); |
todo
第3章 语义陷阱
3.1 指针与数组
二维数组事实上是一个指向一维数组的指针数组
所以取行就只需取一个下标
3.2 非数组的指针
malloc()
可能会返回 NULL
作为内存分配失败的标志(不过一般来说也许不用?)
3.3 作为参数的数组声明
用数组作为函数参数没什么意义,和传入首地址是等效的
他卖了个关子 todo
1 | extern char *hello; |
3.4 避免“举隅法”
玄之又玄,大概就是表达复制指针并不复制原始内容,即shallow copy,当然,修改字符串常量的行为貌似是UB,与编译器实现有关
3.5 空指针并非空字符串
编译器保证由0转换而来的指针不等于任何有效的指针
1 |
不能访问空指针指向的内容
3.6 边界计算与不对称边界
不对称边界:左闭右开,在数学上的不对称的丑陋带来了编程时出奇的方便
另一种看待不对称问题的方式:把上界视作某序列中第一个被占用的元素,把下界视作序列中第一个被释放的元素
来看一段例子:
1 |
|
ANSI C 标准中允许与“溢界”的元素地址相比较,但是对其引用是非法的
memcpy()
提供了一次复制多个字符的方法,并且通常借助汇编语言的实现提高速度
1 | void bufwrite(char *p, int n) { |
在“师出有名”的情况下,我们应该有信心写对这些技巧性很强的代码
另一个打印给定行号or列号字符的函数
3.7 求值顺序
主要说了短路求值
3.8 运算符&&、||和!
逻辑运算符只会返回0或1
&和|两侧的值都会算出,而不会短路求值
3.9 溢出
3.10 为函数 main 提供返回值
忘记显示声明 main 返回值为 int,而又在前面定义了其他 struct
第4章 连接
4.1 什么是连接器
4.2 声明与定义
外部变量尽量不要重复定义
1 | // file1.c |
4.3 命名冲突与static修饰符
1 | static int a; |
将 a 的作用域限制在一个源文件内
因此,如果若干个函数需要共享一组外部对象,可以将这些函数放到一个源文件中,把他们需要用到的对象在同一个源文件中用 static 声明,如果一个函数仅仅被同一个源文件中的其他函数调用,我们就应该声明该函数为 static
4.4 形参、实参与返回值
如果一个函数在被定义或声明之前被调用,它的返回类型默认为 int
C 中形参和实参匹配的规则略为复杂,ANSI C 允许程序员在声明时指定函数的参数类型,如
1 | double square(double); |
1 | square(2); // legal, is same as square((double)2) |
如果一个函数没有 float、short 或者 char 类型的参数,在函数声明中可以省略参数类型的说明
1 | int isvowel(); |
scanf 和 printf
1 |
|
字符 c 附近的内存因为整型所占存储空间大于字符型而被覆盖
4.5 检查外部类型
保证 extern 变量与其声明一致
4.6 头文件
第5章 库函数
尽可能使用库函数
5.1 返回整数的getchar函数
不知所云,不管了
5.2 更新顺序文件
fread 操作,用到时候再细究
1 | while (fread( (char *)&rec, sizeof(rec), 1, fp) == 1) { |
5.3 缓存输出与内存分配
程序输出的两种方式:
- 即时处理(系统负担较大)
- 先暂存再大块写入
因此 C
语言实现通常都允许程序员进行实际的写操作之前控制产生的输出数据量,一般通过库函数
setbuf(stdout, buf)
实现
这个语句通知输入/输出库,所有写入到 stdout 的输出都应该使用 buf 作为缓冲区,直到 buf 被填满或者程序员直接调用 fflush(对于写操作打开的文件,fflush 会直接导致输出缓冲区的内容被实际写入该文件)
1 |
|
5.4 使用errno检测错误
先检测作为错误指示的返回值,确定已经执行失败,再检查 errno 确定出错原因
1 | if (/*wrong return value*/) { |
5.5 库函数signal
signal 是一种捕获异步事件的方式
信号是真正的“异步”,可能在 C 语言执行期间的任何时刻发生,甚至可能出现在 malloc 等复杂函数的执行过程中。因此从安全角度考虑,信号处理函数不应该调用上述类型的库函数
唯一绝对安全可移植的操作:print 错误语句,exit 退出
第6章 预处理器
主要讲的宏的事情
6.1 不能忽视宏定义中的空格
6.2 宏并不是函数
主要是括号的问题
但是比如下面这段就会出现重复计算的bug
经典的把 toupper
定义成宏:
1 | toupper(*p++); |
嵌套宏也可能导致代码过长,比如四层嵌套的 max
语句
其实写成四行 if-else 更好
6.3 宏并不是语句
6.4 宏并不是类型定义
省流:用 typedef
第7章 可移植性缺陷
就感觉...没太大必要
就摸了(不是)