天天看點

C語言結構體中的柔性數組成員

作者:海洋餅幹叔叔

考慮如下問題,我們試圖定義一個名為Student的結構,這個結構應包括學生的姓名,學生已修課程的數量以及已修課程各科的分數。實踐中,每個學生已修課程的數目是不一樣的,這使得我們在定義用于存儲分數的結構成員時面臨兩難的局面:

• 如果将該數組定義得比較小,會存在某學生所修課程數量較多,存不下的情況。

• 如果将該數組定義得很大,比如10000,則對于大多數學生而言,記憶體空間浪費嚴重。而且,無論将該數組定義得再大,理論上都存在實際資料超量,存不下的可能。

知識産權協定

允許以教育/教育訓練為目的向學生或閱聽人進行免費引用,展示或者講述,無須取得作者同意。

不允許以電子/紙質出版為目的進行摘抄或改編。

解決方案之一是把分數數組成員定義為一個指向float的指針,如下述C語言代碼所示:

//Project - StudentScores
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    char sName[20]; //學生姓名
    int  n;         //已修課程數量
    float* scores;  //指針作為結構成員,分數數組
} Student;

int main() {
    Student s = {"Dorothy Henry", 4, NULL};
    printf("sizeof(s) = %lld, sizeof(s.sName) = %lld, "
           "sizeof(s.n) = %lld, sizeof(s.scores) = %lld\n",
           sizeof(s),sizeof(s.sName),sizeof(s.n),sizeof(s.scores));

    s.scores = calloc(s.n,sizeof(float));

    s.scores[0] = 80;  s.scores[1] = 90; s.scores[2] = 90; s.scores[3] = 80;
    float fSum = 0;
    for (int i=0;i<s.n;i++)
        fSum += s.scores[i];
    printf("Average score of %s: %f",s.sName,fSum/s.n);

    free(s.scores);
    return 0;
}           

上述程式的執行結果為:

sizeof(s) = 32, sizeof(s.sName) = 20, sizeof(s.n) = 4, sizeof(s.scores) = 8
Average score of Dorothy Henry: 85.000000           

第5 ~ 9行:定義了Student結構,其包含3個資料成員,分别是20個位元組的學生姓名sName,4個位元組的已修課程數量n,8個位元組的分數”數組“指針scores。其3個成員的位元組數相加,等于一個Student對象的尺寸32個位元組。

對于Student結構而言,scores跟其它成員一樣,隻是資料成員,隻不過類型特殊,是float*。

Student s = {"Dorothy Henry", 4, NULL};           

第12行:s對象的初始化中,将s.n初始化為4,s.scores初始化為空指針。

s.scores = calloc(s.n,sizeof(float));           

第17行:s.scores隻是一個指針,要往s.scores”數組“裡存分數前,需要手動申請需要的記憶體空間。這行代碼為其申請了s.n,即4個float的空間。必要時,如果希望往s.scores“數組”中存入超過4個的分數,可以通過realloc()函數重新調整其動态記憶體的大小。

s.scores[0] = 80;  s.scores[1] = 90; s.scores[2] = 90; s.scores[3] = 80;
float fSum = 0;
for (int i=0;i<s.n;i++)
    fSum += s.scores[i];
printf("Average score of %s: %f",s.sName,fSum/s.n);           

第19 ~ 23行:在配置設定了記憶體空間之後,s.scores指針便可以當成”數組“來使用。使用過程中,程式員會注意避免下标越界。這幾行代碼先把4個分數填入s.scores”數組”,然後再計算平均分并列印出來。

free(s.scores);           

第25行:釋放calloc()申請的動态記憶體。

  這種使用指針成員來管理不定尺寸空間的方法需要程式員手動申請及釋放記憶體,程式會變得比較零散。另外一個解決方案是使用結構的柔性數組成員(flexible array member)。請閱讀下述C語言程式:

//Project - FlexMember
#include <stdio.h>
#include <stdlib.h>

typedef struct {
    char sName[20];  //學生姓名
    int  n;          //已修課程數量
    float scores[];  //柔性數組成員
} Student;

int main() {
    unsigned int nBytes = sizeof(Student) + 4*sizeof(float);
    Student* s = malloc(nBytes);
    printf("sizeof(*s) = %lld, sizeof(s->sName) = %lld, "
           "sizeof(s->n) = %lld, nBytes = %lld\n",
           sizeof(*s),sizeof(s->sName),sizeof(s->n), nBytes);

    printf("s = %p, s->scores = %p\n", s, s->scores);

    s->n = 4;
    s->scores[0] = 80;  s->scores[1] = 90; s->scores[2] = 90; s->scores[3] = 80;
    float fSum = 0;
    for (int i=0;i<s->n;i++)
        fSum += s->scores[i];
    printf("Average score: %f",fSum/s->n);

    free(s);
    return 0;
}           

上述程式的執行結果為:

sizeof(*s) = 24, sizeof(s->sName) = 20, sizeof(s->n) = 4, nBytes = 40
s = 0000000000711480, s->scores = 0000000000711498
Average score: 85.000000           

說明:在讀者的計算機上,執行結果中的位址很可能與本書不同。

typedef struct {
    char sName[20];  //學生姓名
    int  n;          //已修課程數量
    float scores[];  //柔性數組成員
} Student;           

第5 ~ 9行:scores數組成員即為Student結構的柔性數組成員。柔性數組成員的定義要滿足如下要求。

• 該成員必須是結構的最後一個成員;

• 該成員在文法上定義了一個不指定元素數量的“空”數組。

事實上,對于一個Student類型的對象而言, 隻有sName及n成員會被配置設定空間,scores成員是不占空間的。

unsigned int nBytes = sizeof(Student) + 4*sizeof(float);
Student* s = malloc(nBytes);           

第12 ~ 13行:現假設我們要存4門課程的分數,通過一個Student的對象大小加上4個float的對象大小得到需要的記憶體位元組數nBytes。然後,通過malloc()函數配置設定nBytes的堆空間,并把位址傳給指針s。

printf("sizeof(*s) = %lld, sizeof(s->sName) = %lld, "
       "sizeof(s->n) = %lld, nBytes = %lld\n",
       sizeof(*s),sizeof(s->sName),sizeof(s->n), nBytes);           

第14 ~ 16行:通過執行結果可以看到,sName成員占20個位元組,n成員占4個位元組。雖然我們事實上給s所指向的Student對象申請了nBytes = 40個位元組的空間,但在編譯器看來,*s,即s所指向的Student對象的大小隻有24個位元組。

printf("s = %p, s->scores = %p\n", s, s->scores);           

第18行:把s,s->scores按位址格式輸出。根據執行結果,我們可以畫出該Student對象的記憶體結構圖。

C語言結構體中的柔性數組成員

如果把s->scores的位址值減去s的位址值,差為24 = sizeof(Student)。這說明,結構的柔性數組成員事實上是一個指針,它指向緊随該對象的記憶體位址,其值恒等于對象位址+sizeof(類型)。換句話說:如果我們實際配置設定給結構對象的空間大于sizeof(Student),那麼多出來的記憶體可以通過其柔性數組成員來通路。

Student s1;
printf("\n%p - %p",&s1,s1.scores);           

如果我們直接定義類型為Student的變量s1,編譯器會為s1配置設定sizeof(Student) = 24個位元組的空間。但即便如此,s1.scores仍然會等于s1的位址+24。如果我們強行通過s1.scores進行資料通路,事實上通路的是不屬于s1對象的空間,這是程式員需要小心避免的。

s->n = 4;
s->scores[0] = 80;  s->scores[1] = 90; s->scores[2] = 90; s->scores[3] = 80;
float fSum = 0;
for (int i=0;i<s->n;i++)
    fSum += s->scores[i];
printf("Average score: %f",fSum/s->n);           

第20 ~ 25行:給s的柔性數組成員指派,然後計算平均分并列印。由于我們确信s->scores所對應的記憶體空間屬于s指向的結構對象,上述操作是安全的。

free(s);           

第27行:一定不要忘了釋放動态配置設定的記憶體空間。

請讀者注意,将帶有柔性數組成員的結構對象指派給另外一個同類型對象是危險的:

Student s1;
s1 = *s;      //*s是存有4個分數的占40個位元組空間的結構對象           

對于編譯器而言,s1和*s都隻有sizeof(Student) = 24位元組的空間。從*s到s1的指派,隻會拷貝前24個位元組。同樣的危險也會發生在函數傳值時,函數的傳值,可以認為是從實際參數到形式參數的指派。

本案例節選自作者編寫的教材及配套實驗指導書。

《C++程式設計基礎及應用》(高等教育出版社,出版過程中)

《Python程式設計基礎及應用》,高等教育出版社

《Python程式設計基礎及應用實驗教程》,高等教育出版社

C語言結構體中的柔性數組成員

高校教師同行如果期望索取樣書,教學支援資料,加群,請私信作者,聯系時請提供學校及個人姓名為盼,各高校在讀學生勿擾為謝。

青少年讀者們如果期望系統性地學習Python及C/C++程式設計語言,歡迎嘗試下述今日頭條(西瓜)免費視訊課程。

C/C++從入門到放棄(重慶大學現場版)

Python程式設計基礎及應用(重慶大學現場版)

C語言結構體中的柔性數組成員

繼續閱讀