1.10 对于没有初始化的变量的初始值可以作怎样的假定?如果一个全局变量初始值为 “零”, 它可否作为空指针或浮点零?
答:
具有 “静态” 生存期的未初始化变量 (即, 在函数外声明的变量和有静态存储类型的变量) 可以确保初始值为零, 就像程序员键入了 “=0” 一样。 因此, 这些变量如果是指针会被初始化为正确的空指针, 如果是浮点数会被初始化为 0.0 (或正
确的类型, 参见第 5 章)。具有 “自动” 生存期的变量 (即, 没有静态存储类型的局部变量) 如果没有显示地初始化, 则包含的是垃圾内容。 对垃圾内容不能作任何有用的假设。这些规则也适用于数组和结构 (称为 “聚合体” ); 对于初始化来说, 数组和结构都被认为是 “变量”。用 malloc() 和 realloc() 动态分配的内存也可能包含垃圾数据, 因此必须由调用者正确地初始化。 用 calloc() 获得的内存为全零, 但这对指针和浮点值不一定有用 (参见问题 7.26 和第 5 章)。
2.10 我的编译器在结构中留下了空洞, 这导致空间浪费而且无法与外部数据文件进行“二进制”读写。能否关掉填充, 或者控制结构域的对齐方式?
答:(字节对齐)
这些 “空洞” 充当了 “填充”, 为了保持结构中后面的域的对齐, 这也许是必须的。 为了高效的访问, 许多处理器喜欢 (或要求) 多字节对象 (例如, 结构中任何大于 char 的类型) 不能处于随意的内存地址, 而必须是 2 或 4 或对象大小的倍数。编译器可能提供一种扩展用于这种控制 (可能是 #pragma; 参见问题 11.21),但是没有标准的方法。
参见问题 20.3。2.11 为什么 sizeof 返回的值大于结构的期望值, 是不是尾部有填充?
答:
为了确保分配连续的结构数组时正确对齐, 结构可能有这种尾部填充。 即使结构不是数组的成员, 填充也会保持, 以便 sizeof 能够总是返回一致的大小。
参见问题 2.10。
3.2 使用我的编译器,下面的代码 int i=7; printf("%d\n", i++ * i++); 返回 49?不管按什么顺序计算, 难道不该打印出56吗?
答:
尽管后缀自加和后缀自减操作符 ++ 和 -- 在输出其旧值之后才会执行运算,但这里的“之后”常常被误解。 没有任何保证确保自增或自减会在输出变量原值之后和对表达式的其它部分进行计算之前立即进行。 也不能保证变量的更新会在表达式 “完成” (按照 ANSI C 的术语, 在下一个 “序列点” 之前, 参见问题 3.7) 之前的某个时刻进行。 本例中, 编译器选择使用变量的旧值相乘以后再对二者进行自增运算。
包含多个不确定的副作用的代码的行为总是被认为未定义。 (简单而言, “多个不确定副作用” 是指在同一个表达式中使用导致同一对象修改两次或修改以后又被引用的自增, 自减和赋值操作符的任何组合。 这是一个粗略的定义; 严格的定义参见问题 3.7, “未定义” 的含义参见问题 11.32。 ) 甚至都不要试图探究这些东西在你的编译器中是如何实现的 (这与许多 C 教科书上的弱智练习正好相反); 正如 K&R 明智地指出, “如果你不知道它们在不同的机器上如何实现, 这样的无知可能恰恰会有助于保护你。”
3.5 我可否用括号来强制执行我所需要的计算顺序?
答:一般来讲, 不行。 运算符优先级和括弧只能赋予表达是计算部分的顺序. 在如下的代码中
f() + g() * h()尽管我们知道乘法运算在加法之前, 但这并不能说明这三个函数哪个会被首先调用。如果你需要确保子表达式的计算顺序, 你可能需要使用明确的临时变量和独立的语句
4.4 我有个函数,它应该接受并初始化一个指针 void f(int *ip) { static int dummy = 5; ip = &dummy;} 但是当我如下调用时: int *ip; f(ip); 调用者的指针却没有任何变化。
答:你确定函数初始化的是你希望它初始化的东西吗? 请记住在 C 中, 参数是通过值传递的。 被调函数仅仅修改了传入的指针副本。 你需要传入指针的地址 (函数变成接受指针的指针), 或者让函数返回指针。void f(int**p){...}
4.5 我能否用 void** 指针作为参数, 使函数按引用接受一般指针?
答:不可移植。 C 中没有一般的指针的指针类型。 void* 可以用作一般指针只是因为当它和其它类型相互赋值的时候, 如果需要, 它可以自动转换成其它类型; 但是, 如果试图这样转换所指类型为 void* 之外的类型的 void** 指针时, 这个转换不能完成。
4.8 我看到了用指针调用函数的不同语法形式。 到底怎么回事?
答:最初, 一个函数指针必须用 * 操作符 (和一对额外的括弧) “转换为” 一个 “真正的” 函数才能调用:
int r, func(), (*fp)() = func;r = (*fp)();而函数总是通过指针进行调用的, 所有 “真正的” 函数名总是隐式的退化为指针 (在表达式中, 正如在初始化时一样。 参见问题 1.14)。 这个推论表明无论 fp 是函数名和函数的指针r = fp();ANSI C 标准实际上接受后边的解释, 这意味着 * 操作符不再需要, 尽管依然允许。
6.9 既然数组引用会蜕化为指针, 如果 arr 是数组, 那么 arr 和 &arr 又有什么区别呢?
答:区别在于类型。
在标准 C 中, &arr 生成一个 “T 型数组” 的指针, 指向整个数组。 在 ANSI 之前的 C 中, &arr 中的 & 通常会引起一个警告, 它通常被忽略。 在所有的 C 编译器中, 对数组的简单引用(不包括 & 操作符)生成一个 T 的指针类型的指针, 指向数组的第一成员。
7.17 动态分配的内存一旦释放之后你就不能再使用, 是吧?
答:是的。 有些早期的 malloc() 文档提到释放的内存中的内容会 “保留”, 但这个欠考虑的保证并不普遍而且也不是 C 标准要求的。几乎没有那个程序员会有意使用释放的内存, 但是意外的使用却是常有的事。 考虑下面释放单链表的正确代码:struct list *listp, *nextp;for(listp = base; listp != NULL; listp = nextp) { nextp = listp->next; free(listp);} 请注意如果在循环表达式中没有使用临时变量 nextp, 而使用 listp = listp->next会产生什么恶劣后果。
7.19 当我 malloc() 为一个函数的局部指针分配内存时, 我还需要用free() 明确的释放吗?
答:是的。 记住指针和它所指向的东西是完全不同的。 局部变量在函数返回时就会释放, 但是在指针变量这个问题上, 这表示指针被释放, 而不是它所指向的对象。 用 malloc() 分配的内存直到你明确释放它之前都会保留在那里。 一般地, 对于每一个 malloc() 都必须有个对应的 free() 调用。
7.20 我在分配一些结构, 它们包含指向其它动态分配的对象的指针。 我在释放结构的时候, 还需要释放每一个下级指针吗?
答:是的。 一般地, 你必须分别向 free() 传入 malloc() 返回的每一个指针, 仅仅一次 (如果它的确要被释放的话)。 一个好的经验法则是对于程序中的每一个malloc() 调用, 你都可以找到一个对应的 free() 调用以释放 malloc() 分配的内存。
7.22 我有个程序分配了大量的内存, 然后又释放了。 但是从操作系统看,内存的占用率却并没有回去。
答:多数 malloc/free 的实现并不把释放的内存返回操作系统, 而是留着供同一程序的后续 malloc() 使用.
7.26 calloc() 和 malloc() 有什么区别?利用 calloc 的零填充功能安全吗?free() 可以释放 calloc() 分配的内存吗, 还是需要一个cfree()?
答:calloc(m, n) 本质上等价于p = malloc(m * n);memset(p, 0, m * n);填充的零是全零, 因此不能确保生成有用的空指针值或浮点零值。free() 可以安全地用来释放 calloc() 分配的内存。
8.5 我认为我的编译器有问题: 我注意到 sizeof(’a’) 是 2 而不是 1 (即,不是 sizeof(char))。
答:可能有些令人吃惊, C 语言中的字符常数是 int 型, 因此 sizeof(’a’) 是 sizeof(int),这是另一个与 C++ 不同的地方。
10.4 我第一次把一个程序分成多个源文件, 我不知道该把什么放到 .c 文件, 把什么放到 .h 文件。 (“.h” 到底是什么意思?)
答:作为一般规则, 你应该把这些东西放入头 (.h) 文件中:
· 宏定义 (预处理 #defines)· 结构、 联合和枚举声明· typedef 声明· 外部函数声明(参见问题 1.4)· 全局变量声明当声明或定义需要在多个文件中共享时, 尤其需要把它们放入头文件中。 特别是, 永远不要把外部函数原型放到 .c 文件中。 参见问题 1.3。另一方面, 如果定义或声明为一个 .c 文件私有, 则最好留在 .c 文件中。
10.5 一个头文件可以包含另一头文件吗?
答:这是个风格问题, 因此有不少的争论。 很多人认为 “嵌套包含文件” 应该避免:盛名远播的 “印第安山风格指南”(Indian Hill Style Guide, 参见问题 17.7) 对此嗤之以鼻; 它让相关定义更难找到; 如果一个文件被包含了两次, 它会导致重复定义错误; 同时他会令 makefile 的人工维护十分困难。 另一方面, 它使模块化使用头文件成为一种可能 (一个头文件可以包含它所需要的一切, 而不是让每个源文件都包含需要的头文件); 类似 grep 的工具 (或 tags 文件) 使搜索定义十分容易, 无论它在哪里; 一种流行的头文件定义技巧是:
#ifndef HFILENAME_USED#define HFILENAME_USED... 头文件内容 ...#endif每一个头文件都使用了一个独一无二的宏名。 这令头文件可自我识别, 以便可以安全的多次包含; 而自动 Makefile 维护工具 (无论如何, 在大型项目中都是必不可少的) 可以很容易的处理嵌套包含文件的依赖问题。 参见问题 17.8。参考资料: [Rationale, Sec. 4.1.2]。
10.7 完整的头文件搜索规则是怎样的?
答:准确的的行为是由实现定义的, 这就是应该有文档说明; 参见问题 11.32。
通常, 用 <> 括起来的头文件会先在一个或多个标准位置搜索。 用 "" 括起来的头文件会首先在 “当前目录” 中搜索, 然后 (如果没有找到) 再在标准位置搜索。