一区二区三区在线-一区二区三区亚洲视频-一区二区三区亚洲-一区二区三区午夜-一区二区三区四区在线视频-一区二区三区四区在线免费观看

服務器之家:專注于服務器技術及軟件下載分享
分類導航

PHP教程|ASP.NET教程|Java教程|ASP教程|編程技術|正則表達式|C/C++|IOS|C#|Swift|Android|VB|R語言|JavaScript|易語言|vb.net|

服務器之家 - 編程語言 - C/C++ - C++內存管理詳細解析

C++內存管理詳細解析

2022-02-28 15:04妙妙園 C/C++

這篇文章主要給大家分享的是C++內存管理的詳細內容學習,下面文章圍繞C++內存管理的相關資料展開具體學習內容,需要的朋友可以參考一下,希望對你有所幫助

 

一、C++內存管理

C++中有四種內存分配、釋放方式:

C++內存管理詳細解析

最高級的是std::allocator,對應的釋放方式是std::deallocate,可以自由設計來搭配任何容器;new/delete系列是C++函數,可重載;malloc/free屬于C++表達式,不可重載;更低級的內存管理函數是操作系統直接提供的系統調用,通常不會到這個層次來寫C++應用程序。接下來的闡述集中在上三層。

現在讓我們寫一些示例:

// c語言中的malloc/free
void *p1 = malloc(512);
*(int*) p1 = 100;
free(p1);

// c++表達式new/delete 
int *p2 = new int(100);// 這里應該是初始化為100,若要聲明數組,用new int[n];
delete p2;

// c++函數,等價于malloc和free
void *p3 = ::operator new(512);
*(int *)p3 = 100;
::operator delete(p3);

// c++標準庫,通過object調用,以GNU為例
int *p4 = alloctor<int>().allocate(7);// 這里是分配7個int單元而不是7bytes
*p4 = 9;
allocator<int>().deallocate((int *)p4,7);// 7應該與上文匹配

 

1、 new/delete表達式

new表達式的內部實現是由三個步驟組成的,首先調用operator new分配一定的字節數的內存,此時得到的內存指針是void*類型的,將之用static_cast轉換成需要的class類型,然后調用class的構造函數完成初始化。

Complex *pc;
try {
   void* mem = operator new(sizeof(Complex)); // 分配內存
   pc = static_cast<Complex*>(mem);    // cast 轉型 以符合對應的類型,這里對應為Complex*
   pc->Complex::Complex(1,2); // construct
   // 注意:只有編譯器才可以像上面那樣直接呼叫constructor 欲直接調用constructor可通用placement new: new(p) Complex(1,2);
}
catch(std::bad_alloc) {
   // 若allocation失敗就不執行constructor
}


從上面的new的實現中,不難發現,new返回的是對象指針,失敗是拋出bad_alloc異常,我們應該通過是否拋出異常來判斷new執行狀況,而不是像malloc一樣通過判斷返回值是否為nullptr

對于delete,其具體操作與new相反,先調用對象的析構函數,再釋放內存。

// delete pc;
pc->~Complex();  //先析構
operator delete(pc);   //然后釋放內存


array newarray delete可以獲取一組對象,new的時候不能同時初始化,因此通常結合placement new來創建對象。

class A{
public:
 int id;
 
    A(): id(0){}
    A(int i): id(i){}
    ~A(){}
};

A* buf = new A[size]; // 調用三次默認構造函數A():id(0)
A* tmp = buf;
// placement new指定位置new
for(int i = 0; i<size; i++)
    new(tmp++) A(i);  //調用A(int i): id(i)自定義初始化
delete []buf; // 調用三次~A()


new array的構造是從0到size-1依次構造,析構的時候恰恰相反(但這不重要)。new array返回的指針指向第0個對象的起始位置。

數組的new必須和數組的delete聯合使用,否則可能發生內存泄漏:若只使用delete而非delete [] ,則只會將分配的size塊內存空間釋放,但是不會調用對象的析構函數(可能是因為此處將size塊對象視為一個對象之后,找不到合適的析構函數),沒有析構就釋放內存是很不優雅的,如果對象內部還使用了new指向其他空間,那么這部分空間不會被釋放。如果new array分配的是一些析構函數沒有意義的對象

比如:

int* pi = new int[10];
delete pi;


那么是完全沒有問題的,delete等價于delete[]

附圖:聲明了3個對象的時候的內存分布,其中cookie保存著數 組大小等信息。

C++內存管理詳細解析

 

2、new/delete重載

C++內存管理詳細解析

C++ new的調用鏈是圖中的2,operator new將在全局環境下尋找匹配的函數。如果要重構,則最好在此處將其轉為調用類內自定義的Foo::operator new,在Foo::operator new中再調用::operator new。已經定義了重載之后,也可以通過直接調用::operator new來繞過重載。重載的原則是,盡量在高層、部分可見的局部進行重載,使影響盡可能小而且可控。繪制函數調用鏈可以很好地幫助決定重載層次。除了new之外,new array等也可以重載。

 

3、類內自定義allocator(per-class allocator)

本節介紹的是一個類內借助內存池的內存管理。雖然malloc不慢,但減少malloc調用次數總是好的;此外,一次malloc得到的內存塊前總是帶有一個cookie,它占有8個字節。基于以上兩個原因,從時間和空間的角度看,建立內存池都是有必要的。

思考:當每次alloc的時候都alloc固定大小的一大塊的時候,應該更難以產生外部碎片(雖然可能更容易產生內部碎片),而且固定大小對于OS的高級分配器來說是十分友好的。

這里直接看per-class allocator3。

  • embedded pointer和類型轉換
  • 鏈表管理的內存池
  • 抽象的思想
  • if判斷將值放在變量前面,這樣可以避免少寫等號,編譯器不報錯問題,例如 if(1!=p){}
#include <iostream>
#include <complex>
using namespace std;

class my_allocator{
    private:
     struct obj{
            struct obj* next;  // embedded pointer
        };
     obj* freestore = nullptr;
     const int CHUNK = 5;
    public:
     my_allocator(){};
     ~my_allocator(){};
     void* allocate(size_t);
     void deallocate(void*, size_t);
};

void* my_allocator::allocate(size_t size){
    // 從內存池分配一個obj對象大小的內存
    assert(size>0);
    obj* p;
    if(!freestore){
        freestore = p = static_cast<obj*>(malloc(CHUNK*size));
        for(int i = 0;i<CHUNK-1;i++){
            p->next = (obj*)((char*)(p+size));
            p = p->next;
        }
        p->next = nullptr;
    }
    p = freestore;
    freestore = freestore->next;
    return p;
}

void deallocate(void* p, size_t size){
    // 插入到內存池
    (static_cast<obj*>(p))->next = freestore;
    freestore = static_cast<obj*>(p);
}

// example
class Foo{
    public:
     long L;
     string str;
     static my_allocator myAlloc;
     Foo(long l): L(l){}
     static void* operator new(size_t size){
            return myAlloc.allocate(size);
        }
     static void operator delete(void* dead, size_t size){
            return myAlloc.deallocate(dead, size);
        }
};
my_allocator Foo::myAlloc; // 靜態成員變量一定要在類聲明之外定義

以下討論GNU編譯器中的內存管理機制。

allocator是普通的分配器,它通過operator newoperator delete調用mallocfree,沒有特殊的設計。

C++內存管理詳細解析

G4.9的__pool_alloc(相當于G2.9的std::alloc)是在容器中使用的分配器,是利用上了內存池的分配器。std::alloc使用一個16個寫代指針頭的數組來管理內存鏈表,數組的不同元素管理不同大小的區塊,每種區塊大小相差8個字節。內存首先由malloc分配到戰備池pool中,再從戰備池挖適當的空間到鏈表。假設用戶需要32字節的內存,std::alloc首先申請一塊區間,大小為32*20*2,用一條鏈表管理,然后讓數組的#3指針管理這條鏈表,接著將其中一個單元(32字節)分給用戶。這32*20*2中,一半是給用戶的,后一半預留在戰備池中,如果此時用戶需要一個64字節的空間,那么剩下的一半將變成64*10(通常是申請64*20),由另一個鏈表指針指向這里,然后將其中64字節分配給用戶,而不用再一次構建鏈表和申請空間。鏈表數組維護的鏈表最大塊是128字節,如果申請超過了這個大小,那么直接調用malloc給用戶分配,這樣每一塊都會帶上cookie頭和尾。

  • 戰備池,池中內存沒有固定塊大小
  • 多級大小內存池鏈表
  • 兩級分配器:超過最大大小直接使用malloc分配

G2.9中的一級配置器主要是對malloc和free進行了一些封裝,當申請的內存較大的時候,二級分配器將直接調用一級分配器。一級分配器在G4.9中已經棄用。此處不再過多闡述。

二級配置器執行分配器的主要功能。流程圖和部分源碼如下。

C++內存管理詳細解析

static const int __ALLGN = 8; // 上調邊界
static const int __MAX_BYTES = 8; // 分配Chunk的上限
static const int __NFREELISTS = __MAX_BYTES/__ALLGN; // 鏈表的條數

template<bool threads, int inst>
class __default_alloc_template{
 private:
  static size_t ROUND_UP(size_t bytes){ // 向上取整8 
   return (bytes+__ALLGN-1) & ~(__ALLGN-1);
  }
  union obj{ // 亦可用struct
   union obj* free_list_link; // 鏈表的next指針,老規矩用了ebedded pointer
  }
  static obj* volatile free_list[__NFREELISTS]; // 多級大小內存池
  static size_t FREELIST_INDEX(size_t bytes){ // 根據大小確定鏈表index
   return ((bytes+ALLGN-1)/__ALLGN-1);
  }
  static void *refill(size_t size);
  static char* chunk_alloc(size_t size, int &nobjs);
  
  // 戰備池
  static char* start_free; // 指向pool的頭
  static char* end_free; // 指向pool的尾
  static size_t heap_size; // 分配累積量
  
 public:
  static void* allocate(size_t size){
   obj* volatile *my_free_list; // 鏈表的鏈表
   obj* result;
   
   if(size > (size_t)__MAX_BYTES) // 大于128改用第一級分配器
    return (malloc_alloc::allocate(size));
   my_free_list = free_list+FREELIST_INDEX(size);
   result = *my_free_list;
   if(0==result){
    void* t = refill(ROUND_UP(size)); // 對此鏈表充值
    return t;
   }
   *my_free_list = result->free_list_link;
   return result;
  }
  static deallocate(void* p, size_t size){
            obj* q = (obj*)p;
            obj* volatile* my_free_list;
   if(size > static_cast<size_t>(__MAX_BYTES)){
    malloc_alloc::deallocate(p,size); // 大于128改用第一級分配器
    return;
   }
   my_free_list = free_list + FREELIST_INDEX(size);
   q->free_list_link = *my_free_list;
   *my_free_list = q;
  }
  static void* reallocate(void* p, size_t old_size,size_t new_size);
}


/*
    We allocate memory in large chunks inn order to avoid fragmenting the malloc
   heap too much, We assume that size is properly aligned.
    We hold the allocation lock. 
*/
template<bool threads, int inst>
char* __default_alloc_template<threads, inst>::chunk_alloc(size_t size, int& nobjs) {
 
    char* result;
    size_t total_bytes = size * nobjs;
    size_t bytees_left = end_free - start_free;
    
    if(bytees_left >= total_bytes) {  //pool空間足以滿足需求
 
        result = start_free;
        start_free += total_bytes; // 【Q1:如果pool中的空間不連續還能直接分配和相加嗎?A:虛擬地址是連續的】
        return(result); 
    }else if(bytees_left >= size) {  //pool空間只滿足一塊以上
        
        nobjs = bytees_left / size;   //改變需求個數
        total_bytes  = size * nobjs;  //改變需求總量   pass-by-value會改變參數
        result = start_free;
        start_free += total_bytes;
        return (result); 
    }else {   //pool空間不足以滿足一塊需求  碎片&&0
 
        //打算從system free-store上去這么多來充值
        size_t bytes_to_get = 2 * total_bytes + ROUND_UP(heap_size >> 4);
        //處理碎片(將其掛到相應的chunk指針端口)
        if(bytes_to_get > 0) {
            obj* volatile *my_free_list =   //重新定位碎片的指針
               free_list + FREELIST_INDEX(bytees_left); 
            ((obj*)start_free)->free_list_link = *my_free_list;
            *my_free_list = (obj*)start_free; 
        }
 
        //從system free-store中取
        start_free = (char*)malloc(bytes_to_get); 
        if(0 == start_free) {  //如果當前的chunk分配失敗,則向上繼續找相鄰的chunk繼續分配
            obj* volatile *my_free_list, *p;
            for(int i = size; i <= __MAX_BYTES; i += __ALLGN) {
                my_free_list = free_list + FREELIST_INDEX(i); 
                p = *my_free_list;
                if(0 != p) {  //該free-list有可用區塊
                    *my_free_list = p->free_list_link; 
                    start_free  = (char*)p;
                    end_free = start_free + i; 
                    return (chunk_alloc(size, nobjs));   //結果再試一次
                }
            }
            end_free = 0;
            start_free = (char*)malloc_alloc::allocate(bytes_to_get); 
        }
 
        //至此,表示已經從system free-store成功取得很多memory
        heap_size += bytes_to_get;
        end_free = start_free + bytes_to_get;
        return (chunk_alloc(size, nobjs));   //戰備池有內存了,所以遞歸重新處理分配邏輯
    }
}
 
//靜態定義(分配內存)
template<bool threads, int inst>
char* __default_alloc_template<threads, inst>::start_free = 0;
 
 
template<bool threads, int inst>
char* __default_alloc_template<threads, inst>::end_free = 0;
 
template<bool threads, int inst>
size_t __default_alloc_template<threads, inst>::heap_size = 0;
 
 
template<bool threads, int inst>
__default_alloc_template<threads, inst>::obj* volatile
__default_alloc_template<threads, inst>::free_list[__NFREELISTS] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
 
 
//std::alloc為第二級分配器
typedef  __default_alloc_template<false, 0> alloc;

int main(void){
    std::vector<int,MyAllocator<int>> v;
}

 

二、多線程內存分配器

__pool_alloc:For thread-enabled configurations, the pool is locked with a single big lock.

mt_alloc:使用了全局鏈表,分配到線程時移動到線程專享鏈表,在此過程中,只對鏈表的一個bin加鎖。exponentially-increasing allocations

tips:盡量減小鎖的粒度

C++內存管理詳細解析

 

1、malloc/free

malloc/free是 libc實現的庫函數,主要實現了一套內存管理機制,當其管理的內存不夠時,通過brk/mmap等系統調用向內核申請進程的虛擬地址區間,如果其維護的內存能滿足malloc調用,則直接返回,free時會將地址塊返回空閑鏈表。

malloc(size) 的時候,這個函數會多分配一塊空間,用于保存size變量,free的時候,直接通過指針前移一定大小,就可以獲取malloc時保存的size變量,從而free只需要一個指針作為參數就可以了calloc 庫函數相當于 malloc + memset(0)

malloc和free碎片化嚴重(內存站崗),在高并發下性能低下。除了libc自帶的動態內存管理庫malloc, 有時候還可以使用其他的內存管理庫替換,比如使用google實現的tcmalloc ,只需要編譯進程時鏈接上 tcmalloc的靜態庫并包含響應頭文件,就可以透明地使用tcmalloc 了,與libc 的malloc相比, tcmalloc 在內存管理上有很多改進,效率和安全性更好。

 

2、brk和mmap

在Linux下,glibc malloc提供了下面兩種動態內存管理的方法:堆內存分配和mmap的內存分配,此兩種分配方法都是通過相應的Linux 系統調用來進行動態內存管理的。具體使用哪一種方式分配,根據glibc的實現,主要取決于所需分配內存的大小。一般情況中,應用層面的內存從進程堆中分配,當進程堆大小不夠時,可以通過系統調用brk來改變堆的大小,但是在以下情況,一般由mmap系統調用來實現應用層面的內存分配:A、應用需要分配大于1M的內存,B、在沒有連續的內存空間能滿足應用所需大小的內存時。

(1)、調用brk實現進程里堆內存分配

在glibc中,當進程所需要的內存較小時,該內存會從進程的堆中分配,但是堆分配出來的內存空間,系統一般不會回收,只有當進程的堆大小到達最大限額時或者沒有足夠連續大小的空間來為進程繼續分配所需內存時,才會回收不用的堆內存。在這種方式下,glibc會為進程堆維護一些固定大小的內存池以減少內存碎片。

(2)、使用mmap的內存分配(堆和棧中間,稱為“文件映射區域”的地方)

glibc中,一般在比較大的內存分配時使用mmap系統調用,它以頁為單位來分配內存的(在Linux中,一般一頁大小定義為4K),這不可避免會帶來內存浪費,但是當進程調用free釋放所分配的內存時,glibc會立即調用unmmap,把所分配的內存空間釋放回系統。

注意: 這里我們討論的都是虛擬內存的分配(即應用層面上的內存分配),主要由glibc來實現,它與內核中實際物理內存的分配是不同的層面,進程所分配到的虛擬內存可能沒有對應的物理內存。如果所分配的虛擬內存沒有對應的物理內存時,操作系統會利用缺頁機制來為進程分配實際的物理內存。

默認情況下,malloc函數分配內存,如果請求內存大于128K(可由M_MMAP_THRESHOLD選項調節),那就不是去推_edata指針了,而是利用mmap系統調用,從堆和棧的中間分配一塊虛擬內存。

這樣子做主要是因為brk分配的內存需要等到高地址內存釋放以后才能釋放(例如,在B釋放之前,A是不可能釋放的,因為只有一個_edata 指針,這就是內存碎片產生的原因)(圖2緊縮),而mmap分配的內存可以單獨釋放。

malloc當最高地址空間的空閑內存超過128K(可由M_TRIM_THRESHOLD選項調節)時,執行內存緊縮操作(trim)

C++內存管理詳細解析

缺頁中斷:

  • 陷入內核態
  • 檢查要訪問的虛擬地址是否合法
  • 查找/分配一個物理頁[......](buddy+slab)
  • 填充物理頁內容(讀取磁盤,或者直接置0,或者什么都不做)
  • 建立映射關系(虛擬地址到物理地址的映射關系)
  • 重復執行發生缺頁中斷的那條指令

 

三、補充知識

 

1、內存泄漏

內存泄露是很隱蔽的錯誤,通常少量的內存泄露不會造成什么問題,大量的內存泄露可能會有“out of memory(OOM)”錯誤。

內存泄露的檢測通常借助于內存分析工具;( valgrind purify

一般如果是簡單的 new 之后,沒有 delete,這種泄漏最容易發現。真實場景可能比這復雜得多。有時候定位了相應的函數,但是代碼比較復雜,還是找不到泄漏點,可以參考如下幾個地方:

map:c++的map,在下標訪問的時候自動構造 value 對象,可能造成 map 無限增長;

unordered_set: 在插入大量的元素之后,再刪除,內存占用保持不變,需要手動 rehash

容器的 size 很大:通過 gcore -o xxx pidof yyy ,然后 gdb 去查看有嫌疑的容器的長度;

如果容器的 size 正常,但是還是有泄漏,可能跟智能指針有關,例如 shared ptr,被泄漏;

 

2、malloc/free和new/delete的比較

C++內存管理詳細解析

 

3、RAII規則

RAII是指C++語言中的一個慣用法(idiom),它是“Resource Acquisition Is Initialization”的首字母縮寫。中文可將其翻譯為“資源獲取就是初始化”。

需要動態獲取和釋放的都可以稱為“資源”;

獲取資源和釋放資源要對應,這里就會面臨麻煩:釋放的不徹底將會導致memory leak,致使程序臃腫、出錯等。

看到這里自然而然的可以想到C++中的一對特殊函數,構造函數和析構函數。在構造函數中申請資源,以及在析構函數中釋放資源。

類是C++中的主要抽象工具,那么就將資源抽象為類,用局部對象來表示資源,把管理資源的任務轉化為管理局部對象的任務。這就是RAII慣用法,RAII有效地實現了C++資源管理的自動化。

到此這篇關于C++內存管理詳細解析的文章就介紹到這了,更多相關C++內存管理內容請搜索服務器之家以前的文章或繼續瀏覽下面的相關文章希望大家以后多多支持服務器之家!

原文鏈接:https://www.cnblogs.com/MiaoMiaoGarden/p/15579610.html

延伸 · 閱讀

精彩推薦
  • C/C++C/C++經典實例之模擬計算器示例代碼

    C/C++經典實例之模擬計算器示例代碼

    最近在看到的一個需求,本以為比較簡單,但花了不少時間,所以下面這篇文章主要給大家介紹了關于C/C++經典實例之模擬計算器的相關資料,文中通過示...

    jia150610152021-06-07
  • C/C++C語言中炫酷的文件操作實例詳解

    C語言中炫酷的文件操作實例詳解

    內存中的數據都是暫時的,當程序結束時,它們都將丟失,為了永久性的保存大量的數據,C語言提供了對文件的操作,這篇文章主要給大家介紹了關于C語言中文件...

    針眼_6702022-01-24
  • C/C++學習C++編程的必備軟件

    學習C++編程的必備軟件

    本文給大家分享的是作者在學習使用C++進行編程的時候所用到的一些常用的軟件,這里推薦給大家...

    謝恩銘10102021-05-08
  • C/C++C++之重載 重定義與重寫用法詳解

    C++之重載 重定義與重寫用法詳解

    這篇文章主要介紹了C++之重載 重定義與重寫用法詳解,本篇文章通過簡要的案例,講解了該項技術的了解與使用,以下就是詳細內容,需要的朋友可以參考下...

    青山的青6062022-01-04
  • C/C++c++ 單線程實現同時監聽多個端口

    c++ 單線程實現同時監聽多個端口

    這篇文章主要介紹了c++ 單線程實現同時監聽多個端口的方法,幫助大家更好的理解和學習使用c++,感興趣的朋友可以了解下...

    源之緣11542021-10-27
  • C/C++C語言實現電腦關機程序

    C語言實現電腦關機程序

    這篇文章主要為大家詳細介紹了C語言實現電腦關機程序,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下...

    xiaocaidayong8482021-08-20
  • C/C++深入理解goto語句的替代實現方式分析

    深入理解goto語句的替代實現方式分析

    本篇文章是對goto語句的替代實現方式進行了詳細的分析介紹,需要的朋友參考下...

    C語言教程網7342020-12-03
  • C/C++詳解c語言中的 strcpy和strncpy字符串函數使用

    詳解c語言中的 strcpy和strncpy字符串函數使用

    strcpy 和strcnpy函數是字符串復制函數。接下來通過本文給大家介紹c語言中的strcpy和strncpy字符串函數使用,感興趣的朋友跟隨小編要求看看吧...

    spring-go5642021-07-02
主站蜘蛛池模板: 午夜毛片在线观看 | 日本xxx片免费高清在线 | 日韩理论片| 亚洲区精品 | 9966久久精品免费看国产 | 天天色一色 | 国产情侣啪啪 | 国产自拍影院 | 99香蕉网 | 滑进了柔佳火热紧夹的 | 性xxxx中国| 亚洲精品123区在线观看 | 国产免费好大好硬视频 | 久久99re8热在线播放 | freexxx性欧美3d动漫 | 国产成人久久精品推最新 | 操到翻白眼 | china外卖员gay国产xnxx | 欧美精品一区视频 | 五月婷婷丁香色 | 日本中年japanesebear | 欧美久久热 | 男人天堂网www | 亚洲一区二区精品推荐 | 久久日韩精品无码一区 | 亚洲情射 | 国产成人手机在线好好热 | 亚洲欧美日韩国产精品一区 | 精品一区二区三区视频日产 | 国产做a爰片久久毛片 | 性插图动态图无遮挡 | 精品久久久噜噜噜久久久app | 久久婷婷五月综合色精品首页 | 日韩欧美推理片免费在线播放 | 91制片厂制作传媒网站 | 欧美日韩一区二区三区免费不卡 | 清清草在线视频 | 人与动人物人a级特片 | 四虎成人免费大片在线 | 亚州一区二区 | 亚洲视频在线观看免费视频 |