概述
C 语言常见问题集
原著:SteveSummit翻译:朱群英,孙云
修订版0.9.4,2005年6月23日
版权所有c
. 2005
目录
目录 i
前言 xvii
1 声明和初始化 1
1.1 我如何决定使用那种整数类型?................... 1
1.264位机上的64位类型是什么样的?................ 1
1.3 怎样定义和声明全局变量和函数最好?............... 2
1.4 extern 在函数声明中是什么意思?................. 2
1.5关键字auto到底有什么用途?................... 2
1.6我似乎不能成功定义一个链表。我试过typedefstruct{ char
*item; NODEPTR next; } *NODEPTR; 但是编译器报了错误信息。难道在C语言中一个结构不能包含指向自己的指针吗?.... 3
1.7怎样建立和理解非常复杂的声明?例如定义一个包含N个指向返回指向字符的指针的函数的指针的数组?.............. 3
1.8函数只定义了一次,调用了一次,但编译器提示非法重定义了。.. 4
1.9main()的正确定义是什么?voidmain()正确吗?......... 4
1.10 对于没有初始化的变量的初始值可以作怎样的假定?如果一个全局变量初始值为“零”,它可否作为空指针或浮点零?....... 4
1.11代码intf(){ chara[]="Hello,world!";} 不能编译。....... 5
1.12这样的初始化有什么问题?char*p=malloc(10);编译器提示“非法初始式”云云。........................... 5
1.13以下的初始化有什么区别?chara[]="stringliteral";char*p="stringliteral";当我向p[i]赋值的时候,我的程序崩溃了。.... 5
1.14我总算弄清除函数指针的声明方法了,但怎样才能初始化呢?.. 5
2 结构、联合和枚举 7
2.1声明structx1{ ... };和typedefstruct{ ... } x2; 有什么不同?.7
2.2为什么structx{ ... }; x thestruct; 不对?............. 7
2.3 一个结构可以包含指向自己的指针吗?............... 7
2.4 在C 语言中实现抽象数据类型什么方法最好? ........... 7
2.5 在C 中是否有模拟继承等面向对象程序设计特性的好方法?... 7
i
2.6 我遇到这样声明结构的代码: struct name { int namelen; char namestr[1];};然后又使用一些内存分配技巧使namestr数组用起来
好像有多个元素。这样合法和可移植吗?............ 8
2.7 是否有自动比较结构的方法?.................... 8
2.8 如何向接受结构参数的函数传入常数值?.............. 8
2.9怎样从/向数据文件读/写结构?................... 9
2.10我的编译器在结构中留下了空洞,这导致空间浪费而且无法与外部数据文件进行”二进制”读写。能否关掉填充,或者控制结构域的对齐方
式?.............................. 9
2.11为什么sizeof返回的值大于结构的期望值,是不是尾部有填充?.. 9
2.12 如何确定域在结构中的字节偏移?................. 9
2.13 怎样在运行时用名字访问结构中的域?............... 10
2.14程序运行正确,但退出时却“coredump”了,怎么回事?..... 10
2.15 可以初始化一个联合吗?....................... 10
2.16枚举和一组预处理的#de.ne有什么不同?............ 10
2.17 有什么容易的显示枚举值符号的方法?............... 11
3 表达式 13
3.1为什么这样的代码:a[i]=i++;不能工作?............ 13
3.2使用我的编译器,下面的代码inti=7;printf("%d/n",i++*i++);返回49?不管按什么顺序计算,难道不该打印出56吗?...... 13
3.3对于代码inti=3;i=i++;不同编译器给出不同的结果,有的为3,有的为4,哪个是正确的?..................... 14
3.4这是个巧妙的表达式:a.=b.=a.=b它不需要临时变量就可以交换a和b的值。......................... 14
3.5 我可否用括号来强制执行我所需要的计算顺序?.......... 14
3.6可是&&和||运算符呢?我看到过类似while((c=getchar())!=EOF && c != ’/n’)的代码.................... 14
3.7我怎样才能理解复杂表达式?“序列点”是什么?......... 15
3.8那么,对于a[i]=i++;我们不知道a[]的哪一个分量会被改写,但i的确会增加1,对吗?......................... 15
3.9++i和i++有什么区别?...................... 15
3.10如果我不使用表达式的值,我应该用++i或i++来自增一个变量吗?.................................. 15
3.11为什么如下的代码inta=100,b=100;longintc=a*b;不能工作?................................. 15
3.12 我需要根据条件把一个复杂的表达式赋值给两个变量中的一个。可以用下边这样的代码吗?((condition)?a:b)=compli-
catedexpression; ........................... 16
4 指针 17
4.1我想声明一个指针并为它分配一些空间,但却不行。这些代码有什么问题?char*p;*p=malloc(10);................ 17
4.2*p++自增p还是p所指向的变量?................ 17
4.3我有一个char*型指针正巧指向一些int型变量,我想跳过它们。为什么如下的代码((int*)p)++;不行?.............. 17
4.4我有个函数,它应该接受并初始化一个指针voidf(int*ip){ static int dummy = 5; ip = &dummy;} 但是当我如下调用时: int *ip; f
(ip); 调用者的指针却没有任何变化。............... 18
4.5我能否用void**指针作为参数,使函数按引用接受一般指针?.. 18
4.6我有一个函数externintf(int*);它接受指向int型的指针。我怎样用引用方式传入一个常数?下面这样的调用f(&5);似乎不行。. 18
4.7C有“按引用传递”吗?....................... 18
4.8 我看到了用指针调用函数的不同语法形式。到底怎么回事?... 19
4.9我怎样把一个int变量转换为char*型?我试了类型转换,但是不行。.................................. 19
5空(null)指针21
5.1 臭名昭著的空指针到底是什么?................... 21
5.2 怎样在程序里获得一个空指针?................... 21
5.3用缩写的指针比较“if(p)”检查空指针是否可靠?如果空指针的内部表达不是0会怎么样?....................... 22
5.4NULL是什么,它是怎么定义的?.................. 23
5.5在使用非全零作为空指针内部表达的机器上,NULL是如何定义的?.................................. 23
5.6如果NULL定义成#de.neNULL((char*)0)难道不就可以向函数传入不加转换的NULL了吗?.................. 23
5.7如果NULL和0作为空指针常数是等价的,那我到底该用哪一个呢?.................................. 24
5.8但是如果NULL的值改变了,比如在使用非零内部空指针的机器上,难道用NULL(而不是0)不是更好吗?............. 24
5.9用预定义宏#de.neNullptr(type)(type*)0帮助创建正确类型的空指针。................................ 24
5.10这有点奇怪。NULL可以确保是0,但空(null)指针却不一定?.. 24
5.11 为什么有那么多关于空指针的疑惑?为什么这些问题如此经常地出现?................................. 25
5.12 我很困惑。我就是不能理解这些空指针一类的东西。....... 25
5.13考虑到有关空指针的所有这些困惑,难道把要求它们内部表达都必须为0不是更简单吗?....................... 26
5.14说真的,真有机器用非零空指针吗,或者不同类型用不同的表达?26
5.15运行时的“空指针赋值”错误是什么意思?............. 26
6 数组和指针 27
6.1我在一个源文件中定义了chara[6],在另一个中声明了externchar*a。为什么不行?....................... 27
6.2可是我听说chara[]和char*a是一样的。............. 27
6.3那么,在C语言中“指针和数组等价”到底是什么意思?..... 28
6.4 那么为什么作为函数形参的数组和指针申明可以互换呢?..... 28
6.5如果你不能给它赋值,那么数组如何能成为左值呢?........ 29
6.6现实地讲,数组和指针地区别是什么?............... 29
6.7有人跟我讲,数组不过是常指针。.................. 29
6.8我遇到一些“搞笑”的代码,包含5["abcdef"]这样的“表达式”。这为什么是合法的C表达式呢?.................. 29
6.9既然数组引用会蜕化为指针,如果arr是数组,那么arr和&arr又有什么区别呢?............................ 30
6.10 我如何声明一个数组指针?..................... 30
6.11 我如何在运行期设定数组的大小?我怎样才能避免固定大小的数组?.................................. 30
6.12 我如何声明大小和传入的数组一样的局部数组?.......... 30
6.13 我该如何动态分配多维数组?.................... 31
6.14有个灵巧的窍门:如果我这样写intrealarray[10];int*array=&realarray[-1];我就可以把“array”当作下标从1开始的数组。.. 32
6.15当我向一个接受指针的指针的函数传入二维数组的时候,编译器报错了。................................ 32
6.16 我怎样编写接受编译时宽度未知的二维数组的函数?....... 32
6.17 我怎样在函数参数传递时混用静态和动态多维数组?....... 33
6.18当数组是函数的参数时,为什么sizeof不能正确报告数组的大小?34
7 内存分配 35
7.1为什么这段代码不行?char*answer;printf("Typesomething:/n");gets(answer);printf("Youtyped/"%s/"/n",answer);....... 35
7.2我的strcat()不行.我试了char*s1="Hello,";char*s2="world!";char*s3=strcat(s1,s2);但是我得到了奇怪的结果。....... 35
7.3但是strcat的手册页说它接受两个char*型参数。我怎么知道(空间)分配的事情呢?........................ 36
7.4我刚才试了这样的代码char*p;strcpy(p,"abc");而它运行正常?怎么回事?为什么它没有崩溃?................ 36
7.5 一个指针变量分配多少内存?.................... 36
7.6我有个函数,本该返回一个字符串,但当它返回调用者的时候,返回串却是垃圾信息。......................... 36
7.7 那么返回字符串或其它集合的争取方法是什么呢?........ 37
7.8为什么在调用malloc()时,我得到“警告:整数赋向指针需要类型转换”?................................ 37
7.9为什么有些代码小心地把malloc返回的值转换为分配的指针类型。37
7.10在调用malloc()的时候,错误“不能把void*转换为int*”是什么意思?................................ 37
7.11我见到了这样的代码char*p=malloc(strlen(s)+1);strcpy(p,s);难道不应该是malloc((strlen(s)+1)*sizeof(char))?..... 37
7.12 我如何动态分配数组?........................ 38
7.13我听说有的操作系统程序使用的时候才真正分配malloc申请的内存。这合法吗?............................ 38
7.14我用一行这样的代码分配一个巨大的数组,用于数字运算:double
*array=malloc(300*300*sizeof(double));malloc()并没有返回null,但是程序运行得有些奇怪,好像改写了某些内存,或者malloc()并没有分
配我申请的那么多内存,云云。......... 38
7.15我的PC有8兆内存。为什么我只能分配640K左右的内存?.. 38
7.16我的程序总是崩溃,显然在malloc内部的某个地方。但是我看不出哪里有问题。是malloc()有bug吗?............... 38
7.17动态分配的内存一旦释放之后你就不能再使用,是吧?...... 38
7.18为什么在调用free()之后指针没有变空?使用(赋值,比较)释放之后的指针有多么不安全?..................... 39
7.19当我malloc()为一个函数的局部指针分配内存时,我还需要用free() 明确的释放吗?........................ 39
7.20我在分配一些结构,它们包含指向其它动态分配的对象的指针。我在释放结构的时候,还需要释放每一个下级指针吗?...... 39
7.21 我必须在程序退出之前释放分配的所有内存吗?.......... 40
7.22我有个程序分配了大量的内存,然后又释放了。但是从操作系统看, 内存的占用率却并没有回去。.................. 40
7.23 free() 怎么知道有多少字节需要释放?............... 40
7.24那么我能否查询malloc包,可分配的最大块是多大?....... 40
7.25向realloc()的第一个参数传入空指针合法吗?你为什么要这样做?40
7.26calloc()和malloc()有什么区别?利用calloc的零填充功能安全吗?free()可以释放calloc()分配的内存吗,还是需要一个cfree()?40
7.27alloca()是什么?为什么不提倡使用它?.............. 41
8 字符和字符串 43
8.1为什么strcat(string,’!’);不行?.................. 43
8.2我在检查一个字符串是否跟某个值匹配。为什么这样不行?char*string;...if(string=="value"){ /*stringmatches”value”*/... }
.................................. 43
8.3如果我可以写chara[]="Hello,world!";为什么我不能写chara[14];a="Hello,world!";...................... 43
8.4我怎么得到对应字符的数字(字符集)值,或者相反?....... 44
8.5我认为我的编译器有问题:我注意到sizeof(’a’)是2而不是1(即,不是sizeof(char))。.......................... 44
9 布尔表达式和变量 45
9.1 C 语言中布尔值的候选类型是什么?为什么它不是一个标准类型?我应该用#de.ne或enum定义true和false值吗?..... 45
9.2因为在C语言中所有的非零值都被看作“真”,是不是把TRUE定义为1很危险?如果某个内置的函数或关系操作符“返回”不是1的其它值怎
么办?.......................... 45
9.3当p是指针时,if(p)是合法的表达式吗?............. 46
10 C 预处理器 47
10.1这些机巧的预处理宏:#de.nebegin{ #de.ne end } 你觉得怎么样?.................................. 47
10.2 怎么写一个一般用途的宏交换两个值?............... 47
10.3 书写多语句宏的最好方法是什么?................. 47
10.4我第一次把一个程序分成多个源文件,我不知道该把什么放到.c文件,把什么放到.h文件。(“.h”到底是什么意思?)....... 48
10.5 一个头文件可以包含另一头文件吗?................ 48
10.6#include<>和#include""有什么区别?............ 48
10.7 完整的头文件搜索规则是怎样的?................. 49
10.8我在文件的第一个声明就遇到奇怪的语法错误,但是看上去没什么问题。................................ 49
10.9我包含了我使用的库函数的正确头文件,可是连接器还是说它没有定义。................................ 49
10.10我在编译一个程序,看起来我好像缺少需要的一个或多个头文件。谁能发给我一份?........................ 49
10.11我怎样构造比较字符串的#if预处理表达式?........... 49
10.12sizeof操作符可以用于#if预编译指令中吗?............ 50
10.13我可以在#include行里使用#ifdef来定义两个不同的东西吗?. 50
10.14对typdef的类型定义有没有类似#ifdef的东西?......... 50
10.15我如何用#if表达式来判断机器是高字节在前还是低字节在前?. 50
10.16我得到了一些代码,里边有太多的#ifdef。我不想使用预处理器把所有的#include和#ifdef都扩展开,有什么办法只保留一种条件的代码
呢?............................. 50
10.17 如何列出所有的预定义标识符?................... 50
10.18我有些旧代码,试图用这样的宏来构造标识符#de.nePaste(a,b)a/**/b 但是现在不行了。...................... 51
10.19为什么宏#de.neTRACE(n)printf("TRACE:%d/n",n)报出警告“用字符串常量代替宏”?它似乎应该把TRACE(count);扩展为printf
("TRACE:%d/count",count);.............. 51
10.20使用#操作符时,我在字符串常量内使用宏参数有问题。..... 51
10.21我想用预处理做某件事情,但却不知道如何下手。......... 51
10.22 怎样写参数个数可变的宏?..................... 51
11 ANSI/ISO 标准C 53
11.1什么是“ANSIC标准”?....................... 53
11.2 我如何得到一份标准的副本?.................... 53
11.3 我在哪里可以找到标准的更新?................... 54
11.4很多ANSI编译器在遇到以下代码时都会警告类型不匹配。extern int func(.oat); int func(x) .oat x; { .............54
11.5 能否混用旧式的和新型的函数语法?................ 55
11.6为什么声明externintf(structx*p);报出了一个奇怪的警告信息“结构x在参数列表中声明”?.................. 55
11.7 我不明白为什么我不能象这样在初始化和数组维度中使用常量: constintn=5;inta[n]; ....................... 55
11.8既然不能修改字符串常量,为什么不把它们定义为字符常量的数组?.................................. 55
11.9“constchar*p”和“char*constp”有何区别?.......... 56
11.10为什么我不能向接受constchar**的函数传入char**?..... 56
11.11怎样正确声明main()?........................ 56
11.12我能否把main()定义为void,以避免扰人的“main无返回值”警告?.................................. 56
11.13可main()的第三个参数envp是怎么回事?............ 57
11.14我觉得把main()声明为void不会失败,因为我调用了exit()而不是return,况且我的操作系统也忽略了程序的退出/返回状态。.. 57
11.15那么到底会出什么问题?真的有什么系统不支持voidmain()吗?57
11.16我一直用的那本书《熟练傻瓜C语言》总是使用voidmain()。.. 57
11.17从main()中,exit(status)和返回同样的status真的等价吗?... 57
11.18我试图用ANSI“字符串化”预处理操作符#向信息中插入符号常量的值,但它字符串化的总是宏的名字而不是它的值。..... 58
11.19警告信息“warning:macroreplacementwithinastringliteral”是什么意思?.............................. 58
11.20在我用#ifdef去掉的代码里出现了奇怪的语法错误。....... 58
11.21#pragma是什么,有什么用?.................... 59
11.22 “#pragma once” 是什么意思?我在一些头文件中看到了它。.. 59
11.23 a[3]="abc";合法吗?它是什么意思?...............59
11.24 为什么我不能对void*指针进行运算?...............59
11.25 memcpy()和memmove()有什么区别?..............59
11.26 malloc(0)有什么用?返回一个控指针还是指向0字节的指针?.59
11.27 为什么ANSI标准规定了外部标示符的长度和大小写限制?...60
11.28 我的编译对最简单的测试程序报出了一大堆的语法错误。.....60
11.29
为什么有些ASNI/ISO标准库函数未定义?我明明使用的就是ANSI 编译器。............................ 60
11.30
谁有把旧的C程序转化为ANSIC或相反的工具,或者自动生成原型的工具?............................. 60
11.31
为什么声称兼容ANSI的FrobozzMagicC编译器不能编译这些代码?我知道这些代码是ANSI的,因为gcc可以编译。..... 60
11.32
人们好像有些在意实现定义(implementation-de.n-ed)、未明确(unspeci.ed)和无定义(unde.ned)行为的区别。它们的区别到底在哪
里?................................ 61
11.33 一个程序的“合法”,“有效”或“符合”到底是什么意思?.....61
11.34
我很吃惊,ANSI标准竟然有那么多没有定义的东西。标准的唯一任务不就是让这些东西标准化吗?................. 61
11.35
有人说i=i++的行为是未定义的,但是我刚在一个兼容ANSI的编译器上测试,得到了我希望的结果。............... 62
12 标准输入输出库 63
12.1 这样的代码有什么问题?charc;while((c=getchar())!=EOF)...63
12.2
我有个读取直到EOF的简单程序,但是我如何才能在键盘上输入那个“EOF”呢?........................... 63
12.3
为什么这些代码while(!feof(infp)){ fgets(buf, MAXLINE, infp); fputs(buf, outfp); } 把最后一行复制了两遍?........... 63
12.4
我的程序的屏幕提示和中间输出有时显示在屏幕上,尤其是当我用管道向另一个程序输出的时候。................. 63
12.5 我怎样不等待回车键一次输入一个字符?..............64
12.6 我如何在printf的格式串中输出一个’%’?我试过/%,但是不行。64
12.7
有人告诉我在printf中使用%lf不正确。那么,如果scanf()需要%lf,怎么可以用在printf()中用%f输出双精度数呢?....... 64
12.8
对于sizet那样的类型定义,当我不知道它到底是long还是其它类型的时候,我应该使用什么样的printf格式呢?......... 64
12.9
我如何用printf实现可变的域宽度?就是说,我想在运行时确定宽度而不是使用%8d?........................ 64
12.10 如何输出在千位上用逗号隔开的数字?金额数字呢?.......65
12.11 为什么scanf("%d",i)调用不行?..................65
12.12 为什么chars[30];scanf("%s",s);不用&也可以?........65
12.13为什么这些代码doubled;scanf("%f",&d);不行?........ 65
12.14怎样在scanf()格式串中指定可变的宽度?............. 65
12.15当我用“%d/n”调用scanf从键盘读取数字的时候,好像要多输入一行函数才返回。.......................... 65
12.16我用scanf%d读取一个数字,然后再用gets()读取字符串,但是编译器好像跳过了gets()调用!................... 66
12.17 我发现如果坚持检查返回值以确保用户输入的是我期待的数值, 则scanf()的使用会安全很多,但有的时候好像会陷入无限循环。. 66
12.18为什么大家都说不要使用scanf()?那我该用什么来代替呢?... 66
12.19我怎样才知道对于任意的sprintf调用需要多大的目标缓冲区?怎样才能避免sprintf()目标缓冲区溢出?............... 66
12.20为什么大家都说不要使用gets()?.................. 67
12.21为什么调用printf()之后errno内有ENOTTY?.......... 67
12.22fgetops/fsetops和ftell/fseek之间有什么区别?fgetops()和fse-tops() 到底有什么用处?....................... 68
12.23如何清除多余的输入,以防止在下一个提示符下读入?.ush(stdin)可以吗?................................ 68
12.24既然.ush()不能,那么怎样才能清除输入呢?........... 68
12.25对某些路径文件名调用fopen()总是失败。............. 68
12.26我想用“r+”打开一个文件,读出一个字符串,修改之后再写入,从而就地更新一个文件。可是这样不行。............... 69
12.27怎样在程序里把stdin或stdout重定向到文件?.......... 69
12.28一旦使用freopen()之后,怎样才能恢复原来的stdout(或stdin)?69
12.29怎样同时向两个地方输出,如同时输出到屏幕和文件?...... 69
12.30怎样正确的读取二进制文件?我有时看到0x0a和0x0d混淆了,而且如果数据中包含0x1a的话,我好像会提前遇到EOF。.... 70
13 库函数 71
13.1怎样把数字转为字符串(与atoi相反)?有itoa()函数吗?.... 71
13.2为什么strncpy()不能总在目标串放上终止符’/0’?....... 71
13.3为什么有些版本的toupper()对大写字符会有奇怪的反应?为什么有的代码在调用toupper()前先调用tolower()?......... 71
13.4 怎样把字符串分隔成用空白作间隔符的段?怎样实现类似传递给main()的argc和argv?....................... 72
13.5 我需要一些处理正则表达式或通配符匹配的代码。........ 72
13.6我想用strcmp()作为比较函数,调用qsort()对一个字符串数组排序, 但是不行。............................ 72
13.7我想用qsort()对一个结构数组排序。我的比较函数接受结构指针,但是编译器认为这个函数对于qsort()是错误类型。我要怎样转换这个
函数指针才能避免这样的警告?.............. 73
13.8 怎样对一个链表排序?........................ 73
13.9 怎样对多于内存的数据排序?.................... 73
13.10怎样在C程序中取得当前日期或时间?............... 73
13.11我知道库函数localtime()可以把timet转换成结构structtm,而ctime()可以把timet转换成为可打印的字符串。怎样才能进行反向操作,
把structtm或一个字符串转换成timet?....... 74
13.12怎样在日期上加N天?怎样取得两个日期的时间间隔?...... 74
13.13 我需要一个随机数生成器。..................... 75
13.14 怎样获得在一定范围内的随机数?................. 75
13.15每次执行程序,rand()都返回相同顺序的数字。.......... 75
13.16我需要随机的真/假值,所以我用直接用rand()%2,可是我得到交替的0,1,0,1,0......................... 76
13.17 怎样产生标准分布或高斯分布的随机数?.............. 76
13.18我不断得到库函数未定义错误,但是我已经#inlude了所有用到的头文件了。............................. 77
13.19虽然我在连接时明确地指定了正确的函数库,我还是得到库函数未定义错误。............................. 77
13.20连接器说end未定义代表什么意思?................ 77
13.21我的编译器提示printf未定义!这怎么可能?........... 77
14 浮点运算 79
14.1一个.oat变量赋值为3.1时,为什么printf输出的值为3.0999999?79
14.2执行一些开方根运算,可是得到一些疯狂的数字。......... 79
14.3做一些简单的三角函数运算,也引用了#include<math.h>,可是一直得到编译错误“unde.ned:sin”(函数sin未定义)。...... 79
14.4浮点计算程序表现奇怪,在不同的机器上给出不同的结果。.... 79
14.5有什么好的方法来验对浮点数在“足够接近”情况下的等值?... 80
14.6 怎样取整数?............................. 80
14.7为什么C不提供乘幂的运算符?.................. 80
14.8为什么我机器上的<math.h>没有预定义常数MPI?...... 80
14.9怎样测试IEEENaN以及其它特殊值?............... 81
14.10 在C 中如何很好的实现复数?.................... 81
14.11 我要寻找一些实现以下功能的程序源代码:快速傅立叶变换(FFT)、矩阵算术(乘法、倒置等函数)、复数算术。....... 81
14.12TurboC的程序崩溃,显示错误为“.oatingpointformatsnotlinked”(浮点格式未连接)。..................... 81
15 可变参数 83
15.1为什么调用printf()前,必须要用#include<stdio.h>?..... 83
15.2为什么%f可以在printf()参数中,同时表示.oat和double?他们难道不是不同类型吗?........................ 83
15.3为什么当n为longint,printf("%d",n);编译时没有匹配警告?我以为ANSI函数原型可以防止这样的类型不匹配。....... 83
15.4 怎样写一个有可变参数的函数?................... 83
15.5怎样写类似printf()的函数,再把参数转传给printf()去完成大部分工作?................................ 85
15.6怎样写类似scanf()的函数,再把参数转传给scanf()去完成大部分工作?................................ 85
15.7 怎样知道实际上有多少个参数传入函数?.............. 85
15.8 为什么编译器不让我定义一个没有固定参数项的可变参数函数?. 86
15.9我有个接受.oat的可变参函数,为什么vaarg(argp,.oat)不工作?86
15.10 va arg() 不能得到类型为函数指针的参数。............. 86
15.11怎样实现一个可变参数函数,它把参数再传给另一个可变参数函数?.................................. 86
15.12 怎样调用一个参数在执行是才建立的函数?............ 87
16 奇怪的问题 89
16.1遇到不可理解的不合理语法错误,似乎大段的程序没有编译。.. 89
16.2 为什么过程调用不工作?编译器似乎直接跳过去了。....... 89
16.3程序在执行用之前就崩溃了,用调试器单步跟进,在main()之前就死了。................................ 89
16.4程序执行正确,但退出时崩溃在main()最后一个语句之后。为什么会这样?.............................. 89
16.5程序在一台机器上执行完美,但在另一台上却得到怪异的结果。更奇怪的是,增加或去除调试的打印语句,就改变了症状..... 90
16.6为什么代码:char*p="hello,worl!";p[0]=’H’;会崩溃?... 90
16.7“Segmentationviolation”,“Buserror”和“Generalprotectionfault”意味着什么?............................. 91
17 风格 93
17.1什么是C最好的代码布局风格?.................. 93
17.2用if(!strcmp(s1,s2))比较两个字符串等值,是否是个好风格?... 93
17.3为什么有的人用if(0==x)而不是if(x==0)?......... 93
17.4原型说明externintfunc((int,int));中,那些多出来的括号和下划线代表了什么?.......................... 94
17.5为什么有些代码在每次调用printf()前,加了类型转换(void)?.. 94
17.6什么是“匈牙利标志法”(HungarianNotation)?是否值得用?.. 94
17.7哪里可以找到“印第安山风格指南”(IndianHillStyleGuide)及其它编码标准?............................ 94
17.8有些人说goto是邪恶的,我应该永不用它。那是否太极端了?.. 95
18 工具和资源 97
18.1 常用工具列表。............................ 97
18.2怎样抓捕棘手的malloc问题?.................... 98
18.3 有什么免费或便宜的编译器可以使用?............... 98
18.4刚刚输入完一个程序,但它表现的很奇怪。你可以发现有什么错误的地方吗?............................. 98
18.5哪里可以找到兼容ANSI的lint?.................. 99
18.6难道ANSI函数原型说明没有使lint过时吗?........... 99
18.7网上有哪些C的教程或其它资源?................. 99
18.8哪里可以找到好的源代码实例,以供研究和学习?......... 100
18.9有什么好的学习C的书?有哪些高级的书和参考?........ 100
18.10哪里可以找到标准C函数库的源代码?............... 101
18.11是否有一个在线的C参考指南?.................. 101
18.12哪里可以得到ANSI/ISOC标准?................. 101
18.13 我需要分析和评估表达式的代码。................. 101
18.14哪里可以找到C的BNF或YACC语法?............. 101
18.15谁有C编译器的测试套件?..................... 102
18.16 哪里有一些有用的源代码片段和例子的收集?........... 102
18.17 我需要执行多精度算术的代码。................... 102
18.18 在哪里和怎样取得这些可自由发布的程序?............ 102
19 系统依赖 105
19.1怎样从键盘直接读入字符而不用等RETURN键?怎样防止字符输入时的回显?............................ 105
19.2怎样知道有未读的字符,如果有,有多少?如果没有字符,怎样使读入不阻断?............................. 106
19.3怎样显示一个百分比或“转动的短棒”的进展表示器?...... 106
19.4 怎样清屏?怎样输出彩色文本?怎样移动光标到指定位置?... 106
19.5怎样读入方向键,功能键?...................... 107
19.6 怎样读入鼠标输入?......................... 107
19.7怎样做串口(“comm”)的输入输出?................ 107
19.8 怎样直接输出到打印机?....................... 107
19.9 怎样发送控制终端或其它设备的逃逸指令序列?.......... 108
19.10 怎样直接访问输入输出板?..................... 108
19.11 怎样做图形?............................. 108
19.12怎样显示GIF和JPEG图象?................... 108
19.13 怎样检验一个文件是否存在?.................... 108
19.14怎样在读入文件前,知道文件大小?................. 109
19.15 怎样得到文件的修改日期和时间?................. 109
19.16 怎样缩短一个文件而不用清除或重写?............... 109
19.17怎样在文件中插入或删除一行(或记录)?.............. 109
19.18 怎样从一个打开的流或文件描述符得到文件名?.......... 110
19.19 怎样删除一个文件?......................... 110
19.20 怎样复制一个文件?......................... 110
19.21为什么用了详尽的路径还不能打开文件?fopen("c:/ newdir /.le.dat","r")返回错误。...................... 110
19.22fopen()不让我打开文件:"$HOME/.pro.le"和"./.myrc.le"。. 111
19.23怎样制止MS-DOS下令人担忧的“Abort,Retry,Ignore?”信息?111
19.24遇到“Toomanyopen.les(打开文件太多)”的错误,怎样增加同时打开文件的允许数目?....................... 111
19.25怎样在C中读入目录?........................ 111
19.26 怎样找出系统还有多少内存可用?................. 111
19.27怎样分配大于64K的数组或结构?................. 111
19.28错误信息“DGROUPdataallocationexceeds64K(DGROUP数据分配内存超过64K)”说明什么?我应该怎么做?我以为使用了大内存模型,那
我就可以使用多于64K的数据!.......... 112
19.29怎样访问位于某的特定地址的内存(内存映射的设备或图显内存)?112
19.30怎样在一个C程序中调用另一个程序(独立可执行的程序,或系统命令)?................................ 112
19.31怎样调用另一个程序或命令,同时收集它的输出?......... 113
19.32 怎样才能发现程序自己的执行文件的全路径?........... 113
19.33 怎样找出和执行文件在同一目录的配置文件?........... 113
19.34 一个进程如何改变它的调用者的环境变量?............ 113
19.35 怎样读入一个对象文件并跳跃到其中的地址?........... 114
19.36 怎样实现精度小于秒的延时或记录用户回应的时间?....... 114
19.37怎样抓获或忽略像control-C这样的键盘中断?.......... 114
19.38 怎样很好地处理浮点异常?..................... 115
19.39怎样使用socket?网络化?写客户/服务器程序?......... 115
19.40怎样调用BIOS函数?写ISR?创建TSR?............ 115
19.41编译程序,编译器出示“unionREGS”未定义错误信息,连接器出示“int86()”的未定义错误信息。.................. 115
19.42什么是“near”和“far”指针?.................... 116
19.43我不能使用这些非标准、依赖系统的函数,程序需要兼容ANSI!. 116
20 杂项 117
20.1 怎样从一个函数返回多个值?.................... 117
20.2 怎样访问命令行参数?........................ 117
20.3怎样写数据文件,使之可以在不同字大小、字节顺序或浮点格式的机器上读入?............................ 117
20.4怎样调用一个由char*指针指向函数名的函数?......... 117
20.5 怎样实现比特数组或集合?..................... 118
20.6 怎样判断机器的字节顺序是高字节在前还是低字节在前?..... 118
20.7 怎样掉换字节?............................ 118
20.8 怎样转换整数到二进制或十六进制?................ 119
20.9我可以使用二进制常数吗?有printf()的二进制的格式符吗?. . 119
20.10什么是计算整数中比特为1的个数的最有效的方法?....... 119
20.11 什么是提高程序效率的最好方法?................. 119
20.12指针真得比数组快吗?函数调用会拖慢程序多少?++i比i=i+1 快吗?............................... 120
20.13人们说编译器优化的很好,我们不在需要为速度而写汇编了,但我的编译器连用移位代替i/=2都做不到。.............. 120
20.14 怎样不用临时变量而交换两个值?................. 120
20.15 是否有根据字符串做切换的方法?................. 121
20.16是否有使用非常量case标志的方法(例如范围或任意的表达式)?121
20.17 return 语句外层的括号是否真的可选择?.............. 121
20.18为什么C注释不能嵌套?怎样注释掉含有注释的代码?引用字符串内的注释是否合法?........................ 121
20.19C是个伟大的语言还是别的?哪个其它语言可以写象a+++++b这样的代码?............................. 122
20.20为什么C没有嵌套函数?...................... 122
20.21 assert() 是什么?怎样用它?..................... 122
20.22怎样从C中调用FORTRAN(C++,BASIC,Pascal,Ada,LISP)的函数?反之亦然?......................... 122
20.23有什么程序可以做从Pascal或Fortran(或LISP,Ada,awk,“老”C) 到C 的转换?........................... 123
20.24C++是C的超集吗?可以用C++编译器来编译C代码吗?. . 123
20.25需要用到“近似”的strcmp,比较两个字符串的近似度,并不需要完全一样。.............................. 123
20.26 什么是散列法?............................ 124
20.27由一个日期,怎样知道是星期几?.................. 124
20.28(year%4==0)是否足够判断润年?2000年是闰年吗?...... 124
20.29一个难题:怎样写一个输出自己源代码的程序?.......... 124
20.30什么是“达夫设备”(Du.’sDevice)?................ 125
20.31下届国际C混乱代码竞赛(IOCCC)什么时候进行?哪里可以找到当前和以前的获胜代码?..................... 125
20.32 [K&R1] 提到的关健字entry是什么?................126
20.33 C 的名字从何而来?......................... 126
20.34“char”如何发音?.......................... 126
20.35“lvalue”和“rvalue”代表什么意思?................ 126
20.36哪里可以取得本FAQ(英文版)的额外副本?........... 126
21 感谢129
文献131
前言
本文从英文C–FAQ(2004年7月3日修订版)翻译而来。本文的中文版权为朱群英和孙云所有。本文的内容可以自由用于个人目的,但是不可以未
经许可出版发行。英文版权为SteveSummit所有,详情见下面的英文版权说明。
TheEnglishversionofthisFAQlistisCopyright1990-2004bySteveSummit.Contentfromthebook
《CProgrammingFAQs:FrequentlyAskedQuestions》
ismadeavailableherebypermissionoftheauthorandthepublisherasaservicetothecommunity.Itisintendedtocomplementtheuseofthepublish
edtextandisprotectedbyinternationalcopyrightlaws.Theon-
linecontentmaybeaccessedfreelyforpersonalusebutmaynotberepublishedwithoutpermission.
最新的HTML中译版本可以在http://c-faq-chn.sourceforge.net/ 取得。另外在同一地址还提供PDF版本的下载。在
http://sourceforge.net/projects/ c-faq-chn 可以得到本文的LATEX 源文件。
有关英文原文的问题,请咨询SteveSummit(scs@eskimo.com)。有关中文译稿的问题,请联系孙云(sunyun.s@gmail.com,1–12章) 和朱群英
(zhu.qunying@gmail.com, 13–20 章、LATEX文件编辑)。
xvii
第1 章
声明和初始化
1.1 我如何决定使用那种整数类型?
如果需要大数值(大于32,767或小于.32,767),使用long型。否则,如果空间很重要(如有大数组或很多结构),使用short型。除此之外,就使用
int型。如果严格定义的溢出特征很重要而负值无关紧要,或者你希望在操作二进制位和字节时避免符号扩展的问题,请使用对应的无符号类型
。但是,要注意在表达式中混用有符号和无符号值的情况。
尽管字符类型(尤其是无符号字符型)可以当成“小”整型使用,但由于不可预知的符号扩展和代码增大有时这样做可能得不偿失。使用无符号
字符型有所帮助;类似的问题参见问题12.1。
在选择浮点型和双精度浮点型时也有类似的权衡。但如果一个变量的指针必须为特定的类型时,以上规则不再适用。
如果因为某种原因你需要声明一个有严格大小的变量,确保象C99的<int-types.h>那样用某种适当的typedef封装这种选择。通常,这样做唯一
的好原因是试图符合某种外部强加的存储方案,请参见问题20.3。
如果你需要操作超过C的内置类型支持的超大变量,请参见问题18.17。
参考资料: [K&R1,Sec.2.2 p. 34];[K&R2,Sec.2.2 p. 36, Sec. A4.2 pp. 195-6,Sec.B11p.257];[ISO,Sec.5.2.4.2.1,Sec.6.1.2.5];
[H&S,Secs.5.1,5.2pp.110-114]。
1.2 64位机上的64位类型是什么样的?
C99标准定义了longlong类型,其长度可以保证至少64位,这种类型在某些编译器上实现已经颇有时日了。其它的编译器则实现了类似longlong
的扩展。另一方面,也可以实现16位的短整型、32位的整型和64位的长整型,有些编译器正是这样做的。
参见问题18.17。
参考资料:[C9X,Sec.5.2.4.2.1,Sec.6.1.2.5]
1
1.3 怎样定义和声明全局变量和函数最好?
首先,尽管一个全局变量或函数可以(在多个编译单元中)有多处“声明”,但是“定义”却只能允许出现一次。定义是分配空间并赋初值(如果
有)的声明。最好的安排是在某个相关的.c文件中定义,然后在头文件(.h)中进行外部声明,在需要使用的时候,只要包含对应的头文件即可。定
义变量的.c文件也应该包含该头文件,以便编译器检查定义和声明的一致性。
这条规则提供了高度的可移植性:它和ANSIC标准一致,同时也兼容大多数ANSI前的编译器和连接器。Unix编译器和连接器通常使用“通用模式
”允许多重定义,只要保证最多对一处进行初始化就可以了;ANSIC标准称这种行为为“公共扩展”,没有语带双关的意思。
可以使用预处理技巧来使类似
DEFINE(int, i);
的语句在一个头文件中只出现一次,然后根据某个宏的设定在需要的时候转化成定义或声明。但不清楚这样的麻烦是否值得。
如果希望让编译器检查声明的一致性,一定要把全局声明放到头文件中。特别是,永远不要把外部函数的原型放到.c文件中:通常它与定义的一
致性不能得到检查,而矛盾的原型比不用还糟糕。
参见问题10.4 和18.6。
参考资料:[K&R1,Sec.4.5pp.76-7];[K&R2,Sec.4.4pp.80-1];[ISO,Sec.6.1.2.2,Sec.6.7,Sec.6.7.2,Sec.G.5.11];
[Rationale,Sec.3.1.2.2];[H&S, Sec. 4.8 pp.101-104,Sec.9.2.3p.267];[CT&P,Sec.4.2pp.54-56].
1.4 extern 在函数声明中是什么意思?
它可以用作一种格式上的提示表明函数的定义可能在另一个源文件中,但在
extern int f();
和
int f();
之间并没有实质的区别。
参考资料: [ISO,Sec.6.1.2.2, Sec. 6.5.1];[Rationale,Sec.3.1.2.2];[H&S,Secs. 4.3,4.3.1 pp. 75-6].
1.5 关键字auto到底有什么用途?
毫无用途;它已经过时。参见问题20.32。
参考资料:[K&R1,Sec.A8.1p.193];[ISO,Sec.6.1.2.4,Sec.6.5.1;];[H&S,Sec. 4.3 p. 75, Sec. 4.3.1 p. 76].
1.6 我似乎不能成功定义一个链表。我试过typedefstruct{ char
*item; NODEPTR next; } *NODEPTR;但是编译器报了错误信息。难道在C语言中一个结构不能包含指向自己的指针吗?
C 语言中的结构当然可以包含指向自己的指针;[K&R2,第6.5节]的讨论和例子表明了这点。NODEPTR例子的问题是在声明next域的时候typedef
还没有定义。为了解决这个问题,首先赋予这个结构一个标签(“structnode”)。然后,声明“next”域为“structnode*”,或者分开typedef
定义和结构定义,或者两者都采纳。以下是一个修改后的版本:
struct node {
char *item;
struct node *next;
};
typedef struct node *NODEPTR;
至少还有三种同样正确的方法解决这个问题。
在用typedef定义互相引用的两个结构时也会产生类似的问题,可以用同样的方法解决。
参见问题2.1。
参考资料: [K&R1,Sec.6.5 p. 101];[K&R2,Sec.6.5 p. 139];[ISO,Sec.
6.5.2, Sec.6.5.2.3];[H&S,Sec.5.6.1pp.132-3]。
1.7 怎样建立和理解非常复杂的声明?例如定义一个包含N个指向返回指向字符的指针的函数的指针的数组?
这个问题至少有以下3种答案:
1. char *(*(*a[N])())();
2.用typedef逐步完成声明:
typedefchar*pc;/*字符指针*/typedefpcfpc();/*返回字符指针的函数*/typedeffpc*pfpc;/*上面函数的指针*/typedefpfpcfpfpc();/*返回
函数指针的函数*/typedeffpfpc*pfpfpc;/*上面函数的指针*/pfpfpca[N];/*上面指针的数组*/
3.使用cdecl程序,它可以把英文翻译成C或者把C翻译成英文:
cdecl> declare a as array of pointer to function returning pointer to function returning pointer to char char *(*(*a[])())()
通过类型转换,cdecl也可以用于解释复杂的声明,指出参数应该进入哪一对括号(如同在上述的复杂函数定义中)。参见问题18.1。
一本好的C语言书都会解释如何“从内到外”解释和理解这样复杂的C语言声明(“模拟声明使用”)。
上文的例子中的函数指针声明还没有包括参数类型信息。如果参数有复杂类型,声明就会变得真正的混乱了。现代的cdecl版本可以提供帮助。
参考资料:[K&R2,Sec.5.12p.122];[ISO,Sec.6.5.(esp.Sec.6.5.4)];[H&S,Sec. 4.5 pp. 85-92, Sec. 5.10.1 pp. 149-50] 。
1.8 函数只定义了一次,调用了一次,但编译器提示非法重定义了。
在范围内没有声明就调用(可能是第一次调用在函数的定义之前)的函数被认为返回整型(int)(且没有任何参数类型信息),如果函数在后边声明
或定义成其它类型就会导致矛盾。所有函数(非整型函数一定要)必须在调用之前声明。
另一个可能的原因是该函数与某个头文件中声明的另一个函数同名。
参见问题11.4 和15.1
参考资料:[K&R1,Sec.4.2p.70];[K&R2,Sec.4.2p.72];[ISO,Sec.6.3.2.2];[H&S,Sec.4.7p.101].
1.9 main()的正确定义是什么?voidmain()正确吗?
参见问题11.11 到11.16。(这样的定义不正确)。
1.10 对于没有初始化的变量的初始值可以作怎样的假定?如果一个全局变量初始值为“零”,它可否作为空指针或浮点零?
具有“静态”生存期的未初始化变量(即,在函数外声明的变量和有静态存储类型的变量)可以确保初始值为零,就像程序员键入了“=0”一样。
因此,这些变量如果是指针会被初始化为正确的空指针,如果是浮点数会被初始化为0.0(或正确的类型,参见第5 章)。
具有“自动”生存期的变量(即,没有静态存储类型的局部变量)如果没有显示地初始化,则包含的是垃圾内容。对垃圾内容不能作任何有用的假
设。
这些规则也适用于数组和结构(称为“聚合体”);对于初始化来说,数组和结构都被认为是“变量”。
用malloc()和realloc()动态分配的内存也可能包含垃圾数据,因此必须由调用者正确地初始化。用calloc()获得的内存为全零,但这对指针和
浮点值不一定有用(参见问题7.26 和第5 章)。
参考资料:[K&R1,Sec.4.9pp.82-4];[K&R2,Sec.4.9pp.85-86];[ISO,Sec.6.5.7,Sec.7.10.3.1,Sec.7.10.5.3];[H&S,Sec.4.2.8pp.72-
3,Sec.4.6pp.92-3,Sec. 4.6.2 pp. 94-5, Sec. 4.6.3 p. 96, Sec. 16.1 p. 386.] 。
1.11 代码intf(){ chara[]="Hello,world!";} 不能编译。
可能你使用的是ANSI之前的编译器,还不支持“自动聚集”(automaticaggre-gates,即非静态局部数组、结构和联合)的初始化。参见问题
11.28。
1.12 这样的初始化有什么问题?char*p=malloc(10);编译器提示“非法初始式”云云。
这个声明是静态或非局部变量吗?函数调用只能出现在自动变量(即局部非静态变量)的初始式中。
1.13 以下的初始化有什么区别?chara[]="stringliteral";char*p="stringliteral";当我向p[i]赋值的时候,我的程序崩溃了。
字符串常量有两种稍有区别的用法。用作数组初始值(如同在chara[]的声明中),它指明该数组中字符的初始值。其它情况下,它会转化为一个
无名的静态字符数组,可能会存储在只读内存中,这就是造成它不一定能被修改。在表达式环境中,数组通常被立即转化为一个指针(参见第6 章
),因此第二个声明把p初始化成指向无名数组的第一个元素。
为了编译旧代码,有的编译器有一个控制字符串是否可写的开关。参见问题1.11、6.1、6.2 和6.6。参考资料:[K&R2,Sec.5.5p.104];
[ISO,Sec.6.1.4,Sec.6.5.7];[Rationale,
Sec.3.1.4];[H&S,Sec.2.7.4pp.31-2]。
1.14 我总算弄清除函数指针的声明方法了,但怎样才能初始化呢?
用下面这样的代码
extern int func();
int (*fp)() = func;
当一个函数名出现在这样的表达式中时,它就会“蜕变”成一个指针(即,隐式地取出了它的地址),这有点类似数组名的行为。通常函数的显示
声明需要事先知道(也许在一个头文件中)。因为此处并没有隐式的外部函数声明(初始式中函数名并非一个函数调用的一部分)。参见问题1.8
和4.8。
第2 章
结构、联合和枚举
2.1 声明structx1{ ... };和typedefstruct{ ... } x2; 有什么不同?
第一种形式声明了一个“结构标签”;第二种声明了一个“类型定义”。主要的区别是在后文中你需要用“structx1”引用第一种,而用“x2”
引用第二种。也就是说,第二种声明更像一种抽象类新—–用户不必知道它是一个结构,而在声明它的实例时也不需要使用struct关键字。
2.2 为什么structx{ ... }; x thestruct; 不对?
C不是C++。结构标签不能自动生成类型。参见问题2.1。
2.3 一个结构可以包含指向自己的指针吗?
当然可以。参见问题1.6。
2.4 在C 语言中实现抽象数据类型什么方法最好?
让客户使用指向没有公开定义(也许还隐藏在类型定义后边)的结构类型的指针是一个好办法。只要不访问结构成员,声明和使用“匿名”结构
指针(不完全结构类型指针)是合法的。这也是使用抽象数据类型的原因。
2.5 在C 中是否有模拟继承等面向对象程序设计特性的好方法?
把函数指针直接加入到结构中就可以实现简单的“方法”。你可以使用各种不雅而暴力的方法来实现继承,例如通过预处理器或含有“基类”
的结构作为开始的子集,但这些方法都不完美。很明显,也没有运算符的重载和覆盖(例如,“导出类”中的“方法”),那些必须人工去做。
显然的,如果你需要“真”的面向对象的程序设计,你需要使用一个支持这些特性的语言,例如C++。
7
2.6 我遇到这样声明结构的代码: struct name { int namelen; char namestr[1];};然后又使用一些内存分配技巧使namestr数组用起来
好像有多个元素。这样合法和可移植吗?
这种技术十分普遍,尽管DennisRitchie称之为“和C实现的无保证的亲密接触”。官方的解释认定它没有严格遵守C标准,尽管它看来在所有的
实现中都可以工作。仔细检查数组边界的编译器可能会发出警告。
另一种可能是把变长的元素声明为很大,而不是很小;在上例中:
...
char namestr[MAXSIZE];
MAXSIZE比任何可能存储的name值都大。但是,这种技术似乎也不完全符合标准的严格解释。这些“亲密”结构都必须小心使用,因为只有程序
员知道它的大小,而编译器却一无所知。
C99引入了“灵活数组域”概念,允许结构的最后一个域省略数组大小。这为类似问题提供了一个圆满的解决方案。
参考资料:[Rationale,Sec.3.5.4.2];[C9X, Sec. 6.5.2.1] 。
2.7 是否有自动比较结构的方法?
没有。编译器没有简单的好办法实现结构比较(即,支持结构的==操作符),这也符合C的低层特性。简单的按字节比较会由于结构中没有用到的
“空洞”中的随机数据(参见问题2.10) 而失败;而按域比较在处理大结构时需要难以接受的大量重复代码。
如果你需要比较两个结构,你必须自己写函数按域比较。参考资料:[K&R2,Sec.6.2p.129];[Rationale, Sec.3.3.9];[H&S,Sec.5.6.2
p. 133] 。
2.8 如何向接受结构参数的函数传入常数值?
传统的C没有办法生成匿名结构值;你必须使用临时结构变量或一个小的结构生成函数。
C99标准引入了“复合常量”(compoundliterals);复合常量的一种形式就可以允许结构常量。例如,向假想plotpoint()函数传入一个坐标对常
数,可以调用
plotpoint((struct point){1, 2});
与“指定初始值”(designatedinitializers)(C99的另一个功能)结合,也可以用成员名称确定成员值:
plotpoint((struct point){.x=1, .y=2});
参见问题4.6。
参考资料:[C9X,Sec.6.3.2.5,Sec.6.5.8]。
2.9 怎样从/向数据文件读/写结构?
用fwrite()写一个结构相对简单:
fwrite(&somestruct, sizeof somestruct, 1, fp);
对应的fread()调用可以再把它读回来。但是这样写出的文件却不能移植(参见问题2.10 和20.3)。同时注意如果结构包含任何指针,则只有指
针值会被写入文件,当它们再次读回来的时候,很可能已经失效。最后,为了广泛的移植,你必须用“b”标志打开文件;参见问题12.30。
移植性更好的方案是写一对函数,用可移植(可能甚至是人可读)的方式按域读写结构,尽管开始可能工作量稍大。
参考资料:[H&S,Sec.15.13p.381]。
2.10 我的编译器在结构中留下了空洞,这导致空间浪费而且无法与外部数据文件进行”二进制”读写。能否关掉填充,或者控制结构域的对
齐方式?
这些“空洞”充当了“填充”,为了保持结构中后面的域的对齐,这也许是必须的。为了高效的访问,许多处理器喜欢(或要求)多字节对象(例如
,结构中任何大于char的类型)不能处于随意的内存地址,而必须是2或4或对象大小的倍数。
编译器可能提供一种扩展用于这种控制(可能是#pragma;参见问题11.21),但是没有标准的方法。
参见问题20.3。
参考资料:[K&R2,Sec.6.4p.138];[H&S, Sec. 5.6.4 p. 135] 。
2.11 为什么sizeof返回的值大于结构的期望值,是不是尾部有填充?
为了确保分配连续的结构数组时正确对齐,结构可能有这种尾部填充。即使结构不是数组的成员,填充也会保持,以便sizeof能够总是返回一致
的大小。参见问题2.10。
参考资料:[H&S,Sec.5.6.7pp.139-40]。
2.12 如何确定域在结构中的字节偏移?
ANSIC在<stddef.h>中定义了o.setof()宏,用o.setof(structs,f)可以计算出域f在结构s中的偏移量。如果出于某种原因,你需要自己实现这个
功能,可以使用下边这样的代码:
#define offsetof(type, f) ((size_t) / ((char *)&((type *)0)->f -(char *)(type *)0))
这种实现不是100%的可移植;某些编译器可能会合法地拒绝接受。
参考资料: [ISO,Sec.7.1.6];[Rationale,Sec.3.5.4.2];[H&S,Sec.11.1pp.292-3]。
2.13 怎样在运行时用名字访问结构中的域?
保持用o.setof()(参见问题2.12) 计算的域偏移量。如果structp是个结构实体的指针,而域f是个整数,它的偏移量是o.setf,f的值可以间接地
设置:
*(int *)((char *)structp + offsetf) = value;
2.14 程序运行正确,但退出时却“coredump”了,怎么回事?
问题程序:
struct list {
char *item;
struct list *next;
}
/*这里是main程序*/
main(argc, argv)
{ ... }
缺少的一个分号使main()被定义为返回一个结构。由于中间的注释行,这个联系不容易看出来。因为一般上,返回结构的函数在实现时,会加入
一个隐含的返回指针,这个产生的main()函数代码试图接受三个参数,而实际上只有两个传入(这里,由C的启动代码传入)。参见问题10.8 和
16.4。
参考资料:[CT&P,Sec.2.3pp.21-2]。
2.15 可以初始化一个联合吗?
在原来的ANSIC中,只有联合中的第一个命名成员可以被初始化。C99引入了“指定初始值”,可以用来初始化任意成员。
参考资料:[K&R2,Sec.6.8pp.148-9];[ISO,Sec.6.5.7];[C9X,Sec.6.5.8];[H&S,Sec.4.6.7p.100]。
2.16 枚举和一组预处理的#de.ne有什么不同?
只有很小的区别。C标准中允许枚举和其它整形类别自由混用而不会出错。(但是,假如编译器不允许在未经明确类型转换的情况下混用这些类
型,则聪明地使用枚举可以捕捉到某些程序错误。)
枚举的一些优点:自动赋值;调试器在检验枚举变量时,可以显示符号值;它们服从数据块作用域规则。(编译器也可以对在枚举变量被任意地和
其它类型混用时,产生非重要的警告信息,因为这被认为是坏风格。)一个缺点是程序员不能控制这些对非重要的警告;有些程序员则反感于无
法控制枚举变量的大小。
参考资料:[K&R2,Sec.2.3p.39,Sec.A4.2p.196];[ISO, Sec. 6.1.2.5, Sec.
6.5.2, Sec.6.5.2.2,AnnexF];[H&S,Sec.5.5pp.127-9,Sec.5.11.2p.153]。
2.17 有什么容易的显示枚举值符号的方法?
没有。你可以写一个小函数,把一个枚举常量值映射到字符串。(为了调试的目的,一个好的调试器,应该可以自动显示枚举常量值符号。)
第3 章
表达式
3.1 为什么这样的代码:a[i]=i++;不能工作?
子表达式i++有一个副作用—–它会改变i的值—–由于i在同一表达式的其它地方被引用,这会导致无定义的结果,无从判断该引用(左边的a[i]
中)是旧值还是新值。(注意,尽管在K&R中建议这类表达式的行为不确定,但C标准却强烈声明它是无定义的,参见问题11.32。
参考资料:[K&R1,Sec.2.12];[K&R2,Sec.2.12];[ISO,Sec.6.3];[H&S, Sec.
7.12 pp. 227-9] 。
3.2 使用我的编译器,下面的代码inti=7;printf("%d/n",i++*i++);返回49?不管按什么顺序计算,难道不该打印出56吗?
尽管后缀自加和后缀自减操作符++和--在输出其旧值之后才会执行运算,但这里的“之后”常常被误解。没有任何保证确保自增或自减会在输
出变量原值之后和对表达式的其它部分进行计算之前立即进行。也不能保证变量的更新会在表达式“完成”(按照ANSIC的术语,在下一个“序
列点”之前,参见问题3.7) 之前的某个时刻进行。本例中,编译器选择使用变量的旧值相乘以后再对二者进行自增运算。
包含多个不确定的副作用的代码的行为总是被认为未定义。(简单而言,“多个不确定副作用”是指在同一个表达式中使用导致同一对象修改两
次或修改以后又被引用的自增,自减和赋值操作符的任何组合。这是一个粗略的定义;严格的定义参见问题3.7, “未定义”的含义参见问题
11.32。)甚至都不要试图探究这些东西在你的编译器中是如何实现的(这与许多C教科书上的弱智练习正好相反);正如K&R明智地指出,“如果你
不知道它们在不同的机器上如何实现,这样的无知可能恰恰会有助于保护你。”
参考资料:[K&R1,Sec.2.12p.50];[K&R2,Sec.2.12p.54];[ISO,Sec.6.3];[H&S,Sec.7.12pp.227-9];[CT&P,Sec.3.7p.47];[PCS,Sec.9.5pp.120
-1]。
13
3.3 对于代码inti=3;i=i++;不同编译器给出不同的结果,有的为3,有的为4,哪个是正确的?
没有正确答案;这个表达式无定义。参见问题3.1, 3.7 和11.32。同时注意,i++和++i都不同于i+1。如果你要使i自增1,使用i=i+1,i+=1,i++或
++i,而不是任何组合,参见问题3.10。
3.4 这是个巧妙的表达式:a.=b.=a.=b它不需要临时变量就可以交换a和b的值。
这不具有可移植性。它试图在序列点之间两次修改变量a,而这是无定义的。例如,有人报告如下代码:
int a = 123, b = 7654;
a ^= b ^= a ^= b;
在SCO优化C编译器(icc)下会把b置为123,把a置为0。
参见问题3.1、3.7 和20.14。
3.5 我可否用括号来强制执行我所需要的计算顺序?
一般来讲,不行。运算符优先级和括弧只能赋予表达是计算部分的顺序.在如下的代码中
f() + g() * h()
尽管我们知道乘法运算在加法之前,但这并不能说明这三个函数哪个会被首先调用。
如果你需要确保子表达式的计算顺序,你可能需要使用明确的临时变量和独立的语句。
参考资料:[K&R1,Sec.2.12p.49,Sec.A.7p];[K&R2, Sec. 2.12 pp. 52-3, Sec. A.7 p. 200.] 。
3.6 可是&&和||运算符呢?我看到过类似while((c=getchar())!=EOF&&c!=’/n’)的代码..
这些运算符在此处有一个特殊的“短路”例外:如果左边的子表达式决定最终结果(即,真对于||和假对于&&)则右边的子表达式不会计算。因此
,从左至右的计算可以确保,对逗号表达式也是如此。而且,所有这些运算符(包括?:)都会引入一个额外的内部序列点(参见问题3.7)。
参考资料:[K&R1,Sec.2.6p.38,Secs.A7.11-12pp.190-1];[K&R2, Sec.
2.6
p.41,Secs.A7.14-15pp.207-8];[ISO,Sec.6.3.13,Sec.6.3.14,Sec.6.3.15];[H&S,Sec.7.7pp.217-8,Sec.7.8pp.218-20,Sec.7.12.1p.229];
[CT&P, Sec.
3.7
pp. 46-7] 。
3.7 我怎样才能理解复杂表达式?“序列点”是什么?
序列点是一个时间点(在整个表达式全部计算完毕之后或在||、&&、?:或逗号运算符处,或在函数调用之前),此刻尘埃落定,所有的副作用都已
确保结束。ANSI/ISOC标准这样描述:
在上一个和下一个序列点之间,一个对象所保存的值至多只能被表
达式的计算修改一次。而且前一个值只能用于决定将要保存的值。
第二句话比较费解。它说在一个表达式中如果某个对象需要写入,则在同一表达式中对该对象的访问应该只局限于直接用于计算将要写入的值
。这条规则有效地限制了只有能确保在修改之前才访问变量的表达式为合法。例如i=i+1合法,而a[i]=i++则非法(参见问题3.1)。
参见下边的问题3.8。
参考资料:[ISO,Sec.5.1.2.3,Sec.6.3,Sec.6.6,AnnexC];[Rationale, Sec.
2.1.2.3]; [H&S,Sec.7.12.1pp.228-9]。
3.8 那么,对于a[i]=i++;我们不知道a[]的哪一个分量会被改写,但i的确会增加1,对吗?
不一定!如果一个表达式和程序变得未定义,则它的所有方面都会变成未定义。参见问题3.2, 3.3, 11.32 和11.35。
3.9 ++i和i++有什么区别?
如果你的C语言书没有说明它们的区别,那么买一本好的。简单而言:++i在i存储的值上增加一并向使用它的表达式“返回”新的,增加后的值;
而i++对i增加一,但返回原来的是未增加的值。
3.10 如果我不使用表达式的值,我应该用++i或i++来自增一个变量吗?
由于这两种格式区别仅在于生成的值,所以在仅使用它们的副作用时,二者完全一样。但是,在C++中,前缀方式却是首选。参见问题3.3。
3.11 为什么如下的代码inta=100,b=100;longintc=a*b;不能工作?
根据C的内部类型转换规则,乘法是用int进行的,而其结果可能在转换为long型并赋给左边的c之前溢出或被截短。可以使用明确的类型转换,强
迫乘法以long型进行:
long int c = (long int)a * b;
注意,(longint)(a*b)不能达到需要的效果。
当两个整数做除法而结果赋与一个浮点变量时,也有可能有同样类型的问题,解决方法也是类似的。
参考资料:[K&R1,Sec.2.7p.41];[K&R2,Sec.2.7p.44];[ISO,Sec.6.2.1.5];[H&S,使用我的编辑器,下面的代码];[CT&P,Sec.3.9pp.49-50]。
3.12 我需要根据条件把一个复杂的表达式赋值给两个变量中的一个。可以用下边这样的代码吗?((condition)?a:b)=compli-
catedexpression;
不能。?:操作符,跟多数操作符一样,生成一个值,而不能被赋值。换言之,?:不能生成一个“左值”。如果你真的需要,你可以试试下面这样的
代码:
*((condition) ? &a : &b) = complicated_expression;
尽管这毫无优雅可言。
参考资料:[ISO,Sec.6.3.15];[H&S, Sec. 7.1 pp. 179-180] 。
第4 章
指针
4.1 我想声明一个指针并为它分配一些空间,但却不行。这些代码有什么问题?char*p;*p=malloc(10);
你所声明的指针是p,而不是*p,当你操作指针本身时(例如当你对其赋值,使之指向别处时),你只需要使用指针的名字即可:
p = malloc(10);
当你操作指针指向的内存时,你才需要使用*作为间接操作符:
*p = ’H’;
参见问题1.7, 7.1, 7.5 和8.3。
参考资料: [CT&P,Sec.3.1p.28]。
4.2 *p++自增p还是p所指向的变量?
后缀++和--操作符本质上比前缀一目操作的优先级高,因此*p++和
*(p++) 等价,它自增p并返回p自增之前所指向的值。要自增p指向的值,使用(*p)++,如果副作用的顺序无关紧要也可以使用++*p。
参考资料:[K&R1,Sec.5.1p.91];[K&R2,Sec.5.1p.95];[ISO,Sec.6.3.2,Sec. 6.3.3];[H&S,Sec.7.4.4pp.192-
3,Sec.7.5p.193,Secs.7.5.7,7.5.8pp.199-200]。
4.3 我有一个char*型指针正巧指向一些int型变量,我想跳过它们。为什么如下的代码((int*)p)++;不行?
在C语言中,类型转换意味着“把这些二进制位看作另一种类型,并作相应的对待”;这是一个转换操作符,根据定义它只能生成一个右值
(rvalue)。而右值既不能赋值,也不能用++自增。(如果编译器支持这样的扩展,那要么是一个错误,要么是有意作出的非标准扩展。)要达到你
的目的可以用:
p = (char *)((int *)p 1);
或者,因为p是char*型,直接用+
p += sizeof(int);
但是,在可能的情况下,你还是应该首先选择适当的指针类型,而不是一味地试图李代桃僵。
参考资料: [K&R2,Sec.A7.5 p. 205];[ISO,Sec.6.3.4];[Rationale,Sec.
3.3.2.4]; [H&S,Sec.7.1pp.179-80]。
17
4.4 我有个函数,它应该接受并初始化一个指针voidf(int*ip){ static int dummy = 5; ip = &dummy;} 但是当我如下调用时:int*ip;f
(ip);调用者的指针却没有任何变化。
你确定函数初始化的是你希望它初始化的东西吗?请记住在C中,参数是通过值传递的。被调函数仅仅修改了传入的指针副本。你需要传入指针
的地址(函数变成接受指针的指针),或者让函数返回指针。
参见问题4.5 和4.7。
4.5 我能否用void**指针作为参数,使函数按引用接受一般指针?
不可移植。C中没有一般的指针的指针类型。void*可以用作一般指针只是因为当它和其它类型相互赋值的时候,如果需要,它可以自动转换成其
它类型;但是,如果试图这样转换所指类型为void*之外的类型的void**指针时,这个转换不能完成。
4.6 我有一个函数externintf(int*);它接受指向int型的指针。我怎样用引用方式传入一个常数?下面这样的调用f(&5);似乎不行。
在C99中,你可以使用“复合常量”:
f((int[]){5});
在C99之前,你不能直接这样做;你必须先定义一个临时变量,然后把它的地址传给函数:
int five = 5;
f(&five);
参见问题2.8, 4.4 和20.1。
4.7 C有“按引用传递”吗?
真的没有。
严格地讲,C总是按值传递。你可以自己模拟按引用传递,定义接受指针的函数,然后在调用时使用&操作符。事实上,当你向函数传入数组(传入
指针的情况参见问题6.4 及其它)时,编译器本质上就是在模拟按引用传递。但是C没有任何真正等同于正式的按引用传递或C++的引用参数的东
西。另一方面,类似函数的预处理宏可以提供一种“按名称传递”的形式。
参见问题4.4 和20.1。
参考资料:[K&R1,Sec.1.8pp.24-5,Sec.5.2pp.91-3];[K&R2, Sec. 1.8 pp. 27-8,Sec.5.2pp.95-7];[ISO,Sec.6.3.2.2];[H&S, Sec. 9.5 pp.
273-4] 。
4.8 我看到了用指针调用函数的不同语法形式。到底怎么回事?
最初,一个函数指针必须用*操作符(和一对额外的括弧)“转换为”一个“真正的”函数才能调用:
int r, func(), (*fp)() = func;
r = (*fp)();
而函数总是通过指针进行调用的,所有“真正的”函数名总是隐式的退化为指针(在表达式中,正如在初始化时一样。参见问题1.14)。这个推论
表明无论fp是函数名和函数的指针
r = fp();
ANSIC标准实际上接受后边的解释,这意味着*操作符不再需要,尽管依然允许。
参见问题1.14。
参考资料:[K&R1,Sec.5.12p.116];[K&R2,Sec.5.11p.120];[ISO,Sec.
6.3.2.2]; [Rationale,Sec.3.3.2.2];[H&S, Sec. 5.8 p. 147, Sec. 7.4.3 p. 190] 。
4.9 我怎样把一个int变量转换为char*型?我试了类型转换,但是不行。
这取决于你希望做什么。如果你的类型转换不成功,你可能是企图把整数转为字符串,这种情况参见问题13.1。如果你试图把整数转换为字符,
参见问题
8.4。如果你试图让一个指针指向特定的内存地址,参见问题19.29。
第5 章
空(null)指针
5.1 臭名昭著的空指针到底是什么?
语言定义中说明,每一种指针类型都有一个特殊值——“空指针”——它与同类型的其它所有指针值都不相同,它“与任何对象或函数的指针值
都不相等”。也就是说,取地址操作符&永远也不能得到空指针,同样对malloc()的成功调用也不会返回空指针,如果失败,malloc()的确返回空
指针,这是空指针的典型用法:表示“未分配”或者“尚未指向任何地方”的指针。
空指针在概念上不同于未初始化的指针。空指针可以确保不指向任何对象或函数;而未初始化指针则可能指向任何地方。参见问题1.10、7.1
和7.26。
如上文所述,每种指针类型都有一个空指针,而不同类型的空指针的内部表示可能不尽相同。尽管程序员不必知道内部值,但编译器必须时刻明
确需要那种空指针,以便在需要的时候加以区分(参见问题5.2、5.5 和5.6)。
参考资料:[K&R1,Sec.5.4pp.97-8];[K&R2,Sec.5.4p.102];[ISO,Sec.
6.2.2.3]; [Rationale,Sec.3.2.2.3];[H&S, Sec. 5.3.2 pp. 121-3] 。
5.2 怎样在程序里获得一个空指针?
根据语言定义,在指针上下文中的常数0会在编译时转换为空指针。也就是说,在初始化、赋值或比较的时候,如果一边是指针类型的值或表达式
,编译器可以确定另一边的常数0为空指针并生成正确的空指针值。因此下边的代码段完全合法:
char *p = 0;
if(p != 0)
参见问题5.3。
然而,传入函数的参数不一定被当作指针环境,因而编译器可能不能识别未加修饰的0“表示”指针。在函数调用的上下文中生成空指针需要明
确的类型转换,强制把0看作指针。例如,Unix系统调用execl接受变长的以空指针结束的字符指针参数。它应该如下正确调用:
execl("/bin/sh", "sh", "-c", "date", (char *)0);
如果省略最后一个参数的(char*)转换,则编译器无从知道这是一个空指针,从而当作一个0传入。(注意很多Unix手册在这个例子上都弄错了。)
21
如果范围内有函数原型,则参数传递变为“赋值上下文”,从而可以安全省略多数类型转换,因为原型告知编译器需要指针,使之把未加修饰的0
正确转换为适当的指针。函数原型不能为变长参数列表中的可变参数提供类型。(参见问题15.3) 在函数调用时对所有的空指针进行类型转换
可能是预防可变参数和无原型函数出问题的最安全的办法。
摘要:
可以使用未加修饰的0:需要显示的类型转换:
初始化 函数调用,作用域内无原型
赋值 变参函数调用中的可变参数
比较
固定参数的函数调用且在作用域
内有原型
参考资料:[K&R1,Sec.A7.7p.190,Sec.A7.14p.192];[K&R2, Sec. A7.10
p.207,Sec.A7.17p.209];[ISO,Sec.6.2.2.3];[H&S, Sec. 4.6.3 p. 95, Sec. 6.2.7
p. 171] 。
5.3 用缩写的指针比较“if(p)”检查空指针是否可靠?如果空指针的内部表达不是0会怎么样?
当C在表达式中要求布尔值时,如果表达式等于0则认为该值为假,否则为真。换言之,只要写出
if(expr)
无论“expr”是任何表达式,编译器本质上都会把它当
if((expr) != 0)
处理。
如果用指针p代替“expr”则
if(p)等价于if(p!=0)。
而这是一个比较上下文,因此编译器可以看出0实际上是一个空指针常数,并使用正确的空指针值。这里没有任何欺骗;编译器就是这样工作的,
并为、二者生成完全一样的代码。空指针的内部表达无关紧要。布尔否操作符!可如下描述:!expr本质上等价于(expr)?0:1或等价于((expr)
==0)从而得出结论if(!p)等价于if(p==0)类似if(p)这样的“缩写”,尽管完全合法,但被一些人认为是不好的风格(另外
一些人认为恰恰是好的风格;参见问题17.8)。
参见问题9.2。
参考资料:[K&R2,Sec.A7.4.7p.204];[ISO, Sec. 6.3.3.3, Sec. 6.3.9, Sec. 6.3.13, Sec. 6.3.14, Sec. 6.3.15, Sec. 6.6.4.1, Sec.
6.6.5];[H&S,Sec.5.3.2p.122]。
5.4 NULL是什么,它是怎么定义的?
作为一种风格,很多人不愿意在程序中到处出现未加修饰的0。因此定义了预处理宏NULL(在<stdio.h>和其它几个头文件中)为空指针常数,通常
是0或者((void*)0)(参见问题5.6)。希望区别整数0和空指针0的人可以在需要空指针的地方使用NULL。
使用NULL只是一种风格习惯;预处理器把所有的NULL都还原回0,而编译还是依照上文的描述处理指针上下文的0。特别是,在函数调用的参数
里,NULL之前(正如在0之前)的类型转换还是需要。问题5.2 下的表格对0和NULL都有效(带修饰的NULL和带修饰的0完全等价)。
NULL只能用作指针常数;参见问题5.7。
参考资料:[K&R1,Sec.5.4pp.97-8];[K&R2,Sec.5.4p.102];[ISO,Sec.7.1.6,Sec.6.2.2.3];[Rationale,Sec.4.1.5];
[H&S,Sec.5.3.2p.122,Sec.11.1p.292]。
5.5 在使用非全零作为空指针内部表达的机器上,NULL是如何定义的?
跟其它机器一样:定义为0(或某种形式的0;参见问题5.4)。
当程序员请求一个空指针时,无论写“0”还是“NULL”,都是有编译器来生成适合机器的空指针的二进制表达形式。因此,在空指针的内部表达
不为0的机器上定义NULL为0跟在其它机器上一样合法:编译器在指针上下文看到的未加修饰的0都会被生成正确的空指针。参见问题5.2、5.8
和5.14。
参考资料:[ISO,Sec.7.1.6];[Rationale,Sec.4.1.5]。
5.6 如果NULL定义成#de.neNULL((char*)0)难道不就可以向函数传入不加转换的NULL了吗?
一般情况下,不行。复杂之处在于,有的机器不同类型数据的指针有不同的内部表达。这样的NULL定义对于接受字符指针的的函数没有问题,但
对于其它类型的指针参数仍然有问题(在缺少原型的情况下),而合法的构造如
FILE *fp = NULL;
则会失败。
不过,ANSIC允许NULL的可选定义
#define NULL ((void *)0)
除了潜在地帮助错误程序运行(仅限于使用同样类型指针的机器,因此帮助有限)以外,这样的定义还可以发现错误使用NULL的程序(例如,在实际
需要使用ASCIINUL字符的地方;参见问题5.7)。
无论如何,ANSI函数原型确保大多数(尽管不是全部;参见问题5.2)指针参数在传入函数时正确转换。因此,这个问题有些多余。
参考资料:[Rationale,Sec.4.1.5]。
5.7 如果NULL和0作为空指针常数是等价的,那我到底该用哪一个呢?
许多程序员认为在所有的指针上下文中都应该使用NULL,以表明该值应该被看作指针。另一些人则认为用一个宏来定义0,只不过把事情搞得更
复杂,反而令人困惑。因而倾向于使用未加修饰的0。没有正确的答案。(参见问题9.2 和
17.8) C程序员应该明白,在指针上下文中NULL和0是完全等价的,而未加修饰的0也完全可以接受。任何使用NULL(跟0相对)的地方都应该看作一
种温和的提示,是在使用指针;程序员(和编译器都)不能依靠它来区别指针0和整数0。
在需要其它类型的0的时候,即便它可能工作也不能使用NULL,因为这样做发出了错误的格式信息。(而且,ANSI允许把NULL定义为((void*)0),这
在非指针的上下文中完全无效。特别是,不能在需要ASCII空字符(NUL)的地方用NULL。如果有必要,提供你自己的定义
#define NUL ’/0’
参考资料:[K&R1,Sec.5.4pp.97-8];[K&R2, Sec. 5.4 p. 102] 。
5.8 但是如果NULL的值改变了,比如在使用非零内部空指针的机器上,难道用NULL(而不是0)不是更好吗?
不。(用NULL可能更好,但不是这个原因。)尽管符号常量经常代替数字使用以备数字的改变,但这不是用NULL代替0的原因。语言本身确保了源
码中的0(用于指针上下文)会生成空指针。NULL只是用作一种格式习惯。参见问题5.5 和9.2。
5.9 用预定义宏#de.neNullptr(type)(type*)0帮助创建正确类型的空指针。
这种技巧,尽管很流行而且表面上看起来很有吸引力,但却没有多少意义。在赋值和比较时它并不需要;参见问题5.2。它甚至都不能节省键盘输
入。参见问题
9.1 和10.1。
5.10 这有点奇怪。NULL可以确保是0,但空(null)指针却不一定?
随便使用术语“null”或“NULL”时,可能意味着以下一种或几种含义:
1.概念上的空指针,问题5.1 定义的抽象语言概念。它使用以下的东西实现的..
2.空指针的内部(或运行期)表达形式,这可能并不是全零,而且对不用的指针类型可能不一样。真正的值只有编译器开发者才关心。C程序的作
者永远看不到它们,因为他们使用..
3.空指针常数,这是一个常整数0(参见问题5.2)。它通常隐藏在..
4.NULL宏,它被定义为0(参见问题5.4)。最后转移我们注意力到..
5.ASCII空字符(NUL),它的确是全零,但它和空指针除了在名称上以外,没有任何必然关系;而..
6.“空串”(nullstring),它是内容为空的字符串("")。在C中使用空串这个术语可能令人困惑,因为空串包括空字符(’/0’),但不包括空指针
,这让我们绕了一个完整的圈子..
本文用短语“空指针”(“nullpointer”,小写)表示第一种含义,标识“0”或短语“空指针常数”表示含义3,用大写NULL表示含义4。
5.11 为什么有那么多关于空指针的疑惑?为什么这些问题如此经常地出现?
C程序员传统上喜欢知道很多(可能比他们需要知道的还要多)关于机器实现的细节。空指针在源码和大多数机器实现中都用零来表示的事实导
致了很多无根据的猜测。而预处理宏(NULL)的使用又似乎在暗示这个值可能在某个时刻或者在某种怪异的机器上会改变。“if(p==0)”这种结
构又很容易被误认为在比较之前把p转成了整数类型,而不是把0转成了指针类型。最后,术语“空”的几种用法(如上文问题5.10 所列出的)之
间的区别又可能被忽视。
冲出这些迷惘的一个好办法是想象C使用一个关键字(或许象Pascal那样,用“nil”)作为空指针常数。编译器要么在源代码没有歧义的时候把
“nil”转成适当类型的空指针,或者有歧义的时候发出提示。现在事实上,C语言的空指针常数关键字不是“nil”而是“0”,这在多数情况下
都能正常工作,除了一个未加修饰的“0”用在非指针上下文的时候,编译器把它生成整数0而不是发出错误信息,如果那个未加修饰的0是应该是
空指针常数,那么生成的程序不行。
5.12 我很困惑。我就是不能理解这些空指针一类的东西。
有两条简单规则你必须遵循:
1.当你在源码中需要空指针常数时,用“0”或“NULL”。
2.如果在函数调用中“0”或“NULL”用作参数,把它转换成被调函数需要的指针类型
讨论的其它内容是关于别人的误解,关于空指针的内部表达(这你无需了解),和关于函数原型的复杂性的。(考虑到这些复杂性,我们发现规则2
有些保守;但它没什么害处。)理解问题5.1、5.2 和5.4, 考虑问题5.3、5.7、5.10 和5.11 你就就会变得清晰。
5.13 考虑到有关空指针的所有这些困惑,难道把要求它们内部表达都必须为0不是更简单吗?
如果没有其它的原因,这样做会是没脑筋的。因为它会不必要地限制某些实现,阻止它们用特殊的非全零值表达空指针,尤其是当那些值可以为
非法访问引发自动的硬件陷阱的时候。
况且,这样的要求真正完成了什么呢?对空指针的正确理解不需要内部表达的知识,无论是零还是非零。假设空指针内部表达为零不会使任何代
码的编写更容易(除了一些不动脑筋的calloc()调用;参见问题7.26)。用零作空指针的内部表达也不能消除在函数调用时的类型转换,因为指针
的大小可能和int型的大小依然不同。(如果象上文问题5.11 所述,用“nil”来请求空指针,则用0作空指针的内部表达的想法都不会出现。)
5.14 说真的,真有机器用非零空指针吗,或者不同类型用不同的表达?
至少PL/I,Prime50系列用段07777,偏移0作为空指针。后来的型号使用段0,偏移0作为C的空指针,迫使类似TCNP(测试C空指针)的指令明显地成
了现成的作出错误猜想的蹩脚C代码。旧些的按字寻址的Prime机器同样因为要求字节指针(char*)比字指针(int*)长而臭名昭著。
DataGeneral的EclipseMV系列支持三种结构的指针格式(字、字节和比特指针),C编译器使用了其中之二:char*和void*使用字节指针,而其它
的使用字指针。
某些Honeywell-Bull大型机使用比特模式06000作为(内部的)空指针。
CDCCyber180系列使用包含环(ring),段和位移的48位指针。多数用户(在环11上)使用的空指针为0xB00000000000。在旧的1次补码的CDC机器上
用全1表示各种数据,包括非法指针,是十分常见的事情。
旧的HP3000系列对字节地址和字地址使用不同的寻址模式;正如上面的机器一样,它因此也使用不同的形式表达char*和void*型指针及其它指针
。
SymbolicsLisp机器是一种标签结构,它甚至没有传统的数字指针;它使用<NIL,0>对(通常是不存在的<对象,偏移>句柄)作为C空指针。
根据使用的“内存模式”,8086系列处理器(PC兼容机)可能使用16位的数据指针和32位的函数指针,或者相反。
一些64位的Cray机器在一个字的低48位表示int*;char*使用高16位的某些位表示一个字节在一个字中的偏移。
参考资料:[K&R1,Sec.A14.4p.211]。
5.15 运行时的“空指针赋值”错误是什么意思?
这个信息,通常由MS-DOS编译器发出,表明你通过空指针向非法地址(可能是缺省数据段的偏移0位置)写入了数据。参见问题16.7。
第6 章
数组和指针
6.1 我在一个源文件中定义了chara[6],在另一个中声明了externchar
*a 。为什么不行?
你在一个源文件中定义了一个字符串,而在另一个文件中定义了指向字符的指针。externchar*的申明不能和真正的定义匹配。类型T的指针和
类型T的数组并非同种类型。请使用externchara[]。
参考资料:[ISO,Sec.6.5.4.2];[CT&P,Sec.3.3pp.33-4,Sec.4.5pp.64-5]。
6.2 可是我听说chara[]和char*a是一样的。
并非如此。(你所听说的应该跟函数的形式参数有关;参见问题6.4) 数组不是指针。数组定义chara[6]请求预留6个字符的位置,并用名称“a
”表示。也就是说,有一个称为“a”的位置,可以放入6个字符。而指针申明char*p,请求一个位置放置一个指针,用名称“p”表示。这个指针
几乎可以指向任何位置:任何字符和任何连续的字符,或者哪里也不指(参见问题5.1 和1.10)。
一个图形胜过千言万语。声明
char a[] = "hello";
char *p = "world";
将会初始化下图所示的数据结果:
+---+---+---+---+---+---+
a: | h | e | l | l | o |/0 |
+---+---+---+---+---+---+
+-----+ +---+---+---+---+---+---+
p:| *======>|w|o|r|l|d|/0|
+-----+ +---+---+---+---+---+---+
根据x是数组还是指针,类似x[3]这样的引用会生成不同的代码。认识到这一点大有裨益。以上面的声明为例,当编译器看到表达式a[3]的时候,
它生成代码从a的位置开始跳过3个,然后取出那个字符.如果它看到p[3],它生成代码找到“p”的位置,取出其中的指针值,在指针上加3然后取
出指向的字符。换言之,a[3]是名为a的对象(的起始位置)之后3个位置的值,而p[3]是p指向的对象的3个位置之后的值.在上例中,a[3]和p[3]碰
巧都是’l’,但是编译器到达那里的途径不尽相同。本质的区别在于类似a的数组和类似p的指针一旦在表达式中出现就会
27
按照不同的方法计算,不论它们是否有下标。下一问题继续深入解释。参见问题
1.13。参考资料:[K&R2,Sec.5.5p.104];[CT&P, Sec. 4.5 pp. 64-5] 。
6.3 那么,在C语言中“指针和数组等价”到底是什么意思?
在C语言中对数组和指针的困惑多数都来自这句话。说数组和指针“等价”不表示它们相同,甚至也不能互换。它的意思是说数组和指针的算法
定义可以用指针方便的访问数组或者模拟数组。
特别地,等价的基础来自这个关键定义:
一个T的数组类型的左值如果出现在表达式中会蜕变为一个指向数
组第一个成员的指针(除了三种例外情况);结果指针的类型是T的
指针。
这就是说,一旦数组出现在表达式中,编译器会隐式地生成一个指向数组第一个成员地指针,就像程序员写出了&a[0]一样。例外的情况是,数组
为sizeof或&操作符的操作数,或者为字符数组的字符串初始值。
作为这个这个定义的后果,编译器并那么不严格区分数组下标操作符和指针。在形如a[i]的表达式中,根据上边的规则,数组蜕化为指针然后按
照指针变量的方式如p[i]那样寻址,如问题6.2 所述,尽管最终的内存访问并不一样。如果你把数组地址赋给指针:
p = a;
那么p[3]和a[3]将会访问同样的成员。参见问题6.6 和6.11。参考资料:[K&R1,Sec.5.3pp.93-6];[K&R2,Sec.5.3p.99];[ISO,Sec.
6.2.2.1, Sec.6.3.2.1,Sec.6.3.6];[H&S,Sec.5.4.1p.124]。
6.4 那么为什么作为函数形参的数组和指针申明可以互换呢?
这是一种便利。
由于数组会马上蜕变为指针,数组事实上从来没有传入过函数。允许指针参数声明为数组只不过是为让它看起来好像传入了数组,因为该参数可
能在函数内当作数组使用。特别地,任何声明“看起来象”数组的参数,例如
void f(char a[])
{ ... }
在编译器里都被当作指针来处理,因为在传入数组的时候,那正是函数接收到的.
void f(char *a)
{ ... }
这种转换仅限于函数形参的声明,别的地方并不适用。如果这种转换令你困惑,请避免它;很多程序员得出结论,让形参声明“看上去象”调用或
函数内的用法所带来的困惑远远大于它所提供的方便。
参见问题6.18。
参考资料:[K&R1,Sec.5.3p.95,Sec.A10.1p.205];[K&R2, Sec. 5.3 p. 100,Sec.A8.6.3p.218,Sec.A10.1p.226];
[ISO,Sec.6.5.4.3,Sec.6.7.1,Sec.
6.9.6]; [H&S,Sec.9.3p.271];[CT&P, Sec. 3.3 pp. 33-4] 。
6.5 如果你不能给它赋值,那么数组如何能成为左值呢?
ANSIC标准定义了“可变左值”,而数组不是。
参考资料: [ISO,Sec.6.2.2.1];[Rationale,Sec.3.2.2.1];[H&S,Sec.7.1p.179]。
6.6 现实地讲,数组和指针地区别是什么?
数组自动分配空间,但是不能重分配或改变大小。指针必须明确赋值以指向分配的空间(可能使用malloc),但是可以随意重新赋值(即,指向不同
的对象),同时除了表示一个内存块的基址之外,还有许多其它的用途。
由于数组和指针所谓的等价性(参见问题6.3),数组和指针经常看起来可以互换,而事实上指向malloc分配的内存块的指针通常被看作一个真正
的数组(也可以用[]引用)。参见问题6.11 和6.13。但是,要小心sizeof。
参见问题1.13 和20.12。
6.7 有人跟我讲,数组不过是常指针。
这有些过度单纯化了。数组名之所以为“常数”是因为它不能被赋值,但是数组不是指针,问题6.2 的讨论和图画可以说明这个。参见问题6.3
和6.6。
6.8 我遇到一些“搞笑”的代码,包含5["abcdef"]这样的“表达式”。这为什么是合法的C表达式呢?
是的,弗吉尼亚1 ,数组和下标在C语言中可以互换。这个奇怪的事实来自数组下标的指针定义,即对于任何两个表达式a和e,只要其中一个是指
针表达式而另一个为整数,则a[e]和*((a)+(e))完全一样。这种交换性在许多C语言的书中被看作值得骄傲的东西,但是它除了在混乱C语言竞赛
之外,其实鲜有用武之地。
参考资料:[Rationale,Sec.3.3.2.1];[H&S,Sec.5.4.1p.124,Sec.7.4.1pp.186-7]。
1这里有个美国典故,在1897年,有个叫弗吉尼亚(Virginia)的八岁小女孩,她对圣诞老人是否存在感到困惑,因而写了封寻问信给《纽约太阳报
》,于是就有了“Yes,Virginia,thereisaSantaClaus”这篇评论,有兴趣的朋友可以在http://www.hymnsandcarolsofchristmas.com/
santa/virginia’squestion.htm找到相关资料。
6.9 既然数组引用会蜕化为指针,如果arr是数组,那么arr和&arr又有什么区别呢?
区别在于类型。
在标准C中,&arr生成一个“T型数组”的指针,指向整个数组。在ANSI之前的C中,&arr中的&通常会引起一个警告,它通常被忽略。在所有的C编
译器中,对数组的简单引用(不包括&操作符)生成一个T的指针类型的指针,指向数组的第一成员。参见问题6.3, 6.10 和6.15。
参考资料:[ISO,Sec.6.2.2.1,Sec.6.3.3.2];[Rationale,Sec.3.3.3.2];[H&S,Sec. 7.5.6 p. 198] 。
6.10 我如何声明一个数组指针?
通常,你不需要。当人们随便提到数组指针的时候,他们通常想的是指向它的第一个元素的指针。
考虑使用指向数组某个元素的指针,而不是数组的指针。类型T的数组蜕变成类型T的指针(参见问题6.3),这很方便;在结果的指针上使用下标或
增量就可以访问数组中单独的成员。而真正的数组指针,在使用下标或增量时,会跳过整个数组,通常只在操作数组的数组时有用——如果还有
一点用的话。参见问题
6.15。
如果你真的需要声明指向整个数组的指针,使用类似“int(*ap)[N];”这样的声明。其中N是数组的大小(参见问题1.7)。如果数组的大小未知,
原则上可以省略N,但是这样生成的类型,“指向大小未知的数组的指针”,毫无用处。
参见问题6.9。
参考资料:[ISO,Sec.6.2.2.1]。
6.11 我如何在运行期设定数组的大小?我怎样才能避免固定大小的数组?
由于数组和指针的等价性(参见问题6.3),可以用指向malloc分配的内存的指针来模拟数组。执行
#include <stdlib.h>
int *dynarray;
dynarray = malloc(10 * sizeof(int));
以后(如果malloc调用成功),你可以象传统的静态分配的数组那样引用dynarry[i](i从0到9)。唯一的区别是sizeof不能给出“数组”的大小。
参见问题
1.12、6.13 和7.9。
6.12 我如何声明大小和传入的数组一样的局部数组?
直到最近以前,你都不能;C语言的数组维度一直都是编译时常数。但是,C99引入了变长数组(VLA),解决了这个问题;局部数组的大小可以用变量
或其它表达式设置,可能也包括函数参数。(gcc提供参数化数组作为扩展已经有些时候了。)如果你不能使用C99或gcc,你必须使用malloc(),并
在函数返回之前调用free()。参见问题6.11, 6.13, 6.16, 7.19 和7.27。
参考资料:[ISO,Sec.6.4,Sec.6.5.4.2];[C9X, Sec. 6.5.5.2] 。
6.13 我该如何动态分配多维数组?
传统的解决方案是分配一个指针数组,然后把每个指针初始化为动态分配的“列”。以下为一个二维的例子:
#include <stdlib.h>
int **array1 = malloc(nrows * sizeof(int *)); for(i = 0; i < nrows; i++) array1[i] = malloc(ncolumns * sizeof(int));
当然,在真实代码中,所有的malloc返回值都必须检查。你也可以使用sizeof(*array1)和sizeof(**array1)代替sizeof(int*)和sizeof(int)。
你可以让数组的内容连续,但在后来重新分配列的时候会比较困难,得使用一点指针算术:
int **array2 = malloc(nrows * sizeof(int *)); array2[0] = malloc(nrows * ncolumns * sizeof(int)); for(i = 1; i < nrows; i++)
array2[i] = array2[0] + i * ncolumns;
在两种情况下,动态数组的成员都可以用正常的数组下标arrayx[i][j]来访问(for0<=i<nrows和0<=j<ncolumns)。如果上述方案的两次间接因
为某种原因不能接受,你还可以同一个单独的动态分配的一维数组来模拟二维数组:
int *array3 = malloc(nrows * ncolumns * sizeof(int));
但是,你现在必须手工计算下标,用array3[i*ncolumns+j]访问第i,j个成员。使用宏可以隐藏显示的计算,但是调用它的时候要使用括号和逗号
,这看起来不太象多维数组语法,而且宏需要至少访问一维。参见问题6.16。
另一种选择是使用数组指针:
int (*array4)[NCOLUMNS] = malloc(nrows * sizeof(*array4));
但是这个语法变得可怕而且运行时最多只能确定一维。
当然,使用这些技术,你都必须记住在不用的时候释放数组(这可能需要多个步骤;参见问题7.20)。而且你可能不能混用动态数组和传统的静态
分配数组。参见问题6.17 和6.15。
最后,在C99中你可以使用变长数组。所有这些技术都可以延伸到三维或更多的维数。参考资料:[C9X,Sec.6.5.5.2]。
6.14 有个灵巧的窍门:如果我这样写intrealarray[10];int*array=&realarray[-1];我就可以把“array”当作下标从1开始的数组。
尽管这种技术颇有吸引力(而且在《NumericalRecipesinC》一书的旧版中使用过),它却不完全符合C的标准。指针算术只有在指针所指的内存
块之内,或者指向虚构的“终结”元素后的一个时才有定义;否则,即使指针并未解参考,其行为仍然是未定义的。如果在用偏移作下标运算的时
候生成了非法地址(可能因为地址在经过某个内存段之后“回绕”),则这段代码会失败。
参考资料:[K&R2,Sec.5.3p.100,Sec.5.4pp.102-3,Sec.A7.7pp.205-6];[ISO,Sec.6.3.6];[Rationale, Sec. 3.2.2.3] 。
6.15 当我向一个接受指针的指针的函数传入二维数组的时候,编译器报错了。
数组蜕化为指针的规则(参见问题6.3) 不能递归应用。数组的数组(即C语言中的二维数组)蜕化为数组的指针,而不是指针的指针。数组指针常
常令人困惑,需要小心对待;参见问题6.10。
如果你向函数传递二位数组:
int array[NROWS][NCOLUMNS];
f(array);
那么函数的声明必须匹配:
void f(int a[][NCOLUMNS])
{ ... }
或者
voidf(int(*ap)[NCOLUMNS])/*ap是个数组指针*/
{ ... }
在第一个声明中,编译器进行了通常的从“数组的数组”到“数组的指针”的隐式转换(参见问题6.3 和6.4);第二种形式中的指针定义显而易
见。因为被调函数并不为数组分配地址,所以它并不需要知道总的大小,所以行数NROWS可以省略。但数组的宽度依然重要,所以列维度
NCOLUMNS(对于三维或多维数组,相关的维度)必须保留。
如果一个函数已经定义为接受指针的指针,那么几乎可以肯定直接向它传入二维数组毫无意义。
参见问题6.9 和6.12。
参考资料:[K&R1,Sec.5.10p.110];[K&R2,Sec.5.9p.113];[H&S,Sec.
5.4.3 p. 126] 。
6.16 我怎样编写接受编译时宽度未知的二维数组的函数?
这并非易事。一种办法是传入指向[0][0]成员的的指针和两个维数,然后“手工”模拟数组下标。
void f2(int *aryp, int nrows, int ncolumns)
{ ... array[i][j] is accessed as aryp[i * ncolumns + j] ... }
这个函数可以用问题6.15 的数组如下调用:
f2(&array[0][0], NROWS, NCOLUMNS);
但是,必须注明的一点是,用这种方法通过“手工”方式模拟下标的程序未能严格遵循ANSIC标准;根据官方的解释,当x>=NCOLUMNS时,访问
&array[0][0][x]的结果未定义。
C99允许变长数组,一旦接受C99扩展的编译器广泛流传以后,VLA可能是首选的解决方案。gcc支持可变数组已经有些时日了。当你需要使用各种
大小的多维数组的函数时,一种解决方案是象问题6.13 那
样动态模拟所有的数组。参见问题6.15, 6.17, 6.12。参考资料:[ISO,Sec.6.3.6];[C9X, Sec. 6.5.5.2] 。
6.17 我怎样在函数参数传递时混用静态和动态多维数组?
没有完美的方法。假设有如下声明
intarray[NROWS][NCOLUMNS];int**array1;/*不齐的*/int**array2;/*连续的*/int*array3;/*"变平的"*/int(*array4)[NCOLUMNS];
指针的初始值如问题6.13 的程序片段,函数声明如下
void f1a(int a[][NCOLUMNS], int nrows, int ncolumns);
void f1b(int (*a)[NCOLUMNS], int nrows, int ncolumns);
void f2(int
*aryp, int nrows, int ncolumns);
void f3(int **pp, int nrows, int ncolumns);
其中f1a()和f1b()接受传统的二维数组,f2()接受“扁平的”二维数组,f3()接受指针的指针模拟的数组(参见问题6.15 和6.16),下面的调用应
该可以如愿运行:
f1a(array, NROWS, NCOLUMNS);
f1b(array, NROWS, NCOLUMNS);
f1a(array4, nrows, NCOLUMNS);
f1b(array4, nrows, NCOLUMNS);
f2
(&array[0][0], NROWS, NCOLUMNS);
f2(*array, NROWS, NCOLUMNS);
f2(*array2, nrows, ncolumns);
f2(array3, nrows, ncolumns);
f2
(*array4, nrows, NCOLUMNS);
f3(array1, nrows, ncolumns);
f3(array2, nrows, ncolumns);
下面的调用在大多数系统上可能可行,但是有可疑的类型转换,而且只有动态ncolumns和静态NCOLUMNS匹配才行:
f1a((int (*)[NCOLUMNS])(*array2), nrows, ncolumns);
f1a((int (*)[NCOLUMNS])(*array2), nrows, ncolumns);
f1b((int (*)
[NCOLUMNS])array3, nrows, ncolumns);
f1b((int (*)[NCOLUMNS])array3, nrows, ncolumns);
同时必须注意向f2()传递&array[0][0](或者等价的*array)并不完全符合标准;参见问题6.16。如果你能理解为何上述调用可行且必须这样书
写,而未列出的组合不行,那么你对C语言中的数组和指针就有了很好的理解了。
为免受这些东西的困惑,一种使用各种大小的多维数组的办法是令它们“全部”动态分配,如问题6.13 所述。如果没有静态多维数组——如果
所有的数组都按问题6.13 的array1和array2分配——那么所有的函数都可以写成f3()的形式。
6.18 当数组是函数的参数时,为什么sizeof不能正确报告数组的大小?
编译器把数组参数当作指针对待(参见问题6.4),因而报告的时指针的大小。参考资料:[H&S,Sec.7.5.2p.195]。
第7 章
内存分配
7.1 为什么这段代码不行?char*answer;printf("Typesomething:/n");gets(answer);printf("Youtyped/"%s/"/n",answer);
指针变量answer,传入gets(),意在指向保存得到的应答的位置,但却没有指向任何合法的位置。换言之,我们不知道指针answer指向何处。因为
局部变量没有初始化,通常包含垃圾信息,所以甚至都不能保证answer是一个合法的指针。参见问题1.10 和5.1。
改正提问程序的最简单方案是使用局部数组,而不是指针,让编译器考虑分配的问题:
#include <stdio.h>
#include <string.h>
char answer[100], *p;
printf("Type something:/n");
fgets(answer, sizeof answer, stdin);
if((p = strchr(answer, ’/n’)) != NULL)
*p = ’/0’;
printf("You typed /"%s/"/n", answer);
本例中同时用fgets()代替gets(),以便array的结束符不被改写。参见问题
12.20。不幸的是,本例中的fgets()不会象gets()那样自动地去掉结尾的/n。也可以用malloc()分配answer缓冲区。
7.2 我的strcat()不行.我试了char*s1="Hello,";char*s2="world!";char*s3=strcat(s1,s2);但是我得到了奇怪的结果。
跟前面的问题7.1 一样,这里主要的问题是没有正确地为连接结果分配空间。C没有提供自动管理的字符串类型。C编译器只为源码中明确提到
的对象分配空间(对于字符串,这包括字符数组和串常量)。程序员必须为字符串连接这样的运行期操作的结果分配足够的空间,通常可以通过声
明数组或调用malloc()完成。
strcat()不进行任何分配;第二个串原样不动地附加在第一个之后。因此,一种解决办法是把第一个串声明为数组:
char s1[20] = "Hello, ";
35
由于strcat()返回第一个参数的值,本例中为s1,s3实际上是多余的;在strcat()调用之后,s1包含结果。
提问中的strcat()调用实际上有两个问题:s1指向的字符串常数,除了空间不足以放入连接的字符串之外,甚至都不一定可写。参见问题1.13。
参考资料:[CT&P,Sec.3.2p.32]。
7.3 但是strcat的手册页说它接受两个char*型参数。我怎么知道(空间)分配的事情呢?
一般地说,使用指针的时候,你必须总是考虑内存分配,除非明确知道编译器替你做了此事。如果一个库函数的文档没有明确提到内存分配,那么
通常需要调用者来考虑。
Unix型的手册页顶部的大纲段落或ANSIC标准有些误导作用。那里展示的程序片段更像是实现者使用的函数定义而不是调用者使用的形式。特
别地,很多接受指针(如结构指针或串指针)的函数通常在调用时都用到某个由调用者分配的对象(结构,或数组——参见问题6.3 和6.4) 的指针
。其它的常见例子还有time()(参见问题13.10) 和stat()。
7.4 我刚才试了这样的代码char*p;strcpy(p,"abc");而它运行正常?怎么回事?为什么它没有崩溃?
你的运气来了,我猜。未初始化的指针p所指向的随机地址恰好对你来说是可写的,而且很显然也没有用于什么关键的数据。参见问题11.35。
7.5 一个指针变量分配多少内存?
这是个挺有误导性的问题。当你象这样声明一个指针变量的时候,
char *p;
你(或者,更准确地讲,编译器)只分配了足够容纳指针本身的内存;也就是说,这种情况下,你分配了sizeof(char*)个字节的内存。但你还没有分
配任何让指针指向的内存。参见问题7.1 和7.2。
7.6 我有个函数,本该返回一个字符串,但当它返回调用者的时候,返回串却是垃圾信息。
确保指向的内存已经正确分配了。例如,确保你没有做下面这样的事情:
char *itoa(int n)
{
charretbuf[20];/*错!*/
sprintf(retbuf, "%d", n);
returnretbuf;/*错!*/
}
一种解决方案是把返回缓冲区声明为
static char retbuf[20];
本方案并非完美,尤其是有问题的函数可能会递归调用,或者会同时使用到它
的多个返回值时。参见问题7.7, 12.19 和20.1。参考资料:[ISO,Sec.6.1.2.4]。
7.7 那么返回字符串或其它集合的争取方法是什么呢?
返回指针必须是静态分配的缓冲区(如问题7.6 的答案所述),或者调用者传入的缓冲区,或者用malloc()获得的内存,但不能是局部(自动)数组
。参见问题20.1。
7.8 为什么在调用malloc()时,我得到“警告:整数赋向指针需要类型转换”?
你包含了<stdlib.h>或者正确声明了malloc()吗?参见问题1.8。
参考资料:[H&S,Sec.4.7p.101]。
7.9 为什么有些代码小心地把malloc返回的值转换为分配的指针类型。
在ANSI/ISO标准C引入void*一般指针类型之前,这种类型转换通常用于在不兼容指针类型赋值时消除警告(或许也可能导致转换)。在ANSI/ISO
标准C下,这些转换不再需要,而起事实上现代的实践也不鼓励这样做,因为它们可能掩盖malloc()声明错误时产生的重要警告;参见上面的问题
7.8。(但是,因为这样那样的原因,为求与C++兼容,C程序中常常能见到这样的
转换。在C++中从void*的明确转换是必须的。)参考资料:[H&S,Sec.16.1pp.386-7]。
7.10 在调用malloc()的时候,错误“不能把void*转换为int*”是什么意思?
说明你用的是C++编译器而不是C编译器。参见问题7.9。
7.11 我见到了这样的代码char*p=malloc(strlen(s)+1);strcpy(p,s);难道不应该是malloc((strlen(s)+1)*sizeof(char))?
永远也不必乘上sizeof(char),因为根据定义,sizeof(char)严格为1。另一方面,乘上sizeof(char)也没有害处,有时候还可以帮忙为表达式引
入sizet类型。参见问题8.5。
参考资料:[ISO,Sec.6.3.3.4];[H&S, Sec. 7.5.2 p. 195] 。
7.12 我如何动态分配数组?
参见问题6.11 和6.13。
7.13 我听说有的操作系统程序使用的时候才真正分配malloc申请的内存。这合法吗?
很难说。标准没有说系统可以这样做,但它也没有明确说不能。
参考资料:[ISO,Sec.7.10.3]。
7.14 我用一行这样的代码分配一个巨大的数组,用于数字运算:double
*array =malloc(300*300*sizeof(double));malloc()并没有返回null,但是程序运行得有些奇怪,好像改写了某些内存,或者malloc()并没有分
配我申请的那么多内存,云云。
注意300*300是90,000,这在你乘上sizeof(double)以前就已经不能放入16位的int中了。如果你需要分配这样大的内存空间,你可得小心为妙。
如果在你的机器上sizet(malloc()接受的类型)是32位,而int为16位,你可以写300*(300
* sizeof(double))来避免这个问题。(参见问题3.11)。否则,你必须把你的数据结构分解为更小的块,或者使用32位的机器或编译器,或者使用
某种非标准的内存分配函数。参见问题19.27。
7.15 我的PC有8兆内存。为什么我只能分配640K左右的内存?
在PC兼容的分段结构下,很难透明地分配超过640K的内存,尤其是在MS-DOS下。参见问题19.27。
7.16 我的程序总是崩溃,显然在malloc内部的某个地方。但是我看不出哪里有问题。是malloc()有bug吗?
很不幸,malloc的内部数据结构很容易被破坏,而由此引发的问题会十分棘手。最常见的问题来源是向malloc分配的区域写入比所分配的还多的
数据;一个常见的bug是用malloc(strlen(s))而不是strlen(s)+1。其它的问题还包括使用指向已经释放了的内存的指针,释放未从malloc获得
的内存,或者两次释放同一个指针,或者试图重分配空指针,参见问题7.25。
参见问题7.23, 16.7 和18.2。
7.17 动态分配的内存一旦释放之后你就不能再使用,是吧?
是的。有些早期的malloc()文档提到释放的内存中的内容会“保留”,但这个欠考虑的保证并不普遍而且也不是C标准要求的。
几乎没有那个程序员会有意使用释放的内存,但是意外的使用却是常有的事。考虑下面释放单链表的正确代码:
struct list *listp, *nextp;
for(listp = base; listp != NULL; listp = nextp) {
nextp = listp->next;
free(listp);
}
请注意如果在循环表达式中没有使用临时变量nextp,而使用listp=listp->next会产生什么恶劣后果。
参考资料: [K&R2,Sec.7.8.5 p. 167];[ISO,Sec.7.10.3];[Rationale,Sec.
4.10.3.2]; [H&S,Sec.16.2p.387];[CT&P, Sec. 7.10 p. 95] 。
7.18 为什么在调用free()之后指针没有变空?使用(赋值,比较)释放之后的指针有多么不安全?
当你调用free()的时候,传入指针指向的内存被释放,但调用函数的指针值可能保持不变,因为C的按值传参语义意味着被调函数永远不会永久改
变参数的值。参见问题4.4。
严格的讲,被释放的指针值是无效的,对它的任何使用,即使没有解参照,也可能带来问题,尽管作为一种实现质量的表现,多数实现都不会对无伤
大雅的无效指针使用产生例外。
参考资料:[ISO,Sec.7.10.3];[Rationale, Sec. 3.2.2.3] 。
7.19 当我malloc()为一个函数的局部指针分配内存时,我还需要用free()明确的释放吗?
是的。记住指针和它所指向的东西是完全不同的。局部变量在函数返回时就会释放,但是在指针变量这个问题上,这表示指针被释放,而不是它
所指向的对象。用malloc()分配的内存直到你明确释放它之前都会保留在那里。一般地,对于每一个malloc()都必须有个对应的free()调用。
7.20 我在分配一些结构,它们包含指向其它动态分配的对象的指针。我在释放结构的时候,还需要释放每一个下级指针吗?
是的。一般地,你必须分别向free()传入malloc()返回的每一个指针,仅仅一次(如果它的确要被释放的话)。一个好的经验法则是对于程序中的
每一个malloc()调用,你都可以找到一个对应的free()调用以释放malloc()分配的内存。
参见问题7.21。
7.21 我必须在程序退出之前释放分配的所有内存吗?
你不必这样做。一个真正的操作系统毫无疑问会在程序退出的时候回收所有的内存和其它资源。然而,有些个人电脑据称不能可靠地释放内存,
从ANSI/ISOC的角度来看这不过是一个“实现的质量问题”。
参考资料:[ISO,Sec.7.10.3.2]。
7.22 我有个程序分配了大量的内存,然后又释放了。但是从操作系统看,内存的占用率却并没有回去。
多数malloc/free的实现并不把释放的内存返回操作系统,而是留着供同一程序的后续malloc()使用。
7.23 free() 怎么知道有多少字节需要释放?
malloc/free的实现会在分配的时候记下每一块的大小,所以在释放的时候就不必再考虑了。
7.24 那么我能否查询malloc包,可分配的最大块是多大?
不幸的是,没有标准的或可移植的办法。某些编译器提供了非标准的扩展。
7.25 向realloc()的第一个参数传入空指针合法吗?你为什么要这样做?
ANSIC批准了这种用法,以及相关的realloc(...,0),用于释放,尽管一些早期的实现不支持,因此可能不完全可移植。向realloc()传入置空的指
针可以更容易地写出自开始的递增分配算法。
参考资料:[ISO,Sec.7.10.3.4];[H&S, Sec. 16.3 p. 388] 。
7.26 calloc()和malloc()有什么区别?利用calloc的零填充功能安全吗?free()可以释放calloc()分配的内存吗,还是需要一个cfree()?
calloc(m,n)本质上等价于
p = malloc(m * n);
memset(p, 0, m * n);
填充的零是全零,因此不能确保生成有用的空指针值或浮点零值(参见第5 章)。free()可以安全地用来释放calloc()分配的内存。
参考资料:[ISO,Sec.7.10.3to7.10.3.2];[H&S, Sec. 16.1 p. 386, Sec. 16.2 p. 386];[PCS,Sec.11pp.141,142]。
7.27 alloca()是什么?为什么不提倡使用它?
在调用alloca()的函数返回的时候,它分配的内存会自动释放。也就是说,用alloca分配的内存在某种程度上局部于函数的“堆栈帧”或上下文
中。
alloca()不具可移植性,而且在没有传统堆栈的机器上很难实现。当它的返回值直接传入另一个函数时会带来问题,如fgets(alloca
(100),100,stdin)。
由于这些原因,alloca()不合标准,不宜使用在必须广泛移植的程序中,不管它可能多么有用。既然C99支持变长数组(VLA),它可以用来更好的完
成alloca()以前的任务。
参见问题7.19。
参考资料:[Rationale,Sec.4.10.3]。
第8 章
字符和字符串
8.1 为什么strcat(string,’!’);不行?
字符和字符串的区别显而易见,而strcat()用于连接字符串。
C中的字符用它们的字符集值对应的小整数表示,参见下边的问题8.4。字符串用字符数组表示;通常你操作的是字符数组的第一个字符的指针。
二者永远不能混用。要为一个字符串增加!,需要使用
strcat(string, "!");
参见问题1.13, 7.2 和16.6。
参考资料:[CT&P,Sec.1.5pp.9-10]。
8.2 我在检查一个字符串是否跟某个值匹配。为什么这样不行?char*string;...if(string=="value"){ /*stringmatches”value”
*/...}
C中的字符串用字符的数组表示,而C语言从来不会把数组作为一个整体操作(赋值,比较等)。上面代码段中的==操作符比较的是两个指针——指
针变量string的值和字符串常数“value”的指针值——看它们是否相等,也就是说,看它们是否指向同一个位置。它们可能并不相等,所以比较
决不会成功。
要比较两个字符串,一般使用库函数strcmp():
if(strcmp(string, "value") == 0) {
/* string matches "value" */
}
8.3 如果我可以写chara[]="Hello,world!";为什么我不能写chara[14];a="Hello,world!";
字符串是数组,而你不能直接用数组赋值。可以使用strcpy()代替:
strcpy(a, "Hello, world!");
参见问题1.13, 4.1 和7.2。
43
8.4 我怎么得到对应字符的数字(字符集)值,或者相反?
在C语言中字符用它们的字符集值对应的小整数表示。因此,你不需要任何转换函数:如有你有字符,你就有它的值。
数字字符和它们对应的0-9的数字之间相互转换时,加上或减去常数’0’,也就是说,’0’的字符值。
参见问题13.1 和20.8。
8.5 我认为我的编译器有问题:我注意到sizeof(’a’)是2而不是1(即,不是sizeof(char))。
可能有些令人吃惊,C语言中的字符常数是int型,因此sizeof(’a’)是sizeof(int),这是另一个与C++不同的地方。参见问题7.11。
参考资料:[ISO,Sec.6.1.3.4];[H&S, Sec. 2.7.3 p. 29] 。
第9 章
布尔表达式和变量
9.1 C语言中布尔值的候选类型是什么?为什么它不是一个标准类型?我应该用#de.ne或enum定义true和false值吗?
C语言没有提供标准的布尔类型,部分因为选一个这样的类型涉及最好由程序员决定的空间/时间折衷。(使用int可能更快,而使用char可能更节
省数据空间。然而,如果需要和int反复转换,那么小类型也可能生成更大或更慢的代码。)
使用#de.ne还是枚举常数定义true/false可以随便,无关大雅(参见问题
2.16和17.8)。使用以下任何一种形式
#define TRUE 1 #define YES 1
#define FALSE 0 #define NO 0
enum bool {false, true}; enum bool {no, yes};
或直接使用1和0,只要在同一程序或项目中一致即可。如果你的调试器在查看变量的时候能够显示枚举常量的名字,可能使用枚举更好。
有些人更喜欢这样的定义
#define TRUE (1==1)
#define FALSE (!TRUE)
或者定义这样的“辅助”宏
#define Istrue(e) ((e) != 0)
但这样于事无益,参见下边的问题9.2, 5.9 和10.1。
9.2 因为在C语言中所有的非零值都被看作“真”,是不是把TRUE定义为1很危险?如果某个内置的函数或关系操作符“返回”不是1的其它
值怎么办?
C语言中的确任何非零值都都被看作真,但这仅限于“输入”,也就是说,仅限于需要布尔值的地方。内建操作符生成布尔值时,可以保证为1或0
。因此,这样的测试
if((a == b) == TRUE)
能如愿运行(只要TRUE为1),但显然这很傻。事实上,跟TRUE和FALSE的跟TRUE和FALSE的显示比较都不合适,因为有些库函数(如isupper
(),isalpha()等)在成功时返回非零值,但不一定为1。(再说,如果你认为“if((a==b)
45
==TRUE)”比“if(a==b)”好,为什么就此打住呢?为什么不使用“if(((a==b)==TRUE)==TRUE)”呢?)一般规则是只在向布尔变量赋值或函数参
数中才使用TRUE和FALSE(或类似的东西),或者用于函数的返回值,但决不用于比较。
预处理宏TRUE和FALSE(当然还有NULL)只是用于增加代码可读性,而不是因为其值可能改变。(参见问题5.3 和5.8。)
尽管使用TRUE和FALSE这样的宏(或者YES和NO)看上去更清楚,布尔值和定义在C语言中的复杂性让很多程序员觉得TRUE和FALSE宏不过更令人迷
惑,因而更喜欢使用1和0。参见问题5.7。
参考资料:[K&R1,Sec.2.6p.39,Sec.2.7p.41];[K&R2, Sec. 2.6 p. 42, Sec. 2.7 p. 44, Sec. A7.4.7 p. 204, Sec. A7.9 p. 206];
[ISO,Sec.6.3.3.3, Sec. 6.3.8, Sec. 6.3.9, Sec. 6.3.13, Sec. 6.3.14, Sec. 6.3.15, Sec. 6.6.4.1, Sec. 6.6.5];
[H&S,Sec.7.5.4pp.196-7,Sec.7.6.4pp.207-8,Sec.7.6.5pp.208-9,Sec.7.7pp.217-8,Sec.7.8pp.218-9,Sec.8.5pp.238-9,Sec.8.6pp.241-4];
“WhattheTortoiseSaidtoAchilles”。
9.3 当p是指针时,if(p)是合法的表达式吗?
是的.参见问题5.3。
第10章
C 预处理器
10.1 这些机巧的预处理宏:#de.nebegin{ #de.ne end } 你觉得怎么样?
参见第17 章。
10.2 怎么写一个一般用途的宏交换两个值?
对于这个问题没有什么好的答案。如果这两个值是整数,可以使用异或的技术,但是这对浮点值或指针却不行,对同一个值也无能为力。(参见问
题3.4 和
20.14。)如果希望这个宏用于任何类型(通常的目标),那么它不能使用临时变量,因为不知道需要什么类型的临时变量(即使知道也难以找出一
个名字),而且标准C也没有提供typeof操作符。
最好的全面解决方案可能就是忘掉宏这回事,除非你还准备把类型作为第三个参数传入。
10.3 书写多语句宏的最好方法是什么?
通常的目标是书写一个象包含一个单独的函数调用语句的宏。这意味着“调用者”需要提供最终的分号,而宏体则不需要。因此宏体不能为简
单的括弧包围的复合语句,因为如果这样,调用的时候就会发生语法错(明显是一个单独语句,但却多了一个额外的分号),就像在if/else语句的
if分支中多了一个else分句一样。
所以,传统的结局方案就是这样使用:
#define MACRO(arg1, arg2) do { /
/* declarations */ /
stmt1; /
stmt2; /
/* ... */ /
}while(0)/*没有结尾的;*/当调用者加上分号后,宏在任何情况下都会扩展为一个单独的语句。优化的编译器会去掉条件为0的“无效”测试或
分支,尽管lint可能会警告。如果宏体内的语句都是简单语句,没有声明或循环,那么还有一种技术,就是写一个单独的,用一个或多个逗号操作
符分隔的表达式。例如,问题10.22 的第一个DEBUG()宏。这种技术还可以“返回”一个值。参考资料:[H&S,Sec.3.3.2p.45];[CT&P, Sec. 6.3
pp. 82-3] 。
47
10.4 我第一次把一个程序分成多个源文件,我不知道该把什么放到.c文件,把什么放到.h文件。(“.h”到底是什么意思?)
作为一般规则,你应该把这些东西放入头(.h)文件中:
. 宏定义(预处理#de.nes)
. 结构、联合和枚举声明
. typedef声明
. 外部函数声明(参见问题1.4)
. 全局变量声明
当声明或定义需要在多个文件中共享时,尤其需要把它们放入头文件中。特
别是,永远不要把外部函数原型放到.c文件中。参见问题1.3。另一方面,如果定义或声明为一个.c文件私有,则最好留在.c文件中。参见问题
1.3 和10.5。参考资料:[K&R2,Sec.4.5pp.81-2];[H&S,Sec.9.2.3p.267];[CT&P,Sec.
4.6 pp. 66-7] 。
10.5 一个头文件可以包含另一头文件吗?
这是个风格问题,因此有不少的争论。很多人认为“嵌套包含文件”应该避免:盛名远播的“印第安山风格指南”(IndianHillStyleGuide,参见
问题17.7) 对此嗤之以鼻;它让相关定义更难找到;如果一个文件被包含了两次,它会导致重复定义错误;同时他会令make.le的人工维护十分困
难。另一方面,它使模块化使用头文件成为一种可能(一个头文件可以包含它所需要的一切,而不是让每个源文件都包含需要的头文件);类似
grep的工具(或tags文件)使搜索定义十分容易,无论它在哪里;一种流行的头文件定义技巧是:
#ifndefHFILENAME_USED#defineHFILENAME_USED...头文件内容...#endif
每一个头文件都使用了一个独一无二的宏名。这令头文件可自我识别,以便可以安全的多次包含;而自动Make.le维护工具(无论如何,在大型项
目中都是必不可少的)可以很容易的处理嵌套包含文件的依赖问题。参见问题17.8。
参考资料:[Rationale,Sec.4.1.2]。
10.6 #include<>和#include""有什么区别?
<>语法通常用于标准或系统提供的头文件,而""通常用于程序自己的头文件。
10.7 完整的头文件搜索规则是怎样的?
准确的的行为是由实现定义的,这就是应该有文档说明;参见问题11.32。通常,用<>括起来的头文件会先在一个或多个标准位置搜索。用""括
起来的头文件会首先在“当前目录”中搜索,然后(如果没有找到)再在标准位置搜索。
参考资料:[K&R2,Sec.A12.4p.231];[ISO,Sec.6.8.2];[H&S,Sec.3.4p.55]。
10.8 我在文件的第一个声明就遇到奇怪的语法错误,但是看上去没什么问题。
可能你包含的最后一个头文件的最后一行缺一个分号。参见问题2.14, 11.28 和16.1。
10.9 我包含了我使用的库函数的正确头文件,可是连接器还是说它没有定义。
参见问题13.18。
10.10 我在编译一个程序,看起来我好像缺少需要的一个或多个头文件。谁能发给我一份?
根据“缺少的”头文件的种类,有几种情况。
如果缺少的头文件是标准头文件,那么你的编译器有问题。你得向你的供货商或者精通你的编译器的人求助。
对于非标准的头文件问题更复杂一些。有些完全是系统或编译器相关的。某些是完全没有必要的,而且应该用它们的标准等价物代替。例如,用
<stdlib.h>代替<malloc.h>。其它的头文件,如跟流行的附加库相关的,可能有相当的可移植性。
标准头文件存在的部分原因就是提供适合你的编译器,操作系统和处理器的定义。你不能从别人那里随便拷贝一份就指望它能工作,除非别人跟
你使用的是同样的环境.你可能事实上有移植性问题(参见第19 章)或者编译器问题。否则,参见问题18.18。
10.11 我怎样构造比较字符串的#if预处理表达式?
你不能直接这样做;#if预处理指令只处理整数。有一种替代的方法是定义多个整数值不一样的宏,用它们来实现条件比较。
参见问题20.15。
参考资料:[K&R2,Sec.4.11.3p.91;ISOSec.6.8.1];[H&S,Sec.7.11.1p.225]。
10.12 sizeof操作符可以用于#if预编译指令中吗?
不行。预编译在编译过程的早期进行,此时尚未对类型名称进行分析。作为替代,可以考虑使用ANSI的<limits.h>中定义的常量,或者使用“配
置”(con.gure)脚本。更好的办法是,书写与类型大小无关的代码;参见问题1.1。
10.13 我可以在#include行里使用#ifdef来定义两个不同的东西吗?
不行。你不能“让预处理器自己运行”。你能做的就是根据#ifdef设置使用两个完全不同的单独#de.ne行之一。
参考资料:[ISO,Sec.6.8.3,Sec.6.8.3.4];[H&S, Sec. 3.2 pp. 40-1] 。
10.14 对typdef的类型定义有没有类似#ifdef的东西?
不幸的是,没有。你必须保存一套预处理宏(如MYTYPEDEFINED)来记录某个类型是否用typdef声明了。参见问题10.12。
参考资料:[ISO,Sec.5.1.1.2,Sec.6.8.1];[H&S, Sec. 7.11.1 p. 225] 。
10.15 我如何用#if表达式来判断机器是高字节在前还是低字节在前?
恐怕你不能。(预处理运算仅仅使用长整型,而且没有寻址的概念。)你是否真的需要明确知道机器的字节顺序呢?通常写出与字节顺序无关的
代码更好。
参考资料:[ISO,Sec.6.8.1];[H&S, Sec. 7.11.1 p. 225] 。
10.16 我得到了一些代码,里边有太多的#ifdef。我不想使用预处理器把所有的#include和#ifdef都扩展开,有什么办法只保留一种条件的代
码呢?
有几个程序unifdef,rmifdef和scpp(selectiveCpreprocessor)正是完成这种工作的。参见问题18.18。
10.17 如何列出所有的预定义标识符?
尽管这是种常见的需求,但却没有什么标准的办法。gcc提供了和-E一起使用的-dM选项,其它编译器也有类似的选项。如果编译器文档没有帮助
,那么可以使用类似Unix字符串工具的程序取出编译和预处理生成的可执行文件中的可打印字符串。请注意,很多传统的系统相关的预定义标识
符(如“unix”)并不标准(因为和用户的名字空间冲突),因而会被删除或改名。
10.18 我有些旧代码,试图用这样的宏来构造标识符#de.nePaste(a,b)a/**/b但是现在不行了。
这是有些早期预处理器实现(如Reiser)的未公开的功能,注释完全消失,因而可以用来粘结标识符。但ANSI确认(如K&R所言)注释用空白代替。
然而对粘结标识符的需求却十分自然和广泛,因此ANSI引入了一个明确定义的标识符粘结操作符——##,它可以象这样使用
#define Paste(a, b) a##b
参见问题11.18。
参考资料:[ISO,Sec.6.8.3.3];[Rationale,Sec.3.8.3.3];[H&S,Sec.3.3.9p.52]。
10.19 为什么宏#de.neTRACE(n)printf("TRACE:%d/n",n)报出警告“用字符串常量代替宏”?它似乎应该把TRACE(count);扩展为printf
("TRACE:%d/count",count);
参见问题11.19。
10.20 使用#操作符时,我在字符串常量内使用宏参数有问题。
参见问题11.18 和11.19。
10.21 我想用预处理做某件事情,但却不知道如何下手。
C的预处理器并不是一个全能的工具。注意,甚至都不能保证有一个单独的程序。与其强迫它做一些不适当的事情,还不如考虑自己写一个专用
的预处理工具。你可以很容易就得到一个类似make(1)那样的工具帮助你自动运行。
如果你要处理的不是C程序,可以考虑使用一个多用途的预处理器。在多数Unix系统上有m4工具。
10.22 怎样写参数个数可变的宏?
一种流行的技巧是用一个单独的用括弧括起来的的“参数”定义和调用宏,参数在宏扩展的时候成为类似printf()那样的函数的整个参数列表
。
#define DEBUG(args) (printf("DEBUG: "), printf args)
if(n != 0) DEBUG(("n is %d/n", n));
明显的缺陷是调用者必须记住使用一对额外的括弧。
gcc有一个扩展可以让函数式的宏接受可变个数的参数。但这不是标准。另一种可能的解决方案是根据参数个数使用多个宏(DEBUG1,DEBUG2,等
等),或者用逗号玩个这样的花招:
#define DEBUG(args) (printf("DEBUG: "), printf(args))
#define _ ,
DEBUG("i = %d" _ i);
C99引入了对参数个数可变的函数式宏的正式支持。在宏“原型”的末尾加上符号...(就像在参数可变的函数定义中),宏定义中的伪宏VAARGS
就会在调用是替换成可变参数。
最后,你总是可以使用真实的函数,接受明确定义的可变参数。参见问题15.4 和15.5。如果你需要替换宏,使用一个函数和一个非函数式宏,如
#de.neprintfmyprintf。
参考资料:[C9X,Sec.6.8.3,Sec.6.8.3.1]。
第11章
ANSI/ISO 标准C
11.1 什么是“ANSIC标准”?
1983年,美国国家标准协会(ANSI)委任一个委员会X3J11对C语言进行标准化。经过长期艰苦的过程,该委员会的工作于1989年12月14日正式被批
准为ANSX3.159-1989并于1990年春天颁布。ANSIC主要标准化了现存的实践,同时增加了一些来自C++的内容(主要是函数原型)并支持多国字符
集(包括备受争议的三字符序列)。ANSIC标准同时规定了C运行期库例程的标准。
一年左右以后,该标准被接受为国际标准,ISO/IEC9899:1990,这个标准甚至在美国国内(在这里它被称作ANSI/ISO9899-1990[1992])代替了早先
的X3.159。作为一个ISO标准,它会以发行技术勘误和标准附录的形式不断更新。
1994年,技术勘误1(TC1)修正标准中40处地方,多数都是小的修改或明确,而标准附录1(NA1)增加了大约50页的新材料,多数是规定国际化支持的
新库函数。1995年,TC2增加了一些更多的小修改。
最近,该标准的一个重大修订,“C99”,已经完成并被接受。
该标准的数个版本,包括C99和原始的ANSI标准,都包括了一个“基本原理”(Rational),解释它的许多决定并讨论了很多细节问题,包括本文中
提及的某些内容。
11.2 我如何得到一份标准的副本?
可以用18美元从www.ansi.org 联机购买一份电子副本(PDF)。在美国可以从一下地址获取印刷版本
American National Standards Institute
11 W. 42nd St., 13th floor
New York, NY 10036 USA
(+1) 212 642 4900
和
Global Engineering Documents
15 Inverness Way E
Englewood, CO 80112 USA
(+1) 303 397 2715
(800) 854 7179 (U.S. & Canada)
其它国家,可以联系适当的国内标准组织,或Geneva的ISO,地址是:
53
ISO Sales
Case Postale 56
CH-1211 Geneve 20
Switzerland
或者参见URLhttp://www.iso.ch 或查阅comp.std.internatFAQ列表,Stan-dards.Faq。
由HerbertSchild注释的名不副实的《ANSIC标准注解》包含ISO9899的多数内容;这本书由Osborne/McGraw-Hill出版,ISBN为0-07-881952-0,在
美国售价大约40美圆。有人认为这本书的注解并不值它和官方标准的差价:里边错漏百出,有些标准本身的内容甚至都不全。网上又很多人甚至
建议完全忽略里边的注解。在http://www.lysator.liu.se/c/schildt.html 可以找到CliveFeather对该注解的评论(“注解的注解”)。
“ANSI基本原理”的原本文本可以从ftp://ftp.uu.net/doc/standards/ansi/ X3.159-1989 匿名ftp下载(参见问题18.18),也可以在万维网从
http://www. lysator.liu.se/c/rat/title.html 得到。这本基本原理由SiliconPress出版,ISBN为0-929306-07-4。
C9X的公众评论草稿可以从ISO/IECJTC1/SC22/WG14的网站得到,地址为http://www.dkuug.dk/JTC1/SC22/WG14/ 。
参见问题11.3。
11.3 我在哪里可以找到标准的更新?
你可以在以下网站找到相关信息(包括C9X草案):http://www.lysator. liu.se/c/index.html, http://www.dkuug.dk/JTC1/SC22/WG14/ 和
http://www. dmk.com/。
11.4 很多ANSI编译器在遇到以下代码时都会警告类型不匹配。ex-ternintfunc(.oat);intfunc(x).oatx;{ ...
你混用了新型的原型声明“externintfunc(.oat);”和老式的定义“intfunc(x).oatx;”。通常这两种风格可以混同,但是这种情况下不行。
旧的C编译器(包括未使用原型和变长参数列表的ANSIC)会“放宽”传入函数的某些参数。浮点数被升为双精度浮点数,字符和段整型被升为整
型。对于旧式的函数定义,参数值会在被调函数的内部自动转换为对应的较窄的类型,如果在函数中那样声明了。
这个问题可以通过在定义中使用新型的语法一致性:
int func(float x) { ... }
或者把新型原型声明改成跟旧式定义一致。
extern int func(double);
这种情况下,如果可能,最好把就是定义也改成使用双精度数。
毫无疑问,在函数参数和返回值中避免使用“窄的”(char,shortint和.oat)类型要安全得多。
参见问题1.8。
参考资料:[K&R1,Sec.A7.1p.186];[K&R2,Sec.A7.3.2p.202];[ISO,Sec.6.3.2.2, Sec. 6.5.4.3];[Rationale,Sec.3.3.2.2, Sec. 3.5.4.3];
[H&S,Sec.9.2 pp. 265-7, Sec. 9.4 pp. 272-3] 。
11.5 能否混用旧式的和新型的函数语法?
这样做是合法的,但是需要小心为妙,特别参见问题11.4。但是,现代的做法是在声明和定义的时候都是用原型形式。旧式的语法被认为已经废
弃,所以某一天对它的官方支持可能会取消。
参考资料:[ISO,Sec.6.7.1,Sec.6.9.5];[H&S, Sec. 9.2.2 pp. 265-7, Sec. 9.2.5 pp. 269-70] 。
11.6 为什么声明externintf(structx*p);报出了一个奇怪的警告信息“结构x在参数列表中声明”?
与C语言通常的作用范围规则大相径庭的是,在原型中第一次声明(甚至提到)的结构不能和同一源文件中的其它结构兼容,它在原型的结束出就
超出了作用范围。
要解决这个问题,在同一源文件的原型之前放上这样的声明:
struct x;
它在文件范围内提供了一个不完整的结构x的声明,这样,后续的用到结构x的声明至少能够确定它们引用的是同一个结构x。
参考资料:[ISO,Sec.6.1.2.1,Sec.6.1.2.6,Sec.6.5.2.3]。
11.7 我不明白为什么我不能象这样在初始化和数组维度中使用常量: const int n = 5; int a[n];
const限定词真正的含义是“只读的”;用它限定的对象是运行时(同常)不能被赋值的对象。因此用const限定的对象的值并不完全是一个真正
的常量。在这点上C和C++不一样。如果你需要真正的运行时常量,使用预定义宏#de.ne(或enum)。
参考资料:[ISO,Sec.6.4];[H&S, Secs. 7.11.2,7.11.3 pp. 226-7] 。
11.8 既然不能修改字符串常量,为什么不把它们定义为字符常量的数组?
一个原因是太多的代码包含
char *p = "Hello, world!";
这样并不正确的语句。这样的语句要受诊断信息的困扰,但真正的问题却出现在改变p所指目的的任何企图。
参见问题1.13。
11.9 “constchar*p”和“char*constp”有何区别?
“constchar*p”(也可以写成“charconst*p”)声明了一个指向字符常量的指针,因此不能改变它所指向的字符;“char*constp”声明一个指
向(可变)字符的指针常量,就是说,你不能修改指针。
“从里到外”看就可以理解它们;参见问题1.7。
参考资料:[ISO,Sec.6.5.4.1];[Rationale,Sec.3.5.4.1];[H&S,Sec.4.4.4p.81]。
11.10 为什么我不能向接受constchar**的函数传入char**?
你可以向接受const-T的指针的地方传入T的指针(任何类型T都适用)。但是,这个允许在带修饰的指针类型上轻微不匹配的规则(明显的例外)却
不能递归应用,而只能用于最上层。
如果你必须赋值或传递除了在最上层还有修饰符不匹配的指针,你必须明确使用类型转换(本例中,使用(constchar**)),不过,通常需要使用这
样的转换意味着还有转换所不能修复的深层次问题。
参考资料:[ISO,Sec.6.1.2.6,Sec.6.3.16.1,Sec.6.5.3];[H&S,Sec.7.9.1pp.221-2]。
11.11 怎样正确声明main()?
intmain(),intmain(void)或者intmain(intargc,char*argv[])(显然argc和argv的拼写可以随便)。参见问题11.12 到11.16。
参考资料:[ISO,Sec.5.1.2.2.1,Sec.G.5.1];[H&S,Sec.20.1p.416];[CT&P,Sec. 3.10 pp. 50-51] 。
11.12 我能否把main()定义为void,以避免扰人的“main无返回值”警告?
不能。main()必须声明为返回int,且没有参数或者接受适当类型的两个参数。如果你调用了exit()但还是有警告信息,你可能需要插入一条冗
余的return语句(或者使用某种“未到达”指令,如果有的话)。
把函数声明为void并不仅仅关掉了警告信息:它可能导致与调用者(对于main(),就是C运行期初始代码)期待的不同的函数调用/返回顺序。
注意,这里讨论的main()是相对于“宿体”的实现;它们不适用于“自立”的实现,因为它们可能连main()都没有。但是,自立的实现相对比较
少,如果你在使用这样的系统,你也许已经知道了。如果你从没听说过它们之间的不同,你可能正在使用“宿体”的实现,那我们的讨论就适用
。
参考资料:[ISO,5.1.2.2.1,Sec.G.5.1];[H&S,Sec.20.1p.416];[CT&P,Sec.
3.10 pp. 50-51] 。
11.13 可main()的第三个参数envp是怎么回事?
这是一个(尽管很常见但却)非标准的扩展。如果你真的需要用getenv()函数提供的标准方法之外的办法读写环境变量,可能使用全局变量
environ会更好(尽管它也同样并不标准)。
参考资料:[ISO,Sec.G.5.1];[H&S, Sec. 20.1 pp. 416-7] 。
11.14 我觉得把main()声明为void不会失败,因为我调用了exit()而不是return,况且我的操作系统也忽略了程序的退出/返回状态。
这跟main()函数返回与否,或者是否使用返回状态都没有关系;问题是如果main()声明得不对,它的调用者(运行期初始代码)可能甚至都不能正
确调用它(因为可能产生调用习惯冲突;参见问题11.12)。
你的操作系统可能会忽略退出状态,而voidmain()在你那里也可能可行,但这不可移植而且不正确。
11.15 那么到底会出什么问题?真的有什么系统不支持voidmain()吗?
有人报告用BC++4.5编译的使用voidmain()的程序会崩溃。某些编译器(包括DECCV4.1和打开某些选项的gcc)会对voidmain()提出警告。
11.16 我一直用的那本书《熟练傻瓜C语言》总是使用voidmain()。
可能这本书的作者把自己也归为目标读者的一员。很多书不负责任地在例子中使用voidmain(),并宣称这样是正确的。但他们错了。
11.17 从main()中,exit(status)和返回同样的status真的等价吗?
是也不是。标准声称它们等价。但是如果在退出的时候需要使用main()的局部数据,那么从main()return恐怕就不行了;参见问题16.4。少数非
常古老不符合标准的系统可能对其中的某种形式有问题。最后,在main()函数的递归调用时,二者显然不能等价。
参考资料:[K&R2,Sec.7.6pp.163-4];[ISO, Sec. 5.1.2.2.3] 。
11.18 我试图用ANSI“字符串化”预处理操作符#向信息中插入符号常量的值,但它字符串化的总是宏的名字而不是它的值。
你可以用下面这样的两步方法迫使宏既字符串化又扩展:
#define Str(x) #x
#define Xstr(x) Str(x)
#define OP plus
char *opname = Xstr(OP);
这段代码把opname置为“plus”而不是“OP”。
在使用符号粘接操作符##连接两个宏的值(而不是名字)时也要采用同样的“迂回战术”。
参考资料:[ISO,Sec.6.8.3.2,Sec.6.8.3.5]。
11.19 警告信息“warning:macroreplacementwithinastringlit-eral”是什么意思?
有些ANSI前的编译器/预处理器把下面这样的宏
#define TRACE(var, fmt) printf("TRACE: var = fmt/n", var)
解释为
TRACE(i, %d);
这样的调用会被扩展为
printf("TRACE: i = %d/n", i);
换言之,字符串常量内部也作了宏参数扩展。
K&R和标准C都没有定义这样的宏扩展。当你希望把宏参数转成字符串时,你可以使用新的预处理操作符#和字符串常量连接(ANSI的另一个新功
能):
#define TRACE(var, fmt) /
printf("TRACE: " #var " = " #fmt "/n", var)
参见问题11.18。
参考资料:[H&S,Sec.3.3.8p.51]。
11.20 在我用#ifdef去掉的代码里出现了奇怪的语法错误。
在ANSIC中,被#if,#ifdef或#ifndef“关掉”的代码仍然必须包含“合法的预处理符号”。这意味着字符"和’必须像在真正的C代码中那样严
格配对,且这样的配对不能跨行。特别要注意缩略语中的撇号看起来很像字符常量的开始。因此,自然语言的注释和伪代码必须写在“正式的”
注释分界符/*和*/中。但是请参见问题20.18 和10.21。
参考资料:[ISO,Sec.5.1.1.2,Sec.6.1];[H&S, Sec. 3.2 p. 40] 。
11.21 #pragma是什么,有什么用?
#pragam指令提供了一种单一的明确定义的“救生舱”,可以用作各种(不可移植的)实现相关的控制和扩展:源码表控制、结构压缩、警告去除(
就像lint的老/*NOTREACHED*/注释),等等。
参考资料:[ISO,Sec.6.8.6];[H&S, Sec. 3.7 p. 61] 。
11.22 “#pragma once” 是什么意思?我在一些头文件中看到了它。
这是某些预处理器实现的扩展用于使头文件自我识别;它跟问题10.5 中讲到的#ifndef技巧等价,不过移植性差些。
11.23 a[3]="abc";合法吗?它是什么意思?
尽管只在极其有限的环境下有用,可它在ANSIC(可能也包括一些ANSI之前的系统)中是合法的。它声明了一个长度为3的数组,把它的三个字符初
始化为’a’,’b’和’c’,但却没有通常的’/0’字符。因此该数组并不是一个真正的C字符串从而不能用在strcpy,printf%s等当中。
多数时候,你应该让编译器计算数组初始化的初始值个数,在初始值“abc”中,计算得长度当然应该是4。
参考资料:[ISO,Sec.6.5.7];[H&S, Sec. 4.6.4 p. 98] 。
11.24 为什么我不能对void*指针进行运算?
编译器不知道所指对象的大小。在作运算之前,把指针转化为char*或你准备操作的其它指针类型,但是请参考问题4.3。
参考资料:[ISO,Sec.6.1.2.5,Sec.6.3.6];[H&S, Sec. 7.6.2 p. 204] 。
11.25 memcpy()和memmove()有什么区别?
如果源和目的参数有重叠,memmove()提供有保证的行为。而memcpy()则不能提供这样的保证,因此可以实现得更加有效率。如果有疑问,最好使
用memmove()。
参考资料: [K&R2,Sec.B3 p. 250];[ISO,Sec.7.11.2.1, Sec. 7.11.2.2]; [Rationale,Sec.4.11.2];[H&S,Sec.14.3pp.341-2];
[PCS,Sec.11pp.165-6]。
11.26 malloc(0)有什么用?返回一个控指针还是指向0字节的指针?
ANSI/ISO标准声称它可能返回任意一种;其行为由实现定义。参考资料:[ISO,Sec.7.10.3];[PCS, Sec. 16.1 p. 386] 。
11.27 为什么ANSI标准规定了外部标示符的长度和大小写限制?
问题在于连接器既不受ANSI/ISO标准的控制也不遵守C编译器开发者的规定。限制仅限于标识符开始的几个字符而不是整个标识符。在原来的
ANSI标准中限制为6个字符,但在C99中放宽到了31个字符。
参考资料: [ISO,Sec.6.1.2, Sec. 6.9.1];[Rationale,Sec.3.1.2];[C9X,Sec.
6.1.2]; [H&S,Sec.2.5pp.22-3]。
11.28 我的编译对最简单的测试程序报出了一大堆的语法错误。
可能是个ANSI前的编译器,不能接受函数原型或类似的东西。
参见问题1.11, 10.8, 11.29 和16.1。
11.29 为什么有些ASNI/ISO标准库函数未定义?我明明使用的就是ANSI编译器。
你很可能有一个接受ANSI语法的编译器,但并没有安装兼容ANSI的头文件或运行库。事实上,这种情形在使用非供货商提供的编译器,如gcc时非
常常见。参见问题11.28, 12.22 和13.19。
11.30 谁有把旧的C程序转化为ANSIC或相反的工具,或者自动生成原型的工具?
有两个程序protoize和unprotoize可以在有原型和无原型的函数定义和声明之间相互转换。这些程序不能完全完成“经典”C和ANSIC之间的转
换。这些程序是FSF的GNUC编译器发布的一部分;参加问题18.3。
GNUGhostScript包提供了一个叫ansi2knr的程序。
从ANSIC向旧式代码转化之前,请注意这样的转化不能总是正确和自动。ANSIC引入了K&RC没有提供的诸多新功能和复杂性。你得特别小心有原
型的函数调用;也可能需要插入明确的类型转换。参加问题11.4 和11.28。
存在几个类型生成器,其中多数都是对lint的修改。1992年3月在comp.sources.misc上发布了一个叫做CPROTO的程序。还有一个叫做
“cextract”的程序。很多供货商都会随他们的编译器提供类似的小工具。参见问题18.18。但在为“窄”参数的旧函数生成原型可要小心;参
加问题11.4。
11.31 为什么声称兼容ANSI的FrobozzMagicC编译器不能编译这些代码?我知道这些代码是ANSI的,因为gcc可以编译。
许多编译器都支持一些非标准的扩展,gcc尤甚。你能确认被拒绝的代码不依赖这样的扩展吗?通常用试验特定的编译器来确定一种语言的特性
是个坏主意;使用的标准可能允许变化,而编译器也可能有错。参见问题11.35。
11.32 人们好像有些在意实现定义(implementation-de.n-ed)、未明确(unspeci.ed)和无定义(unde.ned)行为的区别。它们的区别到底在哪
里?
简单地说:实现定义意味着实现必须选择某种行为并提供文档。未明确意味着实现必须选择某种行为但不必提供文档。未定义意味着任何事情
都可能发生。标准在任何情况下都不强加需求;前两种情况下,它有时建议一组可能的行为(也可能要求从中选择一种)。
注意既然标准对无定义行为没有强制要求,编译器就绝对可以做任何事情。特别地,对程序其它部分的正常运行没有任何保证;参见问题3.2, 有
一个相对简单的例子。
如果你对书写可移植代码有兴趣,你可以忽略它们的区别,因为通常你都希望
避免依赖三种行为中的任何一种。
参见问题3.8 和11.34。
第四种不那么严格定义的行为是“场景特定”(locale-speci.c)。
参考资料:[ISO,Sec.3.10,Sec.3.16,Sec.3.17];[Rationale, Sec. 1.6] 。
11.33 一个程序的“合法”,“有效”或“符合”到底是什么意思?
简单地说,标准谈到了三种符合:符合程序、严格符合程序和符合实现。
“符合程序”是可以由符合实现接受的程序。
“严格符合程序”是完全按照标准规定使用语言,不依赖任何实现定义、未确定或未定义功能的程序。
“符合实现”是按标准声称的实现的程序。参考资料:[ISO,Sec.];[Rationale, Sec. 1.7] 。
11.34 我很吃惊,ANSI标准竟然有那么多没有定义的东西。标准的唯一任务不就是让这些东西标准化吗?
某些构造随编译器和硬件的实现而变化,这一直是C语言的一个特点。这种有意的不严格规定可以让编译器生成效率更高的代码,而不必让所有
程序为了不合理的情况承担额外的负担。因此,标准只是把现存的实践整理成文。
编程语言标准可以看作是语言使用者和编译器实现者之间的协议。协议的一部分是编译器实现者同意提供,用户可以使用的功能。而其它部分
则包括用户同意遵守,编译器实现者认为会被最受的规则。只要双方都恪守自己的保证,程序就可以正确运行。如果任何一方违背它的诺言,则
结果肯定失败。
参见问题11.35。
参考资料:[Rationale,Sec.1.1]。
11.35 有人说i=i++的行为是未定义的,但是我刚在一个兼容ANSI的编译器上测试,得到了我希望的结果。
面对未定义行为的时候,包括范围内的实现定义行为和未确定行为,编译器可以做任何实现,其中也包括你所有期望的结果。但是依靠这个实现
却不明智。参加问题7.4, 11.31, 11.32 和11.34。
第12章
标准输入输出库
12.1 这样的代码有什么问题?charc;while((c=getchar())!=EOF)...
第一,保存getchar的返回值的变量必须是int型。getchar()可能返回任何字符值,包括EOF。如果把getchar的返回值截为char型,则正常的字符
可能会被错误的解释为EOF,或者EOF可能会被修改(尤其是char型为无符号的时候),从而永不出现。
参考资料: [K&R1,Sec.1.5 p. 14];[K&R2,Sec.1.5.1 p. 16];[ISO,Sec.6.1.2.5,Sec.7.9.1,Sec.7.9.7.5];
[H&S,Sec.5.1.3p.116,Sec.15.1,Sec.15.6];[CT&P,Sec.5.1p.70];[PCS, Sec. 11 p. 157] 。
12.2 我有个读取直到EOF的简单程序,但是我如何才能在键盘上输入那个“EOF”呢?
其实,你的C程序看到的EOF的值和你用键盘发出的文件结束按键组合之间没有任何直接联系。根据你的操作系统,你可能使用不同的按键组合来
表示文件结束,通常是Control-D或Control-Z。
12.3 为什么这些代码while(!feof(infp)){ fgets(buf, MAXLINE, infp); fputs(buf, outfp); } 把最后一行复制了两遍?
在C语言中,只有输入例程试图读并失败以后才能得到文件结束符。换言之,C的I/O和Pascal的不一样。通常你只需要检查输入例程的返回值,例
如,fgets()在遇到文件结束符的时候返回NULL。实际上,在任何情况下,都完全没有必要使用feof()。
参考资料: [K&R2,Sec.7.6 p. 164];[ISO,Sec.7.9.3, Sec. 7.9.7.1, Sec.
7.9.10.2]; [H&S,Sec.15.14p.382]。
12.4 我的程序的屏幕提示和中间输出有时显示在屏幕上,尤其是当我用管道向另一个程序输出的时候。
在输出需要显示的时候最好使用明确的.ush(stdout)调用,尤其是当显示的
63
文本没有/n结束符时。有几种机制会努力帮助你在“适当的时机”执行.ush,但这仅限于stdout为交互终端的时候。参见问题12.21。
参考资料:[ISO,Sec. 7.9.5.2] 。
12.5 我怎样不等待回车键一次输入一个字符?
参见问题19.1。
12.6 我如何在printf的格式串中输出一个’%’?我试过/%, 但是不行。
只需要重复百分号: %% 。
/%不行,因为反斜杠/ 是编译器的转义字符,而这里我们的问题最终是printf的转义字符。
参见问题19.21。
参考资料: [K&R1,Sec.7.3 p. 147];[K&R2,Sec.7.2 p. 154];[ISO,Sec.
7.9.6.1]。
12.7 有人告诉我在printf中使用%lf不正确。那么,如果scanf()需要%lf,怎么可以用在printf()中用%f输出双精度数呢?
printf的%f标识符的确既可以输出浮点数又可以输出双精度数。根据“缺省参数扩展”规则,不论范围内有没有原形都会在在类似printf的可
变长度参数列表中采用,浮点型的变量或扩展为双精度型,因此printf()只会看到双精度数。printf()的确接受%Lf,用于输出长双精度数。参见
问题12.11 和15.2。
参考资料:[K&R1,Sec.7.3pp.145-47,Sec.7.4pp.147-50];[K&R2, Sec.
7.2 pp.153-44,Sec.7.4pp.157-59];[ISO,Sec.7.9.6.1,Sec.7.9.6.2];[H&S, Sec.
15.8 pp.357-64,Sec.15.11pp.366-78];[CT&P,Sec.A.1pp.121-33]。
12.8 对于sizet那样的类型定义,当我不知道它到底是long还是其它类型的时候,我应该使用什么样的printf格式呢?
把那个值转换为一个已知的长度够大的类型,然后使用与之对应的printf格式。例如,输出某种类型的长度,你可以使用
printf("%lu", (unsigned long)sizeof(thetype));
12.9 我如何用printf实现可变的域宽度?就是说,我想在运行时确定宽度而不是使用%8d?
printf("%*d",width,x)就能达到你的要求。参见问题12.14。
参考资料: [K&R1,Sec.7.3];[K&R2,Sec.7.2];[ISO,Sec.7.9.6.1];[H&S,Sec.15.11.6];[CT&P,Sec.A.1]。
12.10 如何输出在千位上用逗号隔开的数字?金额数字呢?
<locale.h>提供了一些函数可以完成这些操作,但是没有完成这些任务的标准方法。printf()惟一一处对应locale的地方就是改变它的小数点
字符。
参考资料:[ISO,Sec.7.4];[H&S,Sec.11.6pp.301-4]。
12.11 为什么scanf("%d",i)调用不行?
传给scanf()的参数必须是指针。改为scanf("%d",&i)即可修正上面的问题。
12.12 为什么chars[30];scanf("%s",s);不用&也可以?
需要指针;并不表示一定需要&操作符。当你向scanf()传入一个指针的时候,你不需要使用&,因为不论是否带&操作符,数组总是以指针形式传入
函数的。参见问题6.3 和6.4。
12.13 为什么这些代码doubled;scanf("%f",&d);不行?
跟printf()不同,scanf()用%lf代表双精度数,用%f代表浮点数。参见问题
12.7。
12.14 怎样在scanf()格式串中指定可变的宽度?
不能:scanf()格式串中的星号表示禁止赋值。你可以使用ANSI的字符串化和字符连接完成同样的事情,或者你可以在运行时创建scanf格式串。
12.15 当我用“%d/n”调用scanf从键盘读取数字的时候,好像要多输入一行函数才返回。
可能令人吃惊, /n在scanf格式串中不表示等待换行符,而是读取并放弃所有的空白字符。参见问题12.18。
参考资料:[K&R2,Sec.B1.3pp.245-6];[ISO,Sec.7.9.6.2];[H&S,Sec.15.8pp. 357-64] 。
12.16 我用scanf%d读取一个数字,然后再用gets()读取字符串,但是编译器好像跳过了gets()调用!
scanf%d不处理结尾的换行符。如果输入的数字后边紧接着一个换行符,则换行符会被gets()处理。
作为一个一般规则,你不能混用scanf()和gets(),或任何其它的输入例程的调用;scanf对换行符的特殊处理几乎一定会带来问题。要么就用
scanf()处理所有的输入,要么干脆不用。
参见问题12.18 和12.20。
参考资料:[ISO,Sec.7.9.6.2];[H&S, Sec. 15.8 pp. 357-64] 。
12.17 我发现如果坚持检查返回值以确保用户输入的是我期待的数值,则scanf()的使用会安全很多,但有的时候好像会陷入无限循环。
在scanf()转换数字的时候,它遇到的任何非数字字符都会终止转换并被保留在输入流中。因此,除非采用了其它的步骤,那么未预料到的非数字
输入会不断“阻塞”scanf():scanf()永远都不能越过错误的非数字字符而处理后边的合法数字字符。如果用户在数字格式的scanf如%d或%f中
输入字符‘x’,那么提示后并用同样的scanf()调用重试的代码会立即遇到同一个’x’。
参见问题12.18。
参考资料:[ISO,Sec.7.9.6.2];[H&S, Sec. 15.8 pp. 357-64] 。
12.18 为什么大家都说不要使用scanf()?那我该用什么来代替呢?
scanf()有很多问题——参见问题12.15, 12.16 和12.17。而且,它的%s格式有着和gets()一样的问题(参见问题12.20) —— 很难保证接收缓
冲不溢出。
更一般地讲,scanf()的设计使用于相对结构化的,格式整齐的输入。设计上,它的名称就是来自于“scanformatted”。如果你注意到,它会告诉
你成功或失败,但它只能提供失败的大略位置,至于失败的原因,就无从得知了。对scanf()多得体的错误恢复几乎是不可能的;通常先用类似
fgets()的函数读入整行,然后再用sscanf()或其它技术解释。strtol(),strtok()和atoi()等函数通常有用;参见问题
13.4。如果你真的要用任何scanf的变体,你要确保检查返回值,以确定找到了期
待的值。而使用%s格式的时候,一定要小心缓冲区溢出。参考资料:[K&R2,Sec.7.4p.159]。
12.19 我怎样才知道对于任意的sprintf调用需要多大的目标缓冲区?怎样才能避免sprintf()目标缓冲区溢出?
当用于sprintf()的格式串已知且相对简单时,你有时可以预测出缓冲区的大小。如果格式串中包含一个或两个%s,你可以数出固定字符的个数
再加上对插入的字符串的strlen()调用的返回值。对于整形,%d输出的字符数不会超过
((sizeof(int) * CHAR_BIT + 2) / 3 + 1) /* +1 for ’-’ */
CHARBIT在<limits.h>中定义,但是这个计算可能有些过于保守了。它计算的是数字以八进制存储需要的字节数;十进制的存储可以保证使用同
样或更少的字节数。
当格式串更复杂或者在运行前未知的时候,预测缓冲区大小会变得跟重新实现sprintf一样困难,而且会很容易出错。有一种最后防线的技术,就
是fprintf()向一块内存区或临时文件输出同样的内容,然后检查fprintf的返回值或临时文件的大小,但请参见问题19.14, 并提防写文件错误
。
如果不能确保缓冲区足够大,你就不能调用sprintf(),以防缓冲区溢出后改写其它的内存区。如果格式串已知,你可以用%.Ns控制%s扩展的长度
,或者使用%.*s,参见问题12.9。
要避免溢出问题,你可以使用限制长度的sprintf()版本,即snprintf()。这样使用:
snprintf(buf, bufsize, "You typed /"%s/"", answer);
snprintf()在几个stdio库中已经提供好几年了,包括GNU和4.4bsd。在C99中已经被标准化了。
作为一个额外的好处,C99的snprintf()提供了预测任意sprintf()调用所需的缓冲区大小的方法。C99的snprintf()返回它可能放到缓冲区的字
符数,而它又可以用0作为缓冲区大小进行调用。因此
nch=snprintf(NULL,0,fmtstring,/*其它参数*/);
这样的调用就可以预测出格式串扩展后所需要的字符数。
另一个(非标准的)选择是asprintf()函数,在bsd和GNU的C库中都有提供,它调用malloc为格式串分配空间,并返回分配内存区的指针。这样使用
:
char *buf;
asprintf(&buf, "%d = %s", 42, "forty-two");
/*现在,buf指向含有格式串的malloc的内存*/
参考资料:[C9X,Sec.7.13.6.6]。
12.20 为什么大家都说不要使用gets()?
跟fgets()不同,gets()不能被告知输入缓冲区的大小,因此不能避免缓冲区的溢出。标准库的fgets()函数对gets()作了很大的改进,尽管它仍
然不完善。如果真的可能输入很长的行,还是需要仔细思考,正确处理。参见问题7.1 用fgets()代替gets()的代码片断。
参考资料:[Rationale,Sec.4.9.7.2];[H&S, Sec. 15.7 p. 356] 。
12.21 为什么调用printf()之后errno内有ENOTTY?
如果stdout是终端,很多stdio包都会对其行为进行细微的调整。为了做出判断,这些实现会执行某些当stdout为终端时会失败的操作。尽管输
出操作成功完成,errno还是会被置为ENOTTY。注意,只有当函数报告错误之后检查errno的内容才有意义。errno在其它情况下也不保证为0。
参考资料: [ISO,Sec.7.1.4, Sec. 7.9.10.3];[CT&P,Sec.5.4 p. 73];[PCS,Sec. 14 p. 254] 。
12.22 fgetops/fsetops和ftell/fseek之间有什么区别?fgetops()和fsetops()到底有什么用处?
ftell()和fseek()用长整型表示文件内的偏移(位置),因此,偏移量被限制在20亿(231 . 1)以内。而新的fgetpos()和fsetpos()函数使用了一
个特殊的类型定义fpost来表示偏移量。这个类型会适当选择,因此,fgetpos()和fsetpos可以表示任意大小的文件偏移。fgetpos()和gsetpos
()也可以用来记录多字节流式文件的状态。参见问题1.2。
参考资料:[K&R2,Sec.B1.6p.248];[ISO, Sec. 7.9.1, Secs. 7.9.9.1,7.9.9.3]; [H&S,Sec.15.5p.252]。
12.23 如何清除多余的输入,以防止在下一个提示符下读入?.ush(stdin)可以吗?
.ush()仅对输出流有效。因为它对“.ush”的定义是用于完成缓冲字符的写入,而对于输入流.ush并不是用于放弃剩余的输入。
参考资料:[ISO,Sec.7.9.5.2];[H&S, Sec. 15.2] 。
12.24 既然.ush()不能,那么怎样才能清除输入呢?
这取决于你要做什么。如果你希望丢掉调用scanf()(参见问题12.16 -12.17) 之后所剩下的换行符和未预知的输入,你可能需要重写你的
scanf()或者换掉它,参见问题12.18。或者你可以用下边这样的代码吃掉一行中多余的字符
while((c=getchar())!=’/n’&&c!=EOF)/*丢弃*/;你也可以使用curses的.ushinp()函数。没有什么标准的办法可以丢弃标准输入流的未读取
字符,即使有,那也不够,因为未读取字符也可能来自其它的操作系统级的输入缓冲区。如果你希望严格丢弃多输入的字符(可能是预测发出临界
提示),你可能需要使用系统相关的技术;参加问题19.1 和19.2。参考资料:[ISO,Sec.7.9.5.2];[H&S, Sec. 15.2] 。
12.25 对某些路径文件名调用fopen()总是失败。
参见问题19.21 和19.22。
12.26 我想用“r+”打开一个文件,读出一个字符串,修改之后再写入,从而就地更新一个文件。可是这样不行。
确保在写操作之前先调用fseek,回到你准备覆盖的字串的开始,况且在读写“+”模式下的读和写操作之间总是需要fseek或.ush。同时,记住改
写同样数量的字符,而且在文本模式下改写可能会在改写处把文件长度截断,因而你可能需要保存行长度。参见问题19.17。
参考资料:[ISO,Sec.7.9.5.3]。
12.27 怎样在程序里把stdin或stdout重定向到文件?
使用freopen(),但请参见下边的问题12.28。
12.28 一旦使用freopen()之后,怎样才能恢复原来的stdout(或stdin)?
没有什么好办法。如果你需要恢复回去,那么最好一开始就不要使用fre-open()。可以使用你自己的可以随意赋值的输出(输入)流变量,而不要
去动原来的输出(或输入)流。
有一种不可移植的办法,可以在调用freopen()之前保存流的信息,以便其后恢复原来的流。一种办法是使用系统相关的调用如dup(),dup2()等
。另一种办法是复制或查看FILE结构的内容,但是这种方法完全没有可移植性而且很不可靠。
12.29 怎样同时向两个地方输出,如同时输出到屏幕和文件?
直接做不到这点。但是你可以写出你自己的printf变体,把所有的内容都输出两次。下边有个简单的例子:
#include <stdio.h>
#include <stdarg.h>
void f2printf(FILE *fp1, FILE *fp2, char *fmt, ...)
{
va_list argp;
va_start(argp, fmt); vfprintf(fp1, fmt, argp); va_end(argp);
va_start(argp, fmt); vfprintf(fp2, fmt, argp); va_end(argp);
}
这里的f2printf()就跟fprintf()一样,除了它接受两个文件指针并同时输出到两个文件。
参见问题15.5。
12.30 怎样正确的读取二进制文件?我有时看到0x0a和0x0d混淆了,而且如果数据中包含0x1a的话,我好像会提前遇到EOF。
读取二进制数据文件的时候你应该用“rb”调用fopen(),确保不会发生文本文件的解释。类似的,写二进制文件时,使用“wb”。
注意文本/二进制区别只是发生在文件打开时:一旦文件打开之后,在其上调用何种I/O函数无关紧要。
参考资料:[ISO,Sec.7.9.5.3];[H&S, Sec. 15.2.1 p. 348] 。
第13章
库函数
13.1 怎样把数字转为字符串(与atoi相反)?有itoa()函数吗?
用sprintf()就可以了。不需担心用sprintf()会小题大作,也不必担心会浪费运行时间或代码空间;实践中它工作得挺好。参见问题7.6 答案中
的实例;以及问题8.4 和12.19。
你也可以用sprintf()把长整形或浮点数转换成字符串(使用%ld或%f)。
参考资料:[K&R1,Sec.3.6p.60];[K&R2, Sec. 3.6 p. 64] 。
13.2 为什么strncpy()不能总在目标串放上终止符’/0’?
strncpy()最初被设计为用来处理一种现在已经废弃的数据结构——定长,不必’/0’结束的“字符串”。strncpy的另一个怪癖是它会用多个
’/0’填充短串,直到达到指定的长度。在其它环境中使用strncpy()有些麻烦,因为你必须经常在目的串末尾手工加’/0’。
你可以用strncat代替strncpy来绕开这个问题:如果目的串开始时为空(就是说,如果你先用*dest=’/0’),strncat()就可以完成你希望
strncpy()完成的事情。另外一个方法是用sprintf(dest,"%.*s",n,source)。
如果需要复制任意字节(而不是字符串),memcpy()是个比strncpy()更好的选择。
13.3 为什么有些版本的toupper()对大写字符会有奇怪的反应?为什么有的代码在调用toupper()前先调用tolower()?
老版的toupper()和tolower()不一定能够正常处理不需要转换的字符参数,例如数字、标点或已经符合请求的字符。在ANSI/ISO标准C中,这些
函数保证对所有的字符参数正常处理。
参考资料:[ISO,Sec.7.3.2];[H&S,Sec.12.9pp.320-1];[PCS,p.182]。
71
13.4 怎样把字符串分隔成用空白作间隔符的段?怎样实现类似传递给main()的argc和argv?
标准中唯一用于这种分隔的函数是strtok(),虽然用起来需要些技巧,而且不一定能做到你所要求的所有事。例如,它不能处理引用。
参考资料:[K&R2,Sec.B3p.250];[ISO,Sec.7.11.5.8];[H&S,Sec.13.7pp.333-4];[PCS,p.178]。
13.5 我需要一些处理正则表达式或通配符匹配的代码。
确保你知道经典的正则表达式和文件名通配符的不同。前者的变体在Unix工具ed和grep等中使用,后者的变体在多数操作系统中使用。
有许多匹配正则表达式的包可以利用。很多包都是用成对的函数,一个“编译”正则表达式,另一个“执行”它,即用它比较字符串。查查头文
件<regex.h>或<regexp.h>和函数regcmp/regex,regcomp/regexec,或recomp/reexec。这些函数可能在一个单独的regexp库中。在
ftp://ftp.cs.toronto.edu/pub/regexp.shar.Z 或其它地方可以找到一个HenrySpencer开发的广受欢迎的regexp包,这个包也可自由再发布。
GNU工程有一个叫做rx的包。参见问题18.18。
文件名通配符匹配(有时称之为“globbing”)在不同的系统上有不同的实现。在Unix上,shell会在进程调用之前自动扩展通配符,因此,程序几
乎从不需要专门考虑它们。在MS-DOS下的编译器中,通常都可以在建立argv的时候连接一个用来扩展通配符的特殊目标文件。有些系统(包括
MS-DOS和VMS)会提供通配符指定文件的列表和打开的系统服务。参见问题19.25 和20.2。
13.6 我想用strcmp()作为比较函数,调用qsort()对一个字符串数组排序,但是不行。
你说的“字符串数组”实际上是“字符指针数组”。qsort比较函数的参数是被排序对象的指针,在这里,也就是字符指针的指针。然而strcmp
()只接受字符指针。因此,不能直接使用strcmp()。写一个下边这样的间接比较函数:
/*通过指针比较字符串*/
int pstrcmp(const void *p1, const void *p2)
{
return strcmp(*(char * const *)p1, *(char * const *)p2);
}
比较函数的参数表示为“一般指针”constvoid*。然后,它们被转换回本来表示的类型(指向字符指针的指针),再复引用,生成可以传入strcmp
()的char*。
不要被[K&R2]5.11节119-20页的讨论所误导,那里讨论的不是标准库中的qsort。
参考资料:[ISO,Sec.7.10.5.2];[H&S, Sec. 20.5 p. 419] 。
13.7 我想用qsort()对一个结构数组排序。我的比较函数接受结构指针,但是编译器认为这个函数对于qsort()是错误类型。我要怎样转换
这个函数指针才能避免这样的警告?
这个转换必须在比较函数中进行,而函数必须定义为接受“一般指针”(constvoid*)的类型,就象上文问题13.6 中所讨论的。比较函数可能像
这样:
int mystructcmp(const void *p1, const void *p2)
{
const struct mystruct *sp1 = p1;
const struct mystruct *sp2 = p2;
/*现在比较sp1->whatever和sp2->...*/
从一般指针到结构mystruct指针的转换过程发生在sp1=p1和sp2=p2的初始化中;由于p1和p2都是void指针,编译器隐式的进行了类型转换。
另一方面,如果你对结构的指针进行排序,则如问题13.6 所示,你需要间接使用:sp1=*(structmystruct*const*)p1。
一般而言,为了让编译器“闭嘴”而进行类型转换是一个坏主意。编译器的警告信息通常希望告诉你某些事情,忽略或轻易去掉会让你陷入危险
,除非你明确知道你在做什么。参见问题4.5。
参考资料:[ISO,Sec.7.10.5.2];[H&S, Sec. 20.5 p. 419] 。
13.8 怎样对一个链表排序?
有时侯,有时侯,在建立链表时就一直保持链表的顺序要简单些(或者用树代替)。插入排序和归并排序算法用在链表最合适了。
如果你希望用标准库函数,你可以分配一个暂时的指针数组,填入链表中所有节点的地址,再调用qsort(),最后依据排序后的数组重新建立链表
。
参考资料:[Knuth,Sec.5.2.1pp.80-102,Sec.5.2.4pp.159-168];[Sedgewick, Sec. 8 pp. 98-100, Sec. 12 pp. 163-175] 。
13.9 怎样对多于内存的数据排序?
你可以用“外部排序”法,[Knuth]第三卷中有详情。基本的思想是对数据分段进行排序,每次的大小尽可能多的填入内存中,把排好序的数据段
存入暂时文件中,再归并它们。如果你的操作系统提供一个通用排序工具,你可以从程序中调用:参见问题19.30 和19.31。
参考资料:[Knuth,Sec.5.4pp.247-378];[Sedgewick, Sec. 13 pp. 177-187] 。
13.10 怎样在C程序中取得当前日期或时间?
只要使用函数time(),ctime(),localtime()和/或strftime()就可以了。下面是个简单的例子:
#include <stdio.h>
#include <time.h>
int main()
{
time_t now;
time(&now);
printf("It’s %s", ctime(&now));
return 0;
}
用函数strftime()可以控制输出的格式。如果你需要小于秒的解析度,参见问题19.36。
参考资料:[K&R2,Sec.B10pp.255-7];[ISO,Sec.7.12];[H&S,Sec.18]。
13.11我知道库函数localtime()可以把timet转换成结构structtm,而ctime()可以把timet转换成为可打印的字符串。怎样才能进行反向操作,
把structtm或一个字符串转换成timet?
ANSIC提供了库函数mktime(),它把structtm转换成timet。
把一个字符串转换成timet比较难些,这是由于可能遇到各种各样的日期和时间格式。某些系统提供函数strptime(),基本上是strftime()的反
向函数。其它常用的函数有partime()(与RCS包一起被广泛的发布)和getdate()(还有少数其它函数,发布在C的新闻组)。参见问题18.18。
参考资料:[K&R2,Sec.B10p.256];[ISO,Sec.7.12.2.3];[H&S,Sec.18.4pp. 401-2] 。
13.12 怎样在日期上加N天?怎样取得两个日期的时间间隔?
ANSI/ISO标准C函数mktime()和di.time()对这两个问题提供了一些有限的支持。mktime()接受没有规格化的日期,所以可以用一个日期的
structtm结构,直接在tmmday域进行加或减,然后调用mktime()对年、月、日域进行规格化,同时也转换成了timet值。可以用mktime()来计算两
个日期的timet值,然后用di.time()计算两个timet值的秒数差分。
但是,这些方法只有日期在timet表达范围内才保证工作正常。对于保守的timet,通常范围是从1970年到大约2037年;注意有些timet的表达不是
按照Unix和Posix标准的。tmmday域是个int,所以日偏移量超出32,736就会上溢。还要注意,在夏令时转换的时候,一天并不是24小时,所以不要
假设可以用86400整除。
另一个解决的方法是用“Julian日期”,这可以支持更宽的时间范围。处理Julian日期的代码可以在以下地方找到:Snippets收集(参见问题
18.16);Simtel/Oakland站点(文件JULCAL10.ZIP,参见问题18.18) 和文献中提到的文章“Dateconversions”[Burki]。
参见问题13.11, 20.27 和20.28。
参考资料: [K&R2,Sec.B10 p. 256];[ISO,Secs.7.12.2.2,7.12.2.3];[H&S,Secs.18.4,18.5pp.401-2];[Burki]。
13.13 我需要一个随机数生成器。
标准C库函数就有一个:rand()。你系统上的实现可能并不完美,但写一个更好的并不是一件容易的事。
如果你需要实现自己的随机数生成器,有许多这方面的文章可供参考;象下面的文献或sci.math.num-analysis的FAQ。网上也有许多这方面的包
:老的可靠的包有r250,RANLIB和FSULTRA(参见问题18.18),还有由Marsaglia,Matumoto和Nishimura新近的成果“MersenneTwister”,另外就是
DonKnuth个人网页上收集的代码。
参考资料:[K&R2,Sec.2.7p.46,Sec.7.8.7p.168];[ISO, Sec. 7.10.2.1]; [H&S,Sec.17.7 p. 393];[PCS,Sec.11 p. 172];[Knuth,Vol.2
Chap. 3 pp. 1-177];[P&M]。
13.14 怎样获得在一定范围内的随机数?
直接的方法是rand()%N/*不好*/试图返回从0到N. 1的数字。但这个方法不好,因为许多随机数发生器的
低位比特并不随机,参见问题13.16。一个较好的方法是:
(int)((double)rand() / ((double)RAND_MAX + 1) * N)
如果你不希望使用浮点,另一个方法是:
rand() / (RAND_MAX / N + 1)
两种方法都需要知道RANDMAX,而且假设N要远远小于RANDMAX。RANDMAX在ANSI里#de.ne在<stdlib.h>。
顺便提一下,RANDMAX是个常数,它告诉你C库函数rand()的固定范围。你不可以设RANDMAX为其它的值,也没有办法要求rand()返回其它范围的值
。
如果你用的随机数发生器返回的是0到1的浮点值,要取得范围在0到N. 1内的整数,只要将随机数乘以N就可以了。
参考资料:[K&R2,Sec.7.8.7p.168];[PCS, PCS Sec. 11 p. 172] 。
13.15 每次执行程序,rand()都返回相同顺序的数字。
你可以调用srand()来初始化模拟随机数发生器的种子,用的值可以是真正随机数或至少是个变量,例如当前时间。这儿有个例子:
#include <stdlib.h>
#include <time.h>
srand((unsigned int)time((time_t *)NULL));
不幸的是,这个代码并不完美——其中,time()返回的timet可能是浮点值,转换到无符号整数时有可能上溢,这造成不可移植。参见问题:19.36
。还要注意到,在一个程序执行中多次调用srand()并不见得有帮助;特别是不要为了试图取得“真随机数”而在每次调用rand()前都调用
srand()。参考资料:[K&R2,Sec.7.8.7p.168];[ISO,Sec.7.10.2.2];[H&S,Sec.17.7
p. 393] 。
13.16 我需要随机的真/假值,所以我用直接用rand()%2,可是我得到交替的0,1,0,1,0..
这是个低劣的伪随机数生成器,在低位比特中不随机。很不幸,某些系统就提供这样的伪随机数生成器。尝试用高位比特;参见问题13.14。参考
资料:[Knuth,Sec.3.2.1.1pp.12-14]。
13.17 怎样产生标准分布或高斯分布的随机数?
这里有一个由Marsaglia首创Knuth推荐的方法:
#include <stdlib.h>
#include <math.h>
double gaussrand()
{
static double V1, V2, S;
static int phase = 0;
double X;
if(phase == 0) {
do { double U1 = (double)rand() / RAND_MAX; double U2 = (double)rand() / RAND_MAX;
V1=2*U1-1;V2=2*U2-1;S=V1*V1+V2*V2;
} while(S >= 1 || S == 0);
X = V1 * sqrt(-2 * log(S) / S); } else X = V2 * sqrt(-2 * log(S) / S);
phase = 1 -phase;
return X;
}
其它的方法参见本文的扩展版本,参见问题20.36。
参考资料: [Knuth,Sec.3.4.1p.117];[Marsaglia&Bray];[Press et al., Sec.
7.2 pp. 288-290] 。
13.18 我不断得到库函数未定义错误,但是我已经#inlude了所有用到的头文件了。
通常,头文件只包含外部说明。某些情况下,特别是如果是非标准函数,当你连接程序时,需要指定正确的函数库以得到函数的定义。#include头
文件并不能给出定义。参见问题10.10, 11.29, 13.19, 14.3 和19.39。
13.19 虽然我在连接时明确地指定了正确的函数库,我还是得到库函数未定义错误。
许多连接器只对对象文件和函数库进行一次扫描,同时从函数库中提取适合当前未定义函数的模块。所以函数库和对象文件(以及对象文件之间
)的连接顺序很重要;通常,你希望最后搜索函数库。例如,在Unix系统中,把-l参数放在命令行的后部。参见问题13.20。
13.20 连接器说end未定义代表什么意思?
这是个老Unix系统中的连接器所用的俏皮话。当有其它符号未定义时,你才会得到end未定义的信息,解决了其它的问题,有关end的错误信息就
会消失。参见问题13.18 和13.19。
13.21 我的编译器提示printf未定义!这怎么可能?
据传闻,某些用于微软视窗系统的C编译器不支持printf()。你也许可以让这样的编译器认为你写的是“控制台程序”,这样编译器会打开“控
制台窗口”从而支持printf()。
第14章
浮点运算
14.1一个.oat变量赋值为3.1时,为什么printf输出的值为3.0999999?
大多数电脑都是用二进制来表示浮点和整数的。在十进制里,0.1是个简单、精确的小数,但是用二进制表示起来却是个循环小数
0.0001100110011...。所以
3.1 在十进制内可以准确地表达,而在二进制下不能。
在对一些二进制中无法精确表示的小数进行赋值或读入再输出时,也就是从十进制转成二进制再转回十进制,你会观察到数值的不一致.这是由
于编译器二进制/十进制转换例程的精确度引起的,这些例程也用在printf中。参见问题14.6。
14.2 执行一些开方根运算,可是得到一些疯狂的数字。
确定你用了#include<math.h>,以及正确说明了其它相关函数返回值为double。另外一个需要注意的库函数是atof(),其原型说明在<stdlib.h>
中。参见问题14.3。
参考资料:[CT&P,Sec.4.5pp.65-6]。
14.3 做一些简单的三角函数运算,也引用了#include<math.h>,可是一直得到编译错误“unde.ned:sin”(函数sin未定义)。
确定你真的连接了数学函数库(mathlibrary)。例如,在Unix或Linux系统中,有一个存在了很久的bug,你需要把参数-lm加在编译或连接命令行
的最后。参见问题13.18, 13.19 和14.2。
14.4 浮点计算程序表现奇怪,在不同的机器上给出不同的结果。
首先阅读问题14.2.
如果问题并不是那么简单,那么回想一下,电脑一般都是用一种浮点的格式来近似的模拟实数的运算,注意是近似,不是完全。下溢、误差的累积
和其它非常规性是常遇到的麻烦。
不要假设浮点运算结果是精确的,特别是别假设两个浮点值可以进行等价比较。也不要随意的引入“模糊因素”;参见问题14.5。
79
这并不是C特有的问题,其它电脑语言有一样的问题。浮点的某些方面被通常定义为“中央处理器(CPU)是这样做的”(参见问题11.34),否则在
一个没有“正确”浮点模型的处理器上,编译器要被迫做代价非凡的仿真。
本文不打算列举在处理浮点运算上的潜在难点和合适的做法。一本好的有关数字编程的书能涵盖基本的知识。参见下面的参考资料。
参考资料:[K&P,Sec.6pp.115-8];[Knuth,Volume2chapter4];[Goldberg]。
14.5 有什么好的方法来验对浮点数在“足够接近”情况下的等值?
浮点数的定义决定它的绝对精确度会随着其代表的值变化,所以比较两个浮点数的最好方法就要利用一个精确的阈值。这个阈值和作比较的浮
点数值大小有关。不要用下面的代码:
double a, b;
...
if(a==b)/*错!*/
要用类似下列的方法:
#include <math.h>
if (fabs(a -b) <= epsilon * fabs(a))
epsilon被赋为一个选定的值来控制“接近度”。你也要确定a不会为0。
参考资料:[Knuth,Sec.4.2.2pp.217-8]。
14.6 怎样取整数?
最简单、直接的方法:
(int)(x + 0.5)
这个方法对于负数并不正常工作。可以使用一个类似的方法:
(int)(x < 0 ? x -0.5 : x + 0.5)
14.7
为什么C不提供乘幂的运算符?
14.8
为什么我机器上的<math.h>没有预定义常数MPI?
因为提供乘幂指令的处理器非常少。C有一个pow()标准函数,原型说明在<math.h>。而对于小的正整数指数,直接用乘法一般会更有效。
这个常数不包含在标准内,它应该是定义准确到机器精度的π值。如果你需要用到π,你需要自己定义,或者用4*atan(1.0)或acos(-1.0)来计算
出来。
参考资料:[PCS,Sec.13p.237]。
14.9 怎样测试IEEENaN以及其它特殊值?
许多实现高质量IEEE浮点的系统会提供简洁的工具去处理这些特殊值。例如,在<math.h>以非标准扩展功能,或可能以<ieee.h>或<nan.h>提供
预定义常数,及象isnan()这类的函数。这些工具的标准化进程正在进行中。一个粗陋但通常有效的测试NaN的方法:
#define isnan(x) ((x) != (x))
虽然一些不支持IEEE的编译器可能会把这个判断优化掉。
C99提供isnan(),fpclassify()及其它一些类别的例程。
必要时,还可以用sprintf()格式化需测试的值,在许多系统上,它会产生“NaN”或“Inf”的字符串。你就可以比较了。
参见问题19.38。
参考资料:[C9X,Sec.7.7.3]。
14.10 在C 中如何很好的实现复数?
这其实非常直接,定义一个简单结构和相关的算术函数就可以了。C99在标准中支持复数类别。参见问题2.8, 14.11。
参考资料:[C9X,Sec.6.1.2.5,Sec.7.8]。
14.11 我要寻找一些实现以下功能的程序源代码:快速傅立叶变换(FFT)、矩阵算术(乘法、倒置等函数)、复数算术。
AjayShah整理了一个免费算术软件列表。这个列表在互联网有广泛的归档。其中一个URL是ftp://ftp.math.psu.edu/pub/FAQ/numcomp-free-c
。参见问题18.8, 18.10. 18.16, 18.18。
14.12 TurboC的程序崩溃,显示错误为“.oatingpointformatsnotlinked”(浮点格式未连接)。
一些在小型机器上使用的编译器,包括TurboC(和Richie最初用在PDP-11上的编译器),编译时会忽略掉某些它认为不需要的浮点支持。特别是用
非浮点版的printf()和scanf()以节省一些空间,也就是忽略处理%e、%f和%g的编码。
然而,Borland用来确定程序是否使用了浮点运算的探测法并不充分,程序员有时必需调用一个空浮点库函数(例如sqrt(),或任何一个函数都可
以)以强制装载浮点支持。参见comp.os.msdos.programmerFAQ以获取更多信息。
第15章
可变参数
15.1 为什么调用printf()前,必须要用#include<stdio.h>?
为了把printf()的正确原型说明引入作用域。
对于用可变参数的函数,编译器可能用不同的调用次序。例如,如果可变参数的调用比固定参数的调用效率低。所以在调用可变参数的函数前,
它的原型说明必须在作用域内,编译器由此知道要用不定长调用机制。在原型说明中用省略号“...”来表示可变参数。
参考资料:[ISO,Sec.6.3.2.2,Sec.7.1.7];[Rationale, Sec. 3.3.2.2, Sec. 4.1.6]; [H&S,Sec.9.2.4pp.268-9,Sec.9.6pp.275-6]。
15.2 为什么%f可以在printf()参数中,同时表示.oat和double?他们难道不是不同类型吗?
“参数默认晋级”规则适用于在可变参数中的可变动部分:char和shortint晋级到int,.oat晋级到double。同样的晋级也适用于在作用域中没
有原型说明的函数调用,即所谓的“旧风格”函数调用,参见问题11.4。所以printf的%f格式总是得到double。类似的,%c总是得到int,%hd也是
。参见问题12.7, 12.13。
参考资料:[ISO,Sec.6.3.2.2];[H&S, Sec. 6.3.5 p. 177, Sec. 9.4 pp. 272-3] 。
15.3 为什么当n为longint,printf("%d",n);编译时没有匹配警告?我以为ANSI函数原型可以防止这样的类型不匹配。
当一个函数用可变参数时,它的原型说明没有也不能提供可变参数的数目和类型。所以通常的参数匹配保护不适用于可变参数中的可变部分。
编译器不能执行内含的转换或警告不匹配问题1。
参见问题5.2, 11.4, 12.7 和15.2。
15.4 怎样写一个有可变参数的函数?
用<stdarg.h>提供的辅助设施。
1译者注:现代的编译器(例如gcc),如果打开编译警告参数,编译器对标准中的可变参数函数(printf,scanf...等)会进行匹配测试。象问题中的
源代码,用“gcc-Wall”进行编译,会给出这样的警告:“warning:intformat,longintarg(arg2)”
83
下面是一个把任意个字符串连接起来的函数,结果存在malloc的内存中:#include<stdlib.h>/*说明
malloc,NULL,size_t*/#include<stdarg.h>/*说明va_相关类型和函数*/#include<string.h>/*说明strcat等*/
char *vstrcat(const char *first, ...)
{
size_t len;
char *retbuf;
va_list argp;
char *p;
if(first == NULL)
return NULL;
len = strlen(first);
va_start(argp, first);
while((p = va_arg(argp, char *)) != NULL)
len += strlen(p);
va_end(argp);
retbuf=malloc(len+1);/*+1包含终止符/0*/
if(retbuf==NULL)returnNULL;/*出错*/
(void)strcpy(retbuf, first);
va_start(argp,first);/*重新开始扫描*/
while((p = va_arg(argp, char *)) != NULL)
(void)strcat(retbuf, p);
va_end(argp);
return retbuf;
}
调用如下:
char *str = vstrcat("Hello, ", "world!", (char *)NULL);
注意最后一个参数的类型重置;参见问题5.2, 15.3。注意调用者要释放返回的存储空间,那是用malloc分配的。
参考资料: [K&R2,Sec. 7.3 p. 155, Sec. B7 p. 254];[ISO,Sec. 7.8]; [Rationale,Sec.4.8];[H&S,Sec.11.4pp.296-9];
[CT&P,Sec.A.3pp.139-141];[PCS,Sec.11pp.184-5,Sec.13p.242]。
15.5 怎样写类似printf()的函数,再把参数转传给printf()去完成大部分工作?
用vprintf(),vfprintf()或vsprintf()。下面是一个error()函数,它列印一个出错信息,在信息前加入字符串“error:”和在信息后加入换行
符:
#include <stdio.h>
#include <stdarg.h>
void error(const char *fmt, ...)
{
va_list argp;
fprintf(stderr, "error: ");
va_start(argp, fmt);
vfprintf(stderr, fmt, argp);
va_end(argp);
fprintf(stderr,
"/n");
}
参考资料:[K&R2,Sec.8.3p.174,Sec.B1.2p.245];[ISO, Secs. 7.9.6.7,7.9.6.8,7.9.6.9]; [H&S,Sec.15.12pp.379-80];[PCS, Sec. 11 pp.
186-7] 。
15.6 怎样写类似scanf()的函数,再把参数转传给scanf()去完成大部分工作?
C99支持vscanf(),vfscanf()和vsscanf(),C99以前的标准不支持。参考资料:[C9X,Secs.7.3.6.12-14]。
15.7 怎样知道实际上有多少个参数传入函数?
这一段信息不可移植。一些旧系统提供一个非标准函数nargs()。然而它的可信度值得怀疑,因为它的典型返回值是参数的字节长度,而不是参
数的个数。结构、整数和浮点类型的值一般需要几个字节的长度。
任何接收可变参数的函数都应该可以从传入的参数本身来得到参数的数目。类printf函数从格式字符串中的格式说明符来确定参数个数,就
象%d这样的格式说明符。所以如果格式字符串和参数数目不符时,此类函数会出错的很厉害。
还有一个常用的技巧,如果所有的参数是同一个类型,在参数列最后加一个标记值。通常用0、-1或适当类型转换的空指针,参见问题5.2 和15.4
例子中exec1()和vstrcat()的用法。
最后,如果类型是可预见的,你可以加一个参数数目的参数。当然调用者通常是很不喜欢这种做法的。参考资料:[PCS,Sec.11pp.167-8]。
15.8 为什么编译器不让我定义一个没有固定参数项的可变参数函数?
标准C要求用可变参数的函数至少有一个固定参数项,这样你才可以使用vastart()。所以编译器不会接受下面定义的函数:
int f(...)
{
...
}
参见问题15.9。
参考资料: [ISO,Sec.6.5.4, Sec. 6.5.4.3, Sec. 7.8.1.1];[H&S,Sec.9.2p.263]。
15.9 我有个接受.oat的可变参函数,为什么vaarg(argp,.oat)不工作?
“参数默认晋级”规则适用于在可变参数中的可变动部分:参数类型为.oat的总是晋级(扩展)到double,char和shortint晋级到int。所以
vaarg(arpg,.oat)是错误的用法。应该总是用vaarg(arpg,double)。同理,要用vaarg(argp,int)来取得原来类型是char,short或int的参数。
基于相同理由,传给vastart()的最后一个“固定”参数项的类型不会被晋级。参见问题11.4 和15.2。
参考资料:[ISO,Sec.6.3.2.2];[Rationale,Sec.4.8.1.2];[H&S,Sec.11.4p.297]。
15.10 va arg() 不能得到类型为函数指针的参数。
宏vaarg()所用的类型重写不能很好地操作于象函数指针这类过度复杂的类型。但是如果你用typedef定义一个函数指针类型,那就一切正常了
。参见问题
1.7。参考资料:[ISO,Sec.7.8.1.2];[Rationale, Sec. 4.8.1.2] 。
15.11 怎样实现一个可变参数函数,它把参数再传给另一个可变参数函数?
通常来说,你做不到。理想情况下,你应该提供另一个版本的函数,这个函数接受valist指针类型的参数。类似于vfprintf(),参见问题15.5。如
果所有的参数必须完整的传给另一个函数,或者你不能重写另一个函数为一个接受valist指针类型参数的函数,这并没有一个可移植的解决方法
。也许可以通过求助于机器的汇编语言来实现。参见问题15.12。
15.12 怎样调用一个参数在执行是才建立的函数?
这没有一个保证工作或可移植的方法。如果你好奇,可以问本文的编辑(SteveSummit),他有一些古怪的点子,也许你可以试试..
也许你可以试着传一个无值型指针(void*)数组,而不是一个参数序列。被调用函数遍历这个数组,就象main()遍历argv一样。当然这一切都建
立在你能控制所有的调用函数上。参见问题19.35。
第16章
奇怪的问题
16.1 遇到不可理解的不合理语法错误,似乎大段的程序没有编译。
检查是否有没有结束的注释,不匹配的#if/#ifdef/#ifndef/#else/#endif指令,又或者没有完成的引号。记得还要检查头文件。参见问题2.14,
10.8 和11.28。
16.2 为什么过程调用不工作?编译器似乎直接跳过去了。
代码是否看起来象这样:
myprocedure;/*我的过程*/
C只有函数,而函数调用总要用圆括号将参数括起来,即使是无参数的函数。用下列代码:
myprocedure();
16.3 程序在执行用之前就崩溃了,用调试器单步跟进,在main()之前就死了。
也许你定义了一个或多个非常大的局部数组(超过上千字节)。许多系统只有固定大小的堆栈,即使那些自动动态堆栈分配的系统也会因为一次
性要分配大段堆栈而失败。
一般对大规模数组,定义为静态的数组会更好。如果由于递归的原因,每次都需要一组新的数组,可以用malloc()动态申请内存,参见问题1.11。
参见问题11.12, 16.4, 16.5 和18.4。
16.4 程序执行正确,但退出时崩溃在main()最后一个语句之后。为什么会这样?
注意是否错误说明了main(),参见问题2.14, 10.8, 11.12 和11.14。是否把局部缓冲传给了setbuf()或setvbuf()。又或者问题出在注册于
atexit()的清理函数。参见问题7.6 和11.17。
参考资料:[CT&P,Sec.5.3pp.72-3]。
89
16.5 程序在一台机器上执行完美,但在另一台上却得到怪异的结果。更奇怪的是,增加或去除调试的打印语句,就改变了症状..
许多地方有可能出错。下面是一些通常的检查要点:
. 未初始化的局部变量,参见问题7.1。
. 整数上溢,特别是在一些16比特的机器上,一些中间计算结果可能上溢,象a
* b / c, 参见问题3.11。
. 未定义的求值顺序,参见问题3.1 到3.5。
. 忽略了外部函数的说明,特别是返回值不是int的函数,或是参数“缩小”或可变的函数。参见问题1.8, 11.4 和15.1。
. 复引用空指针,参见第5 章。
. malloc/free的不适当使用:假设malloc的内存都被清零、已释放的内存还可用、再次释放已释放内存、malloc的内部被破坏,参见问
题7.16 和7.17。
. 指针类常规问题,参见问题16.7。
. printf()格式与参数不符,特别是用%d输出longint,参见问题12.7。
. 试图分配的内存大小超出一个unsignedint类型的范围,特别是在内存有限的机器上,参见问题7.14和19.27。
. 数组边界问题,特别是暂时的小缓冲,也许用于sprinf()来构造一个字符串,参见问题7.1 和12.19。
. 错误的假设了typedef的映射类型,特别是sizet。
. 浮点问题,参见问题14.1 和14.4。
. 任何你自己认为聪明的在特定机器上的机器代码生成小技巧。
正确使用函数原型说明能够捕捉到一些以上的问题。lint会捕捉到更多。参见问题16.3, 16.4 和18.4。
16.6 为什么代码:char*p="hello,worl!";p[0]=’H’;会崩溃?
字符串实字并不总是可以修改的,除非是用在字符数组的初试化。试用:
char a[] = "hello, world!";
参见问题1.13。
参考资料:[ISO,Sec.6.1.4];[H&S, Sec. 2.7.4 pp. 31-2] 。
16.7 “Segmentationviolation”,“Buserror”和“Generalprotec-tionfault”意味着什么?
通常,这意味着你的程序试图访问不该访问的内存地址,一般是由于堆栈出错或是不正确的使用指针。可能的原因有:局部数组溢出(用堆栈分配
的自动变量);不小心,用了空指针(参见问题5.2 和5.15)、未初始化指针、地址未对齐的指针或其它没有适当分配的指针(参见问题7.1 和
7.2);malloc内部被破坏(参见问题
7.16);函数调用参数不匹配,特别是如果用了指针,两个可能出错的函数是scanf()
(参见问题12.11) 和fprintf()(确定他的第一个参数是FILE*)。参见问题16.3 和16.4。
第17章
风格
17.1 什么是C最好的代码布局风格?
K&R提供了最常被抄袭的实例,同时他并不要求大家沿用他的风格:
大括号的位置并不重要,尽管人们对此有着执着的热情。我们在几种流行的风格中选了一种。选一个适合你的风格,然后坚持使用这一风格。
保持布局风格对自己,对邻近及通用源代码的一致比使之完美跟重要。如果你的编码环境(本地习惯或公司政策)没有建议一个风格,你也不想发
明自己的风格,可以沿用K&R中的风格。各种缩进和大括号的放置之间的好与坏可以详尽而细致地探讨,但本文不再次重复了。可以参见《印第
安山风格指南》(IndianHillStyleGuide),问题17.7。
“好风格”的品质并不简单,它包含的内容远远不止代码的布局细节。不要把
时间都花在格式上而忽略了更实质性的代码本身的质量。参见问题10.4。参考资料:[K&R1,Sec.1.2p.10];[K&R2, Sec. 1.2 p. 10] 。
17.2 用if(!strcmp(s1,s2))比较两个字符串等值,是否是个好风格?
这并不是个很好的风格,虽然这是个流行的习惯用法。如果两个字符串相等,这个测试返回为真,但!(“非”)的使用,容易引起误会,以为测试不
等值情况。另一个选择是用一个宏:
#define Streq(s1, s2) (strcmp((s1), (s2)) == 0)
参见问题17.8。
17.3 为什么有的人用if(0==x)而不是if(x==0)?
这是用来防护一个通常错误的小技巧:
if (x = 0)
如果你养成了把常量放在==前面的习惯,当你意外的把代码写成了:
if (0 = x)
那编译器就会报怨。明显的,一些人会觉得记住反换测试比记住输入双=号容易。当然这个技巧只对和常量比较的情况有用。参考资料:
[H&S,Sec.7.6.5pp.209-10]。
93
17.4 原型说明externintfunc((int,int));中,那些多出来的括号和下划线代表了什么?
这是为了可以在使用ANSI以前的编译器时,关掉说明中的原型部分。这是技巧的一部分。
在别的地方,宏被定义为类似下面的代码:
#ifdef __STDC__
#define __(proto) proto
#else
#define __(proto) ()
#endif
原型说明中额外的括号是为了让原型列表被当作宏的单一参数。
17.5 为什么有些代码在每次调用printf()前,加了类型转换(void)?
printf()确实返回一个值,虽然极少数程序员去检验每次调用的返回值。由于有些编译器和lint对于被丢弃的返回值会报警告,清楚的用(void)
作类型转换相当于说:“我决定忽略这次调用的返回值,请继续对于其他忽略返回值的情况(也许是不应该的)提出警告。”通常,无值类型转换
也用于strcpy()和strcat()的调用,他们的返回值从不会令人惊讶。
参考资料: [K&R2,Sec.A6.7 p. 199];[Rationale,Sec.3.3.4];[H&S,Sec.
6.2.9 p. 172, Sec. 7.13 pp. 229-30] 。
17.6 什么是“匈牙利标志法”(HungarianNotation)?是否值得用?
匈牙利标志法是一种命名约定,由CharlesSimonyi发明。他把变量的类型(或者它的预期使用)等信息编码在变量名中。在某些圈子里,它被高度
热爱,而在另一些地方,它被严厉地批评。它的主要优势在于变量名就说明了它的类型或者使用。它的主要缺点在于类型信息并不值得放在变量
名中。
参考资料:[Simonyi&Heller]。
17.7 哪里可以找到“印第安山风格指南”(IndianHillStyleGuide)及其它编码标准?
各种文档在匿名ftp都可以得到:
地址 文档及目录
ftp.cs.washington.edu pub/cstyle.tar.Z
(更新的印第安山风格指南,(IndianHillGuide))
ftp.cs.toronto.edu doc/programming
(包括HenrySpencer的《C程序员的十诫》(“10
Commandments for C Programmers”))
ftp.cs.umd.edu pub/style-guide
也许你会对这些书感兴趣:《TheElementsofProgrammingStyle》[K&P],《PlumHallProgrammingGuidelines》[Plum],
《CStyle:StandardsandGuide-lines》[Straker]。参见文献[21]。
参见问题18.7。
17.8 有些人说goto是邪恶的,我应该永不用它。那是否太极端了?
程序设计风格,就象写作风格一样,是某种程度的艺术,不可以被僵化的教条所束缚。虽然风格的探讨经常都是围绕着这些条例。
对于goto语句,很早以前,就被注意到,随意的使用goto会很快的导致象面糊一样难以维护的代码。然而,不经思考就简单的禁止goto的使用,并
不能立即导至好程序。一个无规划的程序员可以不用任何goto语句而构造出复杂难解的代码,也许使用奇怪的嵌套循环和布尔变量来取代goto
。
通常,把这些程序设计风格的评论或者“条例”当作指导准则比当作条例要更好。当程序员理解这些指导准则所要实现的目标,就会工作的更加
之好。盲目的回避某种构造或者死套条例而不融会贯通,最终还会导致这些条例试图避免的问题。
此外,许多程序设计风格的意见只是意见。通常卷入“风格战争”是毫无意义的。某些问题(象问题5.3, 5.7, 9.2 和10.5),争辩的双方是不可
能同意,认同对方的不同或者是停止争论的。
第18章
工具和资源
注意:本章中的信息比较旧,有些可能已经过时了,特别是各个公共包的URL。
18.1 常用工具列表。
工具 程序名(参见问题18.18)
C 交叉引用生成器 c.ow,cxref,calls,cscope,xscope,ixfw
C源代码美化器/美化打印 cb,indent,GNUindent,vgrind
版本控制和管理工具 CVS, RCS, SCCS
C源代码扰乱器(遮蔽器) obfus, shroud, opqcp
“make”从属关系生成器 makedepend,cc-M或cpp-M
计算源代码度规工具 ccount,Metre,lcount,csize; McCable
andAssociates也有一个商业包出售
C 源代码行数计数器 可以用UNIX的标准工具wc作个大概的
计算,比用grep-c";"要好
C说明帮助(cdecl) 见comp.sources.unix第14卷(参见问题
18.18)和[K&R2]
原型发生器 参见问题11.30
malloc 问题抓捕工具 参见问题18.2
“选择性”的C预处理器 参见问题10.16
语言翻译工具 参见问题11.30和20.23
C校对机(lint) 参见问题18.5
C 编译器 参见问题18.3
这个工具列表并不完全,如果你知道有没列出的工具,欢迎联系本表的维护者。
其它工具列表和关于它们的讨论可以在Usenet的新闻组comp.compilers和comp.software-eng找到。
参见问题18.3 和18.18。
97
18.2 怎样抓捕棘手的malloc问题?
有好几个调试工具包可以用来抓捕malloc问题。其中一个流行的工具是ConorP.Cahill的dbmalloc,公布在comp.sources.misc1992年第32卷。
还有leak公布在comp.sources.unix档案第27卷;“Snippets”合集中的JMalloc.c,JMalloc.h;MEMDEBUG
(ftp://ftp.crpht.lu/pub/sources/memdebug);ElectricFence。参见问题18.18。
还有一些商业调试工具,对调试malloc等棘手问题相当有用:
. CodeCenter(Saber-C),出品CenterlineSoftware(http://www.centerline.com/)
. Insight (now Insure?), 出品ParaSoftCorporation(http://www.parasoft.com/)
. Purify, 出品RationalSoftware(http://www-306.ibm.com/software/rational/,原来是PureSoftware,现在是IBM的一部分)
. ZeroFault,出品TheZeroFaultGroup(http://www.zerofault.com/)
18.3 有什么免费或便宜的编译器可以使用?
自由软件基金的GNUC(gcc,http://gcc.gnu.org/) 是个流行而高质量的免费C编译器.djgpp(http://www.delorie.com/djgpp/)是移植到MS-DOS
的GCC版本。据我所知,有移植到Macs和Windwos上的GCC版本。1
lcc 是另外一个流行的编译器http://www.cs.virginia.edu/~lcc-win32/, http: //www.cs.princeton.edu/software/lcc/)。
PowerC是MixSotfware公司提供的一个非常便宜的MS-DOS下的编译器。公司地址:1132CommerceDrive,Richardson,TX75801,USA,214-783-6001
。
ftp://ftp.hitech.com.au/hitech/paci.c 是个MS-DOS下的试用C编译器。非商业用途的不一定要注册。
新闻组comp.compilers的档案中有许多有关各种语言的编译器、解释器、语法规则的信息。新闻组在http://compilers.iecc.com/ 的档案包
含了一个FAQ列表和免费编译器的目录。
参见问题18.18。
18.4 刚刚输入完一个程序,但它表现的很奇怪。你可以发现有什么错误的地方吗?
看看你是否能先用lint跑一遍(用-a,-c,-h,-p或别的参数)。许多C编译器
1译者注:Windows下有两个移植版本cygwin(http://www.cygwin.com/)和MinGW(http://http://www.mingw.org/)。
实际上只是半个编译器,它们选择不去诊断许多源程序中不会妨碍代码生成的难
点。
参见问题16.5、16.7 和18.5。
参考资料:[LINT]。
18.5 哪里可以找到兼容ANSI的lint?
PC-Lint和FlexeLint是GimpelSoftware公司的产品(http://www.gimpel.com/)。
UnixSystemV版本4的lint兼容ANSI。可以从UNIXSupportLabs或SystemV的销售商单独得到(和其它C工具捆绑在一起)。
另外一个兼容ANSI的lint是Splint(以前叫lclint,http://www.splint.org/)。它可以作一些高级别的正式检验。
如果没有lint,许多现代的编译器可以作出几乎和lint一样多的诊断。许多网友推荐gcc-Wall-pedantic。
18.6 难道ANSI函数原型说明没有使lint过时吗?
实际上不是。首先,原型说明只有在它们存在和正确的情况下才工作。一个无心的错误原型说明比没有更糟。其次,lint会检查多个源程序文档
的一致性,以及数据和函数的说明。最后,像lint这样独立的程序在加强兼容的、可移植的代码惯例上会比任何特定的、特殊实现的、充满特性
和扩展功能的编译器更加谨慎。
如果你确实要用函数原型说明而不是lint来作多文件一致性检查,务必保证原型说明在头文件中的正确性。参见问题1.3 和10.4。
18.7 网上有哪些C的教程或其它资源?
有许多个:
在http://cprog.tomsweb.net 有个TomTorfs的不错的教程。
ChristopherSawtell写的《给C程序员的便筏》(NotesforCprogram-mers)。在下面的地址可以得到:ftp://svr-
ftp.eng.cam.ac.uk/misc/sawtell C. shar, ftp://garbo.uwasa../pc/c-lang/c-lesson.zip, http://www...uib.no/Fysisk/
Teori/KURS/OTHER/newzealand.html 。
TimeLove的《程序员的C》(CforProgrammers)。http://www-h.eng.cam. ac.uk/help/tpl/languages/C/teaching C/
TheCoronadoEnterprisesC教程在Simtel镜像点目录pub/msdos/c,或在http://www.coronadoenterprises.com/tutorials/c/index.html 。
Steve Holmes 的在线教程http://www.strath.ac.uk/IT/Docs/Ccourse/ 。
MartinBrown的网页有一些C教程的资料http://www-isis.ecs.soton.ac.uk/ computing/c/Welcome.html 。
在一些UNIX的机器上,在shell命令行,可以试试“learnc”。注意教程可能比较旧了。
最后,本FAQ的作者以前教授一些C的课程,这些笔记都放在了网上http: //www.eskimo.com/~scs/cclass/cclass.html 。
【不承诺申明:我没有检阅这些我收集的教程,它们可能含有错误。除了那个有我名字的教程,我不能为它们提供保证。而这些信息会很快的变
得过时,也许,当你读到这想用上面的地址时,它们已经不能用了】
这其中的几个教程,再加上许多其它C的信息,可以从http://www.lysator. liu.se/c/index.html 得到。
VinitCarpenter维护着一个学习C和C++的资源列表,公布在新闻组comp.lang.c和comp.lang.c++,也归档在本FAQ所在(参见问题20.36),或者
http://www.cyberdiem.com/vin/learn.html 。
参见问题18.8、18.9 和18.16。
18.8 哪里可以找到好的源代码实例,以供研究和学习?
这里有几个连接可以参考:ftp://garbo.uwasa../pc/c-lang/00index.txt,http: //www.eskimo.com/~scs/src/。
小心,网上也有数之不尽的非常糟糕的代码。不要从坏代码中“学习”,这是每个人都可以做到的,你可以做的更好。参见问题18.7, 18.10,
18.16 和18.18。
18.9 有什么好的学习C的书?有哪些高级的书和参考?
有无数有关C的书,我们无法一一列出,也无法评估所有的书。许多人相信最好的书,也是第一本:由Kernighan和Richie编写的《TheCprogram-
mingLanguage》(“K&R”,现在是第二版了[K&R2])。对这本书是否适合初学者有不同的意见;我们当中许多人是从这本书学的C,而且学的不错;
然而,有些人觉得这本书太客观了点,不大适合那些没有太多程序设计经验的人作为第一个教程。网上有一些评注和勘误表,例
如:http://www.csd.uwo.ca/ ~jamie/.Refs/.Footnotes/C-annotes.html, http://www.eskimo.com/~scs/cclass/ cclass.html,
http://cm.bell-labs.com/cm/cs/cbook/2edi.s.html 。
许多活跃在新闻组comp.lang.c的人推荐K.N.King写的《C:AModernApproach》。
一本极好的参考书是由SamuelP.Harbison和GuyL.Steele和写的《C:AReferenceManual》[H&S]。
虽然并不适合从头开始学C,本FAQ(英文版)已经出版了,见文献[CFAQ]。
C和C++用户协会(AssociationofCandC++,ACCU)维护着一份很全面的有关C/C++的书目评论(http://www.accu.org/bookreviews/public/)。
参见问题18.7。
18.10 哪里可以找到标准C函数库的源代码?
GNU工程有一个完全实现的C函数库(http://www.gnu.org/software/libc/)。另一个来源是由P.J.Plauger写的书《TheStandardCLibrary》
[Plauger],然而它不是公共版权的。参见问题18.8, 18.16 和18.18。
18.11 是否有一个在线的C参考指南?
提供两个选择: http://www.cs.man.ac.uk/standardc/index.html,http://www.dinkumware.com/htmcl/index.html
18.12 哪里可以得到ANSI/ISOC标准?
参见问题11.2。
18.13 我需要分析和评估表达式的代码。
有两个软件包可用:“defunc”,在1993年12月公布于新闻组comp.sources.misc(V41i32,33),1994年1月公布于新闻组alt.sources。可以在这
个URL得到:ftp://sunsite.unc.edu/pub/packages/development/libraries/defunc-1.3.tar. Z; “parse”, 可以从
lamont.ldgo.columbia.edu 得到。其它选择包括S-Lang注释器(http://www.s-lang.org/),共享软件Cmm(“C减减”或“去掉困难部分的C”)
。参见问题18.18 和20.4。
《SoftwareSolutionsinC》[Schumacher,ed.]中也有一些分析和评估的代码(第12章,235到255页)。
18.14 哪里可以找到C的BNF或YACC语法?
ANSI标准中的语法是最权威的。由JimRoskind写的一个语法在ftp://ftp. eskimo.com/u/s/scs/roskindgrammar.Z。一个Je.Lee做的,新鲜出
炉的ANSIC90语法工作实例可以在ftp://ftp.uu.net/usenet/net.sources/ansi.c.grammar.Z 得到,还包含了一个相配的lexer。FSF的GNUC编
译器也含有一个语法,当然还有K&R的附录也有一个。
新闻组comp.compilers的档案中含有更多的有关语法的信息,参见问题
18.3。
参考资料:[K&R1,Sec.A18pp.214-219];[K&R2, Sec. A13 pp. 234-239]; [ISO,Sec.B.2];[H&S, pp. 423-435 Appendix B] 。
18.15 谁有C编译器的测试套件?
PlumHall(以前在Cardi.,NJ,现在在Hawaii)有一个套件出售;RonaldGuilmette的RoadTestTM 编译器测试套件(更多信息在
ftp://netcom.com/pub/ rfg/roadtest/announce.txt);Nullstone的自动编译器性能分析工具(http://www.nullstone.com)。FSF的GNUC(gcc)
发布中含有一个许多编译器通常问题的C严酷测试。Kahan的偏执狂的测试(ftp://netlib.att.com/netlib/paranoia),尽其所能的测试C实现的
浮点能力。
18.16 哪里有一些有用的源代码片段和例子的收集?
BobStout的“SNIPPETS”是个很流行的收集(ftp://ftp.brokersys.com/pub/snippets 或http://www.brokersys.com/snippets/)。
LarsWirzenius的“publib”函数库(ftp://ftp.funet../pub/languages/C/Publib/)。
参考问题14.11, 18.7, 18.8, 18.10 和18.18。
18.17 我需要执行多精度算术的代码。
一些流行的软件包是:“quad”,函数在BSDUnix系统的libc中(ftp.uu.net,/systems/unix/bsd-sources/.../src/lib/libc/quad/*);GNUMP函
数库“libmp”;MIRACL软件包(http://indigo.ie/~mscott/);DavidBell和LandonCurtNoll写的“‘calc”程序;以及老Unix的libmp.a。参见
问题14.11 和18.18。
参考资料:[Schumacher,ed.,Sec.17pp.343-454]。
18.18 在哪里和怎样取得这些可自由发布的程序?
随着可利用程序数目,公共可访问的存档网站数目以及访问的人数的增加,这个问题回答起来变得即容易又困难。
有几个比较大的公共存档网站,例如:ftp.uu.net, archive.umich.edu, oak. oakland.edu, sumex-aim.stanford.edu 和
wuarchive.wustl.edu。它们免费提供极多的软件和信息。FSFGNU工程的中央发布地址是ftp.gnu.org。这些知名的网站往往非常繁忙而难以访
问,但也有不少“镜像”网站来分担负载。
在互联网上,传统取得档案文件的方法是通过匿名ftp。对于不能使用ftp的人,有几个ftp-by-mail的服务器可供使用。越来越多的,万维网
(WWW)被使用在文件的宣告、索引和传输上。也许还会有新的访问方法。
这些是问题中比较容易回答的部分。困难的部分在于详情——本文不能追踪或列表所有的文档网站或各种访问的方法。如果你已经可以访问互
联网了,你可以取得比本文更加及时的活跃网站信息和访问方法。
问题的另一个即难也易的方面是找到哪个网站有你所要的。在这方面有极多的研究,几乎每天都有可能有新的索引服务出台。其中最早的服务
之一
是“archie”,当然还有许多高曝光的商业网络索引和搜索服务,例如AltaVista,Excite和Yahoo。
如果你可以访问Usenet,请查看定期发布在新闻组comp.sources.unix和comp.sources.misc的邮件,其中有说明新闻组归档的政策和怎样访问档
案。其中两个是:ftp://gatekeeper.dec.com/pub/usenet/comp.sources.unix/, ftp://ftp.uu. net/usenet/comp.sources.unix/。新闻组
comp.archives包括了多数的有关各种东西的匿名ftp网站公告。最后,通常新闻组comp.sources.wanted是个适合询问源代码的地方,不过在发
贴前,请先查看它的FAQ“怎样查找资源(Howto.ndsources)”。
参见问题14.11, 18.8, 18.10 和18.16。
第19章
系统依赖
19.1 怎样从键盘直接读入字符而不用等RETURN键?怎样防止字符输入时的回显?
唉,在C里没有一个标准且可移植的方法。在标准中跟本就没有提及屏幕和键盘的概念,只有基于字符“流”的简单输入输出。
在某个级别,与键盘的交互输入一般上都是由系统取得一行的输入才提供给需要的程序。这给操作系统提供了一个加入行编辑的机会(退格、删
除、消除等),使得系统地操作具一致性,而不用每一个程序自己建立。当用户对输入满意,并键入RETURN(或等价的键)后,输入行才被提供给需
要的程序。即使程序中用了读入单个字符的函数(例如getchar()等),第一次调用就会等到完成了一整行的输入才会返回。这时,可能有许多字
符提供给了程序,以后的许多调用(象getchar()的函数)都会马上返回。
当程序想在一个字符输入时马上读入,所用的方式途径就采决于行处理在输入流中的位置,以及如何使之失效。在一些系统下(例如MS-DOS,VMS
的某些模态),程序可以使用一套不同或修改过的操作系统函数来扰过行输入模态。在另外一些系统下(例如Unix,VMS的另一些模态),操作系统
中负责串行输入的部分(通常称为“终端驱动”)必须设置为行输入关闭的模态,这样,所有以后调用的常用输入函数(例如read(),getchar()等)
就会立即返回输入的字符。最后,少数的系统(特别是那些老旧的批处理大型主机)使用外围处理器进行输入,只有行处理模式。
因此,当你需要用到单字符输入时(关闭键盘回显也是类似的问题),你需要用一个针对所用系统的特定方法,假如系统提供的话。新闻组
comp.lang.c讨论的问题基本上都是C语言中有明确支持的,一般上你会从针对个别系统的新闻组以及相对应的常用问题集中得到更好的解答,例
如comp.unix.questions或comp.os.msdos.programmer。另外要注意,有些解答即使是对相似系统的变种也不尽相同,例如Unix的不同变种。同
时也要记住,当回答一些针对特定系统的问题时,你的答案在你的系统上可以工作并不代表可以在所有人的系统上都工作。
然而,这类问题被经常的问起,这里提供一个对于通常情况的简略回答。
某些版本的curses函数库包含了cbreak(),noecho()和getch()函数,这些函数可以做到你所需的。如果你只是想要读入一个简短的口令而不想
回显的话,可以试试getpass()。在Unix系统下,可以用ioctl()来控制终端驱动的模式,“传
105
统”系统下有CBREAK和RAW模式,SystemV或POSIX系统下有ICANON,ccc[VMIN]和ccc[VTIME]模式,而ECHO模式在所有系统中都有。必要时,用函数
system()和stty命令。更多的信息可以查看所用的系统,传统系统下,查看<sgtty.h>和tty(4),SystemV下,查看<termio.h>和termio(4),POSIX
下,查看<termios.h>和termios(4)。在MS-DOS系统下,用函数getch()或getche(),或者相对应的BIOS中断。在VMS下,使用屏幕管理例程(SMG$),
或curses函数库,或者低层$QIO的IO$READVBLK函数,以及IO$MNOECHO等其它函数。也可以通过设置VMS的终端驱动,在单字符输入或“通过”模
式间切换。如果是其它操作系统,你就要靠自己了。
另外需要说明一点,简单的使用setbuf()或setvbuf()来设置sdtin为无缓冲,通常并不能切换到单字符输入模式。
如果你在试图写一个可移植的程序,一个比较好的方法是自己定义三套函数:1)设置终端驱动或输入系统进入单字符输入模式,(如果有必要的
话),2)取得字符,3)程序使用结束后的终端驱动复原。理想上,也许有一天,这样的一组函数可以成为标准的一部分。本常用问题集的扩充版(参
见问题20.36) 含有一套适用于几个流行系统的函数。
参见问题19.2
参考资料:[PCS,Sec.10pp.128-9,Sec.10.1pp.130-1];[POSIX, Sec. 7] 。
19.2 怎样知道有未读的字符,如果有,有多少?如果没有字符,怎样使读入不阻断?
这个问题也是完全和操作系统有关。某些版本的curses函数库有nodelay()的函数。根据所用系统的不同,也许你可以使用“不阻断输入输出
(nonblockingI/O)”,或者系统函数select或poll,或者用ioctl的FIONREAD,ccc[VTIME],kbhit(),rdchk(),open()或fcntl()的参数ONDELAY。
参见问题19.1。
19.3 怎样显示一个百分比或“转动的短棒”的进展表示器?
这个简单的事情,你可以做到相当的可移植。输出字符’/r’通常可以得到一个回车而没有换行,这样你就可以复写当前行。字符’/b’代表退
格,通常会使光标左移一格。记住要调用.ush()。
参考资料:[ISO,Sec.5.2.2]。
19.4 怎样清屏?怎样输出彩色文本?怎样移动光标到指定位置?
这些功能跟你所用的终端类型(或显示器)有关。你需要使用termcap,term-info或curses类的函数库,或者系统提供的特殊函数。在MS-DOS系统
下,有两个函数可以使用clrscr()和gotoxy()。
有一个不彻底的可移植的清屏方法:输出卷纸字符(’/f’),可以清除一部分的显示。还有个更加可移植的办法(尽管很简陋),输出足够多的换
行使当前屏幕清空。最后一个方法:使用system()函数(参见问题19.30) 来调用操作系统的清屏指令。
参考资料[PCS,Sec.5.1.4pp.54-60,Sec.5.1.5pp.60-62]。
19.5 怎样读入方向键,功能键?
terminfo,某些版本的termcap,以及某些版本的curses函数库有对这些非ASCII键的支持。典型的,一个特殊键会发送一个多字符序列(通常以
ESC[’/033’]字符开头)。分析这个多字符序列比较麻烦。如果你首先调用了key-pad(),curses会帮你做分析。
在MS-DOS下,如果你在读入键盘输入时,收到一个值为0的字符(不是字符’0’),这就标志着下一个读入的值代表一个特殊键。有关键盘的编码
可参见任何DOS的编程指南。简单的说明:上、下、左、右键的编码是72,80,75,77,功能键从59到68。
参考资料:[PCS,Sec.5.1.4pp.56-7]。
19.6 怎样读入鼠标输入?
请查阅你的系统文档,或者在特定系统的新闻组寻问,请先查看其组的FAQ。鼠标的处理在XWindown系统,MD-DOS,Macintosh下是完全不同的,也
许每个系统都不一样。
参考资料[PCS,Sec.5.5pp.78-80]。
19.7 怎样做串口(“comm”)的输入输出?
这也是跟所用系统有关的。在Unix下,你通常打开、读入、写出在/dev下的一个设备文件,使用终端驱动提供的工具来调整设备的特性。(参见
问题19.1 和19.2)。在MS-DOS下,你可一使用预定义的stdaux流,或特殊文件COM1,或基础的BIOS中断,又或者你需要更好的性能,使用任何一个
中断驱动的串口输入输出包。许多网友推荐JoeCampbell的书《C程序员串口通讯指南》(“CProgrammer’sGuidetoSerialCommunications”)
。
19.8 怎样直接输出到打印机?
Unix系统下,使用popen()(参见问题19.31) 来把输出写到lp或lpr中,或者打开特殊文件/dev/lp。MS-DOS下,写向预定义的stdprn流(非标准),
或者打开特殊文件PRN或LPT1。在某些情况下,另一个方法(也许是唯一的方法)是用窗口管理者的屏幕截图功能,然后打印得到的图形。
参考资料:[PCS,Sec.5.3pp.72-74]。
19.9 怎样发送控制终端或其它设备的逃逸指令序列?
如果你能够找到发送字符到设备的方法(参见问题19.8),发送逃逸指令序列是件很容易的事。在ASCII代码中,逃逸字符的代码是033(十进制
27),以下代码就可以发送逃逸序列ESC[J:
fprintf(ofd, "/033[J");
19.10 怎样直接访问输入输出板?
通常,有两种办法:使用特定的系统函数,例如inport和outport(如果设备由输入输出接口访问),或者使用人为的指针变量来访问内存映射的输
入输出(memory-mappedI/O)设备。参见问题19.29。
19.11 怎样做图形?
从前,Unix下有一套相当不错且小巧的设备独立的绘制函数(plot(3)和plot(5))。由RobertMaier写的GNUlibplot函数库保持了同样的精神,支
持许多现代的绘制设备(http://www.gnu.org/software/plotutils/plotutils.html)。
OpenGL是一个现代的平台独立的制图函数库,它也支持三维制图和动画。其它有关的制图标准有GKS和PHIGS。
如果你在MS-DOS下编程,你大概需要用到符合VESA或BGI标准的函数库。
如果你要和某一个特定的制图仪打交道,通常发送适当的逃逸序列就可以绘图了,参见问题19.9. 销售商可能会提供一个可以从C调用的函数库,
或者你也许可以在网上找到。
如果你需要在某个特定的视窗环境下编程(Macintosh,XWindow,MicrosoftWindows),你使用其提供的工具;参阅相关的文档、新闻组或FAQ。
参考资料:[PCS,Sec.5.4pp.75-77]。
19.12 怎样显示GIF和JPEG图象?
这跟你用的显示环境有关,有可能环境已经提供了这些函数。http://www. ijg.org/.les/ 有个可供参考的JPEG软件。
19.13 怎样检验一个文件是否存在?
要做到可靠而可移植的检验出乎意料的困难。如果从你检验到你打开文件前,这个文件被(别的进程)生成或删除了,所做的任何检验都会失效。
三个可能用作检验的函数是stat(),access()和fopen()。当使用fopen()作近似检验时,用只读打开,然后马上关闭,但是失败并不代表不存在。
这里,只有fopen()据有广泛的可移植性,如果系统提供access,而程序用了Unix的UID设置特性,要特别小心使用。
不要去预测像打开文件这类操作是否成功,通常直接尝试再查验返回值会更好,如果失败了再申诉。当然,如果你要避免复写已存在的文件,这个
方法并不适用,除非打开文件有象OEXCL的参数,那就可以做到你所要的效果。
参考资料:[PCS,Sec.12pp.189,213];[POSIX, ec. 5.3.1, Sec. 5.6.2, Sec.
5.6.3.]。
19.14 怎样在读入文件前,知道文件大小?
如果文件大小指的是你从C程序中可以读进的字符数量,要得到这个精确的数字可能困难或不可能。
Unix系统函数stat()会给出准确的答案。有些系统提供了类似Unix的stat()函数,但只返回一个近似值。你可以调用fseek()搜索到文件尾,再
调用ftell(),或者调用fstat(),然而这些方法都有同样的问题:fstat()不可移植,通常返回和stat()一样的值;ftell()并不保证可以返回字符
计数,除非是用于二进制文件,但是,严格来讲,二进制文件并不一定支持fseek搜索到SEEKEND。某些系统提供.lesize()或.lelength()的函数,
但是它们明显的不可移植。
你是否确定你必须预先知道文件大小?作为一个C程序最准确的方法就是打开文件并读入,也许可以重新整理代码,使其边读入边计算文件大小
。
参考资料: [ISO,Sec.7.9.9.4];[H&S,Sec.15.5.1];[PCS,Sec.12 p. 213]; [POSIX,Sec.5.6.2]。
19.15 怎样得到文件的修改日期和时间?
Unix和POSIX函数是stat(),某些其它系统也提供。参见问题19.14。
19.16 怎样缩短一个文件而不用清除或重写?
BSD系统提供函数ftruncate(),某些其它系统提供chsize(),还有少数系统提供用于fcntl的参数FFREESP。MS-DOS下,某些时候你可以用write
(fd,"",0)。然而,没有一个可移植的方法,也没有办法删除在文件开头的数据块。参见问题19.17。
19.17 怎样在文件中插入或删除一行(或记录)?
如果你不能重写文件,你大概做不了。通常的做法就是重写文件。也许你可以用简单的标志记录删除来取代真的删除,这样可以避免重写。另外
一个可能性,是使用数据库来取代平坦文件。参见问题12.26 和19.16。
19.18 怎样从一个打开的流或文件描述符得到文件名?
通常情况下,这是没办法解决的。在Unix系统下,理论上需要搜索整个磁盘,也许还牵扯到特殊许可的问题,如果文件描述符是连接到管道(pipe)
或者指向一个已被删除的文件(如果文件有多个连接,也许会返回误导的信息),那搜索也会失败。最好的方法就是你在打开文件时,自己记住(或
许用一个fopen()的包裹函数)。
19.19 怎样删除一个文件?
标准C库函数是remove()。这是本章里少有的几个与系统无关的问题。在一些ANSI之前的老Unix系统,remove()可能不存在,那么你可以试试
unlink()。
参考资料[K&R2,Sec.B1.1p.242;ISOSec.7.9.4.1];[H&S, Sec. 15.15 p. 382];[PCS,Sec.12pp.208,220-221];[POSIX, Sec. 5.5.1, Sec.
8.2.4] 。
19.20 怎样复制一个文件?
可以用函数system()调用你所用操作系统的文件复制工具,参见问题19.30。或者打开源文件和目标文件(用fopen()或一些低层的打开文件的系
统函数),读入字符或数据段,再写出到目标文件中。
参考资料:[K&R2,Sec.1,Sec.7]。
19.21 为什么用了详尽的路径还不能打开文件?fopen("c:/ newdir /.le.dat","r")返回错误。
你实际请求的文件名内含有字符/n 和/f,可能并不存在,也不是你希望的。
在字符常量和字符串中,反斜杠/ 是逃逸字符,它赋予后面紧跟的字符特殊意义。为了正确的把反斜杠传递给fopen()(或其它函数),必须成双的
使用,这样第一个反斜杠引述了第二个:
fopen("c://newdir//file.dat", "r")
另一个选择,在MS-DOS下,正斜杠也被接受为路径分隔符,所以也可以这样用:
fopen("c:/newdir/file.dat", "r")
注意,顺便提一下,用于预处理#include指令的头文件名不是字符串文字,所以不必担心反斜杠的问题。
19.22 fopen()不让我打开文件:"$HOME/.pro.le"和"./.myrc-.le"。
至少在Unix系统下,像$HOME这样的环境变量和家目录的表示符.是由shell来展开的。不存在一个调用fopen()时的自动扩展机制。
19.23 怎样制止MS-DOS下令人担忧的“Abort,Retry,Ignore?”信息?
除了其它的事,你需要截取DOS的重要错误中断,中断24H。详情请参阅comp.os.msdos.programmer的FAQ。
19.24 遇到“Toomanyopen.les(打开文件太多)”的错误,怎样增加同时打开文件的允许数目?
通常有至少两个资源限制了同时打开文件的数目:操作系统可用的低层“文件说明符”或“文件句柄”的数目;和标准stdio函数库可用的FILE
结构数目。两个条件必须符合。在MS-DOS下,可以通过设置CONFIG.SYS,可以控制系统文件handle的数目。一些编译器附有增加stdio的FILE结
构数目的指令(也许是一两个源文件)。
19.25 怎样在C中读入目录?
试试能否使用opendir()和readdir()函数,它们是POSIX标准的一部分,大多数Unix变体都支持。MS-DOS,VMS和其它系统下也有这些函数的实现
。MS-DOS还有FINDFIRST和FINDNEXT函数,它们做的事基本一样,MSWindows有FindFirstFile和FindNextFile。readdir()只返回文件名,如果你
需要该文件更多的信息,试用stat()。如果想匹配文件名和通配符式样,参见问题13.5。
参考资料:cite[Sec.8.6pp.179-184]kr2;[PCS,Sec.13pp.230-1];[POSIX, Sec.5.1];[Schumacher,ed.,Sec.8]。
19.26 怎样找出系统还有多少内存可用?
你所用的系统可能会提供一个例程返回你所需的信息,但是这跟系统相当有关。
19.27 怎样分配大于64K的数组或结构?
一台合理的电脑应该可以让你透明地访问所有的有效内存。如果,你很不幸,你可能需要重新考虑程序使用内存的方式,或者用各种针对系统的
技巧。
64K仍然是一块相当大的内存。不管你的电脑有多少内存,分配这么一大段连续的内存是个不小的要求。标准C不保证一个单独的对象可以大于
32K,或者C99的64K。通常,设计数据结构时的一个好的思想,是使它不要求所有的内存都连续。对于动态分配的多维数组,你可以使用指针的指
针,在问题6.13 中有举例说明。你可以用链接或结构指针数组来代替一个大的结构数组。
如果你使用的是PC兼容机(基于8086)系统,遇到了64K或640K的限制,可以考虑使用“huge”(巨大)内存模型,或者扩展或延伸内存,或malloc的
变体函数halloc()和farmalloc(),或者用一个32比特的“平直”编译器(例如djgpp,参见问题18.3),或某种DOS扩充器,或换一个操作系统。
参考资料:[ISO,Sec.5.2.4.1];[C9X, Sec. 5.2.4.1] 。
19.28 错误信息“DGROUPdataallocationexceeds64K(DGROUP数据分配内存超过64K)”说明什么?我应该怎么做?我以为使用了大内存模型,
那我就可以使用多于64K的数据!
即使使用了大内存模型,MS-DOS的编译器明显地把某些数据(字符串,已初始化的全局或静态变量)都放在了一个缺省的数据段,而这个数据段溢
出了。要么减少全局数据,或者已经限制在一个合理的范围(而引起问题的是由于字符串的数目),你可以告诉编译器对这么大的数据不要使用缺
省数据段。某些编译器只把“小”数据放在缺省数据段,也提供了设置“小”的临界值的方法,例如,Microsoft的编译器可以用参数/Gt。
19.29 怎样访问位于某的特定地址的内存(内存映射的设备或图显内存)?
设置一个适当类型的指针去正确的值,使用明示的类型重制,以保证编译器知道这个不可移植转换是你的意图:
unsigned int *magicloc = (unsigned int *)0x12345678;
那么,*magiloc就指向你所要的地址。如果地址是个内存映射设备的寄存器,你大概需要使用限定词volatile。MS-DOS下,在和段、偏移量打交
道时,你会发现像MKFP这类宏非常好用。
参考资料:[K&R1,Sec.A14.4p.210];[K&R2, Sec.A6.6p.199];[ISO,Sec.
6.3.4;]; [Rationale,Sec.3.3.4];[H&S, Sec. 6.2.7 pp. 171-2] 。
19.30 怎样在一个C程序中调用另一个程序(独立可执行的程序,或系统命令)?
使用库函数system(),它的功能正是你所要的。注意,系统返回的值最多是命令的退出状态值(但这并不是一定的),通常和命令的输出无关。还
要注意,system()只接受一个单独的字符串参数来表述调用程序。如果你要建立复杂的命令行,可以使用sprintf()。
跟据你使用的系统,也许你还可以使用系统函数,例如exec或spawn(或execl,execv,spawnl,spawnv等)。
参见问题19.31。
参考资料:[K&R1,Sec.7.9p.157];[K&R2, Sec. 7.8.4 p. 167, Sec. B6 p. 253];[ISO,Sec.7.10.4.5];[H&S,Sec.19.2p.407];
[PCS,Sec.11p.179]。
19.31 怎样调用另一个程序或命令,同时收集它的输出?
Unix和其它一些系统提供了popen()函数,它在联通运行命令的进程管道设置了stdio流,所以输出可以被读取(或提供输入)。记住,结束使用后,
要调用函数pclose()。
如果你不能使用popen(),你应该可以调用system(),并输出到一个你可以打开读取的文件。
如果你使用Unix,觉得popen()不够用,你可以学习用pipe(),dup(),fork()和exec()。
顺便提一下,freopen()可能并不工作。
参考资料:[PCS,Sec.11p.169]。
19.32 怎样才能发现程序自己的执行文件的全路径?
arg[0]也许含有全部或部分路径,或者什么也没有。如果arg[0]中的路径不全,你也许可以重复命令语言注释器的路径搜索逻辑。但是,没有保
证的解决方法。
参考资料:[K&R1,Sec.5.11p.111];[K&R2,Sec.5.10p.115];[ISO,Sec.
5.1.2.2.1]; [H&S,Sec.20.1p.416.]。
19.33 怎样找出和执行文件在同一目录的配置文件?
这非常困难。参见问题19.32。即使你可以找出一个可行的方法,你可能会考虑通过环境变量或别的方法使程序的辅助(函数库)目录可配置。如
果程序会被数个人使用,例如在多用户系统中,那允许配置文件的变量存放变得特别重要。
19.34 一个进程如何改变它的调用者的环境变量?
这有可能完全做不到。不同的系统使用不同的方法来实现像Unix系统的全局名字/值功能。环境是否可以被运行的进程有效的改变,以及如果可
以,又怎样去做,这些都依赖于系统。
在Unix下,一个进程可以改变自己的环境(某些系统为此提供了setenv()或putenv()函数),被改变的环境通常会被传给子进程,但是这些改变不
会传递到父进程。在MS-DOS下,总环境是可以操作的,但是这需要晦涩难解的技巧。参见MS-DOS的FAQ。
19.35 怎样读入一个对象文件并跳跃到其中的地址?
你需要一个动态的连接程序或载入程序。也许可以malloc一段内存,再读入对象文件,但是你需要知道极多的有关对象文件格式、地址变换等知
识。在BSDUnix下,你可以使用system()和ld-A来实现连接。许多SunOS和SystemV的版本有-ldl函数库,允许动态载入对象文件。在VMS下,使用
LIB$FINDIMAGESYMBOL。GNU有个dld的包可以用。参见问题15.12。
19.36 怎样实现精度小于秒的延时或记录用户回应的时间?
很不幸,这没有可移植解决方法。下面是一些你可以在你的系统中寻找的函数:clock(),delay(),ftime(),getimeofday(),msleep(),nap
(),napms(),nanaosleep(),setitimer(),sleep(),Sleep(),times()和usleep。至少在Unix系统下,函数wait()不是你想要的。函数select()和
poll()(如果存在)可以用来实现简单的延时。在MS-DOS下,可以重新对系统计时器和计时器中断编程。
这些函数中,只有clock()在ANSI标准中。两次调用clock()之间的差分就是执行所用的时间,如果CLOCKSPERSEC的值大于1,你可以得到精确度小
于秒的计时。但是,clock()返回的是执行程序使用的处理器的时间,在多任务系统下,有可能和真实的时间相差很多。
如果你需要实现一个延时,而你只有报告时间的函数可用,你可以实现一个繁忙等待。但是这只是在单用户,单任务系统下可选,因为这个方法对
于其它进程极不友好。在多任务系统下,确保你调用函数,让你的进程在这段时间进入休眠状态。可用函数sleep(),select()或poll()和alarm
()或setitimer()实现。
对于非常短暂的延时,使用一个空循环颇据有诱惑力:
long int i;
for (i = 0; i < 1000000; ++i)
;
但是请尽量抵制这个诱惑!因为,经过你仔细计算的延时循环可能在下个月因为更快的处理器出现而不能正常工作。更糟糕的是,一个聪明的编
译器可能注意到这个循环什么也没做,而把它完全优化掉。
参考资料: [H&S,Sec.18.1 pp. 398-9];[PCS,Sec.12 pp. 197-8,215-6]; [POSIX,Sec.4.5.2]。
19.37 怎样抓获或忽略像control-C这样的键盘中断?
基本步骤是调用signal():
#include <signal.h>
singal(SIGINT, SIG_IGN);
就可以忽略中断信号,或者:
extern void func(int);
signal(SIGINT, func);
使程序在收到中断信号时,调用函数func()。
在多任务系统下(例如Unix),最好使用更加深入的技巧:
extern void func(int);
if(signal(SIGINT, SIG_IGN) != SIG_IGN)
signal(SIGINT, func);
这个测试和额外的调用可以保证前台的键盘中断不会因疏忽而中断了在后台运行的进程,在所有的系统中都用这种调用signal的方法并不会带
来负作用。
在某些系统中,键盘中断处理也是终端输入系统模式的功能,参见问题19.1。在某些系统中,程序只有在读入输入时,才查看键盘中断,因此键盘
中断处理就依赖于调用的输入例程(以及输入例程是否有效)。在MS-DOS下,可以使用setcbrk()或ctrlbrk()。
参考资料:[ISO,Secs.7.7,7.7.1];[H&S,Sec.19.6pp.411-3];[PCS,Sec.12pp.210-2];[POSIX,Secs.3.3.1,3.3.4]。
19.38 怎样很好地处理浮点异常?
在许多系统中,你可以定义一个matherr()的函数,当出现某些浮点错误时(例如<math>中的数学例程),它就会被调用。你也可以使用signal()函
数(参见问题19.37) 截取SIGFPE信号。参见问题14.9。
参考资料:[Rationale,Sec.4.5.1]。
19.39 怎样使用socket?网络化?写客户/服务器程序?
所有这些问题都超出了本文的范围,它们跟你所有的网络设备比C更有关系。有一些这方面主题的好书:DouglasComer撰写的三卷
《InternetworkingwithTCP/IP》和W.R.Stevens撰写的《UNIXNetworkProgramming》。网上也有相当多这方面的信息,有“UnixSocketFAQ”
(http://www.developerweb.net/sock-faq/),“Beej’sGuidetoNetworkProgramming”(http://www.ecst.csuchico. edu/~
beej/guide/net/)。
一个提示:针对你所用的操作系统,你也许需要明示的请求-lsocket或-lnsl函数库;参见问题13.18。
19.40 怎样调用BIOS函数?写ISR?创建TSR?
这些都是针对某一特定系统的问题(最可能的是运行MS-DOS的PC兼容机)。在针对系统的新闻组comp.os.msdos.programmer或它的FAQ里你会取
得更好的信息;另一个很好的资源是RalfBrown的中断列表。
19.41 编译程序,编译器出示“unionREGS”未定义错误信息,连接器出示“int86()”的未定义错误信息。
这些都跟MS-DOS的中断编程有关。它们在其它系统中不存在。
19.42 什么是“near”和“far”指针?
如今,它们差不多被废弃了;它们肯定于特定系统有关。如果你真的要知道,参阅针对DOS或Windows的编程参考资料。
19.43 我不能使用这些非标准、依赖系统的函数,程序需要兼容ANSI!
你很不走运。要么你误解了要求,要么这不可能做到。ANSI/ISOC标准没有定义做这些事的方法;它是个语言的标准,不是操作系统的标准。国际
标准POSIX(IEEE1003.1,ISO/IEC9945-1)倒是定义了许多这方面的方法,而许多系统(不只是Unix)都有兼容POSIX的编程接口。
可以做到,也是可取的做法是使程序的大部分兼容ANSI,将依赖系统的功能集中到少数的例程和文件中。这些例程或文件可以大量使用#ifdef或
针对每一个移植的系统重写。
第20章
杂项
20.1 怎样从一个函数返回多个值?
可以传入多个指针指向不同的地址,函数可以填入,或者使函数返回一个包含了需要值的结构,又或者理论上你可以使用全局变量。参见问题4.4
和7.6。
20.2 怎样访问命令行参数?
当main()被调用时,它们储存在数组argv中。参见问题8.2, 13.5 和19.25。
参考资料:[K&R1,Sec.5.11pp.110-114];[K&R2, Sec. 5.10 pp. 114-118]; [ISO,Sec.5.1.2.2.1];[H&S,Sec.20.1p.416];[PCS,Sec.5.6pp.81
-2,Sec.11p.159,pp.339-40AppendixF];[Schumacher,ed.,Sec.4pp.75-85]。
20.3 怎样写数据文件,使之可以在不同字大小、字节顺序或浮点格式的机器上读入?
最可移植的方法是是用文本文件(通常是ASCII),用fprintf()写入,用fscanf()读入,或类似的函数。同理,这也适用于网络协议。不必太相信那
些说文本文件太大或读写太慢的论点。大多数现实情况下,操作的效率是可接受的,而可以在不同机器间交换和用标准工具就可以对其进行操作
是个巨大的优势。
如果你必须使用二进制文件,你可以通过使用某些标准格式来提高可移植性,还可以利用已经写好的I/O函数库。这些格式包括:Sun的XDR
(RFC1014)、OSI的ASN.1(在CCITTX.409和ISO8825“BasicEncodingRules”中都有引用)、CDF、netCDF或HDF。参见问题2.10 和12.30。
参考资料:[PCS,Sec.6pp.86,88]。
20.4 怎样调用一个由char*指针指向函数名的函数?
最直接的方法就是维护一个名字和函数指针的列表:
int one_func(), two_func();
int red_func(), blue_func();
struct { char *name; int (*funcptr)(); } symtab[] = {
"one_func", one_func,
117
"two_func", two_func,
"red_func", red_func,
"blue_func",blue_func,
};
然后搜索函数名,就可以调用关联的函数指针。参见问题2.13, 18.13 和
19.35。参考资料:[PCS,Sec.11p.168]。
20.5 怎样实现比特数组或集合?
使用int或char数组,再加上访问所需比特的几个宏。这里有一些简单的宏定义,用于char数组:
#include <limits.h> /* for CHAR_BIT */
#define BITMASK(b) (1 << ((b) % CHAR_BIT))
#define BITSLOT(b) ((b) / CHAR_BIT)
#define BITSET(a, b) ((a)[BITSLOT(b)] |=
BITMASK(b))
#define BITTEST(a, b) ((a)[BITSLOT(b)] & BITMASK(b))
如果你没有<limits.h>,可以定义CHARBIT为8。参考资料:[H&S,Sec.7.6.7pp.211-216]。
20.6 怎样判断机器的字节顺序是高字节在前还是低字节在前?
有个使用指针的方法:
int x = 1;
if(*(char *)&x == 1)
printf("little-endian/n");
else
printf("big-endian/n");
另外一个可能是用联合。参见问题10.15 和20.7。
参考资料:[H&S,Sec.6.1.2pp.163-4]。
20.7 怎样掉换字节?
V7Unix有一个swap()的函数,但似乎被遗忘了。
使用明示的字节调换代码有个问题,你必须决定是否要调用;参见问题20.6。更好的方法是使用函数(例如BSD系统中网络函数ntohs()等),函数
会进行已知字符顺序和机器顺序(未知)之间的转换,对于已经和机器匹配的字符顺序,函数不作任何转换。
如果你必须自己写字符转换的代码,两个明显的方法就是使用指针或联合,就象问题20.6 一样。参考资料:[PCS,Sec.11p.179]。
20.8 怎样转换整数到二进制或十六进制?
确定你真的知道你在问什么。整数是以二进制存储的,虽然对于大多数情况下,把它们当成是八进制、十进制或十六进制并没有错,只要方便就
好。数字表达的进制只有在读入或写出到外部世界时才起作用。
在源程序中,非十进制的数字由在前的0或0x表示(分别位八进制和十六进制)。在进行I/O操作时,数字格式的进制在printf和scanf这类函数里,
由格式符决定(%d,%o和%x等);在strtol()和strtoul()中,则由他们的第三个参数决定。如果你想要输出任意进制的数字字符串,你需要自己提
供相关的函数(基本上是strtol的反函数)。在进行二进制I/O时,进制就不相干了。
更多的有关二进制的I/O,参见问题2.9。还有问题8.4 和13.1。
参考资料:[ISO,Secs.7.10.1.5,7.10.1.6]。
20.9 我可以使用二进制常数吗?有printf()的二进制的格式符吗?
两个都不行。你可以用strtol()把二进制的字符串转换成整数。参见问题
20.8。
20.10 什么是计算整数中比特为1的个数的最有效的方法?
许多象这样的比特问题可以使用查找表格来提供效率和速度(参见问题
20.11)。
20.11 什么是提高程序效率的最好方法?
选择好的算法,小心地实现,同时确定程序不做额外的事。例如,即使世界上最优化的字符复制循环也比不上不用复制。
当担心效率时,要保持几样事情在视野中,这很重要。首先,虽然效率是个非常流行的话题,它并不总是象人们想的那样重要。大多数程序的大多
数代码并不是时间紧要的。当代码不是时间紧要时,通常把代码写得清楚和可移植比达到最大效率更重要。记住,电脑运行得非常非常快,那些
看起来“低效率”的代码,也许可以编译得比较有效率,而运行起来也没有明显的延时。
试图预知程序的“热点”是个非常困难的事。当要关心效率时,使用pro.ling软件来确定程序中需要得到关注的地方。通常,实际计算时间都被
外围任务占用了(例如I/O或内存的分配),可以通过使用缓冲和超高速缓存来提高速度。
即使对于时间紧要的代码,最无效的优化技巧是忙乱于代码细节。许多常被建议的“有效的代码技巧”,即使是很简单的编译器也会自动完成(
例如,用移位运算符代替二的幂次方乘)。非常多的手动优化有可能是代码变得笨重而使效率反而低下了,同时几乎不可移植。例如,也许可以在
某台机器上提了速,但在另一台机器上去变慢了。任何情况下,修整代码通常最多得到线性信能提高;更好的算法可以得到更好的回报。
有关效率的更多讨论,以及当效率很重要时,如何提高效率的建议,可以从以下书中得到:Kernighan和Plauger的
《TheElementsofProgrammingStyle》[K&P]中的第七章,和JonBentley的《WritingE.cientPrograms》[Bentley]。
20.12 指针真得比数组快吗?函数调用会拖慢程序多少?++i比i=i+1快吗?
这些问题的精确回答,跟你所用的处理器和编译器有关。如果你必须知道,你就得小心的给程序计时。通常,差别是很小的,小到要经过千万次迭
代才能看到不同。如果可能,查看编译器的汇编输出,看看这两种方法是否被编译的一样。
一般的机器,通常遍历大的数组时,用指针比用数组要快,但是某些处理器就相反。
函数调用,虽然明显比内联代码要慢,但是它对程序的模块化和代码清晰度的贡献,很少有好的理由来避免它。
在修整象i=i+1这样的代码前,记住你是在跟编译器打交道,而不是键击编程的计算器。对于++i,i+=1和i=i+1,任何好的编译器都会生成完全一
样的代码。使用任何一种形式只跟风格有关,于效率无关。参见问题3.10。
20.13 人们说编译器优化的很好,我们不在需要为速度而写汇编了,但我的编译器连用移位代替i/=2都做不到。
i是有符号还是无符号?如果是有符号,移位并不等价(提示:想想如果i是个负的奇数),所以编译器没有使用是对的。
20.14 怎样不用临时变量而交换两个值?
一个标准而古老的汇编程序员的技巧是:
a ^= b;
b ^= a;
a ^= b;
但是这样的代码在现代高级程序设计语言中没什么用处。临时变量基本上是自由使用的,一般上的三个赋值是:
int t = a;
a = b;
b = t;
这不只对读者更清晰,更有可能被编译器辨别出来而变成最有效的代码(例如有可能使用EXCH指令)。后面的代码明显的可以用于指针和浮点值,
而不象XOR技巧只能用于整型。参见问题3.4 和10.2。
20.15 是否有根据字符串做切换的方法?
没有直接的方法。有些时候,更适合使用一个单独的函数以映射字符串到整数代码,然后根据整数代码做切换。否则,你当然也可以使用strcmp
()和传统的if/else链。参见问题10.11, 20.16 和20.26。
参考资料:[K&R1,Sec.3.4p.55];[K&R2,Sec.3.4p.58];[ISO,Sec.6.6.4.2];[H&S,Sec.8.7p.248]。
20.16 是否有使用非常量case标志的方法(例如范围或任意的表达式)?
没有。最初设计switch语句就是为编译器能简单的做转换,所以case标志被限制在单个、整形、常量的表达式。如果你不介意详述的列出所有
的情况,你可以把几个case标志连到同个语句,这样你可以覆盖一个小的范围。
如果你想要根据任意范围或非常量表达式进行选择,你只能用if/else链。
参见问题20.15。
参考资料:[K&R1,Sec.3.4p.55];[K&R2,Sec.3.4p.58];[ISO,Sec.6.6.4.2];[Rationale,Sec.3.6.4.2];[H&S, Sec. 8.7 p. 248] 。
20.17 return 语句外层的括号是否真的可选择?
是的。
很久以前,在C刚起步的时候,它们是必须的,刚好那时有足够的人学习了C,他们写的代码如今还在使用,所以还是需要括号的想法被广泛的流传
。
碰巧的是,在某些起况下,sizeof运算符的括号也是可选择的。
参考资料:[K&R1,Sec.A18.3p.218];[ISO,Sec.6.3.3,Sec.6.6.6];[H&S,
Sec. 8.9 p. 254.] 。
20.18 为什么C注释不能嵌套?怎样注释掉含有注释的代码?引用字符串内的注释是否合法?
C注释不能嵌套最可能的原因是PL/I的注释也不可以,C是借鉴了它而成的。所以,通常使用#ifdef或#if0来“注释”掉大段代码,其中可能含有
注释(参见问题11.20)。
字符序列/*和*/在双引号内的字符串没有特殊含义,所以不要在其中加入
注释,程序可能想输出它们(特别是要产生C代码作为输出的程序)。
注意//在C99中才成为合法的注释符。
参考资料:[K&R1,Sec.A2.1p.179];[K&R2,Sec.A2.2p.192];[ISO,Sec.6.1.9,AnnexF;RationaleSec.3.1.9];[H&S,Sec.2.2pp.18-9];
[PCS,Sec.10p.130]。
20.19 C是个伟大的语言还是别的?哪个其它语言可以写象a+++++b这样的代码?
在C中,写成这样也是没有意义的。词汇分析的规则规定,在一个简单的从左到右扫描中的每个点,最长的记号被划分,不管这样出来的记号序列
是否有意义。问题中的片段被解释为:
a ++ ++ + b
语法上是个不合法的表达式。参考资料:[K&R1,Sec.A2p.179];[K&R2,Sec.A2.1p.192];[ISO,Sec.
6.1]; [H&S,Sec.2.3pp.19-20]。
20.20 为什么C没有嵌套函数?
实现嵌套函数不是件简单的事,它们需要可以正当的访问包含函数的本地变量,为了简单化,这个功能是被故意舍弃的。gcc的扩展功能允许函数
嵌套。许多可能使用嵌套函数的地方(例如qsort的比较函数),一个充分但少许麻烦的解决方法是使用一个定义为静态(static)的邻近函数,如
果需要,可以通过少量静态变量进行通讯。一个干净些的方法是传递一个包含所需内容的结构指针,虽然qsort不支持这种方法。
20.21 assert() 是什么?怎样用它?
这是个定义在<assert.h>中的宏,用来测试断言。一个断言本质上是写下程序员的假设,如果假设被违反,那表明有个严重的程序错误。例如,一
个假设只接受非空指针的函数,可以写:
assert(p != NULL);
一个失败的断言会中断程序。断言不应该用来捕捉意料中的错误,例如malloc()或fopen()的失败。
参考资料:[K&R2,Sec.B6pp.253-4];[ISO,Sec.7.2];[H&S,Sec.19.1p.406]。
20.22 怎样从C中调用FORTRAN(C++,BASIC,Pascal,Ada,LISP)的函数?反之亦然?
这完全依赖于机器以及使用的各个编译器的特别调用顺序,有可能完全做不到。仔细阅读编译器的文档,有些时候有个“混合语言编程指南”,
尽管传递参数以及保证正确的运行启动的技巧通常很晦涩难懂。
对于FORTRAN,更多的信息可以从GlennGeers的FORT.gz找到,这个文档可以从匿名ftp网站suphys.physics.su.oz.au 的src目录取得。
BurkhardBurow写的头文件cfortran.h简化了许多流行机器上的C/FORTRAN接口。可以从匿名ftp网站zebra.desy.de 或http://www-
zeus.desy.de/~burow 取得。
C++中,外部函数说明的“C”修改量表明函数应该按C的调用约定使用。参考资料:[H&S,Sec.4.9.8pp.106-7]。
20.23 有什么程序可以做从Pascal或Fortran(或LISP,Ada,awk,“老”C)到C的转换?
有几个自由发布的程序可以使用:
p2c由DaveGillespie写的Pascal到C的转换器,发布于新闻组comp.sources.unix1990年三月(第21卷);也可以从
ftp://csvax.cs.caltech.edu/pub/p2c-1.
20.tar.Z 取得。
ptoc另外一个Pascal到C的转换器,它是用Pascal写的。(comp.sources.unix,第10卷,补丁在第13卷)。
f2cFortran到C的转换器,由BellLabs,Bellcore和CarnegieMellon的人员共同开发的。可以用以下方法得到更多的f2c信息:发email信息
“sendindexfromf2c”到netlib@research.att.com 或者research!netlib 。(在匿名ftp网站netlib.att.com 的netlib/f2c 目录也可取得。
)
本FAQ的维护者也有一个别的转换器的列表。参见问题11.30 和18.18。
20.24 C++是C的超集吗?可以用C++编译器来编译C代码吗?
C++源自C,而且大部分都建立在C的基础上,但是有一些合法的C代码在C++中不合法。相反的,ANSIC继承了C++的几个特性,包括原型和常量,所以
这两个语言并不是另一个的超集或子集;而且它们在一些通用构造的定义上也不同。尽管有这些不同,许多C程序在C++环境中编译正确,许多最
新的编译器同时提供C和C++的编译模式。但是,把C代码当成C++来编译通常是个坏的注意;两个语言的差异普遍上足够让你得到不好的结果。参
见问题8.5 和20.18。
参考资料:[H&S,p.xviii,Sec.1.1.5p.6,Sec.2.8pp.36-7,Sec.4.9pp.104-107]。
20.25 需要用到“近似”的strcmp,比较两个字符串的近似度,并不需要完全一样。
SunWu和UdiManber写的文章“AGREP–AFastApproximatePattern-MatchingTool”[AGREP]中有一些有用的信息,近似字符串匹配的算法以及有
用的参考文献。
另外一个方法牵涉到“soundex”算法,它把发音相近的词映射到同一个代码。它是为发现近似发音的名字而设计的(作为电话号码目录的帮
助),但是它可以调整用于任意词处理的服务。
参考资料:[Knuth,Sec.6pp.391-2Volume3];[AGREP]。
20.26 什么是散列法?
散列法是把字符串映射到整数的处理,通常是到一个相对小的范围。一个“散列函数”映射一个字符串(或其它的数据结构)到一个有界的数字(
散列存贮桶),这个数字可以更容易的用于数组的索引或者进行反复的比较。明显的,一个从潜在的有很多组的字符串到小范围整数的映射不是
唯一的。任何使用散列的算法都要处理“冲突”的可能。有许多散列函数和相关的算法被开发了出来;一个全面的说明已经超出了本文的范围
。
参考资料: [K&R2,Sec. 6.6];[Knuth,Sec. 6.4 pp. 506-549 Volume 3]; [Sedgewick,Sec.16pp.231-244]。
20.27 由一个日期,怎样知道是星期几?
用mktime()或localtime()(参见问题13.11 和13.12, 如果tmhour的值位0,要注意DST(夏时制)的调整);或者Zeller的congruence(参阅
sci.mathFAQ);或者这个由TomohikoSakamoto提供的优雅的代码:
int dayofweek(int y, int m, int d) /* 0 = Sunday */
{
static int t[] = {0, 3, 2, 5, 0, 3, 5, 1, 4, 6, 2, 4};
y -= m < 3;
return (y + y/4 -y/100 + y/400 + t[m-1] + d) % 7;
}
参见问题13.12 和20.28。
参考资料:[ISO,Sec.7.12.2.3]。
20.28 (year%4==0)是否足够判断润年?2000年是闰年吗?
这个测试并不足够(2000年是闰年)。对于当前用的格里高力历法,完整的表达式为:
year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)
详情请参阅一本好的天文历法的书或其它参考资料。预防无休止的辩论;那些主张还有一个4000年规则的参考资料是错的。参见问题13.12。
20.29 一个难题:怎样写一个输出自己源代码的程序?
要写一个可移植的自我再生的程序是件很困难的事,部分原因是因为引用和字符集的难度。
这里是个经典的例子(应该以一行表示的,虽然第一次执行后它后自我修复):
char*s="char*s=%c%s%c;main(){printf(s,34,s,34);}";
main(){printf(s,34,s,34);}
这段程序有一些依赖,忽略了#include<stdio.h>,还假设了双引号"的值为34,和ASCII中的值一样。
这里还有一个有JamesHu发布的改进版:
#define q(k)main(){return!puts(#k"/nq("#k")");}
q(#define q(k)main(){return!puts(#k"/nq("#k")");})
20.30 什么是“达夫设备”(Du.’sDevice)?
这是个很棒的迂回循环展开法,由TomDu.在Lucas.lm时所设计。它的“传统”形态,是用来复制多个字节:
register n = (count + 7) / 8; /* count > 0 assumed */
switch (count % 8)
{
case 0: do { *to = *from++;
case 7: *to = *from++;
case 6: *to = *from++;
case 5: *to = *from++;
case 4: *to = *from++;
case 3: *to = *from++;
case 2: *to = *from++;
case 1: *to = *from++;
} while (--n > 0);
}
这里count个字节从from指向的数组复制到to指向的内存地址(这是个内存映射的输出寄存器,这也是为什么它没有被增加)。它把swtich语句和
复制8个字节的循环交织在一起,从而解决了剩余字节的处理问题(当count不是8的倍数时)。相信不相信,象这样的把case标志放在嵌套在
swtich语句内的模块中是合法的。当他公布这个技巧给C的开发者和世界时,Du.注意到C的swtich语法,特别是“跌落”行为,一直是被争议的,
而“这段代码在争论中形成了某种论据,但我不清楚是赞成还是反对”。
20.31 下届国际C混乱代码竞赛(IOCCC)什么时候进行?哪里可以找到当前和以前的获胜代码?
竞赛的时间表随着时间而变化;当前详情请参考http://www.ioccc.org/index. html 。
竞赛的优胜者通常在Usenix会议上公布,结果会在晚些时候公布在网上。前几年(追述到1984年)的获胜代码在ftp.uu.net 有档案(参见问题
18.18),在目录pub/ioccc/ 下; 参阅http://www.ioccc.org/index.html 。
20.32 [K&R1]提到的关健字entry是什么?
它是保留起来允许可能有某些函数有多个不同名字的进入点,就象FOR-TRAN那样。据所有人所知,它从没被实现过(也没人记得为它设想的语法
是怎样的)。它被放弃了,也不是ANSIC的关健字。参见问题1.5。
参考资料:[K&R2,p.259AppendixC]
20.33 C 的名字从何而来?
C源自KenThompson的实验性语言B,而B由MartinRichards的BCPL(BasicCombinedProgrammingLanguage)得到灵感,而BCPL是CPL
(CombinedProgrammingLanguage或也许是CambridgeProgrammingLanguage)的简化版。有一段时间,人们猜测C的后继者会命名为P(BCPL的第三
个字母)而不是D,当然,如今最显见的后裔语言是C++。
20.34 “char”如何发音?
C关健字“char”至少有三种发音:象英文词“char”,“care”或“car”(又或者“character”);你可以任选一个。
20.35 “lvalue”和“rvalue”代表什么意思?
简单的说,“lvalue”是个可以出现在赋值语句左方的表达式;你也可以把它想象成有地址的对象。有关数组的,参见问题6.5。“rvalue”就是
有值的表达式,所以可以用在赋值语句的右方。
20.36 哪里可以取得本FAQ(英文版)的额外副本?
最新的副本可以从ftp.eskimo.com 的目录u/s/scs/C-faq/ 取得.你也可以从网上下载。通常,本FAQ在每个月的第一天会发布在新闻组
comp.lang.c。其中含有期限行,应该可以保留一个月。同时,还有一个简略的版本(也发布在新闻组),以及对于每一个有显著更新的版本,都会
有个改变列表。
本FAQ的各个版本也会发布在新闻组comp.answers和news.answers。有几个网站归档了news.answers的帖子和其它FAQ单子,包括本文;其中的两
个网站:rtfm.mit.edu (目录pub/usenet/news.answers/C-faq/ 和pub/usenet/comp. lang.c/);ftp.uu.net (目录usenet/news.answers/C-
faq/)。如果你没有ftp的访问,rtfm.mit.edu 有个邮件服务器可以电邮给你FAQ的名单:发个只有一个词“help”的电邮到mail-
server@rtfm.mit.edu。更多的信息,请参阅news.answers中的meta-FAQ。
本FAQ还有一个超文本(HTML)的版本在万维网上;URLhttp://www. eskimo.com/~scs/C-faq/top.html。一个非常全面的含有所有UsenetFAQ的
网站是http://www.faqs.org/faqs/ 。
本FAQ的扩展版本已经由Addison-Wesley出版了:《CProgrammingFAQs:FrequentlyAskedQuestions》(ISBN0-201-84519-9)[CFAQ]。勘误表在
http:// www.eskimo.com/~scs/C-faq/book/Errata.html 以及ftp://ftp.eskimo.com/u/s/ scs/ftp/C-faq/book/Errata 。
【译者注】最新的HTML中译版本可以在http://c-faq-chn.sourceforge.net/ 取得。另外在同一地址还提供PDF版本的下载。
最后
以上就是隐形招牌为你收集整理的C 语言常见问题集的全部内容,希望文章能够帮你解决C 语言常见问题集所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复