1. 程式人生 > >C語言之動態記憶體管理

C語言之動態記憶體管理

 

C語言之動態記憶體管理

大綱:

  • 儲存器原理
  • 為什麼存在動態記憶體的開闢
  • malloc()
  • free()
  • calloc()
  • realloc()
  • 常見錯誤
  • 例題
  • 柔性陣列

零(上).儲存器原理

之前我們提到了計算機的儲存器,我們再來回憶一下:

 

 

 我們當時說:

棧區:

  這是儲存器用來儲存區域性變數的部分。每當呼叫函式,函式的所有區域性變數都在棧 上建立。它之所以叫棧是因為它看起來就像堆積而成的棧板:當進入函式時,變數會放到棧頂;離開函式時,把變數從棧頂拿走。奇怪的是,棧做起事來顛三倒四,它從儲存器的頂部開始,向下增長。

堆區:

  堆用於動態儲存:程式在執行時建立一些資料, 然後使用很長一段時間,

資料段:

  全域性量位於所有函式之外,並對所有函式 可見。程式一開始執行時就會建立全域性量, 你可以修改它們,

  常量也在程式一開始執行時建立,但它們儲存在只讀儲存器中。常量是一些在程式中要用到的不變數,你不能修改它們的 值,例如字串字面值。

程式碼段:

  很多作業系統都把程式碼放在儲存器地址的低位。程式碼段也是隻讀的, 它是儲存器中用來載入機器程式碼的部分。

零(下).為什麼存在動態記憶體的開闢

在我們之前的學習中,我們關於記憶體的開闢都是靜態的:

如:

int val = 20;//在棧空間上開闢四個位元組
char arr[10] = { 0 };//在棧空間上開闢10個位元組的連續空間

但是我們發現,這樣的記憶體開闢存在兩個特點:

  1. 空間開闢大小是固定的。

  2.陣列在申明的時候,必須指定陣列的長度,它所需要的記憶體在編譯時分配。

可是,我們對於空間的需求,不僅僅是上述的情況,有時我們需要的空間大小需要程式執行的時候,我們才能知道,那這樣對於陣列大小開闢就十分不好滿足了。

所以,我們就只好來試試動態記憶體開闢了!

 

一.malloc()

再C語言中,提供了一個動態記憶體開闢的函式:

我們來看看它的宣告:文件

void* malloc(size_t size);

再來看看文件:

 

 

注意:

  這個函式向記憶體申請一塊連續可用的空間,並返回指向這塊空間的指標。

       如果開闢成功,則返回一個指向開闢好空間的指標。

  如果開闢失敗,則返回一個NULL指標,因此malloc的返回值一定要做檢查。

  返回值的型別是 void* ,所以malloc函式並不知道開闢空間的型別,具體在使用的時候使用者自己來決定。

  引數是你要開闢多少個位元組,如果引數 size 為0,malloc的行為是標準是未定義的,取決於編譯器。

寫一個例子:

//void* malloc(size_t size);
//malloc
#include <stdio.h>
#include <stdlib.h>
#include <limits.h>
#include <errno.h>
#include <string.h>

int main()
{
    int arr[10] = {0};//在棧區上申請了40個位元組的空間

    //動態記憶體開闢 - 堆區上
    //INT_MAX----整形的最大位元組,位於limit.h檔案中
    //int* p = (int*)malloc(INT_MAX);//開闢失敗的情況
    int* p = (int*)malloc(40);//希望把40個位元組當成一個10個整型的陣列,因為我們開闢的指標型別是int*,所以我們也將返回值強行轉換為int*
    if (p == NULL)
    {
        //strerror 在string.h檔案中
        //errno 在errno.h 檔案中
        printf("記憶體開闢失敗: %s\n",strerror(errno));//列印錯誤資訊,errno提供錯誤碼,strerror將提供的錯誤碼翻譯為一個字串
        
        perror("記憶體開闢失敗");//直接列印錯誤資訊,直接包裝好的一個函式,在 stdio.h 中

        char* p = strerror(errno);//如果我們只想得到錯誤資訊,並不想打印出來,我們就可以用strerror(errno)獲得
        printf("%s\n", p);
    }
    else
    {
        //開闢成功
        int i = 0;
        for (i = 0; i < 10; i++)
        {
            *(p + i) = 0;
        }
        for (i = 0; i < 10; i++)
        {
            printf("%d ", p[i]);
        }
        //不再使用p指向的動態記憶體
        //手動釋放動態開闢的記憶體

        free(p);//這是我們開闢記憶體,最後且必要有的一步,釋放我們開闢的記憶體!!
        p = NULL;

        //......
    }

    return 0;
}

這裡是 strerror()的文件:點我

這裡是 errno()的文件:點我

這裡是 perror()的文件:點我

 

注意:

  我們在開闢記憶體的時候,一定要檢查開闢成功了沒有,即下面這段程式碼:

 

    //假設 p 是我們賦予記憶體的指標
    if (p == null)
    {    //沒有開闢成功
        //...
    }
    else
    {
        //開闢成功
        //...
    }

 

以及最後一定要釋放我們開闢的空間,即:

        free(p);//這是我們開闢記憶體,最後且必要有的一步,釋放我們開闢的記憶體!!
        p = NULL;

所以,我們在這在介紹一下free()

二.free()

宣告:文件

void free(void* ptr);

 

 

 注意:

  free函式用來釋放動態開闢的記憶體。

  如果引數 ptr 指向的空間不是動態開闢的,那free函式的行為是未定義的。

  如果引數 ptr 是NULL指標,則函式什麼事都不做。

 

  及時釋放,及時置NULL

 

示例同上

三.calloc()

它與malloc()都是用來開闢記憶體的,只不過malloc()沒有初始化,而calloc()則對於開闢的記憶體進行了初始化(全部置0),並且引數也由一個變成兩個。

宣告:文件

void* calloc (size_t num, size_t size);

 

 

 

 注意:

  函式的功能是為 num 個大小為 size 的元素開闢一塊空間,並且把空間的每個位元組初始化為0。

  與函式 malloc 的區別只在於 calloc 會在返回地址之前把申請的空間的每個位元組初始化為全0。 

 

示例:

int main()
{
    //int arr[10];
    //開闢一個連續的空間
    //malloc開闢的空間不初始化
    //malloc引數只有1個
    //calloc開闢的空間是初始化的
    //calloc引數有2個

    int*p = (int*)calloc(10, sizeof(int));
    if (p == NULL)
    {
        printf("%s\n", strerror(errno));
    }
    else
    {
        int i = 0;
        for (i = 0; i < 10; i++)
        {
            printf("%d ", *(p + i));
        }
        printf("\n");
        //釋放
        free(p);
        p = NULL;
    }
    return 0;
}

我們在這來觀察一下記憶體:

開闢後:

 

 

 

 

 

 正好四十個位元組置為了0.

所以:

  以後我們要是對申請的記憶體空間的內容要求初始化,那麼可以很方便的使用calloc函式來完成任務。

四.realloc()

有時會我們發現過去申請的空間太小了,有時候我們又會覺得申請的空間過大了,那為了合理的時候記憶體,

我們一定會對記憶體的大小做靈活的調整。那 realloc 函式就可以做到對動態開闢記憶體大小的調整。 

宣告:文件

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

 

 

 注意:

  ptr 是要調整的記憶體地址

  size 調整之後新大小

  返回值為調整之後的記憶體起始位置。

  這個函式調整原記憶體空間大小的基礎上,還會將原來記憶體中的資料移動到 新 的空間。

 

realloc在調整記憶體空間的是存在兩種情況:

  情況1:原有空間之後有足夠大的空間

  情況2:原有空間之後沒有足夠大的空間

 

 

 

情況1: 當是情況1 的時候,要擴充套件記憶體就直接原有記憶體之後直接追加空間,原來空間的資料不發生變化。

情況2: 當是情況2 的時候,原有空間之後沒有足夠多的空間時,擴充套件的方法是:在堆空間上另找一個合適大小的連續空間來使用。

      這樣函式返回的是一個新的記憶體地址。 由於上述的兩種情況,realloc函式的使用就要注意一些。

 

舉個例子:

#include <stdio.h>
int main()
{
    int* ptr = malloc(100);
    if (ptr != NULL)
    {
        //業務處理
    }
    else
    {
        exit(EXIT_FAILURE);
    }

    //擴充套件容量

    //程式碼1   ---   不可行
    ptr = realloc(ptr, 1000);//這樣可以嗎?(如果申請失敗會如何?)
    //所以這樣不可行,若是開闢失敗,我們並無法得知,而且還會非法訪問!

    //程式碼2  ---  可行
    int* p = NULL;
    p = realloc(ptr, 1000);
    if (p != NULL)
    {
        ptr = p;//這裡要記得用我們原來的地址接收返回的地址
                //上面我們提到:要是原有空間之後沒有足夠多的空間時,擴充套件的方法是:在堆空間上另找一個合適大小的連續空間來使用。
                //這樣函式返回的是一個新的記憶體地址,所以我們要記得接收!
    }
    //業務處理

    free(ptr);//一定要記得釋放
    ptr = NULL;//置NULL

    return 0;
}

注意點:

      若是開闢成功,則要記得用原來指標來接收返回的指標

    及時釋放,及時置NULL

 

五.常見錯誤

1.對NULL指標的解引用操作

//1. 對NULL指標的解引用操作
//避免出現:對 malloc/calloc/realloc 函式的返回值做檢測

int main()
{
    int*p = (int*)malloc(INT_MAX);
    //p是有可能為NULL指標的,當為NULL的時候,*p就是非法訪問記憶體

    *p = 0;

    return 0;
}

所以我們要記得對 malloc/calloc/realloc 函式的返回值做檢測

如:

  //假設 p 是我們賦予記憶體的指標
    if (p == null)
    {    //沒有開闢成功
        //...
    }
    else
    {
        //開闢成功
        //...
    }

2.對動態開闢空間的越界訪問

//2. 對動態開闢空間的越界訪問
int main()
{
    int*p = (int*)malloc(10 * sizeof(int));
    if (p == NULL)
    {
        return 1;
    }
    else
    {
        int i = 0;
        //越界
        for (i = 0; i <= 10; i++)
        {
            *(p + i) = 0;//等於10的時候就越界了
        }

        free(p);
        p = NULL;
    }
    return 0;
}

對於越界的問題,我們從陣列那便已經提到要注意了

3.對非動態開闢記憶體使用free釋放

//3. 對非動態開闢記憶體使用free釋放
int main()
{
    int a = 10;
    int*p = &a;
    //...
    free(p);
    p = NULL;
    return 0;
}

4. 使用free釋放一塊動態開闢記憶體的一部分

//4. 使用free釋放一塊動態開闢記憶體的一部分

int main()
{
    int*p = (int*)malloc(10 * sizeof(int));
    if (p == NULL)
    {
        return 1;
    }
    else
    {
        int i = 0;
        //err
        for (i = 0; i <5; i++)
        {
            *p++ = 0;//這裡p++是有副作用的,會導致p指向的值改變
            //*(p + i) = 0;//這裡應該寫為*(p + i)
        }
        //釋放
        free(p);//我們釋放記憶體時,一定要從我們開始的位置進行釋放!
        p = NULL;
    }
    return 0;
}

5.對同一塊動態記憶體多次釋放

//5. 對同一塊動態記憶體多次釋放

int main()
{
    int*p = (int*)malloc(10 * sizeof(int));
    if (p == NULL)
    {
        return 1;
    }
    else
    {
        int i = 0;
        //err
        for (i = 0; i <5; i++)
        {
            *(p + i) = 0;
        }
        //多次釋放會有問題
        free(p);
        
        free(p);

        p = NULL;
        
    }

    return 0;
}

6.動態開闢記憶體忘記釋放(記憶體洩漏)

//6.動態開闢記憶體忘記釋放(記憶體洩漏)
void test()
{

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

    if (NULL != p)
    {
        *p = 20;
    }
}
int main()
{
    test();
    while (1);//未釋放記憶體
}

所以我們一定要記得及時釋放,及時置NULL

 

六.例題

1.

 

//例題一
void GetMemory(char* p)
{
    p = (char*)malloc(100);
}
void Test(void)
{

    char* str = NULL;

    GetMemory(str);

    strcpy(str, "hello world");

    printf(str);
}

int main()
{
    Test();
    return 0;
}

//執行Test()會有什麼結果

注意我們在GetMemory()函式中傳了一個NULL;

而對NULL指標我們是無法擴充套件記憶體的,相當於GetMemory()函式什麼也沒幹;

而strcpy()函式是要對傳進的引數進行斷言的,不能為空指標,而我們傳遞過去了一個空指標;

所以這個程式會崩。

 

 2.

//例題二
char* GetMemory(void)
{
    char p[] = "hello world";
    return p;
}
void Test(void)
{

    char* str = NULL;

    str = GetMemory();

    printf(str);
}

int main()
{
    Test();
    return 0;
}
//執行Test()會有什麼結果

我們要注意,在一個自定義函式結束的時候,它所建立的變數會被銷燬;

所以p返回的地址內容不再是函式裡所建立的 h 了,而是被銷燬後,我們也不知道的內容;

 

 3.

//例題三
void GetMemory(char** p, int num)
{
    *p = (char*)malloc(num);
}
void Test(void)
{

    char* str = NULL;

    GetMemory(&str, 100);

    strcpy(str, "hello");

    printf(str);
}
 
int main()
{
    Test();
    return 0;
}
//執行Test()會有什麼結果

這個要注意我們的例一是直接傳了NULL過去,

而在例三,我們是置str為NULL,然後我們傳過去的是str的地址,並不是NULL;

所以在函式裡是對str指向的NULL內容進行改變,而不是NULL本身;

但是,這裡有一點 程式並無free(),所以就會造成記憶體洩漏的問題!

因此,該函式最後的結果就為螢幕上輸出 hello

 

 4.

//例題四
void Test(void)
{
    char* str = (char*)malloc(100);
    strcpy(str, "hello");

    free(str);

    if (str != NULL)
    {
        strcpy(str, "world");
        printf(str);
    }
}
 
int main()
{
    Test();
    return 0;
}
//執行Test()會有什麼結果

這題是提前釋放了記憶體,但並沒有及時置NULL,之後再進行strcpy(),理應是非法訪問,可是編譯器卻給出了world的結果;

這就說明,我們也不要太相信編譯器!

VS 2019 :

gcc:

 
 

 

七.柔性陣列

1.柔性陣列:

  也許你從來沒有聽說過柔性陣列(flexible array)這個概念,但是它確實是存在的。 C99 中,結構中的最後一個元素允許是未知大小的陣列,這就叫做『柔性陣列』成員。

例如:

typedef struct st_type
{

    int i;

    int a[0];//柔性陣列成員
}type_a;

若有一些編譯器報錯,則可換為以下寫法:

typedef struct st_type
{

    int i;

    int a[];//柔性陣列成員//柔性陣列指的是這個陣列的大小是柔性可變的
}type_a;

2.柔性陣列的特點:

  結構中的柔性陣列成員前面必須至少一個其他成員。

  sizeof 返回的這種結構大小不包括柔性陣列的記憶體。

  包含柔性陣列成員的結構用malloc ()函式進行記憶體的動態分配,並且分配的記憶體應該大於結構的大小,以適應柔性陣列的預期大小。

例如:

typedef struct st_type
{

    int i;

    int a[0];//柔性陣列成員
}type_a;
int main()
{
    printf("%d\n", sizeof(type_a));//輸出的是4//在計算機包含柔型陣列成員的結構體的大小的時候,不包含柔性陣列成員
 return 0;
}

因為sizeof 返回的這種結構大小不包括柔性陣列的記憶體,所以結果為 4.

3.柔性陣列的使用

如:

struct S
{
    int n;
    int arr[];//柔性陣列指的是這個陣列的大小是柔性可變的
};

int main()
{
    //struct S s;//不是建立的
    struct S* ps = (struct S*)malloc(sizeof(struct S) + 10 * sizeof(int));//前半部分是指結構體除柔性陣列外的大小,後半部分是給柔性陣列分配的大小
    ps->n = 100;
    int i = 0;
    for (i = 0; i < 10; i++)
    {
        ps->arr[i] = i;
    }
    //釋放
    free(ps);
    ps = NULL;
    return 0;
}

這樣就給柔性陣列分配了10個整形元素大小

 

4.柔性陣列的優勢

那麼說了這麼多,那柔性陣列的優勢在哪呢?

我們來看下面這兩段程式碼:

typedef struct st_type
{
    int i;
    int a[0];//柔性陣列成員
}type_a;

//程式碼1
int main()
{
    int i = 0;
    type_a* p = (type_a*)malloc(sizeof(type_a) + 100 * sizeof(int));
    //業務處理
    p->i = 100;
    for (i = 0; i < 100; i++)
    {
        p->a[i] = i;
    }
    free(p);
    return 0;
}
//程式碼2
typedef struct st_type
{
    int i;
    int* p_a;
}type_a;
int main()
{
    int i = 0;
    type_a* p = malloc(sizeof(type_a));
    p->i = 100;
    p->p_a = (int*)malloc(p->i * sizeof(int));
    //業務處理
    for (i = 0; i < 100; i++)
    {
        p->p_a[i] = i;
    }
    //釋放空間
    free(p->p_a);
    p->p_a = NULL;
    free(p);
    p = NULL;
    return 0;
}

上述程式碼1和程式碼2實現了同樣的功能,但是硬要讓我選擇一個,那我選擇程式碼1

原因如下:

  1.方便記憶體釋放

  如果我們的程式碼是在一個給別人用的函式中,你在裡面做了二次記憶體分配,並把整個結構體返回給使用者。使用者呼叫free可以釋放結構體,但是使用者並不知道這個結構體內的成員也需要free,

  所以你不能指望使用者來發現這個事。所以,如果我們把結構體的記憶體以及其成員要的記憶體一次性分配好了,並返回給使用者一個結構體 指標,使用者做一次free就可以把所有的記憶體也給釋放掉。

  2.這樣有利於訪問速度.

  連續的記憶體有益於提高訪問速度,也有益於減少記憶體碎片。(其實,我個人覺得也沒多高了,反正你跑不了 要用做偏移量的加法來定址)

此處參考:C語言結構體裡的成員陣列和指標

 

 

 

 

 

|------------------------------------------------------------------

到此,對於動態記憶體管理的講解便結束了!

若有錯誤之處,還望指正!

因筆者水平有限,若有錯誤,還請指正!

&n