天天看點

深入淺出變長結構體

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; //不用加偏移量,直接拷貝!
cout<< 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>其他可以節省空間,連續存儲的地方等。

未盡事宜,後續補上……

作者:銘毅天下

轉載請标明出處,原文位址:

http://blog.csdn.net/laoyang360/article/details/11908731

繼續閱讀