C/C++內存管理
1. 動態內存分配
爲什麼存在動態內存分配?
我們已知的內存開闢方式有:
int val = 20;//在棧空間上開闢四個字節
char arr[10] = {0};//在棧空間上開闢10個字節的連續空間
但上述的開闢空間的方式有兩個特點:
1. 空間開闢大小固定。
2. 數組在申明的時候,必須指定數組的長度,它所需要的內存在編譯時分配。 但是對於空間的需求,不僅僅是上述情況。有時候我們需要的空間大小在程序運行的時候才能知道,那數組的編譯時開闢空間的方式就不能滿足。
2. C動態內存函數介紹
動態內存函數都聲明在 stdlib.h 頭文件中
2.1. malloc和free
C語言提供了一個動態內存開闢的函數malloc:
void* malloc (size_t size);
這個函數向內存申請一塊連續可用的空間,並返回指向這塊空間的指針。
1. 如果開闢成功,則返回一個指向開闢好空間的指針。
2. 如果開闢失敗,則返回一個NULL指針,因此malloc的返回值一定要做檢查。
3. 返回值的類型是void* ,所以malloc函數並不知道開闢空間的類型,具體在使用的時候使用者自己來決定。
4. 如果參數 size 爲0,malloc的行爲是標準是未定義的,取決於編譯器。
C語言提供了另外一個函數free,專門是用來做動態內存的釋放和回收的。
函數原型如下:
void free (void* ptr);
free函數用來釋放動態開闢的內存。
如果參數 ptr 指向的空間不是動態開闢的,那free函數的行爲是未定義的。
如果參數 ptr 是NULL指針,則函數什麼事都不做。
測試代碼
#include <stdio.h>
#include <stdlib.h>
int main()
{
int num = 100;
int* ptr = (int*)malloc(num*sizeof(int))
if(NULL != ptr) // 判斷ptr指針是否爲空以確定內存是否申請成功
{
int i = 0;
for(i=0; i<num; i++)
{
*(ptr+i) = 0;
}
}
free(ptr); // 釋放ptr所指向的動態內存
ptr = NULL; // 將NULL置空以避免野指針
return 0;
}
2.2. calloc
C語言還提供了一個函數叫 calloc , calloc 函數也用來動態內存分配。
函數原型如下:
void* calloc (size_t num, size_t size);
函數的功能是爲 num 個大小爲 size 的元素開闢一塊空間,並且把空間的每個字節初始化爲0。
與函數 malloc 的區別只在於 calloc 會在返回地址之前把申請的空間的每個字節初始化爲全0。
測試代碼
#include <stdio.h>
#include <stdlib.h>
int main()
{
int *p = (int*)calloc(10, sizeof(int));
if(p!= NULL)
{
//使用空間
}
free(p);
p = NULL;
return 0;
}
如果我們對申請的內存空間的內容要求初始化,那麼可以使用calloc函數來完成任務。
2.3. realloc
realloc函數的出現讓動態內存管理更加靈活。
有時會我們發現過去申請的空間太小了,有時候我們又會覺得申請的空間過大了,那爲了合理的時候內存,我們一定會對內存的大小做靈活的調整。那realloc 函數就可以做到對動態開闢內存大小的調整。
函數原型如下:
void* realloc (void* ptr, size_t size);
ptr是要調整的內存地址,size爲調整之後新大小
返回值爲調整之後的內存起始位置。
這個函數調整原內存空間大小的基礎上,還會將原來內存中的數據移動到新的空間。
realloc在調整內存空間的是存在兩種情況:
情況1:原有空間之後有足夠大的空間
要擴展內存就直接原有內存之後直接追加空間,原來空間的數據不發生變化。
情況2:原有空間之後沒有足夠大的空間
在堆空間上另找一個合適大小的連續空間來使用。這樣函數返回的是一個新的內存地址,而之前的空間歸還給堆。
測試代碼
#include <stdio.h>
int main()
{
int *ptr = (int*)malloc(100);
if(ptr != NULL)
{
//使用空間
}
else
{
exit(EXIT_FAILURE);
}
//擴展容量
int*p = NULL;
p = (int*)realloc(ptr, 1000);
if(p != NULL)
{
ptr = p;
}
//業務處理
free(ptr);
ptr = NULL;
return 0;
}
2.4. 面試題
請看下面的一段代碼和相關問題
int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
static int staticVar = 1;
int localVar = 1;
int num1[10] = {1, 2, 3, 4};
char char2[] = "abcd";
char* pChar3 = "abcd";
int* ptr1 = (int*)malloc(sizeof(int)*4);
int* ptr2 = (int*)calloc(4, sizeof(int));
int* ptr3 = (int*)realloc(ptr2, sizeof(int)*4);
free(ptr1);
free(ptr2);
free(ptr3);
}
選項: A.棧 B.堆 C.數據段 D.代碼段
globalVar在哪裏?____ staticVar在哪裏?____
staticGlobalVar在哪裏?____ localVar在哪裏?____
num1 在哪裏?____
分析:
globalVar全局變量在數據段;staticGlobalVar靜態全局變量在靜態區;
staticVar靜態局部變量在靜態區;localVar局部變量在棧區;
num1局部變量在棧區
char2在哪裏?____ *char2在哪裏?___
pChar3在哪裏?____ *pChar3在哪裏?____
ptr1在哪裏?____ *ptr1在哪裏?____
分析:
char2局部變量在棧區;*char2得到的是字符串常量字符在代碼段
pChar3局部變量在棧區;pChar3得到的是字符串常量字符在代碼段
ptr1局部變量在棧區ptr1得到的是動態申請空間的數據在堆區
答:C C C A A A D A D A B
【說明】
1. 棧又叫堆棧,非靜態局部變量/函數參數/返回值等等,棧是向下增長的。
2. 內存映射段是高效的I/O映射方式,用於裝載一個共享的動態內存庫。用戶可使用系統接口創建共享共享內存,做進程間通信。
3. 堆用於程序運行時動態內存分配,堆是可以上增長的。
4. 數據段:存儲全局數據和靜態數據。
5. 代碼段:可執行的代碼/只讀常量。
3. C++內存管理方式
C語言內存管理方式在C++中可以繼續使用,但是有些地方就無能爲力而且使用起來較麻煩,因此C++又提出了自己的內存管理方式:通過new和delete操作符進行動態內存管理。
3.1. new/delete操作內置類型
void Test()
{
// 動態申請一個int類型的空間
int* p1 = new int;
// 動態申請一個int類型的空間並初始化爲10
int* p2 = new int(10);
// 動態申請10個int類型的空間
int* p3 = new int[3];
delete p1;
delete p2;
delete[] p3;
p1 = nullptr;
p2 = nullptr;
p3 = nullptr;
}
注意:申請和釋放單個元素的空間,使用new和delete操作符,申請和釋放連續的空間,使用new[]和delete[]
3.2. new和delete操作自定義類型
class A
{
public:
A(int a = 0)
: _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
void test()
{
// 申請單個A類型的空間
A* p1 = (A*)malloc(sizeof(A));
free(p1);
p1 = NULL;
// 申請10個A類型的空間
A* p2 = (A*)malloc(sizeof(A) * 10);
free(p2);
p2 = NULL;
// 申請單個A類型的對象
A* p3 = new A;
delete p3;
p3 = nullptr;
// 申請10個A類型的對象
A* p4 = new A[10];
delete[] p4;
p4 = nullptr;
}
注意:在申請自定義類型的空間時,new會調用構造函數,delete會調用析構函數,而malloc與free不會。
3.3. operator new與operator delete函數
new和delete是用戶進行動態內存申請和釋放的操作符,operator new 和operator delete是系統提供的全局函數,new在底層調用operator new全局函數來申請空間,delete在底層通過operator delete全局函數來釋放空間。
/*
operator new:該函數實際通過malloc來申請空間,當malloc申請空間成功時直接返回;
申請空間失敗,嘗試執行空間不足應對措施,如果改應對措施用戶設置,則繼續申請,否則拋異常。
拋異常需要用try,catch捕獲。
*/
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
void *p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{
// report no memory
// 如果申請內存失敗了,這裏會拋出bad_alloc 類型異常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}
/*
operator delete: 該函數最終是通過free來釋放空間的
*/
void operator delete(void *pUserData)
{
_CrtMemBlockHeader * pHead;
RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
if (pUserData == NULL)
return;
_mlock(_HEAP_LOCK); /* block other threads */
__TRY
/* get a pointer to memory block header */
pHead = pHdr(pUserData);
/* verify block type */
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
_free_dbg( pUserData, pHead->nBlockUse );
__FINALLY
_munlock(_HEAP_LOCK); /* release other threads */
__END_TRY_FINALLY
return;
}
// free的實現
#define free(p)
_free_dbg(p, _NORMAL_BLOCK)
通過上述兩個全局函數的實現可以知道,operator new實際也是通過malloc來申請空間,如果malloc申請空間成功就直接返回,否則執行用戶提供的空間不足應對措施,如果用戶提供該措施就繼續申請,否則就拋異常。operator delete 最終是通過free來釋放空間。
3.4. new和delete的實現原理
3.4.1. 內置類型
如果申請的是內置類型的空間,new和malloc,delete和free基本類似,不同的地方是:new/delete申請和釋放的是單個元素的空間,new[]和delete[]申請的是連續空間,而且new在申請空間失敗時會拋異常,malloc會返回NULL。
3.4.2. 自定義類型
new的原理
1. 調用operator new函數申請空間
2. 在申請的空間上執行構造函數,完成對象的構造
delete的原理
1. 在空間上執行析構函數,完成對象中資源的清理工作
2. 調用operator delete函數釋放對象的空間
new T[N]的原理
1. 調用operator new[]函數,在operator new[]中實際調用operator new函數完成N個對象空間的申請
2. 在申請的空間上執行N次構造函數
delete[]的原理
1. 在釋放的對象空間上執行N次析構函數,完成N個對象中資源的清理
2. 調用operator delete[]釋放空間,實際在operator delete[]中調用operator delete來釋放空間
3.5.面試題
malloc/free和new/delete的區別
malloc/free和new/delete的共同點是:
都是從堆上申請空間,並且需要用戶手動釋放。
不同點是:
1. malloc和free是函數,new和delete是操作符
2. malloc申請的空間不會初始化,new可以初始化
3. malloc申請空間時,需要手動計算空間大小並傳遞。new只需在其後跟上空間的類型即可
4. malloc的返回值爲void*, 在使用時必須強轉。new不需要,因爲new後跟的是空間的類型
5. malloc申請空間失敗時,返回的是NULL,因此使用時必須判空。new不需要,但是new需要捕獲異常
6. 申請自定義類型對象時,malloc/free只會開闢空間,不會調用構造函數與析構函數,而new在申請空間 後會調用構造函數完成對象的初始化,delete在釋放空間前會調用析構函數完成空間中資源的清理
4.內存泄漏
4.1. 什麼是內存泄漏,內存泄漏的危害?
概念:內存泄漏指因爲疏忽或錯誤造成程序未能釋放已經不再使用的內存的情況。內存泄漏並不是指內存在物理上的消失,而是應用程序分配某段內存後,因爲設計錯誤,失去了對該段內存的控制,因而造成了內存的浪費。
內存泄漏的危害:長期運行的程序出現內存泄漏,影響很大,如操作系統、後臺服務等等,出現內存泄漏會導致響應越來越慢,最終卡死。
void MemoryLeaks()
{
// 1.內存申請了忘記釋放
int* p1 = (int*)malloc(sizeof(int));
int* p2 = new int;
// 2.異常安全問題
int* p3 = new int[10];
Func(); // 這裏Func函數拋異常導致 delete[] p3未執行,p3沒被釋放.
delete[] p3;
p3 = nullptr;
}
4.2. 內存泄漏分類
C/C++程序中一般關心兩方面的內存泄漏:
堆內存泄漏(Heap leak)
堆內存指的是程序執行中依據須要分配通過malloc / calloc / realloc /new等從堆中分配的一塊內存,用完後必須通過調用相應的 free或者delete刪掉。假設程序的設計錯誤導致這部分內存沒有被釋放,那麼以後這部分空間將無法再被使用,就會產生Heap Leak。
系統資源泄漏
指程序使用系統分配的資源,比方套接字、文件描述符、管道等沒有使用對應的函數釋放掉,導致系統資源的浪費,嚴重可導致系統效能減少,系統執行不穩定。
4.3. 如何避免內存泄漏
1. 工程前期良好的設計規範,養成良好的編碼規範,申請的內存空間記着匹配的去釋放。
2. 採用RAII思想或者智能指針來管理資源。
3. 有些公司內部規範使用內部實現的私有內存管理庫。這套庫自帶內存泄漏檢測的功能選項。
4. 出問題了使用內存泄漏工具檢測(如valgrind,VLD等) 。
總結:
內存泄漏非常常見,解決方案分爲兩種:
- 事前預防型。如智能指針等
- 事後查錯型。如泄漏檢測工具
4.4. 面試題
如何一次在堆上申請4G的內存?
32位操作系統下,最大內存爲4G。而堆可申請的內存就更少,一般1G-2G之間,所以無論如何也不能申請出4G內存。那麼該怎麼做呢?換成64位即可。
// 將程序編譯成x64的進程
#include <iostream>
using namespace std;
int main()
{
void* p = new char[0xfffffffful];
cout << "new:" << p << endl;
return 0;
}