天天看點

【高品質代碼】如何寫出更高品質的C/C++代碼(1):記憶體管理1、C/C++程式運作時記憶體結構簡介2、記憶體的配置設定方式3、記憶體管理中常見錯誤4、指針和數組的對比5、指針作為函數的參數6、“野指針”導緻的問題7、指針變量同動态記憶體的關系8、malloc/free與new/delete9、記憶體申請失敗

記憶體的管理是C/C++開發程式過程中的一個比較麻煩的問題。對于經驗不是足夠豐富的程式員來說,開發比較複雜的程式的時候幾乎肯定會遇到記憶體管理方面的bug。對C/C++語言以及編譯機制深入的了解和養成良好的程式設計習慣可以盡量減少這類bug産生的幾率。

一個典型的C/C++編譯的程序所占用的記憶體空間通常分為5個部分,由低位址到高位址分别為:

代碼段(Code/Text Segment):儲存可執行程式運作的二進制代碼段。

資料段(Data Segment):儲存程序已經初始化的全局變量0。

BSS段(BSS Segment):儲存已經聲明,但尚未初始化的全局變量,程序開始後這部分資料初始化為0;靜态變量也視為全局變。

堆(Heap):儲存程序動态配置設定的記憶體資料。C中使用malloc/calloc/free配置設定、釋放,C++中使用new/delete配置設定、釋放。如果不釋放,程序會始終儲存這些資料,直到程序退出。

棧(Stack):主要用于處理函數調用過程中的資料,主要有函數的參數、臨時變量和傳回位址等。在棧中儲存的資料在生命周期結束後會自行釋放。棧空間和堆空間按照實際情況确定大小,沒有指定的數值。

通常在程式設計中常用到的記憶體配置設定方式有三種:

(1)從靜态存儲區配置設定,在程式編譯的時候就已經确定。如全局變量、static變量等。

(2)從棧空間分布,如函數内部的局部變量。此類資料配置設定效率很高但容量有限。

(3)從堆空間動态配置設定,在程式運作時由程式員決定申請多少記憶體并負責釋放。這類資料出問題的可能性最高。

實際程式設計中導緻記憶體錯誤的情況通常發生于處理堆空間的資料時。主要有以下幾種:

(1)使用了配置設定失敗的記憶體空間。程式中申請記憶體可能會因為不同原因而失敗,而使用申請空間失敗的記憶體位址将會導緻程序崩潰。通常,在使用記憶體空間之前判斷指針是否為0可以避免類似問題。如果指向一段記憶體的指針作為函數的參數,那麼可以在函數的入口處使用assert(p!=NULL)

處理,如果p指針為空則會傳回錯誤。如果一段記憶體通過malloc/new擷取,那麼使用if(p==null)或if(p=!null)進行預防處理。

(2)配置設定成功,但是使用了未經過初始化的記憶體。此時程序可能不會崩潰,但是會導緻資料引用錯誤。是以,建立數組等結構之後,應第一時間進行初始化,哪怕全部設為0。

(3)空間配置設定、初始化成功,但是讀寫越界。此類問題在使用for循環處理數組時經常出現,比如以下代碼:

問題經常就出現在for循環中究竟是<還是<=。在該使用<的地方使用了<=,就會導緻最後一次循環時記憶體讀寫越界。

(4)記憶體洩露。在堆空間中手動配置設定的記憶體沒有釋放,這部分記憶體在程序退出之前就會一直存在,如果這部分的代碼循環執行,那麼很有可能出現系統的記憶體被耗盡的情況。3和4是新手程式員犯錯誤比較多的部分,隻能在開發時多多留神。

(5)使用了釋放的記憶體。通常會導緻這種情況的有:①函數傳回了指向棧記憶體的指針或引用到上層,這會導緻上層使用該函數傳回的指針/引用時發現指針無效,因為我們想要的資料已經随着函數調用結束而銷毀了;②對于一些全局指針,在free或delete之後,沒有設為null,産生了“野指針”。在後面繼續使用該指針時,通過if(p==null)判斷的方法便失效了。解決方法是注意傳回指針或引用時,應傳回指向全局資料的指針和引用,在釋放記憶體之後第一時間将指針設為null。

在C/C++中,經常可以使用指針和數組達到相同的目的,是以時常會産生這樣的疑惑:即二者是否是等價的?實際上二者由其差別,主要在于:數組在棧空間或者靜态存儲區建立,數組名對應某一塊指定的記憶體區且不可以指向其他記憶體區,一旦建立之後,數組的位址和容量在生命周期内固定,隻可能改變數組的内容;而指針則沒有此限制,可以指向任意類型的位址,是以經常用指針來操作堆空間的動态記憶體。下面的程式可以反映二者的差別:

在該段程式中,a表示一個數組,這個數組有獨立的記憶體空間并初始化為"hello",是以其内部元素的值可以改變;而指針p指向的是儲存在常量文本區的字元串本身,并沒有進行一次資料拷貝,是以試圖修改常量文本的操作會在運作時導緻程式崩潰。

二者另一個差別在于使用sizeof運算符計算記憶體大小時的結果。對于一個初始化過的數組,使用sizeof運算符得到的是數組的大小;而對于一個指針變量,使用sizeof運算符得到的是指針變量的大小(一般為4),與指向的記憶體資料大小無關。如以下程式所示:

需要注意的一點是,當數組名作為函數的參數傳遞時,當做指針變量處理。

一個很重要的原則是:不要使用作為參數的指針去申請記憶體。因為在函數執行時,形參會被重新配置設定一個與原來不同的指針變量,如果使用這個指針去申請記憶體,那麼不但調用上層不能得到記憶體空間,函數内部申請到的記憶體也會因為位址指針丢失無法釋放而造成記憶體洩露。解決此類問題可以通過傳遞“指向指針的指針”或者将記憶體位址通過return傳回的方法。需要注意的一點是,不要return棧記憶體空間,因别這部分空間在函數傳回時将消亡。

free和delete将釋放參數所指向的記憶體位址,但是并沒有将指向該位址的指針置0,記憶體被釋放後,辨別該記憶體的位址指針變量的值并未改變。此時這個指針值是合法的,但是指向的内容卻是非法的,這就造成了“野指針”的産生。如果這個指針變量再次被使用,那麼if(p==null)的合法性判斷将會失效。

造成“野指針”産生還有兩種可能:剛剛定義的指針變量沒有被初始化,局部指針變量在剛剛建立時不會設為NULL而是一個随機值;指針變量的生命周期合法,但是指向的對象已消亡,這是指向該對象的指針也将成為野指針。為了杜絕這類情況,需要注意遵守以下原則:

每一個記憶體區域被釋放後,第一時間将指向該區域的指針指派為NULL。

定義一個指針變量是,或者賦給初值NULL,或者直接令其指向一段合法申請到的記憶體區。

盡量将指針變量定義在與目标對象/記憶體一直的聲明周期,讓其“同生共死”。

C/C++語言其實并不聰明,很多時候并不能了解我們程式設計時的想法。比如,函數的局部變量在函數結束時消亡,但是在内部申請的記憶體卻不會因為指針變量的消亡而被釋放。還有,我們把一段記憶體區釋放,那指向該記憶體的指針變量依然保持原有值,變成了“野指針”。是以,實際上,這二者并沒有直接的聯系,程式設計時一定要分别處理。

malloc和free是C語言的标準庫函數,new和delete是C++的運算符,二者都能實作記憶體的動态申請和釋放。而二者的差別也正反映了兩種語言的設計差異:C是更加面向過程的語言,C++則是面向對象的語言,new和delete除了釋放記憶體之外,更多地考慮了面向對象的一些特性。

C++與C的最本質差別之一在于C++定義了類這一概念,并且對對象的産生和消亡定義了構造函數和析構函數,是以new/delete在生成和釋放對象時,對調用對象的構造和析構函數進行一些該類的個性化的操作,這是malloc/free力所不能及的。

是以,需要遵照的原則是:為了一個C++對象申請動态記憶體是,一定要使用new,釋放是也一定要用delete,否則會因為構造和析構函數沒被調用而産生錯誤。

對于free函數,如果p為NULL,那麼可以多次調用free(p),但是如果p不是NULL,那麼連續兩次進行free就會使得程式崩潰。這也給了我們另一個理由在釋放記憶體之後馬上将指針設為NULL。

通常記憶體申請失敗時,将會傳回NULL給指針變量,此時應判斷指針是否為NULL,如果的确申請失敗,則應該傳回錯誤,或者直接使用exit(n)來結束程序。

繼續閱讀