c陷阱与缺陷《C陷阱和缺陷》读书笔记 ——前车的覆 后车的鉴

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

c陷阱与缺陷:《C陷阱和缺陷》读书笔记——前车的覆 后车的鉴
疯狂代码 / ĵ:http://BlogDigest/Article76354.html
《C陷阱与缺陷》,作者:Andrew Koenig [美], 译:高 巍 。

; ; ; 这本书是作者以自己发表过的一篇论文为基础,结合自己的工作经验扩展而成。

我看过之后“吃了一斤”,它跟以往我看过的教程完全不一样
,它涉及到C的各个方面,细微、精辟。

用两个字形容,那就是实用。

我不打算把整本书抄一遍,只对我认为“比较重要”(其实没有哪一点是不重要的)的部分做了笔记,并且是简要提取了其中的主要内容,以及相应的小例子,希望再次回顾的时候提高效率。

如果你感到意犹未尽,可以下载来看。

C陷阱与缺陷 下载 ; 1.1 = 不同于 ==
; ; ; 来看这样一个例子,
while (c=' ' || c== '\t' || c=='\n')
; ; ; c = getc(f);
; ; ; 由于程序员在比较字符‘ ’和变量c时,误将 == 写成 = ,那么while后的表达式恒为 1,因为' '不等于零,它的ASCII码值为32。

; ; ; 为了避免这种情况,我们可以这样写 while(''==c || '\t'== c || '\n'==c) ,这样即使不小心将 == 写成 =,编译器也会报错。

; ★ ★ 在C中,单引号括起来表示一个整数,双引号括起来表示指针。

例如,字符和字符串。

1.3 词法分析中的“贪心法”
; ; ; C编译器读入一个符号的规则:每一个符号应该包含尽可能多的字符,方向为自左向右。

; ; ; 例如,a---b <==> (a--) -b
; ; ; 而 y = x/*p; 中 /* 被编译器理解为一段注释的开始,如果本意是用x除以p所指向的值,应该重写如下
y = x/ *p; 或更加清楚一点,写作 y = x/(*p);
; ★ ★ 在用双引号括起来的字符串中,注释符 /* 属于字符串的一部分,而在注释中出现的双引号""属于注释的一部分。

; 二、语法陷阱
2.1 理解函数声明
; ; ; 来看一个表达式, (*(void(*)())0)(); 你能看出来这个表达式的含义吗? 看不出来不要紧,慢慢来...
; ; ; 首先,回顾一下C变量的声明,它由两部分组成:类型以及一组类似表达式的声明符,最简单的声明符就是单个变量。

如,float f,g; 这个声明的含义:当对其求值,表达式f和g的类型为浮点型。

; ; ; 因为声明符与表达式类似,我们可以在声明符中任意使用括号:
float ((f));
; ; ; 含义:当对其求值,((f))的类型为浮点型,由此可知,f也是浮点型。

; ; ; 我们将这个逻辑推广到函数和指针的声明,如:
; ; ; float ff();
; ; ; float *pt;
; ; ; 这些形式组合使用,如
; ; ; float *g(),(*h)();
; ; ; 由于()的结合优先级高于 * ,所以g是一个函数,它的返回类型为指向浮点数的指针;h是一个函数指针,h所指向函数的返回值为浮点类型。

; ★ ★ 由声明到类型转换符:只需要把声明中的变量名和声明末尾的分号去掉,再将剩余部分用一个括号整个“封装”。

如,
☆声明 float (*h)(); 为一个指向返回值为float的函数的指针
☆而 ; (float (*)()) 表示一个“指向返回值为float的函数的指针”的类型转换符。

; ★ ★ 函数指针的调用:(*fp)(); 如果fp的声明如下:
void (*fp)();
fp是一个指向返回值为void类型的函数的指针,那么(*fp)()的值为void。


; ; ; 因此 (void (*)())0 表示将常数 0 转型为“指向返回值为 void 的函数的指针”类型。

请认真理解这一点。

; ; ; 我们可以用 (void (*)())0 代替 fp ,从而得到如下调用
(*(void(*)())0)(); 2.3 注意作为语句结束标志的分号
; ; ; 来看一个代码段
if (a>b);
; ; ; a=0;
else
; ; ; b=0;
; ; ; 注意if表达式后的分号,上面的代码相当于
if (a>b)
; ; ; {}
; ; ; a=0;
else
; ; ; b=0;
; ; ; 由于没有if与 else 匹配,编译器将产生警告。

再来看一个例子,
struct logrec{
; ; ; int date;
; ; ; int time;
}
main()
{
...
}
; ; ; 注意到第一个}与main定义之间是没有分号的。

因此这段代码的效果是声明函数main的返回值是结构logrec 类型。

; 三、语义陷阱
3.1 指针与数组
C中的数组值得注意的两点:
; ; ; 一是C中只有一维数组,而且数组的大小必须在编译期就作为一个常数确定下来。

然而,数组的元素可以使任何类型的对象,当然也可以是另外一个数组,这样便可以“仿真”多维数组。

; ; ; 二是对于一个数组,我们只能够做两件事:确定该数组大小,一级获得指向该数组下标为0的元素的指针(下标运算其实都是通过指针进行的)。

; ; ; 来看一个例子,若定义
int s[12][31];
int *p;
int i;
则 p = s[4];是无误的
而 p = s; 则是不行的。

因为s会被转换为指向二维数组的指针,与p类型不匹配。

然而,像下面这样做是可以的 int (*ap)[31];
int s[12][31];
那么 ap = s; 是可以通过的。

; ★ ★ 假如定义了 int a[22]; 则 *(a+i) 即数组a中下标为i的元素的引用,简记为 a[i]。

实际上, a+i 与 i+a 的含义一样,因此 a[i] 与 i[a] 也具有同样的含义。

3.2 非数组的指针
; ; ; 假如我们有两个这样的字符串 s 和 t,并且希望将他们连成单个字符串 r。


char *r;
strcpy(r,s);
strcat(r,t);
不幸的是,这样做事不行的。

因为不能确定r 指向何处。

不仅要让r 指向一个地址,而且r所指向的地址还应该有内存空间可供容纳字符串。

于是我们可以这样做
char *r;
r = malloc(strlen(s) + strlen(t) + 1);
现在可以 strcpy(r,s); strcat(r,t); 了,不过别忘了最后 free(r);
这里注意到给 r 分配内存的时候 +1 了,这是因为 strlen() 返回的值不包括 '\0',所以要多申请存放 '\0' 的空间! 3.6 边界计算与不对称边界
; ; ; 先来看一个例子,我们常常对类似的代码这样处理
int i,a[10];
for (i=0;i<10;i++)
a[i] = 0;
而不是像下面这样:
int i,a[10];
for (i=1;i<=10;i++)
a[i-1] = 0;
或者
int i,a[10];
for (i=0;i<=9;i++)
a[i] = 0;
; ; ; 原因是前一种方式更适合像C这样的数组下标从0开始的语言。

这里包含在取值范围中的 0 为“入界点
”,而不包含在取值范围之中的 10 为“出界点”。

由于是形如 >=0 且 <10 这样不对称的形式,不妨把这种方式称为“不对称边界”。

; ; ; 在处理数组指针的时候,我们将“上界”指向缓冲区第一个未占用字符(假设指针为k),而不是最后一个已占用的字符,则数组的元素个数为该指针减去数组的首地址(假设为p): k - p。

; ; ; 这里细心的读者可能会担心越界的问题。

请看:
; ★ ★ C标准中:数组中 实际不存在的“溢界”元素 的地址位于数组所占内存之后,对该元素的引用是非法的,然而这个地址可以用于进行赋值和比较。

也就是数组最后一个元素所在内存的下一个内存地址可以用于比较,但该内存中的内容却是不可读写的。

; ; ; 这样,结合特例外推法可以准确、直观的处理边界问题。

这部分内容值得反复体会,请参阅原书P45。

3.7 求值顺序
; ; ; C语言中只有四个运算符(&& 、||、?:和 ,)存在规定的求值顺序。

其中:
&&和 || 是先对左侧操作数求值,只在需要时对右操作数求值。

A?B:C 是先对A求值,根据A的值再求B或C的值。

而逗号运算符是先对做操作数求值,然后该值被“丢弃”,再求右值。

; ★ ★ 在函数参数中的逗号并非逗号运算符,其求值顺序未定义。

但在g((x,y))中却是先 x后y求值,该函数只有一个参数,即求值结果。

; ; ; 来看个小技巧,if(y!=0 && x/y > 5) ; complain(); 这里利用 && 的求职顺序先检测y是否为 0 ,避免 0 作为除数。

3.10 为函数 main提供返回值
; ; ; 来看这个函数
main()
{
}
若无显式声明返回类型,则默认返回类型为 int 。

一个返回值为整型的函数如果返回失败,实际上是隐含地返回了某个“垃圾”整数。

但是由这个返回值是无法判断该函数是否执行成功的! 六 预处理器 需求:
①需要将特定数量(例如,某个数据表的大小)在程序里出现的所有实例统统加以修改。

我们希望只改动一处数值,然后重新编译即可实现。

②大多数C语言实现在函数调用时都会带来重大的开销。

我们希望有这样一种程序块,它看上去像函数,但却没有函数调用的开销。

6.1 宏中的空格
对于
#define f (x) ((x)-1)
它展开后为 (x) ((x)-1)
而 #define f(x) ((x)-1)
展开后为 ((x)-1) ,并且对这个宏定义,f(3) 和 f (3)这两种调用方式求值后都为2。

6.2 宏并不是函数
; ; ; 从表面上看,宏玉函数的行为非常相似,但它们并不完全等同。

假如:
#define abs(x) x>0?x:-x
则对 abs(a-b)展开后得到 a-b>0?a-b:-a-b ,其中-a-b即(-a)-b 与我们期望的 -(a-b) 不一致;
又,对abs(a)+1 展开后得到 a>0?a:-a+1 (与上类似,与期望不符)。

为此,应该将宏定义中每一个参数用括号括起来,并且将整个表达式也括起来,以预防引起优先级有关的问题。

正确的定义应该是这样的
#define abs(x) (((x)>0)?(x):(-x))
; ; ; 再来看一个例子,假如有宏定义
#define max(a,b) ((a)>(b)?(a):(b))
程序中有这样的代码:
biggest = x[0];
i=1;
while(i<n)
; ; ; biggest = max(biggest,x[i++]);
如果max为一个真正的函数,则上述代码无误,而如果max为一个宏,那就不能正常工作。

原因是对宏扩展后 biggest = ((biggest)>(x[i++])?(biggest):(x[i++]));
其中这部分 x[i++] 有可能被执行两次,与我们的期望不符。

解决办法是确保宏max中的参数没有副作用,像这样:
biggest = x[0];
for(i = 1;i<n;i++)
; ; ; biggest = max(biggest,x[i]);
; ; ; 由此,我们看到宏和函数的一些区别。

6.3 宏并不是语句
; ; ; 考虑这样一个例子
#define assert(e) if(!(e)) assert_error(__FILE_,_LINE_)
则对以下代码
if(x>0 && y>0)
; ; ; assert(x>y);
else
; ; ; assert(y>x);
展开后得到,
if(x>0 && y>0)
; ; ; if(!(x>y)) assert_error("foo.c",37);
else
; ; ; if(!(y>x)) assert_error("foo.c",39);
注意到,else并不是与第一个if 匹配,这与我们的期望不符。

解决办法是,将宏assert定义为一个表达式而不是一个语句:
#define assert(e) ((void)((e)||_assert_error(_FIL_,_LINE_)))
上述定义利用了对 || 两侧操作数依次顺序求值的性质。

6.4 宏不是类型定义
; ; ; 考虑下面的代码
#define T1 struct foo *
typedef struct foo *T2;
从两个定义来看,T1和T2从概念上完全符同,都是指向结构foo的指针。

但是
T1 a,b;
T2 a,b;
对于第一个声明展开后,struct foo *a,b; ,a为指针,b不是指针,与期望不符。

7.6 内存位置0
; ; ; null 指针并不指向任何对象。

除了用于赋值或比较运算,出于其他任何目的的使用null指针都是非法的。

例如,
p和q都是null指针,那么strcmp(p,q)的值就是未定义的。

; ; ; 暂时就做到这里,若发现有必要再做修改... var alimama_pid= "mm_13864626_0_0"; var alimama_key= ""; var alimama_uniteid= "550557"; var alimama_col= "3"; var alimama_row = "2"; var alimama_height = "213"; var alimama_width = "896"; var alimama_type = "1111"; var alimama_mode= "32"; var alimama_titlecolor= "0000FF"; var alimama_picolor= "CC0000"; var alimama_bgcolor="FFFFFF"; var alimama_bordercolor="E6E6E6"; var alimama_defaultid="2763772954,2511307110,334017198,1293996766,2029692447,1520014165"; var alimama_timestr="1250899319"; 2009-10-6 16:41:01
疯狂代码 /。

相关文档
最新文档