一、词法陷阱
1.1 =和==
=是赋值运算符,返回赋的值。
==比较运算符,返回True or False,或者是1 or 0。
if (x = y)// 等价于 x=y;if(x)
break;
while(c = '') //死循环,空字符ASCII值非0
c = getc(f);
1.2 &和&&,|和||
1.3 词法分析中的贪心算法
编译器将分析程序的过程,从左到右挨个读入字符,如果字符可能组成一个符号则继续读入,直到不可能组成有意义的符号则停止。
a --- b; //等同
a -- - b; //等同
a - -- b; //不同
a+++++b; //等价
a ++ + ++ b; //等价
a ++ ++ + b; //不等价,a++的结果无法作为++的左值。
1.4 整形常量
1.5 字符与字符串
printf("hello\n");
//等价于
char hello[] = {'h','e','l','l','o','\n',0};//0也可以是'\0',空字符
printf(hello);
‘**yes**’是一个字符或者整形,它的值由’y’,’e’,’s’按照编译器定义的方式组合得到。
“yes”是一个字符串,有’y’,’e’,’s’,’\0’组成。
二、语法陷阱
2.1 理解函数声明
(*(void(*)())0)();
//等价于
typedef void *func();
func = 0;
(*func)(); //启动地址0处的函数
//分解过程
1、(*(void(*)())0)();
2、等价于(*(void(*func)())0)();
void(*func)()是一个函数指针,用f简化代替,f是一个函数指针
3、等价于(*(f)0)();
4、等价于(*((f)0))();
(f)0,强制转换0为f类型指针,类似(int*)0,强制0转换为int*指针。用k代替,k是一个值为0的指针。
5、等价于(*(k))();
*(k),取出指针0指向的void(*)()类型函数,用m代替,m是一个函数
6、(m)(); 调用m函数。
float f; //等价
float ((f)); //等价
float *g(); //声明函数g
float *(g()); //等价
float *h(); //注意!这是声明一个名为h的函数。
float (*h)(); //声明函数指针h
(float (*)()) f; //强制转换为函数指针,类似(int*) f;d
void (*signal(int, void(*)(int)))(int); //等价
typedef void(*Handler)(int); //等价
Handler signal(int, Handler)
2.2 运算符的优先级
if(flags & FLAG); //等价
if((flags & FLAG) != 0) //等价
if(flags & FLAG != 0); //不等价
r = hi << 4 + low; //等价
r = hi << (4 + low); //等价
r = (hi << 4) + low; //等价
r = hi << 4 | low //等价
优先级排序:
1、不是真正的运算符:数组下标、函数调用操作,struct的.操作符。
2、单目操作符,++、—、!、~、、&、sizeof等。
3、双目操作符:
a、算术运算符,+、-、、/
b、移位运算符,>>、<<。
c、关系运算符,>、<、==、!=。
d、逻辑运算符,&&、||、!
e、赋值运算符,=
4、三目运算符:条件运算符,?:。
2.3 注意语句结束标志的分号
2.4 switch语句
switch(color){
case 1: printf("red");
break;
case 2: printf("yellow");
break;
case 3: printf("blue");
break;
}
//不等价
switch(color){
case 1:printf("red");
case 2:printf("yellow");
case 3:printf("blue")
}
2.5 函数调用
void f();
f(); //执行函数f
f; //计算函数f地址,不执行。
2.6 悬挂else引发问题
if(x == 0)
if(y == 0) error();
else{
z = x + y;
}
//等价于
if(x == 0){
if(y == 0) error();
else{
z = x + y;
}
}
//为什么C支持14后的逗号?
int days[] = {11,12,13,14,}
//原因
int days[] = {
11,12,
13,14,
}
三、语义陷阱
3.1 指针与数组
C只有一维数组,编译器确定大小,数组元素可以是任何类型,如另外一个数组。
//calendar是一个有12个元素的数组,每个元素是一个有31个int型元素的数组。
//从左往右解释。
int calendar[12][31];
sizeof(calendar) == 12*31*sizeof(int); //true
int i = calendar[4][7]; //等价
int i = *(calendar[4] + 7); //等价
int i = *(*(calendar+4) + 7); //等价
int (*ap)[31] = calendar; //正确,指向数组的指针
int *ap1 = calendar; //错误,ap1是整形指针,calendar是数组的指针
int a[3] = {10, 20, 30};
int *p = a;
printf(*p); //10
printf(*(p+1)); //20
printf(*p+1); //11
printf(sizeof(a)); //数组大小而非第一个元素大小。
(p + 1) == (1 + p); //true
a[i] == i[a]; //true
3.2 非数组的指针
//将字符串s和t,连接一个字符串。
char* p;
p = malloc(strlen(s) + strlen(t)); //错误,必须+1,字符串有空字符结尾。
strcpy(p, s);
strcat(p, t);
...;
free(p)
3.3 作为参数的数组声明
int strlen(char s[]){}; //等价
int strlen(char* s){}; //等价,C把数组声明自动转成指针声明。
main(int, char* argv[]){}; //等价
main(int, char** argv){}; //等价
3.4 避免举隅法
char *p, *q;
p = "xyz";
q = p;
*(p+1) = 'Y'; //错误,ANSIC C禁止对string literal进行修改,
//K&R的说明是试图对字符串常量进行修改的行为是未定义的。
//有些编译器甚至“助纣为虐”。
3.5 空指针
空指针不可以进行解引用(*)。
if( p == (char*) 0); //合法
if( strcmp(p, (char*)0) == 0) //非法
3.6 边界计算与不对称边界
int i, a[10];
for( i = 1; i <= 10; i++ ){
//有些编译器内存分配方式是内存地址递减
//则a数组地址之后紧接着的是i。
//则a[10]的地址就是i,
//a[10]=0等同于i=0
a[i] = 0;
}
3.7 求值顺序
int i = 0;
while(i < n)
y[i] = x[i++]; //无法保证y[i]是否在i++执行前被求值
y[i++] = x[i]; //同上
3.8 逻辑运算符:&&、||、!
3.9 整数溢出
//a和b是有符号
if( a + b <0 ) //可能溢出、溢出结果未知。
complain();
//INT_MAX 在limits.h中定义。
#include<limits.h>
if( (unsigned)a + (unsigned)b >> INT_MAX )
complain();
//同上
if(a > INT_MAX - b)
complain();
3.10 为函数main提供返回值
main函数返回值用来告知操作系统执行成功还是失败。
一般0是成功,非0失败。
main()
{
return 0;
}
main(){
exit(0);
}
四、链接:Linker
4.1链接器
汇编器编译生成的可重定向目标文件(relocated object).o文件,由链接器链接成一个可执行程序。
链接器把.o文件看成是一组外部对象(external object)组成,每个对象代表机器内存一部分,并通过一个外部名称识别。
每个函数、外部变量(external),没有被static都被看成是外部对象。
不同目标模块可能含有同名的外部对象,链接器需要处理。
工作情形
目标模块和库文件,输入链接器,最后输出一个载入模块。
对于目标模块中的每个外部对象,Linker都在载入模块检查是否存在,没有就写入,有的话就要处理命名冲突。
Linker读入一个目标模块,必须解析出目标模块中定义的所有外部对象的引用,并作出标记说明这些外部对象不在是未定义的。
4.2声明与定义
int a;
//在函数外,就是一个外部对象,有默认初始值。
extern int a;
//声明外部变量a,不是定义。
//链接器看来是一个外部变量a的引用。
int i = 7; //定义在a.c文件的外部变量
int i = 9; //定义在b.c文件的外部变量
//链接器的处理行为未知,一般是认为错误,最好不要这样。
4.3 命名冲突与static修饰符
//a.c文件
int p = 1; //外部变量p
//b.c文件
static int p = 1; //外部变量p,作用于仅限b.c文件
//变量这两个文件不会产生命名冲突,因为static限制作用域在当前文件。
4.4 形参、实参和返回值
int abs(int n){ //形参
return n < 0 ? -n : n;
}
int fuck = 1;
abs(fuck); //实参
#include<stdio.h>
void main(){
int i;
char c;
for(i = 0;i < 5; i++){
//内存溢出:输入一个int到一个字符变量地址上。
//超出的值可能会覆盖到i变量中。
//如果编译器分配内存的方式是递减的方式。
scanf("%d", &c);
printf("%d", c);
}
}
4.5 检查外部类型
//a.c文件
extern int i;
//b.c文件
long i;
//结果一:编译报错,类型冲突。
//结果二:正常运行,int和long都是4字节,。
//结果三:不报错可能运行结果正确也可能错误,共享了部分内存。
char filename[] = "hello world"; //字符数组
extern char* filename; //字符指针,引用外部对象不正确
char filename[] = "hello world"; //字符数组
extern char filename[]; //引用外部对象正确
char *filename = "hello world";
extern char* filename; //引用外部对象正确
4.6 头文件
五、库函数
5.1 int getchar()
#include<stdio.h>
main(){
char c;
while((c = getchar()) != EOF);//内容溢出,getchar读入一个int,c只是一个char
putchar(c);
}
5.2 更新顺序文件
FILE *fp;
fp = fopen(file, "r+");
//为了保持与过去的兼容性:读操作之后不能接着写操作。
//fread之后不能接着fwrite,必须先fseek。
5.3 缓冲输出与内存分配
#include<stdio.h>
main(){
int c;
char buf[BUFSIZE];
setbuf(stdout, buf);
while((c = getchar()) != EOF)
putchar(c);
//内存泄露!!!
//buf已经交由stdout使用
//在main函数结束之后,stdout可能还要操作buf,这是buf已经被回收。
}
5.4 使用errno检测错误
库函数会通过外部变量errno,通知程序执行失败。这个与系统相关,有可移植性问题。
//错误示例一
// 调动库函数
if (errno)
//调用库函数失败。
panic();
//有问题:可能没有调用库函数,errno是上一个库函数调用结果。
//错误示例二
errno = 0;
// 调用库函数
if( errno )
//调用库函数失败
panic();
//有问题:调用库函数失败但是不会报错,这样会设置errno。
//比如打开文件失败,这种失败我们认为是正常的。
//正确示例
//调用库函数
if( 调用返回错误值 )
检查errno
//先检查调用函数的返回值。
5.5 库函数signal
#include<signal.h>
signal(signal type, handler function);
//function:signal type事件发生时,调用的函数。
signal可能在任何时候发生,可能在函数执行过程中发生,比如malloc执行到一半。
所以signal用起来非常棘手,最靠谱的做法就是打印日志然后立即退出程序。
异常退出,日志丢失
程序异常终止时,最后几行输出会丢失,大规模的可能是几页。原因是因为程序来不及清除自己的输出缓冲区。调试的时候最好设置成不允许输出缓冲。
setbuf(stdout, (char*)0);
六、预处理器
宏(micro)是一种对C程序字符进行交换的方式,只对文本起作用,不对程序中的对象起作用。
6.1 注意宏中的空格
#define f (x) ((x) - 1) //等价
#define f (x)((x) - 1) //等价
#define f(x) ((x) - 1)
6.2 宏不是函数
#define abs(x) x>0?x:-x //存在重大隐患
abs(a - b); //等价
a-b > 0 ? a-b : -a-b; //等价
#define abs(x) (x)>0?(x):-(x) //没毛病
#define max(a, b) ((a)>(b)?(a):(b)) //a和b可能会被执行两次,这种结果未知
max(a, x[i++]); //i++可能会被执行多次。
6.3 宏不是语句
设置定义assert的宏
#define assert(e) if(!e) assert_error(__FILE__, __LINE__)
//宏里的if可能和外部的else配合,扰乱if-else流程。
#define assert(e) \
{if(!e) assert_error(__FILE__, __LINE__);}
//改进上面的问题,这样定义宏也有问题
y = distance(p, q);
assert(y > 0) //不需要分号了,很诡异
x = sqrt(y);
//正确宏定义
#define assert(e) \
((void)((e) || _assert_error(__FILE__, __LINE__)))
6.4 宏不是类型定义
#define FOOTYPE struct foo //不可取,用typedef最好
FOOTYPE* t1, t2; //t1是指针,h2就不是。
七、可移植性缺陷
7.1 C标准变更
double square(double x);
double square(x) double x; //老版本C
7.2 标识符名称限制
7.3 整数大小
short <= int <= long。
char,默认一般是有符号整形,长度一般8bit,早期的有9bit
7.4 char是有符号还是无符号
char c; //一般都是有符号
unsigned char c; //无符号整形
7.5 移位运算符
右移,高位补什么?
无符号,一般逻辑右移;有符号,算术右移,为了保留正负符号嘛。
2>>11,右移过头了,有现代编译器会对位数进行mod模除。比如这个相当于2>>1。
非负整形,用位移安全代替乘除,有符号整形则不安全!!
(-1) >> 1; //等于0
(-1)/2 == 0; //不等于0
mid = (low + high) >> 1; //
mid = (low + high) / 2; //low+high>=0时,等价
7.6 内存位置0
理论上:
有些编译器,不可访问。
有些编译器,可读不可写。
有些编译器,可读可写。
实际上:除专业需求的C,都不可访问。
#include<stdio.h>
main(){
char *p;
p = NULL;
print("%d", *p);//看看当前编译器是这么看待0地址的。
}
7.7 除法发生截断
q = a / b;
r = a % b;
//理想情况
1、q*b+r==a。
2、a和q同符号
3、b>0时候,r>=0且r<b
//实际情况无法全部满足,大部分选择了放弃第3条。
//最佳做法时候,尽量使用无符号,也就是非负数。
7.8 随机数大小
rand函数,返回一个伪随机的非负数。
早期的C,rand返回数的范围不同,都有2个字节,4个字节。
ANSIC C标准规定了,RAND_MAX是最大值。