C缺陷与陷阱(C Traps and Pitfalls)学习笔记

前言

近来学习操做系统这门课,课程的实验基于linux 0.11,因而从图书馆借来了 C Traps and Pitalls 和 Expert C programming,打算提升一下c语言水平。
先从前一本开始。这本书很薄,即便是英文版也只有140多页,讲的都是c语言中容易犯错的地方。
注意:这篇笔记并无包括整本书的内容,而只是摘抄了本人须要的知识(加上了一些本身的理解)。如需完整了解,还请自行看书。linux

第一章:词法陷阱

符号之间的空白(extra space)是被忽略的。

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

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在运行时堆得到空间;而定义数组,编译器就自动在栈帧里分配空间。指针和数组的操做类似,关于数组的操做大部分都是基于指针的。


指针不是数组 Orz...

这里要指出的是关于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型,那么程序正常,不然类型对不上发生错误。
在没有外部声明的状况下,函数内部不能直接引用其余文件的全局变量。


第五章:库函数

getchar返回int

先看一段代码

#include <stdio.h>
int main()
{
    char c;
    while ((c = getchar()) != EOF)
    {
        putchar(c);    
    }
    return 0;
}

代码的功能就是将输入流的内容转到输出流中,直到输入流为空。如今来讲说代码存在的问题。首先要说明,char为 unsigned charsigned char 是由编译器决定的,大部分编译器默认为 signed charEOF 通常定义为-1。假定int为32位。

  1. 若是char为 unsigned char 。当getchar返回 EOF(0xffffffff) ,那么变量c被赋值为 0xff。与EOF(int)比较,c须要扩展为0x000000ff(无符号扩展用零填充),二者不相等,循环将不会中止。

  2. 若是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来检测错误

没有规定:若是库函数运行正常,要将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函数

原则是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专家编程做为系统了解。

相关文章
相关标签/搜索