一文帶你了解C語言中的0長度數(shù)組(可變數(shù)組/柔性數(shù)組)
零長度數(shù)組概念
眾所周知, GNU/GCC 在標準的 C/C++ 基礎上做了有實用性的擴展, 零長度數(shù)組(Arrays of Length Zero) 就是其中一個知名的擴展.
多數(shù)情況下, 其應用在變長數(shù)組中, 其定義如下:
struct Packet { int state; int len; char cData[0]; //這里的0長結構體就為變長結構體提供了非常好的支持 };
首先對 0長度數(shù)組, 也叫柔性數(shù)組 做一個解釋 :
- 用途 : 長度為0的數(shù)組的主要用途是為了滿足需要變長度的結構體
- 用法 : 在一個結構體的最后, 申明一個長度為0的數(shù)組, 就可以使得這個結構體是可變長的. 對于編譯器來說, 此時長度為0的數(shù)組并不占用空間, 因為數(shù)組名本身不占空間, 它只是一個偏移量, 數(shù)組名這個符號本身代表了一個不可修改的地址常量
(注意 : 數(shù)組名永遠都不會是指針!), 但對于這個數(shù)組的大小, 我們可以進行動態(tài)分配
注意 :如果結構體是通過calloc、malloc或 者new等動態(tài)分配方式生成,在不需要時要釋放相應的空間。
優(yōu)點 :比起在結構體中聲明一個指針變量、再進行動態(tài)分 配的辦法,這種方法效率要高。因為在訪問數(shù)組內容時,不需要間接訪問,避免了兩次訪存。
缺點 :在結構體中,數(shù)組為0的數(shù)組必須在最后聲明,使 用上有一定限制。
對于編譯器而言, 數(shù)組名僅僅是一個符號, 它不會占用任何空間, 它在結構體中, 只是代表了一個偏移量, 代表一個不可修改的地址常量!
0長度數(shù)組的用途
我們設想這樣一個場景, 我們在網(wǎng)絡通信過程中使用的數(shù)據(jù)緩沖區(qū), 緩沖區(qū)包括一個len字段和data字段, 分別標識數(shù)據(jù)的長度和傳輸?shù)臄?shù)據(jù), 我們常見的有幾種設計思路:
- 定長數(shù)據(jù)緩沖區(qū), 設置一個足夠大小 MAX_LENGTH 的數(shù)據(jù)緩沖區(qū)
- 設置一個指向實際數(shù)據(jù)的指針, 每次使用時, 按照數(shù)據(jù)的長度動態(tài)的開辟數(shù)據(jù)緩沖區(qū)的空間
我們從實際場景中應用的設計來考慮他們的優(yōu)劣. 主要考慮的有, 緩沖區(qū)空間的開辟, 釋放和訪問。
1、定長包(開辟空間, 釋放, 訪問):
比如我要發(fā)送 1024 字節(jié)的數(shù)據(jù), 如果用定長包, 假設定長包的長度 MAX_LENGTH 為 2048, 就會浪費 1024 個字節(jié)的空間, 也會造成不必要的流量浪費:
數(shù)據(jù)結構定義:
// 定長緩沖區(qū) struct max_buffer { int len; char data[MAX_LENGTH]; };
數(shù)據(jù)結構大小:考慮對齊, 那么數(shù)據(jù)結構的大小 >= sizeof(int) + sizeof(char) * MAX_LENGTH
由于考慮到數(shù)據(jù)的溢出, 變長數(shù)據(jù)包中的 data 數(shù)組長度一般會設置得足夠長足以容納最大的數(shù)據(jù), 因此 max_buffer 中的 data 數(shù)組很多情況下都沒有填滿數(shù)據(jù), 因此造成了浪費
數(shù)據(jù)包的構造:假如我們要發(fā)送 CURR_LENGTH = 1024 個字節(jié), 我們如何構造這個數(shù)據(jù)包呢;一般來說, 我們會返回一個指向緩沖區(qū)數(shù)據(jù)結構 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 個字節(jié) p->len, 作為包頭(就是多出來的那部分),這個包頭是用來描述緊接著包頭后面的數(shù)據(jù)部分的長度,這里是 1024, 所以前四個字節(jié)賦值為 1024 (既然我們要構造不定長數(shù)據(jù)包,那么這個包到底有多長呢,因此,我們就必須通過一個變量來表明這個數(shù)據(jù)包的長度,這就是len的作用);而緊接其后的內存是真正的數(shù)據(jù)部分, 通過 p->data, 最后, 進行一個 memcpy() 內存拷貝, 把要發(fā)送的數(shù)據(jù)填入到這段內存當中
釋放:那么當使用完畢釋放數(shù)據(jù)的空間的時候, 直接釋放就可以了
/// 銷毀 free(mbuffer); mbuffer = NULL;
2、小結:
使用定長數(shù)組, 作為數(shù)據(jù)緩沖區(qū), 為了避免造成緩沖區(qū)溢出, 數(shù)組的大小一般設為足夠的空間 MAX_LENGTH, 而實際使用過程中, 達到 MAX_LENGTH 長度的數(shù)據(jù)很少, 那么多數(shù)情況下, 緩沖區(qū)的大部分空間都是浪費掉的
但是使用過程很簡單, 數(shù)據(jù)空間的開辟和釋放簡單, 無需程序員考慮額外的操作
3、 指針數(shù)據(jù)包(開辟空間, 釋放, 訪問):
如果你將上面的長度為 MAX_LENGTH 的定長數(shù)組換為指針, 每次使用時動態(tài)的開辟 CURR_LENGTH 大小的空間, 那么就避免造成 MAX_LENGTH - CURR_LENGTH 空間的浪費, 只浪費了一個指針域的空間:
數(shù)據(jù)包定義:
struct point_buffer { int len; char *data; };
數(shù)據(jù)結構大小:考慮對齊, 那么數(shù)據(jù)結構的大小 >= sizeof(int) + sizeof(char *)
空間分配:但是也造成了使用在分配內存時,需采用兩步
// ===================== // 指針數(shù)組 占用-開辟-銷毀 // ===================== /// 占用 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); } }
首先, 需為結構體分配一塊內存空間;其次再為結構體中的成員變量分配內存空間。
這樣兩次分配的內存是不連續(xù)的, 需要分別對其進行管理. 當使用長度為的數(shù)組時, 則是采用一次分配的原則, 一次性將所需的內存全部分配給它。
釋放:相反, 釋放時也是一樣的:
/// 銷毀 free(pbuffer->data); free(pbuffer); pbuffer = NULL;
小結:
- 使用指針結果作為緩沖區(qū), 只多使用了一個指針大小的空間, 無需使用 MAX_LENGTH 長度的數(shù)組, 不會造成空間的大量浪費
- 但那是開辟空間時, 需要額外開辟數(shù)據(jù)域的空間, 施放時候也需要顯示釋放數(shù)據(jù)域的空間, 但是實際使用過程中, 往往在函數(shù)中開辟空間, 然后返回給使用者指向 struct point_buffer 的指針, 這時候我們并不能假定使用者了解我們開辟的細節(jié), 并按照約定的操作釋放空間, 因此使用起來多有不便, 甚至造成內存泄漏。
4、變長數(shù)據(jù)緩沖區(qū)(開辟空間, 釋放, 訪問)
定長數(shù)組使用方便, 但是卻浪費空間, 指針形式只多使用了一個指針的空間, 不會造成大量空間分浪費, 但是使用起來需要多次分配, 多次釋放, 那么有沒有一種實現(xiàn)方式能夠既不浪費空間, 又使用方便的呢?
GNU C 的0長度數(shù)組, 也叫變長數(shù)組, 柔性數(shù)組就是這樣一個擴展. 對于0長數(shù)組的這個特點,很容易構造出變成結構體,如緩沖區(qū),數(shù)據(jù)包等等:
數(shù)據(jù)結構定義:
// 0長度數(shù)組 struct zero_buffer { int len; char data[0]; };
數(shù)據(jù)結構大?。哼@樣的變長數(shù)組常用于網(wǎng)絡通信中構造不定長數(shù)據(jù)包, 不會浪費空間浪費網(wǎng)絡流量, 因為char data[0]; 只是個數(shù)組名, 是不占用存儲空間的:
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;
總結:
// zero_length_array.c #include <stdio.h> #include <stdlib.h> #define MAX_LENGTH 1024 #define CURR_LENGTH 512 // 0長度數(shù)組 struct zero_buffer { int len; char data[0]; }__attribute((packed)); // 定長數(shù)組 struct max_buffer { int len; char data[MAX_LENGTH]; }__attribute((packed)); // 指針數(shù)組 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長度數(shù)組 占用-開辟-銷毀 // ===================== /// 占用 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; // ===================== // 定長數(shù)組 占用-開辟-銷毀 // ===================== /// 占用 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; // ===================== // 指針數(shù)組 占用-開辟-銷毀 // ===================== /// 占用 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; }
GNU Document中 變長數(shù)組的支持
參考:
6.17 Arrays of Length Zero
C Struct Hack – Structure with variable length array
在 C90 之前, 并不支持0長度的數(shù)組, 0長度數(shù)組是 GNU C 的一個擴展, 因此早期的編譯器中是無法通過編譯的;對于 GNU C 增加的擴展, GCC 提供了編譯選項來明確的標識出他們:
- -pedantic 選項,那么使用了擴展語法的地方將產生相應的警告信息
- -Wall 使用它能夠使GCC產生盡可能多的警告信息
- -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長度數(shù)組其實就是靈活的運用的數(shù)組指向的是其后面的連續(xù)的內存空間:
struct buffer { int len; char data[0]; };
在早期沒引入0長度數(shù)組的時候, 大家是通過定長數(shù)組和指針的方式來解決的, 但是:
定長數(shù)組定義了一個足夠大的緩沖區(qū), 這樣使用方便, 但是每次都造成空間的浪費
指針的方式, 要求程序員在釋放空間是必須進行多次的free操作, 而我們在使用的過程中往往在函數(shù)中返回了指向緩沖區(qū)的指針, 我們并不能保證每個人都理解并遵從我們的釋放方式
所以 GNU 就對其進行了0長度數(shù)組的擴展. 當使用data[0]的時候, 也就是0長度數(shù)組的時候,0長度數(shù)組作為數(shù)組名, 并不占用存儲空間.
在C99之后,也加了類似的擴展,只不過用的是 char payload[]這種形式(所以如果你在編譯的時候確實需要用到-pedantic參數(shù),那么你可以將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 編譯后, 不出現(xiàn)警告, 說明這種語法是 C 標準的
gcc 2.c -pedantic -std=c99
所以結構體的末尾, 就是指向了其后面的內存數(shù)據(jù)。因此我們可以很好的將該類型的結構體作為數(shù)據(jù)報文的頭格式,并且最后一個成員變量,也就剛好是數(shù)據(jù)內容了.
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以示區(qū)分。如果你把數(shù)據(jù)打出來。即如下的信息:
f1.x = 1
f1.y[0] = 2
f1.y[1] = 3
f1.y[2] = 4
也就是f1.y指向的是{2,3,4}這塊內存中的數(shù)據(jù)。所以我們就可以輕易的得到,f2.f1.y指向的數(shù)據(jù)也就是正好f2.data的內容了。打印出來的數(shù)據(jù):
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長度的數(shù)組,必須放在結構體的末尾。如果你沒有把它放在末尾的話。編譯的時候,會有如下的錯誤:
main.c:37:9: error: flexible array member not at end of struct
int y[];
^
到這邊,你可能會有疑問,如果將struct f1中的int y[]替換成int *y,又會是如何?這就涉及到數(shù)組和指針的問題了. 有時候吧,這兩個是一樣的,有時候又有區(qū)別。
首先要說明的是,支持0長度數(shù)組的擴展,重點在數(shù)組,也就是不能用int *y指針來替換。sizeof的長度就不一樣了。把struct f1改成這樣:
struct f3 { int x; int *y; };
在32/64位下, int均是4個字節(jié), sizeof(struct f1)=4,而sizeof(struct f3)=16
因為 int *y 是指針, 指針在64位下, 是64位的, sizeof(struct f3) = 16, 如果在32位環(huán)境的話, 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; }
0長度數(shù)組的其他特征
1、為什么0長度數(shù)組不占用存儲空間:
0長度數(shù)組與指針實現(xiàn)有什么區(qū)別呢, 為什么0長度數(shù)組不占用存儲空間呢?
其實本質上涉及到的是一個C語言里面的數(shù)組和指針的區(qū)別問題. char a[1]里面的a和char *b的b相同嗎?
《 Programming Abstractions in C》(Roberts, E. S.,機械工業(yè)出版社,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]本質上是否有區(qū)別?我們可以通過一個例子來說明。
參見如下兩個程序 gdb_zero_length_array.c 和 gdb_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位系統(tǒng)中, 匯編我們看出, 變長數(shù)組結構的大小為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指令, 參見下一列子
從這里可以看到, 訪問成員數(shù)組名其實得到的是數(shù)組的相對地址, 而訪問成員指針其實是相對地址里的內容(這和訪問其它非指針或數(shù)組的變量是一樣的):
訪問相對地址,程序不會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; }
對于 char a[0] 來說, 匯編代碼用了 leal 指令, leal 16(%esp), %eax:
對于 char *a 來說,匯編代碼用了 movl 指令, movl 28(%esp), %eax
2、地址優(yōu)化:
// 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; }
由于0長度數(shù)組是 GNU C 的擴展, 不被標準庫任可, 那么一些巧妙編寫的詭異代碼, 其執(zhí)行結果就是依賴于編譯器和優(yōu)化策略的實現(xiàn)的.
比如上面的代碼, a和b的地址就會被編譯器優(yōu)化到一處, 因為a[0] 和 b[0] 對于程序來說是無法使用的, 這讓我們想到了什么?
編譯器對于相同字符串常量, 往往地址也是優(yōu)化到一處, 減少空間占用:
// 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; }
到此這篇關于一文帶你了解C語言中的0長度數(shù)組(可變數(shù)組/柔性數(shù)組)的文章就介紹到這了,更多相關C語言0長度數(shù)組內容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關文章希望大家以后多多支持腳本之家!
相關文章
C++中范圍(Ranges)與視圖(Views)的常見問題、易錯點
ranges和views是C20引入的重要特性,它們讓代碼更加簡潔、高效且富有表達力,通過理解其基本概念、注意常見的陷阱,并合理應用高級技巧,開發(fā)者可以充分利用這些新特性,提升軟件質量和開發(fā)效率,,本文將深入淺出地探討ranges與views的基礎概念、常見問題、易錯點及避免策略2024-06-06C++實現(xiàn)LeetCode(168.求Excel表列名稱)
這篇文章主要介紹了C++實現(xiàn)LeetCode(168.求Excel表列名稱),本篇文章通過簡要的案例,講解了該項技術的了解與使用,以下就是詳細內容,需要的朋友可以參考下2021-08-08