Chinaunix首页 | 论坛 | 博客
  • 博客访问: 1488567
  • 博文数量: 226
  • 博客积分: 3997
  • 博客等级: 少校
  • 技术积分: 2369
  • 用 户 组: 普通用户
  • 注册时间: 2010-06-19 17:26
个人简介

Never save something for a special occasion. Every day in your life is a special occasion.

文章分类

全部博文(226)

文章存档

2018年(5)

2017年(11)

2016年(1)

2015年(17)

2014年(14)

2013年(30)

2012年(5)

2011年(52)

2010年(107)

分类: 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,前面多一个含意就不同了。

你有没有认真读过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)的值是 37231×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) 2int占用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 ** '

作为函数参数CDE是等效的。

指向数组的指针

int calendar[12][31];

int (*monthp)[31];

monthp = calendar;

这样,monthp将指向数组calendar的第一个元素,也就是数组calendar12个有着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度旋转 前面、后面、左面、右面、顶面、底面

库函数 mallocstrlenstrcpy

malloc 分配指定数量内存。成功 则返回指针,失败 则返回NULL。同类函数还有ralloccalloc

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为模。如果算术运算两操作数分别是有、无符号数,则有符号数将转换为无符号数,“溢出”也不可能发生。但当两操作数都是有符号数时,“溢出”可能发生,而且结果是未定义的。

例如,假定ab是两个非负整型变量,我们需要检查a+b是否会“溢出”。一种想当然的方式是这样:

if(a+b<0)

complain();

 

阅读(872) | 评论(0) | 转发(0) |
0

上一篇:强制类型转换

下一篇:C陷阱与缺陷 2/2

给主人留下些什么吧!~~