千萬不要錯過的後端[純乾貨]面試知識點整理 I I

小魔童哪吒發表於2021-06-10

c++記憶體管理

上次分享整理的面試知識點 I , 今天我們來繼續分享面試知識點整理 II

linux kernel 核心空間、記憶體管理、程式管理裝置、驅動虛擬檔案系統(vfs) 核心空間是受保護的,使用者不能對核心空間讀寫,否則會出現段錯誤
環境變數(env) PATH
命令列引數 char *agrv[]
棧區⬇️ 函式的返回地址,返回值,引數,區域性變數
共享庫(對映區)⬇️ 呼叫動態庫,或者mmap函式進行檔案對映
堆區⬆️ 用new/malloc申請的記憶體,同時需要適用delete/free來釋放採用鏈式儲存結構
.bss區 未初始化的全域性變數和靜態變數以及 初始化為 0 的 全域性變數和靜態變數編譯時就已經分配了空間
.data區 已初始化的全域性變數和靜態變數編譯時就已經分配了空間
.text 1、只讀儲存區 – 常量,const全域性變數2、文字區 – 程式程式碼,機器程式碼
0-4k保護區
#include<stdio.h>

int a;    //未初始化全域性區 .bss
int b=1;    //已初始化全域性區  .data
static int c=2;    //已初始化全域性區  .data
const int d=3;    //只讀資料段,也叫文字常量區 ro.data, d的值不能被修改
int main(void)
{
    int e=4;    //棧區
    static int f=5;    //已初始化全域性區
    const int g=6;    //棧區,不能通過變數名修改其值,但可通過其地址修改其值
    int *p=malloc(sizeof(int))    //指標變數p在棧區,但其所指向的4位元組空間在堆區
    char *str="abcd";    //字串“abcd”存在文字常量區,指標變數str在棧區,存的是“abcd”的起始地址
    return 0;
}

記憶體洩露及分類

img

記憶體洩漏,是由於疏忽或錯誤造成程式未能釋放掉不再使用的記憶體。記憶體洩漏,並不是指記憶體記憶體再實體地址上的消失,而是應用程式分配某段記憶體後,失去了對該段記憶體的控制,因而造成記憶體的浪費。

  • 一般情況是new/malloc 後,沒有及時delete/free釋放記憶體,判斷為記憶體洩露
  • linux中可以使用valgrind來檢測記憶體洩漏

記憶體洩漏的分類:

  • 堆記憶體洩漏 — new/malloc 後 沒有delete/free掉
  • 系統資源洩漏 — 系統分配的資源,沒有用指定的函式釋放掉,導致系統資源的浪費,嚴重影響系統效能,如:socket,bitmap,handle
  • 沒有將父類的解構函式定義為虛擬函式 — 父類指標指向子類物件的時候,釋放記憶體的時候,若父類的解構函式不是virtual的話,子類的記憶體是不會得到釋放的,因此會記憶體洩漏

c++中是如何處理記憶體洩漏的:

使用valgrind,mtrace來檢測記憶體洩漏

避免記憶體洩漏:

1.事前預防型。如智慧指標等。 2.事後查錯型。如洩漏檢測工具。

智慧指標

使用智慧指標,智慧指標會自動刪除被分配的記憶體,他和普通指標類似,只是不需要手動釋放指標,智慧指標自己管理記憶體釋放,不用擔心記憶體洩漏問題

智慧指標有:

  • auto_ptr
  • unique_ptr
  • shared_ptr
  • weak_ptr

其中auto_ptr c++11已經被棄用了

unique_ptr

獨佔的智慧指標,只能有一個物件擁有所有權,獨佔指標的是自己管理記憶體的,指標存在於棧空間,開闢的記憶體在堆空間,這裡的堆空間是和智慧指標繫結的,智慧指標隨著函式結束被銷燬之前,智慧指標會先去把堆裡面的記憶體銷燬

其中涉及

  • move函式 – 可以使用move函式來轉移所有權,轉移所有權後,原來的指標就無權訪問

  • reset函式 – 可以用reset函式來重置所有權,會把之前的物件所有權釋放掉,重新建立一個所有權物件

  • make_unique – 快速的建立一個unique_ptr智慧指標的物件 如 auto myptr = make_unique<person>();

如果希望只有一個智慧指標管理資源 就使用 unique_ptr

#include <iostream>
#include <string>
#include <memory>

using namespace std;

struct person
{
    ~person()
    {
        cout<<"~person"<<endl;
    }
    string str;
};

unique_ptr<person> test()
{
    return unique_ptr<person> (new person);
}

int main()
{
    //unique_ptr is ownership 
    unique_ptr<person> p = test();
    p->str = "hello world";

    unique_ptr<person> p2 = move(p);  //可以使用move函式來轉移所有權,轉移所有權後,原來的指標就無權訪問
    if(!p)
    {
        cout<<"p == null" <<endl;
    }

    if(p2)
    {
        cout<<"p2 have ownership"<<endl;
        cout<<p2->str<<endl;
    }

    p2.reset(new person);//可以用reset函式來重置所有權,會把之前的物件所有權釋放掉,重新建立一個所有權物件
    if(p2->str.empty())
    {
        cout<<"str is null"<<endl;
    }

     return 0;
 }

shared_ptr

共享的智慧指標,shared_ptr使用引用計數(use_count方法),每個shared_ptr的拷貝都指向同一塊記憶體,在最後一個shared_ptr被析構的時候,記憶體才會被釋放

  • shared_ptr 是引用計數的方式,使用use_count檢視計數

  • make_shared 快捷建立 shared_ptr

使用函式返回自己的shared_ptr時,需要繼承enable_shared_from_this類,使用shared_from_this函式進行返回

注意事項:

  • 不要將this指標作為返回值

  • 要避免迴圈引用

  • 不要再函式實參種建立shared_ptr,在呼叫函式之前先定義以及初始化它

  • 不要用一個原始指標初始化多個shared_ptr

希望多個指標管理同一個資源就使用shared_ptr

#include <iostream>
#include <string>
#include <memory>

using namespace std;

struct person
    :enable_shared_from_this<person>{
    string str;
    void show()
    {
        cout<<str<<endl;
    }
    ~person()
    {
        cout<<"~person"<<endl;
    }
    shared_ptr<person> getshared()
    {
        return shared_from_this();
    }
};

int main()
{
    #if 0
    shared_ptr<person> ptr(new person);
    cout<< ptr.use_count()<<endl;

    shared_ptr<person> ptr2 = ptr;
    cout<< ptr.use_count()<<endl;

    shared_ptr<person> a = make_shared<person>();
    cout<< a.use_count()<<endl;
    a = ptr2;
    cout<< ptr.use_count()<<endl;


    shared_ptr<person> mm = a->getshared();

    #endif

    shared_ptr<person> ptr;
    {
        shared_ptr<person> ptr2(new person);
        ptr2->str = "hello";

        ptr = ptr2->getshared();    
        cout<< ptr.use_count()<<endl;
    }
    ptr->show();

    return 0;
}

weak_ptr

弱引用的智慧指標

是用來監視shared_ptr的,不會使用計數器加1,也不會使用計數器減1,主要是為了監視shared_ptr的生命週期,更像是shared_ptr的一個助手。weak_ptr還可以用來返回this指標和解決迴圈引用的問題。

shared_ptr會有迴圈引用的問題 ,解決方式為 把類中的shared_ptr 換成 weak_ptr即可

struct ListNode
{
    std::shared_ptr<ListNode> _next;//std::weak_ptr<ListNode> _next; 就可以解決
    std::shared_ptr<ListNode>  _prev;//std::weak_ptr<ListNode> _pre; 就可以解決

    ~ListNode()
    {
        cout << "~ListNode()" << endl;
    }
};
void test_shared_ptr_cycleRef()
{
    std::shared_ptr<ListNode> cur(new ListNode);
    std::shared_ptr<ListNode> next(new ListNode);

    cur->_next = next;
    next->_prev = cur;

}
int main()
{
    test_shared_ptr_cycleRef();
    system("pause");
    return 0;
}

例如上述程式碼案例

void shared_ptr_cycleRef(){
    std::shared_ptr<LISTNODE> cur LISTNODE;
    std::shared_ptr<LISTNODE> next LISTNODE;

     cur->_next = next;
     next->_pre = cur;
}

Cur 和 next 存在迴圈引用,他們的引用計數都變為 2

出了作用域之後,cur 和 next 被銷燬,引用計數減 1

因此要釋放cur , 就需要釋放next 的 _pre,要釋放next , 就需要釋放cur 的 _next

記憶體洩漏檢測工具

valgrind記憶體檢測工具

valgrind的官方網址是:http://valgrind.org

valgrind被設計成非侵入式的,它直接工作於可執行檔案上,因此在檢查前不需要重新編譯、連線和修改你的程式。要檢查一個程式很簡單

命令如下: valgrind --tool=tool_name program_name

  • 做記憶體檢查: valgrind --tool=memcheck ls -l
  • 檢查記憶體洩漏: valgrind --tool=memcheck --leak-check=yes ls -l

valgrind有如下幾個工具

memcheck

memcheck 探測程式中記憶體管理存在的問題。

它檢查所有對記憶體的讀/寫操作,並擷取所有的malloc/new/free/delete呼叫。因此memcheck工具能夠探測到以下問題:

Memcheck 工具主要檢查下面的程式錯誤:

  • 使用未初始化的記憶體 (Use of uninitialised memory)

  • 使用已經釋放了的記憶體 (Reading/writing memory after it has been free’d)

  • 使用超過 malloc分配的記憶體空間(Reading/writing off the end of malloc’d blocks)

  • 對堆疊的非法訪問 (Reading/writing inappropriate areas on the stack)

  • 申請的空間已經釋放釋放,即記憶體洩漏 (Memory leaks – where pointers to malloc’d blocks are lost forever)

  • malloc/free/new/delete申請和釋放記憶體的匹配(Mismatched use of malloc/new/new [] vs free/delete/delete [])

  • src和dst的重疊(Overlapping src and dst pointers in memcpy() and related functions)

cachegrind

cachegrind 是一個cache剖析器。

它模擬執行CPU中的L1, D1和L2 cache,

因此它能很精確的指出程式碼中的cache未命中。

它可以列印出cache未命中的次數,記憶體引用和發生cache未命中的每一行 程式碼,每一個函式,每一個模組和整個程式的摘要。

若要求更細緻的資訊,它可以列印出每一行機器碼的未命中次數。

在x86和amd64上, cachegrind通過CPUID自動探測機器的cache配置,所以在多數情況下它不再需要更多的配置資訊了。

helgrind

helgrind查詢多執行緒程式中的競爭資料。

helgrind查詢記憶體地址,那些被多於一條執行緒訪問的記憶體地址,但是沒有使用一致的鎖就會被查出。這表示這些地址在多執行緒間訪問的時候沒有進行同步,很可能會引起很難查詢的時序問題。

產生段錯誤的原因

  • 使用野指標
  • 試圖對字串常量進行修改

new和malloc的區別:

在申請記憶體時

  • new是一個操作符,可以被過載,malloc是一個庫函式

  • new在申請記憶體的時候,會按照物件的資料結構分配記憶體,malloc分配指定的記憶體大小

  • new申請記憶體時,會呼叫建構函式,malloc不會

  • new申請記憶體時,返回物件的指標,malloc申請記憶體的時候,返回(void *) 因此需要強轉

  • 申請陣列的時候,new[],會一次性分配所有記憶體,呼叫多個建構函式,因此需要delete[]來銷燬記憶體,呼叫多次解構函式,而 malloc 只能sizeof(int)*n

  • new申請記憶體失敗,會拋bac_malloc異常, malloc申請失敗則返回NULL

  • malloc當分配的記憶體不夠的時候,會使用realloc再次分配記憶體, new沒有這樣的機制。

  • new分配的記憶體需要用delete釋放,delete 會呼叫解構函式,malloc分配的記憶體需要free 函式釋放

realloc的原理:

realloc是在C語言中出現的,c++已經摒棄realloc函式,realloc函式分配一塊新記憶體的時候,會把原記憶體中的記憶體copy到新記憶體中,通過memmove的方式

共享記憶體相關的api

  • shmget 新建共享記憶體
  • shmat 連線共享記憶體到當前地址空間
  • shmdt 分離共享記憶體
  • shmctl 控制共享記憶體

c++ STL記憶體優化

c++11新特性:

關鍵字和語法

  • auto關鍵字

編譯器可以根據初始化來推導資料型別,不能用於函式傳參和以及陣列型別推導

  • nullptr關鍵字

一種特殊型別的字面量,可以被轉成任意的其他型別

  • 初始化列表

初始化類的列表

  • 右值引用

可以實現移動語義和完美轉發,消除兩個物件互動時不必要的拷貝,節省儲存資源,提高效率

新增容器

  • 新增STL array ,tuple、unordered_map,unordered_set

智慧指標,記憶體管理

  • 智慧指標

新增 shared_ptr、weak_ptr用於記憶體管理

多執行緒

  • atomic原子操作

用於多執行緒互斥

其他

  • lamda表示式

可以通過捕獲列表訪問上下文的資料

  • std::function std::bin d封裝可執行物件

防止標頭檔案重複引用:

#ifndef

作用:相同的兩個檔案不會被重複包含。

優點:

  • 受C/C++語言標準的支援,不受編譯器的限制。
  • 不僅僅侷限於避免同一個檔案被重複包含,也能避免內容完全相同的兩個檔案(或程式碼片段)被重複包含。

缺點:

  • 如果不同標頭檔案中的巨集名恰好相同,可能就會導致你看到標頭檔案明明存在,編譯器卻說找不到宣告的情況。
  • 由於編譯器每次都需要開啟標頭檔案才能判定是否有重複定義,因此在編譯大型專案時,#ifndef會使得編譯時間相對較長。

#pragma once

作用:物理上的同一個檔案不會被重複包含。

優點:

  • 避免#ifndef中因為巨集名相同導致的問題。
  • 由於編譯器不需要開啟標頭檔案就能判定是否有重複定義,因此在編譯大型專案時,比#ifndef更快。

缺點:

  • #pragma once只針對同一檔案有效,對相同的兩個檔案(或程式碼片段)使用無效
  • #pragma once不受一些較老版本的編譯器支援,一些支援了的編譯器又打算去掉它,所以它的相容性可能不夠好。

繼承與組合

  • 繼承是物件導向三大基本特徵之一(繼承,封裝,多型),繼承就是子類繼承父類的特徵和行為,使得子類物件(例項)具有父類的例項域和方法,或子類從父類繼承方法,使得子類具有父類相同的行為,繼承強調的是is-a關係,是‘白盒式’的程式碼複用
  • 組合是通過對現有物件進行拼裝即組合產生新的具有更復雜的功能,組合體現的是整體和部分,強調的是has-a的關係,是‘黑盒式’的程式碼複用

繼承與組合使用場景

  • 邏輯上B 是A 的“一種”a kind of

繼承 (如 男人 繼承 人類)

  • 邏輯上A 是B 的“一部分”a part of

組合(如 組合 眼 耳 口 鼻 -> 頭)

繼承與組合區別

  • 在繼承中,父類的內部細節對子類可見,其程式碼屬於白盒式的複用,調的是is-a的關係,關係在編譯期就確定
  • 組合中,物件之間的內部細節不可見,其程式碼屬於黑盒式複用。強調的是has-a的關係,關係一般在執行時確定

繼承與組合優缺點

繼承

優點:

  • 支援擴充套件,通過繼承父類實現,但會使系統結構較複雜
  • 易於修改被複用的程式碼

缺點:

  • 程式碼白盒複用,父類的實現細節暴露給子類,破壞了封裝性
  • 當父類的實現程式碼修改時,可能使得子類也不得不修改,增加維護難度。
  • 子類缺乏獨立性,依賴於父類,耦合度較高
  • 不支援動態擴充,在編譯期就決定了父類

組合

優點:

  • 程式碼黑盒複用,被包括的物件內部實現細節對外不可見,封裝性好。
  • 整體類與區域性類之間鬆耦合,相互獨立。
  • 支援擴充套件
  • 每個類只專注於一項任務
  • 支援動態擴充套件,可在執行時根據具體物件選擇不同型別的組合物件(擴充套件性比繼承好)

缺點:

  • 建立整體類物件時,需要建立所有區域性類物件。導致系統物件很多。

函式指標的好處和作用:

好處:簡化結構和程式通用性的問題,也是實現物件導向程式設計的一種途徑

作用:

  • 實現物件導向程式設計中的多型性

  • 回撥函式

inline函式與巨集定義

inline函式是C++引入的機制,目的是解決使用巨集定義的一些缺點。

為什麼要引入行內函數(行內函數的作用)

用它替代巨集定義,消除巨集定義的缺點。

巨集定義使用前處理器實現,做一些簡單的字元替換因此不能進行引數有效性的檢測。

  • inline 相比巨集定義有哪些優越處

  • inline 函式程式碼是被放到符號表中,使用時像巨集一樣展開,沒有呼叫的開銷效率很高;

  • inline 函式是真正的函式,所以要進行一系列的資料型別檢查;

  • inline 函式作為類的成員函式,可以使用類的保護成員及私有成員;

inline函式使用的場合

  • 使用巨集定義的地方都可以使用 inline 函式;
  • 作為類成員介面函式來讀寫類的私有成員或者保護成員;

為什麼不能把所有的函式寫成 inline 函式

  • 函式體內的程式碼比較長,將導致記憶體消耗代價;
  • 函式體內有迴圈,函式執行時間要比函式呼叫開銷大;
  • 另外類的構造與解構函式不要寫成行內函數。

行內函數與巨集定義區別

  • 行內函數在編譯時展開,巨集在預編譯時展開;
  • 行內函數直接嵌入到目的碼中,巨集是簡單的做文字替換;
  • 行內函數有型別檢測、語法判斷等功能,而巨集沒有;
  • inline 函式是函式,巨集不是;
  • 巨集定義時要注意書寫(引數要括起來)否則容易出現歧義,行內函數不會產生歧義;

總結

  • 分享了記憶體管理,記憶體洩露,智慧指標
  • 記憶體洩露檢測工具
  • 程式碼中產生段錯誤的原因
  • 記憶體優化
  • 其餘小知識點

歡迎點贊,關注,收藏

朋友們,你的支援和鼓勵,是我堅持分享,提高質量的動力

好了,本次就到這裡,*下一次 GO的併發程式設計分享 *

技術是開放的,我們的心態,更應是開放的。擁抱變化,向陽而生,努力向前行。

我是小魔童哪吒,歡迎點贊關注收藏,下次見~

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章