程序人生 | 我的《C陷阱与缺陷》读书笔记

本文首发于 2014-08-04 17:56:55

第一章 词法“陷阱”

1. =不同于==

1
2
if(x = y)
break;

实际上是将 y 赋给 x,再检查 x 是否为 0。

如果真的是这样预期,那么应该改为:

1
2
if((x = y) != 0)
break;

2. &和| 不同于 && 和 ||

3. 词法分析中的“贪心法”

编译器将程序分解成符号的方法是:从左到有一个一个字符的读入,如果该字符可能组成一个符号,那么再读入下一个字符,判断已经读入的两个字符组成的字符床是否可能是一个符号的组成部分;如果可能,继续读入下一个字符,重复上述判断,直到读入的字符组成的字符串已不再可能组成一个有意义的符号。例如:

1
y = x/*p; 会被解析为:/* 注释符号

4. 整型常量

010(八进制数) 不同于 10(十进制)。

5. 字符与字符串

首先是单引号与双引号的区别:

  • 用单引号括起来的一个字符表示一个整数(ASCII 码),而双引号括起来表示一个指针。

第二章 语法“陷阱”

1. 理解函数声明

弄懂(*(void(*)())0)(); //首地址为 0 的函数。

float (*h)(): h 是一个指向返回值为浮点型的函数的指针

所以,(float (*)()) 表示一个“指向返回值为浮点型的函数的指针”的类型转换符。

fp(): 是(*fp)( )的简写。

*fp(): 是 *( (*fp) ( ) )的简写。

1
( *0 )( );

虽然上式编译器不认,但可以把 0 转换为指向“返回值为 void 的”函数的指针,所以 0 可变为: ( void(*) ( ) ) 0 ,代入(*0)(),得到:

1
(*( void(*) ( ) ) 0) ( )

该式子用等价于:

1
2
typedef void  ( *func ) ( );
( *( func ) 0 ) ( );

类似的,signal.h 中对 signal 函数的声明:

1
2
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);

2. 运算符优先级的问题

优先级 运算符 名称或含义 使用形式 结合方向 说明
1 [] 数组下标 数组名[常量表达式] 左到右
1 () 圆括号 (表达式) 函数名(形参表) 左到右
1 . 成员选择(对象) 对象.成员名 左到右
1 -> 成员选择(指针) 对象指针->成员名 左到右
2 - 负号运算符 -表达式 右到左 单目运算符
2 (类型) 强制类型转换 (数据类型)表达式 右到左
2 ++ 自增运算符 ++变量名 变量名++ 右到左 单目运算符
2 自减运算符 –变量名 变量名– 右到左 单目运算符
2 * 取值运算符 *指针变量 右到左 单目运算符
2 & 取地址运算符 &变量名 右到左 单目运算符
2 ! 逻辑非运算符 !表达式 右到左 单目运算符
2 ~ 按位取反运算符 ~表达式 右到左 单目运算符
2 sizeof 长度运算符 sizeof(表达式) 右到左
3 / 表达式 / 表达式 左到右 双目运算符
3 * 表达式*表达式 左到右 双目运算符
3 % 余数(取模) 整型表达式%整型表达式 左到右 双目运算符
4 + 表达式+表达式 左到右 双目运算符
4 - 表达式-表达式 左到右 双目运算符
5 << 左移 变量<<表达式 左到右 双目运算符
5 >> 右移 变量>>表达式 左到右 双目运算符
6 > 大于 表达式>表达式 左到右 双目运算符
6 >= 大于等于 表达式>=表达式 左到右 双目运算符
6 < 小于 表达式<表达式 左到右 双目运算符
6 <= 小于等于 表达式<=表达式 左到右 双目运算符
7 == 等于 表达式==表达式 左到右 双目运算符
7 != 不等于 表达式!= 表达式 左到右 双目运算符
8 & 按位与 表达式&表达式 左到右 双目运算符
9 ^ 按位异或 表达式^表达式 左到右 双目运算符
10 | 按位或 表达式|表达式 左到右 双目运算符
11 && 逻辑与 表达式&&表达式 左到右 双目运算符
12 || 逻辑或 表达式||表达式 左到右 双目运算符
13 ?: 条件运算符 表达式 1? 表达式 2: 表达式 3 右到左 三目运算符
14 = 赋值运算符 变量=表达式 右到左
14 /= 除后赋值 变量/=表达式 右到左
14 *= 乘后赋值 变量*=表达式 右到左
14 %= 取模后赋值 变量%=表达式 右到左
14 += 加后赋值 变量+=表达式 右到左
14 -= 减后赋值 变量-=表达式 右到左
14 <<= 左移后赋值 变量<<=表达式 右到左
14 >>= 右移后赋值 变量>>=表达式 右到左
14 &= 按位与后赋值 变量&=表达式 右到左
14 ^= 按位异或后赋值 变量^=表达式 右到左
14 |= 按位或后赋值 变量|=表达式 右到左
15 , 逗号运算符 表达式,表达式,… 左到右

3. 其他

主要是别多写分号,switch 别忘了 break,别写空 else 分支。

第三章 语义“陷阱”

1. 指针与数组

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct {
Int p[4];
Double x;
}b[17];

int calendar[12][31];
int (*p)[31];
sizeof(calendar):12*31=372

calendar[0] // 指向该一维数组,对应*p
calendar[0][0]
......
calendar[0][30]
calendar[1] // 指向该一维数组,对应*(p+1)
calendar[1][0]
......
calendar[1][30]
......
......
......
calendar[11] // 指向该一维数组,对应*(p+11)
calendar[11][0]
......
calendar[11][30]

2. 内存分配

1
free(r);

用 malloc 显式分配的空间,不会再退出本函数后自动释放掉,而是会等程序员显式释放后才消失。

注意检查,malloc 分配的内存可能失败。

C 语言中会自动地将作为函数参数的数组声明转换为对应的指针声明,如:

1
2
3
int strlen(char s[ ]){ }等价于int strlen(char *s){ }
但在其他情形下不会自动转换,也就是说不等价,如:
extern char hello[ ];和extern char *hello;完全不同。

边界计算

自己实现一个 memcpy 函数:

1
2
3
4
5
void memcpy(char *dest, const char *source, int k)
{
while( --k >= 0 )
*dest++ = *source++;
}

重点是:操作时一定要知道操作数据的长度。

整数溢出

  • 两个有符号整数相加会发生溢出。
  • 两个无符号整数相加不会发生溢出。
  • 一个有符号和一个无符号整数相加,因为有符号被自动转换成无符号,所以也不会溢出。

第四章 连接

编译器一般每次只处理一个文件。编译器的责任是把 C 源程序翻译成对连接器有意义的形式。

许多系统中的连接器是独立于 C 语言实现的,因此如果链接时候错误原因是与 C 语言相关的,连接器无法判断错误原因。但连接器能够理解机器语言和内存布局。

典型的连接器把由汇编器或编译器生成的若干个目标模块,整合成一个被称为载入模块或可执行文件的实体。

连接器通常把目标模块看成是由一组外部对象组成的。每个外部对象代表着机器内存中的某个部分,并通过一个外部名称来识别。因此,程序中的每个函数和每个外部变量,如果没有被声明为 static,就都是一个外部对象。static 的不会与其它源程序文件中的同名函数或同名变量发生冲突。对于非 satatic 的函数或变量的名称冲突的解决办法将在后面讨论。

除了外部对象外,目标模块中还可能包括了对其他模块中的外部对象的引用,当连接器读入一个目标模块时,它必须解析出这些引用,并作出标记说明这些外部对象不再是未定义的。

连接器的输入是一组目标模块文件和库文件。输出是一个载入模块。

避免外部变量的函数的冲突和不一致等问题的办法:

每个外部对象只在一个头文件里声明,需要用到该外部对象的所有模块都应该包括这个头文件。

定义该外部对象的模块也应该包括这个头文件。

第五章 库函数

没什么好说的,就是 apue 的一些函数而已。

第六章 预处理器

宏定义:主要是理解宏不是函数,而是直接替换

  1. 不能忽视宏定义中的空格:
1
#define f (x) ( (x)-1 ):因为f后面多了一个空格,所以f(x)代表(x) ( (x)-1 )
  1. 宏并不是函数,所以注意那些括号:
1
2
#define abs(x) ( ( (x) >= 0)?(x):-(x) )
#define max(a,b) ( (a)>(b)?(a):(b) )
  1. 宏并不是语句:
1
#define assert(e) if (!e) assert_error(__FILE__, __LINE__)
  1. 宏不是类型定义
  • 错误用法:
1
2
#define int_8_ int*
int_8 a,b; //则a是指针,b是int型
  • 正确用法:应该用 typedef
1
typedef int * int_8_;

第七章 可移植性缺陷

主要是:

  1. 应对 C 语言标准的变更;
  2. 标识符名称的限制;
  3. 整数的大小;
  4. 字符是有符号整数还是无符号整数;
  5. 移位运算符;
    1. 在向右移位时,空出的位是由 0 填充还是 1,还是由符号位的副本填充?如果被移位对象是无符号数,那么由 0 填充;如果是有符号数,那么是 0 或符号位的副本。
    2. 移位操作的位数允许的取值范围是什么?如果被移位对象的长度是 n 位,那么移位计数必须大于或等于 0,而严格小于 n。
  6. 移植性需考虑的地方:
    1. 机器的字符表不同。
    2. 有的机器是 one’s complement,有的机器是 two’s complement 的。基于 2 的补码的计算机,所允许表示的附属取值范围要大于正数取值范围,所以有时取负值的运算会导致溢出。
    3. 各机器对取模运算的定义不同。

第八章 惯用与答案

将惯用的c == '\t'写作'\t' == c

一旦写错成=号,编译器就能检查出来。


欢迎关注我的微信公众号【数据库内核】:分享主流开源数据库和存储引擎相关技术。

欢迎关注公众号数据库内核
标题 网址
GitHub https://dbkernel.github.io
知乎 https://www.zhihu.com/people/dbkernel/posts
思否(SegmentFault) https://segmentfault.com/u/dbkernel
掘金 https://juejin.im/user/5e9d3ed251882538083fed1f/posts
CSDN https://blog.csdn.net/dbkernel
博客园(cnblogs) https://www.cnblogs.com/dbkernel
文章目录
  1. 1. 第一章 词法“陷阱”
    1. 1.1. 1. =不同于==
    2. 1.2. 2. &和| 不同于 && 和 ||
    3. 1.3. 3. 词法分析中的“贪心法”
    4. 1.4. 4. 整型常量
    5. 1.5. 5. 字符与字符串
  2. 2. 第二章 语法“陷阱”
    1. 2.1. 1. 理解函数声明
    2. 2.2. 2. 运算符优先级的问题
    3. 2.3. 3. 其他
  3. 3. 第三章 语义“陷阱”
    1. 3.1. 1. 指针与数组
    2. 3.2. 2. 内存分配
  4. 4. 第四章 连接
  5. 5. 第五章 库函数
  6. 6. 第六章 预处理器
  7. 7. 第七章 可移植性缺陷
  8. 8. 第八章 惯用与答案
|