2016年9月15日 星期四

給新手的C++教學 (上冊) - 13 - 6. 更彈性的取得和釋放記憶體

回到「給新手的C++教學 (上冊)」

回到「13. 額外語法 (Extra syntax)」

上一頁

名詞解釋:
「釋放」記憶體:將某塊記憶體標記為「使用完畢」,讓這塊記憶體之後或讓其他程式可以再被利用

在之前的字元字串章節,我們使用了一個很大很大 (大小為100萬) 的陣列來儲存「一個名字」
除非要處理特別多的資料,否則應該是不會需要這麼大的陣列啦XD
但接下來,您會發現,當陣列太大的時候,會發生問題的
為了節省版面,這裡以字元字串章節的最後一份程式碼為例:

#include<cstdio>
int main()
{
    char name[1000001];
    scanf("%s",name);
    printf("Hello, %s!\n",name);
    return 0;
}
輸入名字「Motivation」,會輸出「Hello, Motivation!」

假如哪天你發現某人的名字太長了 (?),大小100萬的陣列不夠用 (!),想要宣告大小1000萬的陣列 (......)
好啊!欣然同意啊XD

#include<cstdio>
int main()
{
    char name[10000001];
    scanf("%s",name);
    printf("Hello, %s!\n",name);
    return 0;
}
欸欸等等我甚麼是都還沒做耶!

咦?怎麼程式掛了?!
根本都還沒開始輸入名字呀!


仔細想想,在輸入名字之前,程式只有執行了一行:char name[10000001];
而我們剛剛的修改也只是把陣列的長度多加一個0而已

What's the problem???
陣列太大導致記憶體不足嗎?
不對啊,仔細算算,我們才用了「10000001 Bytes = 9765.6 KB = 9.5 MB」的記憶體啊
相對電腦隨隨便便就有幾GB (1 GB = 1024 MB) 的閒置記憶體,根本一點都不成問題!

其實,問題不在你身上,是電腦程式本身架構的問題!
一個用C++寫出來的電腦程式使用的記憶體,其實是有分類的
大概可以分成三類:
1. static (靜態)
2. heap (堆積)
3. stack (堆疊)

  • 「static記憶體」的特性是程式執行之前會先配置好哪一塊記憶體要用來幹嘛,之後就不再容許更改,適合儲存從頭到尾都要存在的資料
    例如:程式的邏輯和架構,以後甚至會學到怎麼把一些變數自訂成使用static記憶體
  • 「heap記憶體」的特性是可以在程式執行的過程中隨時取得和釋放,專業上我們稱之可以「動態配置」記憶體。不過機制較為複雜,記憶體的取得和釋放也較為耗時
    通常用來儲存巨量的快取資料 (也就是cache,目的在於解決從硬碟讀取太慢的問題)、或者程式執行過程中臨時需要大量存放的資料 (暫存資料)
  • 「stack記憶體」的特性是會隨著函式的呼叫而使用,在函式內宣告的變數通常會使用stack記憶體。機制簡單,記憶體的取得和釋放很快,但也因為這個機制的缺陷,分配多點記憶體的投資報酬率並不高,電腦並不喜歡提供stack記憶體太多額度 (通常只有4 MB左右)
那麼,當我們寫下一行「char name[10000001];」,使用的是哪一種記憶體呢?
答案是「stack」,也就是被電腦限制使用量的那種
因此,當我們在函式裡面宣告一個很大很大的陣列,很容易就會造成「stack overflow (堆疊溢出)」的情況,也就是使用的stack記憶體超出電腦提供的額度而讓程式發生錯誤

太不公平了吧!整台電腦4 GB的記憶體我們只能使用4 MB?!
別忘了,除了「stack記憶體」之外,我們還有「static記憶體」和「heap記憶體」可以使用
「static記憶體」在使用上有些限制和特色,之後的頁面會介紹
那麼,要怎麼使用「heap記憶體」呢?

請注意,這會需要熟悉指標的用法,對指標還不熟的同學們,請複習一下指標章節

上一頁,我們得知指標就是陣列
因此,我們只要和電腦取得某一塊可用heap記憶體的「指標」,就可以用「陣列」的方式使用heap記憶體
例如,跟電腦要一塊大小為「10000000個char變數」的heap記憶體,並取得其指標的方式如下:

new char[10000000]


這整個東西可以當作一個char* (char的指標),因此我們可以用一個型別為「char*」的變數 (就命名為「name」吧) 來儲存它:

char *name=new char[10000000];

可以發現,現在我們可以直接把「name」當作一個「長度為10000000的char陣列」來用了!
而且使用的是「heap記憶體」,不用理會那可憐的「4 MB」限制!

好了,現在,您知道要怎麼解決本頁開頭程式掛掉的問題了嗎?
希望您可以自行解決此問題,但還是提供解答以供參考:

#include<cstdio>
int main()
{
    char *name=new char[10000001];
    scanf("%s",name);
    printf("Hello, %s!\n",name);
    return 0;
}
程式正常執行了耶!

等等,事情沒有這麼簡單!這份程式碼還是有問題的!
heap記憶體的取得和釋放都很耗時,而且多個指標使用同一塊記憶體是允許的,因此就算程式執行到「name的作用範圍」之外,電腦還是不會自動釋放「name」使用的那塊記憶體
好處是,如果有另一個char*指標 (假設叫做「a」) 和name使用了同一塊記憶體,那麼就算執行到了name的作用範圍之外,我們還是可以透過「a」(如果還在a的作用範圍內) 來繼續使用name使用的那塊記憶體和裡面儲存的資料
舉個例子:

#include<cstdio>
int main()
{
    char *a;
    {
        char *name=new char[10000001];
        scanf("%s",name);
        printf("name: Hello, %s!\n",name);
        a=name;
    }
    printf("a:    Hello, %s!\n",a);
    return 0;
}
我們將「name指標」的資訊 (記憶體編號) 複製給「a指標」
當程式執行到「name」的作用範圍外時,還是可以透過「a」來取得之前「name」使用的那一塊記憶體的資訊:Motivation

電腦並不會自動幫您釋放記憶體,意味著如果您沒有針對這件事做任何處理,程式在全部執行完畢之前,只會無可救藥地消耗越來越多的記憶體 (因為記憶體沒有被釋放就無法被重新利用),這種情況我們稱之「記憶體泄漏 (memory leak)」
如果您的程式執行地夠久 (依據您程式的記憶體使用速率而定),最終一定會將整台電腦的記憶體消耗殆盡
這會是一個很大很大的問題,如果您的作業系統沒有特別去偵測這種情況 (偵測這種東西是不容易的),會有那麼一個時間點,螢幕畫面突然從此停止不動了!
到時候,強制將電源線拔掉並重新開機將是您唯一的選項

如何避免這種問題呢?
首先,您必須學會怎麼手動「釋放」您的程式先前跟電腦要的heap記憶體
方式如下 (以本頁範例中的「name」為例):

delete[] name;

就這麼一行,小動作可以解決大問題!
因此,程式碼應該要這樣寫:

#include<cstdio>
int main()
{
    char *name=new char[10000001];
    scanf("%s",name);
    printf("Hello, %s!\n",name);
    delete[] name;
    return 0;
}

什麼,您說想看看加不加delete的差異?
沒問題!

這裡提供一個例子:

#include<cstdio>
int main()
{
    for(int i=0;i<100;i++)
    {
        char *name=new char[500000001];
        name[0]='M';
        name[1]='o';
        name[2]='t';
        name[3]='i';
        name[4]='v';
        name[5]='a';
        name[6]='t';
        name[7]='i';
        name[8]='o';
        name[9]='n';
        name[10]='\0';
        printf("i = %d, name = %s\n",i,name);
        delete[] name;
    }
    return 0;
}
程式順利執行完畢了

可以看到,程式順利地執行完畢了!
現在,請將有delete的那一行程式碼移除,再執行一次程式看看吧~

#include<cstdio>
int main()
{
    for(int i=0;i<100;i++)
    {
        char *name=new char[500000001];
        name[0]='M';
        name[1]='o';
        name[2]='t';
        name[3]='i';
        name[4]='v';
        name[5]='a';
        name[6]='t';
        name[7]='i';
        name[8]='o';
        name[9]='n';
        name[10]='\0';
        printf("i = %d, name = %s\n",i,name);
    }
    return 0;
}
這次很幸運,程式被強制終止了XD

祝您一路好走,就不送囉~XD

對了,請記得先把重要資料存檔!!!

下一頁

感謝:
(版權所有 All copyright reserved)

沒有留言:

張貼留言

歡迎留言或問問題~
若您的留言中包含程式碼,請參考這篇
如果留言不見了請別慌,那是因為被google誤判成垃圾留言,小莫會盡快將其手動還原

注意:只有此網誌的成員可以留言。