Chinaunix首页 | 论坛 | 博客
  • 博客访问: 692295
  • 博文数量: 34
  • 博客积分: 4020
  • 博客等级: 上校
  • 技术积分: 367
  • 用 户 组: 普通用户
  • 注册时间: 2007-04-05 16:22
文章分类

全部博文(34)

文章存档

2010年(1)

2009年(16)

2008年(9)

2007年(8)

我的朋友

分类: C/C++

2009-08-12 12:25:26

2009/11/26:

  • 对42-45进行了解答

2009/09/16:

  • 对第25题补充了参考材料
  • 完成35~41的回答
2009/09/xx:
  • 修改过n次了:-P

原题见http://blog.chinaunix.net/u1/35320/showart_1773964.html


1. sizeof的运算结果大多数情况下是无符号整形(具体与编译器有关),无符整形与有符号整型运算时,要将有符号整形提升为无符号数。所以-1会成为非常大的整数,因而使得for循环没有执行。为了避免这种问题,可以将TOTAL_ELEMENTS宏修改成如下:
#define TOTAL_ELEMENTS (int)(sizeof(array) / sizeof(array[0]))
或者使用更为彻底一些的办法:
#define        sizeof    (int)sizeof

2. 减号(-)不能作为函数名;

3. 不管continue是在while、for或do循环中,意思都是忽略剩余代码,重新进行循环条条判定(想一想,如果continue连循环条件判定都可以省略掉,那岂不成了无限循环)。所以程序的输出是:
1

4. 先是很多重复的hello-err,之后是重复出现的hello-out,然后按以上方式轮流重复出现。
原因:标准输出流stdout默认是行缓冲的,也就是说只有遇到换行符或缓冲区满才真正将数据输出;而标准输错流stderr默认是无缓冲的,一旦向流中输出马上显示。题目中给出的程序虽然向stdout输出,但由于没有换行符,缓冲区也没有满,所以反倒是输出到stderr的的内容先显示出来,一直到标准输出流缓冲区满之后,全部缓冲内容一次输出。
如果只是为了按代码次序在屏幕上输出内容,可以使用如下两种方法:
  • 在向stdout输出前使用如下语句将将其设置为无缓冲
setvbuf(stdout, NULL, _IONBF, 0);
  • 每次向stdout输出都跟着换行符
fprintf(stdout,"hello-out\n");

5. 宏展开时,如果遇到#或##,则对紧相连的的形参按实参字面值进行替换。是#时,作为字符串替换,是##时,作为记号替换。
展开h(f(1,2))时,由于h的定义体中没有#或##,所以对f(1,2)进行展开,得到g(12),再展开得到"12";
展开g(f(1,2))时,由于g的定义体中包含了#,所以只对形参按实参字面值进行替换,得到"f(1,2)"。

6. 看清楚了,代码中的那个“defa1ut”有拼写错误(字母l和u位置应对调), 这样就相当于是定义了一个标号,而不是流程控制了。而且这种拼写错误极难发现,甚至在一些带语法高亮的编辑器那里,标号也是高亮显示的(如vim),真是防不胜防啊!除了更加仔细和重新设置编辑器的语法高亮外,其实还是有办法的。
  • 编译时使用-Wall打开所有警告,会看到“标号xxx定义但未使用”的信息;
  • 据说使用lint也可以,我安装了linux平台下的splint,但是即使用最严格的-strict参数,也未能检测出该问题,谁知道怎么回事?
7. 这个我不懂。

8. 函数的功能是计算输入参数的二进制模式中1的个数。算法的基本原理是:将所有二进制模式中的数字加起来就是要求的结果,只是算法使用了一种更高明更高效的方式(只用5次就可能计算完成),具体说来:
  • 第一次,将二进制数中的相临两位上的数字相加,然后将相加结果(两位)再赋值到原来的位置。例如,拿最低两位是11的情况来说明,这两位上的数据相加结果为10(十进制的2,表示位模式中有2个1),再通过赋值,得到的结果最低两位是10。
  • 第二次,将上面得到的结果继续处理,这次将相临4位数字按2个两位数相加(这个两位数就是上面分组计算得到的),4位的结果还是赋值到原来的位置;
  • 按上述方法不计算剩余三次,最终得到结果。
9. 问题的根源在于,计算机中的数字都是用二进制表示的,而小数不能用二进制精确表示(十进制小数化为二进制的方法是乘2取整法,大家照此方法拿0.1试试,看能否精确表示),所以最终的结果也是不精确的。
至于我们经常讨论的浮点数比较问题,其根本原因也在于此。

10. 根据C语法
int a = 1,2;
是定义一个名为a的int变量,并初始化为1,定义名为2的int变量,没有初始值。数字2不能作为变量名,所以这是一个语法错误。
如果将代码改为
int a = (1,2);
则意思变为,定义一个名为a的int变量,并初始化为表达式(1,2)的结果,所以a的初始值为2。

11. 这绝对是一个有效的程序,运行结果为
4321
只要知道printf的返回值是输出字符个数就明白为什么了!

12. Duff's device(达夫设备),通过减少比较次数和扩大循环体来优化循环的技巧。函数的功能是从from复制count个字节到to。使用她的理由就是:速度。
不过我认为这里面有一个bug,即当count为0时,还是会复制8个字节到to。
下面是我的修正代码:
void duff(register char *to, register char *from, register int count)
{
    register int n=count/8;
    switch(count%8){
    case 0: while(--n >= 0){ *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++;
            }   
    }
}

13. 这是Brian Kernighan的方法(参考,另外,这里还有其他CountBits的实现方法)。
下面的语句每执行一次,x位模式中1的个数减1。当x等于0时,该语句执行的次数,即为位模式中1的个数。
x = x&(x-1);

14. int foobar(void)和int foobar()当然不一样了,前者明确指出不带任何参数(如果带参数则出错),而后者则没有明确说明(编译时不会对参数进行检查)。所以第2个程序会出现编译错误。

15. 首先说明,printf函数对后面参数的解释是按照格式串中的修饰符来进行的,也就是说,如果修饰符是"%d",那么不管后面对应的参数的类型是什么,它只取够整形需要的位数,并当作整形数处理。明白了这个道理,看一下两个printf语句传递了什么参数:
  • 第一个printf使用的的参数是a,为float类型,根据类型提升的规则,要提升为double;
  • 第二个printf使用的参数是表达式*(int *)&a,其计算结果为int型,是将float变量的前4字节作为int传入(在我的机器上sizeof(int)=4,sizeof(float)=4);
现在再来解释具体发生了什么:
  • 第一个printf,将传入的double数据的前4字节当作int处理,由于a是一个正数而比较小,经提升后前面的位模式极可能为0,所以该句的输出为:0;
  • 第二个printf,将传入的数据作为int输出,由于传入数据实际上是12.5(float)位模式的前4字节,现在当作int进行输出,结果是一个比较大的数字,在我的机器上这个结果是:1095237632。
16. 在我的机器上运行时,得到了一个段错误。
数组和指针是不同的。数组名直接代表了数据存放的地址,而对于指针,其内容才代表数据的地址。访问过程也是不一样的。在以arr[i]的形式访问数据时,如果arr被声明为数组,其过程大致是:以arr以首地址偏移i个元素后,取得结果;当arr被声明为指针时,其过程为:取arr内容为首地址,在偏移i个元素的位置取得结果。
本题中将arr定义为数组,却但成指针使用,所以arr[1]先取数组内容为地址,再进行偏移访问,这样得到的地址极可能访问无效,因而发生了段错误。

17. switch-case匹配跳过了变量b的初始化,因而输出是一个随机数。
下面的代码也有这样的问题:
#include
int main()
{
   goto print;
   int b = 2;

print:
   printf("b is %d\n", b);
   return 0;
}

18. 结果是4。
数组名作为函数参数时,会被编译器转换(退化)为对应类型的指针。
请参考第205页的相关内容。

19. Error函数没有限制用户提供格式化串,这样会产生一个潜在在格式化串溢出bug。
请参考:

20. 通过运行程序,可以知道两个scanf的执行效果还是不一样的。个人认为格式化串中出现的连续空白字符(包括空格、制表符及换行符)将尽可能的与输入流中的连续空白字符匹配。所谓“尽可能”是指如果输入流中有空白字符,则匹配,如果没有,则忽略。
下面是实际的执行情况:
运行程序,执行第一个scanf,输入:
a回车                /*输入流中有两个字符,'a'和'\n'。  */
程序输出:
a                     /*scanf将'a'读到变量c中,所以输出a */
                      /*此时,输入流中还有一个字符'\n'。  */
                      /*执行第二个scanf,输入流中的'\n'与 */
                      /*格式串中的空格匹配,此时输入流为空,*/
                      /*导致%c没有输入匹配,所以等待用户输 */
                      /*入。                           */
并且光标停留在下一行首,等待输入。再输入:
b回车                /*用户输入“b回车”后,输入流又有了两个*/
                      /*字符,'b'和'\n'。对剩下的%c进行匹,*/
                      /*将'b'读入到变量c中。        */
程序输出:
b                   /*输出变量c。输入流中剩余的内容被忽略。*/
同时程序结束。
上面第二个scanf格式串中的空格,匹配了前面输入中的回车。这让我想起上学时经常看到scanf后面跟着一个getchar函数,其目的也是为了吸收多余的回车。
如果将第二个scanf格式串中的空格去掉,重新运行程序,并且做同样的输入,那么第二个scanf会将回车读入到到变量c中,然后用printf("%c\n",c)输出,会得到两个换行。

21. scanf不会对接收缓冲区的长度进行检查,所以存在缓冲区溢出bug。建议使用fgets函数代替。

22. 表达式的值如果在编译时可以确定,则会在编译时计算。sizeof(i++)用于计算int变量占据的空间是编译时就知道的,因而程序时,该参数已经是常数4了。

23. 这个问题解释起来还是比较麻烦的,简单说来,是因为类型不相容。foo的参数要是指向只读字符指针的指针,而实参argv是指向可读写字符指针的指针。详细的解释请参考《C专家编程》1.9节的内容。

24. 我能明确记得的运算符优先级之一就是逗点运算符,它的级别是最低的,所以赋值运算会先于它执行,运行结果为:
1

25. 双目运算符两边的操作数的计算顺序是标准没有定义的,是与具体实现相关的(具体请参考)。所以题目中的减法和除法运算有可能使用了错误的操作数。对于2-1的逆波兰运算,栈中的内容中应为21-(栈顶在末尾),如果先计算左侧的pop(),结果将是-1(1-2的结果)。除法与此类似。

26. 类似unix下的banner命令,将每个字符"放大"输出,但只支持英文字母(如果是小写则会转换为大写)。每个字母都是用6*6的字符矩阵显示,数组t就就是用于存储这26个大写字母的点阵。除最开始的6个0,其余整数每6个为一组表示一个字符的点阵,刚好是26组。每组中的6个整数代表从上到下的6个点阵行,每行只使用其低6位表示点阵行上的6个点。最开始的6个0是用于输出空白符的点阵(非字母字符都将用空白输出),只是实际上只使用了第一个整数(对于非字母字符,变量o的值总是0)。

外层的for循环用于输出所有字符的一行点阵,每输出完一行后换行。
内层的while循环每次取出字符串中的一个字符,将其视为字母转换为大写后,计算出对应的点阵行数字在数组中的下标。如果下标越界,表示本字符不是字母,点阵行的数字下标设定为0。
最内层的for循环总共循环7次,前6次用于输出点阵行中的点(1表示有点,用'#'输出,0表示无点,用' '输出),最后一次用于输出字符间的间隔符。

27. 注意:数组pot的前三个成员是8进制数,最后一个是10进制数。

28. 这个应该是欧几里德算法(即辗转相除法)的变形(详见http://www.cnblogs.com/drizzlecrj/archive/2007/09/14/892340.html),把求余运算用减法运算代替。
scanf正常时返回实际处理的输入项个数,出错时返回EOF。
利用gcd函数实现的计算n个数最大公约数的函数:
int gcdn(int num[], int n)
{
    ASSERT(n >= 2);
    int i = 2;
    int g = gcd(num[0], num[1]);

    while (g != 1 && i        g = gcd(g, num[i++]);

    return g;
}

29. y/*p中的/*会被作为注释处理,该行实际上是y=y;
这告诉我们,要避免在代码中使用嵌套注释,在用gcc编译时,加上-Wall参数,可对嵌套注释进行警告。

30. scanf函数的格式化串中,除空白符和修饰符外(关于空白符,请参考上面第20题),其他字符必须完全匹配才能得到正确的输入。所以那三个'-'也要输入,否则结果错误。

31. scanf接收数据的参数都必须是地址。scanf一行应改为:
scanf("%d\n",&n);
32. 加法的优先级高于移位运算。

33. 三元运算符要求操作数是有返回值的表达式,而不能是retrun语句。

34. 有以下三种方法
  • for( i = 0; -i < n; i-- )
  • for( i = 0; i < n; n-- )
  • for( i = 0; i + n; i-- )
35. ptr1是指向int的指针变量,ptr2是int变量,两者类型不一样。所以两者之间的赋值以及把ptr2当指针进行引用都是无法通过编译的。

36. 不会看到真正的结果,因为在输出结果之前程序程序因为除零,接收到SIGFPE信号,默认情况下进程收到此信号后会结束本进程。

37. 结果是:
8
第一个++i肯定是会执行的,但是由于逻辑操作符的短路处理,所以i++会被跳过,而最后的++i会被执行,这样++操作就执行了两次,结果为8。

38. 有两个问题:
  • malloc的返回值没有错误检查
  • printf语句中的a++改变了a的值,所以后面用free(a)释放内存时,已经不是原先申请到的内存了
39. C语言中计算数据元素地址的方法是首地址+偏移量,题目中的情况只是变成了偏移量+首地址,其实是一样的。

40. 输出结果为:
Life is be
"%[^a]"的意思是从输入流中提取字符串,只到遇到a停止(此时输入流中还剩下autiful等待提取)。

41. 一言已概之,与系统有关,大部分系统的行为与题目一致。详情请大家查阅第67页有关声明与定义的讨论。
42. 计算一个成员变量在结构体中的偏移量。

43. 有如下问题:
  • a和b不能是同一个变量,即如果执行SWAP(a, a)那么不管原来a值是多少,执行后a值被置为0;
  • a和b不能是浮点数,异或操作对浮点数没有意义;
  • a和b不能是结体体等复合数据类型,原因同上;
  • a或b不能是表达式;


44. #x 扩展后,使得x的实际内容成为字符串(如果x中含有\'"等字符,扩展时会自动在特殊字符前加\),所以,
    DPRINTF(cnt);
    宏展开后变为:,
    printf("%s:%d\n","cnt",cnt);

45. 异号相同相加,必定不会溢出,两正数相加,有可能上溢,两负数相加,有可能下溢;
检测溢出的算法是:
两正数相加时,如果INT_MAX减去其中一个加数的差小于另一个加数,说明会上溢,否则不溢出;
两负数相加时,如果INT_MIN减去其中一个加数的差大于另一个加数,说明会下溢,否则不溢出;
编写的代码如下:

int IAddOverFlow(int* result,int a,int b)
{
    *result = a + b;

    if ((a>=0)!= (b>=0))
        return 0;
    else if (a>=0)
        return (INT_MAX - a < b) ? 1 : 0;
    else
        return (INT_MIN - a > b) ? 1 : 0;
}


有关溢出的其他检测方法,请参考:
 
46. 还没看出来,谁会?请指点!
阅读(4112) | 评论(1) | 转发(0) |
给主人留下些什么吧!~~

chinaunix网友2010-10-08 20:53:08

http://c-faq.com/index.html