柔性數組成員——不定長的數據結構


柔性數組,這個名詞對我來說算是比較新穎的,在學習跳躍表的實現時看到的。這么好聽的名字,的背后到底是如何的優雅。


柔性數組,其名稱的獨特和迷惑之處在於“柔性”這個詞。
在C/C++中定義數組,是一個定長的數據結構,最常用的定義如下

int arr[100];

上述代碼的中arr數組的長度已知,我們把上面的語句稱之為聲明語句,因為在編譯期數組的長度已經確定了,我暫且發明了一個詞來稱呼這類數組——“剛性”數組(聲明,這個詞是我臆想的,是不存在這種說法的)。

你可能會說:等等,C/C++不是有可以在運行期通過malloc調用來創建動態數組的做法嗎?
沒錯,柔性數組正是需要malloc來實現的,其柔性也是在這個地方體現的。

套路先行

我們先來看一看柔性數組到底是用來干什么的吧?

柔性數組(flexible array member)也叫伸縮性數組成員,這種結構產生與對動態結構體的去求。在日常編程中,有時需要在結構體中存放一個長度是動態的字符串(也可能是其他數據類型),一般的做法,實在結構體中定義一個指針成員,這個指針成員指向該字符串所在的動態內存空間。

在通常情況下,如果想要高效的利用內存,那么在結構體內部定義靜態的數組是非常浪費的行為。其實柔性數組的想法和動態數組的想法是一樣的。

先修知識

  • 不完整類型

在C/C++中對於不完整類型的定義是這樣的:
不完整類型是一種缺乏足夠的信息去描述一個完整對象的類型
還是以數組的定義/聲明為例子。

// 一個為知長度的數組屬於不完整類型
// 這個語句屬於聲明語句,不是定義語句
extern int a[];

// 這樣的語句是錯誤的, extern關鍵字不能去掉
// int a[]

// 不完整類型的數組需要補充完整才能使用
// 下面的語句是聲明語句(定義+初始化)
int a[] = {10, 20};
  • 結構體

看到這個標題的你可能會說,什么?結構體還用得着你來補充?
如果各位看官對結構體和內存對其比較熟悉的話,可以跳過這部分,看總結本段的總結,對后面柔性數組的說明有點幫助。

對於內存對齊的部分已經超出了文章所要討論的內容了。那我想講的是什么東西,且看下面的代碼

#include<stdio.h>

struct test{
    int i;
    char *p;
};

int main(void){
    struct test t;
    printf("t:\t%p\n", &t);
    printf("t.i:\t%p\n", &(t.i));
    printf("t.p:\t%p\n", &(t.p));
}

內存對齊

我們看到t.i的地址和t的地址是一樣的。t.p的地址就是(&t + 0x8),0×8這個偏移地址就是成員p在編譯時就被編譯器給hard code了的地址。

總結:不管結構體的實例是什么,訪問其成員就是實例的地址加上成員偏移量。這個偏移量是編譯器hard code的,跟內存對齊等因素有關。

千呼萬喚始出來

我們來回顧一下,柔性數組用來在結構體中存放一個長度動態的字符串。
其實不用柔性數組我們一樣可以做到:在結構體中定義一個方法,在方法中動態地將指針指向動態數組

#include<cstring>
#include<cstdlib>
#include<cstdio>

struct Test{
    int a;
    char *p;

    void set_str(const char *str){
        int len = std::strlen(str);
        if(len <=0)
            return;

        p = (char*)std::malloc((len+1)*sizeof(char));
        std::strcpy(p, str);
        p[len] = '\0';
    }
};

int main(){
    const char copy_str[] = "Hello World";

    Test t;
    t.set_str(copy_str);
    printf("Content:\n");
    printf("t.p:\t%s\n", t.p);
    
    printf("Address:\n");
    printf("t.p\t %p\n", t.p);
    printf("&t.p\t %p\n", &(t.p));
}

指針方式
我們看到,上面的代碼的確是可以完成我們想要的結果。我們看了一下指針p和數組的起始地址。我們可以看到動態數組的內存塊和字符串的內存是兩塊不一樣的內存。
折磨程序員的來了,我們在析構對象時,需要顯式地在析構函數里面對指針p引用的內存進行釋放,不然會出現內存泄露的情況。

那么柔性數組是怎么做到的呢?
還是回到上述的結構體

struct Test{
    int a;
    char *p;
};

我們想把字符串和結構體連在一起的話,釋放的內存時候就能夠順便把字符串的內存給釋放掉了,看一看下面的代碼

// 使用上面的結構體Test
const str copy_str[] = "Hello World";
int len = std::strlen(copy_str);
// 申請連續的空間
Test *p_test = (Test*)std::malloc(sizeof(Test)+(len+1)*sizeof(char));
// 復制數組
std::strcpy(p_test+1, copy_str);
((char*)(p_test+1))[len] = '\0';

起始這么依賴,會發現char *p就成了多余的東西了,我們完全可以使用語句(char*)(p_test+1)來獲取字符串的地址了。

聰明的程序員不想被這么丑陋的代碼給糊弄,他們想如果能夠找到一種方法既能直接引用字符串,又不占用結構體的空間就很棒了。符合這個條件的應該是一個非對象的符號地址
回憶一下上文所說的不完整類型,起始就是一個符號地址。在結構體的尾部放一個長度為0的方案似乎不錯,但是C/C++標准規定是不能定義長度為0的數組。標准不允許?編譯器廠商就自行開發唄,有些編譯器把0長度的數組作為自己的非標准擴展。

struct flexible_t{
    int a;
    double b;
    char c[0];
}; 

c就叫柔性數組成員(flexible array member).我覺得翻譯成靈活數組成語也是可以的。此時p_test->c就是數組的首地址,不再需要原來那么丑陋的代碼了。

這種代碼結構這么常用,標准馬上就支持了。在C99標准中便包含了柔性成員數組。
記得上文所說的不完整類型嗎,C99便是使用不完整類型實現柔性數組成員的。為什么使用不完整類型呢,說說我的理解。

int a[] = {10, 20}; 

看到這個聲明語句,我們發現a[]其實就是個數組記號,不完整類型,由於賦值語句,所以在編譯時便確定了數組的大小,是一個完整的數組類型。
在結構體中便利用不完整類型在運行對動態的數組進行指明。

C99標准的定義如下

struct flexible_t{
    int a;
    double b;
    char c[]; // 不只是char類型,其他類型同樣也是可以
}

由於聲明內存連續性的關系,柔性數組成員必須定義在結構體的最后一個,並且不能是唯一的成員。
我們再來看一看整個結構體(包含數組內存的分布情況)

#include<cstring>
#include<cstdlib>
#include<cstdio>

# define new_instance(n) (Felexible*) std::malloc(sizeof(Flexible) + (n+1)*sizeof(char))

struct Flexible{
    int a;
    char p[0];
};

int main(){
    const char copy_str[] = "Hello World";
    // 我們使用宏來把創建對象的代碼簡化
    Flexible *flexible_p = new_instance(std::strlen(copy_str));
    std::strcpy(flexible_p->p, copy_str);
    
    printf("Content:\n");
    printf("%s\n", flexible_p->p);
    printf("Address:\n");
    printf("t.p:\t %p\n", flexible_p->p);
    printf("&t.p:\t %p\n", &(flexible_p->p));
    
    free(flexible_p);
}

柔性數組成員方式

由運行結果就可以看出,整個結構體是連續的,並且釋放結構體的方式也非常簡單直接對結構體指針進行釋放。

warning C4200: 使用了非標准擴展: 結構/聯合中的零大小數組

由於這個是C99的標准,在ISO C和C++的規格說明書中是不允許的。在vs下使用0長度的數組可能會得到一個警告。
然而gcc, clang++預先支持了C99的玩法,所以在Linux下編譯無警告

總結

我們學習了柔性數組成員的來源及一些用法,
其實柔性數組成員在實現跳躍表時有它特別的用法,在Redis的SDS數據結構中和跳躍表的實現上,也使用柔性數組成員。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM