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;
}
記憶體洩露及分類
記憶體洩漏,是由於疏忽或錯誤造成程式未能釋放掉不再使用的記憶體。記憶體洩漏,並不是指記憶體記憶體再實體地址上的消失,而是應用程式分配某段記憶體後,失去了對該段記憶體的控制,因而造成記憶體的浪費。
- 一般情況是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 協議》,轉載必須註明作者和本文連結