关闭

C语言缺陷与陷阱

310人阅读 评论(0) 收藏 举报
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才可以正常地增,直到循环结束。
 
C程序通常将一个字符串转换为一个以空字符尾的字符数。假有两个这样的字符串st,并且我想要将它们连一个独的字符串r。我通常使用函数strcpy()strcat()来完成。下面这种的方法并不会工作:
char *r;
strcpy(r, s);
strcat(r, t);
是因r没有被初始化指向任何地方。尽管r可能潜在地表示某一内存,但并不存在,直到你分配它。
    试试r分配一些内存:
char r[100];
strcpy(r, s);
strcat(r, t);
只有在st所指向的字符串不很大的候才能工作。不幸的是,C要求我们为指定的大小是一个常数,因此无法确定r是否足大。然而,很多C实现带有一个叫做malloc()函数,它接受一个数字并分配这么多的内存。通常有一个函数称strlen(),可以告一个字符串中有多少个字符:因此,我可以写:
char *r, *malloc();
r = malloc(strlen(s) + strlen(t));
strcpy(r, s);
strcat(r, t);
    然而个例子会因两个原因而失。首先,malloc()可能会耗尽内存,而个事件静静地返回一个空指来表示。
    其次,更重要的是,malloc()并没有分配足的内存。一个字符串是以一个空字符束的。而strlen()函数返回其字符串参数中所包含字符的数量,但不包括尾的空字符。因此,如果strlen(s)ns需要n + 1个字符来盛放它。因此我需要r分配外的一个字符。再加上检查malloc()是否成功,我得到:
char *r, *malloc();
r = malloc(strlen(s) + strlen(t) + 1);
if(!r) {
    complain();
    exit(1);
}
strcpy(r, s);
strcat(r, t);
 法(Synecdoche, sin-ECK-duh-key)是一文学手法,有点似于明或暗,在牛津英文典中解如下:“a more comprehensive term is used for a less comprehensive or vice versa; as whole for part or part for whole, genus for species or species for genus, etc.(将全面的位用作不全面的位,或反之;如整体局部或局部整体、一般特殊或特殊一般,等等。)
住的是,制一个指并不能制它所指向的西
将一个整数转换为一个指果是实现的(implementation-dependent),除了一个例外。个例外是常数0,它可以保转换为一个与其它任何有效指都不相等的指通常这样
#define NULL 0
但其效果是相同的。要住的一个重要的事情是,当用0针时它决不能被解除引用话说,当你将0赋给一个指针变量后,你就不能访问它所指向的内存。不能这样写:
if(p == (char *)0) ...
也不能这样写:
if(strcmp(p, (char *)0) == 0) ...
strcmp()是通其参数来看内存地址的。
如果p是一个空指这样写也是无效的:
printf(p);
printf("%s", p);
C于整数操作的上溢或下溢定得非常明确。
    只要有一个操作数是无符号的,果就是无符号的,并且以2n模,其中n。如果两个操作数都是符号的,则结果是未定的。
例如,假ab是两个非整型量,你希望测试a + b是否溢出。一个明法是这样的:
if(a + b < 0)
    complain();
通常,是不会工作的。
    一旦a + b生了溢出,果的任何注都是没有意的。例如,在某些机器上,一个加法运算会将一个内部寄存器:正、、零或溢出。这样的机器上,编译器有将上面的例子实现为首先将ab加在一起,然后检查内部寄存器状是否为负。如果运算溢出,内部寄存器将于溢出状测试会失
    使个特殊的测试成功的一个正确的方法是依于无符号算的良好定,即要在有符号和无符号之间进转换
if((int)((unsigned)a + (unsigned)b) < 0)
    complain();
两个原因会令使用移位运算符的人感到烦恼
1.    在右移运算中,空出的位是用0填充是用符号位填充?
2.    移位的数量允使用哪些数?
第一个问题的答案很简单,但有实现的。如果要行移位的操作数是无符号的,会移入0。如果操作数是符号的,则实现决定是移入0是移入符号位。如果在一个右移操作中你很心空位,那unsigned来声明量。这样你就有空位被0
    第二个问题的答案同样简单:如果待移位的数n移位的数量必大于等于0并且格地小于n。因此,在一次独的操作中不可能将所有的位从量中移出
例如,如果一个int32位,且n是一个int,写n << 31n << 0是合法的,但n << 32n << -1是不合法的。
    注意,即使实现将符号移入空位,一个符号整数的右移运算和除以2的某次也不是等价的。一点,考(-1) >> 1是不可能0的。[注:(-1) / 2果是0]
下面的程序:
#include

main() {
    char c;
//int c

    while((c = getchar()) !=
EOF)
        putchar(c);
}
    段程序看起来好像要将制到出。实际上,它并不完全会做些。
    原因是c被声明字符而不是整数。意味着它将不能接收可能出的所有字符包括EOF
因此里有两可能性。有一些合法的入字符会cEOF相同的,有又会使c无法存放EOF。在前一情况下,程序会在文件的中停止制。在后一情况下,程序会陷入一个无限循
    实际上,存在着第三可能:程序会偶然地正确工作。C言参考手册格地定了表达式
((c = getchar()) != EOF)
果。其6.1中声明:
当一个较长的整数被转换为一个短的整数或一个char,它会被截去左;超出的位被简单弃。
7.14声明:
存在着很多赋值运算符,它都是从右至左合的。它都需要一个左的操作数,而赋值表达式的型就是其左的操作数的型。其就是已经赋过值的左操作数的
两个条款的合效果就是必过丢getchar()果的高位,将其截短字符,之后个被截短的再与EOF行比。作为这个比的一部分,c一个整数,或者采取将左的位用0填充,或者适当地采取符号展。
然而,一些编译器并没有正确地实现这个表达式。它getchar()的低几位赋给c。但在cEOF的比中,它却使用了getchar()这样做的编译器会使个事例程序看起来能正确地工作。
立即安排出的示通常比将其暂时保存在一大一起出要昂得多。因此,C实现通常允程序控制生多少出后在实际地写出它
    个控制通常一个称setbuf()函数。如果buf是一个具有适当大小的字符数
setbuf(stdout, buf);
将告I/O写入到stdout中的出要以buf一个冲,并且等到buf了或程序直接fflush()实际写出。冲区的合适的大小在中定义为BUFSIZ
因此,下面的程序解了通使用setbuf()讲标制到出:
#include

main() {
    int c;

    char buf[BUFSIZ];
    setbuf(stdout, buf);

    while((c = getchar()) !=
EOF)
        putchar(c);
}
    不幸的是,个程序是错误的,因一个微的原因。
    要知道毛病出在哪,我需要知道冲区最后一次刷新是在什么时候。答案;主程序完成之后,将控制交回到操作系之前所行的清理的一部分。在刻,冲区已放了
    有两方法可以避免问题
    首先,使用静态缓冲区,或者将其式地声明
static char buf[BUFSIZ];
或者将整个声明移到主函数之外。
    另一可能的方法是动态地分配冲区并且从不放它:
char *malloc();
setbuf(stdout, malloc(BUFSIZ));
注意在后一情况中,不必检查malloc()的返回,因如果它失了,会返回一个空指。而setbuf()可以接受一个空指其第二个参数,将使得stdout成非冲的。会运行得很慢,但它是可以运行的。
由于宏可以象函数那,有些程序就会将它们视为等价的。因此,看下面的定
#define max(a, b) ((a) > (b) ? (a) : (b))
注意宏体中所有的括号。它了防止出ab有比>低的表达式的情况。
    一个重要的问题是,像max()这样个操作数都会出两次并且会被求两次。因此,在个例子中,如果ab大,a就会被求两次:一次是在比候,而另一次是在max()候。
    是低效的,错误
biggest = x[0];
i = 1;
while(i < n)
    biggest = max(biggest, x[i++]);
max()是一个真正的函数会正常地工作,但当max()是一个宏的候会失。譬如,假x[0]2x[1]3x[2]1。我来看看在第一次循环时生什赋值语句会被
biggest = ((biggest) > (x[i++]) ? (biggest) : (x[i++]));
首先,biggestx[i++]行比。由于i1x[1]3系是。其副作用是,i2
    由于系是x[i++]赋给biggest。然而,这时i2了,因此赋给biggestx[2],即1
避免问题的方法是保max()宏的参数没有副作用:
biggest = x[0];
for(i = 1; i < n; i++)
    biggest = max(biggest, x[i]);
有一个危的例子是混合宏及其副作用是来自UNIX第八版的中putc()宏的定
#define putc(x, p) (--(p)->_cnt >= 0 ? (*(p)->_ptr++ = (x)) : _flsbuf(x, p))
putc()的第一个参数是一个要写入到文件中的字符,第二个参数是一个指向一个表示文件的内部数据构的指。注意第一个参数完全可以使用如*z++西,尽管它在宏中两次出,但只会被求一次。而第二个参数会被求两次(在宏体中,x了两次,但由于它的两次出在一个:的两,因此在putc()的一个例中它之中有且有一个被求)。由于putc()中的文件参数可能有副作用,会出现问题。不,用手册文档中提到:由于putc()实现为宏,其stream可能会具有副作用。特putc(c, *f++)不能正确地工作。但是putc(*c++, f)实现中是可以工作的。
有些C实现很不小心。例如,没有人能正确putc(*c++, f)。另一个例子,考很多C中出toupper()函数。它将一个小写字母转换为的大写字母,而其它字符不。如果我所有的小写字母和所有的大写字母都是相的(大小写之可能有所差距),我可以得到这样的函数:
toupper(c) {
    if(c >= 'a' && c <= 'z')
        c += 'A' - 'a';
    return c;
}
在很多C实现中,了减少比实际计要多的开销,通常将其实现为宏:
#define toupper(c) ((c) >= 'a' && (c) <= 'z' ? (c) + ('A' - 'a') : (c))
很多比函数要快。然而,当你着写toupper(*p++),会出奇怪的果。
    另一个需要注意的地方是使用宏可能会生巨大的表达式。例如,继续max()的定
#define max(a, b) ((a) > (b) ? (a) : (b))
们这个定abcd中的最大。如果我直接写:
max(a, max(b, max(c, d)))
它将被
((a) > (((b) > (((c) > (d) ? (c) : (d))) ? (b) : (((c) > (d) ? (c) : (d))))) ?
 (a) : (((b) > (((c) > (d) ?
(c) : (d))) ? (b) : (((c) > (d) ? (c) : (d))))))
出奇的大。我可以通平衡操作数来使它短一些:
max(max(a, b), max(c, d))
会得到:
((((a) > (b) ? (a) : (b))) > (((c) > (d) ? (c) : (d))) ?
 (((a) > (b) ? (a) : (b))) : (((c) > (d) ? (c) : (d))))
看起来是写:
biggest = a;
if(biggest < b) biggest = b;
if(biggest < c) biggest = c;
if(biggest < d) biggest = d;
好一些。
宏的一个通常的用途是保不同地方的多个事物具有相同的型:
#define FOOTYPE struct foo
FOOTYPE a;
FOOTYPE b, c;
程序可以通只改程序中的一行就能改abc型,尽管abc可能声明在很的不同地方。
    使用这样的宏定义还有着可移植性的优势——所有的C编译器都支持它。很多C编译器并不支持另一方法:
typedef struct foo FOOTYPE;
FOOTYPE义为一个与struct foo等价的新型。
    种为类型命名的方法可以是等价的,但typedef更灵活一些。例如,考下面的例子:
#define T1 struct foo *
typedef struct foo * T2;
两个定使得T1T2都等价于一个struct foo的指。但看看当我们试图在一行中声明多于一个量的候会生什
T1 a, b;
T2 c, d;
第一个声明被
struct foo * a, b;
a被定义为一个构指,但b被定义为一个构(而不是指)。相反,第二个声明中cd都被定义为指向构的指,因T2的行好像真正的型一
今天,一个C程序如果想写出于不同境中的用都有用的程序就必知道很多微的差
一个标识符是一个字符和数字序列,第一个字符必是一个字母。下划线_算作字母。大写字母和小写字母是不同的。只有前八个字符是名,但可以使用更多的字符。可以被多种汇编器和加器使用的外部标识符,有着更多的限制:
下面著的函数:
char *Malloc(unsigned n) {
    char *p, *malloc();
    p = malloc(n);
    if(p == NULL)
        panic("out of memory");
    return p;
}
    个函数是保耗尽内存而不会致没有检测的一个简单法。程序可以通过调Mallo()来代替malloc()。如果malloc()不幸失,将panic()示一个恰当的错误消息并止程序。
然而,考函数用于一个忽略大小写区的系生什这时,名字mallocMalloc是等价的。话说函数malloc()被上面的Malloc()函数完全取代了,当malloc()用的是它自己。然,其果就是第一次尝试分配内存就会陷入一个递归并随之生混乱。但在一些能区分大小写的实现个函数是可以工作的。
C程序提供三整数尺寸:普通、短和有字符,其行像一个很小的整数。C言定义对整数的大小不作任何保
1.    整数的四尺寸是非减的。
2.    普通整数的大小要足存放任意的数
3.    字符的大小应该特定硬件的本
代机器具有8位字符,不过还有一些具有79位字符。因此字符通常是789位。
    整数通常至少32位,因此一个整数可以用于表示文件的大小。
    普通整数通常至少16位,因太小的整数会更多地限制一个数的最大大小。
    短整数是恰好16位。
更可移植的做法是定一个新的型:
typedef long tenmil;
在你就可以使用型来声明一个量并知道它的度了,最坏的情况下,你也只要改变这独的型定就可以使所有量具有正确的型。
问题在将一个char转换为一个更大的整数时变得尤重要。于相反的转换,其果却是定良好的:多余的位被简单弃掉。但一个编译器将一个char转换为一个int却需要作出选择:将char视为带符号量是无符号量?如果是前者,将charint制符号位;如果是后者,要将多余的位用0填充。
个决定的于那些在理字符时习惯将高位置1的人来非常重要。决定着8位的字符范是从-128127是从0255又影响着程序员对哈希表和转换表之西的设计
    如果你心一个字符最高位置一是否被视为一个数,你应该显式地将它声明unsigned char这样就能保转换为整数是基0的,而不像普通char量那在一些实现中是符号的而在另一些实现中是无符号的。
另外,有一种误解是认为c是一个字符,可以通(unsigned)c来得到与c等价的无符号整数。错误的,因一个char行任何操作(包括转换)之前转换为int这时c会首先转换为一个符号整数再转换为一个无符号整数,生奇怪的果。
    正确的方法是写(unsigned char)c
里再一次重:一个心右移操作如何行的程序最好将所有待移位的量声明无符号的。
ba得到商q余数r
q = a / b;
r = a % b;
们暂时b > 0
1.    最重要的,我期望q * b + r == a,因为这余数的定
2.    如果a的符号生改,我期望q的符号也生改,但绝对值
3.    希望保r >= 0r < b。例如,如果余数将作一个哈希表的索引,它必要保证总是一个有效的索引。
    三点清楚地描述了整数除法和求余操作。不幸的是,它不能同时为真。
3 / 2,商10(1)这满足第一点。而-3 / 2呢?根据第二点,商应该-1,但如果是这样,余数必也是-1这违反了第三点。或者,我可以通将余数标记为1足第三点,但这时根据第一点商应该-2反了第二点。
因此C和其他任何实现了整数除法舍入的言必放弃上述三个原中的至少一个。
很多程序设计语言放弃了第三点,要求余数的符号必和被除数相同。可以保第一点和第二点。很多C实现也是这样做的。
 尽管有些候不需要灵活性,C是足可以令除法完成我所要做的、提供我所想知道的。例如,假有一个数n表示一个标识符中的字符的一些函数,并且我想通除法得到一个哈希表入口h,其中0 <= h <= HASHSIZE。如果我知道n是非的,我可以简单地写:
h = n % HASHSIZE;
然而,如果n有可能是的,这样写就不好了,因h可能也是的。然而,我知道h > -HASHSIZE,因此我可以写:
h = n % HASHSIZE;
if(n < 0)
    h += HASHSIZE;
    ,将n声明unsigned也可以。
个尺寸是模糊的,库设计影响。在PDP-11[10]机器上运行的有的C实现中,有一个称rand()的函数可以返回一个()随机非整数。PDP-11中整数度包括符号位是16位,因此rand()返回一个0215-1的整数。
    CVAX-11实现时,整数的变为32。那VAX-11上的rand()函数返回是什呢?
    个系,加利福尼大学的认为rand()的返回值应该涵盖所有可能的非整数,因此它rand()版本返回一个0231-1的整数。
    AT&T的人则觉得如果rand()函数仍然返回一个0215可以很容易地将PDP-11中期望rand()返回一个小于215的程序移植到VAX-11上。
    因此,写出不依赖实现rand()函数的程序。
toupper()tolower()函数有着似的史。他最初都被实现为宏:
#define toupper(c) ((c) + 'A' - 'a')
#define tolower(c) ((c) + 'A' - 'a')
些宏确有一个缺陷,即:当定的西不是一个恰当的字符,它会返回垃圾。因此,下面个通使用些宏来将一个文件转为小写的程序是无法工作的:
int c;
while((c = getchar()) !=
EOF)
    putchar(tolower(c));
写:
int c;
while((c = getchar()) != EOF)
    putchar(isupper(c) ? tolower(c) : c);
一点,AT&T中的UNIX开发组织提醒我toupper()tolower()都是事先经过一些适当的参数测试的。考虑这样重写些宏:
#define toupper(c) ((c) >= 'a' && (c) <= 'z' ? (c) + 'A' - 'a' : (c))
#define tolower(c) ((c) >= 'A' && (c) <= 'Z' ?
(c) + 'a' - 'A' : (c))
但要知道,c的三次出都要被求会破坏如toupper(*p++)这样的表达式。因此,可以考toupper()tolower()重写函数。toupper()看起来可能像这样
int toupper(int c) {
    if(c >= 'a' && c <= 'z')
        return c + 'A' - 'a';
    return c;
}
tolower()似。
    个改变带来更多的问题次使用些函数的候都会引入函数开销。我的英雄认为一些人可能不愿意支付开销,因此他个宏重命名
#define _toupper(c) ((c) + 'A' - 'a')
#define _tolower(c) ((c) + 'a' - 'A')
就允户选择方便或速度。
    里面其只有一个问题:伯克利的人和其他的C实现者并没有跟着这么做。意味着一个在AT&T写的使用了toupper()tolower()的程序,如果没有传递正确大小写字母参数,在其他C实现中可能不会正常工作。
    如果不知道史,可能很难对这类错误进行跟踪。
很多C实现为提供了三个内存分配函数:malloc()realloc()free()malloc(n)返回一个指向有n个字符的新分配的内存的指个指可以由程序使用。free()传递一个指向由malloc()分配的内存的指可以使这块内存得以再次使用。通一个指向已分配区域的指和一个新的大小realloc()可以将这块内存大或小到新尺寸,程中可能要制内存。
    有人会想,真相真是有点微妙啊。下面是System V接口定中出realloc()的描述:
realloc一个由ptr指向的size个字,并返回该块(可能被移)的指在新旧尺寸中比小的一个尺寸之下的内容不会被改此外,包含了描述realloc()的另外一段:
如果在最后一次mallocrealloccalloc放了ptr所指向的realloc依旧可以工作;因此,freemallocrealloc序可以利用malloc压缩找策略。
因此,下面的代片段在UNIX第七版中是合法的:
free (p);
p = realloc(p, newsize);
    一特性保留在从UNIX第七版衍生出来的系中:可以先放一区域,然后再重新分配它。意味着,在些系放的内存中的内容在下一次内存分配之前可以保。因此,在些系中,我可以用下面这种奇特的思想来放一个表中的所有元素:
for(p = head; p != NULL; p = p->next)
    free((char *)p);
而不用担心free()p->next不可用。
不用这种是不推荐的,因不是所有C实现都能在内存被放后将它的内容保留足够长时间。然而,第七版的手册留了一个未声明的问题realloc()的原始实现实际上是必要先放再重新分配的。出于个原因,一些C程序都是先放内存再重新分配的,而当些程序移植到其他实现就会出现问题
下面的程序有两个参数:一个整数和一个函数(的指)。它将整数转换位十制数,并用代表其中一个数字的字符来定的函数。
void printnum(long n, void (*p)()) {
    if(n < 0) {
        (*p)('-');
        n = -n;
    }
    if(n >= 10)
        printnum(n / 10, p);
    (*p)(n % 10 + '0');
}
    个程序非常简单。首先检查n是否为负数;如果是,打印一个符号并将n变为正数。接下来,测试是否n >= 10。如果是,它的十制表示中包含两个或更多个数字,因此我们递归printnum()来打印除最后一个数字外的所有数字。最后,我打印最后一个数字。
    个程序——由于它的简单——具有很多可移植性问题。首先是将n的低位数字转换成字符形式的方法。用n % 10取低位数字的是好的,但它加上'0'得相的字符表示就不好了。个加法假机器中序的数字所对应的字符数序的,没有隔,因此'0' + 5'5'是相同的,等等。尽管个假设对ASCIIEBCDIC字符集是成立的,但于其他一些机器可能不成立。避免问题的方法是使用一个表:
void printnum(long n, void (*p)()) {
    if(n < 0) {
        (*p)('-');
        n = -n;
    }
    if(n >= 10)
        printnum(n / 10, p);
    (*p)("0123456789"[n % 10]);
}
    另一个问题发生在当n < 0这时程序会打印一个号并将n-n赋值生溢出,因在使用2补码的机器上通常能表示的数比正数要多。例如,一个()整数有k位和一个附加位表示符号,-2k可以表示而2k却不能。
    解决问题有很多方法。最直的一是将n赋给一个unsigned long。然而,一些C便一起可能没有实现unsigned long,因此我来看看没有它怎么办
    在第一个实现和第二个实现的机器上,改一个正整数的符号保不会生溢出。问题仅出在改一个数的符号。因此,我可以通避免将n变为正数来避免问题
    当然,一旦我打印了数的符号,我就能数和正数视为是一的。下面的方法就制在打印符号之后n为负数,并且用完成我所有的算法。如果我们这么做,我就必程序中打印符号的部分只行一次;一个简单的方法是将个程序划分两个函数:
void printnum(long n, void (*p)()) {
    if(n < 0) {
        (*p)('-');
        printneg(n, p);
    }
    else
        printneg(-n, p);
}

void printneg(long n, void (*p)()) {
    if(n <= -10)
        printneg(n / 10, p);
    (*p)("0123456789"[-(n % 10)]);
}
    printnum()在只检查要打印的数是否为负数;如果是的话则打印一个符号。否,它以n负绝对值printneg()。我printneg()的函数体来适n数或零一事
    得到什?我使用n / 10n % 10n的前数字和尾数字(经过适当的符号变换)。用整数除法的行在其中一个操作数为负候是实现的。因此,n % 10有可能是正的!这时-(n % 10)数,将会超出我的数字字符数的末尾。
    了解决问题,我建立两个临时变量来存放商和余数。作完除法后,我们检查余数是否在正确的范内,如果不是的话则调两个量。printnum()没有改,因此我只列出printneg()
void printneg(long n, void (*p)()) {
    long q;
    int r;
    if(r > 0) {
        r -= 10;
        q++;
    }
    if(n <= -10) {
        printneg(q, p);
    }
    (*p)("0123456789"[-r]);
}
    The C Programming Language》(Kernighan and Ritchie, Prentice-Hall 1978)是最具威的C著作。它包含了一个秀的教程,面向那些熟悉其他高级语言程序设计的人,和一个参考手册,简洁地描述了整个言。尽管自1978年以来这门语生了不少化,书对于很多主仍然是个定时还包含了本文中多次提到的“C言参考手册
    The C Puzzle Book》(Feuer, Prentice-Hall, 1982)是一本少的磨文法能力的收集了很多谜题(和答案),它的解决方法能够测试读C言精妙之的知
    C: A Referenct Manual》(Harbison and Steele, Prentice Hall 1984)是特意为实现写的一本参考料。其他人也会发现它是特有用的——他能从中参考细节
1.是基于图书C Traps and Pitfalls》(Addison-Wesley, 1989, ISBN 0-201-17928-8)的一个充,有趣的者可以它。 
0
0

查看评论
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
    个人资料
    • 访问:26187次
    • 积分:358
    • 等级:
    • 排名:千里之外
    • 原创:7篇
    • 转载:19篇
    • 译文:0篇
    • 评论:5条