Never save something for a special occasion. Every day in your life is a special occasion.
分类: C/C++
2010-07-08 23:43:13
写有前面
适合读者对象:有一定C语言基础。
本书不是为批判C语言,而是帮助程序员绕过陷阱与障碍。
一些错误一旦被认识和理解,并不难避免。
对于一般人而言最有效的学习方式是从感性的、活生生的事例中学习,比如亲身经历或者他人的经验教训。
It is much harder to understand the best ways of using what one already knows.
目录
一、词法陷阱
二、语法陷阱
三、语义陷阱
四、连接
五、库函数
六、预处理器——宏处理器
七、可移植性缺陷
八、建议
附录A:
printf
可变参数列表
“符号”(token)指程序的基本组成单元,其作用相当于一个句子的单词。Compiler的词法分析器负责将程序分解为一个个符号,采用“大嘴法”
① a---b
等价于
a-- - b
而不是
a- --b
② y = x/*p /* p指向除数 */
编译时不能通过,因为编译器认为 /*p ... */ 是注释。
你应该写成
y = x / *p ...
或者
y = x / (*p)
你是否曾在明白 ==、=;&、&& ... 含意后还将它们错用? 不可掉以轻心,即使你已经过一天劳累!
整型常量 047 == 8*4+7 == 71,前面多一个0 含意就不同了。
你有没有认真读过printf的原型? 如果printf( '\n' ) 没有报错,你不该抱怨 compiler。——printf需要一个指针,如果你给它一个非法的东西,compiler认为程序员是正确的。程序员应该知道自己在做什么。
双引号括起来的串中 注释符 /* 属于串的一部分,而注释中出现的双引号又属于注释的一部分。
要理解一个C程序,仅仅理解组成程序的符号是不够的,还要理解符号是如何组成声明、表达式、语句和程序的。虽然这些组合方式的定义都很完备,几乎无懈可击,但有时这些定义与人们的直觉相悖,或者易混淆。
(*(void(*)())0)();
构造这样一个令人“不寒而栗”的表达式其实只有一条简单规则:按归使用的方式来声明。
这是一个函数调用;被调用函数位于地址0;
理解此句关键2点——函数指针,类型转换
int *g(), (*h)();
g是函数名,h是函数指针。
一旦我们知道如何声明一个给定类型的变量,那么该类型的类型转换符就很容易得到了:只需把声明中的变量名和声明尾部的分号去掉,再将剩余的部分用一个括号括起来即可。
int (*h)(); // 声明一个函数指针
(int (*)() ) // 类型转换符
若fp是一个函数指针,可用如下语句调用它指向的函数
(*fp) ();
ANSI C允许简写为如下形式,但你要清楚 这只是简写而已
fp();
Signal函数与typedef
原始定义——确实难理解
void ( *signal(int, void(*)(int)) )(int);
使用typedef定义——很明了了吧
typedef void (*HANDLER)(int);
HANDLER signal (int, HANDLER);
运算符优先级问题
If(flags & FLAG != 0){...}
即使你觉得自己非常熟悉C,你可能将它理解为
If( (flags & FLAG) != 0){...}
之所以能写出这种令人疑惑的代码,是因为coder并不熟悉这片沼泽。
你是否也曾写出下面的句子?
R = hi<<4 + low
我的第一直觉告诉我这么写,经历一次诧异后,我想我以后再也不会这么写了。
添加括号可以避免此类想当然的问题,但括号太多反而不易理解。记住运算符的优先级是有益的。
C运算符优先级有15级,记住它并非易事,但若恰当分组,并理解各组运算符之间的相对优先级,记住此表并不难。
C语言运算符优先级表(由上至下,优先级减小)
运算符 |
组合性 |
备注 |
() [] -> . |
→ |
优先级最高的其实并不是真正意义上的运算符; |
! ~ ++ -- - (type) * & sizeof |
← |
单目运算符的优先级仅次于前者; |
* / % |
→ |
算术(众所周知 乘除的优先级高于加减) |
<< >> |
→ |
移位(无符号乘除2可用移位实现) |
< <= > >= |
→ |
关系(比较大小,判断等否) |
& ^ | |
→ |
位运算(任何2个位运算符具有不同优先级) |
&& || |
→ |
逻辑 |
?: |
← |
3目运算符 |
assignments |
← |
应该算多目运算符吧,因为可以连续赋值 |
, |
→ |
除在for中使用,很少见——多余吧 |
只需要记住2点:
算术~ > 移位~ > 关系~;
位~ > 逻辑~;
*p++ = *(p++)
(*p)() ≠ *p()
1/2*a = 0
While( c=getc(ifp) != EOF ) putc(c, ofp);
应改为如下才正确
While( (c=getc(ifp)) != EOF ) putc(c, ofp);
注意作为语句结束符的分号
不能多
If() ; = if(){}
... ...
也不能少
If(n<3)
Return
Logrec.date = x[0];
Logreg.time = x[1];
C语言把case标号当作真正意义上的标号,所以 需要显示地break。若确实不需要break, 为使别人不致怀疑,建议加上说明 如 /* fall through */
“悬挂”else引发的问题
If (x == 0)
If( y == 0) error();
Else
{
Z = x+y;
}
注意,else始终与同一对括号内最近的未匹配的if结合。
在使用宏定义时容易出现这种情况,所以 建议将“语句块”用{}括起来,即使只有简单一句——特别是你有意的占位符。
如果你曾是basic程序员,你可能觉得下面的语句更习惯
IF x==0 THEN
IF y==0 THEN
Error();
FI
ELSE
Z = x+y;
FI
使用宏定义可以实现如上收尾定界符的效果
#define IF { if (
#define THEN ) {
#define ELSE } else {
#define FI } }
需要提醒的是 你用这种方式写出的C代码会让别的程序员难于卒读,这样一种解决方案所带来的问题可能比它所解决的问题更糟糕。
指针与数组
它们之间的关系是如此密切不可分,以致于如果不能理解一个概念,就不法彻底理解另一概念。
1、C语言中只有一维数组,而且数组的大小必须在编译时确定。然而,数组的元素可以是任何类型的对象,当然也可以是另一个数组。
2、对于一个数组,我们可能做2件事:确定数组的大小,以及获得指向该数组下标为0的元素的指针。
请多程序设计语言内建有索引运算,在C语言中索引运算是以指针运算的形式来定义的。
Int calendar[12][31];
声明了calendar是一个数组,该数组有12个数组类型的元素,其中每个元素都是一个拥有31个整型元素的数组。
因此,sizeof(calendar)的值是 372(31×12)* sizeof(int).
指针与指针变量
指针即地址;
指针变量是特殊的变量,它的值是另一对象的地址;
实际中我们常将它们混淆,将指针变量简称为指针。
任何指针(变量)都是指针某种类型的变量,它也有自己的地址。
Int i;
Int *ip;
Ip = &i;
指针加减的意义
如果指针指向数组中某一元素,那么给指针加1就能够得到指向下一元素的指针。这说明一个事实:给指针加上整数 与给指针的二进制表示加上同样的整数,两者的含义是截然不同的。
如果两个指针指向同一数组中的元素,那么它们的差表示它们之间元素的个数。
数组名是一个(地址)常量
如果我们在应该出现指针的地方采用了数组名来替换,那么数组名被当作指向数组下标为0的指针。
Int a[3];
Int *p;
P=a;
如果你写成p=&a,有的compiler会报错,有的则不会。
P=&a这种写法在ANSI C中是非法的。因为&a是指向数组的指针,而p是一个指向整型变量的指针,它们的类型不匹配。大多数早期版本的C语言实现并没有所谓的“数组的地址”这一概念,因此&a或者视为非法,或者等于a。
除了a被用作运算符sizeof的参数这一情形,在其他所有的情形中数组名a都代表数组a中下标为0的元素的指针。
sizeof(a) 数组(占用空间)大小
Sizeof(int) 2,int占用2B
Sizeof(p) 4,指针变量占用空间大小
作为参数的数组声明
在C语言中,我们没有办法可以将一个数组作为参数直接传递。函数定义的参数列表中如果出现数组声明,则退化为同类型指针,即数组名被转换为指向第1个元素的指针,不再是常量。
int strlen(char s[]) 等同于 int strlen(char *s)
{...} {...}
main (int argc, char *argv[]) 等同于 main(int argc, char **argv)
{...} {...}
N阶魔方阵问题
bool isMagic( ① , int d)
{...}
main()
{
int M[20][20] = {
{9, 1, 5},
{3, 4, 8},
{6, 7, 2}
};
r = isMagic(M, 3);
}
①处是怎样?
A: int **magic
B: int *magic[20]
C: int (*magic)[20]
D: int magic[20][20]
E: int magic[ ][20]
前2种情况都不能通过编译,编译报错如下
Compile Err: error C2664: 'isMagic' : cannot convert parameter 1 from 'int [20][20]' to 'int ** '
作为函数参数C、D、E是等效的。
指向数组的指针
int calendar[12][31];
int (*monthp)[31];
monthp = calendar;
这样,monthp将指向数组calendar的第一个元素,也就是数组calendar的12个有着31个元素的数组类型元素之一。
int (*monthp)[31];
for(monthp = calendar; monthp < &calendar[12]; monthp++)
{
int *dayp;
for(dayp=*mohthp; dayp<&(*monthp[31]); dayp++)
{
*dayp = 0;
}
}
指针数组
串排序问题
如果使用2D数组来实现,排序过程中频繁的交换是低效的。定义字符指针数组,每个指针指向一个串,排序后再转存字符串。
函数指针数组
BFS解魔方问题
定义3D数组用于存储6面颜色,定义函数实现各种基本操作,定义函数指针数组并用子操作的函数地址初始化之,编写BFS(主度优先搜索)蛮力解魔方。
函数指针的类型定义如下:
typedef (*OP)();
OP ops[6]={ opF, opB, opL, opR, opT, opG}; // 顺时钟方向90度旋转 前面、后面、左面、右面、顶面、底面
库函数 malloc、strlen、strcpy
malloc 分配指定数量内存。成功 则返回指针,失败 则返回NULL。同类函数还有ralloc、calloc。
strlen 返回串长。串长不包括串结尾标志 '\0'。
strcpy 复制串,包括 '\0'。 更安全版本为 strncpy。
下面的代码有何问题
char *s="hi", *t=", pz";
main()
{
char *r, *mallock();
r = malloc(strlen(s) + strlen(t) );
strcpy(r, s);
strcap(r, t);
}
存在的问题:
① 内存泄漏 ② 越界 ③ 未考虑内存分配失败的异常
指针是指针,它指向一个地方,复制指针并不会复制它的指向的区域。
if(strcmp(p, (char *)0) == 0) 将产生错误,因为strcmp并不检查输入是否为NULL,对0解引用将出错。同样 printf(NULL) 的行为是未定义的。 所以,请看好自己的指针,不要随便指。
边界计算与不对称边界
避免“栏杆错误”的2个通用原则:
(1)首先考虑最简单情况下的特例,然后将得到的结束外推。
(2)仔细计算边界,绝不掉以轻心。
数组下标的“不对称边界”惯例:用第一个界点和第一个出界点来表示一个数值范围。
求值顺序
C语言只有4个运算符(&&、||、?: 和 ,)存在规定的求值顺序。其它所有运算符对其操作数求值的顺序是未定义的。特别地,赋值运算符并不保证任何求值顺序。
下面这种从数组x中复制前n个元素到数组y中的做法是不正确的,因为它对求值顺序作了太多的假设。
i = 0;
while( i
y[i] = x[i++];
它假设y[i]的地址将在i的自增操作执行之前被求值。
整数溢出
在无符号算术运算中,没有所谓“溢出”一说,所有无符号数都以2^n为模。如果算术运算两操作数分别是有、无符号数,则有符号数将转换为无符号数,“溢出”也不可能发生。但当两操作数都是有符号数时,“溢出”可能发生,而且结果是未定义的。
例如,假定a、b是两个非负整型变量,我们需要检查a+b是否会“溢出”。一种想当然的方式是这样:
if(a+b<0)
complain();