image.png

一、词法陷阱

1.1 =和==

=是赋值运算符,返回赋的值。
==比较运算符,返回True or False,或者是1 or 0。

  1. if (x = y)// 等价于 x=y;if(x)
  2. break;
  3. while(c = '') //死循环,空字符ASCII值非0
  4. c = getc(f);

1.2 &和&&,|和||

&和|是位运算符,&&和||是逻辑运算符。

1.3 词法分析中的贪心算法

编译器将分析程序的过程,从左到右挨个读入字符,如果字符可能组成一个符号则继续读入,直到不可能组成有意义的符号则停止。

  1. a --- b; //等同
  2. a -- - b; //等同
  3. a - -- b; //不同
  4. a+++++b; //等价
  5. a ++ + ++ b; //等价
  6. a ++ ++ + b; //不等价,a++的结果无法作为++的左值。

1.4 整形常量

0开头的整形是8进制,

1.5 字符与字符串

  1. printf("hello\n");
  2. //等价于
  3. char hello[] = {'h','e','l','l','o','\n',0};//0也可以是'\0',空字符
  4. printf(hello);

‘**yes**’是一个字符或者整形,它的值由’y’,’e’,’s’按照编译器定义的方式组合得到。
“yes”是一个字符串,有’y’,’e’,’s’,’\0’组成。

二、语法陷阱

2.1 理解函数声明

  1. (*(void(*)())0)();
  2. //等价于
  3. typedef void *func();
  4. func = 0;
  5. (*func)(); //启动地址0处的函数
  6. //分解过程
  7. 1、(*(void(*)())0)();
  8. 2、等价于(*(void(*func)())0)();
  9. void(*func)()是一个函数指针,用f简化代替,f是一个函数指针
  10. 3、等价于(*(f)0)();
  11. 4、等价于(*((f)0))();
  12. (f)0,强制转换0f类型指针,类似(int*)0,强制0转换为int*指针。用k代替,k是一个值为0的指针。
  13. 5、等价于(*(k))();
  14. *(k),取出指针0指向的void(*)()类型函数,用m代替,m是一个函数
  15. 6、(m)(); 调用m函数。
  16. float f; //等价
  17. float ((f)); //等价
  18. float *g(); //声明函数g
  19. float *(g()); //等价
  20. float *h(); //注意!这是声明一个名为h的函数。
  21. float (*h)(); //声明函数指针h
  22. (float (*)()) f; //强制转换为函数指针,类似(int*) f;d
  23. void (*signal(int, void(*)(int)))(int); //等价
  24. typedef void(*Handler)(int); //等价
  25. Handler signal(int, Handler)

2.2 运算符的优先级

  1. if(flags & FLAG); //等价
  2. if((flags & FLAG) != 0) //等价
  3. if(flags & FLAG != 0); //不等价
  4. r = hi << 4 + low; //等价
  5. r = hi << (4 + low); //等价
  6. r = (hi << 4) + low; //等价
  7. r = hi << 4 | low //等价

image.png
优先级排序:
1、不是真正的运算符:数组下标、函数调用操作,struct的.操作符。
2、单目操作符,++、—、!、~、、&、sizeof等。
3、双目操作符:
a、算术运算符,+、-、
、/
b、移位运算符,>>、<<。
c、关系运算符,>、<、==、!=。
d、逻辑运算符,&&、||、!
e、赋值运算符,=
4、三目运算符:条件运算符,?:。

2.3 注意语句结束标志的分号

2.4 switch语句

  1. switch(color){
  2. case 1: printf("red");
  3. break;
  4. case 2: printf("yellow");
  5. break;
  6. case 3: printf("blue");
  7. break;
  8. }
  9. //不等价
  10. switch(color){
  11. case 1:printf("red");
  12. case 2:printf("yellow");
  13. case 3:printf("blue")
  14. }

2.5 函数调用

  1. void f();
  2. f(); //执行函数f
  3. f; //计算函数f地址,不执行。

2.6 悬挂else引发问题

  1. if(x == 0)
  2. if(y == 0) error();
  3. else{
  4. z = x + y;
  5. }
  6. //等价于
  7. if(x == 0){
  8. if(y == 0) error();
  9. else{
  10. z = x + y;
  11. }
  12. }
  1. //为什么C支持14后的逗号?
  2. int days[] = {11,12,13,14,}
  3. //原因
  4. int days[] = {
  5. 11,12,
  6. 13,14,
  7. }

三、语义陷阱

3.1 指针与数组

C只有一维数组,编译器确定大小,数组元素可以是任何类型,如另外一个数组。

  1. //calendar是一个有12个元素的数组,每个元素是一个有31个int型元素的数组。
  2. //从左往右解释。
  3. int calendar[12][31];
  4. sizeof(calendar) == 12*31*sizeof(int); //true
  5. int i = calendar[4][7]; //等价
  6. int i = *(calendar[4] + 7); //等价
  7. int i = *(*(calendar+4) + 7); //等价
  8. int (*ap)[31] = calendar; //正确,指向数组的指针
  9. int *ap1 = calendar; //错误,ap1是整形指针,calendar是数组的指针
  10. int a[3] = {10, 20, 30};
  11. int *p = a;
  12. printf(*p); //10
  13. printf(*(p+1)); //20
  14. printf(*p+1); //11
  15. printf(sizeof(a)); //数组大小而非第一个元素大小。
  16. (p + 1) == (1 + p); //true
  17. a[i] == i[a]; //true

3.2 非数组的指针

  1. //将字符串s和t,连接一个字符串。
  2. char* p;
  3. p = malloc(strlen(s) + strlen(t)); //错误,必须+1,字符串有空字符结尾。
  4. strcpy(p, s);
  5. strcat(p, t);
  6. ...;
  7. free(p)

3.3 作为参数的数组声明

  1. int strlen(char s[]){}; //等价
  2. int strlen(char* s){}; //等价,C把数组声明自动转成指针声明。
  3. main(int, char* argv[]){}; //等价
  4. main(int, char** argv){}; //等价

3.4 避免举隅法

  1. char *p, *q;
  2. p = "xyz";
  3. q = p;
  4. *(p+1) = 'Y'; //错误,ANSIC C禁止对string literal进行修改,
  5. //K&R的说明是试图对字符串常量进行修改的行为是未定义的。
  6. //有些编译器甚至“助纣为虐”。

3.5 空指针

空指针不可以进行解引用(*)。

  1. if( p == (char*) 0); //合法
  2. if( strcmp(p, (char*)0) == 0) //非法

3.6 边界计算与不对称边界

  1. int i, a[10];
  2. for( i = 1; i <= 10; i++ ){
  3. //有些编译器内存分配方式是内存地址递减
  4. //则a数组地址之后紧接着的是i。
  5. //则a[10]的地址就是i,
  6. //a[10]=0等同于i=0
  7. a[i] = 0;
  8. }

3.7 求值顺序

  1. int i = 0;
  2. while(i < n)
  3. y[i] = x[i++]; //无法保证y[i]是否在i++执行前被求值
  4. y[i++] = x[i]; //同上

3.8 逻辑运算符:&&、||、!

注意与位运算符区分:&、|、~

3.9 整数溢出

  1. //a和b是有符号
  2. if( a + b <0 ) //可能溢出、溢出结果未知。
  3. complain();
  4. //INT_MAX 在limits.h中定义。
  5. #include<limits.h>
  6. if( (unsigned)a + (unsigned)b >> INT_MAX )
  7. complain();
  8. //同上
  9. if(a > INT_MAX - b)
  10. complain();

3.10 为函数main提供返回值

main函数返回值用来告知操作系统执行成功还是失败。
一般0是成功,非0失败。

  1. main()
  2. {
  3. return 0;
  4. }
  5. main(){
  6. exit(0);
  7. }

四、链接:Linker

4.1链接器

汇编器编译生成的可重定向目标文件(relocated object).o文件,由链接器链接成一个可执行程序。

链接器把.o文件看成是一组外部对象(external object)组成,每个对象代表机器内存一部分,并通过一个外部名称识别。
每个函数、外部变量(external),没有被static都被看成是外部对象。
不同目标模块可能含有同名的外部对象,链接器需要处理。

工作情形

目标模块和库文件,输入链接器,最后输出一个载入模块。
对于目标模块中的每个外部对象,Linker都在载入模块检查是否存在,没有就写入,有的话就要处理命名冲突。
Linker读入一个目标模块,必须解析出目标模块中定义的所有外部对象的引用,并作出标记说明这些外部对象不在是未定义的。

4.2声明与定义

  1. int a;
  2. //在函数外,就是一个外部对象,有默认初始值。
  3. extern int a;
  4. //声明外部变量a,不是定义。
  5. //链接器看来是一个外部变量a的引用。
  6. int i = 7; //定义在a.c文件的外部变量
  7. int i = 9; //定义在b.c文件的外部变量
  8. //链接器的处理行为未知,一般是认为错误,最好不要这样。

4.3 命名冲突与static修饰符

  1. //a.c文件
  2. int p = 1; //外部变量p
  3. //b.c文件
  4. static int p = 1; //外部变量p,作用于仅限b.c文件
  5. //变量这两个文件不会产生命名冲突,因为static限制作用域在当前文件。

4.4 形参、实参和返回值

  1. int abs(int n){ //形参
  2. return n < 0 ? -n : n;
  3. }
  4. int fuck = 1;
  5. abs(fuck); //实参
  1. #include<stdio.h>
  2. void main(){
  3. int i;
  4. char c;
  5. for(i = 0;i < 5; i++){
  6. //内存溢出:输入一个int到一个字符变量地址上。
  7. //超出的值可能会覆盖到i变量中。
  8. //如果编译器分配内存的方式是递减的方式。
  9. scanf("%d", &c);
  10. printf("%d", c);
  11. }
  12. }

4.5 检查外部类型

  1. //a.c文件
  2. extern int i;
  3. //b.c文件
  4. long i;
  5. //结果一:编译报错,类型冲突。
  6. //结果二:正常运行,int和long都是4字节,。
  7. //结果三:不报错可能运行结果正确也可能错误,共享了部分内存。
  8. char filename[] = "hello world"; //字符数组
  9. extern char* filename; //字符指针,引用外部对象不正确
  10. char filename[] = "hello world"; //字符数组
  11. extern char filename[]; //引用外部对象正确
  12. char *filename = "hello world";
  13. extern char* filename; //引用外部对象正确

4.6 头文件

外部对象都在头文件中声明。

五、库函数

5.1 int getchar()

  1. #include<stdio.h>
  2. main(){
  3. char c;
  4. while((c = getchar()) != EOF);//内容溢出,getchar读入一个int,c只是一个char
  5. putchar(c);
  6. }

5.2 更新顺序文件

  1. FILE *fp;
  2. fp = fopen(file, "r+");
  3. //为了保持与过去的兼容性:读操作之后不能接着写操作。
  4. //fread之后不能接着fwrite,必须先fseek。

5.3 缓冲输出与内存分配

  1. #include<stdio.h>
  2. main(){
  3. int c;
  4. char buf[BUFSIZE];
  5. setbuf(stdout, buf);
  6. while((c = getchar()) != EOF)
  7. putchar(c);
  8. //内存泄露!!!
  9. //buf已经交由stdout使用
  10. //在main函数结束之后,stdout可能还要操作buf,这是buf已经被回收。
  11. }

5.4 使用errno检测错误

库函数会通过外部变量errno,通知程序执行失败。这个与系统相关,有可移植性问题。

  1. //错误示例一
  2. // 调动库函数
  3. if (errno)
  4. //调用库函数失败。
  5. panic();
  6. //有问题:可能没有调用库函数,errno是上一个库函数调用结果。
  7. //错误示例二
  8. errno = 0;
  9. // 调用库函数
  10. if( errno )
  11. //调用库函数失败
  12. panic();
  13. //有问题:调用库函数失败但是不会报错,这样会设置errno。
  14. //比如打开文件失败,这种失败我们认为是正常的。
  15. //正确示例
  16. //调用库函数
  17. if( 调用返回错误值 )
  18. 检查errno
  19. //先检查调用函数的返回值。

5.5 库函数signal

  1. #include<signal.h>
  2. signal(signal type, handler function);
  3. //function:signal type事件发生时,调用的函数。

signal可能在任何时候发生,可能在函数执行过程中发生,比如malloc执行到一半。
所以signal用起来非常棘手,最靠谱的做法就是打印日志然后立即退出程序。

异常退出,日志丢失

程序异常终止时,最后几行输出会丢失,大规模的可能是几页。原因是因为程序来不及清除自己的输出缓冲区。调试的时候最好设置成不允许输出缓冲

  1. setbuf(stdout, (char*)0);

六、预处理器

宏(micro)是一种对C程序字符进行交换的方式,只对文本起作用,不对程序中的对象起作用。
6.1 注意宏中的空格

  1. #define f (x) ((x) - 1) //等价
  2. #define f (x)((x) - 1) //等价
  3. #define f(x) ((x) - 1)

6.2 宏不是函数

  1. #define abs(x) x>0?x:-x //存在重大隐患
  2. abs(a - b); //等价
  3. a-b > 0 ? a-b : -a-b; //等价
  4. #define abs(x) (x)>0?(x):-(x) //没毛病
  5. #define max(a, b) ((a)>(b)?(a):(b)) //a和b可能会被执行两次,这种结果未知
  6. max(a, x[i++]); //i++可能会被执行多次。

6.3 宏不是语句

设置定义assert的宏

  1. #define assert(e) if(!e) assert_error(__FILE__, __LINE__)
  2. //宏里的if可能和外部的else配合,扰乱if-else流程。
  3. #define assert(e) \
  4. {if(!e) assert_error(__FILE__, __LINE__);}
  5. //改进上面的问题,这样定义宏也有问题
  6. y = distance(p, q);
  7. assert(y > 0) //不需要分号了,很诡异
  8. x = sqrt(y);
  9. //正确宏定义
  10. #define assert(e) \
  11. ((void)((e) || _assert_error(__FILE__, __LINE__)))

6.4 宏不是类型定义

  1. #define FOOTYPE struct foo //不可取,用typedef最好
  2. FOOTYPE* t1, t2; //t1是指针,h2就不是。

七、可移植性缺陷

7.1 C标准变更

  1. double square(double x);
  2. double square(x) double x; //老版本C

7.2 标识符名称限制

Malloc可能和malloc一样。

7.3 整数大小

short <= int <= long。
char,默认一般是有符号整形,长度一般8bit,早期的有9bit

7.4 char是有符号还是无符号

  1. char c; //一般都是有符号
  2. unsigned char c; //无符号整形

7.5 移位运算符

右移,高位补什么?
无符号,一般逻辑右移;有符号,算术右移,为了保留正负符号嘛。

2>>11,右移过头了,有现代编译器会对位数进行mod模除。比如这个相当于2>>1。

非负整形,用位移安全代替乘除,有符号整形则不安全!!

  1. (-1) >> 1; //等于0
  2. (-1)/2 == 0; //不等于0
  3. mid = (low + high) >> 1; //
  4. mid = (low + high) / 2; //low+high>=0时,等价

7.6 内存位置0

理论上:
有些编译器,不可访问。
有些编译器,可读不可写。
有些编译器,可读可写。
实际上:除专业需求的C,都不可访问。

  1. #include<stdio.h>
  2. main(){
  3. char *p;
  4. p = NULL;
  5. print("%d", *p);//看看当前编译器是这么看待0地址的。
  6. }

7.7 除法发生截断

  1. q = a / b;
  2. r = a % b;
  3. //理想情况
  4. 1q*b+r==a
  5. 2aq同符号
  6. 3b>0时候,r>=0r<b
  7. //实际情况无法全部满足,大部分选择了放弃第3条。
  8. //最佳做法时候,尽量使用无符号,也就是非负数。

7.8 随机数大小

rand函数,返回一个伪随机的非负数。
早期的C,rand返回数的范围不同,都有2个字节,4个字节。
ANSIC C标准规定了,RAND_MAX是最大值。