C程序中的內存管理

相比靜態地分配內存空間,使用動態內存分配具有明顯的優勢:

1, 分配空間的大小夠精確: 設想一個讀取用戶輸入行的程序, 如果使用靜態分配的數組作爲buffer, 那麼, 你如何確定該數組的長度呢? 太大或太小都不合適. 因爲你無法事先知道用戶輸入字符串的長度. 而使用動態內存分配就精準多了.

2, 靜態分配的空間大小無法更改, 而動態分配的內存大小是可調的.

所 以, 理解C語言中的動態內存分配對於編寫實用, 有效, 安全的程序來說必不可少. 本文假設你使用C語言編程, 且使用GNU/Linux系統. (其實由於現在的許多系統都是POSIX兼容的, 本文的內容使用於任何操作系統, 只是其中提到的某些工具僅存於GNU/Linux上.)

要理解內存管理, 首先要理解程序在內存中的佈局, 既: 內存程序映像. 可參考本blog的GNU/Linux平臺的C程序開發及程序運行環境

標準C中的內存管理函數

函數原型如下:

#include <stdlib.h>

void *malloc(size_t size);
void *calloc(size_t nobj, size_t size);
void *realloc(void *ptr, size_t newsize);
若返回的指針=NULL, 則失敗, 否則成功.

void free(void *ptr);
                                                                                  ISO C


上表列出了標準C規定的四個常用內存管理函數, 一般而言, 這4個函數已經足夠我們進行內存管理了.

malloc(), calloc(), realloc()返回的指針若不爲NULL, 那麼該指針指向被分配數據塊中的第一個元素. 並且, 保證該指針能針對各種數據類型滿足對齊要求.

void *malloc(size_t size);

調用malloc()的步驟
1, 針對你想要存放數據的數據結構, 聲明一個指向它的指針.
2, 計算你需要分配的字節數. 通常利用sizeof(數據結構) * n, n爲你要使用的數據結構的個數.
3, 調用malloc()分配內存, 並將它所返回的通用指針(void *)顯式地映射爲指向該數據結構的指針, 並檢查malloc()是否返回了NULL, 若返回值爲NULL, 則進行錯誤處理.

實現代碼
struct num {
    int x, y, z;
};
struct num *p;
int n;

if ((p = (struct num *)malloc(n * sizeof(struct num))) == NULL) {
    錯誤處理;
}
使用p指向的內存; (記得初始化!)

標準C中規定, void *p是一個通用指針, 可以將它賦予任何類型的數據. 但在這裏最好還是顯式地將它的類型映射爲需要分配的數據類型. why? 先看看下面替代上面使用malloc()函數的語句:
if ((p = malloc(n * sizeof(*p))) != NULL)
這裏將sizeof的參數換成了*P, 這樣, 即便p被修改, 指向了不同的數據結構, sizeof也能計算正確的字節數. 這裏省略了類型映射, 但加上它以後, 能夠在p指向不同的數據結構後, 編譯時給出警告信息.
總之, 我們使用這樣的語句來調用malloc():
if ((p = (ds *)malloc(n * sizeof(*p))) == NULL)
其中(ds *)將通用指針顯式地映射到要分配的數據類型.

另外, 傳統c中使用char *作爲通用指針, C++要求對malloc()的返回值進行顯式的映射.

void *realloc(void *ptr, size_t newsize);

使用realloc()可以調整之前分配的數據塊大小. 包括增加或則減小. 一般而言不用減小已分配數據塊的大小.

關於realloc()的原型, 有幾點值得注意:
1, newsize是調整數據塊大小後最終的值, 並非差量.
2, 若ptr != NULL && nesize == 0, 則 realloc(p, 0)等價於free(p).
3, ptr指向需要調整的數據塊, 若ptr == NULL, relalloc(NULL, newsize)等價於malloc(newsize).

雖然可以用realloc()實現free()和malloc()的功能, 但不推薦這樣做. 還是使用標準的接口比較合適.

調用realloc()的步驟:
1, 計算你新需要的字節數.
2, 找到指向你需要調整的數據塊的指針(它是malloc()或calloc()甚至realloc()的返回值.), 並將它和新的字節大小傳遞給realloc(). 注意不要用增量!
3, 調用realloc()分配內存, 並將它所返回的通用指針(void *)顯式地映射爲指向該數據結構的指針, 並檢查malloc()是否返回了NULL, 若返回值爲NULL, 則進行錯誤處理.

注意: GNU Coding Standards規定: 即便realloc()失敗, 之前分配的數據塊也會保持不變, 可以繼續使用.

實現代碼
這裏繼續上述malloc()中的代碼, 假設之前分配的n個num結構不夠, 還需要再分配m個:
struct num *q;
int newsize = (n+m) * sizeof(*p);

if ((q = (struct num *)realloc(p, newsize)) == NULL) {
    錯誤處理;
}
p = q;
繼續使用p;

注意: realloc()返回的地址賦給了一個新的指針q. 在調用完realloc()之後, 又將q的值賦給p , 繼續使用p. 爲何如此麻煩呢?  原因有二:

(1)看看上面的框框, GNU保證即便realloc()返回NULL, 之前調用malloc()分配給p的數據塊也能使用, 但若直接把realloc()的返回值賦給p, 可能令p = NULL, 使得之前p指向的數據段無法使用.

(2) 使用realloc()時, 腦子裏應該時刻銘記一點: 由於對之前的數據塊大小進行了調整, realloc()可能將以前的數據塊挪到內存中別的位置. 考慮增大數據塊的情況: 若之前分配的數據塊所在的內存空間所剩的空間不夠, 那麼realloc()會將以前的數據塊拷貝到內存中其他位置, 並釋放之前分配的數據塊. 這樣之前的p就指向了無效的區域. 即便調用realloc()來減小數據塊, 該數據塊也可能被移到內存中的其他位置!

某個已分配的數據塊b1, 調用realloc()調整b1大小得到b2之後, 不能假設b1和b2的第一個元素在同一位置. 指向b1的所有指針必須被更新!
引用原b1數據塊中的元素有兩種途徑:
1, 使用數組下標.
2, 使用被更新後的指針. 絕不能使用以前指向p1的指針!

void *calloc(size_t nobj, size_t size);

calloc()可視爲malloc()的一個封裝, 下面是它可能的一個實現:
void *calloc(size_t nobj, size_t size)
{
    void *p;
    size_t total;

    total = nobj * size;
    if ((p = malloc(total)) != NULL) {
       memset(p, '/0', total);

    return p;
}

調用calloc()的方法與malloc()相同. calloc()與malloc()的區別在於兩點:
1, calloc()將分配的內存數據塊中的內容初始化爲0. 這裏的0指的是bitwise, 既每個位被清0, 具體的數值由要聯繫數據結構中各元素的類型.

2, 傳遞給calloc()的參數有2個, 第一個是想要分配的數據結構的個數, 第二個是數據結構的大小.

如果傳遞給malloc()或calloc()的size = 0, 標準C並未規定返回的指針一定爲NULL, 它可能爲非NULL. 但是這種情況下不能引用該指針.

void free(void *ptr);

在完成對動態分配的數據塊的使用之後, 要
通過調用free()來釋放它. 這裏所說的"釋放"是指將該數據塊佔用的內存放回到堆中, 以後再調用malloc(), calloc()或realloc()時可以利用該數據塊佔用的內存段. 注意free()並不能夠改變進程地址空間的大小, 被釋放的內存仍位於堆空間中.

如果不及時釋放內存, 會引發內存泄露(memory leaks), 特別是運行時間比較長的程序要注意這個問題, 如果發生了內存泄露, 系統即便不因爲缺少內存資源而崩潰, 也會由於內存抖動(memory thrashing)而性能下降.

調用free()的方法:
free(p);
p = NULL;

調用free()時, 有幾點注意:
1, p必須指向由malloc(), calloc()或realloc()返回的地址. 即 傳遞給free()的參數必須是數據塊第一個元素的地址.  因爲malloc()的實現往往在分配的數據塊的首部存儲一些用來管理分配的數據塊的記賬信息. 如果不將數據塊首部地址傳遞給free(), free()無法知道數據塊的具體信息, 也就無法釋放. 把NULL傳遞給free()是合法的, free()不起任何作用.

NULL == ((void *)0), 在現代系統上, 地址0不在進程地址空間之內, 引用0地址會引發段錯誤.

2, 謹防"dangling pointer(野指針)", 當p指向的數據塊被釋放後, p就成爲了一個野指針. 如果再次通過p引用數據就存在問題了. p可能指向了內存中別的位置.( 如果在p被釋放之後沒有調用內存分配函數, p可能還指向原來的數據塊, 但該情況不確定). 所以, 在調用free(p);之後, 要緊接着將p設爲NULL. 這樣如果引用p, 就會馬上引起段錯誤. 不會干擾程序的其他地方.

3, 一個數據塊只能被釋放一次, 如果對同一數據塊釋放多次, 會引發問題. (多次調用free(NULL)不存在任何問題.)

4, 被釋放的內存依然位於進程地址空間, 用於以後調用malloc(), calloc(), realloc()返回的數據塊.


在棧上分配內存: alloca()

前面的malloc(), calloc(), realloc()都在堆上分配內存, 需要顯式地釋放所分配的內存. 如果使用alloca()在棧上分配內存, 由於每次函數返回時都會釋放它所在的棧空間,  alloca()所分配的內存會像動態變量一樣被自動釋放.

alloca()的原型:

#include <alloca.h>
void *alloca(size_t size);

不推薦使用alloca(), 因爲它不屬於ISO C或POSIX標準, 依賴於具體的系統和編譯器, 即便在支持它的系統上, 它的實現也有bug.


brk()和sbrk()系統調用

在UNIX系統中, malloc(), calloc(), realloc(), free()這4個函數都是在brk()和sbrk()這兩個系統函數基礎上實現的.  在應用程序中, 極少見到這兩個函數, 這裏對它們做一個簡單介紹, 並利用它們來查看進程地址空間信息.

brk(), sbrk()的原型:

#include <unistd.h>
int brk(void *end_data_segment);
void *sbrk(intptr_t increment);

brk()j將進程地址空間的data段尾(既內存程序映像的堆尾)設置爲end_data_segment所指向的位置. 若成功, 返回0, 否則返回-1.

sbrk()使用差量來調整進程地址空間data段尾的位置, 並返回之前data段尾的地址.

下面看看這樣一個程序, 它顯示進程地址空間的相關信息:

     1    /*
     2     * Show address of code, data and stack sections,
     3     * as well as BSS and dynamic memory.
     4     */
     5   
     6    #include <stdio.h>
     7    #include <malloc.h>        /* for definition of ptrdiff_t on GLIBC */
     8    #include <unistd.h>
     9    #include <alloca.h>        /* for demonstration only */
    10   
    11    extern void afunc(void);    /* a function for showing stack growth */
    12   
    13    int bss_var;            /* auto init to 0, should be in BSS */
    14    int data_var = 42;        /* init to nonzero, should be in data */
    15   
    16    int
    17    main(int argc, char **argv)   
    18    {
    19        char *p, *b, *nb;
    20        int i;
    21        printf("Text Locations:/n");
    22        printf("/tAddress of main: %p/n", (void *)main);
    23        printf("/tAddress of afunc: %p/n", afunc);
    24   
    25        printf("Stack Locations:/n");
    26        afunc();
    27   
    28        p = (char *) alloca(32);
    29        if (p != NULL) {
    30            printf("/tStart of alloca()'ed array: %p/n", p);
    31            printf("/tEnd of alloca()'ed array: %p/n", p + 31);
    32        }
    33   
    34        printf("Data Locations:/n");
    35        printf("/tAddress of data_var: %p/n", & data_var);
    36   
    37        printf("BSS Locations:/n");
    38        printf("/tAddress of bss_var: %p/n", & bss_var);
    39   
    40        nb = sbrk((ptrdiff_t) 0);
    41        printf("Heap Locations:/n");
    42        printf("/tInitial end of heap: %p/n", nb);
    43        b = sbrk((ptrdiff_t) (32));    /* lower heap address */
    44        printf("/t  sbrk return : %p/n", b);
    45       
    46   
    47        nb = sbrk((ptrdiff_t) 0);
    48        printf("/tNew end of heap: %p/n", nb);
    49   
    50        b = sbrk((ptrdiff_t) -16);    /* shrink it */
    51        nb = sbrk((ptrdiff_t) 0);
    52        printf("/tFinal end of heap: %p/n", nb);
    53       
    54        printf("Command-Line Arguments:/n");
    55        for (i = 0; argv[i] != NULL; i++)
    56          printf("/tAddress of arg%d(%s) is %p/n", i, argv[i], &(argv[i]));
    57   
    58        return 0;
    59    }
    60   
    61    void
    62    afunc(void)
    63    {
    64        static int level = 0;        /* recursion level */
    65        auto int stack_var;        /* automatic variable, on stack */
    66   
    67        if (++level == 3)        /* avoid infinite recursion */
    68            return;
    69   
    70        printf("/tStack level %d: address of stack_var: %p/n",
    71                level, & stack_var);
    72        afunc();            /* recursive call */
    73    }

在Linux, x86系統中, 代碼段開始於 0x08048000; 棧底地址開始於0xc0000000.
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章