對於程序員,通常來講,咱們能夠簡單將內存分爲三個部分:靜態區,棧,堆。html
靜態區:保存自動全局變量和static 變量(包括static 全局和局部變量)。靜態區的內容在整個程序的生命週期內都存在,由編譯器在編譯的時候分配。linux
棧:保存局部變量。棧上的內容只在函數的範圍內存在,當函數運行結束,這些內容也會自動被銷燬。其特色是效率高,但空間大小有限。程序員
堆:由malloc 系列函數或new 操做符分配的內存。其生命週期由free 或delete決定。在沒有釋放以前一直存在,直到程序結束。其特色是使用靈活,空間比較大,但容易出錯。express
以上,緊密相關的一個詞就是「生命週期」,以及變量的分類。因此,下面咱們簡單總結一下變量類型的內容。小程序
變量的生命週期,也稱生存期,是指變量值保留的期限。按照生命週期,可將變量分爲兩類:靜態變量和動態變量。segmentfault
靜態變量:變量存儲在內存中的靜態存儲區,在編譯時就分配了存儲空間,在整個程序運行期間,該變量佔有固定的存儲單元,變量的值都始終存在,程序結束後,這部分空間才釋放。這類變量的生存期爲整個程序。數組
動態變量:變量存儲在內存中的動態存儲區,在程序運行過程中,只有當變量所在函數被調用時,編譯系統才臨時爲該變量分配一段內存單元,該變量纔有值,函數調用結束,變量值當即消失,這部分空間釋放。咱們說這類變量的生存期僅在函數調用期間。安全
C語言中具備靜態存儲性質的變量:外部變量,靜態局部變量和靜態全局變量ide
C語言中具備動態存儲性質的變量:自動變量(auto,默承認以不寫),寄存器變量(register)函數
與上面的物理內存相對,咱們很容易看出,C語言中,靜態變量存儲在靜態區,動態變量存儲在棧,由程序員本身動態分配的變量就存儲在堆。
變量的做用域也稱爲可見性,指變量的有效範圍,可分爲局部和全局兩種。
局部變量:在一個函數或複合語句內定義的變量是局部變量,局部變量僅在定義它的函數或複合語句內有效。
全局變量:定義在全部函數以外的變量是全局變量,做用範圍是從定義開始,到本文件或程序結束。
C語言中自動變量、寄存器變量和內部靜態變量都屬於局部變量;外部變量是程序級的全局變量,外部靜態變量是源文件級的全局變量。
從上面咱們看到,C語言中的變量有:自動變量、寄存器變量、外部變量、內部靜態變量和外部靜態變量。下面分別就這幾個變量進行簡單說明。
auto:編譯器在默認的缺省狀況下,全部變量都是auto 的。
auto int a 等價於 int a
register:這個關鍵字請求編譯器儘量的將變量存在CPU 內部寄存器中,而不是內存中。由於若是一個變量在程序中頻繁使用,如循環控制變量,大量訪問內存就會影響程序的執行效率。注意是儘量,不是絕對。一個CPU 的寄存器也就那麼幾個或幾十個,若是在一個函數中定義的register 變量多於CPU中的寄存器數量,C編譯程序會自動將寄存器變量轉爲自動變量。
使用register 修飾符的注意點
1)因爲受硬件寄存器長度的限制,素以寄存器變量只能是char,int或指針型,只能用於說明函數中變量或函數中的形參。
2)因爲register變量使用的是CPU中的寄存器,寄存器變量無地址,因此不能用取址運算符「&」來獲取register變量的地址。
外部變量的說明通常形式是:
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,外部文件的加上,這樣能夠給讀源碼的人一個hint。p.s. stdio.h裏的函數聲明都是有extern的。
靜態變量有兩種:外部靜態變量和內部靜態變量
外部靜態變量是全局變量,但做用域僅僅在定義它的那個源文件中,出了該源文件無論是否用extern說明都是不可見的。簡單而言,外部靜態變量僅僅做用於定義它的一個源文件,而外部變量做用於整個程序。
內部靜態變量與自動變量有類似之處。內部靜態變量也是侷限於一個特定的函數內部,出了定義它的函數,即便對於同一個文件中的其餘函數也是不可見的。但它不像自動變量那樣,僅當定義自動變量的函數被調用時才存在,退出函數調用就消失。內部靜態變量是始終存在的,當函數被調用退出後,內部靜態變量會保存數值,再次調用該函數時,之前調用時的數值仍然保留着。
咱們已經清楚C語言中各種變量的存儲屬性,以及對應存儲在計算機中的什麼區域,那麼回答下面幾個問題也垂手可得了。
1.什麼是靜態區越界,什麼是棧越界,什麼是堆越界?
2.爲何在靜態或動態檢測中咱們常聽到的是數組越界,緩衝區溢出,內存泄露,而不是咱們問題1中的這些名詞?
不管是靜態區,棧仍是堆,它們的越界都是指存儲在這些位置(區域)上的變量出現了越界,那麼問題來了,既然都是檢測變量越界,按照不一樣變量進行分類不就行了,好比字符串越界,數組越界…爲何還會對在堆中存儲的變量單獨處理呢?尤爲是對malloc()系列函數,double free,use after free,null dereference等等。由於靜態區和棧的變量存儲空間都是系統編譯器分配和釋放的,而堆中的存儲空間是程序員分配和釋放的,爲編寫程序增長靈活的同時,也增添了風險,因此針對堆,就有了不一樣於棧和靜態區的其餘可能的缺陷,也就被提出來另當別論了。
下面咱們再來解釋一下緩衝區溢出到底包含多少內容。
計算機程序通常都會使用到一些內存,這些內存或是程序內部使用,或是存放用戶的輸入數據,這樣的內存通常稱做緩衝區。溢出是指盛放的東西超出容器容量而溢出來了,在計算機程序中,就是數據使用到了被分配內存空間以外的內存空間。而緩衝區溢出,簡單的說就是計算機對接收的輸入數據沒有進行有效的檢測(理想的狀況是程序檢查數據長度並不容許輸入超過緩衝區長度的字符),向緩衝區內填充數據時超過了緩衝區自己的容量,而致使數據溢出到被分配空間以外的內存空間,使得溢出的數據覆蓋了其餘內存空間的數據。
因此從百度百科摘下來的這段話代表,緩衝區溢出能夠換一種說法,叫作存儲數據的某一部份內存溢出,而內存的範圍就是咱們上面說到的靜態區、棧和堆,也就是說緩衝區溢出包含了溢出問題這一大類,即咱們上面所說的靜態區越界,棧越界,堆越界。
明白了以上的理論基礎知識,咱們就能夠對咱們要解決的問題作很好的分類,下面咱們簡單看一些常見的緩衝區溢出錯誤,以及堆內存泄露方面的系列缺陷
從根本上講,在程序將數據讀入或複製到緩衝區中的任什麼時候候,它須要在複製以前檢查是否有足夠的空間。可以容易看出來的異常就不可能會發生 ―― 可是程序一般會隨時間而變動,從而使得不可能成爲可能。
遺憾的是,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++ 對整數具備很是弱的類型檢查,通常不會檢測操做這些整數的問題。因爲它們要求程序員手工作全部的問題檢測工做,所以以某種可被利用的方式不正確地操做那些整數是很容易的。特別是,當您須要跟蹤緩衝區長度或讀取某個內容的長度時,一般就是這種狀況。可是若是使用一個有符號的值來存儲這個長度值會發生什麼狀況呢 ―― 攻擊者會使它「成爲負值」,而後把該數據解釋爲一個實際上很大的正值嗎?當數字值在不一樣的尺寸之間轉換時,攻擊者會利用這個操做嗎?數值溢出可被利用嗎? 有時處理整數的方式會致使程序缺陷。
因此,咱們發現緩衝區溢出大部分時候是由未知長度的字符串形成的,以後的博客中咱們會繼續溫習字符串的知識,並總結這些由字符串致使的緩衝區溢出問題。
定義了指針變量,可是沒有爲指針分配內存,即指針沒有指向一塊合法的內存。
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 指針分配了內存。
//沒有爲結構體指針分配足夠的內存 //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 指針一樣沒有被分配內存。解決辦法同上。
無論何時,咱們使用指針以前必定要確保指針是有效的。
通常在函數入口處使用assert(NULL != p)對參數進行校驗。在非參數的地方使用if(NULL != p)來校驗。但這都有一個要求,即p 在定義的同時被初始化爲NULL 了。好比上面的例子,即便用if(NULL != 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; }
爲指針分配了內存,可是內存大小不夠,致使出現越界錯誤。
犯這個錯誤每每是因爲沒有初始化的概念或者是覺得內存分配好以後其值天然爲0。未初始化指針變量也許看起來不那麼嚴重,可是它確確實實是個很是嚴重的問題,並且每每出現這種錯誤很難找到緣由。因此在定義一個變量時,第一件事就是初始化。你能夠把它初始化爲一個有效的值,好比:
int i = 10; char *p = (char *)malloc(sizeof(char));
可是每每這個時候咱們還不肯定這個變量的初值,這樣的話能夠初始化爲0 或NULL。
int i = 0; char *p = NULL;
若是定義的是數組的話,能夠這樣初始化:
int a[10] = {0};
或者用memset 函數來初始化爲0:
memset(a,0,sizeof(a));
memset 函數有三個參數,第一個是要被設置的內存起始地址;第二個參數是要被設置的值;第三個參數是要被設置的內存大小,單位爲byte。指針變量若是未被初始化,會致使if 語句或assert 宏校驗失敗。
內存分配成功,且已經初始化,可是操做越過了內存的邊界。這種錯誤常常是因爲操做數組或指針時出現「多1」或「少1」。
會產生泄漏的內存就是堆上的內存(這裏不討論資源或句柄等泄漏狀況),也就是說由malloc 系列函數或new 操做符分配的內存。若是用完以後沒有及時free 或delete,這塊內存就沒法釋放,直到整個程序終止。
有一個問題:用malloc 函數申請0 字節內存會返回NULL 指針嗎?
能夠測試一下,也能夠去查找關於malloc 函數的說明文檔。申請0 字節內存,函數並不返回NULL,而是返回一個正常的內存地址。可是你卻沒法使用這塊大小爲0 的內存。這就像尺子上的某個刻度,刻度自己並無長度,只有某兩個刻度一塊兒才能量出長度。對於這一點必定要當心,由於這時候if(NULL != p)語句校驗將不起做用。
既然使用free 函數以後指針變量p 自己保存的地址並無改變,那咱們就須要從新把p的值變爲NULL,不然,在free(p)以後,你用if(NULL != p)這樣的校驗語句也毫無做用。例如:
char *p = (char *)malloc(100); strcpy(p, 「hello」); free(p); /* p 所指的內存被釋放,可是p 所指的地址仍然不變*/ if (NULL != p) { /* 沒有起到防錯做用*/ strcpy(p, 「world」); /* 出錯*/ }
釋放完一塊內存以後,沒有把指針置NULL,這個指針就成爲了「野指針」,也有書叫「懸掛指針」。這是很危險的,並且也是常常出錯的地方。因此必定要記住一條:free 完以後,必定要給指針置NULL。
3、動態內存分配相關函數及操做符
http://www.cnblogs.com/wangyuxia/p/6115262.html
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,
http://www.cnblogs.com/hazir/p/new_and_delete.html
參考文章及書籍
《C語言程序設計教程》 李鳳霞 北京理工大學出版社
《C語言深度剖析》 陳正衝
https://www.ibm.com/developerworks/cn/linux/l-sp/part4/