深入淺出變長結構體
1、 問題的引出
項目中用到資料包的處理,但包的大小是不固定的,其長度由標頭的2位元組決定。比如如下的標頭:88 0f 0a ob cd ef 23 00 。長度由頭2個位元組880f決定,考慮位元組序,轉為0f88,轉為10進制3976個位元組的包長度。
這個時候存儲包的時候,一方面可以考慮設定包的大小固定:如4K=4*1024=4096個位元組,因為最大包長不可能超過4k,但該方法的有缺陷,存在一種極端就是包最小僅含標頭不含資料域,此時包為8個位元組,浪費了4096-8 =4088個位元組的存儲空間。另一方面考慮有沒有一種方法能根據長度進行存儲,或者說初始不配置設定長度,計算出了長度後再配置設定存儲呢。而實際項目中正是通過標頭計算出了包的整體大小的。
這就引出了變長結構體的概念。
2、 什麼叫變長結構體?
如下所示:
struct Var_Len_Struct
{
int nsize;
char buffer[0];
};
那結構體是怎麼實作可變長的呢?如上所示,請注意看結構體中的最後一個元素,一個沒有元素的數組。我們可以通過動态開辟一個比結構體大的空間,然後讓buffer去指向那些額外的空間,這樣就可以實作可變長的結構體了。更為巧妙的是,我們甚至可以用nsize存儲字元串buffer的長度。
并且,上述的結構體可以擴充,比如筆者項目中遇到的存儲資料包,前面可能類似標頭的部分(存儲類型、長度等資訊),而後面buffer則存儲資料部分。
同時,需要引起注意的:ISO/IEC 9899-1999裡面,這麼寫是非法的,這個僅僅是GNU C的擴充,gcc可以允許這一文法現象的存在。但最新的C/C++不知道是否可以,我沒有測試過。C99允許。
3、變長結構體的好處展現在哪?
可能有的同學會問到,1引出部分如果說定義定長數組浪費空間,定義一個指針不也能指向變長的資料域部分嗎?
是的,是可以實作的。那麼我們就對比下有什麼不同。
結構體1:s_one,用指針指向資料域部分;
結構體2:s_two, 用[0]的數組;
結構體3:s_three, 因為有的編譯器不支援[0],我們用[1]來表示;多了些存儲。
#include <stdafx.h>
#include <iostream>
using namespace std;
const int BUF_SIZE = 100;
struct s_one
ints_one_cnt;
char*s_one_buf;
struct s_two
ints_two_cnt;
chars_two_buf[0];
struct s_three
ints_three_cnt;
chars_three_buf[1];
int main()
//指派用
constchar* tmp_buf = "abcdefghijklmnopqrstuvwxyz";
intntmp_buf_size = strlen(tmp_buf);
//<1>注意s_one 與s_two的大小的不同
cout<< "sizeof(s_one) = " << sizeof(s_one) << endl; //8
cout<< "sizeof(s_two) = " << sizeof(s_two) << endl; //4
cout<< "sizeof(s_three) = " << sizeof(s_three) << endl;//5-->8結構體對齊
cout<< endl;
//為buf配置設定100個位元組大小的空間
intntotal_stwo_len = sizeof(s_two) + (1 + ntmp_buf_size) * sizeof(char);
intntotal_sthree_len = sizeof(s_three) + ntmp_buf_size * sizeof(char);
//給s_one buf指派
s_one*p_sone = (s_one*)malloc(sizeof(s_one));
memset(p_sone,0, sizeof(s_one));
p_sone->s_one_buf= (char*)malloc(1 + ntmp_buf_size);
memset(p_sone->s_one_buf,0, 1 + ntmp_buf_size);
memcpy(p_sone->s_one_buf,tmp_buf, ntmp_buf_size);
//給s_two buf指派
s_two*p_stwo = (s_two*)malloc(ntotal_stwo_len);
memset(p_stwo,0, ntotal_stwo_len);
memcpy((char*)(p_stwo->s_two_buf),tmp_buf, ntmp_buf_size); //不用加偏移量,直接拷貝!
//給s_three_buf指派
s_three*p_sthree = (s_three*)malloc(ntotal_sthree_len);
memset(p_sthree,0, ntotal_sthree_len);
memcpy((char*)(p_sthree->s_three_buf),tmp_buf, ntmp_buf_size);
cout<< "p_sone->s_one_buf = " << p_sone->s_one_buf<< endl;
cout<< "p_stwo->s_two_buf = " << p_stwo->s_two_buf<< endl;
cout<< "p_sthree->s_three_buf = " <<p_sthree->s_three_buf << endl; //不用加偏移量,直接拷貝!
//<2>注意s_one 與s_two釋放的不同!
if(NULL != p_sone->s_one_buf)
free(p_sone->s_one_buf);
p_sone->s_one_buf= NULL;
if(NULL != p_sone)
{
free(p_sone);
p_sone= NULL;
}
cout<< "free(p_sone) successed!" << endl;
}
if(NULL != p_stwo)
free(p_stwo);
p_stwo= NULL;
cout<< "free(p_stwo) successed!" << endl;
if(NULL != p_sthree)
free(p_sthree);
p_sthree= NULL;
cout<< "free(p_sthree) successed!" << endl;
return0;
筆者vc6.0的編譯器會有如下的警告:
運作結果如下:

對比結果,我們能發現:
<1> 存儲大小方面:s_two的存儲較s_one、s_three都要少,[0]的好處,即用指針的方式需要多開辟存儲空間的。
<2> 資料連續存儲方面:s_one明顯資料域是單獨開辟的空間,與前的nsize不在連續的存儲區域,而s_two,s_three則在連續的存儲空間下。
<3>釋放記憶體方面:顯然s_one的指針的方式,需要先釋放資料域部分,才能釋放指向結構體的指針變量;而s_two,s_three可以直接釋放。
總結如下:
結構體最後使用0或1的長度數組的原因,主要是為了友善的管理記憶體緩沖區,如果你直接使用指針而不使用數組,那麼,你在配置設定記憶體緩沖區時,就必須配置設定結構體一次,然後再配置設定結構體内的指針一次,(而此時配置設定的記憶體已經與結構體的記憶體不連續了,是以要分别管理即申請和釋放)。
而如果使用數組,那麼隻需要一次就可以全部配置設定出來,反過來,釋放時也是一樣,使用數組,一次釋放,使用指針,得先釋放結構體内的指針,再釋放結構體。還不能颠倒次序。
其實變長結構體就是配置設定一段連續的的記憶體,減少記憶體的碎片化,簡化記憶體的管理。
4、變長結構體的應用
<1>Socket通信資料包的傳輸;
<2>解析資料包,如筆者遇到的問題。
<3>其他可以節省空間,連續存儲的地方等。