一、數據存儲
1.程序數據段
? 靜態(全局)數據區:全局變量、靜態變量
? 堆內存:程序員手動分配、手動釋放
? 棧內存:編譯器自動分配、自動釋放
? 常量區:編譯時大小、值確定不可修改
2.程序代碼段
? 函數體
二、Stack 棧內存
? 棧內存屬于執行期函數,編譯時大小確定
? 函數執行時,棧空間自動分配
? 函數結束時,棧空間自動銷毀
? 棧上對象線性分配,連續排列,沒有內存碎片效應
? 棧內存具有函數本地性,不存在多線程競爭
? 棧內存有大小限制,可能會溢出,例如Linux默認為8MB,Windows默認為1MB
? 棧內存使用對象或引用直接使用,管理復雜度低
三、Heap 堆內存
? 堆內存屬于具有全局共享特點,大小動態變化
? 對象分配時,手動分配堆內存(malloc/new)
? 對象釋放時,手動釋放堆內存(delete/free)
? 堆上對象鏈式分配,非連續排列
? 堆內存全局共享,存在多線程競爭可能性
? 堆內存大小沒有棧內存嚴格限制,與機器內存總量和進程尋址空間相關
? 堆內存使用指針間接訪問,管理復雜度高
? 堆內存有很高靈活性,雖性能較差,但可通過相關設施和編程技巧精細控制,從而獲得改善。
四、堆-棧內存
? 棧內存分配快,布局連續,緩存友好,釋放快
? 如果生存周期短,拷貝較少(傳參、返回值),棧內存性能更好
? 堆內存有很高靈活性,但性能較差
? 堆內存在長運行程序有內存碎片效應,小塊空閑內存得不到重用
? 堆分配需要尋找合適大小內存塊,會花費更多時間
? 堆空間碎片化,容易降低緩存效率
? 編譯器較難優化使用指針的代碼
? 使用者需要確保申請釋放成對,避免內存泄漏導致堆內存耗盡
? 使用者需要確保內存釋放后不能訪問(懸浮指針)
? 可以通過RAII 和指針移動操作避免拷貝代價。
五、值語義與引用語義
? 對內置類型和用戶自定義類型提供同等支持。不存在特權類型或限定
5.1 值語義詳解
對象以值的方式直接存儲,傳參、返回值、拷貝等。
1.行為特點
拷貝獨立性:每次賦值或傳參時,會生成一個完全獨立的新對象(調用拷貝構造函數或移動構造函數)。
std::vector<int> v1 = {1, 2, 3};
std::vector<int> v2 = v1; // 值語義:v2是v1的深拷貝副本
v2.push_back(4); // 修改v2不影響v1
2.底層原理
深拷貝:值語義的對象(如std::string、std::vector)會遞歸復制所有成員數據到新內存。
自動生命周期:對象超出作用域時自動調用析構函數釋放內存(RAII機制)。
3.優點
安全性:數據隔離,避免意外的副作用。
確定性:生命周期清晰,無需擔心懸空引用,沒有內存泄漏,沒有數據競爭
4.缺點
性能開銷:拷貝大對象(如容器)成本高(可通過移動語義優化)。
不能支持虛函數多態
5.典型場景
需要獨立修改的局部變量。
函數參數傳遞中不希望影響原數據時(如void func(std::string s))。
5.2 引用語義詳解
對象以指針或引用的方式間接存儲,參數、返回值、拷貝傳遞的是指針或引用。
1.行為特點
別名機制:引用或指針是原數據的“標簽”,操作引用即操作原數據。
int a = 10;
int& ref = a; // 引用語義:ref是a的別名
ref = 20; // 直接修改a的值
2.底層原理
內存共享:引用本質是指針的語法糖(編譯器自動解引用),指針直接存儲內存地址。
手動管理:裸指針需注意生命周期(易懸空),引用需確保原對象存活。
3.優點
零拷貝:傳遞大對象時無性能開銷。
共享修改:允許多個部分代碼協同操作同一數據。
4.缺點
安全性風險:懸空引用、數據競爭(需配合const或智能指針)。
復雜性:需顯式管理生命周期(如使用std::shared_ptr)。
5.典型場景
避免拷貝大對象(如void process(const std::vector& data))。
需要跨作用域共享數據(如類成員、回調函數)。
六、變量的生命周期
? 全局變量,函數中的靜態變量,類靜態數據成員:程序啟動后加載,程序結束釋放。
? 局部變量、自動對象(棧):自聲明開始,到聲明語句所在塊結尾 }釋放
? 堆變量,自由存儲對象:new開始,delete結束
? 臨時對象: 和表達式周期一致,通常類似自動對象(綁定引用除外)
? 線程局部對象, thread_local:隨線程創建而創建,隨線程結束而釋放。
在C++中,臨時對象(Temporary Objects)是編譯器在表達式求值過程中隱式創建的、生命周期短暫的匿名對象。它們通常出現在類型轉換、函數返回值或運算符重載等場景中。
臨時對象的常見產生場景
1.函數返回非引用類型的對象
std::string getString() { return "Hello"; // 返回時構造臨時對象
}
// 臨時對象用于初始化s
std::string s = getString();
2.類型轉換(隱式或顯式)
cpp
int a = 10;
double b = a + 3.14; // int轉換為double時生成臨時對象
3.運算符重載
class Complex {
public:Complex operator+(const Complex& rhs) {return Complex(this->real + rhs.real, this->imag + rhs.imag); // 返回臨時對象}
};
Complex c1, c2;
Complex c3 = c1 + c2; // operator+返回的臨時對象用于初始化c3
4.函數參數傳遞時的類型不匹配
void printString(const std::string& s) { /*...*/ }
printString("Hello"); // 從const char*隱式構造臨時std::string
5.構造函數的隱式調用
class Widget { /*...*/ };
void processWidget(Widget w) { /*...*/ }
processWidget(Widget()); // 傳遞臨時Widget對象
臨時對象的生命周期規則
1.一般規則
臨時對象在完整表達式結束時銷毀(通常是分號處)。
std::string s = getString() + " World";
// 臨時對象(getString()的返回值)在分號后銷毀
2.綁定到引用時的擴展
若臨時對象被綁定到const引用或右值引用,其生命周期延長至引用的作用域結束。
const std::string& ref = getString(); // 臨時對象生命周期延長至ref作用域結束
std::string&& rref = getString(); // 同樣延長(C++11起)
3.成員訪問時的陷阱
臨時對象的成員通過指針/引用訪問時,需確保臨時對象存活:
const std::string* p = &(getString().c_str()); // 錯誤:臨時對象已銷毀
std::string&& s = getString(); // 正確:生命周期延長
七、變量的初始化
? 統一初始化:int a1{100}; int a2={100};
? 賦值初始化:int a3=100;
? 構造初始化:int a4(100);
? 大多數情況推薦使用統一初始化,又叫列表初始化,特別是對象、容器;對于數值,可防止隱式窄化轉型。空列表{}使用默認值初始化。
? 基本數值類型,以及auto自動推斷類型聲明,可以繼續使用賦值初始化(除非需要避免數值窄化轉型)。
八、指針是萬惡之源:內存錯誤的罪與罰
? 所有權不清晰(誰分配,誰釋放?)
? 對象類型不清晰(棧對象、堆對象、數組對象、資源句柄?)
? 錯誤百出的指針
1.內存泄漏——忘記delete之前new的內存
2.懸浮指針—— 使用已釋放內存(讀取、或寫入)、返回棧對象地址
3.重復刪除—— 對已經刪除過的對象,進行二次刪除
4.刪除非堆對象指針——對棧對象、全局/靜態對象地址進行刪除
5.分配與刪除錯誤匹配—— new和free搭配,malloc和delete搭配,new[]和delete搭配,new和delete[] 搭配
6.使用空指針
7.使用失效引用
九、基于對象編程
數據成員(字段)+函數成員
對象有什么?
????實例成員與this指針
????靜態成員
?對象在哪里?——空間分析
?基本類型成員
?內嵌對象成員
?內嵌指針成員
?操作符重載
深入理解面向對象
向下:深入理解三大面向對象機制
? 封裝,隱藏內部實現
? 繼承,復用現有代碼
? 多態,改寫對象行為
向上:深刻把握面向對象機制所帶來的抽象意義,理解如何使用這些機制來表達現實世界,掌握什么是“好的面向對象設計”
十、C++ 對象模型基礎
? C++對象內存布局
???? ????按照實例數據成員聲明順序從上到下排列(與C語言保持兼容)
???? ????虛函數指針占用一個指針size
???? ???? 靜態數據成員不參與
? 內存對齊與填充——
???? ???? 對象內存對齊是為了優化CPU存儲數據效率、避免數據截斷
???? ????按對齊系數(4字節、8字節)整倍數
???? ????可使用#pragma pack(4)控制
???? ????簡單優化:長字段放前,短字段置后(聚集)
? 對象有多大?sizeof
可參考我的另一篇文章:單一繼承類成員布局
十一、特殊成員函數與三法則(Rule of Three)
? 四大特殊成員函數
???? ????? 默認構造函數(無參) , 如果不定義任何拷貝構造,編譯器自動生成
???? ????? 析構函數/ 拷貝構造函數 / 賦值操作符,如果不定義,編譯器自動生成
???? ????? 使用 default 讓編譯器自動生成。
???? ????? 使用 delete 讓編譯器不要自動生成。
? 三法則:析構函數、拷貝構造函數、賦值操作符 三者自定義其一,則需要同時定義另外兩個(編譯器自動生成的一般語義錯誤)。
? 編譯器自動生成的拷貝/賦值是按字節拷貝,如不正確,則需要自定義拷貝/賦值/析構行為:
???? ????? 賦值操作符中的 Copy & Swap 慣用法
???? ????? 注意賦值操作中避免“自我賦值”。
? 需要自定義三大函數的類,通常包含指針指向的動態數據成員。
備注:c++11之后增加移動構造和移動賦值函數
十二、清楚對象的構造/析構點
? 構造器什么時候被調用?
???? ????? 當對象(包括嵌套成員)被定義時(堆棧、堆或靜態)。
???? ????? 當對象的數組被定義時(堆棧、堆或靜態)。
???? ????? 當函數參數以值傳遞時
???? ????? 當函數返回一個對象時
???? ????? 甚至適用于編譯器生成的臨時對象。
? 析構器什么時候被調用?
???? ????? 當命名的堆棧對象、數組或參數超出范圍時(包括嵌套對象成員)
???? ????? 當堆對象或數組被刪除時
???? ????? 對于靜態對象,在程序結束時
???? ????? 對于臨時對象,在創建它們的 "完整表達式 "結束時調用
C++內嵌對象的構造和析構時機
在C++中,內嵌對象(作為類的成員變量)的構造和析構時機遵循特定的規則,理解這些規則對于正確管理對象生命周期非常重要。
構造順序
構造時機:內嵌對象在包含它的類對象構造時被構造
構造順序:
按照成員變量在類定義中聲明的順序依次構造(不是初始化列表中的順序)
先構造所有內嵌對象,然后才執行包含類的構造函數體
class A {
public:A() { cout << "A constructed" << endl; }~A() { cout << "A destroyed" << endl; }
};class B {
public:B() { cout << "B constructed" << endl; }~B() { cout << "B destroyed" << endl; }
};class Container {A a;B b;
public:Container() : b(), a() { // 初始化列表順序不影響實際構造順序cout << "Container constructed" << endl;}~Container() {cout << "Container destroyed" << endl;}
};// 使用:
Container c;
/* 輸出順序:
A constructed
B constructed
Container constructed
*/
析構順序
析構時機:內嵌對象在包含它的類對象析構時被析構
析構順序:
先執行包含類的析構函數體
然后按照成員變量聲明順序的逆序析構內嵌對象
// 接上面的例子,當Container對象離開作用域時:
/* 輸出順序:
Container destroyed
B destroyed
A destroyed
*/
十三、常用類型——數組最佳實踐
? 盡量避免使用C風格數組,有很多安全隱患
???? ????? 本質是指針指向的一塊連續內存,引用語義
???? ????? 不帶長度信息,易錯點:拷貝、傳參、返回值
? 不要使用指針傳遞數組,傳遞指針僅代表單個對象
? 不要使用C風格數組承載多態對象(基類、子類)
? 使用抽象管理內存
???? ????? 使用vector<T>
實現變長數組,替代堆上的C風格數組
???? ????? 使用array<T>
實現定長數組,替代棧上的C風格數組
在C++中,使用C風格數組(如Base array[10])來存儲多態對象會導致嚴重的對象切片問題,應該避免這種做法
請看我的另一篇文章不要以多態的方式處理數組
十四、常用類型——字符串最佳實踐
? 盡量避免使用C風格字符串(“”默認字面常量)
???? ????? 零結尾的字符數組,與字符數組有區別
???? ????? 擁有一切C風格數組的安全隱患
? 使用string替代C風格字符串(“”字面常量以s結尾)
???? ????? 能夠正確分配資源,處理所有權、拷貝、擴容等操作
???? ????? 但謹慎處理C風格字符串API和string的交互
? string內部實現了短字符串優化技術,擁有極高性能
???? ????? 短字符串(<14字符)默認存在棧中,長字符串將分配于堆上。
? string_view 字符串的只讀視圖,表示為(指針,長度)
???? ????? 不擁有,,不拷貝字符串,是字符串只讀操作的性能之選
十五、其他類型——枚舉類
? enum class 是一種限制了作用域的強類型枚舉
???? ????? 強類型,不能和整數類型進行隱式轉換。
???? ????? 可以顯式指定枚舉類的基礎類型(存儲類型),默認int
???? ????? 可以使用整數常量初始化枚舉值
???? ????? 和整數之間轉型要使用顯式轉型(static_cast)
? 普通enum類型繼承自C語言
???? ????? 弱類型,和基礎類型存在隱式轉換
???? ????? 作用域位于enum本身所在作用域(名字空間污染)
? 建議使用enum class替換不安全的enum
十六、戒除C語言的“不良習慣”
? 辨析使用場景和對象所有權,謹慎使用裸指針
? 使用C++類型轉換替換C風格的強制轉換
? 使用模板和編譯時計算等替換C風格的宏機制
? 盡量避免全局數據,謹慎使用全局函數
? 嚴格避免 malloc() 和 free()
場景需求 | 推薦轉換方式 |
---|---|
基本類型安全轉換 | static_cast |
多態類型向下轉型 | dynamic_cast |
移除 const/volatile | const_cast |
低級二進制重解釋 | reinterpret_cast |
十七、性能指南——類型與成員(1)
? 如果函數非常小,并時間敏感,將其聲明為 inline
? 如果函數可能在編譯期進行求值,就將其聲明為 constexpr
? 類定義中禁止不期望的復制
? 使用成員初始化式來對類內數據成員進行默認值初始化
? 如果定義或者 =delete 了任何復制、移動或析構函數,請定義或者 =delete 它們全部(五法則)
? 將單參數的構造函數聲明為 explicit,復制和移動構造函數則不? 采用 union 用以節省內存
十八、性能指南——類型與成員(2)
? 通過常量引用傳遞只讀參數,而不是通過值。
? 盡可能地推遲對象的定義:晚加載,早釋放
? 優先在構造函數中進行初始化而不是賦值。
? 考慮重載以避免隱式類型轉換。
? 理解返回值優化(RVO)
返回值優化(Return Value Optimization,RVO)是 C++ 編譯器的一項重要優化技術,用于消除函數返回對象時的臨時對象構造和復制/移動操作。
RVO 的類型
1.命名返回值優化(NRVO, Named Return Value Optimization):
???? ????? 優化命名局部變量的返回
???? ????? 不是強制性的,但大多數現代編譯器都支持
2.純 RVO:
???? ????? 優化臨時對象的返回
觸發 RVO 的條件
1.返回的類型與函數返回類型完全一致
2.返回的是一個局部對象(對于 NRVO)或臨時對象(對于純 RVO)
3.所有返回路徑返回同一個對象(對于 NRVO)
示例:
#include <iostream>class Test {
public:Test() { std::cout << "Constructor\n"; }Test(const Test&) { std::cout << "Copy Constructor\n"; }Test(Test&&) { std::cout << "Move Constructor\n"; }~Test() { std::cout << "Destructor\n"; }
};// 可能應用 NRVO
Test createTest() {Test t;return t; // 可能被優化,不調用拷貝/移動構造函數
}// 純 RVO
Test createTest2() {return Test(); // 從 C++17 開始,保證不調用移動/拷貝構造函數
}int main() {std::cout << "Creating test1:\n";Test test1 = createTest(); // 通常只有一次構造std::cout << "\nCreating test2:\n";Test test2 = createTest2(); // 保證只有一次構造return 0;
}