1. 程式人生 > >C語言 動態儲存管理

C語言 動態儲存管理

為什麼需要動態儲存管理
程式中需要用變數(各種簡單型別變數、陣列變數等)儲存被處理的資料和各種狀態資訊,變數在使用之前必須安排好儲存:放在哪裡、佔據多少儲存單元,等等,這個工作被稱作儲存分配。用機器語言寫程式時,所有儲存分配問題都需要人處理,這個工作瑣碎繁雜、很容易出錯。在用高階語言寫程式時,人通常不需要考慮儲存分配的細節,主要工作由編譯程式在加工程式時自動完成。這也是用高階語言程式設計序效率較高的一個重要原因。

C程式裡的變數分為幾種。外部變數、區域性靜態變數的儲存問題在編譯時確定,其儲存空間的實際分配在程式開始執行前完成。程式執行中訪問這些變數,就是直接訪問與之對應的固定位置。對於區域性自動變數,在執行進入變數定義所在的複合語句時為它們分配儲存。應該看到,這種變數的大小也是靜態確定的。例如,區域性自動陣列的元素個數必須用靜態可求值的表示式描述。這樣,一個函式在呼叫時所需的儲存量(用於安放該函式裡定義的所有自動變數)在編譯時就完全確定了。函式定義裡描述了所需要的自動變數和引數,定義了陣列的規模,這些就決定了該函式在執行時實際需要的儲存空間大小。

以靜態方式安排儲存的好處主要是實現比較方便,效率高,程式執行中需要做的事情比較簡單。但這種做法也形成了對寫程式方式的一種限制,使某些問題在這個框架裡不好解決。舉個簡單的例子:假設現在要寫一個處理一組學生成績資料的程式,被處理資料需要儲存,因此應該定義一個數組。由於每次使用程式時要處理的成績的項數可能不同,我們可能希望在程式啟動後輸入一個表示成績項數的整數(或通過命令列引數提供一個整數,問題完全一樣)。對於這個程式,應該怎樣建立其內部的資料表示呢?

問題在於寫程式時怎樣描述陣列元素的個數。一種理想方式是採用下面的程式框架:

int n;

...

scanf("%d", &n);

double scores[n];

... /* 讀入成績資料,然後進行處理 */

但是這一做法行不通。這裡存在兩個問題:首先是變數定義不能出現在語句之後。這個問題好解決,可以引進一個複合語句,把scores的定義放在複合語句裡。第二個問題更本質,在上面程式段裡,描述陣列scores大小的表示式是一個變數,它無法靜態求出值。也就是說,這個陣列大小不能靜態確定,C語言不允許以這種方式定義陣列。這個問題用至今討論過的機制都無法很好解決。目前可能的解決方案有(一些可能性):

1.         分析實際問題,定義適當大小的陣列,無論每次實際需要處理多少資料都用這個陣列。前面的許多程式採用了這種做法。如果前期分析正確,這樣做一般是可行的。但如果某一次實際需要處理的資料很多,程式裡定義陣列不夠大,這個程式就不能用了(當然,除非使用程式的人有源程式,而且知道如果修改程式,如何編譯等等。在現實生活中,這種情況是例外)。

2.         定義一個很大的陣列,例如在所用的系統裡能定義的最大陣列。這樣做的缺點是可能浪費大量空間(儲存器是計算機系統裡最重要的一種資源)。如果在一個複雜系統裡,有這種情況的陣列不止一個,那就沒辦法了。如果都定義得很大,系統可能根本無法容納它們。而在實際計算中,並不是每個陣列都真需要那麼大的空間。

上面只是一個說明情況的例子。一般情況是:許多執行中的儲存需求在寫程式時無法確定。通過定義變數的方式不能很好地解決這類問題。為此就需要一種機制,使我們能利用它寫出一類程式,其中可以根據執行時的實際儲存需求分配適當大小的儲存區,以便存放到在執行中才能確定大小的資料組。C語言為此提供了動態儲存管理系統。說是“動態”,因為其分配工作完全是在動態執行中確定的,與程式變數的性質完全不同。程式裡可以根據需要,向動態儲存管理系統申請任意大小的儲存塊。

現在有了動態儲存分配,可以要求系統分配一塊儲存,但是怎麼能在程式裡掌握和使用這種儲存塊呢?對於普通的變數,程式裡通過變數名去使用它們。動態分配的儲存塊無法命名(命名是程式設計序時的手段,不是程式執行中可以使用的機制),因此需要另闢蹊徑。一般的語言裡都通過指標實現這種訪問,用指標指向動態分配得到的儲存塊(把儲存塊的地址存入指標),而後通過對指標的間接操作,就可以去使用儲存塊了。引用動態分配的儲存塊是指標的最主要用途之一。


與動態分配對應的是動態釋放。如果以前動態分配得到的儲存塊不再需要了,就應該考慮把它們交回去。動態分配和釋放的工作都由動態儲存管理系統完成,這是支援程式執行的基礎系統(稱為程式執行系統)的一部分。這個系統管理一片儲存區,如果需要儲存塊,就可以呼叫動態分配操作申請一塊儲存;如果以前申請的某塊儲存不需要了,可以呼叫釋放操作將它交還管理系統。動態儲存管理系統管理的這片儲存區通常稱為堆(heap)。 

二》 C語言的動態儲存管理機制
C語言的動態儲存管理由一組標準庫函式實現,其原型在標準檔案<stdlib.h>裡描述,需要用這些功能時應包含這個檔案。與動態儲存分配有關的函式共有四個:

1)儲存分配函式malloc()。函式原型是:

void *malloc(size_t n);

這裡的size_t是標準庫裡定義的一個型別,它是一個無符號整型。這個整型能夠滿足所有對儲存塊大小描述的需要,具體相當於哪個整型由具體的C系統確定。malloc的返回值為(void *)型別(這是通用指標的一個重要用途),它分配一片能存放大小為n的資料的儲存塊,返回對應的指標值;如果不能滿足申請(找不到能滿足要求的儲存塊)就返回NULL。在使用時,應該把malloc的返回值轉換到特定指標型別,賦給一個指標。

例:利用動態儲存管理機制,前面提出的問題可以採用如下方式解決:

int n;

double *scores;

...

scanf("%d", &n);

scores = (double *)malloc(n * sizeof(double));

if (scores == NULL) {

    ....  /* 出問題時的處理,根據實際情況考慮 */

}

..scores[i] ... *(scores+j) ... /* 讀入資料進行處理 */

呼叫malloc時,應該利用sizeof計算儲存塊的大小,不要直接寫整數,以避免不必要的錯誤。此外,每次動態分配都必須檢查成功與否,並考慮兩種情況的處理。

注意,雖然這裡的儲存塊是通過動態分配得到的,但是它的大小也是確定的,同樣不允許越界使用。例如上面程式段分配的塊裡能存n個雙精度資料,隨後的使用就必須在這個範圍內進行。越界使用動態分配的儲存塊,尤其是越界賦值,可能引起非常嚴重的後果,通常會破壞程式的執行系統,可能造成本程式或者整個計算機系統垮臺。

2)帶計數和清0的動態儲存分配函式calloc。函式原型是:

void *calloc(size_t n, size_t size);

引數size意指資料元素的大小,n指要存放的元素個數。calloc將分配一塊儲存,其大小足以存放n個大小各為size的元素,分配之後還把儲存塊裡全部清0(初始化為0值)。如果不能滿足要求就返回NULL。

例:前面程式片段裡的儲存分配也可以用下面語句實現:

scores = (double *)calloc(n, sizeof(double));

注意,malloc對於所分配區域不做任何事情,calloc對整個區域進行初始化,這是兩個函式的主要不同點。另外就是兩個函式的引數不同,calloc主要是為了分配“陣列”。我們可以根據情況選用。

3)動態儲存釋放函式free。原型是:

void free(void *p);

函式free釋放指標p所指的儲存塊。指標p的值(儲存塊地址)必須是以前通過動態儲存分配函式分配得到的。如果當時p的值是空指標,free就什麼也不做。

注意,呼叫free(p) 不會改變p的值(在函式裡不可能改變值引數p),但被p指向的儲存塊的內容卻可能變了(可能由於儲存管理的需要)。釋放後不允許再通過p去訪問已釋放的塊,否則也可能引起災難性後果。

為了保證動態儲存區的有效使用,在知道某個動態分配的儲存塊不再用時,就應及時將它釋放,這應該成為習慣。釋放動態儲存塊只能通過呼叫free完成。下面是一個示例:

int fun (...) {

    int *p;

    ... ,..

    p = (int *)malloc(...);

    ...

    free(p);

    return ...;

}

這裡的free(p)在fun退出前釋放了在函式裡分配的儲存塊。如果沒有最後的這個free(p),函式裡分配的這個儲存塊就可能丟掉。因為fun的退出也是p的存在期結束,此後p儲存的資訊(動態儲存塊地址)就找不到,這個塊就可能丟掉了[1]。丟失動態分配塊的情況稱為動態儲存的“流失”。對於需要長時間執行的程式,儲存流失就可能成為嚴重問題,可能造成程式執行一段後被迫停止。因此,實際系統不能容忍這種情況的發生。

4)分配調整函式realloc。函式原型是:

void *realloc(void *p, size_t n);

這個函式用於更改以前的儲存分配。在呼叫realloc時,指標變數p的值必須是以前通過動態儲存分配得到的指標,引數n表示現在需要的儲存塊大小。realloc在無法滿足新要求時返回NULL,同時也保持p所指的儲存塊的內容不變。如果能夠滿足要求,realloc就返回一片能存放大小為n的資料的儲存塊,並保證該塊的內容與原塊一致:如果新塊較小,其中將存放著原塊裡大小為n的範圍內的那些資料;如果新塊更大,原有資料存在新塊的前面一部分裡,新增的部分不自動初始化。如果分配成功,原儲存塊的內容就可能改變了,因此不允許再通過p去使用它。

假如要把一個現有的雙精度塊改為能存放m個雙精度數,可以用下面程式段處理:

q = (double *)realloc(p, m * sizeof(double));

if (q == NULL) {

    ... ... /* 分配不成功,p仍指向原塊,處理這種情況 */



else {

    p = q;

    ... ... /* 分配成功,通過p可以去用新的儲存塊 */

}

上面的q是另一個雙精度指標。這裡沒有把realloc的返回值賦給直接p,是為了避免分配失敗時丟掉原儲存塊。如果直接賦值,指標p原來的值就會丟掉。如果當時的分配沒有成功,p將被賦空指標值,原來那個塊可能就再也找不到了(除非在這次調整前已經讓另一個指標指向了它)。

請注意:通過動態分配得到的塊是一個整體,只能作為一個整體去管理(無論是釋放還是改變大小)。在呼叫free(p) 或者realloc(p, ...) 時,p當時的值必須是以前通過呼叫儲存分配函式得到的,絕不能對指在動態分配塊裡其他位置的指標呼叫這兩個函式(更不能對並不指向動態分配塊的指標使用它們),那樣做的後果不堪設想。 

三》   兩個程式例項
例:修改篩法程式,令它由命令列引數得到所需的整數範圍。如果沒有命令列引數,就要求使用者輸入一個確定範圍的整數值。

先考慮main的設計。為了使程式更加清晰,我們可以考慮把篩法計算寫成一個函式。這裡還有一個小問題:如果使用者通過命令列引數給出工作範圍,程式就需要從命令列引數字串計算出對應的整數。為此我們定義如下函式:

int s2int(char s[]);

再利用原來的getnumber函式,這個程式的main可以定義為:

 

enum { LARGEST = 32767 };

 

int main(int argc, char **argv)

{

    int i, j, n, *ns;

 

    if (argc == 2) n = s2int(argv[1]);

    else getnumber("Largest number to test: ", 2, LARGEST, 5, &n);

 

    if (n < 2 || n > LARGEST) {

        printf("Largest number must in range [2, %d]", LARGEST);

        return 1;

    }

 

    if ((ns = (int*)malloc(sizeof(int)*(n+1))) == NULL) {

        printf("No enough memory!/n");

        return 2;

    }

 

    sieve(n, ns);

 

    for(j = 1, i = 2; i <= n; ++i)

        if (ns[i] == 1) {

            printf("%7d%c", i, (j%8 == 7 ? '/n' : ' '));

            ++j;

        }

    putchar('/n');

 

    free(ns);

    return 0;

}

 

主函式被清晰地分為三部分:準備工作,主要處理部分,輸出與結束。如果程式得到的範圍不合要求,它就列印錯誤資訊並立即結束。正常情況下完成篩法計算併產生輸出。

使用動態儲存管理的要點

1)必須檢查分配的成功與否。人們常用的寫法是:

if ((p = (... *)malloc(...)) == NULL) {

    .. ... /* 對分配未成功情況的處理 */

}

2)系統對動態分配塊的使用不做任何檢查。程式設計序的人需要保證使用的正確性,絕不可以超出實際儲存塊的範圍進行訪問。這種越界訪問可能造成大災難。

3)一個動態分配塊的存在期並不依賴於分配這個塊的地方。在一個函式裡分配的儲存塊的存在期與該函式的執行期無關。函式結束時不會自動回收這種儲存塊,要回收這種塊,唯一的方法就是通過free釋放(完全由寫程式的人控制)。

4)如果在函式裡分配了一個儲存塊,並用區域性變數指向它,在這個函式退出前就必須考慮如何處理這個塊。如果這個塊已經沒用了,那麼就應該把它釋放掉;如果這個塊還有用(其中儲存著有用的資料),那麼就應該把它的地址賦給存在期更長的變數(例如全域性變數),或者把這個地址作為函式返回值,讓呼叫函式的地方去管理它。

5)其他情況也可能造成儲存塊丟失。例如給一個指向動態儲存塊的指標賦其他值,如果此前沒有其他指標指向這個塊,此後就再也無法找到它了。如果一個儲存塊丟失了,在這個程式隨後的執行中,將永遠不能再用這個儲存塊所佔的儲存。

6)計算機系統裡的儲存管理分很多層次。一個程式執行時,作業系統分給它一部分儲存,供它儲存程式碼和資料。其資料區裡包括一塊動態儲存區,由這個程式的動態儲存管理系統管理。該程式執行中的所有動態儲存申請都在這塊空間裡分配,釋放就是把不用的儲存塊交還程式的動態儲存管理系統。一旦這個程式結束,作業系統就會收回它佔用的所有儲存空間。所以,這裡說“儲存流失”是我們程式內部的問題,並不是整個系統的問題。當然,作業系統也是程式,它也有儲存管理問題,那是另一個層次的問題。
 

getnumber可以直接利用已有的定義(這裡又可以看到函式的價值),剩下的工作就是定義程式裡需要的兩個函式。從數字字串轉換產生整數的函式很簡單,它順序算出各數字字元的整數值並將其加入累加值,每處理一個數位都需要將原值乘10:

 



int s2int(char s[]) {

    int n;

    for (n = 0; isdigit(*s); ++s)

        n = 10 * n + (*s - '0');

    return n;

}

 

在這個函式裡沒有檢查計算的溢位問題。如果需要,很容易加進這種檢查。這裡也可以直接用標準庫函式atoi,該函式完成的就是從數字字串到整數的轉換。有關atoi的情況請查閱本書第11章的介紹。

把篩法計算包裝為函式的工作很容易完成,下面是函式的定義:

 

void sieve(int lim, int an[]) {

    int i, j, upb = sqrt(lim+1);

 

    an[0] = an[1] = 0; // 建立起初始向量

    for (i = 2; i <= lim; ++i) an[i] = 1;

 

    for (i = 2; i <= upb; ++i)

        if (an[i] == 1) // i是素數

            for (j = i*2; j <= lim; j += i)

                an[j] = 0; // 這些數都是i的倍數,因此不是素數

}

 

把這些函式定義(包括getnumber的定義)放到一起,適當安排函式位置,必要時加入原型。在原始檔前部加入適當 #include命令列,整個程式就完成了。

在這個程式裡需要儲存一批資料,但是資料的數目在寫程式時無法確定,因此只能採用動態儲存分配的方式。程式裡申請了一個大儲存塊,其中可以存放所需的int值。用指標指向這樣得到的儲存塊,用起來就像是在使用一個int陣列。

例:改造第6章的學生成績統計和直方圖生成程式,使之能處理任意多的學生成績。

本例的重點是討論一種常見問題的處理技術:通過動態分配的陣列,儲存事先完全無法確定數量的輸入資料。前一個例子是先確定了資料量,而後做一次動態分配。假如直到開始讀入資料的時候還不知道有多少資料項,那又該怎麼辦?下面我們解決這個問題。

在前面的成績直方圖程式用了一個數組,因此也限制了能處理的成績數。現在我們想修改readscores,由它全權處理輸入工作,在輸入過程中根據需要申請適當大小的儲存塊,把輸入資料存入其中。這樣,readscores結束時就需要返回兩項資訊:儲存資料的動態儲存塊地址,以及存於其中的資料項數。一個函式只能有一個返回值,另一“返回值”需要通過引數送出來。下面是修改後readscores的原型和main的定義:

 

double* readscores(int* np); /*讀入資料,返回動態塊地址,通過np送回項數*/
int main()

{

    int n;

    double *scores;

    if ((scores = readscores(&n)) == NULL)

        return 1;

    statistics(n, scores);

    histogram(n, scores, HISTOHIGH);

    return 0;

}

 

由於原程式的組織比較合理,在進行當前這個功能擴充時,我們只需要修改其中的輸入部分,並對main做很區域性的修改,其他部分根本無須任何變動。

現在考慮如何寫readscores。一種可行考慮是先做某種初始分配,在發現數據項數太多,當前的分配無法滿足需要時進行儲存調整。例如把動態資料塊的初始大小定為20(或其他合理的大小),隨後如何調整是一個值得研究的問題。下面採用的策略是每次調整時把容量加倍,有關不同調整方式的分析在後面的方框中。這樣定義出的函式如下:

 

enum { INITNUM = 20 };

 

double* readscores(int* np) {

    unsigned curnum, n;

    double *p, *q, x;

 

    if ((p = (double*)malloc(INITNUM*sizeof(double))) == NULL) {

        printf("No memory. Stop/n");

        *np = 0;

        return NULL;

    }

 

    for(curnum = INITNUM, n = 0; scanf("%lf", &x) == 1; ++n) {

        if (n == curnum) {

            q = (double*)realloc(p, 2*curnum*sizeof(double));

            if (q == NULL) {

                printf("No enough memory. Process %d scores./n", n);

                break;

            }

            p = q; curnum *= 2;

        }

        p[n] = x;

    }

    *np = n;

 

    return p;

}

 

四》動態調整策略

要實現一個能在使用中根據需要增長的“動態”陣列(一個動態分配的,能儲存許多元素的儲存塊可以看成一個“陣列”),需要考慮所採用的增長策略。

一種簡單的想法是設定一個增量,例如10,一旦儲存區滿時就把儲存區擴大10個單元。仔細考慮和計算會發現這樣做有很大的缺陷。實際中對儲存量的需要常常是逐步增加的。一般說,在遇到儲存區滿時,實際上需要另外分配一塊更大的儲存區,並需要把原塊裡已有的元素複製到新塊裡。realloc完成這種操作的代價(雖然沒有顯露出來)通常與已有的元素個數成正比。

假設輸入過程中執行了一系列擴大儲存的動作,如果每加入10個元素做一次複製,把陣列從20增加到包含1000個元素,總的複製數將是20+…+980+990 = 49990。這樣,加入每個元素平均大約做n/20次複製,n是最後的元素個數。當陣列增大到1000000個元素時,每加入一個元素平均要做50000次複製,這個代價比較高。

一種合理的增長方式是每次讓儲存塊加倍。假設儲存塊從1開始增長,增長到1024時所複製元素為1+2+4+…+512 = 1023。進一步增長到1024×1024≈1000000時,元素複製的總次數大約也為1000000次,加入一個元素,平均需要複製一次。可見,增長策略的作用確實很大。當然,如果陣列很小,兩種策略的差異就不那麼明顯了。

採用後一增長策略也有代價(世界上沒有免費的午餐),那就是儲存空間。每次加倍後陣列中就出現了一大塊空區。例如,當陣列有513個元素時,空位有511個之多。隨著陣列的加倍,最大的空位數也差不多增加一倍。也就是說,按照這種方案,最壞情況下浪費了一半空間。而按照第一種增長策略,空閒元素最多隻有9個。

總結一下,這裡也是在時間和空間之間做交易。在計算機科學技術領域裡,這種時間與空間交換的事情到處都可以看到。問題是要考慮需求,綜合權衡。

 
 

函式裡用變數curnum記錄當前分配塊的大小,用n記錄當前存入的資料項數。一旦遇到資料塊滿而且還有新項時,我們就擴大儲存,把容量加倍。

這個函式定義主要顯示了在處理類似問題時常用的一種基本技術,其中並沒有刻意追求函式的進一步完善。例如,如果讀入資料的過程中遇到一個錯誤資料,這個函式就會立即結束,返回的是至此讀入的資料。有關資料檢查和處理等都是前面討論過的問題,進一步修改這個輸入函式,使之能合理處理輸入資料中的錯誤,給出有用的出錯資訊,或者進一步增加其他有用的功能等等的工作並不困難,都留給讀者作為進一步的練習

五》 函式、指標和動態儲存
如果需要在函式裡處理一組資料,並把處理結果反應到呼叫函式的地方,最合適的辦法就是在函式呼叫時提供陣列的起始位置和元素數目(或者結束位置)。這種傳遞成組資料的方式在本章和前一章裡反覆使用。這時函式完全不必知道用的是程式裡定義的陣列變數,還是動態分配的儲存塊。例如,我們完全可以用如下方式呼叫篩法函式:

 

int ns[1000];

 

int main()

{

    int i, j;

    sieve(1000, ns);

    for(j = 1, i = 2; i <= n; ++i)

        if (ns[i] == 1) {

            printf("%7d%c", i, (j%8 ? ' ' : '/n'));

            ++j;

        }

 

    putchar('/n');

    return 0;

}

 

在前一節的篩法程式例項裡,我們在主函式裡通過動態分配取得儲存,而後呼叫函式sieve,最後還是由main函式釋放這塊儲存。這樣,分配和釋放的責任位於同一層次,由同一個函式(函式main)完成。這樣做最清晰,易於把握,是最好的處理方案。

但也存在一些情況,其中不能採用上述做法,例如上面的直方圖程式。程式裡定義了一個讀入函式,它需要根據輸入情況確定如何申請動態儲存。這時動態儲存的申請在被呼叫函式readscores的內部,該函式完成向儲存塊裡填充資料的工作,最後把做好的儲存塊(就像是一個數組)的地址通過返回值送出來。呼叫函式(main)用型別合適的指標接收這個地址值,而後通過這個指標使用這一儲存塊裡的資料。

首先,這一做法完全正確,因為動態分配的儲存塊將一直存在到明確呼叫free釋放它為止。雖然上述儲存塊是在函式readscores裡面分配的,但它的生命週期(生存期)並不隨該函式的退出而結束。語句:

    scores = readscores(&n);

使scores得到函式readscores的執行中申請來並填充好資料的儲存塊,在main裡繼續用這個塊是完全沒問題的。當然,採用這種方式,readscores就不應該在退出前釋放該塊。注意:上面的呼叫除了傳遞有關的資料外,實際上還有儲存管理責任的轉移問題。在readscores把一塊儲存的指標通過返回值送出來時,也把釋放這塊儲存的責任轉交給main。這樣,我們也可以看出前面的程式裡忽略了一件事情,在那裡沒有釋放這一儲存塊。應做的修改就是在main的最後加一個釋放語句(當然,由於main的結束也就是整個程式的結束,未釋放的這塊儲存也不會再有用了。如前所述,在這個程式結束後,作業系統將會收回這個程式佔用的全部儲存)。

現在考慮readscores的設計裡的一個問題。在前面的程式裡,readscores通過int指標引數(實參應該是一個int變數的地址)傳遞實際讀入資料的個數。另一種可能做法是讓函式返回這一整數值,例如將其原型改成:

int readscores(???);

這樣,我們在main裡就可以寫如下形式的呼叫:

if (readscores(... ...) <= 0) { ... } /* 產生錯誤資訊並結束程式 */

(這一寫法使人想起標準庫的輸入函式scanf)。如果這樣設計函式,呼叫readscores的地方就需要通過實參取得函式裡動態分配的儲存塊地址。也就是說,要從引數獲得一個指標值。問題是,這個函式的引數應該如何定義呢?

答案與其他情況完全一樣。如果我們想通過實參取得函式裡送出來的一個int值,就要把一個int變數的地址送進函式,要求函式間接地給這個變數賦值。同理,現在需要得到一個指標值,就應該通過實參把這種指標變數的地址送進去,讓函式通過該地址給呼叫時指定的指標變數賦值。這樣,修改後的函式readscores的原型應該是:

int readscores(double **dpp);

因為double指標的型別是(double*),其地址的型別就是指向(double*)的指標,也就是(double**)。呼叫readscores時應該把這種指標的地址傳給它:

if (readscores(&scores) <= 0) {  /* 產生錯誤並結束程式 */ }

由於scores的型別是(double*),表示式 &scores的型別就是(double**)。函式readscores也需要做相應的修改:

 

int readscores(double **dpp) {

    size_t curnum, n;

    double *p, *q, x;

 

    if ((p = (double*)malloc(INITNUM*sizeof(double))) == NULL) {

        printf("No memory. Stop/n");

        *dpp = NULL;

        return 0;

    }

 

    for(curnum = INITNUM, n = 0; scanf("%lf", &x) == 1; ++n) {

        if (n == curnum) {

            q = (double*)realloc(p, 2*curnum*sizeof(double));

            if (q == NULL) {

                printf("No enough memory. Process %d scores./n", n);

                break;

            }

            p = q; curnum *= 2;

        }

        p[n] = x;

    }

    *dpp = p;

 

    return n;

}

 

這裡展示的也是C程式裡常用的一種技術。在這一處理方案中,我們同樣是把函式裡分配的儲存塊送到函式之外,同時也把管理這一儲存塊的責任轉交給呼叫函式的程式段。不同的是,這次是通過引數傳遞儲存塊的地址。

       在這一節裡,我們介紹了指標、函式與動態分配之間的一些關係,並討論了幾種不同的處理技術。只要有可能,在程式裡最好使用第一種設計,因為它最清晰,也最不容易出現忘記釋放的情況。如果不得已而採用了其他方式,那麼就一定要記得儲存管理責任的交接問題,並在適當的地方釋放動態分配的儲存區。



--------------------------------------------------------------------------------

[1] 這種說法也有例外。我們可以在一個函式裡申請儲存塊,而後在函式裡把儲存塊的地址全域性的指標變數,或者返回指向這個塊的指標值,把這個塊交給呼叫函式的地方用。這些做法也意味著把儲存塊的“擁有權”交給程式的其他部分,此時就不應該釋放它了。

相關推薦

C語言 動態儲存管理

為什麼需要動態儲存管理 程式中需要用變數(各種簡單型別變數、陣列變數等)儲存被處理的資料和各種狀態資訊,變數在使用之前必須安排好儲存:放在哪裡、佔據多少儲存單元,等等,這個工作被稱作儲存分配。用機器語言寫程式時,所有儲存分配問題都需要人處理,這個工作瑣碎繁雜、很容易出錯。在用

C語言動態記憶體管理malloc、calloc、realloc、free的用法和注意事項

此文是參考http://www.cplusplus.com/reference/cstdlib/裡的動態記憶體管理部分所寫,如發現有問題和不足之處,請參看原文,最好能幫忙指出,謝謝。 1.void* malloc (size_t size); malloc:分配一塊size

C語言動態記憶體管理:malloc、realloc、calloc以及free函式

我們已經掌握的記憶體開闢方式有: int val = 20;//在棧空間上開闢四個位元組 char arr[10] = {0};//在棧空間上開闢10個位元組的連續空間 但是這種開闢空間的方式有兩個特點: 1. 空間開闢的大小是固定的。

C語言動態記憶體管理動態記憶體分配

動態記憶體管理同時還具有一個優點:當程式在具有更多記憶體的系統上需要處理更多資料時,不需要重寫程式。標準庫提供以下四個函式用於動態

資料結構之動態儲存管理(C語言)

一、 概述 1. 佔用塊 佔用塊:已分配給使用者使用的地址連續的記憶體區 可利用空間塊:未曾分配的地址連續的記憶體區 2. 動態儲存分配過程的記憶體狀態 系統執行一段時間後,有些程式的記憶體被釋放,造成了上圖(b)中的狀態。假如此時又有新

C語言記憶體的動態儲存管理2-空閒連結串列

空閒連結串列三種結構形式: (1)所有請求的記憶體大小相同。這是一種最簡單的動態儲存管理方式。 對此,系統通常的做法是: a)系統啟動時,將記憶體按大小均分成若干個塊,並形成一個連結串列。 b)分配時,只需將連結串列中第一個節點分配給使用者即可,無需掃描整個連結串列。 c)回

C語言儲存類別、連結與記憶體管理

  第12章 儲存類別、連結和記憶體管理 通過記憶體管理系統指定變數的作用域和生命週期,實現對程式的控制。合理使用記憶體是程式設計的一個要點。 12.1 儲存類別 C提供了多種不同的模型和儲存類別,在記憶體中儲存資料。 被儲存的每一個值都佔用一定的實體記憶體;C語言把這樣一塊記憶體稱為物件

C語言儲存類別和動態記憶體分配

儲存類別分三大類: 靜態儲存類別 自動儲存類別 動態分配記憶體   變數、物件--->記憶體管理 記憶體考慮效率(時間更短、空間更小) 作用域 連結、---->空間 儲存器   ----->時間   其實儲存類別(時間、空間)和資料

c語言學生成績管理系統(可以將學生資訊儲存至txt檔案中)

程式截圖:  標頭檔案說明; 定義全域性變數;   定義、編寫輸入函式; 定義、編寫顯示函式; 定義、編寫修改函式; 定義、編寫查詢函式; 定義、編寫新增函式; 定義、編寫排序函式; 定義、編寫刪除函式; 定義、編

C語言、記憶體管理、堆、棧、動態分配

昨晚整理了一晚上居然沒了?沒儲存還是沒登入我也忘了,賊心累 我捋了捋,還是得從作業系統,程序和記憶體開始理解。 程序     從作業系統的角度簡單介紹一下程序。程序是佔有資源的最小單位,這個資源當然包括記憶體。在現代作業系統中,每個程序所能訪問的記憶體是互相獨立的(一些

C 語言】記憶體管理 ( 動態記憶體分配 | 棧 | 堆 | 靜態儲存區 | 記憶體佈局 | 野指標 )

一. 動態記憶體分配 1. 動態記憶體分配相關概念 動態記憶體分配 : 1.C語言操作與記憶體關係密切 : C 語言中的所有操作都與記憶體相關 ; 2.記憶體別名 : 變數 ( 指標變數 | 普通變數 ) 和 陣

C語言動態內存的申請和釋放

== 否則 med 編程 nbsp 配對 強行 越界 初始化 什麽是動態內存的申請和釋放? 當程序運行到需要一個動態分配的變量時,必須向系統申請取得堆中的一塊所需大小的存儲空間,用於存儲該變量。當不再使用該變量時,也就是它的生命結束時,要顯式釋放它所占用的存儲

c++之動態記憶體管理

1.new/delete 和operator new/operator delete和malloc/free的關係 ①new呼叫operator new分配空間②new呼叫建構函式初始化物件。③delete呼叫解構函式清理物件 ④delete呼叫operator delete釋放空間 ⑤ope

C語言動態順序表的實現

上一次我們實現了靜態順序表,靜態順序表的大小固定,不方便我們去存取資料。 而動態順序表就可以很方便的存取資料。 同樣,我們有以下介面要實現: #ifndef __SEQLIST_H__ #define __SEQLIST_H__ #include<stdio.h> #include

C 語言動態記憶體

文章目錄 說明 記憶體示意圖 alloc() malloc() calloc() realloc() free() 常見錯誤程式碼例項 說明 主要參考以下部落格:

c語言動態分配記憶體及記憶體分配部分函式

#include<stdio.h> /** 在C中動態分配記憶體的基本步驟有: 1,用malloc類的函式分配記憶體; 2,用這些記憶體支援應用程式 3,用free函式釋放記憶體 二、動態記憶體分配函式     malloc :從堆上分配記憶體 &nbs

c語言動態分配空間

問題: typedef struct node{     int num;     struct node*next; }Node,*pNode 在連結串列的create函式中,為什麼定義了連結串列頭之後,以後的每個空間都要new(c++中的用法)或

C語言動態記憶體學習筆記

一、malloc返回引數有兩種情況 1,當分配的記憶體池是空的時候返回一個NULL指標。 2,當可用記憶體無法滿足要求,malloc向作業系統請求,要求更多記憶體,如果它無法向malloc提供更多記憶體就返回一個NULL指標 二、free的引數 free的引數必須是NULL或mall

C語言(記憶體管理、檔案處理)

記憶體的理解 計算機記憶體是以位元組為單位進行儲存,每個位元組都有自己的編號即地址(指標)。 本圖為原始碼 其中01 00 00 00 中的兩個連在一起的數為一個位元組,0x00FAFB7C是01的地址,之後的三個位元組的地址值分別遞增1 上圖中,num[3]為int

嵌入式C語言之深度解讀C語言儲存域,作用域,生命週期,連結屬性

***儲存類:    就是儲存型別,描述,C語言變數的儲存地址。    記憶體的管理方式:棧  堆  資料段  bss段  .text段。    一個變數的儲存型別就是描述這個變數儲存在何種記憶體段之