69pao国产精品视频-久久精品一区二区二三区-精品国产精品亚洲一本大道-99国产综合一区久久

C++學習之如何進行內存資源管理


前言

與java、golang等自帶垃圾回收機制的語言不同,c++并不會自動回收內存。我們必須手動管理堆上內存分配和釋放,這往往會導致內存泄漏和內存溢出等問題。而且,這些問題可能不會立即出現(xiàn),而是運行一段時間后,才會暴露出現(xiàn),排查也很困難。因此,了解和掌握c++中的內存管理技巧和工具是非常重要的,可以提高程序性能、減少錯誤和增加安全性。

 

內存分區(qū)

在c++中,將操作系統(tǒng)分配給程序的內存空間按照用途劃分了代碼段、數(shù)據(jù)段、棧、堆幾個不同的區(qū)域,每個區(qū)域都有其獨特的內存管理機制。

代碼區(qū)

代碼區(qū)是用于存儲程序代碼的區(qū)域,代碼段在程序真正執(zhí)行前就被加載到內存中,在程序執(zhí)行期間,代碼區(qū)內存不會被修改和釋放。

由于代碼區(qū)是只讀的,所以會被多個進程共享。在多個進程同時執(zhí)行同一個程序時,操作系統(tǒng)只需要將代碼段加載到內存中一次,然后讓多個進程共享這個內存區(qū)域即可。

數(shù)據(jù)段

數(shù)據(jù)段用于存儲靜態(tài)全局變量、靜態(tài)局部變量和靜態(tài)常量等靜態(tài)數(shù)據(jù)。在程序運行期間,數(shù)據(jù)段的大小固定不變,但其內容可以被修改。按照變量是否被初始化。數(shù)據(jù)段可分為已初始化數(shù)據(jù)段和未初始化數(shù)據(jù)段。

c++中函數(shù)調用以及函數(shù)內的局部變量的使用,都是通過棧這個內存分區(qū)實現(xiàn)的。棧分區(qū)由操作系統(tǒng)自動分配和釋放,是一種"后進先出"的一種內存分區(qū)。每個棧的大小是固定的,一般只有幾mb,所以如果棧變量太大,或者函數(shù)調用嵌套太深,容易發(fā)生棧溢出(stack overflow)。

先來一段示例代碼,看看c++是如何使用棧進行使用棧來進行函數(shù)調用的。

#include  void inner(int a) {
    std::cout << a << std::endl;
}
void outer(int n) {
 int a = n + 1;
    inner(a);
}

int main() {
    outer(4);
}

上面這段代碼運行過程中的棧變化如下圖

每當程序調用一個函數(shù)時,該函數(shù)的參數(shù)、局部變量和返回地址等信息會被壓入棧中。當函數(shù)執(zhí)行完畢,再將這些信息從棧中彈出。根據(jù)之前壓入的外層調用者壓入棧的返回地址,返回到外層調用者未執(zhí)行的代碼繼續(xù)執(zhí)行。

本地變量是直接存儲在棧上的,當函數(shù)執(zhí)行完成后,這些變量占用的內存就會被釋放掉了。前面例子中的本地變量是簡單類型,在c++中稱為pod類型。對于帶有構造和析構函數(shù)的非pod類型變量,棧上的內存分配同樣有效。編譯器會在合適的時機,插入對構造函數(shù)和析構函數(shù)的調用。

這里有個問題,當函數(shù)執(zhí)行發(fā)生異常時,析構函數(shù)還會被調用嗎?答案是會的,c++對于發(fā)生異常時對析構函數(shù)的調用稱為"棧展開"。通過下面這段代碼演示棧展開。

#include  #include  class obj {
public:
    std::string name_;
    obj(const std::string& name):name_(name){std::cout << "obj() " << name_ << std::endl;};
    ~obj() {std::cout << "~obj() " << name_ << std::endl;};
};


void bar() {
    auto o = obj{"bar"};
    throw "bar exception";
}

int main() {
    try {
        bar();
    } catch (const char* e) {
        std::cout << "catch exception: " << e << std::endl;
    }
}

執(zhí)行代碼的結果是:

obj()bar
~obj()bar
catchexception:barexception

可以發(fā)現(xiàn),發(fā)生異常時,bar函數(shù)中的本地變量o還是能被正常析構。

棧展開的過程實際上是異常發(fā)生時,匹配catch子句的過程。

  • 程序拋出異常,停止當前執(zhí)行的調用鏈,開始尋找與異常匹配的catch子句。
  • 如果異常發(fā)生在try中,則會首先檢查與該try塊匹配的catch子句。如果異常所在函數(shù)體沒有try捕獲異常。則會直接進入下一步。
  • 如果第二步未找到匹配的catch,則會在外層的try塊中查找,直到找到為止。
  • 如果到了最外層還沒有找到匹配的catch,也就是說異常得不到處理,程序會調用標準庫函數(shù)terminate終止函數(shù)的執(zhí)行。

在這期間,棧上所有的對象都會被自動析構。

堆是c++中用來存儲動態(tài)分配內存的內存分區(qū),堆內存的分配和釋放需要手動管理,可以通過new/delete或malloc/free等函數(shù)進行分配和釋放。堆內存的大小通常是不固定的,當我們需要動態(tài)分配內存時,就可以使用堆內存。

堆內存由程序員手動分配和釋放,因此使用堆內存需要注意內存泄漏和內存溢出等問題。當程序員忘記釋放已分配的內存時,會導致內存泄漏問題。而當申請的堆內存超過了操作系統(tǒng)所分配給進程的內存限制時,會導致內存溢出問題。

c++程序絕大多數(shù)的內存泄露,都是由于忘記調用delete/free來釋放堆上的資源。

還是上代碼

#include  #include  class obj {
public:
    std::string name_;
    obj(const std::string& name):name_(name){std::cout << "obj() " << name_ << std::endl;};
    ~obj() {std::cout << "~obj() " << name_ << std::endl;};
};

obj* makeobj() {
 obj* obj = nullptr;
 try {
  obj = new obj{"makeobj"};
  ...
 } catch(...) {
  delete obj;
  throw;
 }
 return obj;
}

obj* foo() {
 obj* obj = nullptr;
 try {
  obj = makeobj();
  ...
 } catch(...) {
  delete obj;
 }
 return obj;
}
int main() {
    obj* obj = foo();
    ...
    delete obj;
}

可以看到,由makeobj函數(shù)創(chuàng)建的堆變量obj, 在每個獲取該變量的上層調用中,都需要關心對該變量的處理。這無疑極大得增加了開發(fā)者的心智負擔。

 

raii

想在堆上創(chuàng)建對象,又不想處理這么復雜的內存釋放操作。c++沒有像java、golang其他語言創(chuàng)建一套垃圾回收機制,而是采用了一種特有的資源管理方式 --- raii(resource acquisition is initialization,資源獲取即初始化)。

raii利用棧對象在作用域結束后會自動調用析構函數(shù)的特點,通過創(chuàng)建棧對象來管理資源。在棧對象構造函數(shù)中獲取資源,在棧對象析構函數(shù)中負責釋放資源,以此保證資源的獲取和釋放。

下面給出一個通過raii來自動釋放堆內存的例子

#include  class autointptr {
public:
    autointptr(int* p = nullptr) : ptr(p) {}
    ~autointptr() { delete ptr; }

    int& operator*() const { return *ptr; }
    int* operator->() const { return ptr; }

private:
    int* ptr;
};

void foo() {
 autointptr p(new int(5));
    std::cout << *p << std::endl; // 5
}

int main() {
    foo();
}

上面例子中,autointptr類封裝了一個動態(tài)分配的int類型的指針,它的構造函數(shù)用于獲取資源(ptr = p),析構函數(shù)用于釋放資源(delete ptr)。當autointptr超出作用域時,自動調用析構函數(shù)來釋放所包含的資源。

基于raii,c++11引入了std::unique_ptr和std::shared_ptr等智能指針用于內存管理類,使得內存管理變得更加方便和安全。這些內存管理類可以自動進行內存釋放,避免了手動釋放內存的繁瑣工作。值得一提的是,上面的autointptr就是一個簡化版的智能指針了。

在實際開發(fā)中,raii的應用很廣。不僅僅用于自動釋放內存。還可以用來關閉文件、釋放數(shù)據(jù)庫連接、釋放同步鎖等。

 

總結

本文介紹了c++中的內存管理機制,包括內存分區(qū)、棧、堆和raii技術等內容。通過學習本文,我們可以更好地掌握c++的內存管理技巧,避免內存泄漏和內存溢出等問題。

相關文章