C語言變量類型與內存管理


對於程序員,一般來說,我們可以簡單將內存分為三個部分:靜態區,棧,堆。

靜態區:保存自動全局變量和static 變量(包括static 全局和局部變量)。靜態區的內容在整個程序的生命周期內都存在,由編譯器在編譯的時候分配。

棧:保存局部變量。棧上的內容只在函數的范圍內存在,當函數運行結束,這些內容也會自動被銷毀。其特點是效率高,但空間大小有限。

堆:由malloc 系列函數或new 操作符分配的內存。其生命周期由free delete決定。在沒有釋放之前一直存在,直到程序結束。其特點是使用靈活,空間比較大,但容易出錯。

 以上,緊密相關的一個詞就是“生命周期”,以及變量的分類。所以,下面我們簡單總結一下變量類型的內容。

一、變量類型

1.生命周期

變量的生命周期,也稱生存期,是指變量值保留的期限。按照生命周期,可將變量分為兩類:靜態變量和動態變量。

靜態變量:變量存儲在內存中的靜態存儲區,在編譯時就分配了存儲空間,在整個程序運行期間,該變量占有固定的存儲單元,變量的值都始終存在,程序結束后,這部分空間才釋放。這類變量的生存期為整個程序。

動態變量:變量存儲在內存中的動態存儲區,在程序運行過程中,只有當變量所在函數被調用時,編譯系統才臨時為該變量分配一段內存單元,該變量才有值,函數調用結束,變量值立即消失,這部分空間釋放。我們說這類變量的生存期僅在函數調用期間

 C語言中具有靜態存儲性質的變量:外部變量,靜態局部變量和靜態全局變量

C語言中具有動態存儲性質的變量:自動變量(auto,默認可以不寫),寄存器變量(register)

與上面的物理內存相對,我們很容易看出,C語言中,靜態變量存儲在靜態區,動態變量存儲在棧,由程序員自己動態分配的變量就存儲在堆。

 2.作用域

變量的作用域也稱為可見性,指變量的有效范圍,可分為局部和全局兩種。

局部變量:在一個函數或復合語句內定義的變量是局部變量,局部變量僅在定義它的函數或復合語句內有效。

全局變量:定義在所有函數之外的變量是全局變量,作用范圍是從定義開始,到本文件或程序結束。

C語言中自動變量、寄存器變量和內部靜態變量都屬於局部變量;外部變量是程序級的全局變量,外部靜態變量是源文件級的全局變量。

 3.C語言變量

從上面我們看到,C語言中的變量有:自動變量、寄存器變量、外部變量、內部靜態變量和外部靜態變量。下面分別就這幾個變量進行簡單說明。

3.1自動變量

auto:編譯器在默認的缺省情況下,所有變量都是auto 的。

auto int a    等價於   int a

3.2寄存器變量

register:這個關鍵字請求編譯器盡可能的將變量存在CPU 內部寄存器中,而不是內存中。因為如果一個變量在程序中頻繁使用,如循環控制變量,大量訪問內存就會影響程序的執行效率。注意是盡可能,不是絕對。一個CPU 的寄存器也就那么幾個或幾十個,如果在一個函數中定義的register 變量多於CPU中的寄存器數量,C編譯程序會自動將寄存器變量轉為自動變量。

使用register 修飾符的注意點

1)由於受硬件寄存器長度的限制,素以寄存器變量只能是char,int或指針型,只能用於說明函數中變量或函數中的形參。

2)由於register變量使用的是CPU中的寄存器,寄存器變量無地址,所以不能用取址運算符&”來獲取register變量的地址

3.3外部變量

外部變量的說明一般形式是:

extern  類型說明符   變量名

所謂“外部”是相對於函數內部而言的,C語言中的外部變量就是定義在所有函數之外的全局變量。

如果外部變量的定義和使用是在同一個文件中,則在該源文件中的函數在使用外部變量時,不需要再進行其他的說明,可直接使用。當外部變量的定義和使用在兩個不同的源文件,若要使用其他源文件中定義的外部變量,就必須在使用該外部變量之前,就必須使用extern存儲類型說明符進行變量的“外部”說明。

下面舉個簡單地小栗子:

文件1
//
定義一個全局變量,並在testExtern中調用, //測試是否在該變量定義的源文件下不需要用extern關鍵字進行變量聲明 //在其他源文件下必須使用extern關鍵字聲明才能使用 //同時注意變量聲明的兩個必備:extern關鍵字; 不顯式賦值 int externVal = 1; //extern void printfExternVal(); void printfExternVal(); void printExternVal() { printf("%-5d\n", externVal); //%-5d 右空5格;%6d 左空6格 }

int _tmain(int argc, _TCHAR* argv[])
{
    printExternVal();
    printfExternVal();
    system("pause");
    return 0;
}

文件2
#include "stdafx.h"
#include <stdio.h>

//當將下面這行聲明注釋掉后
//會顯示錯誤:未定義標識符“externVal”
extern int externVal;
void printfExternVal()
{
    printf("%6d\n", externVal);
}

這時候我們想在文件1中的main函數里調用文件2的函數,怎么辦呢?通常我們看到在大的工程項目中,都是創建一個頭文件,將文件2中函數的聲明放在頭文件中,然后文件1 #include<>這個頭文件就可以用了。我們這里只是一個測試小程序,所以用不着牛刀殺雞。

C語言中不僅有外部變量,而且有外部函數。當需要調用的函數在另一個源文件時,必須使用extern”說明符說明被調用函數是外部函數。加粗部分是我摘抄C語言書上的原話,那么問題來了,我們發現,即使我們不加extern關鍵字,只在文件1中加簡單地函數聲明也是可以編譯運行的,為什么呢?

以下Q&A摘自:https://segmentfault.com/q/1010000000249480

提問:【C語言】調用另一個源文件中的函數需要用extern關鍵字申明嗎?

回答:函數聲明主要是給鏈接器一個明確的hint,從而在匹配函數名字以后還能檢查一下類型是否正確。至於extern關鍵字,對於函數聲明本身是無所謂的,反正末尾一個分號編譯器就懂了,能識別出來這是個聲明而不是定義;只是對於變量的聲明,沒它就不行。C標准里是怎么要求的我不確定,不過建議是,對於本文件的函數不加extern,外部文件的加上,這樣可以給讀源碼的人一個hintp.s. stdio.h里的函數聲明都是有extern的。

3.4靜態變量

靜態變量有兩種:外部靜態變量和內部靜態變量

外部靜態變量是全局變量,但作用域僅僅在定義它的那個源文件中,出了該源文件不管是否用extern說明都是不可見的。簡單而言,外部靜態變量僅僅作用於定義它的一個源文件,而外部變量作用於整個程序。

內部靜態變量與自動變量有相似之處。內部靜態變量也是局限於一個特定的函數內部,出了定義它的函數,即使對於同一個文件中的其他函數也是不可見的。但它不像自動變量那樣,僅當定義自動變量的函數被調用時才存在,退出函數調用就消失。內部靜態變量是始終存在的,當函數被調用退出后,內部靜態變量會保存數值,再次調用該函數時,以前調用時的數值仍然保留着。

 二、內存管理

我們已經清楚C語言中各類變量的存儲屬性,以及對應存儲在計算機中的什么區域,那么回答下面幾個問題也輕而易舉了。

1.什么是靜態區越界,什么是棧越界,什么是堆越界?

2.為什么在靜態或動態檢測中我們常聽到的是數組越界,緩沖區溢出,內存泄露,而不是我們問題1中的這些名詞?

無論是靜態區,棧還是堆,它們的越界都是指存儲在這些位置(區域)上的變量出現了越界,那么問題來了,既然都是檢測變量越界,按照不同變量進行分類不就好了,比如字符串越界,數組越界…為什么還會對在堆中存儲的變量單獨處理呢?尤其是對malloc()系列函數,double free,use after free,null dereference等等。因為靜態區和棧的變量存儲空間都是系統編譯器分配和釋放的,而堆中的存儲空間是程序員分配和釋放的,為編寫程序增加靈活的同時,也增添了風險,所以針對堆,就有了不同於棧和靜態區的其他可能的缺陷,也就被提出來另當別論了。

下面我們再來解釋一下緩沖區溢出到底包含多少內容。

計算機程序一般都會使用到一些內存,這些內存或是程序內部使用,或是存放用戶的輸入數據,這樣的內存一般稱作緩沖區。溢出是指盛放的東西超出容器容量而溢出來了,在計算機程序中,就是數據使用到了被分配內存空間之外的內存空間。而緩沖區溢出,簡單的說就是計算機對接收的輸入數據沒有進行有效的檢測(理想的情況是程序檢查數據長度並不允許輸入超過緩沖區長度的字符),向緩沖區內填充數據時超過了緩沖區本身的容量,而導致數據溢出到被分配空間之外的內存空間,使得溢出的數據覆蓋了其他內存空間的數據。

所以從百度百科摘下來的這段話表明,緩沖區溢出可以換一種說法,叫做存儲數據的某一部分內存溢出,而內存的范圍就是我們上面說到的靜態區、棧和堆,也就是說緩沖區溢出包含了溢出問題這一大類,即我們上面所說的靜態區越界,棧越界,堆越界。

明白了以上的理論基礎知識,我們就可以對我們要解決的問題做很好的分類,下面我們簡單看一些常見的緩沖區溢出錯誤,以及堆內存泄露方面的系列缺陷

 1.導致緩沖區溢出的常見 C C++ 錯誤(摘錄)

從根本上講,在程序將數據讀入或復制到緩沖區中的任何時候,它需要在復制之前檢查是否有足夠的空間。能夠容易看出來的異常就不可能會發生 ―― 但是程序通常會隨時間而變更,從而使得不可能成為可能。

遺憾的是,C C++ 附帶的大量危險函數(或普遍使用的庫)甚至連這點(指檢查空間)也無法做到。程序對這些函數的任何使用都是一個警告信號,因為除非慎重地使用它們,否則它們就會成為程序缺陷。您不需要記住這些函數的列表;我的真正目的是說明這個問題是多么普遍。這些函數包括 strcpy(3)strcat(3)sprintf(3) (及其同類 vsprintf(3) )和 gets(3) scanf() 函數集( scanf(3)fscanf(3)sscanf(3)vscanf(3)vsscanf(3) vfscanf(3) )可能會導致問題,因為使用一個沒有定義最大長度的格式是很容易的(當讀取不受信任的輸入時,使用格式“%s”總是一個錯誤)。

其他危險的函數包括 realpath(3)getopt(3)getpass(3)streadd(3)strecpy(3) strtrns(3) 。 從理論上講, snprintf() 應該是相對安全的 ―― 在現代 GNU/Linux 系統中的確是這樣。但是非常老的 UNIX Linux 系統沒有實現 snprintf() 所應該實現的保護機制。

Microsoft 的庫中還有在相應平台上導致同類問題的其他函數(這些函數包括 wcscpy()_tcscpy()_mbscpy()wcscat()_tcscat()_mbscat() CopyMemory() )。注意,如果使用 Microsoft MultiByteToWideChar() 函數,還存在一個常見的危險錯誤 ―― 該函數需要一個最大尺寸作為字符數目,但是程序員經常將該尺寸以字節計(更普遍的需要),結果導致緩沖區溢出缺陷。

另一個問題是 C C++ 對整數具有非常弱的類型檢查,一般不會檢測操作這些整數的問題。由於它們要求程序員手工做所有的問題檢測工作,因此以某種可被利用的方式不正確地操作那些整數是很容易的。特別是,當您需要跟蹤緩沖區長度或讀取某個內容的長度時,通常就是這種情況。但是如果使用一個有符號的值來存儲這個長度值會發生什么情況呢 ―― 攻擊者會使它“成為負值”,然后把該數據解釋為一個實際上很大的正值嗎?當數字值在不同的尺寸之間轉換時,攻擊者會利用這個操作嗎?數值溢出可被利用嗎? 有時處理整數的方式會導致程序缺陷。

所以,我們發現緩沖區溢出大部分時候是由未知長度的字符串造成的,之后的博客中我們會繼續溫習字符串的知識,並總結這些由字符串導致的緩沖區溢出問題。

 2.常見其他內存錯誤及對策

2.1指針沒有指向一塊合法的內存

定義了指針變量,但是沒有為指針分配內存,即指針沒有指向一塊合法的內存。

1)結構體成員指針未初始化

struct student
{
    char *name;
    int score;
}stu, *pstu;

//結構體中的指針成員namw未初始化
//定義結構體變量stu時,為指針變量name分配了4字節的內存,存放一個指向字符的地址
//但並沒有給name初始化。因此name中存放的是亂碼,而這個亂碼在后面會被理解為一個地址
//並在對應的該地址下存儲字符串“Jimy”
int structPMemberTest_UP()
{
    strcpy(stu.name, "Jimy");
    stu.score = 99;
    return 0;
}

很多初學者犯了這個錯誤還不知道是怎么回事。這里定義了結構體變量stu,但是他沒想到這個結構體內部char *name 這成員在定義結構體變量stu 時,只是給name 這個指針變量本身分配了4 個字節。name 指針並沒有指向一個合法的地址,這時候其內部存的只是一些亂碼。所以在調用strcpy 函數時,會將字符串"Jimy"往亂碼所指的內存上拷貝,而這塊內存name 指針根本就無權訪問,導致出錯。解決的辦法是為name 指針malloc 一塊空間。同樣,也有人犯如下錯誤:

//同上面的錯誤是一樣的
//雖然定義結構體時,malloc了內存空間,可那是存儲結構體的
//name內部值仍然是亂碼
int structPMemberTest_P()
{
    pstu = (struct student*)malloc(sizeof(struct student));
    strcpy(pstu->name, "Jimy");
    pstu->score = 99;
    free(pstu);
    return 0;
}

為指針變量pstu 分配了內存,但是同樣沒有給name 指針分配內存。錯誤與上面第一種情況一樣,解決的辦法也一樣。這里用了一個malloc 給人一種錯覺,以為也給name 指針分配了內存。

2)沒有為結構體指針分配足夠的內存

 

//沒有為結構體指針分配足夠的內存
//struct student* 表示定義了一個結構體student 的指針變量,只有4個字節
//當然name指針同樣沒有被分配內存
int enoughSizeForStruct()
{
    pstu = (struct student*)malloc(sizeof(struct student*));
    strcpy(pstu->name, "Jimy");
    pstu->score = 99;
    free(pstu);
    return 0;
}

 

pstu 分配內存的時候,分配的內存大小不合適。這里把sizeof(struct student)誤寫為sizeof(struct student*)。當然name 指針同樣沒有被分配內存。解決辦法同上。

3)函數的入口校驗

不管什么時候,我們使用指針之前一定要確保指針是有效的。

一般在函數入口處使用assert(NULL != p)參數進行校驗。在非參數的地方使用ifNULL != p)來校驗。但這都有一個要求,即p 在定義的同時被初始化為NULL 了。比如上面的例子,即使用ifNULL != p)校驗也起不了作用,因為name 指針並沒有被初始化為NULL,其內部是一個非NULL 的亂碼。

assert 是一個宏,而不是函數,包含在assert.h 頭文件中。原型定義:

#include <assert.h>
void assert( int expression );

如果其后面括號里expression的值為假(即為0),則程序終止運行,並提示出錯;如果后面括號里的值為真,則繼續運行后面的代碼。這個宏只在Debug 版本上起作用,而在Release 版本被編譯器完全優化掉,這樣就不會影響代碼的性能。舉個例子:

//函數入口出的參數校驗,宏assert
//#include <assert.h>     void assert( int expression );  
//assert翻譯成中文,有斷言的意思,就是我保證
//所以使用assert,一般是在十分確定就是這樣的情況下
//參數定義為const的只讀類型 readonly
char* clone_string(const char *source)
{
    char *result = NULL;
    assert(source != NULL);  //如果括號內表達式為假(0),程序停止運行
    result = (char   *)malloc(strlen(source) + 1);
    if (result != NULL)
    {
        strcpy(result, source);
        assert(strcmp(result, source) == 0);
    }
    return   result;
}

2.2為指針分配的內存太小

為指針分配了內存,但是內存大小不夠,導致出現越界錯誤。

2.3內存分配成功,但並未初始化

犯這個錯誤往往是由於沒有初始化的概念或者是以為內存分配好之后其值自然為0。未初始化指針變量也許看起來不那么嚴重,但是它確確實實是個非常嚴重的問題,而且往往出現這種錯誤很難找到原因。所以在定義一個變量時,第一件事就是初始化。你可以把它初始化為一個有效的值,比如:

int i = 10char *p = (char *)malloc(sizeof(char));

但是往往這個時候我們還不確定這個變量的初值,這樣的話可以初始化為0 NULL

int i = 0char *p = NULL;

如果定義的是數組的話,可以這樣初始化:

int a[10] = {0};

或者用memset 函數來初始化為0

memset(a,0,sizeof(a));

memset 函數有三個參數,第一個是要被設置的內存起始地址;第二個參數是要被設置的值;第三個參數是要被設置的內存大小,單位為byte。指針變量如果未被初始化,會導致if 語句或assert 宏校驗失敗。

2.4內存越界

內存分配成功,且已經初始化,但是操作越過了內存的邊界。這種錯誤經常是由於操作數組或指針時出現“多1”或“少1”。

2.5內存泄漏

會產生泄漏的內存就是堆上的內存(這里不討論資源或句柄等泄漏情況),也就是說由malloc 系列函數或new 操作符分配的內存。如果用完之后沒有及時free delete,這塊內存就無法釋放,直到整個程序終止。

1)malloc 函數申請0 字節內存

有一個問題:用malloc 函數申請0 字節內存會返回NULL 指針嗎?

可以測試一下,也可以去查找關於malloc 函數的說明文檔。申請0 字節內存,函數並不返回NULL,而是返回一個正常的內存地址。但是你卻無法使用這塊大小為0 的內存。這就像尺子上的某個刻度,刻度本身並沒有長度,只有某兩個刻度一起才能量出長度。對於這一點一定要小心,因為這時候ifNULL = p)語句校驗將不起作用。

2)double free no free

3)Use after free

既然使用free 函數之后指針變量p 本身保存的地址並沒有改變,那我們就需要重新把p的值變為NULL,否則,在freep)之后,你用ifNULL = p)這樣的校驗語句也毫無作用。例如:

char *p = (char *)malloc(100);
strcpy(p, “hello”);
free(p); /* p 所指的內存被釋放,但是p 所指的地址仍然不變*/
if (NULL != p)
{
    /* 沒有起到防錯作用*/
    strcpy(p, “world”); /* 出錯*/
}

釋放完一塊內存之后,沒有把指針置NULL,這個指針就成為了“野指針”,也有書叫“懸掛指針”。這是很危險的,而且也是經常出錯的地方。所以一定要記住一條:free 完之后,一定要給指針置NULL

三、動態內存分配相關函數及操作符

1.Malloc

http://www.cnblogs.com/wangyuxia/p/6115262.html

2.Free

free() 函數用來釋放動態分配的內存空間,其原型為:

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

【參數說明】ptr 為將要釋放的內存空間的地址。

free() 可以釋放由 malloc()calloc()realloc() 分配的內存空間,以便其他程序再次使用。free() 只能釋放動態分配的內存空間,並不能釋放任意的內存。下面的寫法是錯誤的:

int a[10];
free(a);

如果 ptr 所指向的內存空間不是由上面的三個函數所分配的,或者已被釋放,那么調用 free() 會有無法預知的情況發生。如果 ptr NULL,那么 free() 不會有任何作用。

注意:free() 不會改變 ptr 變量本身的值,調用 free() 后它仍然會指向相同的內存空間,但是此時該內存已無效,不能被使用。所以建議將 ptr 的值設置為 NULL

3. C++ 中的New delete

http://www.cnblogs.com/hazir/p/new_and_delete.html

 

參考文章及書籍

C語言程序設計教程》  李鳳霞   北京理工大學出版社

C語言深度剖析》     陳正沖

https://www.ibm.com/developerworks/cn/linux/l-sp/part4/

http://c.biancheng.net/cpp/html/135.html

http://www.cnblogs.com/hazir/p/new_and_delete.html


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM