C專家編程精華第二篇----C對內存的使用,底層探索

這裏以Linux中C編程爲例,有些東西可能在不同的系統中處理是不一樣的

/**************運行時:內存的佈局****************/

以下面這段程序爲例:

// A:未初始化的全局和靜態變量
int array[40];
static int num1;
// B:初始化後的全局和靜態變量
int num2 = 10;
static int num3 = 15;
int main()
{
// C:局部變量
int i = 3;
int j;
// D:可執行文件的指令
array[4] = i;
j = num1;
return 0;
}

(1)C源文件各對應可執行文件的部分

1、對於未初始化的全局和靜態變量,它一般被存到可執行文件的BSS段,這BSS段是爲了更有效的節省空間而開闢的,它只存放沒有值的變量,除了運行時記錄BSS段的大小外,它不佔目標文件的任何空間

2、對於初始化後的全局和靜態變量,它被放到了可執行文件的數據段,數據段被保存到目標文件中

3、對於局部變量,它並不會成爲可執行文件的一部分,它會在運行時創建,所以這也解釋局部變量的生存週期相對短的原因了

4、對於可執行文件指令,它被放入可執行文件的文本段,這個文本段最易受到編譯器的優化處理

(2)查看各個段的大小

在Linux下,運行size即可得到可執行文件的各段大小,在windows下可以通過查看對應段的第一個和最後一個變量和所對應的地址來計算出各段的大小,當然,下面這個程序片段能夠得到你的棧頂地址:

int main()
{
int i = 0;
printf("The Top Stack is %p\n",&i);
return 0;
}

(3)可執行文件對應內存地址段的部分

鏈接器會把指令部分直接拷貝到內存中,CPU會從首地址開始逐條語句開始執行

系統會爲可執行文件開闢一段空間,成爲虛擬地址空間,從高地址到底地址一次爲:棧段、BSS段、數據段、文本段、未映射區域

1、可執行文件的BSS段、數據段、文本段一一對應這內存中的BSS段、數據段、文本段

2、棧段:保存程序的局部變量、臨時數據、函數參數等

3、虛擬地址空間的最底端是未映射區域,它位於進程的地址空間,但是並未賦予它物理地址,它一般用於捕捉對空指針使用的情況

注意:BSS段和數據段有時又合稱爲數據段,在數據段中有一個區域是堆區域,相對於可自增長的棧段而言,堆區域也是一段可自增長的區域,它通過動態內存申請增長)

/****************棧的應用:函數調用***************/

在函數進行調用的時候,系統會產生一個過程活動記錄,它包括:局部變量、參數、指向前一個活動記錄的指針、返回地址

一般來說這個過程活動記錄塊是壓到棧中的,不過,如果不利用遞歸程序的,棧段就並非必須的了,因爲當一個簡單的函數被調用的時候不會將它的過程活動記錄入棧,這大大的增加了計算機的運算負擔。

另外棧段還可以用於暫時的存儲區,也就是前面提到的臨時數據的保存,這個臨時數據一般是通過alloca()函數申請的,這也是一個防止內存泄漏的辦法,但是它可能被下一個函數覆蓋,所以它並不是一個好的辦法。

程序例:

int Recall(int i)
{
if(i == 1)
printf("i is the 1\n");
else
Recall(--i);return 0;
}
int main()
{
	int i = 3;
	Recall(i);
	return 0;
}


(1)遞歸調用的流程圖


棧段是一般是由內存高地址向底地址生長,首先main的過程活動記錄入棧,然後調用Recall函數,Recall的過程活動記錄入棧,反覆,知道printf函數執行了,再由printf函數返回,挨着返回,挨着退棧,知道main結束,這是整個遞歸函數的調用過程。

一般來說,對於非遞歸的簡單函數,是不會讓它入棧的,因爲編譯器會考慮到效率問題,從上面的遞歸函數調用原理來看,每一個Recall函數的參數、局部變量、前一個活動的指針和返回地址都要入棧,這大大拖慢了程序的速度,雖然經典的棧模式可以很好的解決遞歸問題,但是先入棧、最後等都執行完了再出棧,這種方法肯定慢了很多,效率不高,所以一般就不要用遞歸函數解決方法,即使它真的很利於大家理解。

(2)從函數中返回值的辦法

從上面對遞歸函數調用原理的解說,我們可以更深刻的理解這麼一個問題:不能從函數中返回一個指向該函數內部局部變量的指針

當這個函數結束的時候,函數的過程活動記錄退棧,內存被回收,其他的函數可能入棧覆蓋掉了該函數,那麼裏面記錄的所有值都是不確定的了,接着指針就成了野指針,這可是相當危險的事情。

但是我們仍然可以從函數中返回一個如我們希望的指針,看看最開始的C源文件在可執行文件的佈局,數據段的生存週期是和程序一樣的,它包括全局變量和靜態變量,這也就是說我們把函數中的指針定義爲靜態變量就可以將這個指針輕鬆的返回了,因爲它不會被回收,在程序結束之前。

/***********內存使用************/

(1)查看程序能被分配的最大內存

int main()
{
int MB = 0;
while(malloc(1<<20))++MB;
printf("The Max Memory %d MB Can Be Alloc.\n",MB);
return 0;
}

這個程序片段可以查看你的電腦能爲你的程序最大分配多大的內存空間,1<<20表示將1乘以2的20次方,也就是1MB。

(2)內存申請:數據段中堆的增長

堆段在內存中的位置處於數據段裏面的最高地址處,與棧段相接。

堆段用於動態分配的存儲,也就是通過malloc函數獲得的內存,利用指針進行訪問,需要自己對它進行回收。一個malloc請求的數據大小一般都會被優化爲圓整爲2的次冪。

堆的回收不必像棧那麼按照分配順序回收,這也導致了動態內存像碎片一樣四處分佈,引起了內存回收的困難。

另外,堆的底部,即堆的最高地址,由一個break指針來標識,它有兩個作用:1、保證你對內存的引用是正確的,如果你引用的內存地址超過了break指針指向的地址,那麼它就會發出警報;2、通過系統調用brk和sbrk來移動break指針,這樣就可以獲得更多的內存

/*************內存泄漏*****************/

(1)避免內存泄漏

前面說了,堆增長(也就是動態內存的申請)會產生很對內存碎片,如果我們不對其進行回收,那麼系統會識別到這些碎片是一直屬於這個程序的,在程序結束之後也不會把這些碎片標記爲可用,那麼其他的程序就無法使用這些內存,相當於碎片會一直存在,除非你重啓電腦,這就是內存泄漏,你的內存再大也可能被它整死機的。

還需要注意的是動態申請的一般會被圓整,像申請215B的內存,其實會被優化爲申請256B的內存,多的41B是不會被引用的,除非你對它非法引用,所以,內存泄漏往往泄漏的比你忘記釋放的數據要多得多的。

內存對齊:數據項只能存儲在地址是數據項大小的整數倍的內存位置上,例如一個4字節的int型數據只能存放到地址是4的倍數的內存中。當然,如果你要檢查內存是否對齊基本是不太可能的,因爲編譯器一般都會通過自動分配和填充數據來進行對齊,例如下面這個結構體定義:

struct student
{
	char name[21];
	int number;
	double id;
};
以我自己的電腦爲例,系統會一次性提取8個字節的內存,那麼上面那個結構體對它sizeof,name會佔3*8=24字節,多餘了3個字節,但是緊接着定義的int型不能剛好放到多餘的3個字節裏,所以有開闢8個字節放number,這裏又多餘了四個字節,顯然id的8字節是不能放到四個字節裏的,所以有開闢8個字節放id,總共sizeof之後就是5*8=40字節了。

如果將name大小改爲25,有4*8=32字節容納它,多了7個字節,能夠容納number,那麼就相當於多餘了7-4=3字節了,顯然id不能放到3字節裏,開闢8字節容納id,這樣sizeof之後仍未5*8=40字節

爲了更好的節省內存空間,我們需要注意類似的變量定義的先後順序,將number和name的位置交換結果又不一樣,因爲指令是逐條執行的。

(3)段錯誤

(還有一種總線錯誤,真心還沒弄懂,求高手指點)

段錯誤是由於內存管理單元異常所致,導致段錯誤有以下幾個原因:

1、解引用一個包含非法值的指針,也就是對一個已經free掉的內存繼續使用

2、解引用一個空指針,一般就是初始化爲NULL了還對它進行解引用,還可能是從函數中返回了NULL卻沒有進行檢查就直接使用

3、對超過了申請內存邊界的內存進行解引用,有點像數組的越界一樣

4、把你的電腦可用動態內存用完了。。。。這個幾乎很困難

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