《你必须知道的495个C语言问题》笔记--自己的

来源:互联网 发布:ember.js 开发工具 编辑:程序博客网 时间:2024/05/16 09:36

1.void main()正确吗?

很多人甚至市面上的一些书籍,都使用了void main( ) ,其实这是错误的。C/C++ 中从来没有定义过void main( ) 。C++ 之父 

Bjarne Stroustrup 在他的主页上的 FAQ 中明确地写着 The definition void main( ) { /*... */ } is not and never has been C++, 

nor has it even been C.(void main( ) 从来就不存在于 C++ 或者 C )。

正确用法:int main()或者

         int main(int argc,char *argv[])

 

2. 如果一个整型常量是以0开头的,那么这个常量被视为八进制;如果一个整型常量是以0x开头的,那么这个常量被视为16进制。

  1. #include <stdio.h>  
  2.   
  3. int main(void){  
  4.         int a = 0x10;  
  5.         int b = 010;  
  6.         int c = 10;  
  7.         printf("%d\n",a);  
  8.         printf("%d\n",b);  
  9.         printf("%d\n",c);  
  10.         return 0;  
  11. }  


上面代码执行结果是:

16
8
10

 

3. 对于没有初始化的变量的初始值可以作怎样的假定?如果一个全局变量初始值为“零”, 它可否作为空指针或浮点零?

具有 “静态” 生存期的未初始化变量 (即, 在函数外声明的变量和有静态存储类型的变量) 可以确保初始值为零, 就像程序员键入了 “=0” 一样。因此, 这些变量如果是指针会被初始化为正确的空指针, 如果是浮点数会被初始化为 0.0 (或正确的类型, 参见第 5 章)。具有“自动” 生存期的变量 (即, 没有静态存储类型的局部变量) 如果没有显示地初始化, 则包含的是垃圾内容。对垃圾内容不能作任何有用的假设。这些规则也适用于数组和结构 (称为 “聚合体” );

 对于初始化来说, 数组和结构都被认为是 “变量”。用 malloc() 和 realloc() 动态分配的内存也可能包含垃圾数据, 因此必须由调用者正确地初始化。用 calloc() 获得的内存为全零, 但这对指针和浮点值不一定有用。

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

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

 

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

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

 

6. 使用我的编译器,下面的代码 int i=7; printf("%d\n", i++ *i++); 返回 49?不管按什么顺序计算, 难道不该打印出56吗?

尽管后缀自加和后缀自减操作符 ++ 和--在输出其旧值之后才会执行运算,但这里的“之后”常常被误解。没有任何保证确保自增或自减会在输出变量原值之后和对表达式的其它部分进行计算之前立即进行。也不能保证变量的更新会在表达式“完成” (按照 ANSI C 的术语, 在下一个 “序列点” 之前, 参见问题 3.7) 之前的某个时刻进行。本例中, 编译器选择使用变量的旧值相乘以后再对二者进行自增运算。

 

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

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

 

8. 臭名昭著的空指针到底是什么?

语言定义中说明, 每一种指针类型都有一个特殊值 —— “空指针” —— 它与同类型的其它所有指针值都不相同, 它 “与任何对象或函数的指针值都不相等”。也就是说, 取地址操作符 & 永远也不能得到空指针, 同样对 malloc() 的成功调用也不会返回空指针, 如果失败, malloc() 的确返回空指针, 这是空指针的典型用法:表示 “未分配” 或者 “尚未指向任何地方” 的指针。

空指针在概念上不同于未初始化的指针。空指针可以确保不指向任何对象或函数; 而未初始化指针则可能指向任何地方。

 

9. NULL 是什么, 它是怎么定义的?

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

使用 NULL 只是一种风格习惯; 预处理器把所有的 NULL 都还原回 0, 而编译还是依照上文的描述处理指针上下文的 0。特别是, 在函数调用的参数里,NULL之前 (正如在 0 之前) 的类型转换还是需要。


 

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

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

 

11.可是我听说 char a[ ] 和 char *a 是一样的。

并非如此。数组不是指针。数组定义 char a[6] 请求预留 6 个字符的位置, 并用名称 “a”表示。也就是说, 有一个称为 “a” 的位置, 可以放入 6 个字符。而指针申明char *p, 请求一个位置放置一个指针, 用名称 “p”表示。这个指针几乎可以指向任何位置: 任何字符和任何连续的字符, 或者哪里也不指。

一个图形胜过千言万语。

声明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 的指针一旦在表达式中出现就会按照不同的方法计算, 不论它们是否有下标。

 

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

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

特别地, 等价的基础来自这个关键定义:一个 T 的数组类型的左值如果出现在表达式中会蜕变为一个指向数组第一个成员的指针(除了三种例外情况); 结果指针的类型是 T 的指针。

这就是说, 一旦数组出现在表达式中, 编译器会隐式地生成一个指向数组第一个成员地指针, 就像程序员写出了 &a[0] 一样。例外的情况是, 数组为 sizeof 或 &操作符的操作数, 或者为字符数组的字符串初始值。作为这个这个定义的后果, 编译器并那么不严格区分数组下标操作符和指针。

在形如 a[i] 的表达式中, 根据上边的规则, 数组蜕化为指针然后按照指针变量的方式如 p[i] 那样寻址,, 尽管最终的内存访问并不一样。如果你把数组地址赋给指针:p = a;那么 p[3] 和 a[3] 将会访问同样的成员。

 

13. 那么为什么作为函数形参的数组和指针申明可以互换呢?

这是一种便利。由于数组会马上蜕变为指针, 数组事实上从来没有传入过函数。允许指针参数声明为数组只不过是为让它看起来好像传入了数组, 因为该参数可能在函数内当作数组使用。

特别地, 任何声明 “看起来象” 数组的参数, 例如void f(char a[]){ ... }在编译器里都被当作指针来处理, 因为在传入数组的时候,那正是函数接收到的.void f(char *a){ ... }这种转换仅限于函数形参的声明, 别的地方并不适用。如果这种转换令你困惑, 请避免它; 很多程序员得出结论, 让形参声明 “看上去象” 调用或函数内的用法所带来的困惑远远大于它所提供的方便。

 

14. 现实地讲, 数组和指针地区别是什么?

数组自动分配空间, 但是不能重分配或改变大小。指针必须明确赋值以指向分配的空间 (可能使用 malloc), 但是可以随意重新赋值 (即, 指向不同的对象), 同时除了表示一个内存块的基址之外, 还有许多其它的用途。由于数组和指针所谓的等价性(参见问题 6.3), 数组和指针经常看起来可以互换, 而事实上指向 malloc 分配的内存块的指针通常被看作一个真正的数组。

 

15. 为什么这段代码不行?char *answer; printf("Typesomething:\n");gets(answer); printf("You typed\"%s\"\n", answer);

指针变量 answer, 传入 gets(), 意在指向保存得到的应答的位置, 但却没有指向任何合法的位置。换言之, 我们不知道指针 answer 指向何处。因为局部变量没有初始化, 通常包含垃圾信息, 所以甚至都不能保证 answer 是一个合法的指针。

改正提问程序的最简单方案是使用局部数组, 而不是指针, 让编译器考虑分配的问题:

#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);

 

16. 我的 strcat() 不行.我试了 char *s1 = "Hello, "; char *s2 ="world!"; char*s3 = strcat(s1, s2); 但是我得到了奇怪的结果。

这里主要的问题是没有正确地为连接结果分配空间。C 没有提供自动管理的字符串类型。C 编译器只为源码中明确提到的对象分配空间 (对于字符串, 这包括字符数组和串常量)。程序员必须为字符串连接这样的运行期操作的结果分配足够的空间, 通常可以通过声明数组或调用 malloc() 完成。strcat() 不进行任何分配; 第二个串原样不动地附加在第一个之后。因此, 一种解决办法是把第一个串声明为数组:char s1[20] ="Hello, ";

   由于 strcat() 返回第一个参数的值, 本例中为 s1, s3 实际上是多余的; 在strcat()调用之后, s1 包含结果。

 

17. 我有个函数, 本该返回一个字符串, 但当它返回调用者的时候, 返回串却是垃圾信息。

确保指向的内存已经正确分配了。例如, 确保你没有做下面这样的事情:

char *itoa(int n)

{

char retbuf[20]; /*错!*/

sprintf(retbuf, "%d", n);

return retbuf; /*错!*/

}

一种解决方案是把返回缓冲区声明为static char retbuf[20];

本方案并非完美, 尤其是有问题的函数可能会递归调用, 或者会同时使用到它的多个返回值时。

 

18. 那么返回字符串或其它集合的争取方法是什么呢?

返回指针必须是静态分配的缓冲区, 或者调用者传入的缓冲区, 或者用malloc() 获得的内存, 但不能是局部 (自动) 数组。

 

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

ANSI/ISO标准 C 引入 void * 一般指针类型之前, 这种类型转换通常用于在不兼容指针类型赋值时消除警告 (或许也可能导致转换)。

在 ANSI/ISO 标准 C 下, 这些转换不再需要, 而起事实上现代的实践也不鼓励这样做, 因为它们可能掩盖 malloc() 声明错误时产生的重要警告; (但是, 因为这样那样的原因, 为求与 C++ 兼容, C 程序中常常能见到这样的转换。

 

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

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

 

21. 我的程序总是崩溃, 显然在 malloc 内部的某个地方。但是我看不出哪里有问题。是 malloc() 有 bug 吗?

很不幸, malloc 的内部数据结构很容易被破坏, 而由此引发的问题会十分棘手。最常见的问题来源是向 malloc 分配的区域写入比所分配的还多的数据; 一个常见的 bug 是用malloc(strlen(s)) 而不是 strlen(s) + 1。

其它的问题还包括使用指向已经释放了的内存的指针, 释放未从 malloc 获得的内存,或者两次释放同一个指针, 或者试图重分配空指针。

 

22. 如果我可以写 char a[] = "Hello, world!"; 为什么我不能写 chara[14]; a = "Hello, world!";

字符串是数组, 而你不能直接用数组赋值。可以使用 strcpy() 代替:strcpy(a, "Hello, world!");

 

23.程序在一台机器上执行完美, 但在另一台上却得到怪异的结果。更奇怪的是, 增加或去除调试的打印语句, 就改变了症状…

a.未初始化的局部变量。 

b.整数上溢, 特别是在一些 16 比特的机器上, 一些中间计算结果可能上溢, 象 a* b / c,。  c.未定义的求值顺序。 

d.忽略了外部函数的说明, 特别是返回值不是 int 的函数, 或是参数“缩小” 或可变的函数。

e.复引用空指针。 

f.malloc/free 的不适当使用: 假设 malloc 的内存都被清零、已释放的内存还可用、再次释放已释放内存、malloc 的内部被破坏。 

g.指针类常规问题。

h. printf() 格式与参数不符, 特别是用 %d 输出 longint。

i.试图分配的内存大小超出一个 unsigned int 类型的范围, 特别是在内存有限的机器上。  j.数组边界问题, 特别是暂时的小缓冲, 也许用于sprinf() 来构造一个字符串。

k.错误的假设了 typedef 的映射类型, 特别是size t。

 

24. 为什么有的人用 if (0 == x) 而不是if (x == 0)?

这是用来防护一个通常错误的小技巧:if (x = 0)如果你养成了把常量放在 == 前面的习惯, 当你意外的把代码写成了:if (0 = x)那编译器就会报怨。明显的, 一些人会觉得记住反换测试比记住输入双 = 号容易。当然这个技巧只对和常量比较的情况有用。

 

25. 原型说明 extern int func__ ((int, int)); 中, 那些多出来的括号和下划线代表了什么?

这是为了可以在使用 ANSI 以前的编译器时,关掉说明中的原型部分。这是技巧的一部分。在别的地方, 宏 被定义为类似下面的代码:

#ifdef __STDC__

#define __(proto) proto

#else

#define __(proto) ()

#endif

原型说明中额外的括号是为了让原型列表被当作宏的单一参数。

 

26. 为什么用了详尽的路径还不能打开文件? fopen("c:\newdir\file.dat", "r") 返回错误。

你实际请求的文件名内含有字符 \n 和 \f, 可能并不存在, 也不是你希望的。在字符常量和字符串中, 反斜杠 \ 是逃逸字符, 它赋予后面紧跟的字符特殊意义。为了正确的把反斜杠传递给 fopen() (或其它函数), 必须成双的使用, 这样第一个反斜杠引述了第二个:fopen("c:\\newdir\\file.dat","r")另一个选择, 在 MS-DOS 下, 正斜杠也被接受为路径分隔符, 所以也可以这样用:fopen("c:/newdir/file.dat", "r")

注意, 顺便提一下, 用于预处理#include 指令的头文件名不是字符串文字, 所以不必担心反斜杠的问题。

 

27. 怎样调用另一个程序或命令, 同时收集它的输出?

Unix 和其它一些系统提供了popen() 函数, 它在联通运行命令的进程管道设置了stdio 流, 所以输出可以被读取 (或提供输入)。记住, 结束使用后, 要调用函数pclose()。如果你不能使用 popen(), 你应该可以调用 system(), 并输出到一个你可以打开读取的文件。如果你使用 Unix, 觉得 popen() 不够用, 你可以学习用pipe(), dup(), fork()和 exec()。

28. 怎样判断机器的字节顺序是高字节在前还是低字节在前?

有个使用指针的方法:

int x= 1;

if(*(char*)&x == 1)

printf("little-endian\n");

else

printf("big-endian\n");

 

29. 什么是提高程序效率的最好方法?

选择好的算法, 小心地实现, 同时确定程序不做额外的事。例如, 即使世界上最优化的字符复制循环也比不上不用复制。当担心效率时, 要保持几样事情在视野中, 这很重要。首先, 虽然效率是个非常流行的话题, 它并不总是象人们想的那样重要。大多数程序的大多数代码并不是时间紧要的。当代码不是时间紧要时, 通常把代码写得清楚和可移植比达到最大效率更重要。记住, 电脑运行得非常非常快, 那些看起来 “低效率” 的代码, 也许可以编译得比较有效率, 而运行起来也没有明显的延时。试图预知程序的 “热点” 是个非常困难的事。当要关心效率时, 使用 profiling软件来确定程序中需要得到关注的地方。通常, 实际计算时间都被外围任务占用了 (例如 I/O 或内存的分配), 可以通过使用缓冲和超高速缓存来提高速度。即使对于时间紧要的代码, 最无效的优化技巧是忙乱于代码细节。许多常被建议的 “有效的代码技巧”, 即使是很简单的编译器也会自动完成 (例如, 用移位运算符代替二的幂次方乘)。非常多的手动优化有可能是代码变得笨重而使效率反而低下了, 同时几乎不可移植。例如, 也许可以在某台机器上提了速, 但在另一台机器上去变慢了。任何情况下, 修整代码通常最多得到线性信能提高; 更好的算法可以得到更好的回报。

 

30.assert()是什么?怎样用它?

这是个定义在 <assert.h> 中的宏, 用来测试断言。

一个断言本质上是写下程序员的假设, 如果假设被违反, 那表明有个严重的程序错误。例如, 一个假设只接受非空指针的函数, 可以写:assert(p != NULL);一个失败的断言会中断程序。

断言不应该用来捕捉意料中的错误, 例如malloc()或 fopen() 的失败。

 

0 0
原创粉丝点击