博客 / 詳情

返回

每日一個C++知識點|底層內存管理

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原則的方法,利用對象的構造/析構函數自動管理資源,或者可以使用智能指針

野指針

除了內存泄漏會導致程序崩潰之外,野指針也會使程序崩潰

野指針是指向已釋放內存或非法內存的指針,使用野指針會導致程序崩潰、數據損壞,甚至比內存泄漏更危險

為什麼會產生野指針呢?主要有一下三種原因:

  1. 指針未初始化
  2. 指針指向的內存被釋放後未置空
  3. 指針越界

針對野指針產生的問題,有以下四種方法解決:

  1. 初始化指針:聲明時直接置空\
    int* p = nullptr;
  2. 釋放內存後置空\
    delete p; p = nullptr;
  3. 避免指針越界\
    使用vector代替原生數組
  4. 使用智能指針

以下通過代碼進行展示:

#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++底層內存管理主要説了以下幾方面的內容:

  1. 在哪裏分配內存(五大內存分區)
  2. 怎麼分配內存(new/delete)
  3. 要注意內存泄漏
  4. 解決內存泄漏(智能指針)

本文寫到這裏就結束了,如果這文章對你有幫助的話,歡迎點贊+關注哦~

user avatar
0 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.