C语言结构体里的成员数组和指针

本文摘自推酷网:作者:陈皓    连接:  http://coolshell.cn/articles/11377.html?utm_source=tuicool

单看这文章的标题,你可能会觉得好像没什么意思。你先别下这个结论,相信这篇文章会对你理解C语言有帮助。这篇文章产生的背景是在微博上,看到@Laruence同学出了一个关于C语言的题,微博链接。微博截图如下。我觉得好多人对这段代码的理解还不够深入,所以写下了这篇文章。

zero_array

为了方便你把代码copy过去编译和调试,我把代码列在下面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include
struct str{
     int len;
     char s[0];
};
 
struct foo {
     struct str *a;
};
 
int main( int argc, char ** argv) {
     struct foo f={0};
     if (f.a->s) {
         printf ( f.a->s);
     }
     return 0;
}

你编译一下上面的代码,在VC++和GCC下都会在14行的printf处crash掉你的程序。@Laruence 说这个是个经典的坑,我觉得这怎么会是经典的坑呢?上面这代码,你一定会问,为什么if语句判断的不是f.a?而是f.a里面的数组?写这样代码的人脑子里在想什么?还是用这样的代码来玩票?不管怎么样,看过原微博的回复,我个人觉得大家主要还是对C语言理解不深,如果这算坑的话,那么全都是坑。

接下来,你调试一下,或是你把14行的printf语句改成:

1
printf ( "%x\n" , f.a->s);

你会看到程序不crash了。程序输出:4。 这下你知道了,访问0×4的内存地址,不crash才怪。于是,你一定会有如下的问题:

1)为什么不是 13行if语句出错?f.a被初始化为空了嘛,用空指针访问成员变量为什么不crash?

2)为什么会访问到了0×4的地址?靠,4是怎么出来的?

3)代码中的第4行,char s[0] 是个什么东西?零长度的数组?为什么要这样玩?

让我们从基础开始一点一点地来解释C语言中这些诡异的问题。

结构体中的成员

首先,我们需要知道——所谓变量,其实是内存地址的一个抽像名字罢了。在静态编译的程序中,所有的变量名都会在编译时被转成内存地址。机器是不知道我们取的名字的,只知道地址。

所以有了——栈内存区,堆内存区,静态内存区,常量内存区,我们代码中的所有变量都会被编译器预先放到这些内存区中。

有了上面这个基础,我们来看一下结构体中的成员的地址是什么?我们先简单化一下代码:

1
2
3
4
struct test{
     int i;
     char *p;
};

上面代码中,test结构中i和p指针,在C的编译器中保存的是相对地址——也就是说,他们的地址是相对于struct test的实例的。如果我们有这样的代码:

1
struct test t;

我们用gdb跟进去,对于实例t,我们可以看到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# t实例中的p就是一个野指针
(gdb) p t
$1 = {i = 0, c = 0 '\000' , d = 0 '\000' , p = 0x4003e0 "1\355I\211\..." }
 
# 输出t的地址
(gdb) p &t
$2 = (struct test *) 0x7fffffffe5f0
 
#输出(t.i)的地址
(gdb) p &(t.i)
$3 = (char **) 0x7fffffffe5f0
 
#输出(t.p)的地址
(gdb) p &(t.p)
$4 = (char **) 0x7fffffffe5f4

我们可以看到,t.i的地址和t的地址是一样的,t.p的址址相对于t的地址多了个4。说白了,t.i 其实就是(&t + 0×0), t.p 的其实就是 (&t + 0×4)。0×0和0×4这个偏移地址就是成员i和p在编译时就被编译器给hard code了的地址。于是,你就知道,不管结构体的实例是什么——访问其成员其实就是加成员的偏移量

下面我们来做个实验:

1
2
3
4
5
6
7
8
9
10
struct test{
     int i;
     short c;
     char *p;
};
 
int main(){
     struct test *pt=NULL;
     return 0;
}

编译后,我们用gdb调试一下,当初始化pt后,我们看看如下的调试:(我们可以看到就算是pt为NULL,访问其中的成员时,其实就是在访问相对于pt的内址)

1
2
3
4
5
6
7
8
(gdb) p pt
$1 = (struct test *) 0x0
(gdb) p pt->i
Cannot access memory at address 0x0
(gdb) p pt->c
Cannot access memory at address 0x4
(gdb) p pt->p
Cannot access memory at address 0x8

注意:上面的pt->p的偏移之所以是0×8而不是0×6,是因为内存对齐了(我在64位系统上)。关于内存对齐,可参看《深入理解C语言》一文。

好了,现在你知道为什么原题中会访问到了0×4的地址了吧,因为是相对地址。

相对地址有很好多处,其可以玩出一些有意思的编程技巧,比如把C搞出面向对象式的感觉来,你可以参看我正好11年前的文章《用C写面向对像的程序》(用指针类型强转的危险玩法——相对于C++来说,C++编译器帮你管了继承和虚函数表,语义也清楚了很多)

指针和数组的差别

有了上面的基础后,你把源代码中的struct str结构体中的char s[0];改成char *s;试试看,你会发现,在13行if条件的时候,程序因为Cannot access memory就直接挂掉了。为什么声明成char s[0],程序会在14行挂掉,而声明成char *s,程序会在13行挂掉呢?那么char *s 和 char s[0]有什么差别呢

在说明这个事之前,有必要看一下汇编代码,用GDB查看后发现:

  • 对于char s[0]来说,汇编代码用了lea指令,lea   0×04(%rax),   %rdx
  • 对于char*s来说,汇编代码用了mov指令,mov 0×04(%rax),   %rdx

lea全称load effective address,是把地址放进去,而mov则是把地址里的内容放进去。所以,就crash了。

从这里,我们可以看到,访问成员数组名其实得到的是数组的相对地址,而访问成员指针其实是相对地址里的内容(这和访问其它非指针或数组的变量是一样的)

换句话说,对于数组 char s[10]来说,数组名 s 和 &s 都是一样的(不信你可以自己写个程序试试)。在我们这个例子中,也就是说,都表示了偏移后的地址。这样,如果我们访问 指针的地址(或是成员变量的地址),那么也就不会让程序挂掉了。

正如下面的代码,可以运行一点也不会crash掉(你汇编一下你会看到用的都是lea指令):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
struct test{
     int i;
     short c;
     char *p;
     char s[10];
};
 
int main(){
     struct test *pt=NULL;
     printf ( "&s = %x\n" , pt->s); //等价于 printf("%x\n", &(pt->s) );
     printf ( "&i = %x\n" , &pt->i); //因为操作符优先级,我没有写成&(pt->i)
     printf ( "&c = %x\n" , &pt->c);
     printf ( "&p = %x\n" , &pt->p);
     return 0;
}

看到这里,你觉得这能算坑吗?不要出什么事都去怪语言,大家要想想是不是问题出在自己身上。

关于零长度的数组

首先,我们要知道,0长度的数组在ISO C和C++的规格说明书中是不允许的。这也就是为什么在VC++2012下编译你会得到一个警告:“arning C4200: 使用了非标准扩展 : 结构/联合中的零大小数组”。

那么为什么gcc可以通过而连一个警告都没有?那是因为gcc 为了预先支持C99的这种玩法,所以,让“零长度数组”这种玩法合法了。关于GCC对于这个事的文档在这里:“Arrays of Length Zero”,文档中给了一个例子(我改了一下,改成可以运行的了):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include
#include
 
struct line {
    int length;
    char contents[0]; // C99的玩法是:char contents[]; 没有指定数组长度
};
 
int main(){
     int this_length=10;
     struct line *thisline = ( struct line *)
                      malloc ( sizeof ( struct line) + this_length);
     thisline->length = this_length;
     memset (thisline->contents, 'a' , this_length);
     return 0;
}

上面这段代码的意思是:我想分配一个不定长的数组,于是我有一个结构体,其中有两个成员,一个是length,代表数组的长度,一个是contents,代码数组的内容。后面代码里的 this_length(长度是10)代表是我想分配的数据的长度。(这看上去是不是像一个C++的类?)这种玩法英文叫:Flexible Array,中文翻译叫:柔性数组。

我们来用gdb看一下:

1
2
3
4
5
6
7
8
(gdb) p thisline
$1 = (struct line *) 0x601010
 
(gdb) p *thisline
$2 = {length = 10, contents = 0x601010 "\n" }
 
(gdb) p thisline->contents
$3 = 0x601014 "aaaaaaaaaa"

我们可以看到:在输出*thisline时,我们发现其中的成员变量contents的地址居然和thisline是一样的(偏移量为0×0??!!)。但是当我们输出thisline->contents的时候,你又发现contents的地址是被offset了0×4了的,内容也变成了10个‘a’。(我觉得这是一个GDB的bug,VC++的调试器就能很好的显示)

我们继续,如果你sizeof(char[0])或是 sizeof(int[0]) 之类的零长度数组,你会发现sizeof返回了0,这就是说,零长度的数组是存在于结构体内的,但是不占结构体的size。你可以简单的理解为一个没有内容的占位标识,直到我们给结构体分配了内存,这个占位标识才变成了一个有长度的数组。

看到这里,你会说,为什么要这样搞啊,把contents声明成一个指针,然后为它再分配一下内存不行么?就像下面一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
struct line {
    int length;
    char *contents;
};
 
int main(){
     int this_length=10;
     struct line *thisline = ( struct line *) malloc ( sizeof ( struct line));
     thisline->contents = ( char *) malloc ( sizeof ( char ) * this_length );
     thisline->length = this_length;
     memset (thisline->contents, 'a' , this_length);
     return 0;
}

这不一样清楚吗?而且也没什么怪异难懂的东西。是的,这也是普遍的编程方式,代码是很清晰,也让人很容易理解。即然这样,那为什么要搞一个零长度的数组?有毛意义?!

这个事情出来的原因是——我们想给一个结构体内的数据分配一个连续的内存!这样做的意义有两个好处:

第一个意义是,方便内存释放。如果我们的代码是在一个给别人用的函数中,你在里面做了二次内存分配,并把整个结构体返回给用户。用户调用free可以释放结构体,但是用户并不知道这个结构体内的成员也需要free,所以你不能指望用户来发现这个事。所以,如果我们把结构体的内存以及其成员要的内存一次性分配好了,并返回给用户一个结构体指针,用户做一次free就可以把所有的内存也给释放掉。(读到这里,你一定会觉得C++的封闭中的析构函数会让这事容易和干净很多)

第二个原因是,这样有利于访问速度。连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得也没多高了,反正你跑不了要用做偏移量的加法来寻址)

我们来看看是怎么个连续的,用gdb的x命令来查看:(我们知道,用struct line {}中的那个char contents[]不占用结构体的内存,所以,struct line就只有一个int成员,4个字节,而我们还要为contents[]分配10个字节长度,所以,一共是14个字节)

1
2
3
(gdb) x /14b thisline
0x601010:       10      0       0       0       97      97      97      97
0x601018:       97      97      97      97      97      97

从上面的内存布局我们可以看到,前4个字节是 int length,后10个字节就是char contents[]。

如果用指针的话,会变成这个样子:

1
2
3
4
5
6
(gdb) x /16b thisline
0x601010:       1       0       0       0       0       0       0       0
0x601018:       32      16      96      0       0       0       0       0
(gdb) x /10b this->contents
0x601020:       97      97      97      97      97      97      97      97
0x601028:       97      97

上面一共输出了四行内存,其中,

  • 第一行前四个字节是 int length,第一行的后四个字节是对齐。
  • 第二行是char* contents,64位系统指针8个长度,他的值是0×20 0×10 0×60 也就是0×601020。
  • 第三行和第四行是char* contents指向的内容。

从这里,我们看到,其中的差别——数组的原地就是内容,而指针的那里保存的是内容的地址

后记

好了,我的文章到这里就结束了。但是,请允许我再唠叨两句。

1)看过这篇文章,你觉得C复杂吗?我觉得并不简单。某些地方的复杂程度不亚于C++。

2)那些学不好C++的人一定是连C都学不好的人。连C都没学好,你们根本没有资格鄙视C++。

3)当你们在说有坑的时候,你得问一下自己,是真有坑还是自己的学习能力上出了问题。

如果你觉得你的C语言还不错,欢迎你看看《C语言的谜题》还有《谁说C语言很简单?》还有《语言的歧义》以及《深入理解C语言》一文。

(全文完)

本人认为评论者中也有大侠,故将大量评论也加了进来,望大家见谅

评论 (118) Trackbacks (3) 发表评论 Trackback
  1. 首席宅男
    2014年4月1日08:45 | #1
    回复 | 引用

    耗子哥太敬业啦

  2. pezy
    2014年4月1日08:55 | #2
    回复 | 引用

    我怎么一直都觉得C比C++更复杂呢。。。

  3. guojun07
    2014年4月1日08:57 | #3
    回复 | 引用

    这是昨天晚上熬夜写出来的不,太牛了。

    理解透了确实是基础问题,使用数组的时候相当于去地址,使用指针的话,指针保存的是地址,等于要去指针的内容。

  4. simon
    2014年4月1日09:01 | #4
    回复 | 引用

    做应用,不做研究。

    VC6 中编译,通过,命令行运行无输出

  5. Fangzhen
    2014年4月1日09:09 | #5
    回复 | 引用

    顶一个。
    对于使用0地址访问结构体成员的使用方式之前还是遇到过的:
    #define OFFSET(type, f) ((int)&(((type*)0)->f))
    还是第一次遇到0长度数组的情况,涨姿势啦

  6. c86jeff
    2014年4月1日09:15 | #6
    回复 | 引用

    真是厉害

  7. liqiyu
    2014年4月1日09:18 | #7
    回复 | 引用

    第二条‘提高速度’,使用数组可以提高spactial locality,一次分配比两次分配,在cache的命中率上应该好很多。

  8. longjingcha
    2014年4月1日09:22 | #8
    回复 | 引用

    顶一个!

  9. winddy
    2014年4月1日09:32 | #9
    回复 | 引用

    这句“t.p 的其实就是 (&t + 0×4)”有点小问题。对于结构体而言,取地址后的偏移是以结构体大小为移动单位的,所以这句其实应该这样写会不会更加准确? t.p 的地址其实就是 ((size_t)&t + 0×4)。呵呵,有点吹毛求疵了。

  10. vx13
    2014年4月1日09:32 | #10
    回复 | 引用

    liqiyu :
    第二条‘提高速度’,使用数组可以提高spactial locality,一次分配比两次分配,在cache的命中率上应该好很多。

    同感,这个是 cpu cache 的问题,而不是指针的问题。连续访存,可以减少 cache miss ,对需要频繁访存的应用来说,性能会提升得比较明显。

  11. twocoldz
    2014年4月1日09:35 | #11
    回复 | 引用

    膜拜下耗子哥。
    想请问下如果想深入了解C语言的特性,应该去看什么资料呢?

  12. ryan
    2014年4月1日09:37 | #12
    回复 | 引用

    浩哥牛逼,又这么有耐心,真是程序员的好榜样.

  13. 飞狐
    2014年4月1日09:43 | #13
    回复 | 引用

    数组的原地就是内容,而指针的那里保存的是内容的地址。这个是核心。就这一句话,如果没有上面的分析,也不是好理解,单看上面的内容没有这句话的注解,也不是很容易理解。

  14. michaeltang
    2014年4月1日09:45 | #14
    回复 | 引用

    $2 = {length = 10, contents = 0×601010 “\n”} 应该是 $2 = {length = 10, contents = 0×601014 “\n”}

    • 陈皓
      2014年4月1日09:49 | #15
      回复 | 引用

      我的gdb中就是那样显示的。

  15. eagle
    2014年4月1日09:46 | #16
    回复 | 引用

    关于赋0和偏移可以参考linux内核中的offset_of宏.

  16. yiyanyao
    2014年4月1日09:51 | #17
    回复 | 引用

    原来叫做柔性数组, 一直找不到相关资料. 以前琢磨过http://t.cn/8sxhqv8, 猜测和性能有关系

  17. Liigo
    2014年4月1日09:52 | #18
    回复 | 引用

    人家写“C语言的经典的坑”也好,你陈浩写这么一大篇也好,结果都是为了把新手吓跑,高端黑啊 (开玩笑的)

  18. chunyang.wen
    2014年4月1日09:56 | #19
    回复 | 引用

    “访问成员数组名其实得到的是数组的相对地址,而访问成员指针其实是相对地址里的内容”

    我觉得这句话后半句存在一定的误解。“访问成员指针其实是相对地址里的内容”,这个内容其实还是个地址。

    就像经典的在函数参数内传入一级指针,然后在函数体内分配内存一样,函数返回后这个指针并没有返回分配的空间;当传入二级指针后,则可以正确分配内存。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    void allocate_mem( char *ptr)
    {
       // 这里已经将之前传进来的指针的值又覆盖了,而不是在原来指针的位置总放入分配的地址
       ptr = new char [10];
    }
     
    void allocate_mem( char **ptr)
    {
       // 二级指针是传值进来的,但是里面的内容和原来是一样的(即一级指针的地址)
       // 传进来之前可能是:char **ptr = new char*;
       *ptr = new char [10];
    }

    传地址实际上还是pass-by-value,只不过这个value不是一般的value,而是指针里面的地址而已。

    一个指针ptr有三个值:其本身的位置&ptr,其指向的地址ptr,其指向地址里的值*ptr。按地址传递实际是把ptr,copy一份到函数的形参中。

  19. cloveryume
    2014年4月1日09:57 | #20
    回复 | 引用

    gdb) p pt
    $1 = (struct test *) 0×0
    (gdb) p pt->i
    Cannot access memory at address 0×0
    (gdb) p pt->c
    Cannot access memory at address 0×4
    (gdb) p pt->p
    Cannot access memory at address 0×8

    (gdb) p pt->p 这里少了取地址符号。

  20. cloveryume
    2014年4月1日10:00 | #21
    回复 | 引用

    没少- -@cloveryume

  21. 又要我的昵称
    2014年4月1日10:01 | #22
    回复 | 引用

    耗子叔有效率,谢谢啦!

  22. michaeltang
    2014年4月1日10:02 | #23
    回复 | 引用

    @陈皓
    额,你的gcc版本是?我在gcc4.5.1下面显示的地址确是有4个字节的便宜

  23. 菜鸟john
    2014年4月1日10:02 | #24
    回复 | 引用

    本来以为这块基本理解了,但是看皓哥的文章还是能获取新的认识.赞!

  24. hughnian
    2014年4月1日10:03 | #25
    回复 | 引用

    指针,数组,这玩意是需要好好把玩的

  25. 小秦
    2014年4月1日10:19 | #26
    回复 | 引用

    个人觉得这句才是关键:
    struct foo f={0};
    C的编译器允许将一个结构体指向 0×0 的位置却不警告(假如0改为1,则编译会就会有warning出来).

  26. aoyaya
    2014年4月1日10:29 | #27
    回复 | 引用

    还有个疑问,将代码修改一下,使用if (f.a->len),代码在if处core了,这个是因为访问了非法地址。
    但是如果是if (&(f.a->len)),就在printf处core了,改成if ((int)&(f.a->len)),就直接跳过if,程序正常结束啦。
    这个地方有啥玄机?

  27. yiyanyao
    2014年4月1日10:34 | #28
    回复 | 引用

    小秦 :
    个人觉得这句才是关键:
    struct foo f={0};
    C的编译器允许将一个结构体指向 0×0 的位置却不警告(假如0改为1,则编译会就会有warning出来).

    这个是结构体的初始化赋值方式….

  28. bolun
    2014年4月1日10:35 | #29
    回复 | 引用

    原来第13行的不会crash的原因在于对数组汇编是lea,对指针是mov。受教了,谢谢。

  29. cannon0102
    2014年4月1日10:41 | #30
    回复 | 引用

    C语言还有好多东西 值得我学习研究。。。

  30. Leo
    2014年4月1日10:42 | #31
    回复 | 引用

    说点题外话,我来介绍C99一个坑吧,叫Variable-length array:http://en.wikipedia.org/wiki/Variable-length_array,它的语义是这样的

    1
    2
    3
    4
    5
    6
    7
    8
    float read_and_process( int n)
    {
         float vals[n];
      
         for ( int i = 0; i < n; i++)
             vals[i] = read_val();
         return process(vals, n);
    }

    可是在维基下面有这么一句话“One problem that may be hidden by a language’s support for VLAs is that of the underlying memory allocation: in environments where there is a clear distinction between a heap and a stack, it may not be clear which, if any, of those will store the VLA”也就是说同一个语义不同编译器有不同的实现,比如GCC是栈分配的,用SIZE_MAX限制大小。而微软用堆分配的,居然隐藏了“库函数”调用:
    _malloca:http://msdn.microsoft.com/en-us/library/5471dc8s.aspx
    _freea:http://msdn.microsoft.com/en-us/library/k8984a8h.aspx

    我很不解为啥C99标准会允许同一个语义存在不一致的实现,而且这种实现还是隐藏的很深,像alloca虽然未纳入标准,起码还是个显示调用,由用户控制逻辑和运行。而_malloca和_freea简直是“库函数”级别,居然允许作为语义实现。令我怀疑这还是“干净的”“可移植性的”“对汇编很薄的胶合层的”C语言吗?感觉有向C++演变的趋势。我个人对C99的部分标准执保留意见,但我的担忧是如果碰上这种代码,这种“未定义”的行为会让程序运行变得琢磨不透,让开发人员平添许多心智负担。

  31. jjj
    2014年4月1日10:47 | #32
    回复 | 引用

    其实就一个新的东西,那就是使用零长度的数组作为占位符,使得可以在后来的初始化中分配连续的内存,方便数组内容的取值和释放一整体的内存。

  32. jjj
    2014年4月1日10:54 | #33
    回复 | 引用

    那就是if 判断地址与内容的区别啊 , 跟博主这篇内容一直在说的相呼应,if (f.a->len) 使用的是 mov 指令,而取地址操作使用的应该是 lea 指令,而你把地址强转为 int 类型后,因为 int 的偏移地址为 0×0 所以 int 的值也是 0, 故 if 条件判断为假 直接忽略 if body 的内容。@aoyaya

  33. 菜板王小龙
    2014年4月1日10:55 | #34
    回复 | 引用

    看了这个又巩固一些,幸亏之前有看过零长度数组的代码,不然真的会有点纠结

  34. jjj
    2014年4月1日10:58 | #35
    回复 | 引用

    aoyaya :
    还有个疑问,将代码修改一下,使用if (f.a->len),代码在if处core了,这个是因为访问了非法地址。
    但是如果是if (&(f.a->len)),就在printf处core了,改成if ((int)&(f.a->len)),就直接跳过if,程序正常结束啦。
    这个地方有啥玄机?

    那就是if 判断地址与内容的区别啊 , 跟博主这篇内容一直在说的相呼应,if (f.a->len) 使用的是 mov 指令,意思是要把结构体中的len内容取出来做真假判断,而if (&(f.a->len))取地址操作使用的应该是 lea 指令,即取得 len 在结构体中的位置,而你把地址强转为 int 类型后,因为 int 的偏移地址为 0×0 所以 int 的值也是 0, 故 if 条件判断为假 直接忽略 if body 的内容。

  35. Lucien
    2014年4月1日11:02 | #36
    回复 | 引用

    响应非常快,不愧是大牛,得建立高执行力,学习了~

  36. 考拉睡睡
    2014年4月1日11:02 | #37
    回复 | 引用

    学习了,感谢耗子大神,让我们看到了真相。

  37. anthonyhl
    2014年4月1日11:02 | #38
    回复 | 引用

    背景知识两句话能说清的,不要这么复杂:

    1. 结构体成员说明了结构体内部的结构(相对首地址的偏移)。->是一种指针运算(加偏移)
    2. 0长的数组可以用于表示一个位置(数组的首地址或偏移)。这里0并不神秘,任何其它常数都有一样的含义,区别只是在计算内存大小,0比较简单。

    解释问题只有一句话:如果有int a[3];,a表示&a[0],不表示a[0];对应到题目中,f.a->s表示&(f.a->s[0]),谁还认为会crash?

  38. ricks
    2014年4月1日11:36 | #39
    回复 | 引用

    耗子威武,涨姿势鸟

  39. aoyaya
    2014年4月1日11:43 | #40
    回复 | 引用

    @jjj
    len在结构体中的位置也是0啊

  40. 互传站长网
    2014年4月1日13:02 | #41
    回复 | 引用

    不错 支持一下

  41. ChanneW
    2014年4月1日13:33 | #42
    回复 | 引用

    直接用指针,多占了一个字的空间。不利于内存拷贝通信。

  42. Albert
    2014年4月1日14:05 | #43
    回复 | 引用

    I kind of getting the feeling that the real reason is hindered by unnecessary using of zero-length array. In fact, the real reason could be better illustrated by using one ordinary array, say `char s[10]`.

  43. 加州沙漠
    2014年4月1日14:11 | #44
    回复 | 引用

    耗子叔,“thisline->length = this_length; ”这一句是不是可以拿掉?还是说有特殊意义。

  44. 33
    2014年4月1日14:21 | #45
    回复 | 引用

    这个其实涉及到汇编了 其实就是个细节 深究的话意义不大

  45. bombless
    2014年4月1日14:28 | #46
    回复 | 引用

    感觉说明的顺序有点问题。直接把最后关于0长度数组的内容放到最前面就清楚了,哪有那么多事……

  46. 哆啦比猫
    2014年4月1日14:35 | #47
    回复 | 引用

    其实是很简单的事啊。所以说应该先学汇编,再学C,了解C语言是如何简化汇编的,而不是C的某语句是什么意思。

  47. SoulCoder
    2014年4月1日14:49 | #48
    回复 | 引用

    耗子叔叔非常牛叉啊,学到了很多!搞懂了其实上面的代码不是非常简单吗!!!
    struct foo f={0};这里f.a指向了内存里面的0地址,这个地址是受保护的,直接访问肯定挂掉啊。
    char s[0]在if里面访问f.a->s是访问的地址肯定没问题,在printf里面是访问内容挂掉
    如果将char s[0]改成char *s不论是在if还是printf里面都是访问的内容那么在if处就会挂掉

  48. pheigenbaum
    2014年4月1日15:28 | #49
    回复 | 引用

    C语言真是不简单,佩服佩服

  49. bhoppi
    2014年4月1日15:41 | #50
    回复 | 引用

    挑个刺儿,“对于数组 char s[10]来说,数组名 s 和 &s 都是一样的”。s和&s对于汇编来说是一样的,但对C本身来说不完全一样,不是同一个类型,可以用sizeof运算符鉴别。
    除此之外,绝对的好文。

  50. dayu
    2014年4月1日16:03 | #51
    回复 | 引用

    说的太好了,又学到一招。

  51. 小wing
    2014年4月1日16:43 | #52
    回复 | 引用

    “结构体中的成员”在人民邮电出版社的《C语言专家编程》中有过类似的介绍,这篇文章看下来更加有助于理解。

    “关于零长度的数组” 介绍的Flexible Array用法实际编码中用到过。是看日本人的代码学会的,第一次看到后觉得很扯,但后来想想后发现的确是挺方便的(至少不用去调两次free)。所以以后的编码中一直在使用。

    耗子的C语言功底的确是很厉害,这是真正见功夫的。不明白为什么现在很多人对C语言不感兴趣。

  52. happen23
    2014年4月1日18:25 | #53
    回复 | 引用

    分析得很透彻!@anthonyhl

  53. pangchongzi
    2014年4月1日19:14 | #54
    回复 | 引用

    很敬业,不只是讲理论,还做了很多实验来验证。
    学习必须要理论结合实际才能来得影响深刻

  54. CodeLearner
    2014年4月1日19:19 | #55
    回复 | 引用

    @aoyaya
    if (&(f.a->len))会跳过该if分支呀,怎么会在printf里core呢?

  55. 惊叹号
    2014年4月1日19:56 | #56
    回复 | 引用

    耗哥,愚人节快乐

  56. zhaoyg
    2014年4月1日20:44 | #57
    回复 | 引用

    感觉有点像C++中“this为NULL时调用虚函数会不会崩” 问题。

  57. MageXellos
    2014年4月1日21:11 | #58
    回复 | 引用

    在浩哥微博看到的题目,终于等到解答了,先顶再看。

  58. aoyaya
    2014年4月1日21:14 | #59
    回复 | 引用

    CodeLearner :
    @aoyaya
    if (&(f.a->len))会跳过该if分支呀,怎么会在printf里core呢?

    在suse中实测是跳不过去的。

  59. 万山围子
    2014年4月1日21:16 | #60
    回复 | 引用

    struct str{int i, char *s}, 指针变量s是一个变量,变量的内容是指针指向内容的地址,而访问指针时,(汇编)代码实际上做了一个操作,将所指向内容的首地址移入某个寄存器中,作为基址。《C专家编程》中探讨指针和数组的区别时,特别指出了这点。

    还有f.a->s是直接计算s的地址,再访问s;而不是(*(f.a)).s,这是指针的便捷之处,但也跳过了对f.a的检查。

  60. zy
    2014年4月1日21:56 | #61
    回复 | 引用

    没有跳过吧,只是计算地址时f.a的为什么是无关紧要的,此时只是求相对偏移。@万山围子

  61. yokay
    2014年4月1日22:15 | #62
    回复 | 引用

    好 high啊~~~

  62. nic
    2014年4月1日22:19 | #63
    回复 | 引用

    `printf(“%x\n”, f.a->s);`是否应该为`printf(“%x\n”, &f.a->s);` ?

  63. Timothy Qiu
    2014年4月1日22:33 | #64
    回复 | 引用

    感觉这篇文章写得很绕。还是直接查看标准文档来得方便直接。

    标准文档 6.3.2.1 里:除非作为 sizeof 和单目 & 的操作数,类型为「array of type」的表达式都会退化为「pointer to type」。

    因为表达式 f.a->s 是 array of char 类型的,所以会被转换为 pointer to char,即指向 s 的指针类型;也就是说,if (f.a->s) 判断的是 f.a->s 的地址。而因为 f.a 是 0,所以 f.a->s 是 4。

    其实就是指针退化。

  64. KiingCode
    2014年4月1日23:10 | #65
    回复 | 引用

    我是Java的 虽然没有看懂但是觉得好高深的样子

  65. niksun
    2014年4月1日23:11 | #66
    回复 | 引用

    我发现用clang/llvm 编译后会自动优化,我是按照两次malloc来进行的

    (lldb) x line
    0x1001039d0: 0a 00 00 00 00 00 00 00 e0 39 10 00 01 00 00 00 ……..�9……
    0x1001039e0: 61 61 61 61 61 61 61 61 61 61 00 00 00 00 00 00 aaaaaaaaaa……
    (lldb) x line->contents
    0x1001039e0: 61 61 61 61 61 61 61 61 61 61 00 00 00 00 00 00 aaaaaaaaaa……
    0x1001039f0: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 …………….

  66. wks
    2014年4月1日23:15 | #67
    回复 | 引用

    C11的标准对->运算符的语义是这样定义的:“A postfix expression followed by the -> operator and an identifier designates a member of a structure or union object. The value is that of the named member of the object to which the first expression points, and is an lvalue.” 但是,并没有说如果x本身是非法指针的情况下会有什么后果。我理解为,这个是“未定行为”。既然是“未定行为”,那么发生任何事情都是有可能的,比如编译器编译出让屏幕上打印出“FUCK YOU\n”的程序,也是“正确”的编译器行为。

    另一方面,C11倒是明确说了*运算符用于非法指针的情况:“If an invalid value has been assigned to the pointer, the behavior of the unary * operator is undefined.”其中invalid pointer包括null指针.

    参考:6.5.2.3和6.5.3.2两节
    http://www.open-std.org/jtc1/sc22/wg14/www/docs/n1570.pdf

  67. neo
    2014年4月1日23:16 | #68
    回复 | 引用

    其实C语言之所以复杂,就是因为一些模棱两可的写法或者说不同的写法有相同的语义。个人感觉如果是C语言中遇到了问题,不妨从汇编角度分析(反汇编的结果)。

  68. wks
    2014年4月1日23:22 | #69
    回复 | 引用

    Fangzhen :
    顶一个。
    对于使用0地址访问结构体成员的使用方式之前还是遇到过的:
    #define OFFSET(type, f) ((int)&(((type*)0)->f))
    还是第一次遇到0长度数组的情况,涨姿势啦

    可以试试stddef.h里的offsetof宏。这个是真的做你认为它应该做的事的工具。对于某些编译器(如gcc),这是用intrinsic来实现的。Wikipedia说这是ANSI C定义的,我没有论证过,但起码C11是规定了这个宏的行为的。

  69. zjw
    2014年4月1日23:31 | #70
    回复 | 引用

    皓哥真是公里深厚啊!

  70. haitao
    2014年4月1日23:59 | #71
    回复 | 引用

    c比c++好的地方是:简洁明了,所以坑少
    但是,这个例子,其实是c的坑,而不是大家学得不够
    坑是歧义,如果要靠汇编代码来解释、理解,其实就已经是坑了!
    好(没坑)的语言,应该语法说怎么做,就真的是怎么做,步骤也是明确的,而不应该是 不同编译器有不同的实现
    社会学里:民众是 法无禁止 皆可为;政府是 法无允许 皆不可为。
    编程语言,也应该:语法没规定的,皆不可为。
    如果都能这样,c的很多怪异写法、技巧就直接是编译报错了。
    比如那个switch的怪异技巧。

    1)为什么不是 13行if语句【if (f.a->s) 】出错?f.a被初始化为空了嘛,用空指针访问成员变量为什么不crash?
    不能说疑问者水平不够,而是编译器没按语言规定的步骤执行了,看汇编代码来解释,也只说明这个编译器是这样做,但是仍然是违反了语言自己规定步骤。

  71. Scan
    2014年4月2日00:01 | #72
    回复 | 引用

    耗叔真没道理,上次也说C++坑比C少啥的,这次一大群人都傻逼了还需要你再写文章解释,仍然说不是坑!
    我是C/C++经验很足,但如果我在review别人的代码中看到这样的写法,是会仔细想想才会说不会crash…
    如果每个现象能解释得通就不叫坑,那么有什么语言特性是翻出生成的汇编码、翻出解释器源码后解释不通的吗?
    我认为这样容易让人迷惑的特性就叫坑!而这种需要专家C程序员来公开解惑的特性就叫坑!
    当然,就这个用法而言,如果不算坑,只能说太少有人会这样写,而不是说它不够反直觉!

    最后你解释这么做的优点:
    “第二个原因是,这样有利于访问速度。连续的内存有益于提高访问速度,也有益于减少内存碎片。(其实,我个人觉得也没多高了,反正你跑不了要用做偏移量的加法来寻址)”

    那个括号有问题,寻址是不一样的。

    struct中嵌变长数组,与指针相比:
    1. 主要优点,只需要一次内存分配,struct的其他字段与访问数组是在连续内存上,spatial locality更好。至于说malloc、free的次数,因为是同时分配、释放,封装过后在易用性上没区别。
    2. 另外一个影响不大的性能优势,访存次数。给你struct的指针p,访问p->a[i],只需要一次访存,从”p+offset a + i * sizeof(a[0])”处取sizeof(a[0])个字节就是;而如果a是指针的话,需要两次访存,先从”p + offset a”处取4/8个字节得到地址q,再从”q + i * sizeof(a[0])”处取sizeof(a[0])个字节得到元素值。这既是说,如果改用指针的话,哪怕恰好指针指向的内存紧跟在struct后,内存连续,但p->a[i]的指针声明仍然会比数组声明多一次访存。
    3. 当然struct后缀变长数组的方法也有限制,即struct需要多个长度不同的变长数组时,只有一个能作为后缀,分配多块内存变得不可避免。

  72. Chris
    2014年4月2日00:10 | #73
    回复 | 引用

    受教了 顶!

  73. haitao
    2014年4月2日00:17 | #74
    回复 | 引用

    switch的怪异技巧:
    http://coolshell.cn/articles/10975.html#more-10975
    case 1居然位于case 0里面的一个for语句块里!简直是“乱伦”了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    int function( void ) {
       static int i, state = 0;
       switch (state) {
         case 0: /* start of function */
         for (i = 0; i < 10; i++) {
           state = 1; /* so we will come back to "case 1" */
           return i;
           case 1:; /* resume control straight after the return */
         }
       }
    }
  74. wks
    2014年4月2日09:12 | #75
    回复 | 引用

    haitao :
    c比c++好的地方是:简洁明了,所以坑少
    但是,这个例子,其实是c的坑,而不是大家学得不够
    坑是歧义,如果要靠汇编代码来解释、理解,其实就已经是坑了!
    好(没坑)的语言,应该语法说怎么做,就真的是怎么做,步骤也是明确的,而不应该是 不同编译器有不同的实现
    社会学里:民众是 法无禁止 皆可为;政府是 法无允许 皆不可为。
    编程语言,也应该:语法没规定的,皆不可为。
    如果都能这样,c的很多怪异写法、技巧就直接是编译报错了。
    比如那个switch的怪异技巧。
    1)为什么不是 13行if语句【if (f.a->s) 】出错?f.a被初始化为空了嘛,用空指针访问成员变量为什么不crash?
    不能说疑问者水平不够,而是编译器没按语言规定的步骤执行了,看汇编代码来解释,也只说明这个编译器是这样做,但是仍然是违反了语言自己规定步骤。

    赞。

    另一方面理解,编程语言的“标准”就是程序员和编译器之间的“契约”。按照标准写的程序,符合标准的编译器必须生成符合标准的目标码。但是,如果程序员写出了标准里没有规定的程序,那么,他就等于超出了契约的规定范围,编译器如何处理都是编译器的自由(比如打印”FUCK YOU\n”)。

  75. haitao
    2014年4月2日10:17 | #76
    回复 | 引用

    @wks
    一般而言,设计语言、开发编译器的人,比使用编译器的人要牛。
    所以,他的语法、标准应该是足够严谨的,最后一条总是:不符合以上规则,均属于错误代码。
    如上面的switch例子,如果严格核对switch该有的模式,编译器首先就报错了:
    switch (表达式) { 重复【case 常量: 无或代码块;】 },那么case 1出现在case 0的代码块里,就应该被认为case 0的代码块里有一个switch,但是漏掉了switch直接出现case了
    编译时就把关卡住,绝对好过 编译通过,但行为无法预期,运行了n久才遇到满足逻辑分支才发生。

    揣测一下这些 非预期 代码 为什么会被编译器通过?
    编译器为了方便实现、编译速度,而放弃了严格的语法模式的匹配?

  76. shenjing
    2014年4月2日10:25 | #77
    回复 | 引用

    为什么写这么无聊的博文, 只能指点指点学生党。

  77. lh_mouse
    2014年4月2日10:39 | #78
    回复 | 引用

    printf(“%x\n”, f.a->s); // 没 (unsigned int) 直接 UB

    所以有了——栈内存区,堆内存区,静态内存区,常量内存区,我们代码中的所有变量都会被编译器预先放到这些内存区中。 // 寄存器是不是“内存”?

    换句话说,对于数组 char s[10]来说,数组名 s 和 &s 都是一样的(不信你可以自己写个程序试试)。 // 还特意加粗了,这是找抽么。

    第一个意义是,方便内存释放。 // 无视语义了么?

    第二个原因是,这样有利于访问速度。 // 结论是对的,解释是错的。倒腾下 cache 试试看?

  78. guoshun
    2014年4月2日11:52 | #79
    回复 | 引用

    按struct str{
    int len;
    char s[0];
    }的定义,s在大尾序的机器上偏移量会不会是0×0呢?

  79. wks
    2014年4月2日12:26 | #80
    回复 | 引用

    @haitao

    haitao :
    @wks
    一般而言,设计语言、开发编译器的人,比使用编译器的人要牛。
    所以,他的语法、标准应该是足够严谨的,最后一条总是:不符合以上规则,均属于错误代码。
    如上面的switch例子,如果严格核对switch该有的模式,编译器首先就报错了:
    switch (表达式) { 重复【case 常量: 无或代码块;】 },那么case 1出现在case 0的代码块里,就应该被认为case 0的代码块里有一个switch,但是漏掉了switch直接出现case了
    编译时就把关卡住,绝对好过 编译通过,但行为无法预期,运行了n久才遇到满足逻辑分支才发生。
    揣测一下这些 非预期 代码 为什么会被编译器通过?
    编译器为了方便实现、编译速度,而放弃了严格的语法模式的匹配?

    至于为什么会有“未定行为”并且让它编译通过,很多未定行为是运行时的,编译时一般检查不出来。比如对空指针访问,数组越界……如果将这些情况规定为“未定行为”,可以使得运行时的检查量降低,使得“正确”的代码执行更快。

    比如,数组越界,某些语言里数组越界必须抛出异常。这样的语言,一般要对每次数组访问判断一下越界(庆幸的是对于现代的计算机,这样的判断代价并不高),或者在能够推断出“肯定不会越界”的情况下将这样的判断消除。

    在另一些语言里,数组越界是未定行为。这样,编译器永远不用加入对数组越界的判断。如果程序员的算法能够保证自己的访问永远不越界,那么写出来的程序就是正确的而且高效。反之要是程序员不能保证,那么要么自己每次检查一下,要么就期待编译器能干出任何事(比如打印”FUCK YOU!\n”)。

    另外,switch语句的语法不是你想像的那样有若干个case或default。C11是这样定义的:

    selection-statement:
    if ( expression ) statement
    if ( expression ) statement else statement switch ( expression ) statement
    switch ( expression ) statement

    statement:
    labeled-statement
    compound-statement
    expression-statement
    selection-statement
    iteration-statement
    jump-statement

    labeled-statement:
    identifier : statement
    case constant-expression : statement
    default : statement

    compound-statement:
    { block-item-listopt }

    block-item-list:
    block-item
    block-item-list block-item

    block-item:
    declaration
    statement

    说明里面只是一个statement,而statement可以是compound-statement(也就是{ stmt1 stmt2 stmt3 …}),而里面的每个stmt都可以是任何语句。其中case xxx:、default:以及some_label:全都属于label-statement。所以,从语法上,那种到处插入case的switch并没有错误。

    switch的语义:

    A switch statement causes control to jump to, into, or past the statement that is the switch body, depending on the value of a controlling expression, and on the presence of a defaultlabelandthevaluesofanycaselabelsonorintheswitchbody. Acaseor default label is accessible only within the closest enclosing switch statement.

    既然是jump,那么就和goto有异曲同工之妙了。所以switch可以用得和goto一样肮脏。

  80. 2014年4月2日15:44 | #81
    回复 | 引用

    @haitao
    净扯淡。
    有谁告诉你数组和指针是一样的了。

    用asm只是为了很明确的告诉你这俩不一样且给你举个例子而已。

  81. 2014年4月2日17:12 | #82
    回复 | 引用

    guoshun :
    按struct str{
    int len;
    char s[0];
    }的定义,s在大尾序的机器上偏移量会不会是0×0呢?

    你想多了,这是俩概念。单一变量才有这个说法,struct是复合变量。

  82. lovecplusplus
    2014年4月2日17:15 | #83
    回复 | 引用

    貌似Java这些什么都看不到吧 – - @KiingCode
    PS:今天就收货了这篇“唠叨”,希望博主以后继续“唠叨” – -
    PPS:建议添加个针对每篇文章生成二维码的功能

  83. Jeff
    2014年4月2日21:48 | #84
    回复 | 引用

    Linux内核中好像有很多这样的0长度数组的用法。

  84. 九百特
    2014年4月2日21:54 | #85
    回复 | 引用

    写的非常不错

  85. 女孩不哭
    2014年4月2日22:24 | #86
    回复 | 引用

    还好, 我能理解~~~
    当然这还要涉及到计算结构体成员偏移的方式. 这里也有一些不错误的介绍.
    http://www.cnblogs.com/memset/archive/2013/01/07/containing_record.html

  86. Lsay
    2014年4月3日01:37 | #87
    回复 | 引用

    文中说到访问数组名是访问该数组的地址,我写了一段代码
    char test[] = “hello”;
    printf(“%x”, test);//输出地址
    printf(“%s”, test);//输出“hello”
    printf(test);//输出“hello”
    第一个输出能理解,但是后面两个是为什么呀?

  87. danny
    2014年4月3日09:37 | #88
    回复 | 引用

    球 :

    guoshun :
    按struct str{
    int len;
    char s[0];
    }的定义,s在大尾序的机器上偏移量会不会是0×0呢?

    你想多了,这是俩概念。单一变量才有这个说法,struct是复合变量。

  88. danny
    2014年4月3日09:50 | #89
    回复 | 引用

    danny :

    球 :

    guoshun :
    按struct str{
    int len;
    char s[0];
    }的定义,s在大尾序的机器上偏移量会不会是0×0呢?

  89. wks
    2014年4月3日10:44 | #90
    回复 | 引用

    @Jeff

    这也部分解释了为什么linux必须用gcc编译(clang通过努力也可以编译linux了,哎,一个编译器反过来适应一个程序……)

  90. wks
    2014年4月3日10:52 | #91
    回复 | 引用

    @Jeff
    GCC扩展了C语言,规定了零长度数组。 http://gcc.gnu.org/onlinedocs/gcc/Zero-Length.html

    但clang有意地不提供这项扩展。“clang does not support the gcc extension that allows variable-length arrays in structures. This is for a few reasons: one, it is tricky to implement, two, the extension is completely undocumented, and three, the extension appears to be rarely used. Note that clang does support flexible array members (arrays with a zero or unspecified size at the end of a structure).”见:http://clang.llvm.org/docs/UsersManual.html

    另一方面,可以发现,Lua不用修改地就可以在另一个操作系统上跑。Lua的作者是一个很严谨的C程序员。为了兼容性,宁可使用3个int的结构表示Value,也不愿意使用高效的tagged pointer,因为ansi C没有“和指针一样长的整数”类型。这一点可以找找Lua作者的论文。 下面这一页说Lua可以直接在这个新操作系统上编译:https://github.com/klange/toaruos

  91. shady
    2014年4月3日14:28 | #92
    回复 | 引用

    还是要写通用的C程序,不依赖于某个特定的平台,C99这玩意。。。

  92. 呵呵后
    2014年4月3日21:25 | #93
    回复 | 引用

    皓哥,你说char *s 和 char s[0] 的区别在于

    对于char s[0]来说,汇编代码用了lea指令,lea 0×04(%rax), %rdx
    对于char*s来说,汇编代码用了mov指令,mov 0×04(%rax), %rdx

    可是我用GDB调试汇编代码是这样的?就没有看见lea指令?求指教?
    char s[0] 的汇编:
    0x000000000040150f : callq 0x4026d0
    0×0000000000401514 : movq $0×0,-0×10(%rbp)
    0x000000000040151c : mov -0×10(%rbp),%rax
    0×0000000000401520 : add $0×4,%rax
    0×0000000000401524 : test %rax,%rax
    0×0000000000401527 : je 0×401539
    char * s的汇编:
    0x000000000040150f : callq 0x4026d0
    0×0000000000401514 : movq $0×0,-0×10(%rbp)
    0x000000000040151c : mov -0×10(%rbp),%rax
    => 0×0000000000401520 : mov 0×8(%rax),%rax
    0×0000000000401524 : test %rax,%rax
    0×0000000000401527 : je 0×401539
    0×0000000000401529 : mov -0×10(%rbp),%rax
    0x000000000040152d : mov 0×8(%rax),%rax

    • 陈皓
      2014年4月4日15:18 | #94
      回复 | 引用

      你用if语句看看。另外,不同的系统可能有不一样的汇编。

  93. Peter
    2014年4月4日12:54 | #95
    回复 | 引用

    看完鸟哥@Laruence的回应,确实觉得这篇文章啰嗦了,皓哥还是该冷静冷静,对事不对人吧

    • 陈皓
      2014年4月4日15:16 | #96
      回复 | 引用

      你要仔细看看你知道我根本都没说Laruence!你的语文能力呢?

  94. lgf2002
    2014年4月5日14:34 | #97
    回复 | 引用

    俺又来冒个泡,觉得这事搞复杂了。
    if (f.a->s) 不会crash,是因为s是一个数组类型,仅仅取了个地址,没有取这个地址里的了内容。
    如果将它改成 if (*(f.a->s)),它也会crash。同样如果将 if (f.a->s)改成 if (f.a->len)也会crash,因为len是int变量,这样是取值,不是取地址,将它改成 if (&(f.a->len)),也不会crash。
    对于printf,第一个参数类型是const char *,正常情况下,期望传入的是格式化字符串,然后去解析这个格式化字符串,遇到非格式化字符直接输出,遇到格式化字符,就根据格式化类型,去读取相应变量的值或者地址。而printf(f.a->s); 这样用,printf就把f.a->s当做格式化字符串去解析,这里也不会报错,因为s是char []类型,满足,printf函数第一个参数的要求。f.a->s的值为4,所以printf就去读这个地址值里的内容,也就crash了。将它改为printf(“%x”,f.a->s);
    不会crash,因为现在格式化字符串是%x,这个%x是存在某个地址处,内容是%x,是合法的,而%x解析出来,又是取地址,所以就去栈里相应的地方取变量的地址值。而将它改为printf(“%s”,f.a->s); 同样也会crash,但是这两个chash的原因是有区别的,前者是将f.a->s当做格式化字符串去解析,去读不合法格式化字符地址里的内容crash,而后者是格式化字符串地址是合法的,去读相应的变量地址里的内容的时候,crash了。
    printf本身是有缺陷的:
    对于 char *ptr=”Hello\n”;
    printf(ptr);
    char arr[]=”hello world\n”;
    printf(arr);
    都没有问题,它将传递给它的字符串,当做格式化字符串去解析,没有遇到格式字符就将内容直接输出。但是如果
    char arr[]=”hello world %s \n”;
    printf(arr);
    那它先输出hello world,然后就在栈里不合理的地址处去读内容值了,这个地址处放的是什么,没人知道,也许它会让你的程序crash掉。
    在代码里尽量不要使用printf,更不要像printf(arr);这种不合理的使用。printf不仅低效,而且因为自己的缺陷,给有恶意的人制造了机会。他们可以修改printf格式化字符串里的内容,让它去执行任何一个地址处的恶意程序,最简单的就是提升系统权限,在机器上想干什么就干什么。
    另外,使用柔性数组,主要是提高访问速度,结构里的柔性数组,分配的空间,和结构里的其它变量空间是连续的。
    用指针,分配的空间,基本上和其它变量的空间是不连续的。CPU读取数据的时候,是按照行读取的,也就是每次从内存里读取cache line大小的数据,然后放在缓存里。比如这里,如果CPU读取len的时候,顺便将数组s的内容一起读到cache line里,当cpu需要s的内容的时候,不许要再去读内存,甚至是硬盘。因为len和s的内存空间是连续的,所以,它们被放到一个cache line的可能性特别大,如果s的内容被顺便读进去,那访问速度比从内存里访问高出一到两个数量级。从代码本身来说,微观角度,高效代码遵守两个原则,1、空间连续性,2、时间连续性。

  95. lgf2002
    2014年4月5日14:46 | #98
    回复 | 引用

    补充,上文“这里也不会报错”指的是编译器编译、链接的时候不会报错

  96. SimLogic
    2014年4月6日18:34 | #99
    回复 | 引用

    个人意见, 其实zero-array最大的目的在于空间节约,因为你不需要多使用一个指针作间接索引,不要小看这一个pointer,如果你系统里面很多地方都需要挂接类似的数据结构,那么它的空间节约也是很明显的,至于side-effect,那就是牺牲了使用指针的灵活性—如果你需要扩充你的数组空间,你唯一能做的就是两个a) realloc b) malloc new and free old.
    第一个看系统,不是每一次realloc都能成功,第二个问题在于如果你原始的数据指针被其他地方引用(比如hash),那么你就必须de-reference,更新所有对原来的地址引用–这个cost也是需要考虑的.但是你直接采用指针,虽然付出了8个字节,但是获得了更多空间重置的灵活性—-在这一点上来看,每一次间接引用(指针)都会带来更多的灵活性,代价是开销.

  97. zhouzhenghui
    2014年4月6日21:18 | #100
    回复 | 引用

    很多回复都是错上加错,这根本就是一个基本的问题,不值得大惊小怪,leo提出c99要求不严的问题倒是的确存在的。

  98. yelt
    2014年4月7日18:02 | #101
    回复 | 引用

    @neo
    同意。
    其实也不需要反汇编。
    具体到这个例子,用纸和笔模拟一下变量在内存中对应位置的值就可以了。

  99. viki_coolshell
    2014年4月7日18:58 | #102
    回复 | 引用

    @lgf2002
    赞!!!!!!!!!!!!!
    char p[0],和char *p的区别,看了你的回复才看明白。
    如果是char *p的话,if(f.a->s)是到0×04这个地址中取指针值,所以会崩溃的。
    而char p[0]的话,就直接取0+4为返回值了。

你可能感兴趣的:(Linux,C/C++语言,我的读物)