1. 程式人生 > >C語言0長度陣列(可變陣列/柔性陣列)詳解

C語言0長度陣列(可變陣列/柔性陣列)詳解

1 零長度陣列概念

眾所周知, GNU/GCC 在標準的 C/C++ 基礎上做了有實用性的擴充套件, 零長度陣列(Arrays of Length Zero) 就是其中一個知名的擴充套件.

多數情況下, 其應用在變長陣列中, 其定義如下

struct Packet
{
    int state;
    int len;
    char cData[0]; //這裡的0長結構體就為變長結構體提供了非常好的支援
};

首先對 0長度陣列, 也叫柔性陣列 做一個解釋 :

  • 用途 : 長度為0的陣列的主要用途是為了滿足需要變長度的結構體

  • 用法 : 在一個結構體的最後, 申明一個長度為0的陣列, 就可以使得這個結構體是可變長的. 對於編譯器來說, 此時長度為0的陣列並不佔用空間, 因為陣列名本身不佔空間, 它只是一個偏移量, 陣列名這個符號本身代表了一個不可修改的地址常量

(注意 : 陣列名永遠都不會是指標!), 但對於這個陣列的大小, 我們可以進行動態分配

注意 :如果結構體是通過calloc、malloc或 者new等動態分配方式生成,在不需要時要釋放相應的空間。

優點 :比起在結構體中宣告一個指標變數、再進行動態分 配的辦法,這種方法效率要高。因為在訪問陣列內容時,不需要間接訪問,避免了兩次訪存。

缺點 :在結構體中,陣列為0的陣列必須在最後宣告,使 用上有一定限制。

對於編譯器而言, 陣列名僅僅是一個符號, 它不會佔用任何空間, 它在結構體中, 只是代表了一個偏移量, 代表一個不可修改的地址常量!

2 0長度陣列的用途

我們設想這樣一個場景, 我們在網路通訊過程中使用的資料緩衝區, 緩衝區包括一個len欄位和data欄位, 分別標識資料的長度和傳輸的資料, 我們常見的有幾種設計思路

  • 定長資料緩衝區, 設定一個足夠大小 MAX_LENGTH 的資料緩衝區

  • 設定一個指向實際資料的指標, 每次使用時, 按照資料的長度動態的開闢資料緩衝區的空間.

我們從實際場景中應用的設計來考慮他們的優劣. 主要考慮的有, 緩衝區空間的開闢, 釋放和訪問.

2.1 定長包(開闢空間, 釋放, 訪問)

比如我要傳送 1024 位元組的資料, 如果用定長包, 假設定長包的長度 MAX_LENGTH2048

, 就會浪費 1024 個位元組的空間, 也會造成不必要的流量浪費.

  • 資料結構定義
//  定長緩衝區
struct max_buffer
{
    int     len;
    char    data[MAX_LENGTH];
};
  • 資料結構大小

考慮對齊, 那麼資料結構的大小 >= sizeof(int) + sizeof(char) * MAX_LENGTH

由於考慮到資料的溢位, 變長資料包中的 data 陣列長度一般會設定得足夠長足以容納最大的資料, 因此 max_buffer 中的 data 陣列很多情況下都沒有填滿資料, 因此造成了浪費

  • 資料包的構造

假如我們要傳送 CURR_LENGTH = 1024 個位元組, 我們如何構造這個資料包呢:

一般來說, 我們會返回一個指向緩衝區資料結構 max_buffer 的指標.

    ///  開闢
    if ((mbuffer = (struct max_buffer *)malloc(sizeof(struct max_buffer))) != NULL)
    {
        mbuffer->len = CURR_LENGTH;
        memcpy(mbuffer->data, "Hello World", CURR_LENGTH);


        printf("%d, %s\n", mbuffer->len, mbuffer->data);
    }
  • 訪問

這段記憶體要分兩部分使用

前部分 4 個位元組 p->len, 作為包頭(就是多出來的那部分),這個包頭是用來描述緊接著包頭後面的資料部分的長度,這裡是 1024, 所以前四個位元組賦值為 1024 (既然我們要構造不定長資料包,那麼這個包到底有多長呢,因此,我們就必須通過一個變數來表明這個資料包的長度,這就是len的作用),

而緊接其後的記憶體是真正的資料部分, 通過 p->data, 最後, 進行一個 memcpy() 記憶體拷貝, 把要傳送的資料填入到這段記憶體當中

  • 釋放

那麼當使用完畢釋放資料的空間的時候, 直接釋放就可以了

    /// 銷燬
    free(mbuffer);
    mbuffer = NULL;
  • 小結

    1. 使用定長陣列, 作為資料緩衝區, 為了避免造成緩衝區溢位, 陣列的大小一般設為足夠的空間 MAX_LENGTH, 而實際使用過程中, 達到 MAX_LENGTH 長度的資料很少, 那麼多數情況下, 緩衝區的大部分空間都是浪費掉的.

    2. 但是使用過程很簡單, 資料空間的開闢和釋放簡單, 無需程式設計師考慮額外的操作

2.2 指標資料包(開闢空間, 釋放, 訪問)

如果你將上面的長度為 MAX_LENGTH 的定長陣列換為指標, 每次使用時動態的開闢 CURR_LENGTH 大小的空間, 那麼就避免造成 MAX_LENGTH - CURR_LENGTH 空間的浪費, 只浪費了一個指標域的空間.

  • 資料包定義
struct point_buffer
{
    int     len;
    char    *data;
};
  • 資料結構大小

考慮對齊, 那麼資料結構的大小 >= sizeof(int) + sizeof(char *)

  • 空間分配

但是也造成了使用在分配記憶體時,需採用兩步

    // =====================
    // 指標陣列  佔用-開闢-銷燬
    // =====================
    ///  佔用
    printf("the length of struct test3:%d\n",sizeof(struct point_buffer));
    ///  開闢
    if ((pbuffer = (struct point_buffer *)malloc(sizeof(struct point_buffer))) != NULL)
    {
        pbuffer->len = CURR_LENGTH;
        if ((pbuffer->data = (char *)malloc(sizeof(char) * CURR_LENGTH)) != NULL)
        {
            memcpy(pbuffer->data, "Hello World", CURR_LENGTH);


            printf("%d, %s\n", pbuffer->len, pbuffer->data);
        }
    }
  1. 首先, 需為結構體分配一塊記憶體空間;

  2. 其次再為結構體中的成員變數分配記憶體空間.

這樣兩次分配的記憶體是不連續的, 需要分別對其進行管理. 當使用長度為的陣列時, 則是採用一次分配的原則, 一次性將所需的記憶體全部分配給它.

  • 釋放

相反, 釋放時也是一樣的.

    /// 銷燬
    free(pbuffer->data);
    free(pbuffer);
    pbuffer = NULL;
  • 小結

    1. 使用指標結果作為緩衝區, 只多使用了一個指標大小的空間, 無需使用 MAX_LENGTH 長度的陣列, 不會造成空間的大量浪費.

    2. 但那是開闢空間時, 需要額外開闢資料域的空間, 施放時候也需要顯示釋放資料域的空間, 但是實際使用過程中, 往往在函式中開闢空間, 然後返回給使用者指向 struct point_buffer 的指標, 這時候我們並不能假定使用者瞭解我們開闢的細節, 並按照約定的操作釋放空間, 因此使用起來多有不便, 甚至造成記憶體洩漏

2.3 變長資料緩衝區(開闢空間, 釋放, 訪問)

定長陣列使用方便, 但是卻浪費空間, 指標形式只多使用了一個指標的空間, 不會造成大量空間分浪費, 但是使用起來需要多次分配, 多次釋放, 那麼有沒有一種實現方式能夠既不浪費空間, 又使用方便的呢?

GNU C 的0長度陣列, 也叫變長陣列, 柔性陣列就是這樣一個擴充套件. 對於0長陣列的這個特點,很容易構造出變成結構體,如緩衝區,資料包等等:

  • 資料結構定義
//  0長度陣列
struct zero_buffer
{
    int     len;
    char    data[0];
};
  • 資料結構大小

這樣的變長陣列常用於網路通訊中構造不定長資料包, 不會浪費空間浪費網路流量, 因為char data[0]; 只是個數組名, 是不佔用儲存空間的,

sizeof(struct zero_buffer) = sizeof(int)

  • 開闢空間

那麼我們使用的時候, 只需要開闢一次空間即可

    ///  開闢
    if ((zbuffer = (struct zero_buffer *)malloc(sizeof(struct zero_buffer) + sizeof(char) * CURR_LENGTH)) != NULL)
    {
        zbuffer->len = CURR_LENGTH;
        memcpy(zbuffer->data, "Hello World", CURR_LENGTH);


        printf("%d, %s\n", zbuffer->len, zbuffer->data);
    }
  • 釋放空間

釋放空間也是一樣的, 一次釋放即可

    ///  銷燬
    free(zbuffer);
    zbuffer = NULL;

2.4 總結

// zero_length_array.c
#include <stdio.h>
#include <stdlib.h>


#define MAX_LENGTH      1024
#define CURR_LENGTH      512

//  0長度陣列
struct zero_buffer
{
    int     len;
    char    data[0];
}__attribute((packed));


//  定長陣列
struct max_buffer
{
    int     len;
    char    data[MAX_LENGTH];
}__attribute((packed));


//  指標陣列
struct point_buffer
{
    int     len;
    char    *data;
}__attribute((packed));

int main(void)
{
    struct zero_buffer  *zbuffer = NULL;
    struct max_buffer   *mbuffer = NULL;
    struct point_buffer *pbuffer = NULL;


    // =====================
    // 0長度陣列  佔用-開闢-銷燬
    // =====================
    ///  佔用
    printf("the length of struct test1:%d\n",sizeof(struct zero_buffer));
    ///  開闢
    if ((zbuffer = (struct zero_buffer *)malloc(sizeof(struct zero_buffer) + sizeof(char) * CURR_LENGTH)) != NULL)
    {
        zbuffer->len = CURR_LENGTH;
        memcpy(zbuffer->data, "Hello World", CURR_LENGTH);


        printf("%d, %s\n", zbuffer->len, zbuffer->data);
    }
    ///  銷燬
    free(zbuffer);
    zbuffer = NULL;


    // =====================
    // 定長陣列  佔用-開闢-銷燬
    // =====================
    ///  佔用
    printf("the length of struct test2:%d\n",sizeof(struct max_buffer));
    ///  開闢
    if ((mbuffer = (struct max_buffer *)malloc(sizeof(struct max_buffer))) != NULL)
    {
        mbuffer->len = CURR_LENGTH;
        memcpy(mbuffer->data, "Hello World", CURR_LENGTH);


        printf("%d, %s\n", mbuffer->len, mbuffer->data);
    }
    /// 銷燬
    free(mbuffer);
    mbuffer = NULL;

    // =====================
    // 指標陣列  佔用-開闢-銷燬
    // =====================
    ///  佔用
    printf("the length of struct test3:%d\n",sizeof(struct point_buffer));
    ///  開闢
    if ((pbuffer = (struct point_buffer *)malloc(sizeof(struct point_buffer))) != NULL)
    {
        pbuffer->len = CURR_LENGTH;
        if ((pbuffer->data = (char *)malloc(sizeof(char) * CURR_LENGTH)) != NULL)
        {
            memcpy(pbuffer->data, "Hello World", CURR_LENGTH);


            printf("%d, %s\n", pbuffer->len, pbuffer->data);
        }
    }
    /// 銷燬
    free(pbuffer->data);
    free(pbuffer);
    pbuffer = NULL;


    return EXIT_SUCCESS;
}

執行結果

  • 長度為0的陣列並不佔有記憶體空間, 而指標方式需要佔用記憶體空間.

  • 對於長度為0陣列, 在申請記憶體空間時, 採用一次性分配的原則進行; 對於包含指標的結構體, 才申請空間時需分別進行, 釋放時也需分別釋放.

  • 對於長度為的陣列的訪問可採用陣列方式進行

3 GNU Document中 變長陣列的支援

C90 之前, 並不支援0長度的陣列, 0長度陣列是 GNU C 的一個擴充套件, 因此早期的編譯器中是無法通過編譯的

對於 GNU C 增加的擴充套件, GCC 提供了編譯選項來明確的標識出他們

1、-pedantic 選項,那麼使用了擴充套件語法的地方將產生相應的警告資訊

2、-Wall 使用它能夠使GCC產生儘可能多的警告資訊

3、-Werror, 它要求GCC將所有的警告當成錯誤進行處理

// 1.c
#include <stdio.h>
#include <stdlib.h>


int main(void)
{
    char a[0];
    printf("%ld", sizeof(a));
    return EXIT_SUCCESS;
}

我們來編譯

gcc 1.c -Wall   # 顯示所有警告
#none warning and error

gcc 1.c -Wall -pedantic  # 對GNU C的擴充套件顯示警告
1.c: In function ‘main’:
1.c:7: warning: ISO C forbids zero-size array ‘a’


gcc 1.c -Werror -Wall -pedantic # 顯示所有警告同時GNU C的擴充套件顯示警告, 將警告用error顯示
cc1: warnings being treated as errors
1.c: In function ‘main’:
1.c:7: error: ISO C forbids zero-size array ‘a’

執行結果

0長度陣列其實就是靈活的運用的陣列指向的是其後面的連續的記憶體空間

struct buffer
{
    int     len;
    char    data[0];
};

在早期沒引入0長度陣列的時候, 大家是通過定長陣列和指標的方式來解決的, 但是

  • 定長陣列定義了一個足夠大的緩衝區, 這樣使用方便, 但是每次都造成空間的浪費
  • 指標的方式, 要求程式設計師在釋放空間是必須進行多次的free操作, 而我們在使用的過程中往往在函式中返回了指向緩衝區的指標, 我們並不能保證每個人都理解並遵從我們的釋放方式

所以 GNU 就對其進行了0長度陣列的擴充套件. 當使用data[0]的時候, 也就是0長度陣列的時候,0長度陣列作為陣列名, 並不佔用儲存空間.

C99之後,也加了類似的擴充套件,只不過用的是 char payload[]這種形式(所以如果你在編譯的時候確實需要用到-pedantic引數,那麼你可以將char payload[0]型別改成char payload[], 這樣就可以編譯通過了,當然你的編譯器必須支援C99標準的,如果太古老的編譯器,那可能不支援了)

// 2.c payload
#include <stdio.h>
#include <stdlib.h>

struct payload
{
    int   len;
    char  data[];
};

int main(void)
{
    struct payload pay;
    printf("%ld", sizeof(pay));
    return EXIT_SUCCESS;
}

使用 -pedantic 編譯後, 不出現警告, 說明這種語法是 C 標準的

gcc 2.c -pedantic -std=c99

2

所以結構體的末尾, 就是指向了其後面的記憶體資料。因此我們可以很好的將該型別的結構體作為資料報文的頭格式,並且最後一個成員變數,也就剛好是資料內容了.

GNU手冊還提供了另外兩個結構體來說明,更容易看懂意思:

struct f1 {
    int x;
    int y[];
} f1 = { 1, { 2, 3, 4 } };

struct f2 {
    struct f1 f1;
    int data[3];
} f2 = { { 1 }, { 5, 6, 7 } };

我把f2裡面的2,3,4改成了5,6,7以示區分。如果你把資料打出來。即如下的資訊:

f1.x = 1
f1.y[0] = 2
f1.y[1] = 3
f1.y[2] = 4

也就是f1.y指向的是{2,3,4}這塊記憶體中的資料。所以我們就可以輕易的得到,f2.f1.y指向的資料也就是正好f2.data的內容了。打印出來的資料:

f2.f1.x = 1
f2.f1.y[0] = 5
f2.f1.y[1] = 6
f2.f1.y[2] = 7

如果你不是很確認其是否佔用空間. 你可以用sizeof來計算一下。就可以知道sizeof(struct f1)=4,也就是int y[]其實是不佔用空間的。但是這個0長度的陣列,必須放在結構體的末尾。如果你沒有把它放在末尾的話。編譯的時候,會有如下的錯誤:

main.c:37:9: error: flexible array member not at end of struct
     int y[];
         ^

到這邊,你可能會有疑問,如果將struct f1中的int y[]替換成int *y,又會是如何?這就涉及到陣列和指標的問題了. 有時候吧,這兩個是一樣的,有時候又有區別。

首先要說明的是,支援0長度陣列的擴充套件,重點在陣列,也就是不能用int *y指標來替換。sizeof的長度就不一樣了。把struct f1改成這樣:

struct f3 {
    int x;
    int *y;
};

在32/64位下, int均是4個位元組, sizeof(struct f1)=4,而sizeof(struct f3)=16

因為 int *y 是指標, 指標在64位下, 是64位的, sizeof(struct f3) = 16, 如果在32位環境的話, sizeof(struct f3) 則是 8 了, sizeof(struct f1) 不變. 所以 int *y 是不能替代 int y[] 的.

程式碼如下

// 3.c
#include <stdio.h>
#include <stdlib.h>


struct f1 {
    int x;
    int y[];
} f1 = { 1, { 2, 3, 4 } };

struct f2 {
    struct f1 f1;
    int data[3];
} f2 = { { 1 }, { 5, 6, 7 } };


struct f3
{
    int x;
    int *y;
};

int main(void)
{
    printf("sizeof(f1) = %d\n", sizeof(struct f1));
    printf("sizeof(f2) = %d\n", sizeof(struct f2));
    printf("szieof(f3) = %d\n\n", sizeof(struct f3));

    printf("f1.x = %d\n", f1.x);
    printf("f1.y[0] = %d\n", f1.y[0]);
    printf("f1.y[1] = %d\n", f1.y[1]);
    printf("f1.y[2] = %d\n", f1.y[2]);


    printf("f2.f1.x = %d\n", f1.x);
    printf("f2.f1.y[0] = %d\n", f2.f1.y[0]);
    printf("f2.f1.y[1] = %d\n", f2.f1.y[1]);
    printf("f2.f1.y[2] = %d\n", f2.f1.y[2]);

    return EXIT_SUCCESS;
}

執行結果

4 0長度陣列的其他特徵

4.1 為什麼0長度陣列不佔用儲存空間

0長度陣列與指標實現有什麼區別呢, 為什麼0長度陣列不佔用儲存空間呢?

其實本質上涉及到的是一個C語言裡面的陣列和指標的區別問題. char a[1]裡面的achar *bb相同嗎?

《 Programming Abstractions in C》(Roberts, E. S.,機械工業出版社,2004.6)82頁裡面說

“arr is defined to be identical to &arr[0]”.

也就是說,char a[1]裡面的a實際是一個常量,等於&a[0]。而char *b是有一個實實在在的指標變數b存在。 所以,a=b是不允許的,而b=a是允許的。 兩種變數都支援下標式的訪問,那麼對於a[0]和b[0]本質上是否有區別?我們可以通過一個例子來說明。

參見如下兩個程式 gdb_zero_length_array.cgdb_zero_length_array.c

//  gdb_zero_length_array.c
#include <stdio.h>
#include <stdlib.h>

struct str
{
    int len;
    char s[0];
};

struct foo
{
    struct str *a;
};

int main(void)
{
    struct foo f = { NULL };

    printf("sizeof(struct str) = %d\n", sizeof(struct str));

    printf("before f.a->s.\n");
    if(f.a->s)
    {
        printf("before printf f.a->s.\n");
        printf(f.a->s);
        printf("before printf f.a->s.\n");
    }

    return EXIT_SUCCESS;
}

測試結果

//  gdb_pzero_length_array.c
#include <stdio.h>
#include <stdlib.h>

struct str
{
    int len;
    char *s;
};

struct foo
{
    struct str *a;
};

int main(void)
{
    struct foo f = { NULL };

    printf("sizeof(struct str) = %d\n", sizeof(struct str));

    printf("before f.a->s.\n");

    if (f.a->s)
    {
        printf("before printf f.a->s.\n");
        printf(f.a->s);
        printf("before printf f.a->s.\n");
    }

    return EXIT_SUCCESS;
}

測試結果

可以看到這兩個程式雖然都存在訪問異常, 但是段錯誤的位置卻不同

我們將兩個程式編譯成彙編, 然戶 diff 檢視他們的彙編程式碼有何不同

gcc -S gdb_zero_length_array.c -o gdb_test.s
gcc -S gdb_pzero_length_array.c -o gdb_ptest
diff gdb_test.s gdb_ptest.s

1c1
<   .file   "gdb_zero_length_array.c"
---
>   .file   "gdb_pzero_length_array.c"
23c23
<   movl    $4, %esi
---
>   movl    $16, %esi
30c30
<   addq    $4, %rax
---
>   movq    8(%rax), %rax
36c36
<   addq    $4, %rax
---
>   movq    8(%rax), %rax
#    printf("sizeof(struct str) = %d\n", sizeof(struct str));
23c23
<   movl    $4, %esi    #printf("sizeof(struct str) = %d\n", sizeof(struct str));
---
>   movl    $16, %esi  #printf("sizeof(struct str) = %d\n", sizeof(struct str));

從64位系統中, 彙編我們看出, 變長陣列結構的大小為4, 而指標形式的結構大小為16

f.a->s
30c30/36c36
<   addq    $4, %rax
---
>   movq    8(%rax), %rax

可以看到有

  • 對於 char s[0] 來說, 彙編程式碼用了 addq 指令, addq $4, %rax

  • 對於 char*s 來說,彙編程式碼用了 movq 指令, movq 8(%rax), %rax

addq%rax + sizeof(struct str), 即str結構的末尾即是char s[0]的地址, 這一步只是拿到了其地址, 而 movq 則是把地址裡的內容放進去, 因此有時也被翻譯為leap指令, 參見下一列子

從這裡可以看到, 訪問成員陣列名其實得到的是陣列的相對地址, 而訪問成員指標其實是相對地址裡的內容(這和訪問其它非指標或陣列的變數是一樣的)

  • 訪問相對地址,程式不會crash,但是,訪問一個非法的地址中的內容,程式就會crash。

有時候

// 4-1.c
#include <stdio.h>
#include <stdlib.h>

int main(void)
{

    char *a;
    printf("%p\n", a);

    return EXIT_SUCCESS;
}
4-2.c
#include <stdio.h>
#include <stdlib.h>

int main(void)
{

    char a[0];
    printf("%p\n", a);

    return EXIT_SUCCESS;
}
$ diff 4-1.s 4-2.s
1c1
<       .file   "4-1.c"
---
>       .file   "4-2.c"
13c13
<       subl    $16, %esp
---
>       subl    $32, %esp
15c15
<       leal    16(%esp), %eax
---
>       movl    28(%esp), %eax
  • 對於 char a[0] 來說, 彙編程式碼用了 leal 指令, leal 16(%esp), %eax

  • 對於 char *a 來說,彙編程式碼用了 movl 指令, movl 28(%esp), %eax

4.2 地址優化

// 5-1.c
#include <stdio.h>
#include <stdlib.h>

int main(void)
{

    char a[0];
    printf("%p\n", a);

    char b[0];
    printf("%p\n", b);

    return EXIT_SUCCESS;
}

5

由於0長度陣列是 GNU C 的擴充套件, 不被標準庫任可, 那麼一些巧妙編寫的詭異程式碼, 其執行結果就是依賴於編譯器和優化策略的實現的.

比如上面的程式碼, a和b的地址就會被編譯器優化到一處, 因為a[0] 和 b[0] 對於程式來說是無法使用的, 這讓我們想到了什麼?

編譯器對於相同字串常量, 往往地址也是優化到一處, 減少空間佔用

//  5-2.c
#include <stdio.h>
#include <stdlib.h>

int main(void)
{

    const char *a = "Hello";
    printf("%p\n", a);

    const char *b = "Hello";
    printf("%p\n", b);

    const char c[] = "Hello";
    printf("%p\n", c);

    return EXIT_SUCCESS;
}

5-2

參考

零長度陣列