Chinaunix首页 | 论坛 | 博客
  • 博客访问: 302245
  • 博文数量: 59
  • 博客积分: 1410
  • 博客等级: 上尉
  • 技术积分: 570
  • 用 户 组: 普通用户
  • 注册时间: 2009-03-21 09:31
文章分类

全部博文(59)

文章存档

2011年(1)

2009年(58)

我的朋友

分类: C/C++

2009-12-09 10:05:48

C言缺陷与陷阱(笔记)
C言像一把雕刻刀,利,并且在技手中非常有用。和任何利的工具一C到那些不能掌握它的人。本文介C害粗心的人的方法,以及如何避免害。
第一部分研究了当程序被划分为记生的问题第二部分继续研究了当程序的号被编译声明、表达式和会出问题第三部分研究了由多个部分成、分别编译定到一起的C程序。第四部分理了概念上的解:当一个程序具体生的事情。第五部分研究了我的程序和它所使用的常用系。在第六部分中,我注意到了我所写的程序也并不是我所运行的程序;预处理器将首先运行。最后,第七部分讨论了可移植性问题:一个能在一个实现中运行的程序无法在另一个实现中运行的原因。
法分析器(lexical analyzer检查组成程序的字符序列,并将它划分为记号(token)一个号是一个由一个或多个字符构成的序列,它在言被编译时具有一个(相地)一的意
C程序被两次划分为记首先是预处理器取程序它必须对程序号划分以发现标识宏的标识符。通过对每个宏行求来替最后,经过宏替的程序又被集成字符流送给编译器。编译器再第二次将个流划分为记号。
1.1= 不是 ==C是用=表示赋值而用==表示比是因为赋值率要高于比,因此其分配更短的符号。C赋值视为一个运算符,因此可以很容易地写出多重赋值(如a = b = c),并且可以将赋值嵌入到一个大的表达式中。
C言参考手册明了如何决定:如果入流到一个定的字符串止已识别为记号,则应该包含下一个字符以成能构成号的最的字符串” “子串原
    赋值运算符如+=实际上是两个号。因此,
a + /* strange */ = 1
a += 1
是一个意思。看起来像一个独的号而实际上是多个号的只有一个特例。特地,
p - > a
是不合法的。它和
p -> a
不是同义词
另一方面,有些老式编译是将=+视为一个独的号并且和+=是同义词
引号中的一个字符只是写整数的另一方法。个整数是定的字符在实现照序列中的一个对应。而一个包在双引号中的字符串,只是写一个有双引号之的字符和一个附加的二值为零的字符所初始化的一个无名数的指的一种简短方法。
使用一个指来代替一个整数通常会得到一个警告消息(反之亦然),使用双引号来代替引号也会得到一个警告消息(反之亦然)。但于不检查参数型的编译器却除外。
由于一个整数通常足大,以至于能放下多个字符,一些C编译器允在一个字符常量中存放多个字符。意味着用'yes'代替"yes"将不会被发现。后者意味着包含yes和一个空字符的四个连续器区域中的第一个的地址,而前者意味着在一些实现式中表示由字符yes合构成的一个整数两者之的任何一致性都属巧合。
理解号是如何构成声明、表达式、句和程序的。
C量声明都具有两个部分:一个型和一具有特定格式的、期望用来对该类型求的表达式。
float *g(), (*h)();
表示*g()(*h)()都是float表达式。由于()*定得更密,*g()*(g())表示同西:g是一个返回指float的函数,而h是一个指向返回float的函数的指
当我知道如何声明一个型的量以后,就能很容易地写出一个型的模型(cast):只要量名和分号并将所有的西包在一对圆括号中即可。
float *g();
声明g是一个返回float的函数,所以(float *())就是它的模型。
(*(void(*)())0)();硬件会用地址0的子程序
(*0)(); 这样并不行,因*运算符要求必有一个指它的操作数。另外,个操作数必是一个指向函数的指,以保*果可以被用。需要将0转换为一个可以描述指向一个返回void的函数的指型。(Void(*)())0
里,我解决问题时没有使用typedef声明。通使用它,我可以更清晰地解决问题
typedef void (*funcptr)();// typedef funcptr void (*)();指向返回void的函数的指针
(*(funcptr)0)();
//调用地址为0处的子程序
定得最密的运算符并不是真正的运算符:下、函数用和选择些都与左关联
接下来是一元运算符。它具有真正的运算符中的最高。由于函数用比一元运算符定得更密,你必(*p)()p指向的函数;*p()表示p是一个返回一个指的函数。转换是一元运算符,并且和其他一元运算符具有相同的。一元运算符是右合的,因此*p++表示*(p++),而不是(*p)++
在接下来是真正的二元运算符。其中数学运算符具有最高的,然后是移位运算符、系运算符、逻辑运算符、赋值运算符,最后是条件运算符。需要住的两个重要的西是:
1.    所有的逻辑运算符具有比所有系运算符都低的
2.    移位运算符比系运算符定得更密,但又不如数学运算符。
乘法、除法和求余具有相同的,加法和减法具有相同的,以及移位运算符具有相同的
有就是六个系运算符并不具有相同的==!=比其他系运算符要低。
逻辑运算符中,没有任何两个具有相同的。按位运算符比所有序运算符定得都密,每种与运算符都比相的或运算符定得更密,并且按位异或(^)运算符介于按位与和按位或之
    三元运算符的比我提到的所有运算符的都低。
个例子还说明了赋值运算符具有比条件运算符更低的是有意的。另外,所有的赋值运算符具有相同的并且是自右至左合的
具有最低的是逗号运算符。赋值是另一运算符,通常具有混合的
或者是一个空句,无任何效果;或者编译器可能提出一个断消息,可以方便除去掉它。一个重要的区是在必跟有一个句的ifwhile句中。另一个因分号引起巨大不同的地方是函数定前面的构声明的末尾下面的程序片段:
struct foo {
    int x;
}

f() {
    ...
}
挨着f的第一个}后面失了一个分号。它的效果是声明了一个函数f,返回值类型是struct foo构成了函数声明的一部分。如果里出了分号,f将被定义为具有默的整型返回[5]
C中的case标签是真正的标签:控制流程可以无限制地入到一个case标签中。
    看看另一形式,假C程序段看起来更像Pascal
switch(color) {
case 1: printf ("red");
case 2: printf ("yellow");
case 3: printf ("blue");
}
并且假color2则该程序将打印yellowblue,因控制自然地入到下一个printf()用。
既是Cswitch句的点又是它的弱点。它是弱点,是因很容易忘一个break句,从而致程序出现隐晦的异常行它是点,是因故意去掉break句,可以很容易实现其他方法实现的控制构。尤其是在一个大型的switch句中,我们经发现对一个case理可以化其他一些特殊的理。
和其他程序设计语言不同,C要求一个函数用必有一个参数列表,但可以没有参数。因此,如果f是一个函数,
f();
就是对该函数用的句,而
f;
也不做。它会作函数地址被求,但不会用它[6]
一个else是与其最近的if关联
一个C程序可能有很多部分成,它被分别编译,并由一个通常称为连接器、编辑器或加器的程序定到一起。由于编译器一次通常只能看到一个文件,因此它无法检测到需要程序的多个源文件的内容才能发现错误
你有一个C程序,被划分两个文件。其中一个包含如下声明:
int n;
而令一个包含如下声明:
long n;
不是一个有效的C程序,因一些外部名称在两个文件中被声明不同的型。然而,很多实现检测不到错误,因为编译器在编译其中一个文件并不知道另一个文件的内容。因此,检查类型的工作只能由接器(或一些工具程序如lint)来完成;如果操作系接器不能识别数据型,C编译器也没法多地制它。
    个程序运行时实际生什有很多可能性:
1.    实现够聪明,能够检测型冲突。会得到一个断消息,n在两个文件中具有不同的型。
2.    你所使用的实现intlong视为相同的型。典型的情况是机器可以自然地32位运算。在这种情况下你的程序或工作,好象你两次都将量声明long(或int)。这种程序的工作属偶然。
3.    n的两个例需要不同的存,它以某方式共享存区,即其中一个的赋值对另一个也有效。可能生,例如,编译器可以将int安排在long的低位。不论这是基于系是基于机器的,这种程序的运行同是偶然。
4.    n的两个例以另一方式共享存区,即其中一个赋值的效果是另一个以不同的。在这种情况下,程序可能失
这种情况生的一个例子出奇地繁。程序的某一个文件包含下面的声明:
char filename[] = "etc/passwd";
而另一个文件包含这样的声明:
char *filename;
    尽管在某些境中数和指的行非常相似,但它是不同的。在第一个声明中,filename是一个字符数的名字。尽管使用数的名字可以生数第一个元素的指,但个指只有在需要的候才生并且不会持。在第二个声明中,filename是一个指的名字。个指可以指向程序员让它指向的任何地方。如果程序没有一个,它将具有一个默0NULL)([]实际上,在C中一个初始化的指通常具有一个随机的是很危的!)。
    两个声明以不同的方式使用存区,它不可能共存。
    避免这种类型冲突的一个方法是使用像lint这样的工具(如果可以的)。了在一个程序的不同编译单元之间检查类型冲突,一些程序需要一次看到其所有部分。典型的编译器无法完成,但lint可以。
    避免该问题的另一方法是将外部声明放到包含文件中这时,一个外部象的一次[7]
    一些C运算符以一已知的、特定的其操作数行求。但另一些不能。例如,考下面的表达式:
a < b && c < d
C言定义规a < b首先被求。如果a小于bc < d须紧接着被求算整个表达式的。但如果a大于或等于bc < d根本不会被求
a < b编译ab的求就会有一个先后。但在一些机器上,它是并行行的。
C中只有四个运算符&&||?:,指定了求值顺序。&&||最先的操作数行求,而右的操作数只有在需要的候才行求。而?:运算符中的三个操作数:abc,最先a行求,之后仅对bc中的一个行求取决于a,运算符首先的操作数行求,然后抛弃它的的操作数行求[8]
C中所有其它的运算符操作数的求值顺序都是未定的。事上,赋值运算符不值顺序做出任何保
    出于个原因,下面这种将数x中的前n个元素制到数y中的方法是不可行的:
i = 0;
while(i < n)
    y[i] = x[i++];
其中的问题y[i]的地址并不保i之前被求。在某些实现中,是可能的;但在另一些实现中却不可能。另一情况出于同的原因会失
i = 0;
while(i < n)
    y[i++] = x[i];
而下面的代是可以工作的:
i = 0;
while(i < n) {
    y[i] = x[i];
    i++;
}
当然,可以
for(i = 0; i < n; i++)
    y[i] = x[i];
    在很多言中,具有n个元素的数其元素的号和它的下是从1n对应的。但在C中不是这样
个具有n个元素的C中没有下标为n的元素,其中的元素的下是从0n - 1。因此从其它C言的程序员应该小心地使用数
int i, a[10];
for(i = 1; i <= 10; i++)
    a[i] = 0;
    下面的程序段由于两个原因会失
double s;
s = sqrt(2);
printf("%g\n", s);
    第一个原因是sqrt()需要一个double它的参数,但没有得到。第二个原因是它返回一个double但没有这样声名。改正的方法只有一个:
double s, sqrt();
s = sqrt(2.0);
printf("%g\n", s);
C中有两个简单规则控制着函数参数的转换(1)int短的整型被转换为int(2)double短的浮点转换为double。所有的其它不被转换确保函数参数型的正确性是程序任。
因此,一个程序如果想使用如sqrt()这样接受一个double型参数的函数,就必须仅传递给floatdouble型的参数。常数2是一个int,因此其型是错误的。
    当一个函数的被用在表达式中,其会被自转换为适当的型。然而,了完成个自动转换编译器必知道函数实际返回的型。没有更声名的函数被假返回int,因此声名这样的函数并不是必的。然而,sqrt()返回double,因此在成功使用它之前必要声名。
里有一个更加壮的例子:
main() {
    int i;
    char c;
    for(i = 0; i < 5; i++) {
        scanf("%d", &c);
        printf("%d", i);
    }
    printf("\n");
}
    表面上看,个程序从入中取五个整数并向出写入0 1 2 3 4实际上,它并不这么做。譬如在一些编译器中,它的0 0 0 0 0 1 2 3 4
    ?因c的声名是char而不是int。当你令scanf()取一个整数,它需要一个指向一个整数的指。但里它得到的是一个字符的指。但scanf()并不知道它没有得到它所需要的:它将入看作是一个指向整数的指并将一个整数存到那里。由于整数占用比字符更多的内存,这样做会影响到c附近的内存。
    c附近确切是什编译器的事;在这种情况下有可能是i的低位。因此,当向c入一个i就被置零。当程序最后到达文件scanf()不再尝试c中放入新i才可以正常地增,直到循环结束。
阅读(1146) | 评论(0) | 转发(0) |
0

上一篇:Qt 深入浅出

下一篇:FTP协议完全详解

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