天天看点

读书摘要——《C FAQ》

2.2    64 位机上的64 位类型是什么样的?

    C99 标准定义了long long 类型,其长度可以保证至少64 位。

2.3    怎样定义和声明全局变量和函数最好?

    最好是在某个相关的.c 文件中定义,然后在头文件(.h) 中进行外部声明在需要使用的时候,只要包含对应的头文件即可。定义该变量的.c 文件也应该包含该头文件,以便编译器检查定义和声明的一致性。

2.5    关键字auto 到底有什么用途?

    毫无用途!它已经过时。

2.7    怎样建立和理解非常复杂的声明?例如定义一个包含N 个指向返回指向字符的指针的函数的指针的数组?

    1.    char *(*(*a[N])())();

    2.    用typedef 逐步完成声明:

    typedef char *pc;                 

    typedef pc fpc();                  

    typedef fpc *pfpc;                

    typedef pfpc fpfpc();             

    typedef fpfpc *pfpfpc;           

    pfpfpc a[N];                         

     3. 使用cdecl 程序,它可以把英文翻译成C 或者把C 翻译成英文:

     cdecl> declare a as array of pointer to function returning pointer to function returning pointer to char

     char *(*(*a[])())()

2.12    这样的初始化有什么问题?char *p = malloc(10); 编译器提示"非法初始式" 云云。

    这个声明是静态或非局部变量吗?函数调用只能用于自动变量(即局部非静态变量) 的初始化表达式中。

2.13    以下的初始化有什么区别?char a[] = "string literal"; char *p= "string literal"; 当我向p[i] 赋值的时候,我的程序崩溃了。

     字符串常量有两种稍有区别的用法。用作数组初始值(如同在char a[] 的声明中),它指明该数组中字符的初始值。其它情况下 它会转化为一个无名的静态字符数组,可能会存储在只读内存中,这就造成它不一定能被修改。在表达式环境中,数组通常被立即转化为一个指针,因此第二个声明 把p初始化成指向无名数组的第一个元素。

3.1    声明struct x1 { . . . }; 和typedef struct { . . . } x2; 有什么不同?

    第一种形式声明了一个"结构标签"; 第二种声明了一个"类型定义"。主要的区别是在后文中你需要用"struct x1" 引用第一种,而用"x2" 引用第二种。也就是说第二种声明更像一种抽象类型——用户不必知道它是一个结构,而在声明它的实例时也不需要使用struct 关键字。

3.2    为什么struct x { . . . }; x thestruct; 不对?

    C 不是C++。结构标签不能自动生成类型。

3.6    我遇到这样声明结构的代码: struct name { int namelen; charnamestr[1];}; 然后又使用一些内存分配技巧使namestr 数组用起来好像有多个元素。这样合法和可移植吗?

    这种技术十分普遍,尽管Dennis Ritchie 称之为"和C实现的无保证的亲密接触"。官方的解释认定它没有严格遵守C 标准,尽管它看来在所有的实现中都可以工作。仔细检查数组边界的编译器可能会发出警告。

    另一种可能是把变长的元素尺寸声明得很大,而不是很小; 在上例中:

    ...

    char namestr[MAXSIZE];

    MAXSIZE 比任何可能存储的name 值都大。但是,这种技术似乎也不完全符合标准的严格解释。这些"亲密" 结构都必须小心使用,因为只有程序员知道它的大小,而编译器却一无所知。

    C99 引入了"灵活数组域" (flexible array member),允许结构的最后一个域省略数组大小,为类似问题提供了一个圆满的解决方案。

3.9    怎样从/向数据文件读/写结构?

    用fwrite() 写一个结构相对简单:fwrite(&somestruct,sizeof somestruct,1,fp);对应的fread() 调用可以再把它读回来。但是这样文件读写却不具备良好的可移植性。

    移植性更好的方案是写一对函数,用可移植(可能甚至是人可读) 的方式按域读写结构,尽管开始可能工作量稍大。

3.12    如何确定域在结构中的字节偏移?

    ANSI C 在<stddef.h> 中定义了offsetof() 宏,用offsetof(struct s,f) 可以计算出域f在结构s中的偏移量。如果出于某种原因,你需要自己实现这个功能,可以使用下边这样的代码:

    #define offsetof(type,f) ((size_t) /

    ((char *)&((type *)0)->f - (char *)(type *)0))

4.1    为什么这样的代码: a[i] = i++; 不能工作?

    子表达式i++ 有一个副作用——它会改变i 的值。由于i 在同一表达式的其它地方被引用,这会导致无定义的结果,无从判断该引用(左边的a[i] 中)是旧值还是新值。注意,尽管在K&R 中建议这类表达式的行为不确定,但C 标准却强烈声明它是无定义的。

4.5    我可否用括号来强制执行我所需要的计算顺序?

    一般来讲,不行。运算符优先级和括弧只能赋予表达是计算部分的顺序. 在如下的代码中:f() + g() * h(); 尽管我们知道乘法运算在加法之前,但这并不能说明这三个函数哪个会被首先调用。

4.6    可是&& 和|| 运算符呢?我看到过类似while((c = getchar()) != EOF && c != ’/n’) 的代码

    这些运算符在此处有一个特殊的"短路"例外:如果左边的子表达式决定最终结果(即真对于|| 和 假对于&& ) ,则右边的子表达式不会计算。因此,从左至右的计算可以确保。逗号表达式也是如此。

5.2    *p++ 自增p 还是p 所指向的变量?

    后缀++ 和-- 操作符本质上比前缀一目操作的优先级高,因此*p++ 和*(p++) 等价,它自增p并返回p 自增之前所指向的值。

5.3    我有一个char * 型指针正巧指向一些int 型变量,我想跳过它们。为什么如下的代码((int *)p)++; 不行?

    在C 语言中,类型转换意味着"把这些二进制位看作另一种类型,并作相应的对待";这是一个转换操作符,根据定义它只能生成一个右值(rvalue)。而右值既不能被赋值,也不能用++进行自增。

5.5    我能否用void** 指针作为参数,使函数按引用接受一般指针?

    不可移植。C 中没有一般的指针的指针类型。void* 可以用作一般指针只是因为当它和其它类型相互赋值的时候,如果需要,它可以自动转换成其它类型; 但是,如果试图这样转换所指类型为void* 之外的类型的void** 指针时,这个转换不能完成。

5.7    C 有"按引用传递" 吗?

    真的没有。

    严格地讲,C 总是按值传递。你可以自己模拟按引用传递,定义接受指针的函数,然后在调用时使用& 操作符。事实上,当你向函数传入数组 时,编译器本质上就是在模拟按引用传递。但是C 没有任何真正等同于正式的按引用传递或C++ 的引用参数的东西。另一方面,类似函数的预处理宏可以提供一种"按名称传递"的形式。

6.3    用缩写的指针比较 ``if(p)" 检查空指针是否可靠?如果空指针的内部表达不是 0 会怎么样?

当 C 在表达式中要求布尔值时, 如果表达式等于 0 则认为该值为假, 否则为真。换言之, 只要写出

    if(expr)

无论 ``expr" 是任何表达式, 编译器本质上都会把它当

    if((expr) != 0)

处理。 

    因此,这个问题的答案是:可以!两者之间不存在差别

6.4    NULL是什么,它是怎么定义的?

    作为一种风格,很多人不愿意在程序中到处出 现未加修饰的0。因此定义了预处理宏NULL (在<stdio.h> 和其它几个头文件中) 为空指针常数,通常是0 或者((void *)0) 。希望区别整数0 和空指针0 的人可以在需要空指针的地方使用NULL。

    NULL 只是一种风格习惯;预处理器把所有的NULL 都还原回0。

6.5     在使用非全零作为空指针内部表达的机器上,NULL 是如何定义的?

    跟其它机器一样:定义为0或某种形式的0。 当程序员请求一个空指针时,无论写"0" 还是"NULL",都是由编译器来生成适合机器的空指针的二进制表达形式。因此,在空指针的内部表达不为0 的机器上定义NULL 为0,跟在其它机器上一样合法:编译器在指针上下文看到的未加修饰的0 都会被生成正确的空指针。

21。NULL 定义成#define NULL ((char *)0) 难道不就可以向函数传入不加转换的NULL 了吗?

    一般情况下,不行。复杂之处在于,有的机器不同类型数据的指针有不同的内部表达。这样的NULL 定义对于接受字符指针的的函数没有问题,但对于其它类型的指针参数仍然有问题(在缺少原型的情况下),而合法的构造如 FILE *fp = NULL;则会失败。

    不过,ANSI C 允许NULL 的可选定义 #define NULL ((void *)0)

6.7    如果NULL 和0 作为空指针常数是等价的,那我到底该用哪一个呢?

    C 程序员应该明白,在指针上下文中NULL 和0 是完全等价的,未加修饰的0 也完全可以接受。任何使用NULL (跟0 相对) 的地方都应该看作一种温和的提示,是在使用指针; 程序员和编译器都不能依靠它来区别指针0 和整数0。

6.8    但是如果NULL 的值改变了,比如在使用非零内部空指针的机器上,难道用NULL (而不是0) 不是更好吗?

    不。用NULL 可能更好,但不是这个原因。尽管符号常量经常代替数字使用以备数字的改变,但这不是用NULL 代替0 的原因。语言本身确保了源码中的0 (用于指针上下文) 会生成空指针。NULL 只是用作一种格式习惯。

6.10    这有点奇怪。NULL 可以确保是0,但空(null) 指针却不一定?

    随便使用术语"null" 或"NULL" 时,可能意味着以下一种或几种含义:

    1.    概念上的空指针,抽象语言概念。它使用以下的东西实现。

    2.    空指针的内部(或运行期) 表达形式,这可能并不是全零,而且对不用的指针类型可能不一样。真正的值只有编译器开发者才关心。C 程序的作者永远看不到它们,因为他们使用。。。

    3.    空指针常数,这是一个常整数0 。

    4.    NULL 宏,它被定义为0 。

    5.    ASCII 空字符(NUL),它的确是全零,但它和空指针除了在名称上以外,没有任何必然关系。

    6.    "空串" (null string),它是内容为空的字符串("")。在C 中使用空串这个术语可能令人困惑,因为空串包括空字符(’/0’),但不包括空指针。

    本文用短语"空指针" ("null pointer",小写) 表示第一种含义,标识"0" 或短语"空指针常数" 表示含义3,用大写NULL 表示含义4。

7.1    我在一个源文件中定义了char a[6],在另一个中声明了extern char *a 。为什么不行?

    你在一个源文件中定义了一个字符串,而在另一个文件中定义了指向字符的指针。extern char * 的声明和真正的定义无法匹配。类型T 的指针和类型T的数组并非同种类型。

7.3    那么,在C 语言中"指针和数组等价" 到底是什么意思?

    在C 语言中对数组和指针的困惑多数都来自这句话。说数组和指针"等价"不表示它们相同,或者能够互换。它的意思是说数组和指针的运算法则允许使用指针方便的访问数组或者模拟数组。

    特别需要注意的是,等价的基础来自这个关键定义:

    一个类型T的数组出现在表达式中,会蜕变为一个指向数组第一个成员的指针,除了三种例外情况。

    这就是说,一旦数组出现在表达式中,编译器会隐式地生成一个指向数组第一个成员地指针,就像程序员写出了&a[0] 一样。

    例外的情况是,数组为sizeof 或&操作符的操作数,或者为字符数组的字符串初始值。

    作为这个这个定义的后果,编译器并那么不严格区分数组下标操作符和指针。

7.8    我遇到一些"搞笑" 的代码,包含5["abcdef"] 这样的"表达式"。这为什么是合法的C 表达式呢?

    是的,数组和下标在C 语言中可以互换。这个奇怪的事实来自数组下标的指针定义,即对于任何两个表达式a 和e,只要其中一个是指针表达式而另一个为整数,则a[e] 和*((a)+(e)) 完全一样。这种交换性在许多C 语言的书中被看作值得骄傲的东西,但是它除了在混乱C 语言竞赛之外,其实鲜有用武之地。

7.9    既然数组引用会蜕化为指针,如果arr 是数组,那么arr 和&arr 又有什么区别呢?

    区别在于类型。

    在标准C 中,&arr 生成一个"T 型数组" 的指针,指向整个数组。

    在所有的C 编译器中,对数组的简单引用(不包括& 操作符)生成一个类型为T的指针,指向数组的第一元素。

7.10    我如何声明一个数组指针?

    通常,你不需要。当人们随便提到数组指针的时候,他们通常想的是指向它的第一个元素的指针。

    如果你真的需要声明指向整个数组的指针,使用类似"int (*ap)[N];" 这样的声明。其中N 是数组的大小

7.15    当我向一个接受指针的指针的函数传入二维数组的时候,编译器报错了。

    数组蜕化为指针的规则不能递归应用。数组的数组(即C 语言中的二维数组) 蜕化为数组的指针,而不是指针的指针。

    如果一个函数已经定义为接受指针的指针,那么几乎可以肯定直接向它传入二维数组毫无意义。

7.18    当数组是函数的参数时,为什么sizeof 不能正确报告数组的大小?

    编译器把数组参数当作指针对待,因而报告的是指针的大小。

8.10    在调用malloc() 的时候,错误"不能把void * 转换为int *" 是什么意思?

    说明你用的是C++ 编译器而不是C 编译器。

8.19    当我malloc() 为一个函数的局部指针分配内存时,我还需要用free() 明确的释放吗?

    是的。记住指针和它所指向的东西是完全不同的。局部变量在函数返回时就会释放,但是在指针变量这个问题上,这表示指针被释放,而不是它所指向的对 象。用malloc() 分配的内存直到你明确释放它之前都会保留在那里。一般地,对于每一个malloc() 都必须有个对应的free() 调用。

8.22    我有个程序分配了大量的内存,然后又释放了。但是从操作系统看,内存的占用率却并没有回去。

    多数malloc/free 的实现并不把释放的内存返回操作系统,而是留着供同一程序的后续malloc() 使用。

9.5    我认为我的编译器有问题: 我注意到sizeof(’a’) 是2 而不是1 (即,不是sizeof(char))。

    可能有些令人吃惊,C语言中的字符常量是int 型,因此sizeof(’a’) 是sizeof(int),这是另一个与C++ 不同的地方。

11.5    一个头文件可以包含另一头文件吗?

    这是个风格问题,因此有不少的争论。

    很多人认为"嵌套包含文件" 应该避免:它让相关定义更难找到; 如果一个文件被包含了两次,它会导致重复定义错误;它会增大编译时间,同时会令makefile 的人工维护十分困难。

    另一方面,它使模块化使用头文件成为一种可能(一个头文件可以包含它所需要的一切,而不是让每个源文件都包含需要的头文件); 类似grep 的工具(或tags 文件) 使搜索定义十分容易,无论它在哪里;

    一种流行的头文件定义技巧是:

    #ifndef HFILENAME_USED

    #define HFILENAME_USED

    ......

    #endif

    每一个头文件都使用了一个独一无二的宏名。这令头文件可自我识别,以便可以安全的多次包含; 而自动Makefile 维护工具(无论如何,在大型项目中都是必不可少的) 可以很容易的处理嵌套包含文件的依赖问题。

11.12    sizeof 操作符可以用于#if 预编译指令中吗?

    不行。预编译在编译过程的早期进行,此时尚未对类型名称进行分析。作为替代,可以考虑使用ANSI 的<limits.h> 中定义的常量,或者使用"配置"(configure) 脚本。更好的办法是,书写与类型大小无关的代码。

12.7    我不明白为什么我不能象这样在初始化和数组维度中使用常量:const int n = 5; int a[n];

     const 限定词真正的含义是"只读的"; 用它限定的对象是运行时不能被赋值的对象。因此用const 限定的对象的值并不完全是一个真正的常量。在这点上C 和C++ 不一样。

12.10    为什么我不能向接受const char ** 的函数传入char **?

    你可以向接受const-T 的指针的地方传入T 的指针(任何类型T 都适用)。但是,这个允许在带修饰的指针类型上轻微不匹配的规则却不能递归应用,而只能用于最上层。

    如果你必须赋值或传递除了在最上层还有修饰符不匹配的指针,你必须明确使用类型转换(本例中,使用(const char **)),不过,通常需要使用这样的转换意味着还有转换所不能修复的深层次问题。

12.11    怎样正确声明main()?

    int main(),int main(void) 或者int main(int argc,char *argv[]) (显然argc 和argv 的拼写可以随便)。

12.12    我能否把main() 定义为void,以避免扰人的"main无返回值"警告?

    不能。main() 必须声明为返回int,没有参数或者接受适当类型的两个参数。如果你调用了exit() 但还是有警告信息,你可能需要插入一条冗余的return语句(或者使用某种"未到达" 指令,如果有的话)。把函数声明为void 并不仅仅关掉了警告信息:它可能导致与调用者(对于main(),就是C 运行期初始代码) 期待的不同的函数调用/返回顺序。

12.13    可main() 的第三个参数envp 是怎么回事?

    这是一个尽管很常见但却非标准的扩展。如果你真的需要用getenv() 函数提供的标准方法之外的办法读写环境变量,可能使用全局变量environ 会更好——尽管它也同样并不标准。

12.16    我一直用的那本书《熟练傻瓜C语言》总是使用void main()。

    可能这本书的作者把自己也归为目标读者的一员。很多书不负责任地在例子中使用void main(),并宣称这样是正确的。但他们错了。

12.22    "#pragma once" 是什么意思?我在一些头文件中看到了它。

    这是某些预处理器实现的扩展用于使头文件自我识别;它跟问题11.5 中讲到的#ifndef 技巧等价,不过移植性差些。

12.25    memcpy() 和memmove() 有什么区别?

    如果源和目的有重叠,memmove() 提供有保证的行为。而memcpy()则不能提供这样的保证,因此可以实现得更加有效率。如果有疑问,最好使用memmove()。

12.27    为什么ANSI 标准规定了外部标示符的长度和大小写限制?

    问题在于连接器既不受ANSI/ISO 标准的控制也不遵守C 编译器开发者的规定。

13.3    为什么这些代码while(!feof(infp)) { fgets(buf,MAXLINE,infp); fputs(buf,outfp); } 把最后一行复制了两遍?

    在C 语言中,只有函数读并失败以后才能得到文件结束符。换言之,C 的I/O 和Pascal 的不一样。通常你只需要检查输入例程的返回值,例如,fgets()在遇到文件结束符的时候返回NULL。实际上,在任何情况下都完全没有必要使用feof()。

13.6    我如何在printf 的格式串中输出一个’%’?我试过/%,但是不行。

    只需要重复百分号: %%。n%不行,因为反斜杠'/'是C编译器的转义字符,而这里我们的问题是printf中的转义字符。

13.9    我如何用printf 实现可变的域宽度?就是说,我想在运行时确定宽度而不是使用%8d?

    printf("%*d",width,x) 就能达到你的要求。

13.16    我用scanf %d 读取一个数字,然后再用gets() 读取字符串,但是编译器好像跳过了gets() 调用!

    scanf %d 不处理结尾的换行符。如果输入的数字后边紧接着一个换行符,则换行符会被gets() 处理。

    作为一个一般规则,你不能混用scanf() 和gets();scanf 对换行符的特殊处理几乎一定会带来问题。要么就用scanf() 处理所有的输入,要么干脆不用。

13.17    我发现如果坚持检查返回值以确保用户输入的是我期待的数值,则scanf() 的使用会安全很多,但有的时候好像会陷入无限循环。

    在scanf() 转换数字的时候,它遇到的任何非数字字符都会终止转换并被保留在输入流中。因此,除非采用了其它的步骤,那么未预料到的非数字输入会不断阻塞scanf(): scanf() 永远都不能越过错误的非数字字符而处理后边的合法数字字符。如果用户在数字格式的scanf 如%d 或%f 中输入字符‘x’,那么提示后并用同样的scanf() 调用重试的代码会立即遇到同一个’x’。

13.18    为什么大家都说不要使用scanf()?那我该用什么来代替呢?

    scanf() 有很多问题而且它的%s 格式有着和gets() 一样的问题——很难保证缓冲区不出现溢出情况。

    更一般地讲,scanf() 的设计适用于相对结构化的、格式整齐的输入。设计上,它的名称就是来自于"scan formatted"。如果你留心的话,它会告诉你成功或失败,但它只能提供失败的大略位置,至于失败的原因,就无从得知了。

13.20    为什么大家都说不要使用gets()?

    跟fgets() 不同,gets() 不能指明输入缓冲区的大小,因此不能避免缓冲区的溢出。标准库的fgets() 函数对gets() 作了很大的改进,尽管它仍然不完善。

13.22    fgetops/fsetops 和ftell/fseek 之间有什么区别? fgetops() 和fsetops() 到底有什么用处?

    ftell() 和fseek() 用长整型表示文件内的偏移(位置),因此,偏移量被限制在2**31-1以内。而新的fgetpos() 和fsetpos() 函数使用了一个特殊的typedef fpos_t 来表示偏移量。这个typedef会选择合适的值,因此,fgetpos() 和fsetpos 可以表示任意大小的文件偏移。fgetpos() 和gsetpos() 也可以用来记录多字节流式文件的状态。

13.23    如何清除多余的输入,以防止在下一个提示符下读入?fflush(stdin)可以吗?

    fflush() 仅对输出流有效。它对"flush" 的定义是用于完成输出缓冲字符的输出,而不是放弃剩余的输出缓冲。

14.6    我想用strcmp() 作为比较函数,调用qsort() 对一个字符串数组排序,但是不行。

    你说的"字符串数组" 实际上是"字符指针数组"。qsort 比较函数的参数是被排序对象的指针,在这里,也就是字符指针的指针。然而strcmp() 只接受字符指针。因此,不能直接使用strcmp()。写一个下边这样的间接比较函数:

    int pstrcmp(const void *p1,const void *p2)

    {

    return strcmp(*(char * const *)p1,*(char * const *)p2);

    }

    比较函数的参数表示为"一般指针" const void *。然后,它们被转换回本来表示的类型(指向字符指针的指针),再解引用,生成可以传入strcmp() 的char*。

    不要被[K&R2] 5.11 节119-20页的讨论所误导,那里讨论的不是标准库中的qsort。

14.14    怎样获得在一定范围内的随机数?

    直接的方法是      rand() % N     

    但这个方法不好,因为许多随机数发生器的低位比特并不随机。

    一个较好的方法是:    (int)((double)rand() / ((double)RAND_MAX + 1) * N)

    如果你不希望使用浮点,另一个方法是:rand() / (RAND_MAX / N + 1)

    RAND_MAX 是个常数,它告诉你C 库函数rand() 的固定范围。

14.18    我不断得到库函数未定义错误,但是我已经#inlude 了所有用到的头文件了。

    通常头文件只包含外部说明。某些情况下,特别是如果是非标准函数,当你连接程序时需要指定正确的函数库以得到函数的义。

15.4    浮点计算程序表现奇怪,在不同的机器上给出不同的结果。

    回想一下,电脑一般都是用一种浮点的格式来近似的模拟实数的运算,注意是近似而不是精确。下溢、误差的累积和其它非常规性是常遇到的麻烦。

    不要假设浮点运算结果是精确的,特别是不要假设两个浮点值可以进行等值比较。

15.7    为什么C 不提供幂 的运算符?

   因为提供幂乘指令的处理器非常少。

16.3    为什么当n 为long int,printf("%d",n); 编译时没有匹配警告?我以为ANSI 函数原型可以防止这样的类型不匹配。

      当一个函数用可变参数时,它的原型说明没有也不能提供可变参数的数目和类型。所以通常的参数匹配保护不适用于可变参数中的可变部分。编译器不能执行内含的转换或警告不匹配问题。

16.8    为什么编译器不让我定义一个没有固定参数项的可变参数函数?

     标准C 要求用可变参数的函数至少有一个固定参数项,这样你才可以使用va start()。所以编译器不会接受下面定义的函数:

     int f(...)

     {

     ...

     }

16.10    va_arg() 不能得到类型为函数指针的参数。

    宏va_arg() 所用的类型cast不能很好地操作于象函数指针这类过度复杂的类型。但是如果你用typedef 定义一个函数指针类型,那就一切正常了。

17.4     程序执行正确,但退出时崩溃在main() 最后一个语句之后。为什么会这样?

    注意是否错误说明了main()。是否把局部缓冲传给了setbuf() 或setvbuf()。又或者问题出在注册于atexit() 的清理函数。

21.12    指针真得比数组快吗?

    一般的机器, 通常遍历大的数组时用指针比用数组要快, 但是某些处理器就相反。

21.28    (year%4 == 0) 是否足够判断润年?2000 年是闰年吗?

    这个测试并不足够 (2000 年是闰年)。对于当前用的格里高力历法, 完整的表达式为:

    year % 4 == 0 && (year % 100 != 0 || year % 400 == 0)