C traps and pitfalls 读书笔记

· 本书详细讨论的错误并非是编译错误,而是程序没有按照程序员所期望的方式运行的错误;(也就是考察程序与程序员的心智模式mental model之间的差别)

· 第1章 词法错误
     Pascal中:=为赋值,而=表示比较,而在C中=和==分别发挥上述两个作用;C的做法更合理因为赋值操作更常见。
·  贪心法读取符号。经典的例子如a/*p和a---b。需要避免误会的话可以加空格或括号。
· 注意10和010表达的含义截然不同,因为后者是8进制记法,对应于十进制的8;所以不要为了整齐而出现
int a[] = { 023, 074, 125}; 之类的白痴写法;
· 趣题:写一个测试程序,判断编译器是否允许/* */构成的嵌套注释,要求它在两种编译器下都可以运行,但是结果不同。pdf P134给出了详细解答,其中解法二很神。(提示:考虑编译器用贪心法断词的过程,在允许嵌套注释的编译器下,/* 之后的/*会被视为一个有意义的符号,而在不允许嵌套注释的编译器中,/*之后唯一被关注的符号就是*/,所以如果/*后出现了/*/, 则断词的方式为/    */);

· 第2章 语法陷阱
出现了经典的例子(曾在C语言深度剖析中提到)
( * ( void ( * ) ( ) ) 0 ) ();
表示对地址为0位置的参数表为空、返回值为空的函数的调用;
记住fp()是对(*fp)()的简写形式,都是表示对名称为fp的函数的调用;

· 分组记住优先级顺序:
     - 最高级别并不是真正意义上的运算符,它们是[] () . 和->
     - 接下来, 单目运算符(! ~ ++ -负号 *取值)等仅次之,大于所有双目、三目运算符
     -  在双目运算符中,优先级顺序为 运算 > 移位 > 关系 > 逻辑;例如 if( ( c = getc(input) )!= EOF) 中加粗的括号是必需的;
     - 接下来是三目运算符;
     - 次之为赋值; 最后为逗号(逗号运算符在宏定义中很有用,可以在宏定义中添加用逗号连接的临时变量,避免多次自增的问题,见第6章
     - 分组内部仍然有细致的分别,其一,乘除取余高于加减;其二, < <=等高于==和!=;其三, 按位运算符高于逻辑与或并且 与 高于 异或 高于 或
     - 注意 单目运算符、赋值以及三目运算符的结合顺序为自右向左;这就是说,*p++的含义是*(p++); a = b = c;表示从右向左依次赋值等等;
另外,加一个特别的注释: int a = b = c; 中仅完成了对a的定义,所以如果b和c是未被定义或声明的变量,则会编译错误;

· 悬挂else问题,没啥好说,不过书中提到了一种很有才的避免错误的方法,即用宏定义的方法:
#define IF { if(
#define THEN ){
#ELSE }else{
#END } }
这当然给程序带来了很多的麻烦,不过是一种很有意思的想法。

· 第3章 语义陷阱
数组与指针:
对于一个数组,我们只能做两件事,确定该数组的大小(利用sizeof,注意sizeof(a)返回的是a数组的总大小。)以及获得首元素地址(数组名)。其他有关数组的操作事实上都是由指针方式完成的。例如,访问a的第i个元素的方法是*(a+i),正因为此操作很常见,才被简记为a[i], 而由于*(a+i) = *(i+a),所以 i[a]与a[i]的含义完全相同(Oh my god!),只是我们不常用而已。更多的内容可以回顾《C语言深度剖析》。
· 给长度为L的字符串申请内存空间时,不要忘记多申请一个字节,以存放结束符'\0';
· 申请空间时一定记得检查是否成功申请,如果没有只能中止程序;
· 数组作为函数参数时,编译器自动解释为指向首元素的指针,但在其他场合下,int a[] 和 int *a 是有着本质区别的,比方说在extern的时候不可混用;
· 注意: 有关字符串的特别之处,char *p; p = "abc"; 这样的写法是合理的,但别忘了p的内容是"abc"的地址,而不是字符串"abc"本身
· 空指针NULL是不能解引用(访问相应地址)的,所以 if (p == NULL)是可以的,但 if(! strcmp(p, NULL))是错误的,因为strcmp要求参数必须是有效的字符串指针,即便字符串是空串"\0";
· 经典例子:
int i, a[10];
for(i = 0; i <=10; i++)
{
a[i]=0;
cout<<a[i]<<endl;
}
return 0;
这段程序运行后会有和结果?根据书中的说法是可能陷入死循环,因为编译器很可能按照地址递减的方式压栈分配内存,所以在一段连续的由小到大地址上依次储存着a[0] a[1] ... a[9] i; 这样循环中试图访问a[10]时,实际上将i置为了0。我在VS2010上实测时没有遇到死循环,但是试图访问a[10]时程序中断报错,内容为“试图破坏数组a周围的栈结构”,可见新的编译器对内存的保护做得更严格了。出于好奇,我把变量定义语句变为int a[10], i;后一样出现了错误,把i的定义写在for(int i...)的位置也不能运行。

· 所谓非对称边界的写法正是为了避免上述错误,在数组的左边界采用不严格比较(i>=0)后边界采用严格小于号(i<10),这样还可以带来额外的好处,比方说右边界与i相比较的数刚好是数组的大小。一般在入栈或写buffer时 *p++ = value; 将p永远指向第一个未被使用的地址,也是这种不对称边界思想的体现。判断buffer满可以用 p == &a[N], 注意,虽然a[N]是不存在的,但是编译器承认&a[N]这种取址的写法。

· 求值顺序:运算符两侧的操作数先求哪个、后求哪个;C语言中仅有四个运算符规定了求值顺序:&&、||、?:和 , 。前两个先求左边,再确定是不是需要求右边。三目运算符先求左面,再计算冒号左右两边需要计算的那个。 逗号运算符先计算左边,然后丢弃结果,再计算右边并返回。值得注意的是连接函数参数的并非逗号运算符,它会从左到右依次传递参数,例如f(x,y); 而在g((x,y))中x和y之间的是逗号运算符,并且传递给g的只有y的值。而 其他运算符的求值顺序是不确定的,例如赋值运算 x[i] = y[i++]; 中不能确保x[i]中的 i 是否已经自增过,所以不可以采用这种写法
· 按位运算符有 &、| 和 ~ 三个,逻辑运算符为&&、||、!,注意前者一定会计算运算符两侧的值,并且求值顺序不确定。

· 第4章 连接
·  static存储类型修饰符可以把变量或函数的名称作用域限制在本文件中,这是一个减少名字冲突错误的办法。(const也有类似的作用,但显然我们并不希望所有变量都是只读的)
·  关于字符串数组和指针的区别的一个经典例子,在本书与C语言深入剖析中都出现了。分析:在一个文件中定义char a[] = "abcd"; 在另一个文件中声明extern char *a; 会导致什么问题?如果定义为指针,声明为指针又会导致什么问题?
     - 每一个变量有四个要素:地址、类型、名称和值。在定义char a[] = "abcd"; 中变量名为a,地址(设为A1)为某个编译器决定的位置,类型为字符数组,而值是一个地址(设为A2),这个地址是一个常量,不可以被修改;A2为内存一块可读可写区域的起始地址,其中被依次保存字符a b c d 和'\0'。而在定义char *a = "abcd";中 变量名为a,地址(同样设为A1)由编译器决定,类型为字符指针,而值是一个地址(A3),这是一个变量,可以修改,A3对应内存一块只读区域,其中保存着字符常量“abcd”;
     - 变量类型不同决定了访问内存的方式也不同。在字符数组中,编译器把 a 解析为相应位置的字符而非地址常量(我个人认为这是字符数组相对于其他数组的区别,例如对于int a[3]; 如果cout<<a;得到的将是地址,而char a[3]; 中,若cout<<a; 得到的将是字符串本身);在字符指针中,a仍然是地址。这两种访问的根本差别在于访问数组是直接取值,访问指针则是先取址后取值;
     -  如果定义为指针,声明为数组,编译器把a的值——实际是地址——当做需要的值理解;如果定义为数组,声明为指针,编译器把a当做指针指向的值"abcd",再把它与偏移量相加,得到新的“地址”,再从新地址取值;两种方式显然都会造成严重的错误

· getchar()函数(#include<stdio.h>)的返回值是int类型的,以容纳EOF;

· 【存疑】书中提到了程序不能交替对一个文件进行读和写,在fread和fwrite函数之间必须加入fseek函数调整文件状态,需要再多学习写东西;

· 第5章 库函数 & 第6章 预处理器
· assert是宏,其作用是检测其参数是否为0,如果是则停止当前程序并调用错误提示函数,否则什么都不错。它的一种定义方法如下:
#define assert(e) ((void)(e)||_assert_error(_FILE_,_LINE_))
之所以不采用 if 语句进行判断,是为了避免嵌入程序后 if 与 程序中原有的else进行错误的匹配;
·  针对#define max(a,b) ((a)>(b)?(a):(b))中存在的重复计算问题(如果计算max(i++,j++),那么 i 和 j 中的一个经历了两次自增,尽管本意肯定不是如此),可以用定义全局变量a_temp和b_temp, 并在宏定义中首先把参数赋值给临时变量,再求值;赋值语句和三目运算之间用逗号运算符连接,这样就可以把最右侧语句的返回值最为最终返回值

· 第7章 可移植性缺陷
· 如果a是一个n位的整数,那么n<<m或n>>m中m的取值范围是:0 <= m < n
· 注意 n = -n; 这个赋值操作(例如在求绝对值的函数中)可能出现溢出;因为通常最小的负数比最大的正数的绝对值大1;

· 字符串常量在被处理时,可以依情况被视为指针或者字符串本身,这使得有一些令人惊异的事情发生。例如, "abcde"[2]这个写法是合理的,它的结果为数组abcde的第2个字符(从0编号),即c;事实上它被解析为 *( 2 + "abcde"), 而 2 +“abcde”被理解为指针与常量的加法。另外值得注意的是cout<<2+"abcde";与cout<<*(2+"abcde");的结果是相同的。

· 对于一些需要分别对正整数和负整数进行处理的程序,如果这样写:
if ( n  < 0 )
n = -n; 
process_pos(n); ...
则可能产生溢出错误,因为通常最小的负数比最大的正数绝对值大一。
更好的方式是:
if( n > 0)
n = -n; 
process_neg(n);
在process_neg函数中注意避免把n转换为对应的正数的操作。

· 第8章 建议与答案 (略)
· 附录A
printf函数组
· printf(stuff); 等价于 fprintf(stdout, stuff);
· %d 整数 %u 无符号整数 %o 八进制整数 %x %X 十六进制整数(区别在于超过10的部分是ab..e还是AB..E)%s 字符串 %c 单个字符 %f 浮点形式的浮点数 %e科学技术法形式的浮点数 %g 默认保留六位有效数字,依情况采用浮点形式或科学技术法

· 修饰符 l 表示long 例如写法%ld %之后可以紧随宽度符,例如%2d表示以宽度2右对齐输出整数 精度符紧随一个小数点,表示希望保留的小数点位数,如%.10f ( 而与%g配合时则表示有效数字的位数)

· 注意printf(s) 和 printf(%s, s); 是不同的,因为前者把s中的所有%视为格式项的标志,因此如果s中出现了%d等,却没有相应的整数等内容出现,会引发难以预期的错误;而后者会如实打印s的内容;
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值