C-FAQ

目录

一、声明和初始化

1.1 main的定义

1.2 对于没有初始化的变量的初始值可以作怎样的假定 

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

二、结构、联合和枚举

2.1 我的编译器在结构中留下了空洞, 这导致空间浪费而且无法与外部数据文件进行 ”二进制” 读写。 能否关掉填充, 或者控制结构域的对齐方式?

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

2.3 枚举和一组预处理的 #define 有什么不同

三、表达式

3.1 这是个巧妙的表达式: a ˆ= b ˆ= a ˆ= b 它不需要临时变量就可以交换 a 和 b 的值。

3.2 我怎样才能理解复杂表达式?“序列点” 是什么?

3.3 为什么如下的代码 int a = 100, b = 100; long int c = a * b; 不能工作?

3.4 我需要根据条件把一个复杂的表达式赋值给两个变量中的一个。可以用下边这样的代码吗? ((condition) ? a : b) = complicated expression;

四、指针

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

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

4.3 我有一个函数 extern int f(int *); 它接受指向 int 型的指针。 我怎样用引用方式传入一个常数?下面这样的调用 f(&5); 似乎不行。

五、空指针

六、数组和指针

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

6.2 在 C 语言中 “指针和数组等价” 到底是什么意思?

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

七、内存分配

7.1 为什么在调用 malloc() 时, 我得到 “警告: 整数赋向指针需要类型转换”

7.2 为什么有些代码小心地把 malloc 返回的值转换为分配的指针类型

7.3 我用一行这样的代码分配一个巨大的数组, 用于数字运算: double*array = malloc(300 * 300 * sizeof( double )); malloc() 并没有返回 null, 但是程序运行得有些奇怪, 好像改写了某些内存, 或者malloc() 并没有分配我申请的那么多内存, 云云。

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

7.3 向 realloc() 的第一个参数传入空指针合法吗?你为什么要这样做?

八、字符和字符串

8.1 为什么 strcat(string, ’!’); 不行?

8.2 如果我可以写 char a[] = "Hello, world!"; 为什么我不能写 chara[14]; a = "Hello, world!";字符串是数组, 而你不能直接用数组赋值。 可以使用 strcpy() 代替:

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

九、布尔表达式和变量

十、C预处理器

10.1 我第一次把一个程序分成多个源文件, 我不知道该把什么放到 .c 文件, 把什么放到 .h 文件。 (“.h” 到底是什么意思?)

10.2 完整的头文件搜索规则是怎样的?

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

10.4我得到了一些代码, 里边有太多的 #ifdef。 我不想使用预处理器把所有的 #include 和 #ifdef 都扩展开, 有什么办法只保留一种条件的代码呢?

10.5 我有些旧代码, 试图用这样的宏来构造标识符 #define Paste(a,b) a/**/b 但是现在不行了。

十一、ANSI/ISO 标准 C

11.1 在我用 #ifdef 去掉的代码里出现了奇怪的语法错误。

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

十二、标准输入输出库

12.1 这样的代码有什么问题?char c; while((c = getchar()) != EOF)

12.2 对于 size t 那样的类型定义, 当我不知道它到底是 long 还是其它类型的时候, 我应该使用什么样的 printf 格式呢?

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

12.4 为什么这些代码 double d; scanf("%f", &d); 不行?

12.5 当我用 “%d\n” 调用 scanf 从键盘读取数字的时候, 好像要多输入一行函数才返回。

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

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

12.8 我怎样才知道对于任意的 sprintf 调用需要多大的目标缓冲区?怎样才能避免 sprintf() 目标缓冲区溢出?

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

12.10 既然 fflush() 不能, 那么怎样才能清除输入呢?

十三、库函数

13.1 怎样在 C 程序中取得当前日期或时间?

13.2 我知道库函数 localtime() 可以把 time t 转换成结构 struct tm,而 ctime() 可以把 time t 转换成为可打印的字符串。 怎样才能进行反向操作, 把 struct tm 或一个字符串转换成 time t?

13.3 怎样在日期上加 N 天?怎样取得两个日期的时间间隔?

13.4 有什么好的方法来验对浮点数在 “足够接近” 情况下的等值?

十四、浮点运算

十五、可变参数

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

15.2 我有个接受 float 的可变参函数, 为什么 va arg(argp, float) 不工作?

15.3 va arg() 不能得到类型为函数指针的参数。

十六、奇怪的问题

16.1 为什么过程调用不工作?编译器似乎直接跳过去了。

16.2 程序在执行用之前就崩溃了, 用调试器单步跟进, 在 main() 之前就死了。

十七、风格

十八、工具和资源

18.1 怎样抓捕棘手的 malloc 问题?

18.2 哪里可以找到兼容 ANSI 的 lint?

十九、系统依赖

19.1 遇到 “Too many open files (打开文件太多)” 的错误, 怎样增加同时打开文件的允许数目?

19.2 怎样才能发现程序自己的执行文件的全路径?

19.3 怎样实现精度小于秒的延时或记录用户回应的时间?

19.4 怎样很好地处理浮点异常?

二十、杂项

20.1 怎样不用临时变量而交换两个值?

20.2 assert() 是什么?怎样用它?


《C 语言常见问题集》中摘录的自己需要注意的部分,原文下载链接:PDF版本HTML版本。摘录部分内容基本上自己理解之后,粘贴的文档中的原文,有任何疑问,欢迎留言讨论。

一、声明和初始化

1.1 main的定义

C99中规定的正确定义方式:

int main(void)
int main(int argc,char **argv)

错误的定义:

 void main()

解释:

          「C语言」int main还是void main?

1.2 对于没有初始化的变量的初始值可以作怎样的假定 

       对于没有初始化的变量的初始值可以作怎样的假定具有 “静态” 生存期的未初始化变量 (即, 在函数外声明的变量和有静态存储类型的变量) 可以确保初始值为零, 就像程序员键入了 “=0” 一样。 因此, 这些变量如果是指针会被初始化为正确的空指针, 如果是浮点数会被初始化为 0.0 。
       具有 “自动” 生存期的变量 (即, 没有静态存储类型的局部变量) 如果没有显示地初始化, 则包含的是垃圾内容。 对垃圾内容不能作任何有用的假设。
       用 malloc() 和 realloc() 动态分配的内存也可能包含垃圾数据, 因此必须由调用者正确地初始化。 用 calloc() 获得的内存为全零, 但这对指针和浮点值不一定有用。

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

       字符串常量有两种稍有区别的用法。 用作数组初始值 (如同在 char a[] 的声明中), 它指明该数组中字符的初始值;

       其它情况下, 它会转化为一个无名的静态字符数组, 可能会存储在只读内存中, 这就是造成它不一定能被修改。

       在表达式环境中, 数组通常被立即转化为一个指针 (参见第 6 章), 因此第二个声明把 p 初始化成指向无名数组的第一个元素。

二、结构、联合和枚举

2.1 我的编译器在结构中留下了空洞, 这导致空间浪费而且无法与外部数据文件进行 ”二进制” 读写。 能否关掉填充, 或者控制结构域的对齐方式?


这些 “空洞” 充当了 “填充”, 为了保持结构中后面的域的对齐, 这也许是必须的。 为了高效的访问, 许多处理器喜欢 (或要求) 多字节对象 (例如, 结构中任何大于 char 的类型) 不能处于随意的内存地址, 而必须是 2 或 4 或对象大小的倍数。编译器可能提供一种扩展用于这种控制 (可能是 #pragma; #pragam 指令提供了一种单一的明确定义的 “救生舱”, 可以用作各种 (不可移植的) 实现相关的控制和扩展: 源码表控制、 结构压缩、 警告去除 (就像 lint 的老 /* NOTREACHED */ 注释), 等等),但是没有标准的方法。

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

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

#define offsetof(type, f)     ((size_t)((char *)&((type *)0)->f - (char *)(type *)0))

       这种实现不是 100% 的可移植; 某些编译器可能会合法地拒绝接受。

2.3 枚举和一组预处理的 #define 有什么不同

       只有很小的区别。 C 标准中允许枚举和其它整形类别自由混用而不会出错。( 但是, 假如编译器不允许在未经明确类型转换的情况下混用这些类型, 则聪明地使用枚举可以捕捉到某些程序错误。)

       枚举的一些优点: 自动赋值;调试器在检验枚举变量时, 可以显示符号值;它们服从数据块作用域规则。 ( 编译器也可以对在枚举变量被任意地和其它类型混用时, 产生非重要的警告信息, 因为这被认为是坏风格。 ) 一个缺点是程序员不能控制这些对非重要的警告; 有些程序员则反感于无法控制枚举变量的大小。

三、表达式

3.1 这是个巧妙的表达式: a ˆ= b ˆ= a ˆ= b 它不需要临时变量就可以交换 a 和 b 的值。

这不具有可移植性。 它试图在序列点之间两次修改变量 a, 而这是无定义的。
例如,有人报告如下代码:

int a = 123, b = 7654;
a ^= b ^= a ^= b;


在 SCO 优化 C 编译器 (icc) 下会把 b 置为 123, 把 a 置为 0。

3.2 我怎样才能理解复杂表达式?“序列点” 是什么?

序列点是一个时间点(在整个表达式全部计算完毕之后或在 ||、 &&、 ? : 或逗号运算符处, 或在函数调用之前), 此刻尘埃落定, 所有的副作用都已确保结束。
ANSI/ISO C 标准这样描述:

在上一个和下一个序列点之间, 一个对象所保存的值至多只能被表达式的计算修改一次。 而且前一个值只能用于决定将要保存的值。

第二句话比较费解。 它说在一个表达式中如果某个对象需要写入, 则在同一表达式中对该对象的访问应该只局限于直接用于计算将要写入的值。 这条规则有效地限制了只有能确保在修改之前才访问变量的表达式为合法。

例如 i = i+1 合法, 而 a[i] = i++ 则非法。

为什么这样的代码:a[i] = i++; 不能工作?子表达式 i++ 有一个副作用 — 它会改变 i 的值 — 由于 i 在同一表达式的其它地方被引用,这会导致无定义的结果,无从判断该引用(左边的 a[i] 中)是旧值还是新值。那么,对于 a[i] = i++; 我们不知道 a[] 的哪一个分量会被改写,但 i 的确会增加 1,对吗?不一定!如果一个表达式和程序变得未定义,则它的所有方面都会变成未定义。

3.3 为什么如下的代码 int a = 100, b = 100; long int c = a * b; 不能工作?

根据 C 的内部类型转换规则, 乘法是用 int 进行的, 而其结果可能在转换为long 型并赋给左边的 c 之前溢出或被截短。 可以使用明确的类型转换, 强迫乘法以 long 型进行:

long int c = (long int)a * b;
注意, (long int)(a * b) 不能达到需要的效果。

当两个整数做除法而结果赋与一个浮点变量时, 也有可能有同样类型的问题,解决方法也是类似的。

3.4 我需要根据条件把一个复杂的表达式赋值给两个变量中的一个。可以用下边这样的代码吗? ((condition) ? a : b) = complicated expression;

不能。 ? : 操作符, 跟多数操作符一样, 生成一个值, 而不能被赋值。 换言之, ? : 不能生成一个 “左值”。 如果你真的需要, 你可以试试下面这样的代码:

*((condition) ? &a : &b) = complicated_expression;

尽管这毫无优雅可言。
 

四、指针

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

后缀 ++ 和 -- 操作符本质上比前缀一目操作的优先级高, 因此 *p++ 和*(p++) 等价, 它自增 p 并返回 p 自增之前所指向的值。 要自增 p 指向的值, 使用(*p)++, 如果副作用的顺序无关紧要也可以使用 ++*p。

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

在 C 语言中, 类型转换意味着 “把这些二进制位看作另一种类型, 并作相应的对待”; 这是一个转换操作符, 根据定义它只能生成一个右值 (rvalue)。 而右值既不能赋值, 也不能用 ++ 自增。 (如果编译器支持这样的扩展, 那要么是一个错误, 要么是有意作出的非标准扩展。 ) 要达到你的目的可以用:

p = (char *)((int *)p + 1);

或者,因为 p 是 char * 型, 直接用

p += sizeof(int);

但是, 在可能的情况下, 你还是应该首先选择适当的指针类型, 而不是一味地试图李代桃僵。

4.3 我有一个函数 extern int f(int *); 它接受指向 int 型的指针。 我怎样用引用方式传入一个常数?下面这样的调用 f(&5); 似乎不行。

在 C99 中, 你可以使用 “复合常量”:

f((int[]){5});

在 C99 之前, 你不能直接这样做; 你必须先定义一个临时变量, 然后把它的地址传给函数:

int five = 5;
f(&five);

五、空指针

六、数组和指针

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

你在一个源文件中定义了一个字符串, 而在另一个文件中定义了指向字符的指针。 extern char * 的申明不能和真正的定义匹配。 类型 T 的指针和类型 T 的数组并非同种类型。 请使用 extern char a[ ]。

6.2 在 C 语言中 “指针和数组等价” 到底是什么意思?

在 C 语言中对数组和指针的困惑多数都来自这句话。 说数组和指针 “等价”不表示它们相同, 甚至也不能互换。 它的意思是说数组和指针的算法定义可以用指针方便的访问数组或者模拟数组。特别地, 等价的基础来自这个关键定义:

一个 T 的数组类型的左值如果出现在表达式中会蜕变为一个指向数组第一个成员的指针(除了三种例外情况); 结果指针的类型是 T 的指针。

这就是说, 一旦数组出现在表达式中, 编译器会隐式地生成一个指向数组第一个成员地指针, 就像程序员写出了 &a[0] 一样。 例外的情况是, 数组为 sizeof 或 &操作符的操作数, 或者为字符数组的字符串初始值。作为这个这个定义的后果, 编译器并那么不严格区分数组下标操作符和指针。 在形如 a[i] 的表达式中, 根据上边的规则, 数组蜕化为指针然后按照指针变量的方式如 p[i] 那样寻址, 如问题 6.2 所述, 尽管最终的内存访问并不一样。 如果你把数组地址赋给指针:
p = a;
那么 p[3] 和 a[3] 将会访问同样的成员。

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

数组蜕化为指针的规则  不能递归应用。 数组的数组 (即 C 语言中的二维数组) 蜕化为数组的指针, 而不是指针的指针。 数组指针常常令人困惑, 需要小心对待; 
如果你向函数传递二位数组:

int array[NROWS][NCOLUMNS];
f(array);

那么函数的声明必须匹配:

void f(int a[][NCOLUMNS])
{ ... }

或者

void f(int (*ap)[NCOLUMNS]) /* ap 是个数组指针 */
{ ... }

在第一个声明中, 编译器进行了通常的从 “数组的数组” 到 “数组的指针” 的隐式转换 ; 第二种形式中的指针定义显而易见。 因为被调函数并不为数组分配地址, 所以它并不需要知道总的大小, 所以行数 NROWS 可以省略。 但数组的宽度依然重要, 所以列维度 NCOLUMNS (对于三维或多维数组, 相关的维度) 必须保留。如果一个函数已经定义为接受指针的指针, 那么几乎可以肯定直接向它传入二维数组毫无意义。

七、内存分配

7.1 为什么在调用 malloc() 时, 我得到 “警告: 整数赋向指针需要类型转换”

你包含了 <stdlib.h> 或者正确声明了 malloc() 吗?(这一条虽然没有遇到过,但是还是先留着吧。)

7.2 为什么有些代码小心地把 malloc 返回的值转换为分配的指针类型

在 ANSI/ISO 标准 C 引入 void * 一般指针类型之前, 这种类型转换通常用于在不兼容指针类型赋值时消除警告 (或许也可能导致转换)。
在 ANSI/ISO 标准 C 下, 这些转换不再需要, 而起事实上现代的实践也不鼓励这样做, 因为它们可能掩盖 malloc() 声明错误时产生的重要警告;  (但是, 因为这样那样的原因, 为求与 C++ 兼容, C 程序中常常能见到这样的转换。 在 C++ 中从 void * 的明确转换是必须的。 )(一直都是这么用的,还真是没想到可能会有这些问题。

7.3 我用一行这样的代码分配一个巨大的数组, 用于数字运算: double*array = malloc(300 * 300 * sizeof( double )); malloc() 并没有返回 null, 但是程序运行得有些奇怪, 好像改写了某些内存, 或者malloc() 并没有分配我申请的那么多内存, 云云。

注意 300 * 300 是 90,000, 这在你乘上 sizeof(double) 以前就已经不能放入 16位的 int 中了。 如果你需要分配这样大的内存空间, 你可得小心为妙。 如果在你的机器上 size t (malloc() 接受的类型) 是 32位, 而 int 为 16 位, 你可以写 300 * (300* sizeof(double)) 来避免这个问题。 否则, 你必须把你的数据结构分解为更小的块, 或者使用 32 位的机器或编译器, 或者使用某种非标准的内存分配函数。(分配大内存的注意事项,尽管可能用于也用不上

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

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

7.3 向 realloc() 的第一个参数传入空指针合法吗?你为什么要这样做?

ANSI C 批准了这种用法, 以及相关的 realloc(. . . , 0), 用于释放, 尽管一些早期的实现不支持, 因此可能不完全可移植。 向 realloc() 传入置空的指针可以更容易地写出自开始的递增分配算法。(我真的为了递增分配地址,写过这种代码,原来是有风险的

八、字符和字符串

我的字符串处理能力是真的差......

8.1 为什么 strcat(string, ’!’); 不行?

字符和字符串的区别显而易见, 而 strcat() 用于连接字符串。C 中的字符用它们的字符集值对应的小整数表示, 参见下边的问题 8.4。 字符
串用字符数组表示; 通常你操作的是字符数组的第一个字符的指针。 二者永远不能混用。 要为一个字符串增加 !, 需要使用

strcat(string, "!");

8.2 如果我可以写 char a[] = "Hello, world!"; 为什么我不能写 chara[14]; a = "Hello, world!";
字符串是数组, 而你不能直接用数组赋值。 可以使用 strcpy() 代替:

strcpy(a, "Hello, world!");

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

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

int main()
{
	char a = 'a';

	printf("sizeof('a') = %d, sizeof(a) = %d\n", sizeof('a'), sizeof(a));

	return 0;
}

输出结果为:

sizeof('a') = 4, sizeof(a) = 1

九、布尔表达式和变量

十、C预处理器

10.1 我第一次把一个程序分成多个源文件, 我不知道该把什么放到 .c 文件, 把什么放到 .h 文件。 (“.h” 到底是什么意思?)

(一开始写代码的时候,确实会为了这些问题纠结。)作为一般规则, 你应该把这些东西放入头 (.h) 文件中:

  • 宏定义 (预处理 #defines)
  • 结构、 联合和枚举声明
  • typedef 声明
  • 外部函数声明
  • 全局变量声明

当声明或定义需要在多个文件中共享时, 尤其需要把它们放入头文件中。 特别是, 永远不要把外部函数原型放到 .c 文件中。另一方面, 如果定义或声明为一个 .c 文件私有, 则最好留在 .c 文件中。

10.2 完整的头文件搜索规则是怎样的?

准确的的行为是由实现定义的, 这就是应该有文档说明。 通常, 用 <> 括起来的头文件会先在一个或多个标准位置搜索。 用 "" 括起来的头文件会首先在 “当前目录” 中搜索, 然后 (如果没有找到) 再在标准位置搜索。(这个回答,并没有完整的解释描述的问题。在一个大工程中,有时会对这个“当前目录”的概念比较疑惑:只有该.c文件的目录才叫当前目录吗,同project的其他文件夹呢,这里需要有时间再做补充

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

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

10.4我得到了一些代码, 里边有太多的 #ifdef。 我不想使用预处理器把所有的 #include 和 #ifdef 都扩展开, 有什么办法只保留一种条件的代码呢?

有几个程序 unifdef, rmifdef 和 scpp (selective C preprocessor) 正是完成这种工作的。(强大的source insight为我们集成了这个功能

10.5 我有些旧代码, 试图用这样的宏来构造标识符 #define Paste(a,b) a/**/b 但是现在不行了。


这是有些早期预处理器实现 (如 Reiser) 的未公开的功能, 注释完全消失, 因而可以用来粘结标识符。 但 ANSI 确认 (如 K&R所言) 注释用空白代替。 然而对粘结标识符的需求却十分自然和广泛, 因此 ANSI 引入了一个明确定义的标识符粘结操作符 —— ##, 它可以象这样使用

#define Paste(a, b) a##b

##还没用溜呢,又冒出来个/**/

十一、ANSI/ISO 标准 C

11.1 在我用 #ifdef 去掉的代码里出现了奇怪的语法错误。

在 ANSI C 中, 被 #if, #ifdef 或 #ifndef “关掉” 的代码仍然必须包含 “合法的预处理符号”。 这意味着字符 " 和 ’ 必须像在真正的 C 代码中那样严格配对, 且这样的配对不能跨行。 特别要注意缩略语中的撇号看起来很像字符常量的开始。 因此, 自然语言的注释和伪代码必须写在 “正式的” 注释分界符 /* 和 */ 中。

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

如果源和目的参数有重叠, memmove() 提供有保证的行为。 而 memcpy()则不能提供这样的保证, 因此可以实现得更加有效率。 如果有疑问, 最好使用memmove()。(man描述,memmove可以用于操作src和dst重合的情况会先把src的内容复制到一个与src和dst不重合的临时空间,再从临时空间拷贝到dst)

十二、标准输入输出库

12.1 这样的代码有什么问题?char c; while((c = getchar()) != EOF)

保存 getchar 的返回值的变量必须是 int 型。 getchar() 可能返回任何字符值, 包括 EOF。 如果把 getchar 的返回值截为 char 型, 则正常的字符可能会被错误的解释为 EOF, 或者 EOF 可能会被修改 (尤其是 char 型为无符号的时候), 从而永不出现。

12.2 对于 size t 那样的类型定义, 当我不知道它到底是 long 还是其它类型的时候, 我应该使用什么样的 printf 格式呢?

把那个值转换为一个已知的长度够大的类型, 然后使用与之对应的 printf 格式。 例如, 输出某种类型的长度, 你可以使用
 

printf("%lu", (unsigned long)sizeof(thetype));

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

printf("%*d", width, x) 

12.4 为什么这些代码 double d; scanf("%f", &d); 不行?

跟 printf() 不同, scanf() 用 %lf 代表双精度数, 用 %f 代表浮点数。

12.5 当我用 “%d\n” 调用 scanf 从键盘读取数字的时候, 好像要多输入一行函数才返回。

可能令人吃惊, \n 在 scanf 格式串中不表示等待换行符, 而是读取并放弃所有的空白字符。

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

scanf %d 不处理结尾的换行符。 如果输入的数字后边紧接着一个换行符, 则换行符会被 gets() 处理。
作为一个一般规则, 你不能混用 scanf() 和 gets(), 或任何其它的输入例程的调用;scanf 对换行符的特殊处理几乎一定会带来问题。 要么就用 scanf() 处理所有的输入, 要么干脆不用。
 

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

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

12.8 我怎样才知道对于任意的 sprintf 调用需要多大的目标缓冲区?怎样才能避免 sprintf() 目标缓冲区溢出?

当用于 sprintf() 的格式串已知且相对简单时, 你有时可以预测出缓冲区的大小。 如果格式串中包含一个或两个 %s, 你可以数出固定字符的个数再加上对插入的字符串的 strlen() 调用的返回值。 对于整形, %d 输出的字符数不会超过((sizeof(int) * CHAR_BIT + 2) / 3 + 1) /* +1 for ’-’ */CHAR BIT 在 <limits.h> 中定义, 但是这个计算可能有些过于保守了。 它计算的是数字以八进制存储需要的字节数; 十进制的存储可以保证使用同样或更少的字节数。
当格式串更复杂或者在运行前未知的时候, 预测缓冲区大小会变得跟重新实现 sprintf 一样困难, 而且会很容易出错。 有一种最后防线的技术, 就是 fprintf() 向一块内存区或临时文件输出同样的内容, 然后检查 fprintf 的返回值或临时文件的大小。
如果不能确保缓冲区足够大, 你就不能调用 sprintf(), 以防缓冲区溢出后改写其它的内存区。 如果格式串已知, 你可以用 %.Ns 控制 %s 扩展的长度, 或者使用%.*s。
要避免溢出问题, 你可以使用限制长度的 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 的内存 */

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

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

12.10 既然 fflush() 不能, 那么怎样才能清除输入呢?

这取决于你要做什么。 如果你希望丢掉调用 scanf() 之后所剩下的换行符和未预知的输入, 你可能需要重写你的 scanf() 或者换掉它,
或者你可以用下边这样的代码吃掉一行中多余的字符

while((c = getchar()) != ’\n’ && c != EOF)
/* 丢弃 */ ;

你也可以使用 curses 的 flushinp() 函数。
没有什么标准的办法可以丢弃标准输入流的未读取字符, 即使有, 那也不够,因为未读取字符也可能来自其它的操作系统级的输入缓冲区。 如果你希望严格丢弃多输入的字符 (可能是预测发出临界提示), 你可能需要使用系统相关的技术。
 

十三、库函数

13.1 怎样在 C 程序中取得当前日期或时间?

只要使用函数 time(), ctime(), localtime() 和/或 strftime() 就可以了。用函数 strftime() 可以控制输出的格式。下面是个简单的例子:

#include <stdio.h>
#include <time.h>

int main()
{
    time_t now;

    time(&now);

    printf("It’s %s", ctime(&now));

    return 0;
}

13.2 我知道库函数 localtime() 可以把 time t 转换成结构 struct tm,而 ctime() 可以把 time t 转换成为可打印的字符串。 怎样才能进行反向操作, 把 struct tm 或一个字符串转换成 time t?

ANSI C 提供了库函数 mktime(), 它把 struct tm 转换成 time t。把一个字符串转换成 time t 比较难些, 这是由于可能遇到各种各样的日期和
时间格式。 某些系统提供函数 strptime(), 基本上是 strftime() 的反向函数。 其它常用的函数有 partime() (与 RCS 包一起被广泛的发布) 和 getdate() (还有少数其它函数, 发布在 C 的新闻组)。

13.3 怎样在日期上加 N 天?怎样取得两个日期的时间间隔?

       ANSI/ISO 标准 C 函数 mktime() 和 difftime() 对这两个问题提供了一些有限的支持。 mktime() 接受没有规格化的日期, 所以可以用一个日期的 struct tm 结构, 直接在 tm mday 域进行加或减, 然后调用 mktime() 对年、 月、 日域进行规格化, 同时也转换成了 time t 值。 可以用 mktime() 来计算两个日期的 time t 值, 然后用 difftime() 计算两个 time t 值的秒数差分。
       但是, 这些方法只有日期在 time t 表达范围内才保证工作正常。 对于保守的 time t, 通常范围是从 1970 年到大约 2037 年; 注意有些 time t 的表达不是按照 Unix 和 Posix 标准的。 tm mday 域是个 int, 所以日偏移量超出 32,736 就会上溢。 还要注意, 在夏令时转换的时候, 一天并不是 24 小时, 所以不要假设可以用86400 整除
       另一个解决的方法是用 “Julian 日期”, 这可以支持更宽的时间范围。 处理 Julian 日期的代码可以在以下地方找到: Snippets 收集 ;Simtel/Oakland 站点 (文件 JULCAL10.ZIP, 参见问题 18.18) 和 文献中提到的文章 “Date conversions” [Burki]

13.4 有什么好的方法来验对浮点数在 “足够接近” 情况下的等值?

浮点数的定义决定它的绝对精确度会随着其代表的值变化, 所以比较两个浮点数的最好方法就要利用一个精确的阈值。 这个阈值和作比较的浮点数值大小有关。 不要用下面的代码:

double a, b;
...
if (a == b) /* 错! */

要用类似下列的方法:

#include <math.h>
if (fabs(a - b) <= epsilon * fabs(a))

epsilon 被赋为一个选定的值来控制 “接近度”。 你也要确定 a 不会为 0

十四、浮点运算

十五、可变参数

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

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

int f(...)
{
...
}

15.2 我有个接受 float 的可变参函数, 为什么 va arg(argp, float) 不工作?

“参数默认晋级” 规则适用于在可变参数中的可变动部分: 参数类型为 float 的总是晋级 (扩展) 到 double, char 和 short int 晋级到 int。 所以 va arg(arpg, float)是错误的用法。 应该总是用 va arg(arpg, double)。 同理, 要用 va arg(argp, int) 来取得原来类型是 char, short 或 int 的参数。 基于相同理由, 传给 va start() 的最后一个 “固定” 参数项的类型不会被晋级

15.3 va arg() 不能得到类型为函数指针的参数。

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

十六、奇怪的问题

16.1 为什么过程调用不工作?编译器似乎直接跳过去了。

代码是否看起来象这样:

myprocedure; /* 我的过程 */

C 只有函数, 而函数调用总要用圆括号将参数括起来, 即使是无参数的函数。用下列代码:

myprocedure();

16.2 程序在执行用之前就崩溃了, 用调试器单步跟进, 在 main() 之前就死了。

       也许你定义了一个或多个非常大的局部数组 (超过上千字节)。 许多系统只有固定大小的堆栈, 即使那些自动动态堆栈分配的系统也会因为一次性要分配大段堆栈而失败。
       一般对大规模数组, 定义为静态的数组会更好。 如果由于递归的原因, 每次都需要一组新的数组, 可以用 malloc() 动态申请内存。

十七、风格

十八、工具和资源

注意: 本章中的信息比较旧, 有些可能已经过时了, 特别是各个公共包的URL。

18.1 怎样抓捕棘手的 malloc 问题?

有好几个调试工具包可以用来抓捕 malloc 问题。 其中一个流行的工具是 Conor P. Cahill 的 dbmalloc, 公布在 comp.sources.misc 1992 年第 32 卷。 还有 leak 公布在 comp.sources.unix 档案第 27 卷; “Snippets” 合集中的 JMalloc.c,JMalloc.h; MEMDEBUG (ftp://ftp.crpht.lu/pub/sources/memdebug); ElectricFence。
还有一些商业调试工具, 对调试 malloc 等棘手问题相当有用:

  • CodeCenter (Saber-C), 出品 Centerline Software (http://www.centerline.com/)
  • Insight (now Insure?), 出品 ParaSoft Corporation (http://www.parasoft.com/)
  • Purify, 出品 Rational Software (http://www-306.ibm.com/software/rational/,原来是 Pure Software, 现在是 IBM 的一部分)
  • ZeroFault, 出品 The ZeroFault Group (http://www.zerofault.com/)

18.2 哪里可以找到兼容 ANSI 的 lint?

PC-Lint 和 FlexeLint 是 Gimpel Software 公司的产品 (http://www.gimpel.com/)。
Unix System V 版本 4 的 lint 兼容 ANSI。 可以从 UNIX Support Labs 或System V 的销售商单独得到 (和其它 C 工具捆绑在一起)。
另外一个兼容 ANSI 的 lint 是 Splint (以前叫 lclint, http://www.splint.org/)。它可以作一些高级别的正式检验。
如果没有 lint, 许多现代的编译器可以作出几乎和 lint 一样多的诊断。 许多网友推荐 gcc -Wall -pedantic。

十九、系统依赖

19.1 遇到 “Too many open files (打开文件太多)” 的错误, 怎样增加同时打开文件的允许数目?

通常有至少两个资源限制了同时打开文件的数目: 操作系统可用的低层 “文件说明符” 或 “文件句柄” 的数目; 和标准 stdio 函数库可用的 FILE 结构数目。 两个条件必须符合。 在 MS-DOS 下, 可以通过设置 CONFIG.SYS, 可以控制系统文件handle 的数目。 一些编译器附有增加 stdio 的 FILE 结构数目的指令 (也许是一两个源文件)。

19.2 怎样才能发现程序自己的执行文件的全路径?

arg[0] 也许含有全部或部分路径, 或者什么也没有。 如果 arg[0] 中的路径不全,你也许可以重复命令语言注释器的路径搜索逻辑。 但是, 没有保证的解决方法。

19.3 怎样实现精度小于秒的延时或记录用户回应的时间?

很 不 幸, 这 没 有 可 移 植 解 决 方 法 。 下 面 是 一 些 你 可 以 在 你 的 系 统 中 寻找的函数: clock(), delay(), ftime(), getimeofday(), msleep(), nap(), napms(),nanaosleep(), setitimer(), sleep(), Sleep(), times() 和 usleep。 至少在 Unix 系统下, 函数 wait() 不是你想要的。 函数 select() 和 poll() (如果存在) 可以用来实现简单的延时。 在 MS-DOS 下, 可以重新对系统计时器和计时器中断编程。这些函数中, 只有 clock() 在 ANSI 标准中。 两次调用 clock() 之间的差分就是执行所用的时间, 如果 CLOCKS PER SEC 的值大于 1, 你可以得到精确度小于秒的计时。 但是, clock() 返回的是执行程序使用的处理器的时间, 在多任务系统下,有可能和真实的时间相差很多。如果你需要实现一个延时, 而你只有报告时间的函数可用, 你可以实现一个繁忙等待。 但是这只是在单用户, 单任务系统下可选, 因为这个方法对于其它进程极不友好。 在多任务系统下, 确保你调用函数, 让你的进程在这段时间进入休眠状态。 可用函数 sleep(), select() 或 poll() 和 alarm() 或 setitimer()实现。
对于非常短暂的延时, 使用一个空循环颇据有诱惑力:
long int i;
for (i = 0; i < 1000000; ++i)
;
但是请尽量抵制这个诱惑! 因为, 经过你仔细计算的延时循环可能在下个月因为更快的处理器出现而不能正常工作。 更糟糕的是, 一个聪明的编译器可能注意到这个循环什么也没做, 而把它完全优化掉。

19.4 怎样很好地处理浮点异常?

在许多系统中, 你可以定义一个 matherr() 的函数, 当出现某些浮点错误时 (例如 <math> 中的数学例程), 它就会被调用。 你也可以使用 signal() 函数截取 SIGFPE 信号。

二十、杂项

20.1 怎样不用临时变量而交换两个值?

一个标准而古老的汇编程序员的技巧是:

a ^= b;
b ^= a;
a ^= b;

但是这样的代码在现代高级程序设计语言中没什么用处。 临时变量基本上是自由使用的, 一般上的三个赋值是:
int t = a;
a = b;
b = t;
这不只对读者更清晰, 更有可能被编译器辨别出来而变成最有效的代码 (例如有可能使用 EXCH 指令)。 后面的代码明显的可以用于指针和浮点值, 而不象XOR 技巧只能用于整型。

20.2 assert() 是什么?怎样用它?

这是个定义在 <assert.h> 中的宏, 用来测试断言。 一个断言本质上是写下程序员的假设, 如果假设被违反, 那表明有个严重的程序错误。 例如, 一个假设只接受非空指针的函数, 可以写:assert(p != NULL);
一个失败的断言会中断程序。 断言不应该用来捕捉意料中的错误, 例如malloc() 或 fopen() 的失败。
 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值