C/C++內存管理
1.C/C++內存分布
我們先來看一段代碼,來了解一下C/C++中的數據內存分布。
# include <stdlib.h>int globalVar = 1;
static int staticGlobalVar = 1; // 比globalVar還要先銷毀,同一個文件下后定義的先析構
// 全局變量存在 數據段(靜態區)但是 鏈接方式和靜態變量不同,全部文件下都能知道globalvar的存在
// 但是staticGlobalVar只在當前文件(test.cpp)下存在
// 注意: 多個文件的話,那個全局變量先析構是不確定的
// 注意: 全局變量和全局靜態變量唯一的區別就是鏈接屬性不同
void Test()
{static int staticVar = 1; // 作用域只在Test函數中,函數棧幀銷毀后,也跟著銷毀了int localVar = 1;int num1[10] = { 1, 2, 3, 4 };char char2[] = "abcd";const 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(ptr3);
}/*
1. 選擇題:
選項 : A.棧 B.堆 C.數據段(靜態區) D.代碼段(常量區)
globalVar在哪里?__C__ staticGlobalVar在哪里?__C__
staticVar在哪里?__C__ localVar在哪里?__A__
num1 在哪里?__A__char2在哪里?__A__ *char2在哪里?_A__ // *char拿的是數組的首元素,整個數組都位于棧區,首元素自然也在棧區
pChar3在哪里?__A__ *pChar3在哪里?__D__ //*pChar3 是常量字符串的首元素,pChar3指向常量區的常量字符串
ptr1在哪里?__A__ *ptr1在哪里?__B__ // ptr1是在棧區的一個指向堆區數組的指針2. 填空題:
sizeof(num1) = __40__;
sizeof(char2) = __5__; strlen(char2) = __4__; // 5是因為還有一個\0
sizeof(pChar3) = __8/4__; strlen(pChar3) = __4__;// pChar3是指針變量
sizeof(ptr1) = __8/4__; // ptr1也是指針變量3. sizeof 和 strlen 區別?
sizeof就是去算所占空間的大小
strlen 就是讀取一個字符串的長度,遇到\0就會停下來
*/
有關函數內的四個在棧區的變量的圖解
更加詳細的圖解:
- 注意: 全局變量和全局靜態變量唯一的區別就是鏈接屬性不同
【說明】
-
棧又叫堆棧–非靜態局部變量/函數參數/返回值等等,棧是向下增長的。
-
內存映射段是高效的I/O映射方式,用于裝載一個共享的動態內存庫。用戶可使用系統接口創建共享共享內存,做進程間通信。(Linux課程如果沒學到這塊,現在只需要了解一下)
-
堆用于程序運行時動態內存分配,堆是可以上增長的。
-
數據段–存儲全局數據和靜態數據。
-
代碼段–可執行的代碼/只讀常量
拓展:
在我們上面做的題目中,有這樣一個題
sizeof(ptr1) = __8/4__;
為什么在64位和32位的環境下,指針所占內存空間的大小不一樣呢?
首先我們要知道一個虛擬內存,我們在VS下調試看到的變量的地址都是虛擬內存下的地址,這個虛擬內存本來一開始在32位下是只有 2^32次方的字節的內存,也就是4個G的內存大小,當時環境的物理內存也差不多只有4個G的內存大小。
但是隨著科技發展,內存逐漸便宜并且容量越來越大,這個時候虛擬內存就變小了,無法完全映射到物理內存上。因此64位的進程就出來了,它有著2^64個字節的虛擬內存,大概160億的G的虛擬內存,非常龐大。
而我們又知道,內存中的每個字節都需要地址,也就是對應的編號,0x00000001之類的,那有264個字節,自然就需要264個地址。那么指針要記錄地址,自然需要能存下2^64字節的空間,因此就需要8個字節的大小來存儲地址編號,一個字節有8個比特位,8個字節就是也就意味著能存下 2^64個數據。
這就是為什么在32位下指針大小是4個字節,64位下指針大小是8個字節
2.C語言中動態內存管理方式:malloc/calloc/realloc/free
void Test ()
{int* p1 = (int*) malloc(sizeof(int));free(p1);// 1.malloc/calloc/realloc的區別是什么?int* p2 = (int*)calloc(4, sizeof (int));int* p3 = (int*)realloc(p2, sizeof(int)*10);// 這里需要free(p2)嗎?free(p3 );
}
在C語言中,我們學習動態開辟內存空間的時候,介紹過三個函數,來搭配free使用,可以達到開辟動態內存空間的效果。
【面試題】
- malloc/calloc/realloc的區別?
- malloc的實現原理? glibc中malloc實現原理_bilibili
3. C++內存管理方式
C語言內存管理方式在C++中可以繼續使用,但有些地方就無能為力,而且使用起來比較麻煩,因
此C++又提出了自己的內存管理方式:通過new和delete操作符進行動態內存管理。
3.1new/delete操作內置類型
// C++內存管理方式
int main()
{// c++提供new操作符來動態開辟空間int* p1 = new int; // 開辟int類型的空間int* p2 = new int[10]; // 開辟10個int類型的空間int* p3 = new int(10); // 開辟一個int類型的空間,并初始化成10// c++提供delete操作符來釋放空間delete p1;delete[] p2; // 數組的話要記得加 []delete p3;return 0;
}
注意:申請和釋放單個元素的空間,使用new和delete操作符,申請和釋放連續的空間,使用new[]和delete[],注意:匹配起來使用。
3.2new和delete操作自定義類型
既然有了malloc 和 free了。那new和 delete有什么意義呢?
- 對于內置類型來說,new 和 delete跟malloc和free起到的作用是一樣的。
- 但是對于自定義的類型來說,new和delete的意義就出來了
// 既然有了malloc 和 free了。那new和 delete有什么意義呢?
// 1.對于內置類型來說,new 和 delete跟malloc和free起到的作用是一樣的。
// 2.但是對于自定義的類型來說,new和delete的意義就出來了
// malloc 只會申請空間, new則會申請空間 + 調用構造函數初始化
// free 只會釋放空間, delete則會 調用析構函數 + 釋放空間 [注意是先析構再釋放空間]# include<iostream>
using namespace std;class A
{
public:A(int a = 0):_a(a){cout << "A()" << endl;}~A(){cout << "~A" << endl;}private:int _a;
};int main()
{// 為自定義類型申請空間下 new和malloc的區別A* p1 = (A*)malloc(sizeof(A));A* p2 = new A; // 申請空間 + 調用默認構造函數// 為自定義類型對象釋放空間下 delete 和 free的區別free(p1); // 釋放空間delete p2; // 調用析構函數 + 釋放空間return 0;
}
根據調試我們可以知道,p2的成員變量 _a = 0,說明調用了構造函數
p2在調用delete的情況下**,先調用了析構函數,在進行空間的釋放**
總結:
在申請自定義類型的空間時,new會調用構造函數,delete會調用析構函數,而malloc與free不會
再來看一個例子來直觀的感受一下new 和delete的用處
# include<stdlib.h>
# include<iostream>
using namespace std;typedef struct ListNode
{int _val;struct ListNode* _next; // 兼容C語言的用法ListNode* _prev;// cpp專有用法,因為在c++中struct可以看做一個類,唯一區別就是默認的訪問限定符不同ListNode(int val = 0):_val(val),_next(nullptr),_prev(nullptr){}~ListNode(){cout << "~ListNode" << endl;}
}ListNode;int main()
{// 在之前使用C語言來實現鏈表的時候,我們每次申請節點都要malloc,因此我們會寫一個BuyNode函數// 并且函數中,還要編寫初始化的代碼,但是對于new 和 delete來說,就不用這么麻煩了。ListNode* node1 = (ListNode*)malloc(sizeof(ListNode));node1->_val = 0;node1->_next = nullptr;node1->_prev = nullptr;ListNode* node2 = new ListNode;ListNode* node3 = new ListNode(10);// 在C語言中會調用Destroy之類的函數來銷毀我們申請的節點free(node1); // Destory函數中是free函數釋放空間// c++中使用delete不僅會釋放空間,在釋放空間之前還會調研析構函數delete node2;delete node3;return 0;
}
調試如下:
4.operator new 與operator delete函數(重要)
我們來看operator new 和operator delete函數的使用例子:
// operator new與operator delete函數
# include<iostream>
using namespace std;class A
{
public:A(int a = 0):_a(a){cout << "ListNode()" << endl;}~A(){cout << "~ListNode" << endl;}
private:int _a;
};int main()
{A* p1 = (A*)malloc(sizeof(A)); // operator new 的用法和malloc完全一樣A* p2 = (A*)operator new(sizeof(A));// operator new和 malloc的不同在于:申請空間失敗后的處理方式void* p3 = malloc(INT_MAX * 2);// 我們知道malloc是有可能申請空間失敗的,失敗會返回空指針,也就是0// 失敗要不就是開著開著空間不夠開了,要不就是一上來開的太大不夠開了if (p3 == NULL){// 之前我們學習過要這樣處理perror("malloc()");// malloc(): Not enough space}// 而operator new 在申請失敗后,會拋異常// 這里了解一下,后面會學習拋異常// 這里是失敗拋異常——是面向對象處理錯誤的方式try{void* p4 = operator new(INT_MAX * 2);cout << p4 << endl;} catch (exception& e){cout << e.what() << endl;// bad allocation}free(p1);operator delete(p2);// 釋放空間失敗不會拋異常// p3 p4 都申請失敗了 不用釋放了return 0;
}
new和delete是用戶進行動態內存申請和釋放的操作符,operator ne和operator delete是
系統提供的全局函數,new在底層調用operator new全局函數來申請空間,delete在底層通過operator delete全局函數來釋放空間。
我們來看看new的底層是如何實現的:
/*
operator new:該函數實際通過malloc來申請空間,當malloc申請空間成功時直接返回;申請空間
失敗,嘗試執行空間不足應對措施,如果改應對措施用戶設置了,則繼續申請,否
則拋異常。
*/
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{// try to allocate size bytesvoid* 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只要不申請失敗,和malloc是完全一樣的,就是通過malloc實現的,但是如果申請失敗,會多出一個拋異常的處理。
-
operator new == malloc + 申請失敗拋異常
-
new == operator new + 調用構造函數
因此可以說operator new是為了new操作符而產生。因為new如果直接去調用 malloc的話,申請失敗了無法拋異常,而new如果去調用operator new 的話 就既有了申請失敗拋異常,又多了調用構造函數
再來看看operator delete的底層是如何實現的:
/*
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); // 這里說明最終是通過free實現的__FINALLY_munlock(_HEAP_LOCK); /* release other threads */__END_TRY_FINALLYreturn;
}
/*
free的實現
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
delete如果釋放空間失敗了不會拋異常。operator delete其實和free是一樣的,也是通過free來釋放空間的。
-
operator delete == free
-
delete == 調用析構函數 + operator delete
通過上述兩個全局函數的實現知道,operator new 實際也是通過malloc來申請空間,如果
malloc申請空間成功就直接返回,否則執行用戶提供的空間不足應對措施,如果用戶提供該措施
就繼續申請,否則就拋異常。operator delete 最終是通過free來釋放空間的。
5.new和delete的實現原理
其實之前在講述new和delete操作自定義類型哪里已經講述過了。
5.1內置類型
對于內置類型來說,new和delete與malloc 和 free是沒有什么區別的,不同的地方在于,new和delete只能對單個元素進行空間的申請和釋放,new[]和delete[] 可以對多個元素進行空間的申請和釋放。并且new在申請失敗之后會拋異常,但是malloc申請失敗之后返回NULL
5.2自定義類型
對于自定義類型來說
new的原理:
- new == 申請空間 + 調用該自定義類型的默認構造函數進行初始化 + 申請失敗拋異常
-
調用operator new函數申請空間
-
在申請的空間上執行構造函數,完成對象的構造
delete的原理:
- delete == 調用該自定義類型的默認析構函數 + 釋放空間
- 在空間上調用析構函數,完成對對象中資源的清理工作
- 調用operator 函數釋放 空間
new T[n]的原理:
- new T[n] == 申請n個大小為T類型字節的空間 + 調用T類的默認構造函數初始化 + 申請失敗拋異常
- 調用operator new[]函數,operator new[]函數中實際上調用了n次operator new函數。完成了n個對象空間的申請
- 調用n次T類的默認構造函數完成初始化
注意了:
在調用operator new[]函數的時候,operator new[]函數中實際上調用了n次operator new函數。每次調用operator new函數申請空間之后,都會調用構造函數。
delete[]的原理:
- delete[] = 調用n次T類的析構函數 + 釋放空間
- 在釋放的對象空間上執行N次析構函數,完成N個對象中資源的清理
- 調用delete[]函數,該函數中調用了N次delete函數,完成對象空間的釋放
6.定new表達式(placement-new) (了解)
定位new表達式是在已分配的原始內存空間中調用構造函數初始化一個對象。
使用格式:
new (place_address) type或者new (place_address) type(initializer-list)
place_address必須是一個指針,initializer-list是類型的初始化列表
使用場景:
定位new表達式在實際中一般是配合內存池使用。因為內存池分配出的內存沒有初始化,所以如
果是自定義類型的對象,需要使用new的定義表達式進行顯示調構造函數進行初始化。
我們直接來看代碼:
// 定new表達式
//使用格式:
//new (place_address) type或者 new (place_address) type(initializer - list)
# include<iostream>
using namespace std;class A
{
public:A(int a = 0):_a(a){cout << "ListNode()" << endl;}~A(){cout << "~ListNode" << endl;}
private:int _a;
};int main()
{// 我們想要開辟一個空間來存儲A類的對象A* p1 = new A; // 我們會通過new來實現空間的開辟 + 調用構造函數delete p1;// 但是有時候,我們拿到了一個類的對象,但是它沒有被初始化,這個時候我們就需要定new表達式// 因為我們需要去顯式的調用構造函數,而構造函數往往是這個對象在定義的時候默認調用的A* p2 = (A*)operator new(sizeof(A));// 我們拿到了一個p2對象,但是它沒有被初始化,也就是沒有調用構造函數new(p2)A; // 調用p2所屬類的構造函數// new (place_address) type == new(需要調用構造函數的對象)對象所屬類名// 還可以傳參,讓其初始化成我們想給的值new(p2)A(10); // 調用其構造函數并傳參// new (place_address) type(initializer - list)return 0;
}
7.常見面試題
7.1 malloc/free 和 new/delete的區別?
- new和malloc的區別:
- 在使用方法上,malloc需要傳申請空間的大小,對其返回的指針void*,需要強制轉換。但是new就不需要,new只需要傳 類型和你想要申請空間的數量。new自動返回對應類型的指針
- malloc函數在申請空間失敗之后會返回NULL,需要我們手動編寫代碼判空,new在申請空間失敗之后會拋異常
- malloc函數只會開辟你想要的空間大小,但是new還會再開辟空間之后,對自定義類型調用其默認構造函數進行初始化。
- malloc是函數,new是操作符。
- free和delete的區別:
- free是函數,delete是操作符
- free函數使用后,只會釋放空間,但是delete的對象類型如果是自定義類型,會調用其析構函數清理對象的資源,再釋放空間
7.2內存泄漏
7.2.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;
}
7.2.2內存泄漏的危害?
內存泄漏的危害:
長期運行的程序出現內存泄漏,影響很大,如操作系統、后臺服務等等,出現內存泄漏會導致響應越來越慢,最終卡死.
或者設備本身內存很小的情況下,也會影響很大,容易卡死
7.2.3內存泄漏分類(了解)
7.2.2 內存泄漏分類(了解)
C/C++程序中一般我們關心兩種方面的內存泄漏:
- 堆內存泄漏(Heap leak)
堆內存指的是程序執行中依據須要分配通過malloc / calloc / realloc / new等從堆中分配的一塊內存,用完后必須通過調用相應的 free或者delete 刪掉。假設程序的設計錯誤導致這部分內存沒有被釋放,那么以后這部分空間將無法再被使用,就會產生Heap Leak。
- 系統資源泄漏
指程序使用系統分配的資源,比方套接字、文件描述符、管道等沒有使用對應的函數釋放掉,導致系統資源的浪費,嚴重可導致系統效能減少,系統執行不穩定
7.2.4內存泄漏檢測(了解)
在vs下,可以使用windows操作系統提供的**_CrtDumpMemoryLeaks()** 函數進行簡單檢測,該函數只報出了大概泄漏了多少個字節,沒有其他更準確的位置信息。
int main()
{int* p = new int[10];// 將該函數放在main函數之后,每次程序退出的時候就會檢測是否存在內存泄漏_CrtDumpMemoryLeaks();return 0;
}
// 程序退出后,在輸出窗口中可以檢測到泄漏了多少字節,但是沒有具體的位置
Detected memory leaks!
Dumping objects ->
{79} normal block at 0x00EC5FB8, 40 bytes long.
Data: < > CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD CD
Object dump complete.
因此寫代碼時一定要小心,尤其是動態內存操作時,一定要記著釋放。但有些情況下總是防不勝
防,簡單的可以采用上述方式快速定位下。如果工程比較大,內存泄漏位置比較多,不太好查時
一般都是借助第三方內存泄漏檢測工具處理的。
-
在linux下內存泄漏檢測:linux下幾款內存泄漏檢測工具
-
在windows下使用第三方工具:VLD工具說明
-
其他工具:內存泄漏工具比較
7.2.5如何避免內存泄漏?
-
工程前期良好的設計規范,養成良好的編碼規范,申請的內存空間記著匹配的去釋放。ps:這個理想狀態。但是如果碰上異常時,就算注意釋放了,還是可能會出問題。需要下一條智能指針來管理才有保證。
-
采用RAII思想或者智能指針來管理資源。
-
有些公司內部規范使用內部實現的私有內存管理庫。這套庫自帶內存泄漏檢測的功能選項。
-
出問題了使用內存泄漏工具檢測。ps:不過很多工具都不夠靠譜,或者收費昂貴。
總結一下:
內存泄漏非常常見,解決方案分為兩種:1、事前預防型。如智能指針等。2、事后查錯型。如泄漏檢測工具
7.2.6如何開辟4個G的內存空間?
在32位下無法申請4個G的內存空間,因為虛擬內存就只有4個G,無法單獨給堆區給4個G了。別說4個G了,2個G都無法實現
但是在x64環境下就可以實現
代碼如下:
# include<iostream>
using namespace std;int main()
{size_t n = 4;int* p = new int[n * 1024 * 1024 * 1024];cout << "&p:" << p << endl;return 0;
}