近来学习操做系统这门课,课程的实验基于linux 0.11,因而从图书馆借来了 C Traps and Pitalls 和 Expert C programming,打算提升一下c语言水平。
先从前一本开始。这本书很薄,即便是英文版也只有140多页,讲的都是c语言中容易犯错的地方。
注意:这篇笔记并无包括整本书的内容,而只是摘抄了本人须要的知识(加上了一些本身的理解)。如需完整了解,还请自行看书。linux
extra space包括空格、制表符和换行
好比你以为这个函数声明太长程序员
int* foo(int arg1, const char *arg2, double arg3, struct bar *arg4) { /*...*/ }
能够写成编程
int* foo( int arg1, const char *arg2, double arg3, struct bar *arg4) { /*...*/ }
编译器从左到右分析符号,使获取的符号尽量地长。a---b;
等价于 a-- -b;
数组
因此 a = b/*p;
这种写法极可能不是你想要表达的意思。 /*
会被认为是注释符的左边,因而它后面的内容都成了注释,实现除法的写法应该是 a = b / *p;
。缓存
可是,形如a-----b
就不能理解成 (a--)-- (-b)
,而应该是 (a--)-(--b)
,由于 (a--)--
这样是不符合语法的。函数
int a = 90;
和 int b = 090;
是不同的呀。
后者在9的前面多了一个0,编译器就会觉得090是八进制,因而b的值就是9*8^1 + 0*8^0 = 72d
学习
char就至关于整型,只是它的大小是1字节。相应地,字符'a'就是对应ASCII码里的数字。
char*是指针,32位下大小为4字节。与其它指针无异。
一个用双引号括起来的字符串(如"hello"
),就是一个指向字符串(加上结尾的'\0'
)首地址的指针缩写。
因此spa
printf("hello");
相似于操作系统
char* str = "hello"; printf(str);
假若有函数debug
void bar(int arg);
那么:bar();
执行函数int (*p)(int a) = bar;
bar是函数地址,能够赋值给函数指针(固然类型要相同)
Only two things can be done to an array: determine its size and obtain a pointer to element 0 of the array. All other array operations are actually done with pointers, even if they are written with what look like subscripts.
若是定义指针来当数组用,须要手动用malloc在运行时堆得到空间;而定义数组,编译器就自动在栈帧里分配空间。指针和数组的操做类似,关于数组的操做大部分都是基于指针的。
这里要指出的是关于char类型数组和指针的两个不一样。
假定如今须要一个hello的字符串,能够这么写:
char* str = "hello"; char str[6] = {'h','e','l','l','o','\0'};
前者编译器会在字符串后面加上结束符 '\0'
,后者则须要本身注意留出空间存放该符号。
字符串常量(好比上面的 "hello"
)存放在常量区,在编译时就肯定好了,在运行的时候不能修改。因此下面的操做是错误的。
char* str = "hello"; //指针指向常量区 str[3] = 'a'; //错误,修改了常量区的内容
而使用数组则不会出现这样的问题,由于char数组会将内存常量的内容复制到栈帧中。
char str[6] = "hello"; //将内存常量中的"hello"复制给栈帧中的数组 str[3] = 'a'; //正确,修改的是栈帧中的内容
不能将整个数组看成参数传入函数,使用数组名做为参数会把数组首地址做为指针传入函数。因而
int strlen(char s[]) { /* stuff */ }
跟
int strlen(char *s) { /* stuff */ }
的效果是同样的。因此常见的参数 char* argv[]
和 char** argv
也是同样的。
更准确的说法是,数组做为参数传入函数会“退化”成指针。在定义该数组的函数里使用 sizeof
能够正确得到数组大小,而将该数组首地址传入函数后,只用 sizeof
只能得到指针大小。例如
int main() { char str[] = "hello"; printf("%u", sizeof(str)); return 0; } //输出结果为6
int main(int argc, char* argv[]) { printf("%u", sizeof(argv[0])); return 0; } //输出结果为4
写for循环的时候,会遇到一个问题:
for (int i = 0; i < 10; i++) { a[i] = 0; }
for (int i = 1; i <= 10; i++) { a[i] = 0; }
哪一个写法好呢?本书给出了后者写法的理由。
左闭右开
若是 x>=20 && x<=40
为真,那么x有多少种可能的取值?20?答案应该是 40 - 20 + 1 = 21
。这样的写法彷佛挺反直觉,容易出错,若是写成左闭右开 x>=20 && x<41
就好多了。相似地,在C语言里,数组第1个元素下标为0,因而定义一个10个元素的数组,最大的下标为9。可能挺反直觉,可是只要利用好这特色,使代码简洁,逻辑简单。例如:
#define N 128 static char buffer[N]; void bufwrite(char *p, int n) { int i; int temp; for (i = 0; i < N && (temp=getchar()) != EOF; i++) { bufer[i] = temp; } }
那么退出循环的时候i
就必定是缓冲区中元素的个数了。
其实,采用左开右闭写法习惯,更可能是由于c语言本数对内存的描述是地址加偏移量。好比数组int a[10]
不存在a[10]
而存在a[0]
。咱们主动迎合这种作法能够避免很多麻烦。不然,想要一个10个元素的数组就要定义int a[11]
。这么作增长了出错的可能。
操做符&&
||
?:
,
都是从左到右执行。
其中, &&
左边操做数为假,右边不会执行; ||
左边操做数为真,右边不会执行。能够利用这个特色简化代码,例如
int a[100]; int i = 0; while (i < 100 && a[i] != 0) { /* stuff */ } 其中,`&&` 保证了数组访问不会越界。
除了特定的操做符 &&
||
?:
,
,操做符对其操做数的求值顺序是不肯定的。常见的
int i = 0; while (i < n) { y[i] = x[i++]; }
赋值号左边的i是不肯定的。正确写法能够为:
int i = 0; while (i < n) { y[i] = x[i]; i++; }
两个unsigned类型相加不会有溢出的问题(简单舍去进位),一个unsigned和一个int相加也没有问题(int将会被转换为unsigned)。两个int相加就可能存在溢出问题了。
假定两个int都为正数。对于检查溢出,这样写是不对的
if (a + b < 0)
由于在检查的过程当中已经致使溢出发生。而不一样机器对溢出的处理是不同的,不可以假定溢出了什么事都不发生。因此检查溢出时应该避免溢出发生:
if ((unsigned)a + (unsigned)b > INT_MAX) //或 if (a > INT_MAX - b)
c程序是先编译后连接。具体能够看CSAPP的第七章,有关于连接的基本介绍
为了兼容旧版本的c程序,对于函数的声明,须要忽略参数。以库函数square为例,声明为double square();
,只要在调用函数的时候可以正确地传入参数,就能够正常调用函数。
有一点须要注意:对于上面square的声明,若是传入函数的参数为float类型,它在传入时会被转化为double类型,这样没什么不妥;对于参数类型为short、char的函数,若是声明时忽略参数,传入的参数会被转化为int,就会产生问题:函数要的是8位的char,而int的长度通常都不是8,进行位运算可能会出现问题。
固然,以上都是为了兼容才在声明时忽略参数。咱们写程序仍是老老实实把声明写完整吧。
好比,定义一个全局变量 int a;
,而在另外之外一个c文件错误地声明为 extern long a;
。显然,这样是错的。编译器和连接器足够聪明的话,能发现这个问题,而后给出错误。不然,编译、连接经过,问题潜伏在程序中。程序可能正常运行(int和long都为32位等缘由),或者出错(声明为double,定义为int等)。总之,程序员有义务保证声明和定义一致。
定义和声明不一致致使错误很正常啊,为何要拿一小节来说呢?由于定义全局变量 char str[];
和声明 extern char* str;
就是定义和声明不一致的错误,而咱们不易察觉。问题在于:char指针不是char数组啊(Orz二者像归像,仍是有不一样啊....)
char a[128]; //a表明这个数组的首地址 char *b = a; //b是一个指针变量,它的值为数组a的首地址 a++; //错误 b++; //正确
体现的是char类型的数组与指针的区别。
头文件了解决上面一小节的问题。作法是,将全部外部变量的声明都写在头文件,而后将头文件include在每一个涉及这些变量的文件中,包括定义该变量的文件。缘由是,在同一文件中声明和定义一个全局变量,那么编译器就能够检查声明和定义是否一致,若是一致,在其它文件中只要include这个头文件,就可使用该变量了。将函数声明放入头文件也是一样道理。
有一点要说明:函数默认是全局的(除非是 static
),因此在其余文件声明的时候能够不加 extern
,但出于阅读方便,咱们都是加上 extern
(标准库头文件都有);变量则必定要加(不加 extern
就变成定义了)。
探究:若是不使用头文件会怎样?能够直接引用外部函数和变量吗?
若是没有声明就直接引用外部变量和函数,那么编译器就假定函数的返回值为int,变量为int类型,而后连接器把引用连接起来。若是碰巧引用的变量的确是int型,那么程序正常,不然类型对不上发生错误。
在没有外部声明的状况下,函数内部不能直接引用其余文件的全局变量。
先看一段代码
#include <stdio.h> int main() { char c; while ((c = getchar()) != EOF) { putchar(c); } return 0; }
代码的功能就是将输入流的内容转到输出流中,直到输入流为空。如今来讲说代码存在的问题。首先要说明,char为 unsigned char
或 signed char
是由编译器决定的,大部分编译器默认为 signed char
,EOF
通常定义为-1。假定int为32位。
若是char为 unsigned char
。当getchar返回 EOF(0xffffffff)
,那么变量c被赋值为 0xff
。与EOF(int)比较,c须要扩展为0x000000ff
(无符号扩展用零填充),二者不相等,循环将不会中止。
若是char是 signed char
。当getchar读到字符 0xff
,因而返回 0x000000ff
,c被赋值为 0xff
。与EOF(int)比较,c须要扩展为 0xffffffff
(有符号扩展用符号位填充),二者相等,循环提早结束。
出于以上,c也应该改成int,使代码正常工做。
注意:某些编译器直接拿getchar的返回值与EOF比较。这样虽然不能正确表达代码的意思,但能使程序正常工做。
手动为文件分配缓冲区,可使用setbuf函数,在缓冲区满时输出。
#include <stdio.h> int main() { int c; char buf[BUFSIZ]; setbuf(stdout, buf); while ((c = getchar()) != EOF) { putchar(c); } return 0; }
这段代码出错在于,缓冲输出在main结束的时候,这时buf数组已经不存在。
解决办法是为数组buf加上关键字 static
,成为静态变量(但不建议在函数内部定义静态函数);或者使用malloc函数,如
setbuf(stdout, malloc(BUFSIZE));
这样,main结束时缓冲区仍然存在。可是要时刻留意malloc以后要不要free!
并没有规定:若是库函数运行正常,要将errno设置为0。因而如下写法错误:
call library function if (errno != 0) complain
那么在使用函数前就把errno设置为0呢?
errno = 0; call library function if (errno != 0) complain
不行!由于库函数内部可能会调用其它库函数。好比调用fopen,函数会调用其它库函数去检查某个文件是否存在,若是文件不存在,则errno会被设置,而后建立新文件,返回指针。这时候fopen是正常工做的,可是errno却不为0。
那errno该怎么用?最好的办法应该是结合返回值使用了。应该在函数返回错误信息后,再检查errno
call library routine if (error return) examine errno
原则是signal处理函数尽量简单。最好是输出相关信息后就用exit退出程序。缘由是,信号接收可能出如今任什么时候候(malloc时接收到信号,信号处理又调用malloc);并且信号处理完后不一样机器有不一样操做(某些机器在某些信号处理后重复失败的操做,如除数为零)。
切记宏只是简单地复制粘贴!
如下面的定义为例
#define MAX(a,b) ((a)>(b)?(a):(b))
不加括号必然引发悲剧,不用多说。
其次,谨慎使用 ++
--
之类的运算, MAX(a,a++)
也会产生奇怪的结果。
再次,谨慎嵌套,如 MAX(a, MAX(b, MAX(c, d))
,展开后表达式很长,debug时会很痛苦,并且会产生没必要要的重复运算。
好比,谨慎在宏用 if
。假如assert是这样定义的:
#define assert(e) if(!e) assert_error(__FILE__,__LINE__)
那么
if (a > 0 && b > 0) assert(x > y); else assert(x < y);
展开就变成
if (a > 0 && b > 0) { if (!(x > y)) assert_error(__FILE__,__LINE__); else if (!(x < y)) assert_error(__FILE__,__LINE__); }
这显然不是咱们要的结果。可能会想到将assert用花括号围起来。但这样就会在if和else之间出现语法错误。
实际上,assert是这样定义的:
#define assert(e) ((void)((e)||_assert_error(__FILE__,__LINE__))
它是一个值而不是语句。
阅读完这本书后,对这些陷阱能够总结为:只作正确的事,不要作一些感受应该正确的事。有些陷阱是历史缘由,有些是奇怪的缘故,还有些是逻辑的问题。总之,谨慎!同时对c的认识又加深了:c是贴近硬件的语言。还加上嵌入汇编的功能,难怪写操做系统要用c;还有,这本书提升了我查文档的能力!这本书对数组和指针的解释比较分散,建议阅读c专家编程做为系统了解。