深入理解C語言內存管理

之前在學Java的時候對於Java虛擬機中的內存分佈有一定的瞭解,但是最近在看一些C,發現居然自己對於C語言的內存分配了解的太少。

問題不能拖,我這就來學習一下吧,爭取一次搞定。 在任何程序設計環境及語言中,內存管理都十分重要。

內存管理的基本概念

分析C語言內存的分佈先從Linux下可執行的C程序入手。現在有一個簡單的C源程序hello.c

複製代碼
1 #include <stdio.h>
2 #include <stdlib.h>
3 int var1 = 1;
4 
5 int main(void) {
6   int var2 = 2;
7   printf("hello, world!\n");
8   exit(0);
9 }
複製代碼

經過gcc hello.c進行編譯之後得到了名爲a.out的可執行文件

[tuhooo@localhost leet_code]$ ls -al a.out
-rwxrwxr-x. 1 tuhooo tuhooo 8592 Jul 22 20:40 a.out

ls命令是查看文件的元數據信息

[tuhooo@localhost leet_code]$ file a.out
a.out: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), dynamically linked (uses shared libs), for GNU/Linux 2.6.32, BuildID[sha1]=23c58f2cad39d8b15b91f0cc8129055833372afe, not stripped

file命令用來識別文件類型,也可用來辨別一些文件的編碼格式。

它是通過查看文件的頭部信息來獲取文件類型,而不是像Windows通過擴展名來確定文件類型的。

[tuhooo@localhost leet_code]$ size a.out

text  data bss dec hex filename
(代碼區靜態數據) (全局初始化靜態數據) (未初始化數據區)  (十進制總和) (十六制總和) (文件名)
1301 560 8 1869 74d a.out

顯示一個目標文件或者鏈接庫文件中的目標文件的各個段的大小,當沒有輸入文件名時,默認爲a.out。

size:支持的目標: elf32-i386 a.out-i386-linux efi-app-ia32 elf32-little elf32-big srec symbolsrec tekhex binary ihex trad-core。

那啥,可執行文件在存儲(也就是還沒有載入到內存中)的時候,分爲:代碼區數據區未初始化數據區3個部分。

進一步解讀

(1)代碼區(text segment)。存放CPU執行的機器指令(machine instructions)。通常,代碼區是可共享的(即另外的執行程序可以調用它),因爲對於頻繁被執行的程序,只需要在內存中有一份代碼即可。代碼區通常是隻讀的,使其只讀的原因是防止程序意外地修改了它的指令。另外,代碼區還規劃了局部變量的相關信息。

(2)全局初始化數據區/靜態數據區(initialized data segment/data segment)。該區包含了在程序中明確被初始化的全局變量、靜態變量(包括全局靜態變量和局部靜態變量)和常量數據(如字符串常量)。例如,一個不在任何函數內的聲明(全局數據):

1 int maxcount = 99;

使得變量maxcount根據其初始值被存儲到初始化數據區中。

1 static mincount = 100; 

這聲明瞭一個靜態數據,如果是在任何函數體外聲明,則表示其爲一個全局靜態變量,如果在函數體內(局部),則表示其爲一個局部靜態變量。另外,如果在函數名前加上static,則表示此函數只能在當前文件中被調用。

(3)未初始化數據區。亦稱BSS區(uninitialized data segment),存入的是全局未初始化變量。BSS這個叫法是根據一個早期的彙編運算符而來,這個彙編運算符標誌着一個塊的開始。BSS區的數據在程序開始執行之前被內核初始化爲0或者空指針(NULL)。例如一個不在任何函數內的聲明:

1 long sum[1000];

將變量sum存儲到未初始化數據區。

下圖所示爲可執行代碼存儲時結構和運行時結構的對照圖。一個正在運行着的C編譯程序佔用的內存分爲代碼區、初始化數據區、未初始化數據區、堆區和棧區5個部分。

 

再來看一張圖,多個一個命令行參數區:

1)代碼區(text segment)。代碼區指令根據程序設計流程依次執行,對於順序指令,則只會執行一次(每個進程),如果反覆,則需要使用跳轉指令,如果進行遞歸,則需要藉助棧來實現。代碼段: 代碼段(code segment/text segment )通常是指用來存放程序執行代碼的一塊內存區域。這部分區域的大小在程序運行前就已經確定,並且內存區域通常屬於只讀, 某些架構也允許代碼段爲可寫,即允許修改程序。在代碼段中,也有可能包含一些只讀的常數變量,例如字符串常量等。代碼區的指令中包括操作碼和要操作的對象(或對象地址引用)。如果是立即數(即具體的數值,如5),將直接包含在代碼中;如果是局部數據,將在棧區分配空間,然後引用該數據地址;如果是BSS區和數據區,在代碼中同樣將引用該數據地址。另外,代碼段還規劃了局部數據所申請的內存空間信息。

2)全局初始化數據區/靜態數據區(Data Segment)。只初始化一次。數據段: 數據段(data segment )通常是指用來存放程序中已初始化的全局變量的一塊內存區域。數據段屬於靜態內存分配。data段中的靜態數據區存放的是程序中已初始化的全局變量、靜態變量和常量。

3)未初始化數據區(BSS)。在運行時改變其值。BSS 段: BSS 段(bss segment )通常是指用來存放程序中未初始化的全局變量的一塊內存區域。BSS 是英文Block Started by Symbol 的簡稱。BSS 段屬於靜態內存分配,即程序一開始就將其清零了。一般在初始化時BSS段部分將會清零。

4)棧區(stack)。由編譯器自動分配釋放,存放函數的參數值、局部變量的值等。存放函數的參數值、局部變量的值,以及在進行任務切換時存放當前任務的上下文內容。其操作方式類似於數據結構中的棧。每當一個函數被調用,該函數返回地址和一些關於調用的信息,比如某些寄存器的內容,被存儲到棧區。然後這個被調用的函數再爲它的自動變量和臨時變量在棧區上分配空間,這就是C實現函數遞歸調用的方法。每執行一次遞歸函數調用,一個新的棧框架就會被使用,這樣這個新實例棧裏的變量就不會和該函數的另一個實例棧裏面的變量混淆。棧(stack) :棧又稱堆棧, 是用戶存放程序臨時創建的局部變量,也就是說我們函數括弧"{}"中定義的變量(但不包括static 聲明的變量,static 意味着在數據段中存放變量)。除此以外,在函數被調用時,其參數也會被壓入發起調用的進程棧中,並且待到調用結束後,函數的返回值也會被存放回棧中。由於棧的先進先出特點,所以棧特別方便用來保存/ 恢復調用現場。從這個意義上講,我們可以把堆棧看成一個寄存、交換臨時數據的內存區。

5)堆區(heap)。用於動態內存分配。堆在內存中位於bss區和棧區之間。一般由程序員分配和釋放,若程序員不釋放,程序結束時有可能由OS回收。堆(heap): 堆是用於存放進程運行中被動態分配的內存段,它的大小並不固定,可動態擴張或縮減。當進程調用malloc 等函數分配內存時,新分配的內存就被動態添加到堆上(堆被擴張);當利用free 等函數釋放內存時,被釋放的內存從堆中被剔除(堆被縮減)。在將應用程序加載到內存空間執行時,操作系統負責代碼段、數據段和BSS段的加載,並將在內存中爲這些段分配空間。棧段亦由操作系統分配和管理,而不需要程序員顯示地管理;堆段由程序員自己管理,即顯式地申請和釋放空間。

另外,可執行程序在運行時具有相應的程序屬性。在有操作系統支持時,這些屬性頁由操作系統管理和維護。

C語言程序編譯完成之後,已初始化的全局變量保存在DATA段中,未初始化的全局變量保存在BSS段中。TEXT和DATA段都在可執行文件中,由系統從可執行文件中加載;而BSS段不在可執行文件中,由系統初始化。BSS段只保存沒有值的變量,所以事實上它並不需要保存這些變量的映像。運行時所需要的BSS段大小記錄在目標文件中,但是BSS段並不佔據目標文件的任何空間。

以上兩圖來自於《C語言專家編程》。

在操作系統中,一個進程就是處於執行期的程序(當然包括系統資源),實際上正在執行的程序代碼的活標本。那麼進程的邏輯地址空間是如何劃分的呢?

 

左邊的是UNIX/LINUX系統的執行文件,右邊是對應進程邏輯地址空間的劃分情況。

首先是堆棧區(stack),堆棧是由編譯器自動分配釋放,存放函數的參數值,局部變量的值等。其操作方式類似於數據結構中的棧。棧的申請是由系統自動分配,如在函數內部申請一個局部變量 int h,同時判別所申請空間是否小於棧的剩餘空間,如若小於的話,在堆棧中爲其開闢空間,爲程序提供內存,否則將報異常提示棧溢出。  

其次是堆(heap),堆一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收 。注意它與數據結構中的堆是兩回事,分配方式倒是類似於鏈表。堆的申請是由程序員自己來操作的,在C中使用malloc函數,而C++中使用new運算符,但是堆的申請過程比較複雜:當系統收到程序的申請時,會遍歷記錄空閒內存地址的鏈表,以求尋找第一個空間大於所申請空間的堆結點,然後將該結點從空閒 結點鏈表中刪除,並將該結點的空間分配給程序,此處應該注意的是有些情況下,新申請的內存塊的首地址記錄本次分配的內存塊大小,這樣在delete尤其是 delete[]時就能正確的釋放內存空間。

接着是全局數據區(靜態區) (static),全局變量和靜態變量的存儲是放在一塊的初始化的全局變量和靜態變量在一塊區域, 未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域。 另外文字常量區,常量字符串就是放在這裏,程序結束後有系統釋放

最後是程序代碼區,放着函數體的二進制代碼。

爲什麼要這麼分配內存?

(1)一個進程在運行過程中,代碼是根據流程依次執行的,只需要訪問一次,當然跳轉和遞歸有可能使代碼執行多次,而數據一般都需要訪問多次,因此單獨開闢空間以方便訪問和節約空間。

(2)臨時數據及需要再次使用的代碼在運行時放入棧區中,生命週期短。

(3)全局數據和靜態數據有可能在整個程序執行過程中都需要訪問,因此單獨存儲管理。

(4)堆區由用戶自由分配,以便管理。

舉例說明內存分佈情況

複製代碼
 1 /* memory_allocate.c用於演示內存分佈情況 */
 2 
 3 int a = 0;                      /* a在全局已初始化數據區 */
 4 char *p1;                       /* p1在BSS區(未初始化全局變量) */
 5 
 6 int main(void) {
 7   int b;                        /* b在棧區 */
 8   char s[] = "abc";             /* s爲數組變量, 存儲在棧區 */
 9   /* "abc"爲字符串常量, 存儲在已初始化數據區 */
10   char *p1, p2;                 /* p1、p2在棧區 */
11   char *p3 = "123456";          /* "123456\0"已初始化在數據區, p3在棧區 */
12   static int c = 0;             /* c爲全局(靜態)數據, 存在於已初始化數據區 */
13   /* 另外, 靜態數據會自動初始化 */
14   p1 = (char *)malloc(10);      /* 分配的10個字節的區域存在於堆區 */
15   p2 = (char *)malloc(20);      /* 分配得來的20個字節的區域存在於堆區 */
16   
17   free(p1);
18   free(p2);
19 }
複製代碼

內存的分配方式

在C語言中,對象可以使用靜態或動態的方式分配內存空間。

靜態分配:編譯器在處理程序源代碼時分配。

動態分配:程序在執行時調用malloc庫函數申請分配。

靜態內存分配是在程序執行之前進行的因而效率比較高,而動態內存分配則可以靈活的處理未知數目的。

靜態與動態內存分配的主要區別如下:

靜態對象是有名字的變量,可以直接對其進行操作;動態對象是沒有名字的一段地址,需要通過指針間接地對它進行操作。

靜態對象的分配與釋放由編譯器自動處理;動態對象的分配與釋放必須由程序員顯式地管理,它通過malloc()和free兩個函數來完成。

以下是採用靜態分配方式的例子。

1 int a = 100;

此行代碼指示編譯器分配足夠的存儲區以存放一個整型值,該存儲區與名字a相關聯,並用數值100初始化該存儲區。

以下是採用動態分配方式的例子:

1 p1 = (char *)malloc(10*sizeof(int));

此行代碼分配了10個int類型的對象,然後返回對象在內存中的地址,接着這個地址被用來初始化指針對象p1,對於動態分配的內存唯一的訪問方式是通過指針間接地訪問,其釋放方法爲:

1 free(p1);

棧和堆的區別

前面已經介紹過,棧是由編譯器在需要時分配的,不需要時自動清除的變量存儲區。裏面的變量通常是局部變量、函數參數等。堆是由malloc()函數分配的內存塊,內存釋放由程序員手動控制,在C語言爲free函數完成。棧和堆的主要區別有以下幾點:

(1)管理方式不同。

棧編譯器自動管理,無需程序員手工控制;而堆空間的申請釋放工作由程序員控制,容易產生內存泄漏。對於棧來講,是由編譯器自動管理,無需我們手工控制;對於堆來說,釋放工作由程序員控制,容易產生memory leak。空間大小:一般來講在32位系統下,堆內存可以達到4G的空間,從這個角度來看堆內存幾乎是沒有什麼限制的。但是對於棧來講,一般都是有一定的空間大小的,例如,在VC6下面,默認的棧空間大小是1M。當然,這個值可以修改。碎片問題:對於堆來講,頻繁的new/delete勢必會造成內存空間的不連續,從而造成大量的碎片,使程序效率降低。對於棧來講,則不會存在這個問 題,因爲棧是先進後出的隊列,他們是如此的一一對應,以至於永遠都不可能有一個內存塊從棧中間彈出,在它彈出之前,在它上面的後進的棧內容已經被彈出,詳細的可以參考數據結構。生長方向:對於堆來講,生長方向是向上的,也就是向着內存地址增加的方向;對於棧來講,它的生長方向是向下的,是向着內存地址減小的方向增長。分配方式:堆都是動態分配的,沒有靜態分配的堆。棧有2種分配方式:靜態分配和動態分配。靜態分配是編譯器完成的, 比如局部變量的分配。動態分配由malloca函數進行分配,但是棧的動態分配和堆是不同的,它的動態分配是由編譯器進行釋放,無需我們手工實現。分配效率:棧是機器系統提供的數據結構,計算機會在底層對棧提供支持:分配專門的寄存器存放棧的地址,壓棧出棧都有專門的指令執行,這就決定了棧的效率比較高。堆則是C/C++函數庫提供的,它的機制是很複雜的,例如爲了分配一塊內存,庫函數會按照一定的算法(具體的算法可以參考數據結構/操作系統)在堆內存中搜索可用的足夠大小的空間,如果沒有足夠大小的空間(可能是由於內存碎片太多),就有可能調用系統功能去增加程序數據段的內存空間,這樣就有機會分到足夠大小的內存,然後進行返回。顯然,堆的效率比棧要低得多。從這裏我們可以看到,堆和棧相比,由於大量new/delete的使用,容易造成大量的內存碎片;由於沒有專門的系統支持,效率很低;由於可能引發用戶態和核心態的切換,內存的申請,代價變得更加昂貴。所以棧在程序中是應用最廣泛的,就算是函數的調用也利用棧去完成,函數調用過程中的參數,返回地址, EBP和局部變量都採用棧的方式存放。所以,我們推薦大家儘量用棧,而不是用堆。雖然棧有如此衆多的好處,但是由於和堆相比不是那麼靈活,有時候分配大量的內存空間,還是用堆好一些。無論是堆還是棧,都要防止越界現象的發生(除非你是故意使其越界),因爲越界的結果要麼是程序崩潰,要麼是摧毀程序的堆、棧結構,產生以想不到的結果。

(2)空間大小不同。

棧是向低地址擴展的數據結構,是一塊連續的內存區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,當申請的空間超過棧的剩餘空間時,將提示溢出。因此,用戶能從棧獲得的空間較小。

堆是向高地址擴展的數據結構,是不連續的內存區域。因爲系統是用鏈表來存儲空閒內存地址的,且鏈表的遍歷方向是由低地址向高地址。由此可見,堆獲得的空間較靈活,也較大。棧中元素都是一一對應的,不會存在一個內存塊從棧中間彈出的情況。

在Windows下,棧是向低地址擴展的數據結構,是一塊連續的內存的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在 WINDOWS下,棧的大小是2M(也有的說是1M,總之是一個編譯時就確定的常數),如果申請的空間超過棧的剩餘空間時,將提示overflow。因此,能從棧獲得的空間較小。堆:堆是向高地址擴展的數據結構,是不連續的內存區域。這是由於系統是用鏈表來存儲的空閒內存地址的,自然是不連續的,而鏈表的遍歷方向是由低地址向高地址。堆的大小受限於計算機系統中有效的虛擬內存。由此可見,堆獲得的空間比較靈活,也比較大。

(3)是否產生碎片。

對於堆來講,頻繁的malloc/free(new/delete)勢必會造成內存空間的不連續,從而造成大量的碎片,使程序效率降低(雖然程序在退出後操作系統會對內存進行回收管理)。對於棧來講,則不會存在這個問題。

(4)增長方向不同。

堆的增長方向是向上的,即向着內存地址增加的方向;棧的增長方向是向下的,即向着內存地址減小的方向。

(5)分配方式不同。

堆都是程序中由malloc()函數動態申請分配並由free()函數釋放的;棧的分配和釋放是由編譯器完成的,棧的動態分配由alloca()函數完成,但是棧的動態分配和堆是不同的,他的動態分配是由編譯器進行申請和釋放的,無需手工實現。

STACK: 由系統自動分配。例如,聲明在函數中一個局部變量 int b;系統自動在棧中爲b開闢空間。HEAP:需要程序員自己申請,並指明大小,在C中malloc函數。指向堆中分配內存的指針則可能是存放在棧中的。

(6)分配效率不同。

棧是機器系統提供的數據結構,計算機會在底層對棧提供支持:分配專門的寄存器存放棧的地址,壓棧出棧都有專門的指令執行。堆則是C函數庫提供的,它的機制很複雜,例如爲了分配一塊內存,庫函數會按照一定的算法(具體的算法可以參考數據結構/操作系統)在堆內存中搜索可用的足夠大的空間,如果沒有足夠大的空間(可能是由於內存碎片太多),就有需要操作系統來重新整理內存空間,這樣就有機會分到足夠大小的內存,然後返回。顯然,堆的效率比棧要低得多。

棧由系統自動分配,速度較快。但程序員是無法控制的。

堆是由new分配的內存,一般速度比較慢,而且容易產生內存碎片,不過用起來最方便。

(7)申請後系統的響應

棧:只要棧的剩餘空間大於所申請空間,系統將爲程序提供內存,否則將報異常提示棧溢出。

堆:首先應該知道操作系統有一個記錄空閒內存地址的鏈表,當系統收到程序的申請時,會遍歷該鏈表,尋找第一個空間大於所申請空間的堆結點,然後將該結點從空閒結點鏈表中刪除,並將該結點的空間分配給程序。對於大多數系統,會在這塊內存空間中的首地址處記錄本次分配的大小,這樣,代碼中的delete語句才能正確的釋放本內存空間。由於找到的堆結點的大小不一定正好等於申請的大小,系統會自動的將多餘的那部分重新放入空閒鏈表中。

(8)堆和棧中的存儲內容

棧:在函數調用時,第一個進棧的是主函數中後的下一條指令(函數調用語句的下一條可執行語句)的地址,然後是函數的各個參數,在大多數的C編譯器中,參數是由右往左入棧的,然後是函數中的局部變量。注意靜態變量是不入棧的。當本次函數調用結束後,局部變量先出棧,然後是參數,最後棧頂指針指向最開始存的地址,也就是主函數中的下一條指令,程序由該點繼續運行。棧中的內存是在程序編譯完成以後就可以確定的,不論佔用空間大小,還是每個變量的類型。

堆:一般是在堆的頭部用一個字節存放堆的大小。堆中的具體內容由程序員安排。

(9)存取效率的比較

1 char s1[] = "a";
2 char *s2 = "b";

a是在運行時刻賦值的;而b是在編譯時就確定的但是,在以後的存取中,在棧上的數組比指針所指向的字符串(例如堆)快。

(10)防止越界發生

無論是堆還是棧,都要防止越界現象的發生(除非你是故意使其越界),因爲越界的結果要麼是程序崩潰,要麼是摧毀程序的堆、棧結構,產生以想不到的結果,就算是在你的程序運行過程中,沒有發生上面的問題,你還是要小心,說不定什麼時候就崩掉,那時候debug可是相當困難的

數據存儲區域實例

此程序顯示了數據存儲區域實例,在此程序中,使用了etext、edata和end3個外部全局變量,這是與用戶進程相關的虛擬地址。在程序源代碼中列出了各數據的存儲位置,同時在程序運行時顯示了各數據的運行位置,下圖所示爲程序運行過程中各變量的存儲位置。

mem_add.c

複製代碼
 1 /* mem_add.c演示了C語言中地址的分佈情況 */
 2 
 3 #include <stdio.h>
 4 #include <stdlib.h>
 5 
 6 extern void afunc(void);
 7 extern etext, edata, end;
 8 
 9 int bss_var;                    /* 未初始化全局數據存儲在BSS區 */
10 int data_var = 42;              /* 初始化全局數據區域存儲在數據區 */
11 #define SHW_ADDR(ID, I) printf("the %8s\t is at addr:%8x\n", ID, &I); /* 打印地址 */
12 
13 int main(int argc, char *argv[]) {
14 
15   char *p, *b, *nb;
16   printf("Addr etext: %8x\t Addr edata %8x\t Addr end %8x\t\n", &etext, &edata, &end);
17 
18   printf("\ntext Location:\n");
19   SHW_ADDR("main", main);       /* 查看代碼段main函數位置 */
20   SHW_ADDR("afunc", afunc);     /* 查看代碼段afunc函數位置 */
21   printf("\nbss Location:\n");
22   SHW_ADDR("bss_var", bss_var); /* 查看BSS段變量的位置 */
23   printf("\ndata Location:\n");
24   SHW_ADDR("data_var", data_var); /* 查看數據段變量的位置 */
25   printf("\nStack Locations:\n");
26 
27   afunc();
28   p = (char *)alloca(32);       /* 從棧中分配空間 */
29   if(p != NULL) {
30     SHW_ADDR("start", p);
31     SHW_ADDR("end", p+31);
32   }
33 
34   b = (char *)malloc(32*sizeof(char)); /* 從堆中分配空間 */
35   nb = (char *)malloc(16*sizeof(char)); /* 從堆中分配空間 */
36   printf("\nHeap Locations:\n");
37   printf("the Heap start: %p\n", b); /* 堆的起始位置 */
38   printf("the Heap end: %p\n", (nb+16*sizeof(char))); /* 堆的結束位置 */
39   printf("\nb and nb in Stack\n");
40 
41   SHW_ADDR("b", b);             /* 顯示棧中數據b的位置 */
42   
43   SHW_ADDR("nb", nb);           /* 顯示棧中數據nb的位置 */
44   
45   free(b);                      /* 釋放申請的空間 */
46   free(nb);                     /* 釋放申請的空間 */
47 }
複製代碼

 afunc.c

複製代碼
 1 /* afunc.c */
 2 #include <stdio.h>
 3 #define SHW_ADDR(ID, I) printf("the %s\t is at addr:%p\n", ID, &I); /* 打印地址 */
 4 void afunc(void) {
 5   static int long level = 0;    /* 靜態數據存儲在數據段中 */
 6   int stack_var;                /* 局部變量存儲在棧區 */
 7   
 8   if(++level == 5)
 9     return;
10   
11   printf("stack_var%d is at: %p\n", level, &stack_var);
12   SHW_ADDR("stack_var in stack section", stack_var);
13   SHW_ADDR("level in data section", level);
14   
15   afunc();
16 }
複製代碼

 gcc mem_add.c afunc.c進行編譯然後執行輸出的可執行的文件,可得到如下結果(本機有效):

然後可以根據地址的大小來進行一個排序,並可視化:

如果運行環境不一樣,運行程序的地址與此將有差異,但是,各區域之間的相對關係不會發生變化。可以通過readelf命令來查看可執行文件的詳細內容。

readelf -a a.out

其他知識點

來看一個問題,下面代碼的輸出結果是啥?

第一個文件code1.c

複製代碼
 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 
 4 char* toStr() {
 5   char *s = "abcdefghijk";
 6   return s;
 7 }
 8 
 9 int main(void) {
10   printf("%s\n", toStr());
11 }
複製代碼

 第二個文件code2.c

複製代碼
 1 #include <stdio.h>
 2 #include <stdlib.h>
 3 
 4 char* toStr() {
 5   char s[] = "abcdefghijk";
 6   return s;
 7 }
 8 
 9 int main(void) {
10   printf("%s\n", toStr());
11 }
複製代碼

 其實我在用gcc編譯第二的時候已經有warning了:

第一個可以正常輸出,而第二個要麼亂碼,要麼是空的。

兩段代碼都很簡單,輸出一段字符,類型不同,一個是char*字符串,一個是char[]數據。

結果:第一個正確輸出,第二個輸出亂碼。

原因:在於局部變量的作用域和內存分配的問題,第一char*是指向一個常量,作用域爲函數內部,被分配在程序的常量區,直到整個程序結束才被銷燬,所以在程序結束前常量還是存在的。而第二個是數組存放的,作用域爲函數內部,被分配在棧中,就會在函數調用結束後被釋放掉,這時你再調用,肯定就錯誤了。

我發現了一個新的問題,如果你把這兩個文件合成一個的話,第二個其實可以打印出正確的字符的,代碼如下:

複製代碼
 1 /* toStr.c演示內存分配問題哦 */
 2 
 3 #include <stdio.h>
 4 #include <stdlib.h>
 5 
 6 char* toStr1() {
 7   char *s = "abcdefghijklmn";
 8   return s;
 9 }
10 
11 char* toStr2() {
12   char s[] = "abcdefghijklmn";
13   return s;
14 }
15 
16 void printStr() {
17   int a[] = {1,2,3,4,5,6,7};
18 }
19 
20 int main(void) {
21   printf("調用toStr1()返回的結果: %s\n",toStr1());
22   printf("調用toStr2()返回的結果: %s\n",toStr2());
23   // printStr();
24   exit(0);
25 
26 }
複製代碼

 

不知道爲啥,第二個還是可以正常打印的。但是隻打印第二個,或者先打印第二個,然後在打印第一個的話,不輸出亂碼,倒是輸出空串。

顧名思義,局部變量就是在一個有限的範圍內的變量,作用域是有限的,對於程序來說,在一個函數體內部聲明的普通變量都是局部變量,局部變量會在棧上申請空間,函數結束後,申請的空間會自動釋放。而全局變量是在函數體外申請的,會被存放在全局(靜態區)上,知道程序結束後纔會被結束,這樣它的作用域就是整個程序。靜態變量和全局變量的存儲方式相同,在函數體內聲明爲static就可以使此變量像全局變量一樣使用,不用擔心函數結束而被釋放。

  • 棧區(stack)—由編譯器自動分配釋放,存放函數的參數值,局部變量的值等。其操作方式類似於數據結構中的棧。
  • 堆區(heap)—一般由程序員分配釋放,若程序員不釋放,程序結束時可能由OS回收。注意它與數據結構中的堆是兩回事,分配方式倒是類似於鏈表
  • 全局區(靜態區)(static)—全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域,未初始化的全局變量和未初始化的靜態                                  變量在相鄰的另一塊區域。  程序結束後由系統釋放。
  • 常量區—常量字符串就是放在這裏的,直到程序結束後由系統釋放。上面的問題就在這裏!!!
  • 代碼區—存放函數體的二進制代碼。

一般編譯器和操作系統實現來說,對於虛擬地址空間的最低(從0開始的幾K)的一段空間是未被映射的,也就是說它在進程空間中,但沒有賦予物理地址,不能被訪問。這也就是對空指針的訪問會導致crash的原因,因爲空指針的地址是0。至於爲什麼預留的不是一個字節而是幾K,是因爲內存是分頁的,至少要一頁;另外幾k的空間還可以用來捕捉使用空指針的情況。

char *a 與char a[] 的區別

char *d = "hello" 中的a是指向第一個字符‘a'的一個指針;char s[20] = "hello" 中數組名a也是執行數組第一個字符'h'的指針。現執行下列操作:strcat(d, s)。把字符串加到指針所指的字串上去,出現段錯誤,本質原因:*d="0123456789"存放在常量區,是無法修的。而數組是存放在棧中,是可以修改的。兩者區別如下:

讀寫能力:char *a = "abcd"此時"abcd"存放在常量區。通過指針只可以訪問字符串常量,而不可以改變它。而char a[20] = "abcd"; 此時 "abcd"存放在棧。可以通過指針去訪問和修改數組內容。

賦值時刻:char *a = "abcd"是在編譯時就確定了(因爲爲常量)。而char a[20] = "abcd"; 在運行時確定

存取效率:char *a = "abcd"; 存於靜態存儲區。在棧上的數組比指針所指向字符串快。因此慢,而char a[20] = "abcd"存於棧上,快。
另外注意:char a[] = "01234",雖然沒有指明字符串的長度,但是此時系統已經開好了,就是大小爲6-----'0' '1' '2' '3' '4' '5' '\0',(注意strlen(a)是不計'\0')

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章