C++的手動內存管理機制賦予了程序員極高的靈活性,但也帶來了內存泄漏、野指針等風險。本文從內存區分開始,逐步從深入瞭解C++內存的核心知識~
內存分區
在C++程序運行時,內存會被劃分為五個區域,分別是棧區、堆區、全局/靜態區、常量區和代碼區,如圖所示:
棧區是程序運行中一塊連續的內存區域,主要用來存儲局部變量,由編譯器自動分配和釋放,空間小但速度快
堆區是程序運行時由程序員手動分配和釋放的非連續內存區域,主要用於存儲動態數組、對象等數據
全局/靜態區是專門存儲全局變量和靜態變量的區域,程序啓動時分配,退出時釋放
常量區是存儲字符串常量、const常量的區域,都是隻讀不可修改
代碼區是存儲程序的二進制執行代碼的區域
下面通過代碼進行舉例,通過註釋標出以上分區對應的代碼段,使我們有一個直觀的感受:
#include <iostream>
#include <string>
using namespace std;
// 全局/靜態區:全局變量
int global_num = 100;
// 簡單的學生類(用於演示對象存儲)
class Student {
public:
string name;
int age;
Student(string n, int a) : name(n), age(a) {}
};
int main() {
// 棧區:局部變量和局部對象
int local_num = 200;
Student stu1("張三", 18);
// 堆區:動態分配的對象
Student* stu2 = new Student("李四", 19);
// 靜態區:靜態變量
static int static_num = 300;
// 常量區:只讀字符串常量
const char* greeting = "Hello C++";
// 輸出數據驗證
cout << "全局變量:" << global_num << endl;
cout << "局部變量:" << local_num << endl;
cout << "棧區對象:" << stu1.name << "," << stu1.age << endl;
cout << "堆區對象:" << stu2->name << "," << stu2->age << endl;
cout << "靜態變量:" << static_num << endl;
cout << "常量字符串:" << greeting << endl;
// 手動釋放堆區內存
delete stu2;
stu2 = nullptr;
return 0;
}
上述代碼中為什麼沒有代碼區呢?代碼區存儲的是程序的二進制執行指令,而非可直接操作的變量和數據,因而代碼區無法在上述代碼中展示,以上就是五大內存分區的主要內容~
內存的分配和釋放
我們初步瞭解五大內存分區的基本情況之後,接下來便要了解內存分配和釋放的原理和方法
作為程序員,這裏我們主要講的是程序員手動分配和釋放內存的區域,就是堆區
在堆區,我們創建和銷燬內存時可以使用 C 語言的malloc/free函數庫,也可以使用 C++ 特有的new/delete操作符,下面我們通過具體的代碼示例對比這兩種方法:
C語言:malloc+free
int* c_int = (int*)malloc(sizeof(int)); // 僅分配內存,未初始化
if (c_int == nullptr) { // 需手動檢查分配是否成功
cerr << "malloc失敗" << endl;
return 1;
}
*c_int = 10; // 手動賦值
cout << "malloc分配的int值:" << *c_int << endl;
free(c_int); // 僅釋放內存,無其他操作
c_int = nullptr; // 避免野指針
C++:new+delete
int* cpp_int = new int(20); // 分配內存 + 直接初始化(值為20)
// new失敗會拋異常(默認),無需手動檢查(除非用nothrow版本:new (nothrow) int)
cout << "new分配的int值:" << *cpp_int << endl;
delete cpp_int; // 僅釋放內存,無析構(基本類型無析構)
cpp_int = nullptr;
運行結果如下:
兩種方法都實現了內存創建和釋放的功能
但如果時涉及構造函數和析構函數的時候,new會自動調構造函數,malloc不會;delete會自動調用析構函數,free不會;舉例如下:
// 定義一個測試類(用於體現new/delete的構造/析構特性)
class Test {
public:
// 構造函數(new會自動調用,malloc不會)
Test(int val = 0) : num(val) {
cout << "Test構造函數:num = " << num << endl;
}
// 析構函數(delete會自動調用,free不會)
~Test() {
cout << "Test析構函數:num = " << num << endl;
}
int num; // 成員變量
};
測試如下:
// C語言:malloc+free(無法調用構造/析構)
Test* c_test = (Test*)malloc(sizeof(Test)); // 僅分配內存,構造函數未執行
if (c_test == nullptr) {
cerr << "malloc失敗" << endl;
return 1;
}
// 手動調用構造函數(需用placement new,非常規操作)
new (c_test) Test(30); // 僅演示,實際極少用
cout << "malloc+placement new的Test值:" << c_test->num << endl;
c_test->~Test(); // 手動調用析構函數
free(c_test); // 釋放內存
c_test = nullptr;
// C++:new+delete(自動調用構造/析構)
Test* cpp_test = new Test(40); // 分配內存 + 自動調用構造函數
cout << "new分配的Test值:" << cpp_test->num << endl;
delete cpp_test; // 自動調用析構函數 + 釋放內存
cpp_test = nullptr;
運行結果如下:
由此可知:malloc僅分配內存,不會自動調用構造函數;free僅釋放內存,不會自動調用析構函數;new/delete既可以手動分配/釋放內存,又可以自動調用構造函數和析構函數來分配和釋放內存
內存泄漏
上面我們已經瞭解內存的分配和釋放的過程和方法了,但是如果已分配的堆內存不再使用,但未被釋放,就會導致系統內存被持續佔用,最終可能使程序崩潰,這就是內存泄漏
例如,創建了一個動態對象後,不小心覆蓋了指向它的指針,導致內存無法釋放:
#include <iostream>
#include <string>
using namespace std;
// 簡單的字符串包裝類
class MyString {
public:
MyString(const string& s) {
cout << "創建字符串:" << s << endl;
// 模擬分配堆內存
data = new char[s.size() + 1];
copy(s.begin(), s.end(), data);
data[s.size()] = '\0';
}
~MyString() {
cout << "釋放字符串:" << data << endl;
delete[] data;
}
private:
char* data;
};
int main() {
// 內存泄漏場景1:覆蓋指針,失去對堆內存的引用
MyString* str1 = new MyString("hello");
str1 = new MyString("world"); // 原str1的內存無法釋放,泄漏!
// 內存泄漏場景2:函數中分配內存,未返回也未釋放
auto create_string = []() {
MyString* str = new MyString("test");
// 忘記return或delete,內存泄漏!
};
create_string();
return 0;
}
正確做法:及時釋放,避免覆蓋指針
MyString* str2 = new MyString("correct");
delete str2;
str2 = nullptr; // 置空,避免野指針
避免內存泄漏的方法除了及時釋放外,還可以使用RAII原則的方法,利用對象的構造/析構函數自動管理資源,或者可以使用智能指針
野指針
除了內存泄漏會導致程序崩潰之外,野指針也會使程序崩潰
野指針是指向已釋放內存或非法內存的指針,使用野指針會導致程序崩潰、數據損壞,甚至比內存泄漏更危險
為什麼會產生野指針呢?主要有一下三種原因:
- 指針未初始化
- 指針指向的內存被釋放後未置空
- 指針越界
針對野指針產生的問題,有以下四種方法解決:
- 初始化指針:聲明時直接置空\
int* p = nullptr; - 釋放內存後置空\
delete p; p = nullptr; - 避免指針越界\
使用vector代替原生數組 - 使用智能指針
以下通過代碼進行展示:
#include <iostream>
using namespace std;
// 簡單的整數包裝類
class MyInt {
public:
MyInt(int v) : val(v) {
cout << "創建MyInt:" << val << endl;
}
~MyInt() {
cout << "銷燬MyInt:" << val << endl;
}
int val;
};
int main() {
// 野指針場景1:指針未初始化
MyInt* p1;
// p1->val = 10; // 未定義行為,程序可能崩潰!
// 野指針場景2:釋放後未置空
MyInt* p2 = new MyInt(20);
delete p2;
// p2->val = 30; // 野指針,訪問已釋放內存!
p2 = nullptr; // 置空後,訪問會直接崩潰(便於調試)
// 正確做法:初始化+釋放後置空
MyInt* p3 = nullptr;
p3 = new MyInt(40);
if (p3 != nullptr) { // 判空後使用
cout << "MyInt的值:" << p3->val << endl;
delete p3;
p3 = nullptr;
}
return 0;
}
智能指針
C++開發中內存管理是一件大事,經常會出現內存泄漏或野指針這種問題導致程序崩潰,而且手動管理內存容易出錯,有什麼辦法可以一勞永逸呢?
C++11引入了智能指針,基於RAII原則,將指針封裝成類,在構造函數中分配內存,析構函數中自動釋放內存,從根源上解決內存泄漏和野指針問題,這就是智能指針
智能指針有三種,分別是unique_ptr,shared_ptr,weak_ptr
unique_ptr
unique_ptr是獨佔式智能指針,獨佔所管理的內存,不允許拷貝和賦值,只能移動
其適用場景是單一對象的獨佔管理,比如單個動態對象、動態數組等
shared_ptr
shared_ptr是共享式智能指針,通過引用計數實現多個指針共享同一塊內存,引用計數為0時釋放內存
適用場景是多個對象共享同一個資源
weak_ptr
weak_ptr是弱引用智能指針,配合shared_ptr使用,不增加引用計數,解決循環引用問題
主要適用場景是打破shared_ptr的循環引用,如雙向鏈表的節點相互引用
下面通過代碼示例來體現智能指針的用法
#include <iostream>
#include <memory>
using namespace std;
// 簡單的日誌類(作為共享資源)
class Logger {
public:
Logger(const string& n) : name(n) {
cout << "創建日誌器:" << name << endl;
}
~Logger() {
cout << "銷燬日誌器:" << name << endl;
}
void log(const string& msg) {
cout << "[" << name << "] " << msg << endl;
}
string name;
};
// 業務類(使用日誌器)
class Business {
public:
Business(shared_ptr<Logger> l) : logger(l) {
cout << "創建業務對象,使用日誌器:" << l->name << endl;
}
~Business() {
cout << "銷燬業務對象" << endl;
}
void do_work() {
logger->log("執行業務邏輯");
}
private:
shared_ptr<Logger> logger;
};
int main() {
// 1. unique_ptr:獨佔資源
unique_ptr<Logger> log1(new Logger("獨佔日誌器"));
// unique_ptr<Logger> log2 = log1; // 報錯:不能拷貝
unique_ptr<Logger> log2 = move(log1); // 移動語義,log1變為空
// 2. shared_ptr:共享資源(多個業務對象共享同一個日誌器)
shared_ptr<Logger> shared_log(new Logger("共享日誌器"));
Business b1(shared_log);
Business b2(shared_log);
cout << "日誌器的引用計數:" << shared_log.use_count() << endl; // 輸出:3(shared_log + b1 + b2)
b1.do_work();
b2.do_work();
return 0;
}
shared_ptr 的循環引用
shared_ptr的引用計數機制看似完美,但當兩個shared_ptr互相指向對方時,會產生循環引用,導致引用計數永遠不為 0,內存無法釋放。
那麼應該如何解決循環引用問題呢?將其中一個shared_ptr改為weak_ptr,因為weak_ptr不增加引用計數,僅作為弱引用
#include <iostream>
#include <memory>
using namespace std;
// 簡單的節點類(用於演示循環引用)
class Node {
public:
string name;
// 子節點:shared_ptr
shared_ptr<Node> child;
// 父節點:weak_ptr(解決循環引用)
weak_ptr<Node> parent; // 若改為shared_ptr<Node> parent,則產生循環引用
Node(string name_) : name(name_) {
cout << "創建節點:" << name << endl;
}
~Node() {
cout << "銷燬節點:" << name << endl;
}
};
int main() {
// 創建父節點和子節點
shared_ptr<Node> parent(new Node("父節點"));
shared_ptr<Node> child(new Node("子節點"));
// 建立引用關係
parent->child = child;
child->parent = parent;
cout << "父節點引用計數:" << parent.use_count() << endl; // 輸出:1
cout << "子節點引用計數:" << child.use_count() << endl; // 輸出:2(child + parent->child)
// weak_ptr的使用:lock()轉換為shared_ptr
shared_ptr<Node> p = child->parent.lock();
if (p) {
cout << "子節點的父節點:" << p->name << endl;
}
return 0;
}
如果將weak_ptr<Node> parent改為shared_ptr<Node> parent,則會產生循環引用,節點的析構函數不會被調用,導致內存泄漏
以上就是內存管理的基本內容~
總結
C++底層內存管理主要説了以下幾方面的內容:
- 在哪裏分配內存(五大內存分區)
- 怎麼分配內存(new/delete)
- 要注意內存泄漏
- 解決內存泄漏(智能指針)
本文寫到這裏就結束了,如果這文章對你有幫助的話,歡迎點贊+關注哦~