C語言內(nèi)存管理講解

謹(jǐn)記

人生有兩條路,一天需要用心走,叫做夢想;一條需要用腳走,叫做現(xiàn)實(shí)。心走的太快,會迷路的;腳走的太快,會摔倒的;心走的太慢,現(xiàn)實(shí)會蒼白;腳走的太慢,夢不會高飛。人生的精彩,是心走得好,腳步剛好能跟上。掌控好你的心,讓它走正;加快你的步伐,讓所有夢想生出美麗的翅膀。

前言

今天為大家?guī)淼氖荂語言里面的內(nèi)存管理的知識點(diǎn),這篇文章過后,我們C語言的大體基本知識就已經(jīng)介紹完了,那么下一篇文章開始,我講講解OC語法,也就是蘋果公司推出的Objective-C語言,這是蘋果應(yīng)用開發(fā)的語言,也歡迎大家閱讀,本篇文章是對C語言內(nèi)存管理的一個講解,內(nèi)存的使用是程序設(shè)計(jì)中需要考慮的重要因素之一,這不僅由于系統(tǒng)內(nèi)存是有限的(尤其在嵌入式系統(tǒng)中),而且內(nèi)存分配也會直接影響到程序的效率。因此,讀者要對C語言中的內(nèi)存管理,有個系統(tǒng)的了解。

內(nèi)存管理

在C語言中,定義了4個內(nèi)存區(qū)間:代碼區(qū);全局變量與靜態(tài)變量區(qū);局部變量區(qū)即棧區(qū);動態(tài)存儲區(qū),即堆區(qū)。下面分別對這4個區(qū)進(jìn)行介紹。
① 代碼區(qū)。代碼區(qū)中主要存放程序中的代碼,屬性是只讀的。
② 全局變量與靜態(tài)變量區(qū)。也稱為靜態(tài)存儲區(qū)域。內(nèi)存在程序編譯的時(shí)候就已經(jīng)分配好,這塊內(nèi)存在程序的整個運(yùn)行期間都存在。例如:全局變量、靜態(tài)變量和字符串常量。分配在這個區(qū)域中的變量,當(dāng)程序結(jié)束時(shí),才釋放內(nèi)存。因此,經(jīng)常利用這樣的變量,在函數(shù)間傳遞信息。
③ 棧區(qū)。在棧上創(chuàng)建。在執(zhí)行函數(shù)時(shí),函數(shù)內(nèi)局部變量的存儲單元都可以在棧上創(chuàng)
建,函數(shù)執(zhí)行結(jié)束時(shí)這些存儲單元自動被釋放。棧內(nèi)存分配運(yùn)算內(nèi)置于處理器的指令集中,效率很高,但是分配的內(nèi)存容量有限。在linux系統(tǒng)中,通過命令“ulimit –s”,可以看到,棧的容量為8192kbytes,即8M。
這種內(nèi)存方式,變量內(nèi)存的分配和釋放都自動進(jìn)行,程序員不需要考慮內(nèi)存管理的問題,很方便使用。但缺點(diǎn)是,棧的容量有限制,且當(dāng)相應(yīng)的范圍結(jié)束時(shí),局部變量就不能在使用。
④ 堆區(qū)。有些操作對象只有在程序運(yùn)行時(shí)才能確定,這樣編譯器在編譯時(shí)就無法為他們預(yù)先分配空間,只能在程序運(yùn)行時(shí)分配,所以稱為動態(tài)分配。
比如:下面的結(jié)構(gòu)體定義:

struct employee
{
char name[8];
int age;
char gender;
float salary;
}; 

在該結(jié)構(gòu)體定義中,員工的姓名是用字符數(shù)組來存儲。若員工的姓名由用戶輸入,則只有在用戶輸入結(jié)束后,才能精確的知道,需要多少內(nèi)存,在這種情況下,使用動態(tài)內(nèi)存分配更合乎邏輯,應(yīng)該把結(jié)構(gòu)體的定義改成下面的形式:

struct employee
{
char *name;
int age;
char gender;
float salary;
};

動態(tài)分配內(nèi)存就是在堆區(qū)上分配。程序在運(yùn)行的時(shí)候用malloc申請任意多少的內(nèi)存,程序員自己負(fù)責(zé)在何時(shí)用free釋放內(nèi)存。動態(tài)內(nèi)存的生存期由我們決定,使用非常靈活,但問題也最多。
下面的這段程序說明了不同類型的內(nèi)存分配。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
/*C語言中數(shù)據(jù)的內(nèi)存分配*/
int a = 0; 
char *p1; 
int main()
{
    int b;                  /* b在棧 */
    char s[] = "abc";           /* s在棧, "abc"在常量區(qū) */
    char *p2;                   /* p2在棧 */
    char *p3 = "123456";        /*"123456"在常量區(qū),p3在棧*/
    static int c =0;            /*可讀可寫數(shù)據(jù)段*/
    p1 = (char *)malloc(10);    /*分配得來的10個字節(jié)的區(qū)域在堆區(qū)*/
    p2 = (char *)malloc(20);    /*分配得來的20個字節(jié)的區(qū)域在堆區(qū)*/
    /* 從常量區(qū)的“Hello World”字符串復(fù)制到剛分配到的堆區(qū) */
    strcpy(p1, “Hello World");
    return 0;
}

動態(tài)內(nèi)存的申請和分配

當(dāng)程序運(yùn)行到需要一個動態(tài)分配的變量時(shí),必須向系統(tǒng)申請取得堆中的一塊所需大小的存儲空間,用于存儲該變量。當(dāng)不再使用該變量時(shí),也就是它的生命結(jié)束時(shí),要顯式釋放它所占用的存儲空間,這樣系統(tǒng)就能對該堆空間進(jìn)行再次分配,做到重復(fù)使用有限的資源。下面將介紹動態(tài)內(nèi)存申請和釋放的函數(shù)。

malloc函數(shù)
在C語言中,使用malloc函數(shù)來申請內(nèi)存。函數(shù)原型如下:

#include <stdlib.h>

void *malloc(size_t size);

其中,參數(shù)size代表需要動態(tài)申請的內(nèi)存的字節(jié)數(shù)。若內(nèi)存申請成功,函數(shù)返回申請到的內(nèi)存的起始地址,若申請失敗,返回NULL。使用該函數(shù)時(shí),有下面幾點(diǎn)要注意:
(1)只關(guān)心申請內(nèi)存的大小。該函數(shù)的參數(shù),很簡單,只有申請內(nèi)存的大小,單位是字節(jié)。
(2)申請的是一塊連續(xù)的內(nèi)存。該函數(shù)一定是申請一塊連續(xù)的區(qū)間,可能申請到的內(nèi)存比實(shí)際申請的大。也可能申請不到,若申請失敗,返回NULL。讀者,一定記得寫出錯判斷。
(3)返回值類型是void *。函數(shù)的返回值是void *,不是某種具體類型的指針。讀者可以理解成,該函數(shù)只是申請內(nèi)存,對在內(nèi)存中存儲什么類型的數(shù)據(jù),沒有要求。因此,返回值是void *。在實(shí)際編程中,根據(jù)實(shí)際情況,將void * 轉(zhuǎn)換成所需要的指針類型。
(4)顯示初始化。注意,堆區(qū)是不會自動在分配時(shí)做初始化的(包括清零),所以程序中需要顯式的初始化。

free函數(shù)
在堆區(qū)上分配的內(nèi)存,需要用free函數(shù)顯示釋放。函數(shù)原型如下:

#include <stdlib.h>
void free(void *ptr);

函數(shù)的參數(shù)ptr,指的是需要釋放的內(nèi)存的起始地址。該函數(shù)沒有返回值。使用該函數(shù),也有下面幾點(diǎn)需要注意:
(1)必須提供內(nèi)存的起始地址。調(diào)用該函數(shù)時(shí),必須提供內(nèi)存的起始地址,不能提供部分地址,釋放內(nèi)存中的一部分是不允許的。因此,必須保存好malloc返回的指針值,若丟失,則所分配的堆空間無法回收,稱內(nèi)存泄漏。
(2)malloc和free配對使用。編譯器不負(fù)責(zé)動態(tài)內(nèi)存的釋放,需要程序員顯示釋放。因此,malloc與free是配對使用的,避免內(nèi)存泄漏。
示例代碼

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int *get_memory(int n){
    int *p, i;
    if ((p = (int *)malloc(n * sizeof(int))) == NULL){
        printf("malloc error\n");
        return p;
    }
    memset(p, 0, n * sizeof(int));
    for (i = 0; i < n; i++)
        p[i] = i+1;
    return p;
}
int main(int argc, const char * argv[]) {
    int n, *p, i;
    printf("input n:");
    scanf("%d", &n);
    if ((p = get_memory(n)) == NULL)
        return 0;
    for (i = 0; i < n; i++)
        printf("%d ", p[i]);
    printf("\n");
    free(p);
    p = NULL;
    return 0;
}
輸出結(jié)果:
input n:10
1 2 3 4 5 6 7 8 9 10
Program ended with exit code: 0

說明:該程序演示了動態(tài)內(nèi)存的標(biāo)準(zhǔn)用法。動態(tài)內(nèi)存的申請,通過一個指針函數(shù)來完成。內(nèi)存申請時(shí),判斷是否申請成功,成功后,對內(nèi)存初始化。在主調(diào)函數(shù)中,動態(tài)內(nèi)存依然可以訪問,不再訪問內(nèi)存時(shí),用free函數(shù)釋放。
3)不允許重復(fù)釋放。同一空間的重復(fù)釋放也是危險(xiǎn)的,因?yàn)樵摽臻g可能已另分配。在上面程序中,如果釋放堆空間兩次(連續(xù)調(diào)用兩次free(p)),會出現(xiàn)崩潰,控制臺打印很多內(nèi)存指令。
(4)free只能釋放堆空間。像代碼區(qū)、全局變量與靜態(tài)變量區(qū)、棧區(qū)上的變量,都不需要程序員顯示釋放,這些區(qū)域上的空間,不能通過free函數(shù)來釋放,否則執(zhí)行時(shí),會出錯。

#include <stdio.h>
#include <string.h>
#include <stdlib.h>
int main(int argc, const char * argv[]) {
    int a[10] = {0};
    free(a);
    return 0;
}
輸出結(jié)果:
內(nèi)存管理(1624,0x10007f000) malloc: *** error for object 0x7fff5fbff820: pointer being freed was not allocated
*** set a breakpoint in malloc_error_break to debug
這里程序運(yùn)行后會報(bào)錯,直接會崩潰,這里讀者自己去嘗試一下。

野指針
提到野指針,前面我們說指針的時(shí)候,都已經(jīng)提過了,這里就簡單的在提一下。
野指針指的是指向“垃圾”內(nèi)存的指針,不是NULL指針。出現(xiàn)“野指針”主要有以下原因:
(1)指針變量沒有被初始化。指針變量和其它的變量一樣,若沒有初始化,值是不確定的。也就是說,沒有初始化的指針,指向的是垃圾內(nèi)存,非常危險(xiǎn)。

#include <stdio.h>
int main(int argc, const char * argv[]) {
    int *p;
    printf("%d\n", *p);
    *p = 10;
    printf("%d\n", *p);
    return 0;
}

(2)指針p被free之后,沒有置為NULL。free函數(shù)是把指針?biāo)赶虻膬?nèi)存釋放掉,使內(nèi)存成為了自由內(nèi)存。但是,該函數(shù)并沒有把指針本身的內(nèi)容清楚。指針仍指向已經(jīng)釋放的動態(tài)內(nèi)存,這是很危險(xiǎn)。
程序員稍有疏忽,會誤以為是個合法的指針。就有可能再通過指針去訪問動態(tài)內(nèi)存。實(shí)際上,這時(shí)的內(nèi)存已經(jīng)是垃圾內(nèi)存了。
關(guān)于野指針會造成什么樣的后果,這是很難估計(jì)的。若內(nèi)存仍然是空閑的,可能程序暫時(shí)正常運(yùn)行;若內(nèi)存被再次分配,又通過野指針對內(nèi)存進(jìn)行了寫操作,則原有的合法數(shù)據(jù),會被覆蓋,這時(shí),野指針造成的影響將是無法估計(jì)的。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, const char * argv[]) {
    int n = 5, *p, i;
    if ((p = (int *)malloc(n * sizeof(int))) == NULL){
        printf("malloc error\n");
        return 0;
    }
    memset(p, 0, n * sizeof(int));
    for (i = 0; i < n; i++){
        p[i] = i+1;
        printf("%d ", p[i]);
    }
    printf("\n");
    printf("p=%p *p=%d\n", p, *p);
    free(p);
    printf("after free:p=%p *p=%d\n", p, *p);
    *p = 100;
    printf("p=%p *p=%d\n", p, *p);
    return 0;
}
說明:該程序中,故意在執(zhí)行了“free(p)”之后,通過野指針p對動態(tài)內(nèi)存進(jìn)行了讀寫,程序正常執(zhí)行,也在預(yù)料之中。前面已經(jīng)分析過,內(nèi)存釋放后,若繼續(xù)訪問甚至修改,后果是不可預(yù)料的。

(3)指針操作超越了變量的作用范圍。指針操作時(shí),由于邏輯上的錯誤,導(dǎo)致指針訪問了非法內(nèi)存,這種情況讓人防不勝防,只能依靠程序員好的編碼風(fēng)格,已及扎實(shí)的基本功。

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
int main(int argc, const char * argv[]) {
    int a[5] = {1, 9, 6, 2, 10}, *p, i, n;
    n = sizeof(a) / sizeof(n);
    p = a;
    for (i = 0; i <= n; i++){
        printf("%d ", *p);
        p++;
    }
    printf("\n");
    *p = 100;
    printf("*p=%d\n", *p);
    return 0;
}
說明:該程序故意出了兩個錯誤,一是for循環(huán)的條件“i <= n”,p指針指向了數(shù)組以外的空間。二是“*p = 100”,對非法內(nèi)存進(jìn)行了寫操作。

(4)不要返回指向棧內(nèi)存的指針。在函數(shù)中,詳細(xì)介紹了指針函數(shù),指針函數(shù)會返回一個指針。在主調(diào)函數(shù)中,往往會通過返回的指針,繼續(xù)訪問指向的內(nèi)存。因此,指針函數(shù)不能返回棧內(nèi)存的起始地址,因?yàn)闂?nèi)存在函數(shù)結(jié)束時(shí)會被釋放。

堆和棧的區(qū)別

1.申請方式
棧(stack)是由系統(tǒng)自動分配的。例如,聲明函數(shù)中一個局部變量“int b;”,那么系統(tǒng)自動在棧中為b開辟空間。堆(heap)需要程序員自己申請,并在申請時(shí)指定大小。使用C語言中的malloc函數(shù)的例子如下所示。
p1 = (char *)malloc(10);
2.申請后系統(tǒng)的響應(yīng)
堆在操作系統(tǒng)中有一個記錄空閑內(nèi)存地址的鏈表。當(dāng)系統(tǒng)收到程序的申請時(shí),系統(tǒng)就會開始遍歷該鏈表,尋找第一個空間大于所申請空間的堆節(jié)點(diǎn),然后將該節(jié)點(diǎn)從空閑節(jié)點(diǎn)鏈表中刪除,并將該節(jié)點(diǎn)的空間分配給程序。另外,對于大多數(shù)系統(tǒng),會在這塊內(nèi)存空間中的首地址處記錄本次分配的大小。這樣,代碼中的刪除語句才能正確地釋放本內(nèi)存空間。如果找到的堆節(jié)點(diǎn)的大小與申請的大小不相同,系統(tǒng)會自動地將多余的那部分重新放入空閑鏈表中。
只有棧的剩余空間大于所申請空間,系統(tǒng)才為程序提供內(nèi)存,否則將報(bào)異常,提示棧溢出。
3.申請大小的限制
堆是向高地址擴(kuò)展的數(shù)據(jù)結(jié)構(gòu),是不連續(xù)的內(nèi)存區(qū)域。這是由于系統(tǒng)用鏈表來存儲的空閑內(nèi)存地址,地址是不連續(xù)的,而鏈表的遍歷方向是由低地址向高地址。堆的大小受限于計(jì)算機(jī)系統(tǒng)中有效的虛擬內(nèi)存,因此堆獲得的空間比較靈活,也比較大。
棧是向低地址擴(kuò)展的數(shù)據(jù)結(jié)構(gòu),是一塊連續(xù)的內(nèi)存區(qū)域。因此,棧頂?shù)牡刂泛蜅5淖畲笕萘渴窍到y(tǒng)預(yù)先規(guī)定好的,如果申請的空間超過棧的剩余空間時(shí),將提示棧溢出,因此,能從棧獲得的空間較小。
4.申請速度的限制
堆是由malloc等語句分配的內(nèi)存,一般速度比較慢,而且容易產(chǎn)生內(nèi)存碎片,不過用起來很方便。棧由系統(tǒng)自動分配,速度較快,但程序員一般無法控制。
5.堆和棧中的存儲內(nèi)容
堆一般在堆的頭部用一個字節(jié)存放堆的大小,堆中的具體內(nèi)容由程序員安排。
在調(diào)用函數(shù)時(shí),第一個進(jìn)棧的是函數(shù)調(diào)用語句的下一條可執(zhí)行語句的地址,然后是函數(shù)的各個參數(shù),在大多數(shù)的C語言編譯器中,參數(shù)是由右往左入棧的,然后是函數(shù)中的局部變量。當(dāng)本次函數(shù)調(diào)用結(jié)束后,局部變量先出棧,然后是參數(shù),最后棧頂指針指向最開始的存儲地址,也就是調(diào)用該函數(shù)處的下一條指令,程序由該點(diǎn)繼續(xù)運(yùn)行。

C語言關(guān)鍵字

C語言關(guān)鍵字volatile
C語言關(guān)鍵字volatile(注意它是用來修飾變量而不是上面介紹的volatile)表明某個變量的值可能隨時(shí)被外部改變(例如,外設(shè)端口寄存器值),因此對這些變量的存取不能緩存到寄存器,每次使用時(shí)需要重新讀取。
該關(guān)鍵字在多線程環(huán)境下經(jīng)常使用,因?yàn)樵诰帉懚嗑€程的程序時(shí),同一個變量可能被多個線程修改,而程序通過該變量同步各個線程。對于C語言編譯器來說,它并不知道這個值會被其他線程修改,自然就把它緩存到寄存器里面。volatile的本意是指這個值可能會在當(dāng)前線程外部被改變,此時(shí)編譯器知道該變量的值會在外部改變,因此每次訪問該變量時(shí)會重新讀取。這個關(guān)鍵字在外設(shè)接口編程中經(jīng)常被使用。

總結(jié)

本篇文章簡單的介紹C語言中的內(nèi)存管理,希望讀者掌握。

結(jié)尾

希望讀者真誠的對待每一件事情,每天都能學(xué)到新的知識點(diǎn),要記住,認(rèn)識短暫,開心也是過一天,不開心也是一天,無所事事也是一天,小小收獲也是一天,歡迎收藏和點(diǎn)贊、喜歡。最后送讀者一句話:你的路你自己選擇。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

推薦閱讀更多精彩內(nèi)容

  • C語言中內(nèi)存分配 在任何程序設(shè)計(jì)環(huán)境及語言中,內(nèi)存管理都十分重要。在目前的計(jì)算機(jī)系統(tǒng)或嵌入式系統(tǒng)中,內(nèi)存資源仍然是...
    一生信仰閱讀 1,193評論 0 2
  • 前言 C語言作為一門應(yīng)用途廣泛、功能強(qiáng)大、使用靈活的面向過程式編程語言。既可用于編寫應(yīng)用軟件,又能用于編寫系統(tǒng)軟件...
    老板娘來盤一血閱讀 13,034評論 32 83
  • (JG-2014-08-20)(前半部分經(jīng)過網(wǎng)上多篇文章對比整理)(后半部分根據(jù)ExceptionalCpp、C+...
    JasonGao閱讀 5,628評論 2 23
  • 2016年國慶假期終于把此書過完,整理筆記和體會于此。 關(guān)于書名 書名源于俄羅斯的演員斯坦尼斯拉夫斯基創(chuàng)作的《演員...
    李劍飛的簡書閱讀 7,305評論 2 65
  • 以前在廣告公司里的時(shí)候,經(jīng)常聽見掌門人拍拍手,說,“來,大家來,看這邊”。就知道所有人來點(diǎn)評投票Idea的時(shí)間到了...
    Graceland閱讀 607評論 4 8