一.內存的分配方式
1. 程序代碼區
2. 靜態數據區
3. 動態數據區
二.動態內存
1. 在棧上創建的內存
2. 從堆上分配的內存
3. 小結
三.指針與內存
1. 操作內存
2. 指針與數組
3. 指針參數
四.malloc/free 與new/delete
1. malloc/free 的使用要點
2. new/delete 的使用要點
3. malloc/free 與new/delete 的比較..
五.常見內存錯誤
1.內存泄露
2.內存越界訪問
3.野指針
4.內存分配未成功,卻使用了它
5.內存分配雖然成功,但是尚未初始化就引用它
5.返回指向臨時變量的指針
6.試圖修改常量
7.誤解傳值與傳引用
六.參考文獻
一.內存的分配方式
對於一個進程的內存空間而言,可以在邏輯上分成個部份:
1. 程序代碼區
存放函數體的二進制代碼.
2. 靜態數據區
存放全局變量(global)和靜態變量(static).初始化的全局變量和靜態變量在一塊區域,未初始化的全局變量和未初始化的靜態變量在相鄰的另一塊區域.程序結束後由系統釋放.
3. 動態數據區
動態數據區一般就是"堆"和"棧","堆"和"棧"是兩種不同的動態數據區,
棧(stack)是一種線性結構.由編譯器自動分配釋放,存放函數的參數值,局部變量,本地變量的值等.其操作方式類似於數據結構中的棧.
堆(heap)是一種鏈式結構.一般由程序員分配釋放,若程序員不釋放,則會造成內存泄漏.注意它與數據結構中的堆是兩回事.
例1: 內存分配方式舉例
#include <iostream>
using namespace std;
int a = 0; // 全局初始化區
char *p1; // 全局未初始化區
int main()
{
int b; // 棧
char s[] = "abc"; // 棧
char *p2; // 棧
char *p3 = "12345"; // 12345/0 在常量區, p3在棧上
static int c = 0; // 靜態初始化區
p1 = new char(10); // 分配而來的10 和20 字節的區域就在堆區
p2 = new char(20);
strcpy(p1, "12345"); // 12345/0 在常量區
// 編譯器可能會將它與p3 所指向的"12345"優化成一個地方
return 0;
}
例2: 各種變量的分配方式
#include <iostream>
using namespace std;
int g1 = 0;
int g2 = 0;
int g3 = 0;
int main()
{
static int s1 = 0;
static int s2 = 0;
static int s3 = 0;
int v1 = 0;
int v2 = 0;
int v3 = 0;
/* 打印出各個變量的內存地址 */
cout << "本地變量的內存地址:" << endl;
cout << "0x" << &v1 << endl;
cout << "0x" << &v2 << endl;
cout << "0x" << &v3 << endl;
cout << endl;
cout << "全局變量的內存地址:" << endl;
cout << "0x" << &g1 << endl;
cout << "0x" << &g2 << endl;
cout << "0x" << &g3 << endl;
cout << endl;
cout << "靜態變量的內存地址:" << endl;
cout << "0x" << &s1 << endl;
cout << "0x" << &s2 << endl;
cout << "0x" << &s3 << endl;
cout << endl;
return 0;
}
/*
運行結果
本地變量的內存地址:
0x0012FF7C
0x0012FF78
0x0012FF74
全局變量的內存地址:
0x0047CDEC
0x0047CDF0
0x0047CDF4
靜態變量的內存地址:
0x0047CDF8
0x0047CDFC
0x0047CE00
*/
程序中v1,v2,v3是本地變量,g1,g2,g3是全局變量,s1,s2,s3是靜態變量.
可以看出每一組變量在內存中是連續分佈的,但是本地變量和全局變量分配的內存地址差了十萬八千里,而全局變量和靜態變量分配的內存是連續的.這是因爲本地變量和全局/靜態變量是分配在不同類型的內存區域中的結果.
├———————┤低端內存區域
│……│
├———————┤
│動態數據區│
├———————┤
│……│
├———————┤
│代碼區│
├———————┤
│靜態數據區│
├———————┤
│……│
├———————┤高端內存區域
二.動態內存
在程序中,最常用的,也是最難掌握的,就是動態內存了.下面就動態內存的兩種分配方式來討論一下動態內存的使用.
1. 在棧上創建的內存.
在執行函數時,函數內局部變量的存儲單元都可以在棧上創建,函數執行結束時這些存儲單元自動被釋放.棧內存分配運算內置於處理器的指令集中,效率很高,但是分配的內存容量有限.
在棧中分配的空間的生命期與這個變量所在的函數和類相關.如果是函數中定義的局部變量,那麼它的生命期就是函數被調用時,如果函數運行結束,那麼這塊內存就會被回收.如果是類中的成員變量,則它的生命期與類實例的生命期相同.
例3: 棧內存的生命週期
#include <iostream>
using namespace std;
/** 在棧上申請內存,函數結束時被銷燬**/
char * getString1()
{
char p[] = "Hello world!";
// 編譯器將提出警告, 比如
// returning address of local variable or temporary (VC++6.0)
return p;
}
/** 在常量區申請內存,函數結束時仍然可用**/
char * getString2()
{
char * p = "Hello world!";
return p;
}
int main()
{
char *str1 = getString1();
// str1 的內容是垃圾
cout << str1 << endl;
char *str2 = getString2();
// 輸出Hello world!
cout << str2 << endl;
return 0;
}
在C/C++ 中,所有的方法調用都是通過棧來進行的,所有的局部變量,形式參數都是從棧中分配內存空間的.
需要注意的是,在分配的時候,比如爲一個即將要調用的程序模塊分配數據區時,應事先知道這個數據區的大小,也就說是雖然分配是在程序運行時進行的,但是分配的大小多少是確定的,不變的,而這個"大小多少"是在編譯時確定的,不是在運行時.
如果想要在運行時才決定要分配的內存的大小,就要用到"堆內存".
2. 從堆上分配的內存
程序在運行的時候用new(malloc)申請任意多少的內存,程序員自己負責在何時用delete(free)釋放內存.動態內存的生存期由我們決定,使用非常靈活,但問題也最多.其生命期是從調用new(malloc)開始,到調用delete(free)結束.如果不掉用delete或者free.則這塊空間必須到軟件運行結束後才能被系統回收.
例4: 堆內存的使用
#include <iostream>
using namespace std;
int main()
{
int arraySize;
int *array;
/* 堆內存的大小在程序運行時才確定*/
cin >> arraySize;
/* 開闢堆內存*/
array = new int[arraySize];
/* 對堆內存進行操作*/
int i;
for (i = 0; i < arraySize; i++)
{
array[i] = i;
}
for (i = 0; i < arraySize; i++)
{
cout << array[i] << " ";
}
cout << endl;
/* 釋放堆內存*/
delete [] array;
return 0;
}
堆是應用程序在運行的時候請求操作系統分配給自己內存,由於從操作系統管理的內存分配,所以在分配和銷燬時都要佔用時間,因此用堆的效率非常低.但是堆的優點在於,編譯器不必知道要從堆裏分配多少存儲空間.也不必知道存儲的數據要在堆裏停留多長的時間,因此,用堆保存數據時會得到更大的靈活性.
事實上,面向對象的多態性,堆內存分配是必不可少的,因爲多態變量所需的存儲空間只有在運行時創建了對象之後才能確定.在C++中,要求創建一個對象時,只需用new命令編制相關的代碼即可.執行這些代碼時,會在堆裏自動進行數據的保存.當然,爲達到這種靈活性,必然會付出一定的代價: 在堆裏分配存儲空間時會花掉更長的時間!這也正是導致效率低的原因.
3. 小結
堆和棧的區別可以用如下的比喻來看出:
使用棧就象我們去飯館裏吃飯,只管點菜(發出申請),付錢,和吃(使用),吃飽了就走,不必理會切菜,洗菜等準備工作和洗碗,刷鍋等掃尾工作.他的好處是快捷,但是自由度小.
使用堆就象是自己動手做喜歡吃的菜餚,比較麻煩,但是比較符合自己的口味,而且自由度大.
三.指針與內存
1. 操作內存
什麼是指針?
其實指針就像是其它變量一樣,所不同的是一般的變量包含的是實際的真實的數據,而指針是一個指示器,它告訴程序在內存的哪塊區域可以找到數據.指針是一個數據類型,本身也需要佔用四個字節的存儲空間。.所以用sizeof(void*)獲得的值爲.
作爲一個C++程序員,指針的直接操作內存,在數據操作方面有着速度快,節約內存等優點,仍是很多C++程序員的最愛.指針是一把雙刃劍,用好了它,你就會發現指針有多麼的方便,反之,你可能就頭疼了,往往會出現意想不到的問題.
爲了方便,人們賦予了指針極大的權利,然而就是這些權利,使指針在爲程序員提供種種便利的同時,也可能對程序造成巨大的破壞.
例5: 指針的權利
#include <iostream>
using namespace std;
class A
{
int value;
public:
A( int n = 0 ) : value( n ) {}
int GetValue()
{
return value;
}
};
int main()
{
A a;
*( (int *)&a ) = 5;
return 0;
}
2. 指針與數組
C/C++程序中,指針和數組在不少地方可以相互替換着用,讓人產生一種錯覺,以爲兩者是等價的.
數組要麼在靜態存儲區被創建(如全局數組).要麼在棧上被創建/數組名對應着(而不是指向)一塊內存,其地址與容量在生命期內保持不變,只有數組的內容可以改變.
指針可以隨時指向任意類型的內存塊,它的特徵是"可變",所以我們常用指針來操作動態內存.指針遠比數組靈活,但也更危險.
例6: 指針與數組的比較-修改內容
#include <iostream>
using namespace std;
int main()
{
char a[] = "hello";
a[0] = 'X';
cout << a << endl; // Xello
char *p = "world"; // p 指向常量字符串
p[0] = 'X'; // 編譯器不能發現該錯誤
cout << p << endl; // 運行錯誤
return 0;
}
在例中,字符數組a 的容量是個字符,其內容爲hello/0.a的內容可以改變,如a[0] ='X' .
指針p 指向常量字符串"world"(位於靜態存儲區.內容爲world/0).常量字符串的內容是不可以被修改的.從語法上看,編譯器並不覺得語句p[0] = 'X'有什麼不妥.但是該語句企圖修改常量字符串的內容而導致運行錯誤.
例7: 指針與數組的比較-內容複製與比較
#include <iostream>
using namespace std;
int main()
{
/* 數組*/
char a[] = "hello";
char b[10];
strcpy(b, a); // 不能用b = a;
cout << strcmp(b, a) << endl; // 不能用b == a
/* 指針*/
int len = strlen(a);
char *p = new char(len+1);
strcpy(p, a); // 不要用p = a;
cout << strcmp(p, a) << endl; // 不要用p == a
return 0;
}
不能對數組名進行直接複製與比較,例中,若想把數組a的內容複製給數組b,不能用語句b = a ,否則將產生編譯錯誤.應該用標準庫函數strcpy進行復制.同理,比較b和a的內容是否相同,不能用b==a 來判斷,應該用標準庫函數strcmp進行比較.
語句p = a 並不能把a的內容複製指針p,而是把a的地址賦給了p.要想複製a的內容,可以先爲p申請一塊容量爲strlen(a)+1個字符的內存,再用strcpy進行字符串複製.同理,語句p==a 比較的不是內容而是地址,應該用庫函數strcmp來比較.
例8: 指針與數組的比較-內存容量
#include <iostream>
using namespace std;
void fun(char a[100])
{
cout << sizeof(a) << endl; // 4 (字節)
}
int main()
{
char a[] = "hello world";
char *p = a;
cout << sizeof(a) << endl; // 12 (字節)
cout << sizeof(p) << endl; // 4 (字節)
fun(a);
return 0;
}
用運算符sizeof可以計算出數組的容量(字節數).示例中,sizeof(a)的值是(注意別忘了'/0').指針p指向a,但是sizeof(p)的值卻是.這是因爲sizeof(p)得到的是一個指針變量的字節數,相當於sizeof(char*),而不是p所指的內存容量.C/C++語言沒有辦法知道指針所指的內存容量,除非在申請內存時記住它.
注意當數組作爲函數的參數進行傳遞時,該數組自動退化爲同類型的指針.例中,不論數組a的容量是多少,sizeof(a)始終等於sizeof(char *).
3. 指針參數
如果函數的參數是一個指針,不要指望用該指針去申請動態內存.
例9: 指針作爲函數的參數
#include <iostream>
using namespace std;
void getMemory(char *p, int num)
{
p = new char(num);
}
int main()
{
char *str = NULL;
getMemory(str, 100); // str 仍然爲NULL
strcpy(str, "hello"); // 運行錯誤
return 0;
}
例中,語句getMemory(str, 200)並沒有使str獲得期望的內存,str依舊是NULL,爲什麼?
毛病出在函數getMemory中.編譯器總是要爲函數的每個參數製作臨時副本,指針參數p的副本是_p,編譯器使_p = p.如果函數體內的程序修改了_p的內容,就導致參數p的內容作相應的修改.這就是指針可以用作輸出參數的原因.
在本例中,_p申請了新的內存,只是把_p所指的內存地址改變了,但是p絲毫未變.所以函數GetMemory並不能輸出任何東西.事實上,每執行一次getMemory就會泄露一塊內存,因爲沒有用delete釋放內存.
如果非得要用指針參數去申請內存,有兩種方法: 1.改用"指向指針的指針"; 2.用函數返回值來傳遞動態內存.
例10: 指針參數與生命期
#include <iostream>
using namespace std;
int *g;
void fun(int *q)
{
q = new int(10);
g = q;
cout << "in function" << endl;
cout << "&p: " << &q << endl;
cout << "p : " << q << endl;
cout << "*p: " << *q << endl;
cout << endl;
}
int main()
{
int *p;
cout << "before function" << endl;
// 編譯器發出警告:
// local variable 'p' used without having been initialized
cout << "&p: " << &p << endl;
cout << "p : " << p << endl;
//cout << "*p: " << *p << endl;
cout << endl;
fun(p);
cout << "after function" << endl;
cout << "&p: " << &p << endl;
cout << "p : " << p << endl;
//cout << "*p: " << *p << endl;
cout << endl;
cout << "globe_pointer" << endl;
cout << "&g: " << &g << endl;
cout << "g : " << g << endl;
cout << "*g: " << *g << endl;
cout << endl;
return 0;
}
/*
運行結果
before function
&p: 0012FF7C
p : CCCCCCCC
in function
&p: 0012FF2C
p : 00371D48
*p: 10
after function
&p: 0012FF7C
p : CCCCCCCC
globe_pointer
&g: 0047CDE8
g : 00371D48
*g: 10
*/
對於指針p, &p 輸出的是指針自身的地址; p 輸出的是指針指向的地址; *p 輸出的是指針指向的地址的值.
在例中,無論是否執行了函數fun(),指針p都沒有任何改變.可見函數fun()並沒有起到預期的作用.
另外,在函數fun()中,q位於棧內存中,而其申請的內存位於堆內存中.所以,當程序推出函數fun()中時,指針q隨着棧內存的銷燬而銷亡,但其指向的堆內存並未銷燬.被全局指針g記錄下來.所以g, *g 的值與q, *q的值相等(但兩個指針各自的地址&g, &q是不同的).
四.malloc/free 與new/delete
1. malloc/free 的使用要點
函數malloc的原型如下"
void * malloc(size_t size);
用malloc申請一塊長度爲length的整數類型的內存,程序如下:
int *p = (int *) malloc(sizeof(int) * length);
我們應當把注意力集中在兩個要素上,"類型轉換"和"sizeof".
malloc返回值的類型是void *,所以在調用malloc時要顯式地進行類型轉換,將void * 轉換成所需要的指針類型.
malloc函數本身並不識別要申請的內存是什麼類型,它只關心內存的總字節數.在malloc的"()"中使用sizeof運算符是良好的風格.
函數free的原型如下:
void free( void * memblock );
爲什麼free函數不象malloc函數那樣複雜呢?這是因爲指針p的類型以及它所指的內存的容量事先都是知道的,語句free(p)能正確地釋放內存。如果p是NULL指針,那麼free對p無論操作多少次都不會出問題。如果p不是NULL指針,那麼free對p連續操作兩次就會導致程序運行錯誤。
2. new/delete 的使用要點
運算符new使用起來要比函數malloc簡單得多.如:
int *p1 = (int *)malloc(sizeof(int) * length);
int *p2 = new int[length];
這是因爲new內置了sizeof、類型轉換和類型安全檢查功能.對於非內部數據類型的對象而言,new在創建動態對象的同時完成了初始化工作.如果對象有多個構造函數,那麼new的語句也可以有多種形式.
例11: new/delete 的使用
#include <iostream>
using namespace std;
class Obj
{
private:
int a;
public:
Obj(void) { a= 10; } // 無參數的構造函數
Obj(int x) : a(x) {} // 帶一個參數的構造函數
};
int main()
{
Obj *a = new Obj;
Obj *b = new Obj(1); // 初值爲
Obj *c = new Obj[100]; // 調用對象的無參數構造函數, 初值爲
// 這種寫法是不對的
//Obj *d = new Obj[100](1); // 創建個動態對象的同時賦初值
delete a;
delete b;
delete []c; // 留意符號[] 的使用
return 0;
}
需要注意的是,和malloc/free 不同,new/delete 是運算符.既然是運算符,就可以被重載.
局部重載new和delete. 重載一個與類相關的new和delete函數,只需重載運算符函數成爲該類的成員函數.此時重載的new和delete僅用於該特定的類,在其他數據類型上仍然使用原始版本的new和delete.即遇到new和delete時,編譯程序首先檢查正在使用對象所在的類是否重載了new和delete,如果重載了,則使用這個重載版本;否則,使用全局定義的new和delete.
全局重載new和delete. 在任何類外重載new和delete,使它成爲全局的.此時,C++中原來的new和delete被忽略,程序中使用重載的new和delete.
例12: new/delete 的重載
#include <iostream>
using namespace std;
class A
{
private:
int x, y;
public:
A(int x1, int y1) : x(x1), y(y1) {}
~A() {}
void * operator new (unsigned int size);
void operator delete (void *p);
};
/** 局部重載運算符new **/
void * A::operator new (unsigned int size)
{
return malloc(size);
}
/** 局部重載運算符delete **/
void A::operator delete (void *p)
{
free(p);
}
int main()
{
A *a;
a = new A(3, 10); // 調用類A的重載運算符new
delete a; // 調用類A的重載運算符delete
int *i = new int (100); // 調用系統運算符new
delete []i; // 調用系統運算符delete
return 0;
}
3. malloc/free 與new/delete 的比較
malloc與free是C++/C語言的標準庫函數,new/delete是C++的運算符.它們都可用於申請動態內存和釋放內存.
對於非內部數據類型的對象而言,光用maloc/free無法滿足動態對象的要求.對象在創建的同時要自動執行構造函數,對象在消亡之前要自動執行析構函數.由於malloc/free是庫函數而不是運算符,不在編譯器控制權限之內,不能夠把執行構造函數和析構函數的任務強加於malloc/free.
因此C++語言需要一個能完成動態內存分配和初始化工作的運算符new,以及一個能完成清理與釋放內存工作的運算符delete.注意new/delete不是庫函數.
new Obj (Obj是一個類名)實際上做了件事: 調用opeator new,在自由存儲區分配一個sizeof(Obj)大小的內存空間;然後調用構造函數Obj(),在這塊內存空間上類磚砌瓦,建造起我們的對象.同樣對於delete,則做了相反的兩件事:調用析構函數~Obj(),銷燬對象,調用operator delete,釋放內存.
既然new/delete的功能完全覆蓋了malloc/free,爲什麼C++不把malloc/free淘汰出局呢?這是因爲C++程序經常要調用C函數,而C程序只能用malloc/free管理動態內存.
總的來說,運算符new和delete提供了存儲的動態分配和釋放功能.它的作用相當於C語言的函數malloc()和free(),但是性能更爲優越.使用new比使用malloc()有以下的幾個優點
1、new自動計算要分配類型的大小,不使用sizeof運算符,比較省事,可以避免錯誤.
2、它自動地返回正確的指針類型,不用進行強制指針類型轉換.
3、可以用new對分配的對象進行初始化.
五.常見內存錯誤
1.內存泄露
在堆上分配的內存,如果不再使用了,應該把它釋放掉,以便後面其它地方可以重用.在C/C++中,內存管理器不會幫你自動回收不再使用的內存.如果你忘了釋放不再使用的內存,這些內存就不能被重用,就造成了所謂的內存泄露.
含有這種錯誤的函數每被調用一次就丟失一塊內存.剛開始時系統的內存充足,你看不到錯誤.終有一次程序突然死掉,系統出現提示:內存耗盡.
動態內存的申請與釋放必須配對,程序中new與delete的使用次數一定要相同,否則肯定有錯誤(malloc/free同理).
2.內存越界訪問
內存越界訪問有兩種:一種是讀越界,即讀了不屬於自己的數據,如果所讀的內存地址是無效的,程度立刻就崩潰了.如果所讀內存地址是有效的,在讀的時候不會出問題,但由於讀到的數據是隨機的,它會產生不可預料的後果.另外一種是寫越界,又叫緩衝區溢出.所寫入的數據對別人來說是隨機的,它也會產生不可預料的後果.
內存越界訪問造成的後果非常嚴重,是程序穩定性的致命威脅之一.更麻煩的是,它造成的後果是隨機的,表現出來的症狀和時機也是隨機的,讓BUG的現象和本質看似沒有什麼聯繫,這給BUG的定位帶來極大的困難.
3.野指針
"野指針"不是NULL指針,是指向“垃圾”內存的指針.人們一般不會錯用NULL指針,因爲用if語句很容易判斷.但是野指針是很危險的.if語句對它不起作用/
當你調用free(p)時,你真正清楚這個動作背後的內容嗎?你會說p指向的內存被釋放了.沒錯,p本身有變化嗎?答案是p本身沒有變化.它指向的內存仍然是有效的,你繼續讀寫p指向的內存,沒有人能攔得住你.
釋放掉的內存會被內存管理器重新分配,此時,野指針指向的內存已經被賦予新的意義.對野指針指向內存的訪問,無論是有意還是無意的,都爲此會付出巨大代價,因爲它造成的後果,如同越界訪問一樣是不可預料的.
釋放內存後立即把對應指針置爲空值,這是避免野指針常用的方法.這個方法簡單有效,只是要注意,當然指針是從函數外層傳入的時,在函數內把指針置爲空值,對外層的指針沒有影響.比如,你在析構函數裏把this指針置爲空值,沒有任何效果,這時應該在函數外層把指針置爲空值.
4.內存分配未成功,卻使用了它
編程新手常犯這種錯誤,因爲他們沒有意識到內存分配會不成功.常用解決辦法是,在使用內存之前檢查指針是否爲NULL.如果指針p是函數的參數,那麼在函數的入口處用assert(p!=NULL)進行檢查.如果是用malloc或new來申請內存,應該用if(p==NULL) 或if(p!=NULL)進行防錯處理.
5.內存分配雖然成功,但是尚未初始化就引用它
犯這種錯誤主要有兩個起因:一是沒有初始化的觀念;二是誤以爲內存的缺省初值全爲零,導致引用初值錯誤(例如數組).
內存的缺省初值究竟是什麼並沒有統一的標準,儘管有些時候爲零值,我們寧可信其無不可信其有.所以無論用何種方式創建數組,都別忘了賦初值,即便是賦零值也不可省略,不要嫌麻煩.
5.返回指向臨時變量的指針
大家都知道,棧裏面的變量都是臨時的.當前函數執行完成時,相關的臨時變量和參數都被清除了.不能把指向這些臨時變量的指針返回給調用者,這樣的指針指向的數據是隨機的,會給程序造成不可預料的後果.
參見例3: 棧內存的生命週期
6.試圖修改常量
在函數參數前加上const修飾符,只是給編譯器做類型檢查用的,編譯器禁止修改這樣的變量.但這並不是強制的,你完全可以用強制類型轉換繞過去,一般也不會出什麼錯.
而全局常量和字符串,用強制類型轉換繞過去,運行時仍然會出錯.原因在於它們是是放在.rodata裏面的,而.rodata內存頁面是不能修改的.試圖對它們修改,會引發內存錯誤.
參見例6: 指針與數組的比較-修改內容
7.誤解傳值與傳引用
在C/C++中,參數默認傳遞方式是傳值的,即在參數入棧時被拷貝一份.在函數裏修改這些參數,不會影響外面的調用者.
參見例9: 指針作爲函數的參數