V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
wangxn
V2EX  ›  C

请问 C 中的空数组怎么理解?

  •  
  •   wangxn · 2016-07-29 17:21:37 +08:00 · 3104 次点击
    这是一个创建于 3042 天前的主题,其中的信息可能已经有所发展或是发生改变。

    比如这个程序:

    #include <stdio.h>
    
    struct sdshdr {
        long len;
        long free;
        char buf[];
    };
    
    int main(void) {
        char buf[];
        printf("%d %d %d\n", sizeof(buf), sizeof(long), sizeof(struct sdshdr));
        return 0;
    }
    

    sdshdr中的buf是合法的,但main中的buf却不是合法的。请问怎么理解呢?有哪里可以看到相关的语法说明呢? 谢谢!

    40 条回复    2016-08-03 16:42:30 +08:00
    lawlielt
        1
    lawlielt  
       2016-07-29 17:38:15 +08:00   ❤️ 1
    main 中变量声明会分配内存的 你这怎么分配? struct 只是一个结构
    suikator
        2
    suikator  
       2016-07-29 17:40:26 +08:00 via Android   ❤️ 1
    struct 中 buf 执行默认初始化, mian 中 buf 不执行初始化,没初始化自然不合法。
    wangxn
        3
    wangxn  
    OP
       2016-07-29 17:42:50 +08:00
    @lawlielt 虽然是这个道理,但感觉挺奇怪的。
    krizex
        4
    krizex  
       2016-07-29 17:44:40 +08:00   ❤️ 1
    你的例子里面 struct 中的 buf 指向 free 之后开始的内存,是一种方便的用法,省的你用 sdshdr*(0)->free + sizeof(sdshdr*(0)->free)来找到这个地址了。
    krizex
        5
    krizex  
       2016-07-29 17:45:56 +08:00   ❤️ 1
    struct sdshdr {
    long len;
    long free;
    char buf[];
    };
    中的 buf 本身不占内存,在可变长度的 struct 中,这个是个常见的用法。
    zts1993
        6
    zts1993  
       2016-07-29 17:46:02 +08:00
    redis sds 嘿嘿嘿
    wangxn
        7
    wangxn  
    OP
       2016-07-29 17:48:43 +08:00
    @krizex 多谢详细讲解。这种 C 的惯用法确实很巧妙。
    wangxn
        8
    wangxn  
    OP
       2016-07-29 17:49:25 +08:00
    @zts1993 厉害,一眼就看出了出处。
    HarveyDent
        9
    HarveyDent  
       2016-07-29 18:01:24 +08:00
    这个东西应该是叫变长数组吧。搜一下就晓得了
    rushcheyo
        10
    rushcheyo  
       2016-07-29 18:08:05 +08:00
    这叫位域……没有人知道它的学名吗?这不是 “惯用法” 而是标准规定的正常语法。
    jccg90
        11
    jccg90  
       2016-07-29 18:16:57 +08:00
    @rushcheyo 怎么我搜到的位域不是这个样子的
    wangxn
        12
    wangxn  
    OP
       2016-07-29 18:20:33 +08:00   ❤️ 1
    @rushcheyo 这个不是位域……
    ```C
    // 位域示例
    struct sdshdr {
    long len:2;
    long free:6;
    char buf[];
    };
    ```
    Zirconi
        13
    Zirconi  
       2016-07-29 18:20:52 +08:00   ❤️ 1
    shimanooo
        14
    shimanooo  
       2016-07-29 18:27:07 +08:00
    auto p = malloc(sizeof(struct sdshdr) + variable_length)
    p->buf[index_within_variable_length]

    效果就是省一次 malloc ?代价是只能有一个变长数组,且必须放在末尾。
    wangxn
        15
    wangxn  
    OP
       2016-07-29 18:42:22 +08:00
    @Zirconi 谢谢,终于找到了正规的语法说明!
    redsonic
        16
    redsonic  
       2016-07-29 19:20:08 +08:00
    exploit 中这种写法很常见,可能 hacker 们都比较懒。
    nicevar
        17
    nicevar  
       2016-07-29 19:22:09 +08:00
    看文档抽象,会汇编的话,直接 gcc -S 输出汇编代码,把 struct 的 char buf[]改成 char *buf 对照一下就好理解了,你会看到往寄存器 mov 的时候,前者不占空间
    wangxn
        18
    wangxn  
    OP
       2016-07-29 19:28:50 +08:00 via Android
    @nicevar 这个和字符指针还是有区别的。这种方式不会额外占空间,但用字符指针的话却是实实在在多 4/8 字节的。
    wangxn
        19
    wangxn  
    OP
       2016-07-29 19:30:20 +08:00 via Android
    @wangxn 糟糕,我理解错了,那位朋友的说法和我的一样,抱歉。
    zhicheng
        20
    zhicheng  
       2016-07-29 19:41:10 +08:00 via Android   ❤️ 2
    既不神奇,也不巧妙。不是变长数组,更不是位域。只是指向结构体末尾的指针。这种结构体不能直接定义变量使用,只能强转或动态内存。用不好有安全隐患,在数据库和服务器编程里经常这么用,解析磁盘文件和协议比较省事,因为通常二进制协议设计的时候数据包开始会有一个长度,后面紧跟着数据。
    zhicheng
        21
    zhicheng  
       2016-07-29 19:49:13 +08:00
    另外在结构体中,如果要求比较严格的话,不会定义成空。会定义成

    struct {
    size_t len;
    char buf[1];
    };

    至于为什么,楼主可以自己琢磨一下。
    jeffersonpig
        22
    jeffersonpig  
       2016-07-29 20:26:49 +08:00   ❤️ 3
    这是 C99 标准增加的结构体柔性数组成员,结构体的最后一个成员可以是不指定长度的数组,用 sizeof 操作符取得的这种结构体的大小将不包括柔性数组成员的大小。包含柔性数组成员的结构体只能用动态内存分配进行构造,且分配的内存大小应大于结构体大小,否则数组将无法包含任何数据。数组的大小由动态分配的内存决定
    jeffersonpig
        23
    jeffersonpig  
       2016-07-29 20:28:21 +08:00
    话说 LZ 这个 redis 的代码版本有点旧吧
    wangxn
        24
    wangxn  
    OP
       2016-07-29 21:20:34 +08:00
    @jeffersonpig 是的,好几年历史了,拿个老点的版本看起,不然新版本太多东西看得都眼花缭乱了。
    kingddc314
        25
    kingddc314  
       2016-07-29 21:31:15 +08:00 via Android   ❤️ 1
    特别用于结构体后面,是柔性数组,不占据空间,但是却可以根据指针进行寻址结构体后面的数据, C99 以前写法是 char buf[0],一般是在结构体后面附带数组时使用,用来节省指针的 4 个或 8 个字节。
    sinxccc
        26
    sinxccc  
       2016-07-29 22:03:02 +08:00
    @kingddc314 主要目的不是节约字节,而是解析一段内存的时候可以直接套上去用。
    kingddc314
        27
    kingddc314  
       2016-07-29 22:40:39 +08:00
    @sinxccc 我感觉主要还是节省内存吧,特别是 redis 缓存这样内存很珍贵的,不然何不加一个指针成员更直观
    sinxccc
        28
    sinxccc  
       2016-07-29 23:41:11 +08:00
    @kingddc314 嗯,我对 redis 的源代码不熟…不过我指的是这样定义的结构体,在解析一段内存的时候可以直接把内存 buffer 强转成结构体指针,然后按结构体读取。指针成员是做不到的。
    SIGEV13
        29
    SIGEV13  
       2016-07-29 23:58:15 +08:00   ❤️ 1
    这个 struct 内部的空数组只能出现在末尾, (sdshdr *) var_name->buf[n] 方便地指向那个 struct 末尾地址 + n * sizeof(char).

    一般用在 header/metadata + data 这种结构里: 预先划分一大块内存,然后在这段内存里填上这种结构,方便解析。

    从名字看,这个可能用来管理内存, len 这个结构 buf 的长度, free 标记这个块有没有被占用吧。
    zhujinliang
        30
    zhujinliang  
       2016-07-30 00:01:58 +08:00 via iPhone
    这个跟指针有区别么?
    hsyu53
        31
    hsyu53  
       2016-07-30 00:02:59 +08:00   ❤️ 1
    个人理解,应该是为了让整个结构体占用一段连续的内存区域。
    kingddc314
        32
    kingddc314  
       2016-07-30 00:33:02 +08:00 via Android
    @sinxccc 才想到确实有你说的这方面原因,指针成员的话,不太方便保证指针内存和结构体内存连续,而这个柔性数组处理只需分配更大的空间就很自然了。在 Redis 里面 antirez 的 sds 是 C 语言的字符串封装,其中的 sdshdr 结构是保存头信息,结构体后面的堆空间才是保存字符串的,这样的情况就只能是连续空间。
    kingddc314
        33
    kingddc314  
       2016-07-30 00:36:10 +08:00 via Android
    kohnv
        34
    kohnv  
       2016-07-30 00:38:54 +08:00   ❤️ 1
    这个叫柔性数组, 实现可变长 struct 的一种常用技巧.
    xpol
        35
    xpol  
       2016-07-30 10:20:03 +08:00 via Android   ❤️ 1
    结构体中变长数组,是一个惯用法。
    结构体的最后一个成员可以这么用。 malloc 的时候会 malloc 实际要的数组长度的内存。结构体和数组一次性 malloc 。
    如果你用指针的话,需要 malloc 两次,一次结构体,一次数组。另外就是访问方便,不用通过指针间接一次。
    可惜有些编译器会报 warning 。可以在结构体中把长度设置为 1 来解决。
    xpol
        36
    xpol  
       2016-07-30 10:22:08 +08:00 via Android
    补充一下,不是 c99 (误?)中的 vla 。
    cwlmxwb
        37
    cwlmxwb  
       2016-07-30 11:15:25 +08:00   ❤️ 1
    #include <stdio.h>
    #include <malloc.h>
    #include <string.h>

    /*
    flexiable array
    */

    typedef struct flexiable_array_s{
    int a;
    double b;
    char s[0];
    }flexiable_array_t;

    int main(int argc, char const *argv[])
    {
    flexiable_array_t *test = (flexiable_array_t *)malloc(sizeof(flexiable_array_t) + 100 * sizeof(char));
    char *s = "hello world!";
    strcpy((char*)(test + 1), s); //strncpy would be better
    printf("%s\n", test->s);
    free(test);
    return 0;
    }
    jeffersonpig
        38
    jeffersonpig  
       2016-07-30 13:30:10 +08:00   ❤️ 1
    @zhujinliang 有,区别在于 buf 保存的字符串跟其它结构体成员的内存空间是否在一起。 redis 用这样的结构体封装了自己的 sds 字符串类型,一个 sds 实际上就是 sdshdr.buf ,可以通过结构体成员的地址偏移来获取该 sds 的类型和实际内容长度
    franklinyu
        39
    franklinyu  
       2016-07-31 01:44:01 +08:00
    @cwlmxwb 你這用的是 C90 的語法,樓主用的是 C99 裡面的「柔性數組( flexible array )」特性。
    jasonlz
        40
    jasonlz  
       2016-08-03 16:42:30 +08:00
    其正确区分声明和定义的区别,声明一个空数组并不违法,使用一个未定义的变量就是违法行为。不管是结构体里的 buf 还是 main 函数里的 buf ,都知识声明而未定义,即没有分配内存,未定义变量访问就会报错。同样的你如果使用一个未定义的结构体里的 buf ,也是会报错。而上面讨论的 zero-length array 是结构体在定义的时候自动给可变数组一个定义。
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   5392 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 29ms · UTC 08:16 · PVG 16:16 · LAX 00:16 · JFK 03:16
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.