商湯C++二面:new/delete的封裝與malloc/free的底層機制,它們本質有什么差異?
在 C++ 和 C 的內存管理中,new/delete 與 malloc/free 是兩組核心工具,卻有著本質區別。malloc/free 是 C 語言的庫函數,僅負責內存的分配與釋放,不涉及類型信息,返回 void * 需手動轉換。而 new/delete 是 C++ 的操作符,封裝了更復雜的邏輯:new 會先調用 operator new 分配內存(底層常調用 malloc),再自動調用對象構造函數;delete 則先執行析構函數清理資源,再通過 operator delete 釋放內存(底層常調用 free)。
這種差異源于語言特性:C 是面向過程的,僅關注內存塊本身;C++ 為面向對象設計,需保證對象生命周期完整 —— 包括初始化與資源回收。此外,new/delete 支持重載以定制內存管理策略,而 malloc/free 的行為相對固定。本質上,new/delete 是對象級別的內存管理,malloc/free 是原始內存塊操作,前者是對后者的面向對象封裝與擴展。
一、走進 malloc 和 free 的世界
1.1 基本用法
在 C 語言的世界里,malloc和free是我們進行動態內存分配和釋放的得力助手。malloc函數的全稱是 “memory allocation”,從名字就可以看出它的主要職責是分配內存。它的函數原型是void* malloc(size_t size),這里的size參數表示需要分配的內存字節數,返回值是一個指向分配內存起始地址的指針,如果分配失敗,就會返回NULL。
下面來看一個簡單的示例:
#include <stdio.h>
#include <stdlib.h>
int main() {
// 分配一個整數大小的內存空間
int* ptr = (int*)malloc(sizeof(int));
if (ptr == NULL) {
printf("內存分配失敗\n");
return 1;
}
// 使用分配的內存
*ptr = 10;
printf("分配的內存中的值: %d\n", *ptr);
// 釋放內存
free(ptr);
return 0;
}在這段代碼中,我們首先使用malloc分配了一個int類型大小的內存空間,并將返回的指針強制轉換為int*類型,賦值給ptr。然后檢查ptr是否為NULL,以確保內存分配成功。如果分配成功,我們就可以通過ptr來訪問和操作這塊內存,這里將其賦值為10并打印出來。最后,使用free函數釋放這塊內存,將其歸還給系統,以便后續重新分配使用。
1.2 底層原理
當我們調用malloc函數時,它內部是如何與操作系統交互來分配內存的呢?在 Linux 系統中,malloc函數通常是通過glibc(GNU C Library)來實現的。glibc維護了一個內存池,當調用malloc時,它會首先在內存池中查找是否有足夠的空閑內存來滿足請求。如果內存池中有足夠的空閑內存,就直接從內存池中分配,并返回相應的指針,這樣可以減少系統調用的開銷,因為系統調用涉及到用戶態和內核態的切換,會帶來一定的性能損耗。
然而,如果內存池中的空閑內存不足以滿足請求,malloc函數就需要借助系統調用與操作系統進行交互。主要涉及到兩個系統調用:brk和mmap。brk系統調用通過移動程序數據段的結束地址(即 “堆頂” 指針)來增加堆的大小,從而分配新的內存。例如,當程序需要更多內存時,brk會將堆頂指針向上移動,分配出一塊新的內存區域供程序使用 。
而mmap系統調用則是通過在文件映射區域分配一塊內存來滿足請求,通常用于分配較大的內存塊。一般來說,當請求的內存大小小于一定閾值(在大多數系統中,這個閾值通常為 128KB )時,malloc函數會優先使用brk系統調用來分配內存;當請求的內存大小大于這個閾值時,則會使用mmap系統調用。
當我們使用free函數釋放內存時,它會將釋放的內存塊重新標記為空閑狀態,并將其添加到內存池的空閑鏈表中,以便后續的malloc請求再次使用。如果相鄰的內存塊都是空閑的,free還可能會將它們合并成一個更大的空閑內存塊,以減少內存碎片的產生。內存碎片就像是一個雜亂的倉庫,雖然有很多空閑空間,但由于空間零散,無法存放大型貨物,會降低內存的利用率。free的這種合并操作就像是對倉庫進行整理,將零散的空閑空間合并成更大的可用空間,提高內存的使用效率。
1.3 使用注意事項與常見陷阱
在使用malloc和free時,有一些需要特別注意的地方,稍不留意就可能會陷入一些常見的陷阱,導致程序出現各種難以調試的問題。
首先,每次調用malloc后,一定要檢查返回值是否為NULL,以判斷內存分配是否成功。因為如果系統內存不足或者其他原因導致分配失敗,malloc會返回NULL,如果我們不進行檢查,直接使用這個NULL指針去訪問內存,就會導致程序崩潰。例如:
int* ptr = (int*)malloc(100 * sizeof(int));
// 忘記檢查ptr是否為NULL
*ptr = 10; // 這里如果malloc失敗,ptr為NULL,會導致程序崩潰其次,使用free釋放內存后,一定要將指針置為NULL,以避免懸空指針問題。懸空指針就是指針指向的內存已經被釋放,但指針本身沒有被置為NULL,仍然保存著之前內存的地址,此時再訪問這個指針所指向的已釋放內存,就會產生錯誤。比如:
int* ptr = (int*)malloc(sizeof(int));
*ptr = 10;
free(ptr);
// 沒有將ptr置為NULL
*ptr = 20; // 這里ptr成為懸空指針,訪問已釋放內存,會導致未定義行為另外,還要注意避免內存泄漏。內存泄漏是指程序分配了內存,但在使用完后沒有釋放,隨著程序的運行,內存不斷被消耗卻得不到釋放,最終可能導致系統內存耗盡。例如:
void memory_leak_example() {
int* ptr = (int*)malloc(sizeof(int));
// 這里忘記調用free釋放ptr指向的內存,導致內存泄漏
}最后,不要重復釋放內存。對同一塊內存調用多次free會導致未定義行為,可能會引發程序崩潰或內存損壞。例如:
int* ptr = (int*)malloc(sizeof(int));
free(ptr);
free(ptr); // 錯誤:重復釋放同一塊內存,會導致未定義行為二、new 和 delete 的獨特魅力
2.1 基礎操作展示
在 C++ 的世界里,new和delete是專門用于動態內存管理的操作符,它們為對象的創建和釋放提供了更加面向對象的方式 。與malloc和free不同,new操作符不僅會分配內存,還會調用對象的構造函數進行初始化,而delete操作符則會調用對象的析構函數來清理資源,然后釋放內存。
下面通過一個簡單的示例來展示new和delete的基本用法:
#include <iostream>
class MyClass {
public:
MyClass() {
std::cout << "MyClass的構造函數被調用" << std::endl;
}
~MyClass() {
std::cout << "MyClass的析構函數被調用" << std::endl;
}
};
int main() {
// 使用new創建一個MyClass對象
MyClass* obj = new MyClass;
// 使用delete釋放obj指向的對象
delete obj;
return 0;
}在這段代碼中,我們定義了一個MyClass類,它包含一個構造函數和一個析構函數。在main函數中,使用new MyClass創建了一個MyClass對象,并將返回的指針賦值給obj。此時,會自動調用MyClass的構造函數,輸出 “MyClass的構造函數被調用”。當程序執行到delete obj時,會先調用MyClass的析構函數,輸出 “MyClass的析構函數被調用”,然后釋放obj所指向的內存。
對比前面malloc和free的例子,malloc只是簡單地分配內存,不會調用構造函數進行初始化,free也只是釋放內存,不會調用析構函數清理資源。這就體現了new和delete在處理對象時的優勢,它們能夠更好地管理對象的生命周期,確保對象在創建和銷毀時都能正確地進行初始化和清理工作。
2.2 背后的構造與析構
new和delete之所以能夠實現對象的動態創建和銷毀,關鍵在于它們與構造函數和析構函數的緊密配合。當我們使用new操作符創建對象時,它會首先調用operator new函數來分配內存,這個過程類似于malloc,從堆上分配一塊指定大小的內存空間。如果內存分配成功,new操作符會接著調用對象的構造函數,對分配的內存進行初始化,將其轉化為一個真正的對象。構造函數會負責初始化對象的成員變量,執行一些必要的初始化操作,確保對象處于一個可用的狀態。
例如,假設有一個包含成員變量的類:
class Person {
public:
std::string name;
int age;
Person(const std::string& n, int a) : name(n), age(a) {
std::cout << "Person的構造函數被調用,name: " << name << ", age: " << age << std::endl;
}
~Person() {
std::cout << "Person的析構函數被調用,name: " << name << ", age: " << age << std::endl;
}
};當我們使用new Person("Alice", 25)創建一個Person對象時,operator new先分配內存,然后調用Person的構造函數,將"Alice"和25分別賦值給name和age成員變量,并輸出相應的構造信息。
而當我們使用delete操作符釋放對象時,它會首先調用對象的析構函數,析構函數會負責清理對象所占用的資源,比如關閉打開的文件、釋放動態分配的子對象等。在析構函數執行完畢后,delete操作符會調用operator delete函數來釋放之前分配的內存,將其歸還給系統。這樣,就完成了對象從創建到銷毀的整個生命周期管理,確保了資源的正確使用和釋放,避免了內存泄漏和資源未正確清理等問題。
2.3 多種 new 的形式
在 C++ 中,new操作符有著多種形式,除了前面介紹的普通new,還有不拋異常的new(nothrow new)和定位new(placement new),它們各自有著獨特的特點和使用場景,為我們在不同的編程需求下提供了更多的選擇。
(1)普通 new:這是我們最常用的new形式,如int* num = new int(10); ,它會分配內存并調用構造函數初始化對象。如果內存分配失敗,會拋出std::bad_alloc異常,所以在使用時通常需要使用try-catch塊來捕獲異常,以確保程序的健壯性。例如:
try {
int* num = new int(10);
// 使用num
delete num;
} catch (const std::bad_alloc& e) {
std::cerr << "內存分配失敗: " << e.what() << std::endl;
}(2)不拋異常的 new(nothrow new):nothrow new在內存分配失敗時不會拋出異常,而是返回NULL。這在一些不希望因為內存分配失敗而拋出異常中斷程序流程的場景中非常有用,比如在嵌入式系統開發中,可能更傾向于通過檢查返回值來處理內存分配失敗的情況。它的使用方式如下:
#include <new>
int* num = new (std::nothrow) int(10);
if (num == NULL) {
std::cerr << "內存分配失敗" << std::endl;
} else {
// 使用num
delete num;
}(3)定位 new(placement new):placement new允許在一塊已經分配好的內存上構造對象,它不負責分配內存,只是調用對象的構造函數在指定的內存位置上初始化對象。這在一些需要精確控制內存使用的場景中,如內存池的實現、在特定的內存地址上創建對象等非常有用。使用placement new需要包含<new>頭文件,示例如下:
#include <new>
#include <iostream>
class MyClass {
public:
MyClass() {
std::cout << "MyClass的構造函數被調用" << std::endl;
}
~MyClass() {
std::cout << "MyClass的析構函數被調用" << std::endl;
}
};
int main() {
// 預先分配一塊內存
char buffer[sizeof(MyClass)];
// 使用placement new在buffer上構造MyClass對象
MyClass* obj = new (buffer) MyClass;
// 使用obj
// 注意:使用placement new創建的對象,需要手動調用析構函數
obj->~MyClass();
return 0;
}在這個例子中,我們首先分配了一塊大小為sizeof(MyClass)的字符數組buffer,然后使用placement new在buffer的內存位置上構造了一個MyClass對象。需要特別注意的是,使用placement new創建的對象,在使用完畢后,需要手動調用析構函數來清理對象資源,但不需要手動釋放內存,因為內存是預先分配的,不是由placement new分配的。
三、 兩者深度大對比
3.1 分配內存方式
從分配內存的方式來看,malloc和new有著明顯的差異。malloc是 C 語言的庫函數,在 C++ 中也可使用,它需要我們手動計算要分配的內存大小,單位是字節。例如,當我們要分配一個包含 10 個int類型元素的數組內存時,需要這樣寫:int* arr = (int*)malloc(10 * sizeof(int));,這里10 * sizeof(int)就是手動計算出的所需內存字節數 ,并且malloc返回的是一個void*類型的指針,意味著它不指向任何特定的數據類型,所以在使用時需要將其強制轉換為我們需要的指針類型,如這里轉換為int*。
而new是 C++ 的操作符,它會自動計算所需分配的內存大小,無需我們手動計算。比如,同樣是分配一個包含 10 個int類型元素的數組內存,使用new可以這樣寫:int* arr = new int[10];,new會根據int類型的大小自動計算出所需的內存空間,并且直接返回指向int類型的指針,不需要進行額外的類型轉換,這使得代碼更加簡潔和類型安全。
3.2 初始化與清理
在初始化和清理方面,malloc和free與new和delete也有著截然不同的行為。malloc分配的內存不會進行初始化,里面的內容是未定義的,可能包含任意值,也就是我們常說的 “垃圾數據”。如果我們需要使用這塊內存來存儲特定的值,就需要手動進行初始化。例如,在前面分配int數組內存的例子中,使用malloc分配后,數組中的元素值是不確定的,若要將數組元素初始化為 0,需要使用memset函數:
int* arr = (int*)malloc(10 * sizeof(int));
if (arr != NULL) {
memset(arr, 0, 10 * sizeof(int));
}當使用free釋放內存時,它僅僅是將內存歸還給系統,不會調用對象的析構函數,所以如果內存中存儲的是對象,且對象在析構時需要清理一些資源(如打開的文件、分配的其他子對象等),使用free就無法正確清理這些資源,可能會導致資源泄漏。
相比之下,new在分配內存后,會自動調用對象的構造函數進行初始化。如果分配的是基本數據類型,如int、double等,會將其初始化為默認值(對于int等整型為 0,對于double為 0.0 等);如果是自定義類對象,會調用類的構造函數進行初始化,確保對象在創建時處于一個合理的初始狀態。例如:
class MyClass {
public:
int data;
MyClass() : data(0) {
std::cout << "MyClass的構造函數被調用,初始化data為0" << std::endl;
}
~MyClass() {
std::cout << "MyClass的析構函數被調用" << std::endl;
}
};
MyClass* obj = new MyClass;這里創建MyClass對象時,new會調用其構造函數,將data初始化為 0,并輸出相應的構造信息。當使用delete釋放對象時,會先調用對象的析構函數,清理對象占用的資源,然后再釋放內存。這樣就能確保對象的資源得到正確的管理和釋放,避免資源泄漏等問題。
3.3 錯誤處理機制
malloc和new在錯誤處理機制上也有很大的不同。當malloc分配內存失敗時,它會返回NULL指針,這就要求我們在使用malloc返回的指針之前,必須手動檢查指針是否為NULL,以避免使用空指針導致程序崩潰。例如:
int* ptr = (int*)malloc(1000 * sizeof(int));
if (ptr == NULL) {
std::cerr << "內存分配失敗" << std::endl;
// 進行錯誤處理,如返回錯誤代碼、釋放已分配的其他資源等
} else {
// 使用ptr進行后續操作
}而new在分配內存失敗時,會拋出std::bad_alloc異常。這就需要我們使用 C++ 的異常處理機制(try-catch塊)來捕獲和處理這個異常,以確保程序在內存分配失敗的情況下仍能保持穩定運行,不會突然崩潰。例如:
try {
int* ptr = new int[1000000000];
// 使用ptr進行后續操作
} catch (const std::bad_alloc& e) {
std::cerr << "內存分配失敗: " << e.what() << std::endl;
// 進行錯誤處理,如返回錯誤信息給用戶、記錄日志等
}這種異常處理機制使得new在錯誤處理方面更加靈活和強大,能夠更好地適應復雜的程序邏輯和錯誤處理需求。
3.4 內存釋放關鍵:是否調用析構函數
在內存釋放環節,malloc 和 free 與 new 和 delete 的區別也十分顯著。free 函數只是單純地將申請的內存歸還給系統,不會調用對象的析構函數 。這就好比你歸還了租來的房子,但房子里你自己添置的家具、裝修等都沒有進行清理。
還是以上面的Person類為例,如果使用malloc來為Person對象分配內存,然后用free釋放:
Person* p1 = (Person*)malloc(sizeof(Person));
// 這里p1指向的內存只是開辟了空間,對象未初始化,沒有調用構造函數
free(p1);
// 直接釋放內存,不會調用Person類的析構函數,可能導致對象內部資源未釋放在這個例子中,free(p1) 只是釋放了p1所指向的內存空間,但Person對象內部可能存在一些資源,比如動態分配的成員變量、打開的文件句柄等,這些資源由于析構函數沒有被調用而無法得到正確釋放,從而造成內存泄漏 。
而 delete 操作符在釋放內存之前,會先調用對象的析構函數 。析構函數會負責清理對象內部的資源,比如釋放動態分配的成員變量內存、關閉文件句柄等,就像你在歸還房子前,把自己添置的東西都清理干凈了。之后,delete 再將對象占用的內存釋放掉 。比如:
Person* p2 = new Person("Bob", 30);
delete p2;
// 先調用Person類的析構函數,清理對象內部資源,再釋放內存在這個例子中,delete p2 會先調用Person對象的析構函數,輸出 “Destructor called for Bob” ,確保對象內部資源得到正確釋放,然后再釋放對象占用的內存空間 。這種在釋放內存前調用析構函數的機制,使得delete在處理自定義類型對象時,能夠更全面、安全地管理對象的生命周期,有效避免內存泄漏和資源未釋放的問題 。
四、實際應用場景分析
4.1 C 語言項目
在 C 語言項目中,malloc和free無疑是內存管理的主力軍,它們的身影無處不在。以一個簡單的學生信息管理系統為例,假設我們需要存儲若干學生的信息,包括姓名、年齡、成績等,由于學生數量在程序運行前是未知的,所以需要使用動態內存分配。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
typedef struct {
char name[50];
int age;
float score;
} Student;
int main() {
int n;
printf("請輸入學生數量: ");
scanf("%d", &n);
// 使用malloc分配存儲n個Student結構體的內存空間
Student* students = (Student*)malloc(n * sizeof(Student));
if (students == NULL) {
printf("內存分配失敗\n");
return 1;
}
for (int i = 0; i < n; i++) {
printf("請輸入第 %d 個學生的姓名: ", i + 1);
scanf("%s", students[i].name);
printf("請輸入第 %d 個學生的年齡: ", i + 1);
scanf("%d", &students[i].age);
printf("請輸入第 %d 個學生的成績: ", i + 1);
scanf("%f", &students[i].score);
}
// 打印學生信息
printf("\n學生信息如下:\n");
for (int i = 0; i < n; i++) {
printf("姓名: %s, 年齡: %d, 成績: %.2f\n", students[i].name, students[i].age, students[i].score);
}
// 使用free釋放內存
free(students);
return 0;
}在這個例子中,malloc根據用戶輸入的學生數量動態分配內存來存儲學生信息,free在使用完內存后將其釋放,確保了內存的合理使用。這體現了malloc和free在 C 語言項目中,對于動態內存分配和釋放的重要性和實用性,它們為處理不確定大小的數據提供了靈活的方式。
4.2 C++ 項目
在 C++ 項目中,new和delete有著獨特的優勢,尤其在創建自定義類型對象時表現得淋漓盡致。例如,當我們開發一個游戲項目,其中有一個Character類,用于表示游戲角色,每個角色都有自己的屬性和行為,如生命值、攻擊力、移動等。
#include <iostream>
#include <string>
class Character {
public:
std::string name;
int health;
int attackPower;
Character(const std::string& n, int h, int ap) : name(n), health(h), attackPower(ap) {
std::cout << "角色 " << name << " 被創建" << std::endl;
}
~Character() {
std::cout << "角色 " << name << " 被銷毀" << std::endl;
}
void move() {
std::cout << name << " 正在移動" << std::endl;
}
void attack() {
std::cout << name << " 發動攻擊,攻擊力: " << attackPower << std::endl;
}
};
int main() {
// 使用new創建Character對象
Character* player = new Character("勇者", 100, 20);
player->move();
player->attack();
// 使用delete釋放對象
delete player;
return 0;
}這里使用new創建Character對象時,會自動調用構造函數進行初始化,輸出角色創建信息;使用delete釋放對象時,會自動調用析構函數,輸出角色銷毀信息。這種自動調用構造函數和析構函數的特性,使得new和delete在處理復雜的自定義類型對象時,能夠更好地管理對象的生命周期,確保對象的資源得到正確的初始化和清理,提高了代碼的安全性和可讀性。
4.3 混合使用情況
在一些 C++ 項目中,可能會存在 C 和 C++ 代碼混合的情況,這就需要我們正確地混合使用malloc/free和new/delete。例如,在一個大型的圖形處理庫項目中,部分底層的圖形數據處理函數是用 C 語言編寫的,而上層的圖形界面交互部分是用 C++ 編寫的。在這種情況下,當 C++ 代碼調用 C 函數獲取數據時,可能會涉及到內存的分配和釋放,需要注意兩組內存管理方式的匹配使用。
假設 C 函數返回一個動態分配的字符數組:
#include <stdlib.h>
char* c_function() {
char* str = (char*)malloc(100 * sizeof(char));
if (str != NULL) {
// 這里進行一些字符串初始化操作,比如賦值為"Hello, World!"
const char* temp = "Hello, World!";
int i = 0;
while (temp[i] != '\0') {
str[i] = temp[i];
i++;
}
str[i] = '\0';
}
return str;
}在 C++ 代碼中調用這個 C 函數,并進行內存釋放:
#include <iostream>
extern "C" {
char* c_function();
}
int main() {
char* str = c_function();
if (str != NULL) {
std::cout << "從C函數獲取的字符串: " << str << std::endl;
// 使用free釋放從C函數中malloc分配的內存
free(str);
}
return 0;
}在這個例子中,C++ 代碼調用 C 函數c_function獲取了一個由malloc分配的字符串,在使用完后,必須使用free來釋放這塊內存,以確保內存的正確管理。如果使用delete來釋放malloc分配的內存,或者使用free來釋放new分配的內存,都可能會導致程序出錯,甚至崩潰。所以在混合使用 C 和 C++ 代碼時,一定要明確不同內存管理方式的適用范圍,嚴格遵循malloc與free配對、new與delete配對的原則,以保證程序的穩定性和正確性。
錯誤案例與后果:混用的代價
在實際使用中,千萬不能把 malloc、free 和 new、delete 混用,否則會帶來嚴重的后果。
比如,用 malloc 申請內存,卻用 delete 釋放:
int* ptr1 = (int*)malloc(sizeof(int));
delete ptr1;這里,delete 會嘗試調用析構函數,但由于 ptr1 是通過 malloc 分配的,沒有構造函數被調用,也就沒有合適的析構函數可調用,這就會導致未定義行為,可能引發程序崩潰。
反過來,用 new 申請內存,卻用 free 釋放:
int* ptr2 = new int;
free(ptr2);free 函數只負責釋放內存,不會調用對象的析構函數,對于自定義類型對象,其內部資源無法得到清理,從而導致內存泄漏 。
另外,使用 delete 釋放數組時,如果沒有用 delete [] ,同樣會出問題。例如:
int* arr = new int[10];
delete arr;這種情況下,delete 只會調用數組中第一個元素的析構函數,而其他元素的析構函數不會被調用,剩余元素占用的內存也無法正確釋放,這不僅會導致內存泄漏,還可能造成內存損壞,使程序在后續運行中出現各種難以調試的錯誤 。
五、最佳實踐與建議
5.1 C++ 場景下的選擇
在 C++ 項目中,由于new和delete能夠自動調用構造函數和析構函數,更好地支持面向對象編程,所以在大多數情況下,優先使用new和delete來管理對象內存是一個明智的選擇。比如在開發一個圖形渲染引擎時,其中涉及到各種圖形對象,如Shape(形狀)類、Texture(紋理)類等,使用new創建這些對象時,能夠確保它們的成員變量被正確初始化,相關資源(如紋理數據的加載)被正確設置;使用delete釋放對象時,能夠保證資源(如釋放紋理占用的顯存)被正確清理,避免內存泄漏和資源未正確釋放的問題。
5.2 智能指針的運用
為了進一步提高內存管理的安全性和便利性,C++11 引入了智能指針,如std::unique_ptr、std::shared_ptr和std::weak_ptr ,它們能夠自動管理對象的生命周期,有效避免內存泄漏和懸空指針等問題。
(1)std::unique_ptr:適用于對象只有一個所有者的場景,它采用獨占所有權模式,當std::unique_ptr離開作用域時,會自動釋放其所指向的對象。例如,在實現一個資源管理器類時,管理一些只需要一個實例的資源,如日志文件句柄,可以使用std::unique_ptr來確保資源在不再使用時被正確釋放。
#include <memory>
#include <fstream>
class Logger {
public:
void log(const std::string& message) {
logFile << message << std::endl;
}
~Logger() {
logFile.close();
}
private:
std::ofstream logFile;
};
class ResourceManager {
public:
std::unique_ptr<Logger> logger;
ResourceManager() : logger(std::make_unique<Logger>()) {}
};(2)std::shared_ptr:用于多個地方需要共享同一個對象的場景,它通過引用計數來管理對象的生命周期,當引用計數為 0 時,對象會自動被釋放。比如在實現一個多線程的網絡服務器時,多個線程可能需要共享同一個數據庫連接對象,使用std::shared_ptr可以方便地管理數據庫連接的生命周期,確保在所有線程都不再使用連接時,連接被正確關閉和資源被釋放。
#include <memory>
#include <mysql/mysql.h>
class DatabaseConnection {
public:
DatabaseConnection() {
// 初始化數據庫連接
mysql_init(&mysql);
if (!mysql_real_connect(&mysql, "localhost", "user", "password", "database", 0, NULL, 0)) {
throw std::runtime_error("數據庫連接失敗");
}
}
~DatabaseConnection() {
// 關閉數據庫連接
mysql_close(&mysql);
}
MYSQL* getConnection() {
return &mysql;
}
private:
MYSQL mysql;
};
class Server {
public:
std::shared_ptr<DatabaseConnection> dbConnection;
Server() : dbConnection(std::make_shared<DatabaseConnection>()) {}
};(3)std::weak_ptr:通常與std::shared_ptr一起使用,用于解決std::shared_ptr可能出現的循環引用問題。比如在實現一個雙向鏈表時,節點之間通過std::shared_ptr相互引用可能會導致循環引用,使用std::weak_ptr可以避免這種情況。
#include <memory>
#include <iostream>
class Node;
using NodePtr = std::shared_ptr<Node>;
using WeakNodePtr = std::weak_ptr<Node>;
class Node {
public:
int data;
NodePtr next;
WeakNodePtr prev;
Node(int d) : data(d) {}
};
int main() {
NodePtr node1 = std::make_shared<Node>(1);
NodePtr node2 = std::make_shared<Node>(2);
node1->next = node2;
node2->prev = node1;
return 0;
}5.3 內存管理的其他建議
在進行內存管理時,還有一些通用的建議可以幫助我們編寫出更健壯、高效的代碼。首先,一定要遵循內存分配和釋放的配對原則,使用malloc分配的內存必須使用free釋放,使用new分配的內存必須使用delete釋放,并且要確保在程序的所有可能執行路徑上,內存都能被正確釋放,避免出現內存泄漏。其次,在分配內存后,要及時檢查分配是否成功,malloc返回NULL或者new拋出異常時,要進行適當的錯誤處理,如記錄日志、返回錯誤信息給用戶等,而不是讓程序繼續執行可能導致崩潰的操作。
另外,對于頻繁分配和釋放小內存塊的場景,可以考慮使用內存池技術,提前分配一塊較大的內存,然后在需要時從內存池中分配小塊內存,使用完后再歸還到內存池中,這樣可以減少系統調用的開銷,提高內存分配和釋放的效率。最后,利用一些工具如 Valgrind(在 Linux 系統中)、AddressSanitizer(在 Clang 和 GCC 編譯器中支持)等,來檢測程序中的內存泄漏、懸空指針、越界訪問等內存問題,這些工具能夠幫助我們在開發和測試階段及時發現并解決內存相關的錯誤,提高程序的穩定性和可靠性。




























