8/15/2010

读书笔记--C Programming FAQs(1)

最近在看一本写的比较有意思的C语言书,实际上是本关于C的问答录。书的英文原名叫做“C Programming FAQs”,中文译名译的很强势,叫做“你必须知道的495个C语言问题”,译名的最大亮点是“你必须知道”,呵呵。这本书源自于作者Steve Summit从1990年5月开始在comp.lang.c上发布的常见问题(FAQ)列表收集的一些并不显而易见的问题和提供的答案。
下面选个比较有意思的问答先看下。
问:为什么以前流行的那些C语言书总是使用void main()?
答:可能这本书的作者把自己也归为目标读者的一员。很多书不负责任地在例子中使用void main(),并宣称这样是正确的。但他们错了。或者他们假定每个人都在恰巧能工作的系统上编写代码。
-------------------------------------------------华丽丽的分割线------------------------------------------------
这个问答大概也可以从一个侧面反映出C语言的一些本质:告诉你规则,但不花费大量成本去检测你是否遵守了这个规则。类似的还有对数组和指针的越界操作不检测等等。
言归正传,到网上查了下不能使用void main()的原因,CVS如下:
When people ask why using a void is wrong, (since it seems to work), the answer is usually one of the following:
Because the standard says so. (To which the answer is usually of the form "but it works for me!")
Because the startup routines that call main could be assuming that the return value will be pushed onto the stack. If main() does not do this, then this could lead to stack corruption in the program's exit sequence, and cause it to crash. (To which the answer is usually of the form "but it works for me!")
Because you are likely to return a random value to the invokation environment. This is bad, because if someone wants to check whether your program failed, or to call your program from a makefile, then they won't be able to guarantee that a non-zero return code implies failure. (To which the answer is usually of the form "that's their problem").
这段英文比较容易理解,就懒得翻译了。
下面介绍一个比较纠结的问题。
问:我遇到这样声明结构的代码:
       Struct name {
              int namelen;
              char namestr[1];
       };
然后又使用一些内存分配技巧使namestr数组用起来好像有多个元素,namelen记录了元素个数,它是怎样工作的?这样是合法的和可移植的吗?
答:不清楚这样做是否合法或可移植,但这种技术十分普遍。
……
C99引入了“灵活数组域概念”,允许结构的最后一个域省略数组大小。这为类似问题提供了一个定义明确的解决方案。
-------------------------------------------------华丽丽的分割线------------------------------------------------
首先,说明下“灵活数组域”的相关内容。它允许结构的最后一个数组变量的长度是可变的,即对这样的结构使用malloc分配内存的时候,程序员可以根据需要,对灵活数组域的数组分配任意长度的空间。
但是这个灵活数组域必须满足下面两个条件:
1、  结构体中必须有一个数组成员并且这个灵活数组成员必须放在最后一个成员的位置;
2、  包含有灵活数组成员的结构体不允许嵌套在其它的结构体或者数组中。
然后,针对问题中的结构声明,我们还可以给出另外一种声明:
       struct name {
              int namelen;
              char *namestr;
       };
书中提到“真正安全的正确做法是使用字符指针,而不是数组”,即上面的这种声明。使用字符指针的结构声明时,计算结构体大小的结果,即sizeof(name),是不包括灵活数组域成员的大小的。下面这段代码是对问题中结构的一个内存分配和赋值操作。
……
char *str = “qisinidewenrou”;
struct name *nm = (*name)malloc(sizeof(struct name) + strlen(str) + 1);
nm->namelen = strlen(str);
strcpy(nm->namestr, str);
……
从上面代码可以看出,灵活数组域的一个优点是不用两次分配和释放内存,一次搞定。但是,书中还提到了一个注意事项,“像这样一次malloc调用将第二个区域接上的技巧只有在第二个区域是char型数组的时候可移植。对于任何大一些的类型,对齐变得十分重要,必须保持”。因此有下面实验:
struct test {
              char i;
              double *d;
};
……
       tt = ( struct test * )malloc( sizeof( struct test ) + sizeof( double ) * 5 );
       if( tt != NULL ) {
              tt->i = 'i';
              tt->d[0] = 1.1;
       }
在实验中,编译通过,运行直到最后赋值语句是才报错:“该内存不能为written”。原因应该是结构中的成员在内存中没有对齐,导致了越界写等问题。
但是,在网上查阅资料时,看到有些结构声明如下:
struct test {
              char i;
              double d[];
};
在这种结构声明下,上述的实验代码可以正常赋值,也能正常读取,即书中的注意事项在这种结构声明下是无用的。分析其原因,是由于这种结构声明给予编译器足够的信息,在内存分配的时候实现了对齐,因此能够正常的读写。
不知道这个问题是否于编译器有关,我的编译器是VS2008。
PS:好久没有写文章,写一篇感觉蛮费劲的。文中肯定有很多不通顺导致不好理解的地方,先凑合看吧,以后有时间回头看的时候再改好了。

PS: 本篇文章作者为chenhao727,由于技术原因造成作者显示错误,请您谅解,谢谢。