C++中运行一个程序的内存分配情况及qt中的内存管理机制
一個由c/C++編譯的程序占用的內存分為以下幾個部分
1、棧區(stack)— 由編譯器自動分配釋放 ,存放函數的參數值,局部變量的值等。其操作方式類似于數據結構中的棧。
2、堆區(heap) — 一般由程序員分配釋放, 若程序員不釋放,程序結束時可能由OS回收 。注意它與數據結構中的堆是兩回事,分配方式倒是類似于鏈表,呵呵。
3、全局區(靜態區)(static)—,全局變量和靜態變量的存儲是放在一塊的,初始化的全局變量和靜態變量在一塊區域, 未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域。- 程序結束后有系統釋放
4、文字常量區—常量字符串就是放在這里的。程序結束后由系統釋放
5、程序代碼區—存放函數體的二進制代碼。
例子程序
這是一個前輩寫的,非常詳細
//main.cpp
int?a?=?0;?//全局初始化區
int?a?=?0;?//全局初始化區
char?*p1;?//全局未初始化區
main()?{
????int?b;?//棧
????char?s[]?=?"abc";?//棧
????char?*p2;?//棧
????char?*p3?=?"123456";?//123456\0在常量區,p3在棧上。
????static?int?c?=?0;?//全局(靜態)初始化區
????p1?=?(char?*)malloc(10);
????p2?=?(char?*)malloc(20);
????//分配得來得10和20字節的區域就在堆區。
????strcpy(p1,?"123456");?//123456\0放在常量區,編譯器可能會將它與p3所指向的"123456"優化成一個地方。
}
二、堆和棧的理論知識
2.1申請方式
stack:
由系統自動分配。例如,聲明在函數中一個局部變量 int b; 系統自動在棧中為b開辟空間
heap:
需要程序員自己申請,并指明大小,在c中malloc函數
如p1 = (char *)malloc(10);
在C++中用new運算符
如p2 = (char *)malloc(10);
但是注意p1、p2本身是在棧中的。
2.2 申請后系統的響應
棧:只要棧的剩余空間大于所申請空間,系統將為程序提供內存,否則將報異常提示棧溢出。
堆:首先應該知道操作系統有一個記錄空閑內存地址的鏈表,當系統收到程序的申請時,
會遍歷該鏈表,尋找第一個空間大于所申請空間的堆結點,然后將該結點從空閑結點鏈表中刪除,并將該結點的空間分配給程序,另外,對于大多數系統,會在這塊內存空間中的首地址處記錄本次分配的大小,這樣,代碼中的delete語句才能正確的釋放本內存空間。另外,由于找到的堆結點的大小不一定正好等于申請的大小,系統會自動的將多余的那部分重新放入空閑鏈表中。
2.3 申請大小的限制
棧:在Windows下,棧是向低地址擴展的數據結構,是一塊連續的內存的區域。這句話的意思是棧頂的地址和棧的最大容量是系統預先規定好的,在WINDOWS下,棧的大小是2M(也有的說是1M,總之是一個編譯時就確定的常數),如果申請的空間超過棧的剩余空間時,將提示overflow。因此,能從棧獲得的空間較小。
堆:堆是向高地址擴展的數據結構,是不連續的內存區域。這是由于系統是用鏈表來存儲的空閑內存地址的,自然是不連續的,而鏈表的遍歷方向是由低地址向高地址。堆的大小受限于計算機系統中有效的虛擬內存。由此可見,堆獲得的空間比較靈活,也比較大。
2.4 申請效率的比較:
棧由系統自動分配,速度較快。但程序員是無法控制的。
堆是由new分配的內存,一般速度比較慢,而且容易產生內存碎片,不過用起來最方便.
另外,在WINDOWS下,最好的方式是用VirtualAlloc分配內存,他不是在堆,也不是在棧是直接在進程的地址空間中保留一快內存,雖然用起來最不方便。但是速度快,也最靈活。
2.5 堆和棧中的存儲內容
棧:在函數調用時,第一個進棧的是主函數中后的下一條指令(函數調用語句的下一條可執行語句)的地址,然后是函數的各個參數,在大多數的C編譯器中,參數是由右往左入棧的,然后是函數中的局部變量。注意靜態變量是不入棧的。
當本次函數調用結束后,局部變量先出棧,然后是參數,最后棧頂指針指向最開始存的地址,也就是主函數中的下一條指令,程序由該點繼續運行。
堆:一般是在堆的頭部用一個字節存放堆的大小。堆中的具體內容有程序員安排。
2.6 存取效率的比較
char s1[] = "aaaaaaaaaaaaaaa";
char *s2 = "bbbbbbbbbbbbbbbbb";
aaaaaaaaaaa是在運行時刻賦值的;
而bbbbbbbbbbb是在編譯時就確定的;
但是,在以后的存取中,在棧上的數組比指針所指向的字符串(例如堆)快。
比如:
#include
void?main()?{
????char?a?=?1;
????char?c[]?=?"1234567890";
????char?*p?="1234567890";
????a?=?c[1];
????a?=?p[1];
????return;
}
對應的匯編代碼
10:?a?=?c[1];
00401067?8A?4D?F1?mov?cl,byte?ptr?[ebp-0Fh]
0040106A?88?4D?FC?mov?byte?ptr?[ebp-4],cl
11:?a?=?p[1];
0040106D?8B?55?EC?mov?edx,dword?ptr?[ebp-14h]
00401070?8A?42?01?mov?al,byte?ptr?[edx+1]
00401073?88?45?FC?mov?byte?ptr?[ebp-4],al
第一種在讀取時直接就把字符串中的元素讀到寄存器cl中,而第二種則要先把指針值讀到edx中,在根據edx讀取字符,顯然慢了。
2.7小結:
堆和棧的區別可以用如下的比喻來看出:
使用棧就象我們去飯館里吃飯,只管點菜(發出申請)、付錢、和吃(使用),吃飽了就走,不必理會切菜、洗菜等準備工作和洗碗、刷鍋等掃尾工作,他的好處是快捷,但是自由度小。
使用堆就象是自己動手做喜歡吃的菜肴,比較麻煩,但是比較符合自己的口味,而且自由度大。
三 、windows進程中的內存結構
在閱讀本文之前,如果你連堆棧是什么多不知道的話,請先閱讀文章后面的基礎知識。
接觸過編程的人都知道,高級語言都能通過變量名來訪問內存中的數據。那么這些變量在內存中是如何存放的呢?程序又是如何使用這些變量的呢?下面就會對此進行深入的討論。下文中的C語言代碼如沒有特別聲明,默認都使用VC編譯的release版。
首先,來了解一下 C 語言的變量是如何在內存分部的。C 語言有全局變量(Global)、本地變量(Local),靜態變量(Static)、寄存器變量(Regeister)。每種變量都有不同的分配方式。先來看下面這段代碼:
#include?<stdio.h>
int?g1=0,?g2=0,?g3=0;
int?main()
{
????static?int?s1=0,?s2=0,?s3=0;
????int?v1=0,?v2=0,?v3=0;
????//打印出各個變量的內存地址????
????printf("0x%08x\n",&v1);?//打印各本地變量的內存地址
????printf("0x%08x\n",&v2);
????printf("0x%08x\n\n",&v3);
????printf("0x%08x\n",&g1);?//打印各全局變量的內存地址
????printf("0x%08x\n",&g2);
????printf("0x%08x\n\n",&g3);
????printf("0x%08x\n",&s1);?//打印各靜態變量的內存地址
????printf("0x%08x\n",&s2);
????printf("0x%08x\n\n",&s3);
????return?0;
}
編譯后的執行結果是:
0x0012ff78
0x0012ff7c
0x0012ff80
0x004068d0
0x004068d4
0x004068d8
0x004068dc
0x004068e0
0x004068e4
輸出的結果就是變量的內存地址。其中v1,v2,v3是本地變量,g1,g2,g3是全局變量,s1,s2,s3是靜態變量。你可以看到這些變量在內存是連續分布的,但是本地變量和全局變量分配的內存地址差了十萬八千里,而全局變量和靜態變量分配的內存是連續的。這是因為本地變量和全局/靜態變量是分配在不同類型的內存區域中的結果。對于一個進程的內存空間而言,可以在邏輯上分成3個部份:代碼區,靜態數據區和動態數據區。動態數據區一般就是“堆棧”。“棧(stack)”和“堆(heap)”是兩種不同的動態數據區,棧是一種線性結構,堆是一種鏈式結構。進程的每個線程都有私有的“棧”,所以每個線程雖然代碼一樣,但本地變量的數據都是互不干擾。一個堆棧可以通過“基地址”和“棧頂”地址來描述。全局變量和靜態變量分配在靜態數據區,本地變量分配在動態數據區,即堆棧中。程序通過堆棧的基地址和偏移量來訪問本地變量。
├———————┤低端內存區域
│?……?│
├———————┤
│?動態數據區?│
├———————┤
│?……?│
├———————┤
│?代碼區?│
├———————┤
│?靜態數據區?│
├———————┤
│?……?│
├———————┤高端內存區域
堆棧是一個先進后出的數據結構,棧頂地址總是小于等于棧的基地址。我們可以先了解一下函數調用的過程,以便對堆棧在程序中的作用有更深入的了解。不同的語言有不同的函數調用規定,這些因素有參數的壓入規則和堆棧的平衡。windows API的調用規則和ANSI C的函數調用規則是不一樣的,前者由被調函數調整堆棧,后者由調用者調整堆棧。兩者通過“__stdcall”和“__cdecl”前綴區分。先看下面這段代碼:
#include?<stdio.h>
void?__stdcall?func(int?param1,int?param2,int?param3)
{
????int?var1=param1;
????int?var2=param2;
????int?var3=param3;
????printf("0x%08x\n",param1);?//打印出各個變量的內存地址
????printf("0x%08x\n",param2);
????printf("0x%08x\n\n",param3);
????printf("0x%08x\n",&var1);
????printf("0x%08x\n",&var2);
????printf("0x%08x\n\n",&var3);
????return;
}
int?main()?{
????func(1,2,3);
????return?0;
}
編譯后的執行結果是:
0x0012ff78
0x0012ff7c
0x0012ff80
0x0012ff68
0x0012ff6c
0x0012ff70
├———————┤<—函數執行時的棧頂(ESP)、低端內存區域
│?……?│
├———————┤
│?var?1?│
├———————┤
│?var?2?│
├———————┤
│?var?3?│
├———————┤
│?RET?│
├———————┤<—“__cdecl”函數返回后的棧頂(ESP)
│?parameter?1?│
├———————┤
│?parameter?2?│
├———————┤
│?parameter?3?│
├———————┤<—“__stdcall”函數返回后的棧頂(ESP)
│?……?│
├———————┤<—棧底(基地址?EBP)、高端內存區域
上圖就是函數調用過程中堆棧的樣子了。首先,三個參數以從右到左的次序壓入堆棧,先壓“param3”,再壓“param2”,最后壓入“param1”;然后壓入函數的返回地址(RET),接著跳轉到函數地址接著執行(這里要補充一點,介紹UNIX下的緩沖溢出原理的文章中都提到在壓入RET后,繼續壓入當前EBP,然后用當前ESP代替EBP。然而,有一篇介紹windows下函數調用的文章中說,在windows下的函數調用也有這一步驟,但根據我的實際調試,并未發現這一步,這還可以從param3和var1之間只有4字節的間隙這點看出來);第三步,將棧頂(ESP)減去一個數,為本地變量分配內存空間,上例中是減去12字節(ESP=ESP-3*4,每個int變量占用4個字節);接著就初始化本地變量的內存空間。由于“__stdcall”調用由被調函數調整堆棧,所以在函數返回前要恢復堆棧,先回收本地變量占用的內存(ESP=ESP+3*4),然后取出返回地址,填入EIP寄存器,回收先前壓入參數占用的內存(ESP=ESP+3*4),繼續執行調用者的代碼。參見下列匯編代碼:
;--------------func?函數的匯編代碼-------------------
:00401000?83EC0C?sub?esp,?0000000C?//創建本地變量的內存空間
:00401003?8B442410?mov?eax,?dword?ptr?[esp+10]
:00401007?8B4C2414?mov?ecx,?dword?ptr?[esp+14]
:0040100B?8B542418?mov?edx,?dword?ptr?[esp+18]
:0040100F?89442400?mov?dword?ptr?[esp],?eax
:00401013?8D442410?lea?eax,?dword?ptr?[esp+10]
:00401017?894C2404?mov?dword?ptr?[esp+04],?ecx
……………………(省略若干代碼)
:00401075?83C43C?add?esp,?0000003C?;恢復堆棧,回收本地變量的內存空間
:00401078?C3?ret?000C?;函數返回,恢復參數占用的內存空間
;如果是“__cdecl”的話,這里是“ret”,堆棧將由調用者恢復
;-------------------函數結束-------------------------
;--------------主程序調用func函數的代碼--------------
:00401080?6A03?push?00000003?//壓入參數param3
:00401082?6A02?push?00000002?//壓入參數param2
:00401084?6A01?push?00000001?//壓入參數param1
:00401086?E875FFFFFF?call?00401000?//調用func函數
;如果是“__cdecl”的話,將在這里恢復堆棧,“add?esp,?0000000C”
聰明的讀者看到這里,差不多就明白緩沖溢出的原理了。先來看下面的代碼:
#include?<stdio.h>
#include?<string.h>
void?__stdcall?func()?{
????char?lpBuff[8]="\0";
????strcat(lpBuff,"AAAAAAAAAAA");
????return;
}
int?main()?{
????func();
????return?0;
}
編譯后執行一下回怎么樣?哈,“”0x00414141”指令引用的”0x00000000”內存。該內存不能為”read”。”,“非法操作”嘍!”41”就是”A”的16進制的ASCII碼了,那明顯就是strcat這句出的問題了。”lpBuff”的大小只有8字節,算進結尾的\0,那strcat最多只能寫入7個”A”,但程序實際寫入了11個”A”外加1個\0。再來看看上面那幅圖,多出來的4個字節正好覆蓋了RET的所在的內存空間,導致函數返回到一個錯誤的內存地址,執行了錯誤的指令。如果能精心構造這個字符串,使它分成三部分,前一部份僅僅是填充的無意義數據以達到溢出的目的,接著是一個覆蓋RET的數據,緊接著是一段shellcode,那只要這個RET地址能指向這段shellcode的第一個指令,那函數返回時就能執行shellcode了。但是軟件的不同版本和不同的運行環境都可能影響這段shellcode在內存中的位置,那么要構造這個RET是十分困難的。一般都在RET和shellcode之間填充大量的NOP指令,使得exploit有更強的通用性。
├———————┤<—低端內存區域
│?……?│
├———————┤<—由exploit填入數據的開始
│?│
│?buffer?│<—填入無用的數據
│?│
├———————┤
│?RET?│<—指向shellcode,或NOP指令的范圍
├———————┤
│?NOP?│
│?……?│<—填入的NOP指令,是RET可指向的范圍
│?NOP?│
├———————┤
│?│
│?shellcode?│
│?│
├———————┤<—由exploit填入數據的結束
│?……?│
├———————┤<—高端內存區域
windows下的動態數據除了可存放在棧中,還可以存放在堆中。了解C++的朋友都知道,C++可以使用new關鍵字來動態分配內存。來看下面的C++代碼:
#include?<stdio.h>
#include?<iostream.h>
#include?<windows.h>
void?func()
{
????char?*buffer=new?char[128];
????char?bufflocal[128];
????static?char?buffstatic[128];
????printf("0x%08x\n",buffer);?//打印堆中變量的內存地址
????printf("0x%08x\n",bufflocal);?//打印本地變量的內存地址
????printf("0x%08x\n",buffstatic);?//打印靜態變量的內存地址
}
void?main()?{
????func();
????return;
}
程序執行結果為:
0x004107d0
0x0012ff04
0x004068c0
可以發現用new關鍵字分配的內存即不在棧中,也不在靜態數據區。VC編譯器是通過windows下的“堆(heap)”來實現new關鍵字的內存動態分配。在講“堆”之前,先來了解一下和“堆”有關的幾個API函數:
-?HeapAlloc?在堆中申請內存空間
-?HeapCreate?創建一個新的堆對象
-?HeapDestroy?銷毀一個堆對象
-?HeapFree?釋放申請的內存
-?HeapWalk?枚舉堆對象的所有內存塊
-?GetProcessHeap?取得進程的默認堆對象
-?GetProcessHeaps?取得進程所有的堆對象
-?LocalAlloc
-?GlobalAlloc
當進程初始化時,系統會自動為進程創建一個默認堆,這個堆默認所占內存的大小為1M。堆對象由系統進行管理,它在內存中以鏈式結構存在。通過下面的代碼可以通過堆動態申請內存空間:
HANDLE?hHeap=GetProcessHeap();
char?*buff=HeapAlloc(hHeap,0,8);
其中hHeap是堆對象的句柄,buff是指向申請的內存空間的地址。那這個hHeap究竟是什么呢?它的值有什么意義嗎?看看下面這段代碼吧:
#pragma?comment(linker,"/entry:main")?//定義程序的入口
#include?<windows.h>
_CRTIMP?int?(__cdecl?*printf)(const?char?*,?...);?//定義STL函數printf
/*---------------------------------------------------------------------------
?寫到這里,我們順便來復習一下前面所講的知識:
?(*注)printf函數是C語言的標準函數庫中函數,VC的標準函數庫由msvcrt.dll模塊實現。
?由函數定義可見,printf的參數個數是可變的,函數內部無法預先知道調用者壓入的參數個數,函數只能通過分析第一個參數字符串的格式來獲得壓入參數的信息,由于這里參數的個數是動態的,所以必須由調用者來平衡堆棧,這里便使用了__cdecl調用規則。BTW,Windows系統的API函數基本上是__stdcall調用形式,只有一個API例外,那就是wsprintf,它使用__cdecl調用規則,同printf函數一樣,這是由于它的參數個數是可變的緣故。
?---------------------------------------------------------------------------*/
void?main()
{
????HANDLE?hHeap=GetProcessHeap();
????char?*buff=HeapAlloc(hHeap,0,0x10);
????char?*buff2=HeapAlloc(hHeap,0,0x10);
????HMODULE?hMsvcrt=LoadLibrary("msvcrt.dll");
????printf=(void?*)GetProcAddress(hMsvcrt,"printf");
????printf("0x%08x\n",hHeap);
????printf("0x%08x\n",buff);
????printf("0x%08x\n\n",buff2);
}
執行結果為:
0x00130000
0x00133100
0x00133118
hHeap的值怎么和那個buff的值那么接近呢?其實hHeap這個句柄就是指向HEAP首部的地址。在進程的用戶區存著一個叫PEB(進程環境塊)的結構,這個結構中存放著一些有關進程的重要信息,其中在PEB首地址偏移0x18處存放的ProcessHeap就是進程默認堆的地址,而偏移0x90處存放了指向進程所有堆的地址列表的指針。windows有很多API都使用進程的默認堆來存放動態數據,如windows 2000下的所有ANSI版本的函數都是在默認堆中申請內存來轉換ANSI字符串到Unicode字符串的。對一個堆的訪問是順序進行的,同一時刻只能有一個線程訪問堆中的數據,當多個線程同時有訪問要求時,只能排隊等待,這樣便造成程序執行效率下降。
最后來說說內存中的數據對齊。所位數據對齊,是指數據所在的內存地址必須是該數據長度的整數倍,DWORD數據的內存起始地址能被4除盡,WORD數據的內存起始地址能被2除盡,x86 CPU能直接訪問對齊的數據,當他試圖訪問一個未對齊的數據時,會在內部進行一系列的調整,這些調整對于程序來說是透明的,但是會降低運行速度,所以編譯器在編譯程序時會盡量保證數據對齊。同樣一段代碼,我們來看看用VC、Dev-C++和lcc三個不同編譯器編譯出來的程序的執行結果:
#include?<stdio.h>
int?main()
????{
????int?a;
????char?b;
????int?c;
????printf("0x%08x\n",&a);
????printf("0x%08x\n",&b);
????printf("0x%08x\n",&c);
????return?0;
}
這是用VC編譯后的執行結果:
0x0012ff7c
0x0012ff7b
0x0012ff80
變量在內存中的順序:b(1字節)-a(4字節)-c(4字節)。
這是用Dev-C++編譯后的執行結果:
0x0022ff7c
0x0022ff7b
0x0022ff74
變量在內存中的順序:c(4字節)-中間相隔3字節-b(占1字節)-a(4字節)。
這是用lcc編譯后的執行結果:
0x0012ff6c
0x0012ff6b
0x0012ff64
變量在內存中的順序:同上。
三個編譯器都做到了數據對齊,但是后兩個編譯器顯然沒VC“聰明”,讓一個char占了4字節,浪費內存。
/***************************************************************************
MFC與Qt的內存管理
最近在做MFC向Qt的移植,在內存管理方面遇到了很頭疼的問題,雖然不知道問題到底出在哪,先了解下這兩個庫的內存管理方式。于是轉載兩篇關于內存管理的文章。
一. Qt內存管理:
在Qt的程序中經常會看到只有new而不delete的情況,其實是因為Qt有一套回收內存的機制,主要的規則如下:
1.所有繼承自QObject類的類,如果在new的時候指定了父親,那么它的清理時在父親被delete的時候delete的,所以如果一個程序中,所有的QObject類都指定了父親,那么他們是會一級級的在最上面的父親清理時被清理,而不用自己清理;
2. 程序通常最上層會有一個根的QObject,就是放在setCentralWidget()中的那個QObject,這個QObject在 new的時候不必指定它的父親,因為這個語句將設定它的父親為總的QApplication,當整個QApplication沒有時它就自動清理,所以也 無需清理。這里Qt4和Qt3有不同,Qt3中用的是setmainwidget函數,但是這個函數不作為里面QObject的父親,所以Qt3中這個頂 層的QObject要自行銷毀)。
3.這是有人可能會問那如果我自行delete掉這些Qt接管負責銷毀的指針了會出現什么情況呢,如果 這么做的話,正常情況下被delete的對象的父親會知道這件事情,它會知道它的兒子被你直接delete了,這樣它會將這個兒子移出它的列表,并且重新 構建顯示內容,但是直接這樣做是有風險的!也就是要說的下一條。
4.當一個QObject正在接受事件隊列時如果中途被你DELETE掉 了,就是出現問題了,所以Qt中建議大家不要直接DELETE掉一個QObject,如果一定要這樣做,要使用QObject的 deleteLater()函數,它會讓所有事件都發送完一切處理好后馬上清除這片內存,而且就算調用多次的deletelater也不會有問題。
5.Qt 不建議在一個QObject對象的父親的范圍之外持有對這個對象的指針,因為如果這樣外面的指針很可能不會察覺這個QObject被釋放,會出現錯誤。如 果一定要這樣,就要記住你在哪這樣做了,然后抓住那個被你違規使用的QObject的destroyed()信號,當它沒有時趕快置零你的外部指針。當然 我認為這樣做是及其麻煩也不符合高效率編程規范的,所以如果要這樣在外部持有QObject的指針,建議使用引用或者用智能指針,如Qt就提供了智能指針 針對這些情況,見最后一條。
6.Qt中的智能指針封裝為QPointer類,所有QObject的子類都可以用這個智能指針來包裝,很多用法與普通指針一樣,可以詳見Qt assistant
通過調查這個Qt的內存管理功能,發現了很多東西,現在覺得雖然這個Qt弄的有點小復雜,但是使用起來還是很方便的,最后要說的是某些內存泄露的檢測工具會認為Qt的程序因為這種方式存在內存泄露,發現時大可不必理會~
原帖地址:http://blog.csdn.net/leonwei/archive/2009/01/04/3703598.aspx
二. MFC內存分配方式與調試機制
1 內存分配
1.1 內存分配函數
???? MFCWin32或者C語言的內存分配API,有四種內存分配API可供使用。
Win32的堆分配函數
每一個進程都可以使用堆分配函數創建一個私有的堆──調用進程地址空間的一個或者多個頁面。DLL創建的私有堆必定在調用DLL的進程的地址空間內,只能被調用進程訪問。
HeapCreate用來創建堆;HeapAlloc用來從堆中分配一定數量的空間,HeapAlloc分配的內存是不能移動的;HeapSize可以確定從堆中分配的空間的大小;HeapFree用來釋放從堆中分配的空間;HeapDestroy銷毀創建的堆。
Windows傳統的全局或者局部內存分配函數
由于Win32采用平面內存結構模式,Win32下的全局和局部內存函數除了名字不同外,其他完全相同。任一函數都可以用來分配任意大小的內存(僅僅受可用物理內存的限制)。用法可以和Win16下基本一樣。
Win32下保留這類函數保證了和Win16的兼容。
C語言的標準內存分配函數
C語言的標準內存分配函數包括以下函數:
malloc,calloc,realloc,free,等。
這些函數最后都映射成堆API函數,所以,malloc分配的內存是不能移動的。這些函數的調式版本為
malloc_dbg,calloc_dbg,realloc_dbg,free_dbg,等。
Win32的虛擬內存分配函數
虛擬內存API是其他API的基礎。虛擬內存API以頁為最小分配單位,X86上頁長度為4KB,可以用GetSystemInfo函數提取頁長度。虛擬內存分配函數包括以下函數:
該函數用來分配一定范圍的虛擬頁。參數1指定起始地址;參數2指定分配內存的長度;參數3指定分配方式,取值 MEM_COMMINT或者MEM_RESERVE;參數4指定控制訪問本次分配的內存的標識,取值為PAGE_READONLY、 PAGE_READWRITE或者PAGE_NOACCESS。
該函數功能類似于VirtualAlloc,但是允許指定進程process。VirtaulFree、VirtualProtect、VirtualQuery都有對應的擴展函數。
該函數用來回收或者釋放分配的虛擬內存。參數1指定希望回收或者釋放內存的基地址;如果是回收,參數2可以指向虛 擬地址范圍內的任何地方,如果是釋放,參數2必須是VirtualAlloc返回的地址;參數3指定是否釋放或者回收內存,取值為 MEM_DECOMMINT或者MEM_RELEASE。
該函數用來把已經分配的頁改變成保護頁。參數1指定分配頁的基地址;參數2指定保護頁的長度;參數3指定頁的保護屬性,取值PAGE_READ、PAGE_WRITE、PAGE_READWRITE等等;參數4用來返回原來的保護屬性。
該函數用來查詢內存中指定頁的特性。參數1指向希望查詢的虛擬地址;參數2是指向內存基本信息結構的指針;參數3指定查詢的長度。
該函數用來鎖定內存,鎖定的內存頁不能交換到頁文件。參數1指定要鎖定內存的起始地址;參數2指定鎖定的長度。?
參數1指定要解鎖的內存的起始地址;參數2指定要解鎖的內存的長度。
1.2 C++的new 和 delete操作符
??? MFC定義了兩種作用范圍的new和delete操作符。對于new,不論哪種,參數1類型必須是size_t,且返回void類型指針。
全局范圍內的new和delete操作符
原型如下:
void _cdecl ::operator new(size_t nSize);
void __cdecl operator delete(void* p);
調試版本:
void* __cdecl operator new(size_t nSize, int nType,
LPCSTR lpszFileName, int nLine)
類定義的new和delete操作符
原型如下:
類的operator new操作符是類的靜態成員函數,對該類的對象來說將覆蓋全局的operator new。全局的operator new用來給內部類型對象(如int)、沒有定義operator new操作符的類的對象分配內存。
new操作符被映射成malloc或者malloc_dbg,delete被映射成free或者free_dbg。
2 調試手段
??? MFC應用程序可以使用C運行庫的調試手段,也可以使用MFC提供的調試手段。兩種調試手段分別論述如下。
2.1 C運行庫提供和支持的調試功能
??? C運行庫提供和支持的調試功能如下:
調試信息報告函數
用來報告應用程序的調試版本運行時的警告和出錯信息。包括:
_CrtDbgReport 用來報告調試信息;
_CrtSetReportMode 設置是否警告、出錯或者斷言信息;
_CrtSetReportFile 設置是否把調試信息寫入到一個文件。
條件驗證或者斷言宏:
斷言宏主要有:
assert 檢驗某個條件是否滿足,不滿足終止程序執行。
驗證函數主要有:
_CrtIsValidHeapPointer 驗證某個指針是否在本地堆中;
_CrtIsValidPointer 驗證指定范圍的內存是否可以讀寫;
_CrtIsMemoryBlock 驗證某個內存塊是否在本地堆中。
內存(堆)調試:
malloc_dbg 分配內存時保存有關內存分配的信息,如在什么文件、哪一行分配的內存等。有一系列用來提供內存診斷的函數:
_CrtMemCheckpoint 保存內存快照在一個_CrtMemState結構中;
_CrtMemDifference 比較兩個_CrtMemState;
_CrtMemDumpStatistics 轉儲輸出一_CrtMemState結構的內容;
_CrtMemDumpAllObjectsSince 輸出上次快照或程序開始執行以來在堆中分配的所有對象的信息;
_CrtDumpMemoryLeaks 檢測程序執行以來的內存漏洞,如果有漏洞則輸出所有分配的對象。
2.2 MFC提供的調試手段
??? MFC在C運行庫提供和支持的調試功能基礎上,設計了一些類、函數等來協助調試。
MFC的TRACE、ASSERT
ASSERT
使用ASSERT斷言判定程序是否可以繼續執行。
TRACE
使用TRACE宏顯示或者打印調試信息。TRACE是通過函數AfxTrace實現的。由于AfxTrace函數使用了cdecl調用約定,故可以接受個數不定的參數,如同printf函數一樣。它的定義和實現如下:
在程序源碼中,可以控制是否顯示跟蹤信息,顯示什么跟蹤信息。如果全局變量afxTraceEnabled為 TRUE,則TRACE宏可以輸出;否則,沒有TRACE信息被輸出。如果通過afxTraceFlags指定了跟蹤什么消息,則輸出有關跟蹤信息,例如 為了指定“Multilple Application Debug”,令AfxTraceFlags|=traceMultiApp。可以跟蹤的信息有:
這樣,應用程序可以在需要的地方指定afxTraceEnabled的值打開或者關閉TRACE開關,指定AfxTraceFlags的值過濾跟蹤信息。
Visual C++提供了一個TRACE工具,也可以用來完成上述功能。
為了顯示消息信息,MFC內部定義了一個AFX_MAP_MESSAG類型的數組allMessages,儲存了Windows消息和消息名映射對。例如:
MFC內部還使用函數_AfxTraceMsg顯示跟蹤消息,它可以接收一個字符串和一個MSG指針,然后,把該字符串和MSG的各個域的信息組合成一個大的字符串并使用AfxTrace顯示出來。
allMessages和函數_AfxTraceMsg的詳細實現可以參見AfxTrace.cpp。
MFC對象內容轉儲
對象內容轉儲是CObject類提供的功能,所有從它派生的類都可以通過覆蓋虛擬函數DUMP來支持該功能。在講述CObject類時曾提到過。
虛擬函數Dump的定義:
在使用Dump時,必須給它提供一個CDumpContext類型的參數,該參數指定的對象將負責輸出調試信 息。為此,MFC提供了一個預定義的全局CDumpContext對象afxDump,它把調試信息輸送給調試器的調試窗口。從前面AfxTrace的實 現可以知道,MFC使用了afxDump輸出跟蹤信息到調試窗口。
CDumpContext類沒有基類,它提供了以文本形式輸出診斷信息的功能。
例如:
MFC對象有效性檢測
對象有效性檢測是CObject類提供的功能,所有從它派生的類都可以通過覆蓋虛擬函數AssertValid來支持該功能。在講述CObject類時曾提到過。
虛擬函數AssertValid的定義:
使用ASSERT_VALID宏判斷一個對象是否有效,該對象的類必須覆蓋了AssertValid函數。形式為:ASSERT_VALID(pObject)。
另外,MFC提供了一些函數來判斷地址是否有效,如:
AfxIsMemoryBlock,AfxIsString,AfxIsValidAddress。
3 內存診斷
MFC使用DEBUG_NEW來跟蹤內存分配時的執行的源碼文件和行數。
把#define new DEBUG_NEW插入到每一個源文件中,這樣,調試版本就使用_malloc_dbg來分配內存。MFC Appwizard在創建框架文件時已經作了這樣的處理。
AfxDoForAllObjects
MFC提供了函數AfxDoForAllObjects來追蹤動態分配的內存對象,函數原型如下:
void AfxDoForAllObjects( void (*pfn)(CObject* pObject,
void* pContext), void* pContext );
其中:
參數1是一個函數指針,AfxDoForAllObjects對每個對象調用該指針表示的函數。
參數2將傳遞給參數1指定的函數。
AfxDoForAllObjects可以檢測到所有使用new分配的CObject對象或者CObject類派生的對象,但全局對象、嵌入對象和棧中分配的對象除外。
內存漏洞檢測
僅僅用于new的DEBUG版本分配的內存。
完成內存漏洞檢測,需要如下系列步驟:
調用AfxEnableMemoryTracking(TRUE/FALSE)打開/關閉內存診斷。在調試版本下,缺省是打開的;關閉內存診斷可以加快程序執行速度,減少診斷輸出。
使用MFC全局變量afxMemDF更精確地指定診斷輸出的特征,缺省值是allocMemDF,可以取如下值或者這些值相或:
afxMemDF,delayFreeMemDF,checkAlwaysMemDF
其中:allocMemDF表示可以進行內存診斷輸出;delayFreeMemDF表示是否是在應用程序結束時 才調用free或者delete,這樣導致程序最大可能的分配內存;checkAlwaysMemDF表示每一次分配或者釋放內存之后都調用函數 AfxCheckMemory進行內存檢測(AfxCheckMemory檢查堆中所有通過new分配的內存(不含malloc))。
這一步是可選步驟,非必須。
創建一個CMemState類型的變量oldMemState,調用CMemState的成員函數CheckPoint獲得初次內存快照。
執行了系列內存分配或者釋放之后,創建另一個CMemState類型變量newMemState,調用CMemState的成員函數CheckPoint獲得新的內存快照。
創建第三個CMemState類型變量difMemState,調用CMemState的成員函數Difference比較oldMemState和newMemState,結果保存在變量difMemState中。如果沒有不同,則返回FALSE,否則返回TRUE。
如果不同,則調用成員函數DumpStatistics輸出比較結果。
/**********************************************************
Qt內存管理機制
前言
內存管理,是對軟件中內存資源的分配與釋放進行有效管理的方法和理論。
眾所周知,內存管理是軟件開發的一個重要的內容。軟件規模越大,內存管理可能出現的問題越多。如果像C語言一樣手動地管理內存,一會給開發人員帶來巨大的負擔,二是手動管理內存的可靠性較差。
Qt為軟件開發人員提供了一套內存管理機制,用以替代手動內存管理。
下面開始逐條講述Qt中的內存管理機制。
一脈相承的棧與堆的內存管理
了解C語言的同學都知道,C語言中的內存分配有兩種形式:棧內存、堆內存。
棧內存
棧內存的管理是由編譯器來做的,棧上申請的內存變量,生存期由所在作用域決定,超出作用域的棧內存變量會被編譯器自動釋放。
值得一提的是,作用域的顯著標志是一對大括號,大括號內部即為作用域內部,大括號外部即為作用域外部。
參考下列代碼:
| int main() | |
| { | |
| int a = 0; | |
| return 1; | |
| } |
變量a在棧內存上,main函數返回時,作用域結束,a的內存自動被釋放。
從以上描述也可以看出,棧內存的使用是在編譯器嚴密監管之下進行的,遵循嚴格的作用域規則,所以棧內存的大小、申請時機、釋放時機都能在編譯的時候確定。
堆內存
堆內存是另外一種管理方式。堆內存最大的特點是可以動態分配,即在運行時可以根據需要進行申請。當然隨之而來的弊端也顯而易見:需要開發人員對堆內存的釋放進行嚴格管理,稍有疏漏會導致內存泄漏,甚至軟件崩潰等問題。
參考下列代碼:
| int main() | |
| { | |
| // 申請堆內存 | |
| int *intArray = (int *)malloc(100); | |
| // 使用堆內存... | |
| // 釋放堆內存 | |
| free(intArray); | |
| return 1; | |
| } |
如上述代碼,堆內存分配的寫法區別于棧內存。C語言中,堆內存使用malloc分配,使用free釋放。C++中可以使用new分配,使用delete釋放。
至此,我們介紹了C語言中的內存管理方式。我們知道Qt是C++的框架,C++是對C語言的擴展,所以C語言中的內存管理方式(堆、棧)和動態內存管理(堆內存釋放問題)存在的問題,在C++中仍然存在。所以Qt中自然而然也有相同的問題。說起來可能有點亂,下面用一張圖來說明它們的關系:
那么,Qt是如何為我們解決動態內存管理問題的呢?下面開始正式講解。
使用對象父子關系進行內存管理
使用對象父子關系進行內存管理的原理,簡述為:
在創建類的對象時,為對象指定父對象指針。當父對象在某一時刻被銷毀釋放時,父對象會先遍歷其所有的子對象,并逐個將子對象銷毀釋放。
為了直觀理解上述過程,以如下代碼為例進行說明:
| #include <QApplication> | |
| #include <QLabel> | |
| int main(int argc, char *argv[]) | |
| { | |
| QApplication a(argc, argv); | |
| // 創建主窗口 | |
| QWidget mainWidget; | |
| mainWidget.resize(400, 300); | |
| // 創建文字標簽 | |
| QLabel *label = new QLabel("Hello World!", &mainWidget); | |
| // 顯示主窗口 | |
| mainWidget.show(); | |
| return a.exec(); | |
| } |
運行結果如下:
上述代碼中,mainWidget為主窗口對象,類型為QWidget;label為子窗口對象,類型為QLabel?*。
注意代碼第13行,在創建label文本標簽窗口對象時,new QLabel的第二個參數即為父對象地址(參考Qt Assistant中QLabel的說明文檔),這里給的值是主窗口的地址。
在main函數退出時,mainWidget超出main函數作用域會析構,析構時會自動刪除label窗口對象,所以這里,我們不需要再寫一行:delete label; 來釋放label的內存,很方便而且又能節省時間精力。
使用引用計數對內存進行管理
引用計數
引用計數可以說是軟件開發人員必知必會的知識點,它在內存管理領域的地位是數一數二的。
引用計數的原理,還是力所能及地用最簡單的話來描述:
引用計數需要從三個方面來全面理解:
使用場景:一個資源,多處使用(使用即引用)。
問題:到底誰來釋放資源。
原理:使用一個整形變量來統計,此資源在多少個地方被使用,此變量稱為引用計數。當某處使用完資源以后,將引用計數減1。當引用計數為0時,即沒有任何地方再使用此資源時,真正釋放此資源。這里的資源,在動態內存管理中就是指堆內存。
用一句話描述就是:誰最后使用資源,誰負責釋放資源。
我們很容易聯想到現實中的例子,就是日常生活中的刷碗問題的解決方案,即誰最后吃完誰刷碗。
需要說明的是,引用計數不僅僅是在內存管理中使用,它是一個通用的機制,凡是涉及到資源管理的問題,都可以考慮使用引用計數。
下面將要介紹基于引用計數原理的兩種衍生的機制:顯式共享和隱式共享。
顯式共享
顯式共享,是僅僅使用引用計數控制資源的生命周期的一種共享管理機制。這種機制下,無論資源在何處被引用,自始至終所有引用指向資源都是同一個。
之所以叫顯式共享,是因為這種共享方式很直接,沒有隱含的操作,如:Copy on Write寫時拷貝(見隱式共享的相關說明)。如果想要拷貝并建立新的引用計數,必須手動調用detach()函數。
從使用者的角度看,從頭到尾資源只有一份,一個地方修改了,另一個地方就能讀取到修改后的資源。
**相關Qt類:**QExplicitlySharedDataPointer,更加深入的用法和編碼,需要參考Qt文檔中的相關說明及Demo。
隱式共享
隱式共享,也是一種基于引用計數的控制資源的生命周期的共享管理機制。
隱式共享,對不同的操作有不同的處理:
-
讀取時,在所有引用的地方使用同一個資源;
-
在寫入、修改時自動復制一份資源出來做修改,自動脫離原始的引用計數,因為是新的資源,所以要建立新的引用計數。這種操作叫Copy on Write寫時復制技術,是自動隱含進行的。
從使用者的角度看,每個使用者都像是擁有獨立的一份資源。在一個地方修改,修改的只是原始資源的拷貝,不會影響原始資源的內容,自然就不會影響到其他使用者。所以這種共享方式稱為隱式共享。
相關Qt類有QString、QByteArray、QImage、QList、QMap、QHash等。
推薦閱讀:Qt文檔中的Implicit Sharing專題。
智能指針
智能指針是對C/C++指針的擴展,同樣基于引用計數。
智能指針和顯示共享和隱式共享有何區別?它們區別是:智能指針是輕量級的引用計數,它將顯式共享、隱式共享中的引用計數實現部分單獨提取了出來,制作成模板類,形成了多種特性各異的指針。
例如,QString除了實現引用計數,還實現了字符串相關的豐富的操作接口。QList也實現了引用計數,還實現了列表這種數據結構的各種操作。可以說,顯式共享和隱式共享一般是封裝在功能類中的,不需要開發者來管理。
智能指針將引用計數功能剝離出來,為Qt開發者提供了便捷的引用計數基礎設施。
強(智能)指針
Qt中的強指針實現類是:QSharedPointer,此類是模板類,可以指向多種類型的數據,主要用來管理堆內存。關于QSharedPointer在Qt Assistant中有詳細描述。
它的原理和顯式共享一樣:最后使用的地方負責釋放刪除資源,如類對象、內存塊。
強指針中的“強”,是指每多一個使用者,引用計數都會老老實實地**+1**。而弱指針就不同,下面就接著講解弱指針。
弱(智能)指針
Qt中的弱指針實現類是QWeakPointer,此類亦為模板類,可以指向多種類型的數據,同樣主要用來管理堆內存。關于QWeakPointer在Qt Assistant中有詳細描述。
弱指針只能從強指針QSharedPointer轉化而來,獲取弱指針,不增加引用計數,它只是一個強指針的觀察者,觀察而不干預。只要強指針存在,弱指針也可以轉換成強指針。可見弱指針和強指針是一對形影不離的組合,通常結合起來使用。
局部指針
局部指針,是一種超出作用域自動刪除、釋放堆內存、對象的工具。它結合了棧內存管理和堆內存管理的優點。
Qt中的實現類有:QScopedPointer,QScopedArrayPointer,具體可以參考Qt Assistant。
觀察者指針
上面說弱指針的時候,講到過觀察者。觀察者是指僅僅做查詢作用的指針,不會影響到引用計數。
Qt中的觀察者指針是QPointer,它必須指向QObject的子類對象,才能對對象生命周期進行觀察。因為只有QObject子類才會在析構的時候通知QPointer已失效。
QPointer是防止懸掛指針(即野指針)的有效手段,因為所指對象一旦被刪除,QPointer會自動置空,在使用時,判斷指針是否為空即可,不為空說明對象可以使用,不會產生內存訪問錯誤的問題。
總結
本篇文章講解了Qt中的各種內存管理機制,算是做了一個比較全面的描述。
之所以說是必讀,是因為筆者在工作中發現,內存管理確實非常重要。Qt內存管理機制是貫穿整個Qt中所有類的核心線索之一,搞懂了內存管理
- 能在腦海中形成內存中對象的布局圖,寫代碼的時候才能下筆如有神,管理起項目中眾多的對象才能游刃有余,提高開發效率;
- 能夠減少bug的產生。有經驗的開發者應該知道,內存問題很難調試定位到具體的位置,往往導致奇怪的bug出現。
- 能夠幫助理解Qt眾多類的底層不變的邏輯,學起來更容易。
本文只是對Qt中內存管理進行了梳理,無法涵蓋很多細節問題,讀者需要花一些時間去詳細閱讀Qt助手文檔,最好是寫幾個demo測試驗證。花時間是值得的,因為技術是日新月異的,但是核心的原理變化是不大的。Qt中的內存管理思想和方法,在很多語言、框架中(Python、Objective C、JavaScript等等)都有類似的應用。
值得一提的是,之所以Qt中具有各種各樣的內存管理方式,是因為它能夠減輕開發者的負擔,更加專注于業務代碼的實現,而不是被內存問題折騰的焦頭爛額。不使用Qt中的內存管理,只用C的手動內存管理仍然可以寫可以運行的代碼!前提是不考慮成本問題,并假設開發者在內存問題上不會犯錯。總之一句話,不要對立各種技術,每種技術都有適用的場景,拋開場景談方法都是不理智的。
/*********************************************************************
內存為程序分配空間的四種分配方式
存儲器是個寶貴但卻有限的資源。一流的操作系統,需要能夠有效地管理及利用存儲器。
內存為程序分配空間有四種分配方式:
- 1、連續分配方式
- 2、基本分頁存儲管理方式
- 3、基本分段存儲管理方式
- 4、段頁式存儲管理方式
連續分配方式
首先講連續分配方式。連續分配方式出現的時間比較早,曾廣泛應用于20世紀60~70年代的OS中,但是它至今仍然在內存管理方式中占有一席之地,原因在于它實現起來比較方便,所需的硬件支持最少。連續分配方式又可細分為四種:單一連續分配、固定分區分配、動態分區分配和動態重定位分區分配。
其中固定分區的分配方式,因為分區固定,所以缺乏靈活性,即當程序太小時,會造成內存空間的浪費(內部碎片);程序太大時,一個分區又不足以容納,致使程序無法運行(外部碎片)。但盡管如此,當一臺計算機去控制多個相同對象的時候,由于這些對象內存大小相同,所以完全可以采用這種內存管理方式,而且是最高效的。這里我們可以看出存儲器管理機制的多面性:沒有那種存儲器管理機制是完全沒有用的,在適合的場合下,一種被認為最不合理的分配方案卻可能稱為最高效的分配方案。一切都要從實際問題出發,進行設計。
為了解決固定分區分配方式的缺乏靈活性,出現了動態分配方式。動態分配方式采用一些尋表(Eg:空閑鏈表)的方式,查找能符合程序需要的空閑內存分區。但代價是增加了系統運行的開銷,而且內存空閑表本身是一個文件,必然會占用一部分寶貴的內存資源,而且有些算法還會增加內存碎片。
可重定位分區分配通過對程序實現成定位,從而可以將內存塊進行搬移,將小塊拼成大塊,將小空閑“緊湊”成大空閑,騰出較大的內存以容納新的程序進程。
基本分頁存儲管理方式
連續分配方式會形成許多“碎片”,雖然可以通過“緊湊”方式將許多碎片拼接成可用的大塊空間,但須為之付出很大開銷。所以提出了“離散分配方式”的想法。如果離散分配的基本單位是頁,則稱為分頁管理方式;如果離散分配的基本單位是段,則稱為分段管理方式。
分頁存儲管理是將一個進程的邏輯地址空間分成若干個大小相等的片,稱為頁面或頁,并為各頁加以編號,從0開始,如第0頁、第1頁等。相應地,也把內存空間分成與頁面相同大小的若干個存儲塊,稱為(物理)塊或頁框(frame),也同樣為它們加以編號,如0#塊、1#塊等等。在為進程分配內存時,以塊為單位將進程中的若干個頁分別裝入到多個可以不相鄰接的物理塊中。由于進程的最后一頁經常裝不滿一塊而形成了不可利用的碎片,稱之為“頁內碎片”。
在分頁系統中,允許將進程的各個頁離散地存儲在內存不同的物理塊中(所以能實現離散分配方式),但系統應能保證進程的正確運行,即能在內存中找到每個頁面所對應的物理塊。為此,系統又為每個進程建立了一張頁面映像表,簡稱頁表。在進程地址空間內的所有頁,依次在頁表中有一頁表項,其中記錄了相應頁在內存中對應的物理塊號。在配置了頁表后,進程執行時,通過查找該表,即可找到每頁在內存中的物理塊號。可見,頁表的作用是實現從頁號到物理塊號的地址映射。
為了能夠將用戶地址空間中的邏輯地址,變換為內存空間中的物理地址,在系統中必須設置地址變換機構。地址變換任務是借助于頁表來完成的。
頁表的功能可由一組專門的寄存器來實現。由于寄存器成本較高,且大多數現代計算機的頁表又很大,使頁表項總數可達幾千甚至幾十萬個,顯然這些頁表項不可能都用寄存器來實現,因此,頁表大多駐留在內存中。因為一個進程可以通過它的PCB來時時保存自己的狀態,等到CPU要處理它的時候才將PCB交給寄存器,所以,系統中雖然可以運行多個進程,但也只需要一個頁表寄存器就可以了。
由于頁表是存放在內存中的,這使得CPU在每存取一個數據時,都要兩次訪問內存。為了提高地址變換速度,在地址變化機構中增設了一個具有并行查詢能力的高速緩沖寄存器,又稱為“聯想寄存器”(Associative Lookaside Buffer)。
在單級頁表的基礎上,為了適應非常大的邏輯空間,出現了兩級和多級頁表,但是,他們的原理和單級頁表是一樣的,只不過為了適應地址變換層次的增加,需要在地址變換機構中增設外層的頁表寄存器。
基本分段存儲管理方式
分段存儲管理方式的目的,主要是為了滿足用戶(程序員)在編程和使用上多方面的要求,其中有些要求是其他幾種存儲管理方式所難以滿足的。因此,這種存儲管理方式已成為當今所有存儲管理方式的基礎。
- (1)方便編程;
- (2)信息共享:分頁系統中的“頁”只是存放信息的物理單位(塊),并無完整的意義,不便于實現共享;然而段位卻是信息的邏輯單位。由此可知,為了實現段的共享,希望存儲器管理能與用戶程序分段的組織方式相適應。
- (3)信息保護;
- (4)動態增長;
- (5)動態鏈接。
分段管理方式和分頁管理方式在實現思路上是很相似的,只不過他們的基本單位不同。分段有段表,也有地址變換機構,為了提高檢索速度,同樣增設聯想寄存器(具有并行查詢能力的高速緩沖寄存器)。所以有些具體細節在這個不再贅述。
分頁和分段的主要區別:
1、兩者相似之處:兩者都采用離散分配方式,且都要通過地址映射機構來實現地址變換。
2、兩者的不同之處:
(1)頁是信息的物理單位,分頁是為實現離散分配方式,以消減內存的外零頭,提高內存的利用率。或者說,分頁僅僅是由于系統管理的需要而不是用戶的需要。段則是信息的邏輯單位,它含有一組其意義相對完整的信息。分段的目的是為了能更好地滿足用戶的需要。
(2)頁的大小固定且由系統決定,而段的長度卻不固定。
(3)分頁的作業地址空間是一維的,即單一的線性地址空間;而分段的作業地址空間則是二維的。
段頁式存儲管理方式
前面所介紹的分頁和分段存儲管理方式都各有優缺點。分頁系統能有效地提高內存利用率,而分段系統則能很好地滿足用戶需求。我們希望能夠把兩者的優點結合,于是出現了段頁式存儲管理方式。
段頁式系統的基本原理,是分段和分頁原理的結合,即先將用戶程序分成若干個段,再把每個段分成若干個頁,并為每一個段賦予一個段名。在段頁式系統中,地址結構由段號、段內頁號和頁內地址三部分組成。
和前兩種存儲管理方式相同,段頁式存儲管理方式同樣需要增設聯想寄存器。
離散分配方式基于將一個進程直接分散地分配到許多不相鄰的分區中的思想,分為分頁式存儲管理,分段式存儲管理和段頁式存儲管理. 分頁式存儲管理旨在提高內存利用率,滿足系統管理的需要,分段式存儲管理則旨在滿足用戶(程序員)的需要,在實現共享和保護方面優于分頁式存儲管理,而段頁式存儲管理則是將兩者結合起來,取長補短,即具有分段系統便于實現,可共享,易于保護,可動態鏈接等優點,又能像分頁系統那樣很好的解決外部碎片的問題,以及為各個分段可離散分配內存等問題,顯然是一種比較有效的存儲管理方式。
/**************************************************************************
當我們在使用Qt時不可避免得需要接觸到內存的分配和使用,即使是在使用Python,Golang這種帶有自動垃圾回收器(GC)的語言時我們仍然需要對Qt的內存管理機制有所了解,以更加清楚的認識Qt對象的生命周期并在適當的時機加以控制或者避免進入陷阱。
這篇文章里我們將學習QObject & parent對象管理機制,以及QWidget與內存管理這兩點Qt的基礎知識。
QObject和內存管理
在Qt中,我們可以大致把對象分為兩類,一類是QObject和它的派生類;另一類則是普通的C++類。
對于第二種對象,它的生命周期與管理和普通的C++類基本沒有區別,而QObject和它的派生類則有以下的顯著區別:
- QObject和其派生類可以使用SIGNAL/SLOT機制
- 它們一般會有一個parent父對象的指針,用于內存管理(后面重點說明)
- 對于QWidget和其派生類來說,內存管理要稍微復雜一些,因為QWidget需要和eventloop高度配合才能工作(后面也會重點說明)
signal和slot一般來說并不會對內存管理產生影響,但是對close()槽的處理會對QWidget產生一些影響,所以我們放在后面講解。
那么先來看一下QObject和parent機制。
QObject的parent
我們時常能看到QWidget或者其他的控件的構造函數中有一項參數parent,默認值都為NULL,例如:
| QLineEdit(const QString &contents, QWidget *parent = nullptr); | |
| QWidget(QWidget *parent = nullptr, Qt::WindowFlags f = ...); |
這個parent的作用就在于使當前的對象實例加入parent指定的QObject及其派生類的children中,當一個QObject被delete或者調用了它的析構函數時,所有加入的children也會全部被析構。
如果parent設置為NULL,會有如下的情況:
- 如果是構造時直接指定了NULL,那么當前實例不會有父對象存在,Qt也不能自動析構該實例除非實例超出作用域導致析構函數被調用,或者用戶在恰當的實際使用delete操作符或者使用deleteLater方法;
- 如果已經指定了非NULL的parent,這時將它設置成了NULL,那么當前實例會從父對象的children中刪除,不再受到QObject & parent機制的影響;
- 對于QWidget,parent為NULL時代表其為一個頂層窗口,也可以就是獨立于其他widget在系統任務欄單獨出現的widget,對于永遠都是頂層窗口的widget,例如QDialog,當parent不為NULL時他會顯示在父widget中心區域的上層;
- 如果QWidget的parent為NULL或是其他值,在其加入布局管理器或者QMainWindow設置widget時,會自動將parent設置為相應的父widget,在父控件銷毀時這些子控件以及布局管理器對象會一并銷毀。
所以我們可以看出,QObject對象實際上擁有一顆類實例關系樹,在樹中保存了所有通過指定parent注冊的子對象,而子對象里又保存有其子對象的關系樹,所以當一個父對象被銷毀時,所有依賴或間接依賴于它的對象都會被正確的釋放,使用者無需手動管理這些資源的釋放操作。
基于此原理,我們可以放心的讓Qt管理資源,這里有幾個建議:
QWidget和內存的釋放
QWidget也是QObject的子類,所以在parent機制上是沒有區別的,然而實際使用時我們更多的是使用“關閉”(close)而不是delete去刪除控件,所以差異就出現了。
先提一下widget關閉的流程,首先用戶觸發close()槽,然后Qt向widget發送QCloseEvent,默認的QCloseEvent會做如下處理:
我們可以看到,widget的關閉實際是將其隱藏,而沒有釋放內存,雖然我們有時會重寫closeEvent但也不會手動釋放widget。
看一個因為close機制導致的內存泄漏的例子,我們在button被單擊后彈出某個自定義對話框:
| button.ConnectClicked(func (_ bool) { | |
| dialog := NewMyDialog() | |
| dialog.Exec() | |
| }) |
因為dialog在close時會被隱藏,而且沒有設置DeleteOnClose,所以Qt不會去釋放dialog,而用戶也無法回收dialog的資源,也行你會說golang的GC不是能處理這種情況嗎,然而遺憾的是GC并不能處理cgo分配的資源,所以如果你期望GC做善后的話恐怕要失望了,每次點擊按鈕后內存用量都會增加一點,沒錯,內存泄露了。
那么給dialog設置一個parent,像這樣,會如何呢?
| dialog.SetParent(self) |
遺憾的是,并沒有什么區別,因為這樣只是把dialog加入父控件的children,并沒有刪除dialog,只有父對象被銷毀時內存才會真正釋放。
解決辦法也有三個。
第一種是使用deleteLater,例如:
| dialog.DeleteLater() |
這會通知Qt的eventloop在下次進入主循環的時候析構dialog,這樣一來確實解決了內存泄露,不過缺點是會有不可預測的延遲存在,有時候延遲是難以接受的。
第二種是手動刪除widget,適用于parent為NULL的場合:
C++:
| delete dialog; |
golang:
| dialog.DestroyMyDialog() |
說明一下,DestroyType也是qtmoc生產的幫助函數,因為golang沒有析構函數的概念,所以goqt使用生成的該幫助函數顯示調用底層C++對象的析構函數。
第三種比較簡單,對于單純顯示而不需要和父控件做交互的widget,直接設置DeleteOnClose即可,close時widget會被自動析構。
當然對于PyQt5來說并不會存在如上的問題,sip庫能很好的與python的GC一起工作。唯一需要注意的是有時底層C++對象已經被釋放,但是上層python對象依然存在,這時使用該對象將導致拋錯。
/********************************************************************************
qt 如何 指針 自動 釋放內存_C++|程序中的內存操作、管理
程序加載到內存后代碼存儲到代碼區,并將全局變量、靜態變量初始化到全局/靜態內存區,然后會分配2M左右的棧內存區用于存儲局部變量,并在運行時根據需要可以在堆內存區(空閑內存區及硬盤的虛擬內存區)申請空間。
程序可使用的內存分區↓
各基本類型所需的字節長度↓
?
程序中的輸入、輸出與內存↓
?
內存本質上是一個線性結構↓
1 內存分配方式
內存分配方式有三種:
?
(1)從靜態存儲區域分配。內存在程序編譯的時候就已經分配好,這塊內存在程序的整個運行期間都存在。例如全局變量,static 變量。
(2)在棧上創建。在執行函數時,函數內局部變量的存儲單元都可以在棧上創建, 函數執行結束時這些存儲單元自動被釋放。棧內存分配運算內置于處理器的指令集中,效率很高,但是分配的內存容量有限。
(3)從堆上分配,亦稱動態內存分配。程序在運行的時候用 malloc 或 new 申請任意多少的內存,程序員自己負責在何時用 free 或 delete 釋放內存。動態內存的生存期由我們決定,使用非常靈活,但問題也最多。
2 常見的內存錯誤及其對策
發生內存錯誤是件非常麻煩的事情。編譯器不能自動發現這些錯誤,通常是在程序運行時才能捕捉到。而這些錯誤大多沒有明顯的癥狀,時隱時現,增加了改錯的難度。有時用戶怒氣沖沖地把你找來,程序卻沒有發生任何問題,你一走,錯誤又發作了。
常見的內存錯誤及其對策如下:
2.1 內存分配未成功,卻使用了它
編程新手常犯這種錯誤,因為他們沒有意識到內存分配會不成功。常用解決辦法是,在使用內存之前檢查指針是否為 NULL。如果指針 p 是函數的參數,那么在函數的入口處用 assert(p!=NULL)進行檢查。如果是用 malloc 或 new 來申請內存,應該用 if(p==NULL) 或 if(p!=NULL)進行防錯處理。
2.2 內存分配雖然成功,但是尚未初始化就引用它
犯這種錯誤主要有兩個起因:一是沒有初始化的觀念;二是誤以為內存的缺省初值全為零,導致引用初值錯誤(例如數組)。
內存的缺省初值究竟是什么并沒有統一的標準,盡管有些時候為零值,我們寧可信其無不可信其有。所以無論用何種方式創建數組,都別忘了賦初值,即便是賦零值也不可省略,不要嫌麻煩。
2.3 內存分配成功并且已經初始化,但操作越過了內存的邊界
例如在使用數組時經常發生下標“多 1”或者“少 1”的操作。特別是在 for循環語句中,循環次數很容易搞錯,導致數組操作越界。
2.4 忘記了釋放內存,造成內存泄露
含有這種錯誤的函數每被調用一次就丟失一塊內存。剛開始時系統的內存充足,你看不到錯誤。終有一次程序突然死掉,系統出現提示:內存耗盡。
動態內存的申請與釋放必須配對,程序中 malloc 與 free 的使用次數一定要相同,否則肯定有錯誤(new/delete 同理)。
2.5 釋放了內存卻繼續使用它
有三種情況:
(1)程序中的對象調用關系過于復雜,實在難以搞清楚某個對象究竟是否已經釋放了內存,此時應該重新設計數據結構,從根本上解決對象管理的混亂局面。
(2)函數的 return 語句寫錯了,注意不要返回指向“棧內存”的“指針”或者“引用”,因為該內存在函數體結束時被自動銷毀。
(3)使用 free 或 delete 釋放了內存后,沒有將指針設置為 NULL。導致產生“ 野指針”。
對策:
【1】用 malloc 或 new 申請內存之后,應該立即檢查指針值是否為 NULL。防止使用指針值為 NULL 的內存。【2】不要忘記為數組和動態內存賦初值。防止將未被初始化的內存作為右值使用。【3】避免數組或指針的下標越界,特別要當心發生“多 1”或者“少 1”操作。【4】動態內存的申請與釋放必須配對,防止內存泄漏。【5】用 free 或 delete 釋放了內存之后,立即將指針設置為 NULL,防止產生“野指針”。
3 字符串的三種存儲空間
字符串可以存儲在棧區、堆區、或常量空間:
char str1[] = "abc";// 字符串存儲在棧中char str2[] = "abc";char* str3 = "abc";// 字符串存儲在常量區char* str4 = "abc";// 嚴格的寫法應該是 const char* str4 = "abc";char* str5 = (char*)malloc(4);// 字符串存儲在堆中char* str6 = (char*)malloc(4);str4[0]='x'; // 編譯錯誤
4 內存操作函數
在頭文件中主要有C風格字符串的操作函數以外,還有一類mem系列函數,主要是用來操作內存(不止字符串的操作):
?
①void *memset(void*s ,int ch,size_t n);將內存地址s處的n個字節的每個字節都替換為ch,并返回s。②void *memcmp(const void*buf1,const void *buf2,unsigned int count);比較內存區域buf1和buf2的前count個字節③void *memcpy(void* d,const void*s,size_t n)內存拷貝,將地址s位置的連續n個字節的內容復制到從地址d開始的內存空間上來。④void *memmove(void* dest,const void* src,size_t count);由src所指的內存區域復制count個字節到dest所指的內存區域。⑤void *memchr(const void *buf, int ch, size_t count)從buf所指內存區域的前count個字節查找字符ch,返回指向ch的指針⑥void* memccpy(void* dest, void* src, unsigned char ch, unsigned int count)由src所指內存區域復制不大于count個字節到dest所指內存區域,如果遇到字符ch則停止復制,返回值為NULL,如果ch沒有被復制,返回值為一個指向緊接著dest區域后的字符指針。⑦int memicmp(void* buf1, void* buf2, unsigned int count)比較內存中字符的大小(不區分大小寫)
5 計算內存容量
用運算符 sizeof 可以計算出數組的容量(字節數)。如有數組a,sizeof(a)的值就是其數組元素加上’0’的字節總和。指針 p 指向 a,但是 sizeof(p) 的值卻是 4。這是因為 sizeof(p)得到的是一個指針變量的字節數,相當于sizeof(char*),而不是 p 所指的內存容量。C++/C 語言沒有辦法知道指針所指的內存容量,除非在申請內存時記住它。
char a[] = "hello world";char *p = a;cout<< sizeof(a) << endl; // 12 字節cout<< sizeof(p) << endl; // 4 字節
當數組作為函數的參數進行傳遞時,該數組自動退化為同類型的指針。不論數組 的容量是多少,sizeof(a)始終等于 sizeof(char*)。
void Func(char a[100]){cout<< sizeof(a) << endl; // 4 字節而不是 100 字節}
6 二級指針參數可以傳遞內存
如果函數的參數是一個指針,不要指望用該指針去申請動態內存。void GetMemory(char *p, int num){p = (char *)malloc(sizeof(char) * num);//指針要解引用操作才可以改變}void Test(void){char *str = NULL;GetMemory(str, 100);// str 仍然為 NULLstrcpy(str, "hello");// 運行錯誤}
如果非得要用指針參數去申請內存,那么應該改用“指向指針的指針”:
void GetMemory2(char **p, int num){*p = (char *)malloc(sizeof(char) * num);//指針p的解引用*p}void Test2(void){char *str = NULL;GetMemory2(&str, 100);// 注意參數是 &str,而不是 strstrcpy(str, "hello");cout<< str << endl;free(str);}
7 free 和delete 后的指針其地址值并未改變(也未置NULL),只是釋放了指針指向的內存
別看 free 和 delete 的名字惡狠狠的(尤其是 delete),它們只是把指針所指的內存給釋放掉,但并沒有把指針本身干掉。
如下例,指針 p 被 free 以后其地址仍然不變(非 NULL), 只是該地址對應的內存是垃圾,p 成了“野指針”。如果此時不把 p 設置為 NULL, 會讓人誤以為 p 是個合法的指針。
char *p = (char *) malloc(100);strcpy(p, “hello”);free(p); // p 所指的內存被釋放,但是 p 所指的地址仍然不變…if(p != NULL) // 沒有起到防錯作用{strcpy(p, “world”); // 出錯}
如果程序比較長,我們有時記不住 p 所指的內存是否已經被釋放,在繼續使用 p 之前,通常會用語句 if (p != NULL)進行防錯處理。很遺憾,此時 if 語句起不到防錯作用,因為即便 p 不是 NULL 指針,它也不指向合法的內存塊。
8 動態內存在運行出作用域時并不會被自動釋放
函數體內的局部變量在函數結束時自動消亡。很多人誤以為以下代碼是正確的。理由是 p 是局部的指針變量,它消亡的時候會讓它所指的動態內存一起完蛋。這是錯覺!
void Func(void){char *p = (char *) malloc(100); // 動態內存會自動釋放嗎?}
我們發現指針有一些“似是而非”的特征:
(1)指針消亡了,并不表示它所指的內存會被自動釋放。(2)內存被釋放了,并不表示指針會消亡或者成了NULL 指針。
9 杜絕“野指針”
“野指針”不是 NULL 指針,是指向“垃圾”內存的指針。人們一般不會錯用 NULL 指針,因為用 if 語句很容易判斷。但是“野指針”是很危險的,if 語句對它不起作用。
“野指針”的成因主要有兩種:
(1)指針變量沒有被初始化。任何指針變量剛被創建時不會自動成為 NULL 指針,它的缺省值是隨機的,它會亂指一氣。所以,指針變量在創建的同時應當被初始化,要么將指針設置為 NULL,要么讓它指向合法的內存。例如
char *p = NULL;char *str = (char *) malloc(100);
(2)指針 p 被 free 或者 delete 之后,沒有置為 NULL,讓人誤以為 p 是個合法的指針。參見 5 節。
(3)指針操作超越了變量的作用范圍。這種情況讓人防不勝防,示例程序如下:
class A{public:void Func(void){ cout << “Func of class A” << endl; }};void Test(void){A *p;{A a;p = &a; // 注意 a 的生命期}p->Func(); // p 是“野指針”}
函數 Test 在執行語句 p->Func()時,對象 a 已經消失,而 p 是指向 a 的,所以 p 就成了“野指針”。但奇怪的是我運行這個程序時居然沒有出錯,這可能與編譯器有關。
10 有了 malloc/free 為什么還要new/delete ?
malloc 與 free 是 C++/C 語言的標準庫函數,new/delete 是 C++ 的運算符。它們都可用于申請動態內存和釋放內存。
對于非內部數據類型的對象而言,光用 maloc/free 無法滿足動態對象的要求。對象在創建的同時要自動執行構造函數,對象在消亡之前要自動執行析構函數。由于 malloc/free 是庫函數而不是運算符,不在編譯器控制權限之內,不能夠把執行構造函數和析構函數的任務強加于 malloc/free。
因此 C++語言需要一個能完成動態內存分配和初始化工作的運算符 new,以及一個能完成清理與釋放內存工作的運算符 delete。注意 new/delete 不是庫函數。
所以我們不要企圖用 malloc/free 來完成動態對象的內存管理,應該用new/delete。由于內部數據類型的“對象”沒有構造與析構的過程,對它們而言malloc/free 和 new/delete 是等價的。
既然 new/delete 的功能完全覆蓋了 malloc/free,為什么 C++不把malloc/free 淘汰出局呢?這是因為 C++程序經常要調用 C 函數,而 C 程序只能用 malloc/free 管理動態內存。
如果用 free 釋放“new 創建的動態對象”,那么該對象因無法執行析構函數而可能導致程序出錯。如果用 delete 釋放“malloc 申請的動態內存”,理論上講程序不會出錯,但是該程序的可讀性很差。所以 new/delete 必須配對使用, malloc/free 也一樣。
11 各類別內存空間耗盡了怎么辦?
如果在申請動態內存時找不到足夠大的內存塊,malloc 和 new 將返回 NULL 指針,宣告內存申請失敗。通常有三種方式處理“內存耗盡”問題。
11.1判斷指針是否為 NULL,如果是則馬上用 return 語句終止本函數。例如:
void Func(void){A *a = new A;if(a == NULL){return;}…}
11.2判斷指針是否為 NULL,如果是則馬上用 exit(1)終止整個程序的運行。例如:
void Func(void){A *a = new A;if(a == NULL){cout << “Memory Exhausted” << endl;exit(1);}…}
11.3 為 new 和 malloc 設置異常處理函數。
例如 Visual C++可以用_set_new_hander 函數為 new 設置用戶自己定義的異常處理函數,也可以讓malloc 享用與 new 相同的異常處理函數。
上述11.1、11.2方式使用最普遍。如果一個函數內有多處需要申請動態內存,那么方式11.1就顯得力不從心(釋放內存很麻煩),應該用方式11.2來處理。
很多人不忍心用 exit(1),問:“不編寫出錯處理程序,讓操作系統自己解決行不行?”
不行。如果發生“內存耗盡”這樣的事情,一般說來應用程序已經無藥可救。如果不用 exit(1) 把壞程序殺死,它可能會害死操作系統。
有一個很重要的現象要告訴大家。對于 32 位以上的應用程序而言,無論怎樣使用 malloc 與 new,幾乎不可能導致“內存耗盡”。如以下程序會無休止地運行下去,根本不會終止。因為 32 位操作系統支持“虛擬內存”,內存用完了,自動用硬盤空間頂替。我只聽到硬盤嘎吱嘎吱地響,OS已經累得對鍵盤、鼠標毫無反應。
// “內存耗盡”測試程序void main(void){float *p = NULL;while(TRUE){p = new float[1000000];cout << “eat memory” << endl;if(p==NULL)exit(1);}}
12 malloc/free 的使用要點
函數 malloc 的原型如下:
void * malloc(size_t size);
用 malloc 申請一塊長度為 length 的整數類型的內存,程序如下:
int *p = (int *) malloc(sizeof(int) * length);
我們應當把注意力集中在兩個要素上:“類型轉換”和“sizeof”。
(1)malloc 返回值的類型是 void *,所以在調用 malloc 時要顯式地進行類型轉換,將 void * 轉換成所需要的指針類型。
(2)malloc 函數本身并不識別要申請的內存是什么類型,它只關心內存的總字節數。我們通常記不住 int、float 等數據類型的變量的確切字節數使用sizeof()即可。
在 malloc 的“()”中使用 sizeof 運算符是良好的風格,但要當心有時我們會昏了頭,寫出 p = malloc(sizeof(p))這樣的程序來。
(3)函數 free 的原型如下:
void free( void * memblock );
為什么 free 函數不象 malloc 函數那樣復雜呢?這是因為指針 p 的類型以及它所指的內存的容量事先都是知道的,語句 free(p)能正確地釋放內存。如果 p 是 NULL 指針,那么 free 對 p 無論操作多少次都不會出問題。如果 p 不是NULL 指針,那么 free 對 p 連續操作兩次就會導致程序運行錯誤。
13 new/delete 的使用要點
運算符 new 使用起來要比函數 malloc 簡單得多,例如:
int *p1 = (int *)malloc(sizeof(int) * length);
int *p2 = new int[length];
這是因為 new 內置了 sizeof、類型轉換和類型安全檢查功能。對于非內部數據類型的對象而言,new 在創建動態對象的同時完成了初始化工作。如果對象有多個構造函數,那么 new 的語句也可以有多種形式。例如
class Obj{public :Obj(void); // 無參數的構造函數Obj(int x); // 帶一個參數的構造函數…};void Test(void){Obj*a = new Obj;Obj*b = new Obj(1);// 初值為 1…delete a;delete b;}
如果用 new 創建對象數組,那么只能使用對象的無參數構造函數。例如
Obj *objects = new Obj[100]; // 創建 100 個動態對象
不能寫成
Obj *objects = new Obj[100](1);// 創建 100 個動態對象的同時賦初值 1
在用 delete 釋放對象數組時,留意不要丟了符號‘[]’。例如
delete []objects; // 正確的用法
delete objects; // 錯誤的用法
后者相當于 delete objects[0],漏掉了另外 99 個對象。
?
總結
以上是生活随笔為你收集整理的C++中运行一个程序的内存分配情况及qt中的内存管理机制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Ubuntu 16.04 快捷键截图
- 下一篇: python中value的含义_pyth