C語言內存淺談
操作系統的內存分配問題與內存對齊問題對於地層程序設計來說是非常重要的,對內存分配的理解直接影響到代碼質量、正確率、效率以及程序員對內存使用情況、溢出、洩露等的判斷力。而內存對齊是常常被忽略的問題,理解內存對齊原理及方法則有助於幫助程序員判斷訪問非法內存。一般c/c++程序占用的內存主要分為以下五種:
1.棧區(stack):系統自動分配,由程序自動創建、自動釋放。函數參數、局部變量以及返回值等信息都存在其中
2.堆區(heap):使用自由,不需要預先確定大小。多少情況下需要程序員手動申請、釋放。如果不釋放,程序結束後有操作系統垃圾回收機制收回。例如,s = (char *)malloc(10),
3.靜態區/全局區(static):全局變量和靜態變量的存儲區域。程序結束後由系統釋放
4.常量區:用於存放常量的內存區域
5.代碼區:存放代碼
例如:
#include
int quanju;/*全局變量,全局區/靜態區(static)*/
void fun(int f_jubu); /*程序代碼區*/
int main(void)/**/
{
int m_jubu;/*棧區(stack)*/
static int m_jingtai;/*靜態變量,全局區/靜態區(static)*/
char *m_zifum,*m_zifuc = "hello";/*指針本身位於棧。指向字符串"hello",位於文字常量區*/
void (*pfun)(int); /*棧區(stack)*/
pfun=&fun;
m_zifum = (char *)malloc(sizeof(char)*10);/*指針內容指向分配空間,位於堆區(heap)*/
pfun(1);
printf("&quanju : %x/n",&quanju);
printf("&m_jubu : %x/n",&m_jubu);
printf("&m_jingtai: %x/n",&m_jingtai);
printf("m_zifuc : %x/n",m_zifuc);
printf("&m_zifuc : %x/n",&m_zifuc);
printf("m_zifum : %x/n",m_zifum);
printf("&m_zifum : %x/n",&m_zifum);
printf("pfun : %x/n",pfun);
printf("&pfun : %x/n",&pfun);
getch();
return 0;
}
void fun(int f_jubu)
{
static int f_jingtai;
printf("&f_jingtai: %x/n",&f_jingtai);
printf("&f_jubu : %x/n",&f_jubu);/*棧區(stack),但是與主函數中m_jubu位於不同的棧*/
}
堆和棧
1.申請方式
stack:
由系統自動分配。例如,在函數中聲明一個局部變量char c;系統自動在棧中為c開辟空間
heap:
需要程序員手動申請,並指明大小,在c中,有malloc函數完成。如p1 = (char *)malloc(10)
2.申請後系統的響應
stack:
只要棧的剩余空間大於所申請空間,系統將為程序提供內存,否則將報異常提示棧溢出。
heap:
大多數操作系統有一個記錄空閒內存地址的鏈表,當系統收到程序的申請時,會遍歷該鏈表,尋找第一個空間大於所申請空間的堆結點,然後將該結點從空閒結點鏈表中刪除,並將該結點的空間分配給程序,另外,對於大多數系統,會在這塊內存空間中的首地址處記錄本次分配的大小,這樣,代碼中的free函數才能正確的釋放本內存空間。另外,由於找到的堆結點的大小不一定正好等於申請的大小,系統會自動的將多余的那部分重新放入空閒鏈表中
3.申請大小的限制
stack:
在Windows下,棧是向低地址擴展的數據結構,是一塊連續的內存的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在WINDOWS下,棧的大小是2M(也有的說是1M,總之是一個編譯時就確定的常數),如果申請的空間超過棧的剩余空間時,將提示overflow。因此,能從棧獲得的空間較小
heap:
堆是向高地址擴展的數據結構,是不連續的內存區域。這是由於系統是用鏈表來存儲的空閒內存地址的,自然是不連續的,而鏈表的遍歷方向是由低地址向高地址。堆的大小受限於計算機系統中有效的虛擬內存。由此可見,堆獲得的空間比較靈活,也比較大
4.申請效率的比較
stack:
由系統自動分配,速度較快。是程序員無法控制的
heap:
由程序員手動分配的內存,一般速度比較慢,而且容易產生內存碎片,不過用起來最方便
5.堆和棧中的存儲內容
stack:
在函數調用時,第一個進棧的是函數調用語句的下一條可執行語句的地址,然後是函數的各個參數,在大多數的C編譯器中,參數是由右往左入棧的,然後是函數中的局部變量。注意靜態變量是不入棧的。 當本次函數調用結束後,局部變量先出棧,然後是參數,最後棧頂指針指向最開始存的地址,也就是函數中的下一條指令,程序由該點繼續運行
heap:
一般是在堆的頭部用一個字節存放堆的大小。堆中的具體內容由程序員安排
內存對齊問題
現代計算機中內存空間都是按照byte劃分的,從理論上講似乎對任何類型的變量的訪問可以從任何地址開始,但實際情況是在訪問特定變量的時候經常在特定的內存地址訪問,這就需要各類型數據按照一定的規則在空間上排列,而不是順序的一個接一個的排放,這就是對齊。通常,我們寫程序的時候,不需要考慮對齊問題。編譯器會替我們選擇適合目標平台的對齊策略。當然,我們也可以通知給編譯器傳遞預編譯指令而改變對指定數據的對齊方法。
1.內存對齊的原因
各個硬件平台對存儲空間的處理上有很大的不同。一些平台對某些特定類型的數據只能從某些特定地址開始存取。其他平台可能沒有這種情況, 但是最常見的是如果不按照適合其平台的要求對數據存放進行對齊,會在存取效率上帶來損失。比如有些平台每次讀都是從偶地址開始,如果一個int型(假設為 32位)如果存放在偶地址開始的地方,那麼一個讀周期就可以讀出,而如果存放在奇地址開始的地方,就可能會需要2個讀周期,並對兩次讀出的結果的高低 字節進行拼湊才能得到該int數據。顯然在讀取效率上下降很多。這也是空間和時間的博弈
2.正確處理字節對齊
對於標准數據類型,它的地址只要是它的長度的整數倍就行了,而非標准數據類型按下面的原則對齊:
a.數組:按照基本數據類型對齊,第一個對齊了後面的自然也就對齊了
b.聯合:按其包含的長度最大的數據類型對齊
c.結構體:結構體中每個數據類型都要對齊
從結構體的首地址開始向後依次為每個成員尋找第一個滿足條件的首地址x,該條件是x % N = 0,並且整個結構的長度必須為各個成員所使用的對齊參數中最大的那個值的最小整數倍,不夠就補空字節
3.對齊規則
每個特定平台上的編譯器都有自己的默認“對齊系數”(也叫對齊模數)。程序員可以通過預編譯命令#pragma pack(n),n=1,2,4,8,16來改變這一系數,其中的n就是你要指定的“對齊系數”。對齊規則如下:
a.數據成員對齊規則:結構(struct)(或聯合(union))的數據成員,第一個數據成員放在offset為0的地方,以後每個數據成員的對齊按照#pragma pack指定的數值(或默認值)和這個數據成員類型長度中,比較小的那個進行。在上一個對齊後的地方開始尋找能被當前對齊數值整除的地址
b.結構(或聯合)的整體對齊規則:在數據成員完成各自對齊之後,結構(或聯合)本身也要進行對齊.主要體現在,最後一個元素對齊後,後面是否填補空字節,如果填補,填補多少.對齊將按照#pragma pack指定的數值(或默認值)和結構(或聯合)最大數據成員類型長度中,比較小的那個進行
c.結合1、2可推斷:當#pragma pack的n值等於或超過所有數據成員類型長度的時候,這個n值的大小將不產生任何效果
4.有四個概念值:
1.數據類型自身的對齊值:就是上面交代的基本數據類型的自身對齊值。
2.指定對齊值:#pragma pack (value)時的指定對齊值value。
3.結構體或者類的自身對齊值:其成員中自身對齊值最大的那個值。
4.數據成員、結構體和類的有效對齊值:自身對齊值和指定對齊值中較小的那個值。
由於各個平台和編譯器的不同,我的使用的gcc version 4.1.2 20080704 (Red Hat 4.1.2-52),來討論編譯器對struct數據結構中各個成員如何進行對齊的。例如:
1.struct A {
int a;
char b;
short c;
}; #結構體A中包含了4字節長度的int一個,1字節長度的char一個和2字節長度的short型數據一個。所以A用到的空間應該是7字節。但是因為編譯器要對數據成員在空間上進行對齊,所以使用sizeof(strcut A)值為8。
2.struct B {
char b;
int a;
short c;
};
#假設B從地址空間0x0000開始排放。該例子中沒有定義指定對齊值,在筆者環境下,該值默認為4。第一個成員變量b的自身對齊值是1,比指定或者默認指 定對齊值4小,所以其有效對齊值為1,所以其存放地址0x0000符合0x0000%1=0.第二個成員變量a,其自身對齊值為4,所以有效對齊值也為 4,所以只能存放在起始地址為0x0004到0x0007這四個連續的字節空間中,復核0x0004%4=0,且緊靠第一個變量。第三個變量c,自身對齊 值為2,所以有效對齊值也是2,可以存放在0x0008到0x0009這兩個字節空間中,符合0x0008%2=0。所以從0x0000到0x0009存
放的都是B內容。再看數據結構B的自身對齊值為其變量中最大對齊值(這裡是b)所以就是4,所以結構體的有效對齊值也是4。根據結構體圓整的要求, 0x0009到0x0000=10字節,(10+2)%4=0。所以0x0000A到0x000B也為結構體B所占用。故B從0x0000到0x000B 共有12個字節,sizeof(struct B)=12
3.#pragma pack(2) /*指定按2字節對齊*/
struct C {
char b;
int a;
short c;
};
#pragma pack() /*取消指定對齊,恢復缺省對齊*/
#們使用預編譯指令#pragma pack (value)來告訴編譯器。第一個變量b的自身對齊值為1,指定對齊值為2,所以,其有效對齊值為1,假設C從0x0000開始,那麼b存放在0x0000,符合0x0000%1= 0;第二個變量,自身對齊值為4,指定對齊值為2,所以有效對齊值為2,所以順序存放在0x0002、0x0003、0x0004、0x0005四個連續 字節中,符合0x0002%2=0。第三個變量c的自身對齊值為2,所以有效對齊值為2,順序存放
在0x0006、0x0007中,符合 0x0006%2=0。所以從0x0000到0x00007共八字節存放的是C的變量。又C的自身對齊值為4,所以 C的有效對齊值為2。又8%2=0,C只占用0x0000到0x0007的八個字節。所以sizeof(struct C)=8
4.#pragma pack (1) /*指定按1字節對齊*/
struct D {
char b;
int a;
short c;
};
#pragma pack () /*取消指定對齊,恢復缺省對齊*/
#sizeof(struct C)值是7
5.union E{
int a[5];
char b;
double c;
}; #我想的是union中變量共用內存,應以最長的為准,那就是20。可實際不然E中各變量的默認內存對齊方式,必須以最長的double 8字節對齊,故應該是sizeof(E)=24
注意:
1.數組對齊值為:min(數組元素類型,指定對齊長度).但數組中的元素是連續存放,存放時還是按照數組實際的長度.
如char t[9],對齊長度為1,實際占用連續的9byte.然後根據下一個元素的對齊長度決定在下一個元素之前填補多少byte.
2.嵌套的結構體假設
struct A
{
......
struct B b;
......
};
對於B結構體在A中的對齊長度為:min(B結構體的對齊長度,指定的對齊長度).B結構體的對齊長度為:上述2中結構整體對齊規則中的對齊長度.