笔记②:牛客校招冲刺集训营---C++工程师(面向对象(友元、运算符重载、继承、多态) -- 内存管理 -- 名称空间、模板(类模板/函数模板) -- STL)
0618
C++工程師
- 第5章 高頻考點與真題精講
- 5.1 指針 & 5.2 函數(shù)
- 5.3 面向?qū)ο?#xff08;和5.4、5.5共三次直播課)
- 5.3.1 - 5.3.11
- 5.3.12-14 友元
- 友元全局函數(shù)
- 友元類
- 友元成員函數(shù)
- 友元的注意事項
- 5.3.15-23 運(yùn)算符重載 -->靜態(tài)多態(tài)
- 運(yùn)算符 + = == != ++ () << 重載
- ☆☆☆指針運(yùn)算符重載(* 和 ->)(寫個智能指針類)
- 運(yùn)算符重載的應(yīng)用:封裝字符串類
- 5.3.24 類的自動類型轉(zhuǎn)換和強(qiáng)制類型轉(zhuǎn)換(關(guān)鍵字`explicit`)
- 5.3.25-34 繼承
- 繼承的概念、好處、弊端
- 繼承的基本語法
- protected訪問權(quán)限
- 繼承中的對象模型(成員變量和成員函數(shù)分開存儲)
- 繼承中的構(gòu)造和析構(gòu)順序
- 訪問繼承中的 非靜態(tài) 同名 成員
- 訪問繼承中的 靜態(tài) 同名 成員
- 總結(jié):如何訪問繼承中的同名成員(靜態(tài)成員和非靜態(tài)成員)
- 多繼承/多重繼承
- 菱形繼承-->虛繼承、虛基類☆☆☆
- 虛繼承的實現(xiàn)原理
- 5.3.35-38 多態(tài)(一般是指動態(tài)多態(tài))
- 靜態(tài)聯(lián)編和動態(tài)聯(lián)編(靜態(tài)多態(tài)和動態(tài)多態(tài))
- 虛函數(shù)的原理(多態(tài)的底層原理)
- 多態(tài)案例(開閉原則:對擴(kuò)展進(jìn)行開放,對修改進(jìn)行關(guān)閉)
- 純虛函數(shù) --> 抽象類
- 虛析構(gòu)和純虛析構(gòu)(解決 調(diào)用不到子類析構(gòu)函數(shù) 的問題)
- C++面試寶典-->第一章-->1.3 面向?qū)ο?/li>
- 5.6 內(nèi)存管理①(結(jié)合計算機(jī)操作系統(tǒng)筆記)
- 1.重定位(地址轉(zhuǎn)換:邏輯地址-->物理地址)
- 2.交換技術(shù)
- 3.分段(將各段分別放入內(nèi)存)
- 4.分頁(從連續(xù)到離散)
- 5.7 內(nèi)存管理②(結(jié)合計算機(jī)操作系統(tǒng)筆記)
- 5.段頁式管理 和 虛擬內(nèi)存(虛擬地址空間/虛擬內(nèi)存地址)
- 6.虛擬內(nèi)存和段頁式存儲管理知識補(bǔ)充
- 參考鏈接1
- 參考鏈接2
- 參考鏈接3
- 1.如何將段和頁結(jié)合在一起
- 2.段頁結(jié)合時進(jìn)程對內(nèi)存的使用
- 看個例子(邏輯地址、虛擬地址、物理地址)
- 虛擬內(nèi)存和虛擬存儲器的區(qū)別?
- 參考鏈接4、5
- 7.(5和6的)總結(jié)☆☆☆☆☆
- 7.1 虛擬內(nèi)存的提出是為了解決什么問題?
- 7.2 虛擬內(nèi)存的原理解釋
- 7.3 分頁和頁表
- 7.4 虛擬內(nèi)存地址(邏輯地址)-->物理地址 (快表、缺頁中斷)
- 7.5 虛擬內(nèi)存的功能
- 7.6 段頁式內(nèi)存管理與虛擬內(nèi)存☆☆☆
- C++面試寶典--> 1.2 C++內(nèi)存 和 第2章 操作系統(tǒng)
- 5.7 名稱空間、模板
- 補(bǔ)充:內(nèi)存對齊/字節(jié)對齊
- 名稱空間
- 作用域解析運(yùn)算符(兩個冒號`::`)
- 名稱空間
- using聲明和using編譯指令
- 模板
- 函數(shù)模板
- ①函數(shù)模板語法
- ②函數(shù)模板和普通函數(shù)的區(qū)別
- ③函數(shù)模板調(diào)用規(guī)則
- ④模板實現(xiàn)機(jī)制及局限性
- 類模板
- ①類模板基礎(chǔ)語法
- ②類模板中成員函數(shù)的創(chuàng)建時機(jī)
- ③類模板做函數(shù)參數(shù)
- ④類模板派生類
- ⑤類模板成員函數(shù)類內(nèi)實現(xiàn)
- ⑥類模板成員函數(shù)類外實現(xiàn)
- ⑦類模板分文件編寫(類模板文件 .hpp)
- ⑧模板類碰到友元函數(shù)
- ⑨模板案例---數(shù)組類封裝
- 5.8 STL(標(biāo)準(zhǔn)模板庫)
- 總結(jié) 常用容器的**排序**的區(qū)別:
- 0.自定義排序規(guī)則的實現(xiàn)方式
- 1.vector容器,deque容器;&& 2.list容器
- 3.set容器,map容器:
- 4.匯總
- 函數(shù)對象 & 謂詞
- 內(nèi)建函數(shù)對象
- STL—常用算法
- 5.9 C++新特性
第5章 高頻考點與真題精講
5.1 指針 & 5.2 函數(shù)
5.3 面向?qū)ο?#xff08;和5.4、5.5共三次直播課)
5.3.1 - 5.3.11
見筆記①:牛客校招沖刺集訓(xùn)營—C++工程師
5.3.12-14 友元
友元分為三種:
- 友元全局函數(shù)
- 友元成員函數(shù)
- 友元類
友元的目的就是讓一個 函數(shù)或者類 能夠訪問另一個類中私有成員,關(guān)鍵字是 friend。重載<<運(yùn)算符時,會用到友元。
友元全局函數(shù)
一個全局函數(shù)想訪問一個類中的私有成員變量,那么就把這個全局函數(shù)設(shè)置為這個類的友元(friend),就可以訪問了。
友元類
一個好朋友類想訪問Building類中的私有成員變量,那么就把好朋友類設(shè)置為Building類的友元(friend),就可以訪問了。
友元成員函數(shù)
好朋友類的成員函數(shù)visit()訪問另一類的對象的私有成員變量:
成員函數(shù)visit()在類外實現(xiàn):
Building類中把GoodFriend類的成員函數(shù)visit()設(shè)置為友元,就可以訪問自己的m_BedRoom屬性;
但GoodFriend類的成員函數(shù)visit1()沒被設(shè)置為友元,所以還是無法訪問到自己的m_BedRoom屬性。
友元的注意事項
友元關(guān)系:
1.不能被繼承; 2.是單向的; 3.不具有傳遞性。
5.3.15-23 運(yùn)算符重載 -->靜態(tài)多態(tài)
運(yùn)算符 + = == != ++ () << 重載
C++筆記10:運(yùn)算符重載
概念:
對已有的運(yùn)算符重新進(jìn)行定義,賦予其另一種功能,以適應(yīng)不同的數(shù)據(jù)類型。
例如:加號運(yùn)算符重載的作用就是實現(xiàn)兩個自定義數(shù)據(jù)類型(類)相加的運(yùn)算。
實現(xiàn)的方式有兩種:
- 利用成員函數(shù)重載
- 利用全局函數(shù)重載
C++筆記10:運(yùn)算符重載中實現(xiàn)了:
- 加號運(yùn)算符(+)重載
- 左移運(yùn)算符(<<)重載
改進(jìn) - 遞增運(yùn)算符(++)重載(前置++ 和 后置++)
- 賦值運(yùn)算符(=)重載
如果類的成員變量在堆區(qū),做賦值操作時就會出現(xiàn)深拷貝與淺拷貝問題。 - 關(guān)系運(yùn)算符重載(== 和 !=)
- 函數(shù)調(diào)用運(yùn)算符()重載—仿函數(shù)(在STL中用的比較多)
| 加號運(yùn)算符(+)重載 | Students operator+(const Students& stu){} | friend Students operator+(const Students& stu1, const Students& stu2){} | |
| 左移運(yùn)算符(<<)重載 | friend ostream& operator<<(ostream& cout, const Students& stu){}//可以實現(xiàn)連續(xù)cout輸出 | ||
| 遞增運(yùn)算符(++)重載 | 前置++:Students& operator++(){} 后置++:Students operator++(int){}//這個int是為了和前置++形成重載,以通過編譯,int本身沒啥用 | 前置++:friend Students& operator++(Students& stu){} 后置++:friend Students operator++(Students& stu,int){}//這個int是為了和前置++形成重載,以通過編譯,int本身沒啥用 | |
| 關(guān)系運(yùn)算符(== 和 !=)重載 | bool operator==(Students& stu) {} | friend bool operator==(const Students& stu1, const Students& stu2){} | |
| 賦值運(yùn)算符(=)重載 | void operator=(const Person& p){} 升級版:Person& operator=(const Person& p){}//可以實現(xiàn)連續(xù)賦值 | ||
| 函數(shù)調(diào)用運(yùn)算符()重載 | 也叫仿函數(shù) |
(以上是筆記中復(fù)制來的內(nèi)容)
☆☆☆指針運(yùn)算符重載(* 和 ->)(寫個智能指針類)
(視頻課從01:32:10開始)
指針的操作就是解引用*和箭頭->,所以就是重載這兩個運(yùn)算符。
示例:
#include<iostream> using namespace std;class Person{ public:Person(int age){cout << "Person的構(gòu)造函數(shù)" << endl;m_Age = age;}//輸出成員信息:void showAge(){cout << "m_Age = " << this->m_Age << endl;}~Person(){cout << "Person的析構(gòu)函數(shù)" << endl;} private:int m_Age; };//智能指針類: class SmartPointer{ public://重載箭頭運(yùn)算符->Person* operator->(){return this->m_Person;}//重載解引用運(yùn)算符*Person& operator*(){return *m_Person;}SmartPointer(Person* p){cout << "SmartPointer的構(gòu)造函數(shù)" << endl;m_Person = p;}~SmartPointer(){cout << "SmartPointer析構(gòu)函數(shù)" << endl;if(this->m_Person != nullptr){delete this->m_Person;this->m_Person == nullptr;}} //private:Person* m_Person;//內(nèi)部維護(hù)的Person的指針 };int main(){Person* p = new Person(30);p->showAge();(*p).showAge();//正常應(yīng)該執(zhí)行下面這行,如果忘了就會造成內(nèi)存泄漏,所以就用下面的智能指針類//delete p;cout << "=================================" << endl;//智能指針類就是把p替換成了sp,并且在其析構(gòu)函數(shù)中將new的內(nèi)容delete掉,這樣就解決了可能造成的內(nèi)存泄漏問題SmartPointer sp(p);sp->showAge();//相當(dāng)于sp.operator->()->showAge(); 其中sp.operator->()就相當(dāng)于psp.operator->()->showAge();//編譯器會把sp.operator->()->showAge()優(yōu)化成sp->showAge();(*sp).showAge();//相當(dāng)于sp.operator*().showAge();其中sp.operator*()相當(dāng)于*psp.operator*().showAge();//編譯器會把sp.operator*().showAge()優(yōu)化成(*sp).showAge();return 0;}編譯運(yùn)行:
Person的構(gòu)造函數(shù) m_Age = 30 m_Age = 30 ================================= SmartPointer的構(gòu)造函數(shù) m_Age = 30 m_Age = 30 m_Age = 30 m_Age = 30 SmartPointer析構(gòu)函數(shù) Person的析構(gòu)函數(shù)運(yùn)算符重載的應(yīng)用:封裝字符串類
視頻課:點這里
5.3.24 類的自動類型轉(zhuǎn)換和強(qiáng)制類型轉(zhuǎn)換(關(guān)鍵字explicit)
被explicit修飾的構(gòu)造函數(shù)不能用于自動類型轉(zhuǎn)換(隱式類型轉(zhuǎn)換)。
示例:
#include <iostream> using namespace std;class MyString { public:// 被explicit修飾的構(gòu)造函數(shù)不能用于自動類型轉(zhuǎn)換(隱式類型轉(zhuǎn)換)explicit MyString(int len) {cout << "MyString有參構(gòu)造MyString(int len)..." << endl;}//explicit MyString(const char* str) {cout << "MyString有參構(gòu)造MyString(const char* str)..." << endl;}// 轉(zhuǎn)換函數(shù):轉(zhuǎn)換成doubleoperator double() {// 業(yè)務(wù)邏輯return 20.1;}};// 類的自動類型轉(zhuǎn)換和強(qiáng)制類型轉(zhuǎn)換 /*如果我們想讓類對象轉(zhuǎn)換成基本類型的數(shù)據(jù),我們需要在類中添加轉(zhuǎn)換函數(shù)1.轉(zhuǎn)換函數(shù)必須是類方法2.轉(zhuǎn)換函數(shù)不能指定返回類型3.轉(zhuǎn)換函數(shù)不能有參數(shù) */ int main() {// 基本內(nèi)置數(shù)據(jù)類型的自動類型轉(zhuǎn)換和強(qiáng)制類型轉(zhuǎn)換long count = 8;double time = 10;int size = 3.14;cout << count << endl; //8cout << time << endl; //10cout << size << endl; //3double num = 20.3;cout << num << endl; //20.3// 強(qiáng)制轉(zhuǎn)換成int類型的數(shù)據(jù)int num1 = (int)num; int num2 = int(num);cout << num1 << endl; //20cout << num2 << endl; //20MyString str = "hello"; //// MyString str = MyString("hello");// MyString str1 = 10; //不能通過隱式類型轉(zhuǎn)換創(chuàng)建對象了MyString str1 = MyString(10);// 類的強(qiáng)制類型轉(zhuǎn)換double d = str1;cout << d << endl; //20.1double d1 = double(str);double d2 = (double)str;cout << d1 << endl; //20.1cout << d2 << endl; //20.1return 0; }結(jié)果:
8 10 3 20.3 20 20 MyString有參構(gòu)造MyString(const char* str)... MyString有參構(gòu)造MyString(int len)... 20.1 20.1 20.15.3.25-34 繼承
繼承的概念、好處、弊端
繼承是面向?qū)ο蟮囊淮筇卣鳌?/p>
繼承好處:
- 提高代碼的復(fù)用性;
- 提高代碼的維護(hù)性(方便維護(hù));
- 讓類與類之間產(chǎn)生了關(guān)系,是(動態(tài))多態(tài)的前提。
繼承的弊端:
- 類的耦合性增加了
開發(fā)的原則是:高內(nèi)聚,低耦合
內(nèi)聚:自己完成一件事的能力;
耦合:類和類之間的關(guān)系。
繼承的基本語法
繼承的語法:
class 子類:繼承方式 父類{ ... };例如: class dogs: public Animals { ... };繼承方式分為public,protected,private,即公共繼承,保護(hù)繼承,私有繼承。
注意:
如果不寫繼承方式,默認(rèn)是私有繼承。
protected訪問權(quán)限
三種權(quán)限:
- public: 公共的訪問權(quán)限,被修飾的成員在類內(nèi)和類外都能夠被訪問;
- protected: 受保護(hù)的訪問權(quán)限,如果沒有繼承關(guān)系,就和private的特點一樣;
- privated: 私有的訪問權(quán)限,被修飾的成員只能在類內(nèi)被訪問,在類外不能夠被訪問;
在繼承關(guān)系中,父類中的protected修飾的成員,子類中可以直接訪問,但在類外的其他地方不能訪問。
成員變量一般使用privated私有訪問控制,不要使用protected受保護(hù)的訪問控制;
成員方法如果想要讓子類訪問,但是不想讓外界訪問,就可以使用protected受保護(hù)的訪問控制。
(下面的內(nèi)容來自C++筆記3:C++核心編程中的4.6.2 繼承方式)
總結(jié):
1.父類中的私有內(nèi)容(private)任何一種繼承方式都訪問不到,即無法被訪問/被繼承;
2.公共繼承:父類中的各訪問權(quán)限不變
3.保護(hù)繼承:父類中的各訪問權(quán)限都變成protected保護(hù)權(quán)限
4.私有繼承:父類中的各訪問權(quán)限都變成private私有權(quán)限
無繼承關(guān)系:
| public修飾的num1 | 可以訪問 | 可以訪問 |
| protected修飾的num2 | 可以訪問 | 不能訪問 |
| private修飾的num3 | 可以訪問 | 不能訪問 |
子類公共繼承父類:class Zi : public Fu{ ... };
| public修飾的num1 | public修飾的num1 | 可以訪問 | 可以訪問 | 可以訪問 | 可以訪問 |
| protected修飾的num2 | protected修飾的num2 | 可以訪問 | (因為有繼承關(guān)系,所以) 可以訪問 | 不能訪問 | 不能訪問 |
| private修飾的num3 | private修飾的num3 | 可以訪問 | 不能訪問 | 不能訪問 | 不能訪問 |
子類保護(hù)繼承父類:class Zi : protected Fu{ ... };
| public修飾的num1 | protected修飾的num1 | 可以訪問 | 可以訪問 | 可以訪問 | 不能訪問 |
| protected修飾的num2 | protected修飾的num2 | 可以訪問 | (因為有繼承關(guān)系,所以) 可以訪問 | 不能訪問 | 不能訪問 |
| private修飾的num3 | private修飾的num3 | 可以訪問 | 不能訪問 | 不能訪問 | 不能訪問 |
子類私有繼承父類:class Zi : private Fu{ ... };
| public修飾的num1 | private修飾的num1 | 可以訪問 | 可以訪問 | 可以訪問 | 不能訪問 |
| protected修飾的num2 | private修飾的num2 | 可以訪問 | (因為有繼承關(guān)系,所以) 可以訪問 | 不能訪問 | 不能訪問 |
| private修飾的num3 | private修飾的num3 | 可以訪問 | 不能訪問 | 不能訪問 | 不能訪問 |
示例:
#include<iostream> using namespace std;class Fu{ public:int num1; protected:int num2; private:int num3;public:void func(){num1 = 10;num2 = 20;num3 = 30;} }; class Zi : protected Fu{ //protected保護(hù)繼承 public公共繼承 private私有繼承 public:void func1(){num1 = 10;num2 = 20;num3 = 30;} };int main(){Fu fu;cout << fu.num1 << endl;cout << fu.num2 << endl;cout << fu.num3 << endl;Zi zi;cout << zi.num1 << endl;cout << zi.num2 << endl;cout << zi.num3 << endl; }繼承中的對象模型(成員變量和成員函數(shù)分開存儲)
類中的成員變量和成員函數(shù)是分開存儲的。
在對象中,只保存了(非靜態(tài))成員變量的信息;
子類將父類中的所有成員都繼承了過來,包括私有的成員(變量和方法)。
(筆記①:牛客校招沖刺集訓(xùn)營—C++工程師中的5.3.8 靜態(tài)成員(靜態(tài)成員變量和靜態(tài)成員函數(shù))— 補(bǔ)充:成員變量和成員函數(shù)分開存儲 中也有講到)
普通成員變量(非靜態(tài)成員變量)屬于類的對象;
普通成員函數(shù)(非靜態(tài)成員函數(shù))、靜態(tài)成員變量、靜態(tài)成員函數(shù)屬于類本身。
空對象占1個字節(jié)。
示例:
#include<iostream> using namespace std;class Fu{ public:int num1;//4個字節(jié) 非靜態(tài)成員變量 protected:int* num2;//8個字節(jié) 非靜態(tài)成員變量 private:long num3;//8個字節(jié) 非靜態(tài)成員變量 public:static int num4;//靜態(tài)成員變量void func(){}//非靜態(tài)成員函數(shù)static void func1(){}//靜態(tài)成員函數(shù) }; class Zi : public Fu{ public:int num5;//4個字節(jié) 非靜態(tài)成員變量 protected: int* num6;//8個字節(jié) 非靜態(tài)成員變量 private: long num7;//8個字節(jié) 非靜態(tài)成員變量 public:static int num8; //靜態(tài)成員變量void func2(){}//非靜態(tài)成員函數(shù)static void func3(){} //靜態(tài)成員函數(shù) };int main(){Fu fu;cout << "一個具體的對象的大小:" << sizeof(fu) << endl;//24cout << "一個類的大小:" << sizeof(Fu) << endl;//48Zi zi;cout << "一個具體的對象的大小:" << sizeof(zi) << endl;cout << "一個類的大小:" << sizeof(Zi) << endl;return 0; }結(jié)果:
(為什么是24不是20,因為字節(jié)對齊(內(nèi)存對齊))
如果把父類中的內(nèi)容都屏蔽掉,結(jié)果如下:(空對象占1個字節(jié))
class Fu{ public://int num1;//4個字節(jié) 非靜態(tài)成員變量//int* num2;//8個字節(jié) 非靜態(tài)成員變量//long num3;//8個字節(jié) 非靜態(tài)成員變量//static int num4; //靜態(tài)成員變量//void func(){}//非靜態(tài)成員函數(shù)//static void func1(){} //靜態(tài)成員函數(shù) };結(jié)果: 一個具體的對象的大小:1 一個類的大小:1 一個具體的對象的大小:24 一個類的大小:24如果只屏蔽父類中的非靜態(tài)成員變量,結(jié)果如下:(空對象占1個字節(jié))
class Fu{ public://int num1;//4個字節(jié) 非靜態(tài)成員變量//int* num2;//8個字節(jié) 非靜態(tài)成員變量//long num3;//8個字節(jié) 非靜態(tài)成員變量static int num4; //靜態(tài)成員變量void func(){}//非靜態(tài)成員函數(shù)static void func1(){} //靜態(tài)成員函數(shù) };結(jié)果: 一個具體的對象的大小:1 一個類的大小:1 一個具體的對象的大小:24 一個類的大小:24所以說:
普通成員變量(非靜態(tài)成員變量)屬于類的對象;
普通成員函數(shù)(非靜態(tài)成員函數(shù))、靜態(tài)成員變量、靜態(tài)成員函數(shù)屬于類本身。
(這里第一次用到了對象模型來輔助理解)
具體是通過Visual Studio的工具查看一個類的內(nèi)存分布:
1.打開這個工具;
2.切換到當(dāng)前原文件的目錄;
3.cl /d1 reportSingleClassLayout類名 文件名
繼承中的構(gòu)造和析構(gòu)順序
(見筆記①:牛客校招沖刺集訓(xùn)營—C++工程師中的5.3.7 類對象作為類成員(構(gòu)造和析構(gòu)的順序))
(見C++筆記3:C++核心編程中的4.6.4 繼承中的構(gòu)造和析構(gòu)順序)
繼承中 先調(diào)用父類構(gòu)造函數(shù),再調(diào)用子類構(gòu)造函數(shù),析構(gòu)順序與構(gòu)造相反。
視頻課中的筆記:
訪問繼承中的 非靜態(tài) 同名 成員
出現(xiàn)同名成員,就會出現(xiàn)二義性的現(xiàn)象,一般是通過加作用域的方式來避免出現(xiàn)二義性。
(見C++筆記3:C++核心編程中的4.6.5 繼承同名成員處理方式)
問題:當(dāng)子類與父類出現(xiàn)同名的成員,如何通過子類對象,訪問到子類或父類中同名的數(shù)據(jù)呢?
答:
子類對象可以直接訪問到子類中同名成員;
子類對象加作用域可以訪問到父類同名成員;
當(dāng)子類與父類擁有同名的成員函數(shù),子類會隱藏父類中所有同名成員函數(shù),加作用域可以訪問到父類中同名函數(shù)。
視頻課里的例子:(視頻課從19:28開始)
訪問繼承中的 靜態(tài) 同名 成員
當(dāng)子類與父類出現(xiàn)同名的成員,如何通過子類對象,訪問到子類或父類中同名的數(shù)據(jù)呢?
對于同名的靜態(tài)成員,也是加上作用域:
但是,由于靜態(tài)成員(變量和函數(shù))被所有對象共享,所以一般不通過子類對象來訪問這些同名成員,而是直接加上父類類名的作用域來訪問。
總結(jié):如何訪問繼承中的同名成員(靜態(tài)成員和非靜態(tài)成員)
訪問繼承中的同名成員有一下幾種方式:
答:靜態(tài)成員可以直接加個作用域來訪問,非靜態(tài)成員必須通過特定的對象來訪問。
方式4的理由:
由于靜態(tài)成員(變量和函數(shù))被所有對象共享,所以一般不通過子類對象來訪問這些同名成員,而是直接加上父類類名的作用域來訪問;
但非靜態(tài)成員就必須要通過具體的對象來訪問了。
代碼:
#include<iostream> using namespace std;class Fu{ public:int num1;//非靜態(tài)成員變量static int num4;//靜態(tài)成員變量void func(){}//非靜態(tài)成員函數(shù)static void func1(){}//靜態(tài)成員函數(shù) }; class Zi : public Fu{ public:int num1;//非靜態(tài)成員變量static int num4;//靜態(tài)成員變量void func(){}//非靜態(tài)成員函數(shù)static void func1(){}//靜態(tài)成員函數(shù) };int main(){//通過父類對象訪問父類中的非靜態(tài)成員和靜態(tài)成員:Fu fu;cout << fu.num1 << endl;cout << fu.num4 << endl;fu.func();fu.func1();//通過子類對象訪問子類中的非靜態(tài)成員和靜態(tài)成員:Zi zi;cout << zi.num1 << zi.num4 << endl;zi.func();zi.func1();//通過子類對象訪問父類中的非靜態(tài)成員和靜態(tài)成員:cout << zi.Fu::num1 << endl;cout << zi.Fu::num4 << endl;zi.Fu::func();zi.Fu::func1();//不通過對象來訪問,直接加個作用域行不行? 靜態(tài)成員可以直接加個作用域來訪問,非靜態(tài)成員必須通過特定的對象來訪問cout << Fu::num1 << endl;//非靜態(tài)成員引用必須與特定對象相對cout << Fu::num4 << endl;Fu::func();//非靜態(tài)成員引用必須與特定對象相對Fu::func1();cout << Zi::num1 << endl;//非靜態(tài)成員引用必須與特定對象相對cout << Zi::num4 << endl;Zi::func();//非靜態(tài)成員引用必須與特定對象相對Zi::func1();return 0; }多繼承/多重繼承
多繼承
概念:一個類繼承多個類
語法:class 子類: 繼承方式 父類1, 繼承方式 父類2...
注意:繼承方式不要省略,否則就默認(rèn)是私有繼承。
C++實際開發(fā)中不建議用多繼承,從多個類繼承可能導(dǎo)致成員方法或成員變量同名產(chǎn)生較多的歧義。
看個示例:
基類Base1:
基類Base2:
子類多繼承Base1和Base2:
main函數(shù):
輸出m_A的時候兩個父類都有,就沒辦法直接訪問了,要加上作用域;
輸出m_B的時候就可以直接輸出,或者也可以加上作用域。
菱形繼承–>虛繼承、虛基類☆☆☆
什么是菱形繼承?
菱形繼承會帶來什么問題?
怎么解決?
示例:
Person類:
Singer類和Waiter類都繼承自Person類,然后SingerWaiter類多繼承Singer類和Waiter類:
main函數(shù):
解決方法:給Singer類和Waiter類的繼承方式后面加上virtual關(guān)鍵字,就成了虛繼承,此時Person類被稱為虛基類
結(jié)果:
虛繼承的實現(xiàn)原理
vbptr:virtual base pointer(虛基類指針)
原理:
只有一個唯一的成員,通過保存虛基類指針,這個指針指向一張表(虛基類表),這個表中保存了當(dāng)前獲取到唯一的數(shù)據(jù)的偏移量offset。
(第二次用了對象模型來輔助理解)
5.3.35-38 多態(tài)(一般是指動態(tài)多態(tài))
靜態(tài)聯(lián)編和動態(tài)聯(lián)編(靜態(tài)多態(tài)和動態(tài)多態(tài))
以下的內(nèi)容來自c++筆記3的4.7.1 多態(tài)的基本概念
①多態(tài)分為兩類:
- 靜態(tài)多態(tài): 函數(shù)重載 和 運(yùn)算符重載 屬于靜態(tài)多態(tài),復(fù)用函數(shù)名;
- 動態(tài)多態(tài): 派生類和虛函數(shù)實現(xiàn)運(yùn)行時多態(tài),一般我們說多態(tài)指的都是動態(tài)多態(tài)。
②靜態(tài)多態(tài)和動態(tài)多態(tài)區(qū)別:
- 靜態(tài)多態(tài)的函數(shù)地址早綁定 - 編譯階段確定函數(shù)地址
- 動態(tài)多態(tài)的函數(shù)地址晚綁定 - 運(yùn)行階段確定函數(shù)地址
③動態(tài)多態(tài)滿足條件:
- 有繼承關(guān)系
- 子類重寫父類中的虛函數(shù)(virtual + 函數(shù)名)
④動態(tài)多態(tài)使用條件
- 父類指針或引用指向子類對象
⑤重寫和重載的區(qū)別
重寫:
- 函數(shù)返回值類型、函數(shù)名、參數(shù)列表完全一致稱為重寫;
- 重寫也叫覆蓋、覆寫。
重載:
①同一個作用域下;
②函數(shù)名稱相同;
③函數(shù)參數(shù)的 類型不同、個數(shù)不同 、順序不同;
④和返回值類型、函數(shù)形參名無關(guān);
示例:
關(guān)鍵在于父類中成員函數(shù)前的virtual關(guān)鍵字
父類的speak函數(shù)前不加關(guān)鍵字virtual,結(jié)果如下:(靜態(tài)多態(tài))
sizeof Animals類 = 1 //空類(非靜態(tài)成員函數(shù)不屬于對象的內(nèi)存) sizeof Cats類 = 1 動物在說話 動物在說話結(jié)果是調(diào)用了父類的speak函數(shù),這屬于地址早綁定,也叫靜態(tài)聯(lián)編,因為在編譯期間就知道父類speak函數(shù)的地址了。
而我們想要的結(jié)果是如果傳進(jìn)來是貓類的對象,就執(zhí)行貓的speak函數(shù);傳進(jìn)來是狗類的對象,就執(zhí)行狗的speak函數(shù);這屬于地址晚綁定,也叫動態(tài)聯(lián)編。具體做法就是
在父類的speak函數(shù)前加關(guān)鍵字virtual,結(jié)果如下:(動態(tài)多態(tài))
sizeof Animals類 = 8 //虛函數(shù)表指針vfptr的大小 sizeof Cats類 = 8 小貓在說話 小狗在說話虛函數(shù)的原理(多態(tài)的底層原理)
main函數(shù)中的部分代碼:
cout << "sizeof Animals類 = " << sizeof(Animals) << endl;//有virtual關(guān)鍵字的Animals類占4字節(jié)//沒有virtual關(guān)鍵字的Animals類占1字節(jié),空類,并且非靜態(tài)成員函數(shù)不屬于類的內(nèi)存(見4.3.1 成員變量和成員函數(shù)分開存儲)//加了virtual關(guān)鍵字后Animals類占4字節(jié),不再是空類,而是多了一個指針,叫vfptr(虛函數(shù)表指針),表內(nèi)記錄虛函數(shù)的地址Cats cat;//當(dāng)子類 重寫 父類的 虛函數(shù) ,子類中的虛函數(shù)表內(nèi)部會替換成子類的虛函數(shù)地址//子類重寫父類的虛函數(shù),即子類也有個vfptr(虛函數(shù)表指針),表內(nèi)記錄虛函數(shù)的地址cout << "sizeof Cats類 = " << sizeof(Cats) << endl;//4解釋:
①沒有virtual關(guān)鍵字的Animals類占1字節(jié),空類,因為非靜態(tài)成員函數(shù)不屬于對象的內(nèi)存(見繼承中的對象模型(成員變量和成員函數(shù)分開存儲));
②加了virtual關(guān)鍵字后Animals類占4字節(jié)(32位操作系統(tǒng))/8個字節(jié)(64位操作系統(tǒng)),不再是空類,而是多了一個指針,叫vfptr(虛函數(shù)表指針),表內(nèi)記錄虛函數(shù)的地址;虛函數(shù)表指針指向虛函數(shù)表vftable;
③由于子類重寫父類的虛函數(shù),因此子類也有個vfptr(虛函數(shù)表指針),表內(nèi)記錄虛函數(shù)的地址;
④在子類重寫父類的虛函數(shù)時,子類中的虛函數(shù)表內(nèi)部會替換成子類的虛函數(shù)地址;
多態(tài)的底層原理:
- 首先在父類中的虛函數(shù)(virtual void Speak(){}),使得父類占4個字節(jié)(32位操作系統(tǒng))/8個字節(jié)(64位操作系統(tǒng)),這4個字節(jié)是個vfptr(虛函數(shù)表指針),它指向虛函數(shù)表(vftable),表內(nèi)記錄虛函數(shù)的地址(&Animal::speak);
- 然后是子類重寫父類中的虛函數(shù),因此子類也占4個字節(jié),這4個字節(jié)也是個vfptr(虛函數(shù)表指針),它也指向虛函數(shù)表(vftable),表內(nèi)也記錄虛函數(shù)的地址;在子類重寫父類中的虛函數(shù)后,子類中的虛函數(shù)表內(nèi)部會替換成子類的虛函數(shù)地址(&Cats::speak)。
- 最后是當(dāng)父類的指針或者引用指向子類對象時,就發(fā)生了多態(tài)。
派生類虛表:
1.先將基類的虛表中的內(nèi)容拷貝一份;
2.如果派生類對基類中的虛函數(shù)進(jìn)行重寫,使用派生類的虛函數(shù)替換相同偏移量位置的基類虛函數(shù);
3.如果派生類中新增加自己的虛函數(shù),按照其在派生類中的聲明次序,放在上述虛函數(shù)之后。
原文鏈接:https://blog.csdn.net/qq_39412582/article/details/81628254
(視頻課中從01:22:00開始)
靜態(tài)多態(tài):
動態(tài)多態(tài):
(第三次用了對象模型來輔助理解)
①父類中的speak函數(shù)前沒加virtual,查看Animal類的內(nèi)存分布:
②父類中的speak函數(shù)前加了virtual:
查看Animal類的內(nèi)存分布:
然后再查看Cats類的內(nèi)存分布:
再查看Dog類的內(nèi)存分布:
如果Cats類不重寫父類的speak函數(shù),它的內(nèi)存分布就變成了:
多態(tài)案例(開閉原則:對擴(kuò)展進(jìn)行開放,對修改進(jìn)行關(guān)閉)
視頻課中的案例和c++筆記3的4.7.2 多態(tài)案例1—計算器類中的案例一樣。
如果想擴(kuò)展新的功能,需要修改源碼。
在真實開發(fā)中提倡開閉原則。
開閉原則:對擴(kuò)展進(jìn)行開放,對修改進(jìn)行關(guān)閉。
多態(tài)技術(shù):
①繼承②父類有虛函數(shù)③子類重寫父類的虛函數(shù)④父類的指針或引用指向子類對象
重寫:函數(shù)返回值類型 函數(shù)名 參數(shù)列表 完全一致稱為重寫
純虛函數(shù) --> 抽象類
在多態(tài)中,通常父類中虛函數(shù)的現(xiàn)實無意義的,主要都是調(diào)用子類重寫的內(nèi)容,所以可以將虛函數(shù)改為“純虛函數(shù)”。
純虛函數(shù)語法:virtual 返回值類型 函數(shù)名 (形參列表) = 0;
父類中有了純虛函數(shù),這個類就被稱為抽象類。
抽象類特點:
- ①無法實例化對象;
- ②子類必須重寫抽象類中的純虛函數(shù),
如果子類沒有重寫父類的純虛函數(shù),那么子類也是一個抽象類。
示例:
//父類:抽象計算器 class abstractCalculator { public:int a = 0, b = 0;//virtual int getResult() {//②虛函數(shù)// return 0;//}virtual int getResult() = 0;//純虛函數(shù) };虛析構(gòu)和純虛析構(gòu)(解決 調(diào)用不到子類析構(gòu)函數(shù) 的問題)
多態(tài)使用時,如果子類中有屬性開辟到堆區(qū),那么父類指針在釋放時無法調(diào)用到子類的析構(gòu)代碼。
解決方式:將父類中的析構(gòu)函數(shù)改為虛析構(gòu)或者純虛析構(gòu)(☆☆☆推薦用虛析構(gòu))。
總結(jié):
純虛析構(gòu)的目的只有一個:讓類成為抽象類。
純虛析構(gòu)需要有聲明,也要有實現(xiàn);
(在類內(nèi)聲明)virtual ~Animal() = 0;
(在類外實現(xiàn))Animal::~Animal(){}
注意:
純虛函數(shù)不用實現(xiàn),但子類必須重寫純虛函數(shù),否則子類也是抽象類;
而純虛析構(gòu)必須要實現(xiàn),而且只能在類外實現(xiàn);
示例:
#include<iostream> #include<string.h> using namespace std;class Animal{ public:Animal(){cout << "Animal類的構(gòu)造" << endl;}virtual void speak() = 0;//純虛函數(shù)//常規(guī)的析構(gòu)函數(shù): 會帶來 無法調(diào)用子類析構(gòu)函數(shù) 的問題~Animal(){cout << "Animal類的析構(gòu)" << endl; }//虛析構(gòu): // virtual ~Animal(){ // cout << "Animal類的析構(gòu)" << endl; // }//純虛析構(gòu):要聲明,也要實現(xiàn) // virtual ~Animal() = 0;//在類內(nèi)聲明 };//純虛析構(gòu):在類外實現(xiàn) // Animal::~Animal(){ // cout << "Animal類的析構(gòu)" << endl; // }class Cats : public Animal{ public: Cats(const char* name){cout << "Cats類的有參構(gòu)造" << endl;this->m_Name = new char(strlen(name) + 1);strcpy(this->m_Name, name);}void speak(){cout << this->m_Name << "小貓在說話..." << endl;}~Cats(){cout << "Cats類的析構(gòu)" << endl; if(this->m_Name != nullptr){delete[] this->m_Name;this->m_Name = nullptr;}} private:char* m_Name; };int main(){Animal* ani = new Cats("Tom");ani->speak();delete ani;return 0; }結(jié)果:
Animal類的構(gòu)造 Cats類的有參構(gòu)造 Tom小貓在說話... Animal類的析構(gòu)并沒有調(diào)用子類的析構(gòu)函數(shù),解決辦法是在父類的析構(gòu)函數(shù)前面加個virtual,將其變?yōu)樘撐鰳?gòu)。
class Animal{ public:Animal(){cout << "Animal類的構(gòu)造" << endl;}virtual void speak() = 0;//純虛函數(shù)//虛析構(gòu):virtual ~Animal(){cout << "Animal類的析構(gòu)" << endl; } };然后再編譯運(yùn)行:
Animal類的構(gòu)造 Cats類的有參構(gòu)造 Tom小貓在說話... Cats類的析構(gòu) Animal類的析構(gòu)或者寫成純虛析構(gòu)的形式:
class Animal{ public:Animal(){cout << "Animal類的構(gòu)造" << endl;}virtual void speak() = 0;//純虛函數(shù)//純虛析構(gòu):要聲明,也要實現(xiàn)virtual ~Animal() = 0;//在類內(nèi)聲明 };//純虛析構(gòu):在類外實現(xiàn) Animal::~Animal(){cout << "Animal類的析構(gòu)" << endl; }那么當(dāng)子類中有屬性開辟到堆區(qū),為什么會出現(xiàn)無法調(diào)用到子類的析構(gòu)代碼的問題呢?
可以聯(lián)想到上面的靜態(tài)聯(lián)編和動態(tài)聯(lián)編中說的:
父類的speak函數(shù)前如果不加關(guān)鍵字virtual,最終的結(jié)果就是調(diào)用了父類的speak函數(shù)(即輸出"動物在說話"),這里也一樣,父類的析構(gòu)函數(shù)前沒有關(guān)鍵字virtual,最終的結(jié)果就是調(diào)用了父類的析構(gòu)函數(shù),所以就沒有調(diào)用子類的析構(gòu)函數(shù)。
那么怎么解決呢?
方法和上圖中的內(nèi)容類似,就是在父類的析構(gòu)函數(shù)前加上關(guān)鍵字virtual,這樣的話就可以調(diào)用到子類的析構(gòu)函數(shù)了。具體解釋見下圖:
(視頻課中從01:56:40開始)
(第四次用了對象模型)
Animal類的析構(gòu)函數(shù)前加了關(guān)鍵字virtual,查看Cat類的內(nèi)存分布:
Cat類的析構(gòu)函數(shù)就會加入到虛函數(shù)表中,這樣就可以調(diào)用到了:
析構(gòu)函數(shù):destructor
構(gòu)造函數(shù):constructor
C++面試寶典–>第一章–>1.3 面向?qū)ο?/h3>
E:\找工作\C++八股文\C面試寶典完整版最最最新.pdf
視頻課中從02:00:39開始
5.6 內(nèi)存管理①(結(jié)合計算機(jī)操作系統(tǒng)筆記)
(這部分內(nèi)容可以看計算機(jī)操作系統(tǒng)筆記 的0620開始一直到第三章結(jié)束)
程序就是文件;
運(yùn)行起來的程序被稱為進(jìn)程;
01:00:35開始回顧第一節(jié)課的內(nèi)容:
1.重定位(地址轉(zhuǎn)換:邏輯地址–>物理地址)
重定位:修改該程序中的地址(相對地址)
什么時候完成重定位?編譯時?還是載入時?
都不對:
編譯時重定位的程序只能放在內(nèi)存固定位置;
載入時重定位的程序一旦載入內(nèi)存就不能動了;
答案是在運(yùn)行時(執(zhí)行每條指令時才)完成重定位,找到真正的物理地址。
(分別對應(yīng)操作系統(tǒng)筆記中的絕對裝入(載入)、靜態(tài)重定位、動態(tài)重定位)
也可以叫地址翻譯:基地址(起始地址) + 邏輯地址(偏移量)–> 物理地址
2.交換技術(shù)
(對應(yīng)操作系統(tǒng)筆記中的內(nèi)存空間的擴(kuò)充:交換技術(shù),是指內(nèi)存和外存之間交換進(jìn)程,以緩解內(nèi)存吃緊的問題)
交換技術(shù):
(要運(yùn)行進(jìn)程2,但內(nèi)存不夠了)
(進(jìn)程1處于睡眠狀態(tài),就把進(jìn)程1換出到磁盤中,把進(jìn)程2換入到內(nèi)存中)
(然后進(jìn)程3頁進(jìn)入睡眠狀態(tài),把進(jìn)程3換出磁盤,把剛剛換出的進(jìn)程1再換入到內(nèi)存中)
3.分段(將各段分別放入內(nèi)存)
程序是分段管理的;
程序運(yùn)行時是分段加載的,而不是將整個程序一次性全部載入內(nèi)存。
(對應(yīng)操作系統(tǒng)筆記中的分段存儲管理的邏輯地址結(jié)構(gòu):<段號/段名,段內(nèi)地址/段內(nèi)偏移量>)
分段:將各段程序分別放入內(nèi)存。
(對應(yīng)操作系統(tǒng)比較中的段表中記錄的內(nèi)容:段號、段的起始地址、段的長度)
具體怎么分段?
有固定分區(qū)和可變分區(qū)(動態(tài)分區(qū)):
(對應(yīng)操作系統(tǒng)筆記中的連續(xù)分配中的固定分區(qū)分配和可變分區(qū)分配(動態(tài)分區(qū)分配))
可變分區(qū)算法:首先適配、最佳適配、最差適配。
(對應(yīng)操作系統(tǒng)筆記中的動態(tài)分區(qū)分配算法:首次適應(yīng)、最佳時應(yīng)、最壞適應(yīng)、臨近時應(yīng))
內(nèi)存緊縮 & 內(nèi)存碎片
(對應(yīng)操作系統(tǒng)筆記中的內(nèi)存緊縮技術(shù),用來解決外部碎片的問題。)
4.分頁(從連續(xù)到離散)
分頁:頁表的內(nèi)容(頁號、內(nèi)存塊號/頁框號) 邏輯地址結(jié)構(gòu):<頁號,頁內(nèi)地址偏移量>
為了提高內(nèi)存空間利用率,頁的大小應(yīng)該足夠小,但頁表就大了,所以就有了二級頁表,即頁表的頁表,稱為頁目錄表。
二級頁表的邏輯地址結(jié)構(gòu):頁目錄號、頁號、頁內(nèi)偏移量
二級頁表的出現(xiàn)是因為沒必要把所有的頁表項都放在內(nèi)存中,很占內(nèi)存,所以就弄成二級頁表,把用的頁表放到內(nèi)存中,沒用到的先放外存中,這樣就提高了內(nèi)存的利用率。
通過二級頁表訪問一個邏輯地址需要三次訪存:
第一次訪問內(nèi)存中的頁目錄表,第二次訪問內(nèi)存中的頁表,第三次訪問目標(biāo)內(nèi)存單元。
因為需要三次訪存,所以引入快表,可以讓訪存次數(shù)降低一次。快表的查詢速度非常快。
5.7 內(nèi)存管理②(結(jié)合計算機(jī)操作系統(tǒng)筆記)
5.段頁式管理 和 虛擬內(nèi)存(虛擬地址空間/虛擬內(nèi)存地址)
(視頻課中從32:52開始)
這里面的段就是虛擬內(nèi)存,那虛擬內(nèi)存中具體是怎么分段的,都分為哪些分區(qū)?就是下面的虛擬地址空間,或者叫虛擬內(nèi)存地址。
C++筆記3:C++核心編程 --> 1、內(nèi)存分區(qū)模型
C++ Primer Plus(嵌入式公開課)—第4章 復(fù)合類型–>4.8.5 自動存儲、靜態(tài)存儲和動態(tài)存儲
C++的內(nèi)存分區(qū)/內(nèi)存模型:(下圖的虛擬內(nèi)存地址中的用戶區(qū))
全局區(qū)(靜態(tài)存儲)、棧區(qū)(自動存儲)、堆區(qū)/自由存儲區(qū)(動態(tài)存儲)
每個進(jìn)程都有一個虛擬地址空間;
同一個進(jìn)程下的不同線程共用一個虛擬地址空間。
共享庫:靜態(tài)庫、共享內(nèi)存;
棧:局部變量、返回值(自動釋放)后進(jìn)先出
堆區(qū):malloc或者new的數(shù)據(jù),要手動釋放
全局區(qū):.bss(未初始化或初始化為0的全局變量)、.data(已初始化全局變量)、.text(代碼段、二進(jìn)制機(jī)器指令)
| 共享庫 | 靜態(tài)庫、共享內(nèi)存 | ||
| 棧區(qū) | 局部變量、返回值(自動釋放) | 后進(jìn)先出 | 可讀可寫 |
| 堆區(qū) | malloc或者new的數(shù)據(jù) | 要手動釋放free或者delete | 可讀可寫 |
| 全局區(qū): | |||
| .bss未初始化或初始化為0的全局變量 | |||
| .data已初始化的全局變量 | |||
| .text代碼段 | 只讀 | ||
| 常量(全局常量+字符串常量) | 只讀 | ||
| 靜態(tài)變量(static) | |||
缺頁中斷
6.虛擬內(nèi)存和段頁式存儲管理知識補(bǔ)充
參考鏈接1
(以下內(nèi)容來自電子發(fā)燒友的文章)
什么是虛擬內(nèi)存?
在現(xiàn)代操作系統(tǒng)中,多任務(wù)已是標(biāo)配。多任務(wù)并行,大大提升了 CPU 利用率,但卻引出了多個進(jìn)程對內(nèi)存操作的沖突問題,虛擬內(nèi)存概念的提出就是為了解決這個問題。
操作系統(tǒng)有一塊物理內(nèi)存(中間的部分),有兩個進(jìn)程(實際會更多)P1 和 P2,操作系統(tǒng)偷偷地分別告訴 P1 和 P2,我的整個內(nèi)存都是你的,隨便用,管夠。可事實上呢,操作系統(tǒng)只是給它們畫了個大餅,這些內(nèi)存說是都給了 P1 和 P2,實際上只給了它們一個序號而已。只有當(dāng) P1 和 P2 真正開始使用這些內(nèi)存時,系統(tǒng)才開始使用輾轉(zhuǎn)挪移,拼湊出各個塊給進(jìn)程用,P2 以為自己在用 A 內(nèi)存,實際上已經(jīng)被系統(tǒng)悄悄重定向到真正的 B 去了,甚至,當(dāng) P1 和 P2 共用了 C 內(nèi)存,他們也不知道。(確保了進(jìn)程之間互不影響,也可以讓兩個進(jìn)程共享同一個內(nèi)存的內(nèi)容)
操作系統(tǒng)的這種欺騙進(jìn)程的手段,就是虛擬內(nèi)存。
對 P1 和 P2 等進(jìn)程來說,它們都以為自己占用了整個內(nèi)存,而自己使用的物理內(nèi)存的哪段地址,它們并不知道也無需關(guān)心。
分頁和頁表?
虛擬內(nèi)存是操作系統(tǒng)里的概念,對操作系統(tǒng)來說,虛擬內(nèi)存就是一張張的對照表,P1 獲取 A 內(nèi)存里的數(shù)據(jù)時應(yīng)該去物理內(nèi)存的 A 地址找,而找 B 內(nèi)存里的數(shù)據(jù)應(yīng)該去物理內(nèi)存的 C 地址。
我們知道系統(tǒng)里的基本單位都是 Byte 字節(jié),如果將每一個虛擬內(nèi)存的 Byte 都對應(yīng)到物理內(nèi)存的地址,每個條目最少需要 8字節(jié)(32位虛擬地址->32位物理地址),在 4G 內(nèi)存的情況下,就需要 32GB 的空間來存放對照表,那么這張表就大得真正的物理地址也放不下了,于是操作系統(tǒng)引入了頁(Page)的概念。
在系統(tǒng)啟動時,操作系統(tǒng)將整個物理內(nèi)存以 4K 為單位,劃分為各個頁。之后進(jìn)行內(nèi)存分配時,都以頁為單位,那么虛擬內(nèi)存頁對應(yīng)物理內(nèi)存頁的映射表就大大減小了。4G 內(nèi)存,只需要 8M 的映射表即可,一些進(jìn)程沒有使用到的虛擬內(nèi)存,也并不需要保存映射關(guān)系,而且Linux 還為大內(nèi)存設(shè)計了多級頁表,可以進(jìn)一頁減少了內(nèi)存消耗。
操作系統(tǒng)虛擬內(nèi)存到物理內(nèi)存的映射表,就被稱為頁表。
內(nèi)存尋址和內(nèi)存分配?
我們知道通過虛擬內(nèi)存機(jī)制,每個進(jìn)程都以為自己占用了全部內(nèi)存,進(jìn)程訪問內(nèi)存時,操作系統(tǒng)都會把進(jìn)程提供的虛擬內(nèi)存地址轉(zhuǎn)換為物理地址,再去對應(yīng)的物理地址上獲取數(shù)據(jù)。CPU 中有一種硬件,內(nèi)存管理單元 MMU(Memory Management Unit)專門用來將翻譯虛擬內(nèi)存地址。CPU 還為頁表尋址設(shè)置了緩存策略(快表),由于程序的局部性原理,其緩存命中率能達(dá)到 98%。(快表其實就是一種特殊的高速緩沖寄存器Cache,高速緩存)
以上情況是頁表內(nèi)存在虛擬地址到物理地址的映射,而如果進(jìn)程訪問的物理地址還沒有被分配,系統(tǒng)則會產(chǎn)生一個缺頁中斷,在中斷處理時,系統(tǒng)切到內(nèi)核態(tài)為進(jìn)程虛擬地址分配物理地址。
虛擬內(nèi)存的功能:
- 虛擬內(nèi)存不僅通過內(nèi)存地址轉(zhuǎn)換解決了多個進(jìn)程訪問內(nèi)存沖突的問題,還帶來更多的益處。
- 它有助于進(jìn)程內(nèi)存管理,主要體現(xiàn)在:
內(nèi)存完整性:由于虛擬內(nèi)存對進(jìn)程的”欺騙”,每個進(jìn)程都認(rèn)為自己獲取的內(nèi)存是一塊連續(xù)的地址。我們在編寫應(yīng)用程序時,就不用考慮大塊地址的分配,總是認(rèn)為系統(tǒng)有足夠的大塊內(nèi)存即可。
安全:由于進(jìn)程訪問內(nèi)存時,都要通過頁表來尋址,操作系統(tǒng)在頁表的各個項目上添加各種訪問權(quán)限標(biāo)識位,就可以實現(xiàn)內(nèi)存的權(quán)限控制。 - 通過虛擬內(nèi)存更容易實現(xiàn)內(nèi)存和數(shù)據(jù)的共享。
在進(jìn)程加載系統(tǒng)庫時,總是先分配一塊內(nèi)存,將磁盤中的庫文件加載到這塊內(nèi)存中,在直接使用物理內(nèi)存時,由于物理內(nèi)存地址唯一,即使系統(tǒng)發(fā)現(xiàn)同一個庫在系統(tǒng)內(nèi)加載了兩次,但每個進(jìn)程指定的加載內(nèi)存不一樣,系統(tǒng)也無能為力。
而在使用虛擬內(nèi)存時,系統(tǒng)只需要將進(jìn)程的虛擬內(nèi)存地址指向庫文件所在的物理內(nèi)存地址即可。如上文圖中所示,進(jìn)程 P1 和 P2 的 B 地址都指向了物理地址 C。
而通過使用虛擬內(nèi)存使用共享內(nèi)存也很簡單,系統(tǒng)只需要將各個進(jìn)程的虛擬內(nèi)存地址指向系統(tǒng)分配的共享內(nèi)存地址即可。 - 虛擬內(nèi)存可以幫進(jìn)程”擴(kuò)充”內(nèi)存。
我們前文提到了虛擬內(nèi)存通過缺頁中斷為進(jìn)程分配物理內(nèi)存,內(nèi)存總是有限的,如果所有的物理內(nèi)存都被占用了怎么辦呢?
Linux 提出 SWAP 的概念,Linux 中可以使用 SWAP 分區(qū),在分配物理內(nèi)存,但可用內(nèi)存不足時,將暫時不用的內(nèi)存數(shù)據(jù)先放到磁盤上,讓有需要的進(jìn)程先使用,等進(jìn)程再需要使用這些數(shù)據(jù)時,再將這些數(shù)據(jù)加載到內(nèi)存中,通過這種”交換”技術(shù),Linux 可以讓進(jìn)程使用更多的內(nèi)存。
參考鏈接2
(下面內(nèi)容來自電子發(fā)燒友的文章)
各個進(jìn)程的虛擬內(nèi)存地址相互獨(dú)立。因此,兩個進(jìn)程空間可以有相同的虛擬內(nèi)存地址,如0x10001000。虛擬內(nèi)存地址和物理內(nèi)存地址又有一定的對應(yīng)關(guān)系,如圖1所示。對進(jìn)程某個虛擬內(nèi)存地址的操作,會被CPU翻譯成對某個具體內(nèi)存地址的操作。
應(yīng)用程序?qū)?strong>物理內(nèi)存地址一無所知。它只可能通過虛擬內(nèi)存地址來進(jìn)行數(shù)據(jù)讀寫。程序中表達(dá)的內(nèi)存地址,也都是虛擬內(nèi)存地址。進(jìn)程對虛擬內(nèi)存地址的操作,會被操作系統(tǒng)翻譯成對某個物理內(nèi)存地址的操作。由于翻譯的過程由操作系統(tǒng)全權(quán)負(fù)責(zé),所以應(yīng)用程序可以在全過程中對物理內(nèi)存地址一無所知。
本質(zhì)上說,虛擬內(nèi)存地址剝奪了應(yīng)用程序自由訪問物理內(nèi)存地址的權(quán)利。進(jìn)程對物理內(nèi)存的訪問,必須經(jīng)過操作系統(tǒng)的審查。因此,掌握著內(nèi)存對應(yīng)關(guān)系的操作系統(tǒng),也掌握了應(yīng)用程序訪問內(nèi)存的閘門。借助虛擬內(nèi)存地址,操作系統(tǒng)可以保障進(jìn)程空間的獨(dú)立性。只要操作系統(tǒng)把兩個進(jìn)程的進(jìn)程空間對應(yīng)到不同的內(nèi)存區(qū)域,就讓兩個進(jìn)程空間成為“老死不相往來”的兩個小王國。兩個進(jìn)程就不可能相互篡改對方的數(shù)據(jù),進(jìn)程出錯的可能性就大為減少。
另一方面,有了虛擬內(nèi)存地址,內(nèi)存共享也變得簡單。操作系統(tǒng)可以把同一物理內(nèi)存區(qū)域?qū)?yīng)到多個進(jìn)程空間。這樣,不需要任何的數(shù)據(jù)復(fù)制,多個進(jìn)程就可以看到相同的數(shù)據(jù)。內(nèi)核和共享庫的映射,就是通過這種方式進(jìn)行的。每個進(jìn)程空間中,最初一部分的虛擬內(nèi)存地址,都對應(yīng)到物理內(nèi)存中預(yù)留給內(nèi)核的空間。這樣,所有的進(jìn)程就可以共享同一套內(nèi)核數(shù)據(jù)。共享庫的情況也是類似。對于任何一個共享庫,計算機(jī)只需要往物理內(nèi)存中加載一次,就可以通過操縱對應(yīng)關(guān)系,來讓多個進(jìn)程共同使用。IPO中的共享內(nèi)存,也有賴于虛擬內(nèi)存地址。
虛擬內(nèi)存地址和物理內(nèi)存地址的分離,給進(jìn)程帶來便利性和安全性。但虛擬內(nèi)存地址和物理內(nèi)存地址的翻譯,又會額外耗費(fèi)計算機(jī)資源。在多任務(wù)的現(xiàn)代計算機(jī)中,虛擬內(nèi)存地址已經(jīng)成為必備的設(shè)計。那么,操作系統(tǒng)必須要考慮清楚,如何能高效地翻譯虛擬內(nèi)存地址?
記錄對應(yīng)關(guān)系最簡單的辦法,就是把對應(yīng)關(guān)系記錄在一張表中。為了讓翻譯速度足夠地快,這個表必須加載在內(nèi)存中。不過,這種記錄方式驚人地浪費(fèi)。如果樹莓派1GB物理內(nèi)存的每個字節(jié)都有一個對應(yīng)記錄的話,那么光是對應(yīng)關(guān)系就要遠(yuǎn)遠(yuǎn)超過內(nèi)存的空間。由于對應(yīng)關(guān)系的條目眾多,搜索到一個對應(yīng)關(guān)系所需的時間也很長。這樣的話,會讓樹莓派陷入癱瘓。
因此,Linux采用了分頁(paging)的方式來記錄對應(yīng)關(guān)系。所謂的分頁,就是以更大尺寸的單位頁(page)來管理內(nèi)存。在Linux中,通常每頁大小為4KB。如果想要獲取當(dāng)前樹莓派的內(nèi)存頁大小,可以使用命令:
getconf PAGE_SIZE得到結(jié)果,即內(nèi)存分頁的字節(jié)數(shù):
4096返回的4096代表每個內(nèi)存頁可以存放4096個字節(jié),即4KB。
Linux把物理內(nèi)存和進(jìn)程空間都分割成頁。
內(nèi)存分頁,可以極大地減少所要記錄的內(nèi)存對應(yīng)關(guān)系。我們已經(jīng)看到,以字節(jié)為單位的對應(yīng)記錄實在太多。如果把物理內(nèi)存和進(jìn)程空間的地址都分成頁,內(nèi)核只需要記錄頁的對應(yīng)關(guān)系,相關(guān)的工作量就會大為減少。由于每頁的大小是每個字節(jié)的4096倍。因此,內(nèi)存中的總頁數(shù)只是總字節(jié)數(shù)的四千分之一。對應(yīng)關(guān)系也縮減為原始策略的四千分之一。分頁讓虛擬內(nèi)存地址的設(shè)計有了實現(xiàn)的可能。
也就是說,分頁其實分的就是虛擬內(nèi)存地址和物理內(nèi)存地址的對應(yīng)關(guān)系。
無論是虛擬頁,還是物理頁,一頁之內(nèi)的地址都是連續(xù)的。這樣的話,一個虛擬頁和一個物理頁對應(yīng)起來,頁內(nèi)的數(shù)據(jù)就可以按順序一一對應(yīng)。這意味著,虛擬內(nèi)存地址和物理內(nèi)存地址的末尾部分應(yīng)該完全相同。大多數(shù)情況下,每一頁有4096個字節(jié)。由于4096是2的12次方,所以地址最后12位的對應(yīng)關(guān)系天然成立。我們把地址的這一部分稱為偏移量(offset)。偏移量實際上表達(dá)了該字節(jié)在頁內(nèi)的位置。地址的前一部分則是頁編號。操作系統(tǒng)只需要記錄頁編號的對應(yīng)關(guān)系(用的頁表,頁號對應(yīng)虛擬頁,頁框號/內(nèi)存塊號對應(yīng)物理頁)。
內(nèi)存分頁制度的關(guān)鍵,在于管理進(jìn)程空間頁(虛擬頁)和物理頁的對應(yīng)關(guān)系。操作系統(tǒng)把對應(yīng)關(guān)系記錄在分頁表(page table)(即頁表)中。這種對應(yīng)關(guān)系讓上層的抽象內(nèi)存和下層的物理內(nèi)存分離,從而讓Linux能靈活地進(jìn)行內(nèi)存管理。由于每個進(jìn)程會有一套虛擬內(nèi)存地址,那么每個進(jìn)程都會有一個分頁表。
參考鏈接3
操作系統(tǒng)——段頁式內(nèi)存管理與虛擬內(nèi)存
在虛擬內(nèi)存中分段、建立段表、將虛擬頁映射到空閑物理頁框,建立頁表。
1.如何將段和頁結(jié)合在一起
在對內(nèi)存進(jìn)行使用的過程中,用戶更希望程序以段的形式被放入內(nèi)存,這樣符合用戶的使用習(xí)慣,譬如找內(nèi)存中“代碼段的第40條指令”。而內(nèi)存更希望將自己等分成若干頁,可以避免因內(nèi)存碎片導(dǎo)致的內(nèi)存利用效率的降低。
為了同時滿足用戶和內(nèi)存的要求,操作系統(tǒng)需要一種中間結(jié)構(gòu)來完成段與頁機(jī)制的統(tǒng)一,這就是虛擬內(nèi)存。
虛擬內(nèi)存是一個抽象的概念,本身并不存在,它是一連串的虛擬地址構(gòu)成的。
當(dāng)程序分段后,從虛擬內(nèi)存上分割出相應(yīng)的分區(qū)與各段建立映射關(guān)系,完成分段機(jī)制;(段表:程序段號、段在虛擬內(nèi)存中的起始地址、段的長度)
再將虛擬內(nèi)存分割成頁,將這些頁放在頁框中,并建立頁和頁框的映射,完成分頁機(jī)制(頁表:頁號、頁框號/內(nèi)存塊號)。
2.段頁結(jié)合時進(jìn)程對內(nèi)存的使用
提出了虛擬內(nèi)存的概念后,我們已經(jīng)能夠?qū)⒎侄螜C(jī)制和分頁機(jī)制有機(jī)地結(jié)合在一起了。現(xiàn)在又要引出兩個問題?程序又是如何放置到內(nèi)存中去的了?又是如何得以正確執(zhí)行的了?
當(dāng)一個程序想要放入內(nèi)存,會經(jīng)歷以下的步驟:
(1)在虛擬內(nèi)存中劃分區(qū)域,將程序分段載入到虛擬內(nèi)存中,其實就是建立了程序段與虛擬內(nèi)存各個分區(qū)間的映射關(guān)系,將這種映射關(guān)系放到段表中,記錄各段與虛擬內(nèi)存的映射關(guān)系。
(2)將虛擬內(nèi)存中的各段打散分成頁,然后建立頁表,記錄虛擬頁號和物理頁框號/內(nèi)存塊號之間的映射關(guān)系。
看個例子(邏輯地址、虛擬地址、物理地址)
以指令“call 40”為例,邏輯地址40。設(shè)代碼段為第一個代碼段,頁面大小為100(頁面大小和頁框大小相同)。
段號為0,找到該段在虛擬內(nèi)存中的起始地址為1000,偏移量(邏輯地址)是40,1000+40=1040,這是在虛擬內(nèi)存中的地址。
再用1040除以頁面大小100,得到虛擬頁號為10,查找頁表發(fā)現(xiàn)對應(yīng)的物理頁框號為5,那么實際內(nèi)存地址為5*100+40=540,就是“mov 1, [300]”指令。
只要將特定的寄存器(LDTR、CR3)的值設(shè)置為正確段表初始地址和頁表初始地址,執(zhí)行每條指令時MMU都會自動完成上述地址轉(zhuǎn)換過程。
虛擬內(nèi)存和虛擬存儲器的區(qū)別?
參考鏈接4、5
參考鏈接
電子發(fā)燒友的文章
一、虛擬內(nèi)存(操作系統(tǒng)筆記中的內(nèi)容)
windows下的虛擬內(nèi)存其實是借用磁盤空間假裝它是內(nèi)存,當(dāng)應(yīng)用訪問虛擬內(nèi)存地址的時候,如果內(nèi)存管理器發(fā)現(xiàn)對應(yīng)的物理地址在磁盤中,那內(nèi)存管理器就會將這部分信息從磁盤中加載回內(nèi)存中。
(以下的內(nèi)容來自操作系統(tǒng)筆記中的3.4.3 虛擬存儲技術(shù))
虛擬內(nèi)存:
在程序裝入(載入)時,可以將程序中很快會用到的部分裝入內(nèi)存,暫時用不到的部分留在外存,就可以讓程序開始執(zhí)行了;
在程序執(zhí)行過程中,當(dāng)所訪問的信息不在內(nèi)存時,由操作系統(tǒng)負(fù)責(zé)將所需信息從外存調(diào)入內(nèi)存,然后繼續(xù)執(zhí)行程序;(外存–>內(nèi)存)
若內(nèi)存空間不夠,由操作系統(tǒng)負(fù)責(zé)將內(nèi)存中暫時用不到的信息換出到外存;(內(nèi)存–>外存)
在操作系統(tǒng)的管理下,在用戶看來似乎有一個比實際內(nèi)存大得多的存儲器,這就是虛擬存儲器。
如何實現(xiàn)虛擬內(nèi)存技術(shù)?
要用到操作系統(tǒng)提供的兩個功能:請求調(diào)頁功能和頁面置換功能。
二、虛擬存儲器(也叫虛擬內(nèi)存???)(牛客的視頻課中的內(nèi)容)
更像是一種機(jī)制,這種機(jī)制在有的書稱為虛擬內(nèi)存,有的書稱為虛擬存儲器,但是這不重要,重要的是其中的原理、核心。
虛擬內(nèi)存是硬件異常、硬件地址翻譯、主存、磁盤文件和內(nèi)核文件的完美交互,它為每個進(jìn)程提供了一個大的、一致的、私有的地址空間。
通過一個很清晰的機(jī)制,虛擬內(nèi)存提供了三個重要的能力:
1)它將主存看成是一個存儲在磁盤上的地址空間的高速緩存,在主存中只保存活動區(qū)域,并根據(jù)需要在磁盤和主存之間來回傳送數(shù)據(jù),通過這種方式,它高效地使用了主存;
2)它為每個進(jìn)程提供了一致的地址空間,從而簡化了內(nèi)存管理;
3)它為每個進(jìn)程提供了私有的地址空間,從而保護(hù)了每個進(jìn)程的地址空間不被其他進(jìn)程破壞。
虛擬內(nèi)存是計算機(jī)系統(tǒng)內(nèi)存管理的一種技術(shù)。 它使得應(yīng)用程序認(rèn)為它擁有連續(xù)可用的內(nèi)存(一個連續(xù)完整的地址空間),而實際上物理內(nèi)存通常被分隔成多個內(nèi)存碎片,還有部分暫時存儲在外部磁盤存儲器上,在需要時進(jìn)行數(shù)據(jù)交換。
三、總結(jié)
虛擬存儲技術(shù):
借用磁盤空間假裝它是內(nèi)存,當(dāng)應(yīng)用訪問虛擬內(nèi)存地址的時候,如果內(nèi)存管理器發(fā)現(xiàn)對應(yīng)的物理地址在磁盤中,那內(nèi)存管理器就會將這部分信息從磁盤(外存)中加載回內(nèi)存中。
虛擬內(nèi)存:
操作系統(tǒng)為每個進(jìn)程提供了一個大的、一致的、私有的地址空間,叫虛擬內(nèi)存地址,或者虛擬地址空間。
1)在主存中只保存活動區(qū)域,并根據(jù)需要在磁盤和主存之間來回傳送數(shù)據(jù),通過這種方式,它高效地使用了主存;
2)它為每個進(jìn)程提供了一致的地址空間,從而簡化了內(nèi)存管理;
3)它為每個進(jìn)程提供了私有的地址空間,從而保護(hù)了每個進(jìn)程的地址空間不被其他進(jìn)程破壞。
7.(5和6的)總結(jié)☆☆☆☆☆
7.1 虛擬內(nèi)存的提出是為了解決什么問題?
操作系統(tǒng)中有個概念叫并行,就是多核處理器中每個核都處理一個進(jìn)程,這些進(jìn)程是同時進(jìn)行的,那么就會有多個進(jìn)程對內(nèi)存操作的沖突問題,而虛擬內(nèi)存概念的提出就是為了解決這個問題。
7.2 虛擬內(nèi)存的原理解釋
首先,每個進(jìn)程都有一個虛擬地址空間(但同一個進(jìn)程下的不同線程共用一個虛擬地址空間),這樣就確保了進(jìn)程之間互不影響;
程序中表達(dá)的內(nèi)存地址,也都是虛擬內(nèi)存地址。進(jìn)程對虛擬內(nèi)存地址的操作,會被操作系統(tǒng)翻譯成對某個物理內(nèi)存地址的操作。由于翻譯的過程由操作系統(tǒng)全權(quán)負(fù)責(zé),所以應(yīng)用程序可以在全過程中對物理內(nèi)存地址一無所知。
本質(zhì)上說,虛擬內(nèi)存地址剝奪了應(yīng)用程序自由訪問物理內(nèi)存地址的權(quán)利。進(jìn)程對物理內(nèi)存的訪問,必須經(jīng)過操作系統(tǒng)的審查。因此,掌握著內(nèi)存對應(yīng)關(guān)系的操作系統(tǒng),也掌握了應(yīng)用程序訪問內(nèi)存的閘門。借助虛擬內(nèi)存地址,操作系統(tǒng)可以保障進(jìn)程空間的獨(dú)立性。只要操作系統(tǒng)把兩個進(jìn)程的進(jìn)程空間對應(yīng)到不同的內(nèi)存區(qū)域,就讓兩個進(jìn)程空間成為“老死不相往來”的兩個小王國。兩個進(jìn)程就不可能相互篡改對方的數(shù)據(jù),進(jìn)程出錯的可能性就大為減少。
操作系統(tǒng)有一塊物理內(nèi)存(中間的部分),有兩個進(jìn)程(實際會更多)P1 和 P2,操作系統(tǒng)偷偷地分別告訴 P1 和 P2,我的整個內(nèi)存都是你的,隨便用,管夠。可事實上呢,操作系統(tǒng)只是給它們畫了個大餅,這些內(nèi)存說是都給了 P1 和 P2,實際上只給了它們一個序號而已。只有當(dāng) P1 和 P2 真正開始使用這些內(nèi)存時,系統(tǒng)才開始使用輾轉(zhuǎn)挪移,拼湊出各個塊給進(jìn)程用,P2 以為自己在用 A 內(nèi)存,實際上已經(jīng)被系統(tǒng)悄悄重定向到真正的 B 去了;甚至,當(dāng) P1 和 P2 共用了 C 內(nèi)存,他們也不知道。
操作系統(tǒng)的這種欺騙進(jìn)程的手段,就是虛擬內(nèi)存。對 P1 和 P2 進(jìn)程來說,它們都以為自己占用了整個內(nèi)存,而自己使用的物理內(nèi)存的哪段地址,它們并不知道,也無需關(guān)心。
虛擬內(nèi)存是操作系統(tǒng)里的概念,對操作系統(tǒng)來說,虛擬內(nèi)存就是一張張的對照表:
P1 獲取 A 內(nèi)存里的數(shù)據(jù)時應(yīng)該去物理內(nèi)存的 A 地址找,而找 B 內(nèi)存里的數(shù)據(jù)應(yīng)該去物理內(nèi)存的 C 地址。(這就是邏輯地址和物理地址的一個對應(yīng)關(guān)系)
此外,由于每個進(jìn)程都一個虛擬地址空間,所以兩個進(jìn)程可以有相同的虛擬內(nèi)存地址,但經(jīng)過地址轉(zhuǎn)換后不一定指向同一塊物理內(nèi)存:
7.3 分頁和頁表
我們知道系統(tǒng)里的基本單位都是 Byte 字節(jié),如果將每一個虛擬內(nèi)存的 Byte 都對應(yīng)到物理內(nèi)存的地址,每個條目最少需要 8字節(jié)(32位虛擬地址->32位物理地址),在 4G 內(nèi)存的情況下,就需要 32GB 的空間來存放對照表,那么這張表就大得真正的物理地址也放不下了,于是操作系統(tǒng)引入了頁(Page)的概念。
在系統(tǒng)啟動時,操作系統(tǒng)將整個物理內(nèi)存以 4K 為單位,劃分為各個頁。之后進(jìn)行內(nèi)存分配時,都以頁為單位,那么虛擬內(nèi)存頁 和 物理內(nèi)存頁 的映射表就大大減小了。
4G 內(nèi)存,只需要 8M 的映射表即可:
32位系統(tǒng)的內(nèi)存是4G = 2^32
每頁是4K = 2^12
所以一共有2^20個頁,每個頁最少占8個字節(jié)B
所以就是8M = 2^3 * 2^20
并且一些進(jìn)程沒有使用到的虛擬內(nèi)存,也并不需要保存映射關(guān)系,此外Linux 還為大內(nèi)存設(shè)計了多級頁表,可以進(jìn)一頁減少了內(nèi)存消耗。
從虛擬內(nèi)存到物理內(nèi)存的映射表,就被稱為頁表,即頁表記錄的是虛擬頁號和頁框號/內(nèi)存塊號的對應(yīng)關(guān)系。
無論是虛擬頁,還是物理頁,一頁之內(nèi)的地址都是連續(xù)的。這樣的話,一個虛擬頁和一個物理頁對應(yīng)起來,頁內(nèi)的數(shù)據(jù)就可以按順序一一對應(yīng)。
由于每個進(jìn)程會有一套虛擬內(nèi)存地址,那么每個進(jìn)程都會有一個分頁表。
7.4 虛擬內(nèi)存地址(邏輯地址)–>物理地址 (快表、缺頁中斷)
我們知道通過虛擬內(nèi)存機(jī)制,每個進(jìn)程都以為自己占用了全部內(nèi)存,進(jìn)程訪問內(nèi)存時,操作系統(tǒng)都會把進(jìn)程提供的虛擬內(nèi)存地址轉(zhuǎn)換為物理地址,再去對應(yīng)的物理地址上獲取數(shù)據(jù)。CPU 中有一種硬件,內(nèi)存管理單元 MMU(Memory Management Unit)專門用來翻譯虛擬內(nèi)存地址。
CPU 還為頁表尋址設(shè)置了緩存策略(快表),由于程序的局部性原理,其緩存命中率能達(dá)到 98%。(快表其實就是一種特殊的高速緩沖寄存器Cache,高速緩存)
以上情況是頁表內(nèi)存在虛擬地址到物理地址的映射,而如果進(jìn)程訪問的物理地址還沒有被分配,系統(tǒng)則會產(chǎn)生一個缺頁中斷,在中斷處理時,系統(tǒng)切到內(nèi)核態(tài)為進(jìn)程提供的虛擬地址分配物理地址。
7.5 虛擬內(nèi)存的功能
- 解決了多個進(jìn)程訪問內(nèi)存沖突的問題;
- 內(nèi)存完整性:由于虛擬內(nèi)存對進(jìn)程的”欺騙”,讓每個進(jìn)程都認(rèn)為自己獲取的內(nèi)存是一塊連續(xù)的地址,并且內(nèi)存足夠大;
- 安全:由于進(jìn)程訪問內(nèi)存時,都要通過頁表來尋址,操作系統(tǒng)在頁表的各個項目上添加各種訪問權(quán)限標(biāo)識位,就可以實現(xiàn)內(nèi)存的訪問權(quán)限控制;
虛擬內(nèi)存地址和物理內(nèi)存地址的分離,給進(jìn)程帶來便利性和安全性。 - 更容易實現(xiàn)內(nèi)存和數(shù)據(jù)的共享,如上文圖中所示,進(jìn)程 P1 和 P2 的 B 地址都指向了物理地址 C。(分段的優(yōu)點:實現(xiàn)信息的共享和保護(hù))
操作系統(tǒng)可以把同一物理內(nèi)存區(qū)域?qū)?yīng)到多個進(jìn)程空間。這樣,不需要任何的數(shù)據(jù)復(fù)制,多個進(jìn)程就可以看到相同的數(shù)據(jù)。 - 可以幫進(jìn)程”擴(kuò)充”內(nèi)存,利用交換技術(shù),在內(nèi)存吃緊的時候把暫時用不到的數(shù)據(jù)換出到外存中;(裝Linux系統(tǒng)的時候有一步是設(shè)置交換分區(qū)的大小)
7.6 段頁式內(nèi)存管理與虛擬內(nèi)存☆☆☆
直接看上面參考鏈接3的全部內(nèi)容,主要配合例子來理解一下。
C++面試寶典–> 1.2 C++內(nèi)存 和 第2章 操作系統(tǒng)
E:\找工作\C++八股文\C面試寶典完整版最最最新.pdf
視頻課中從01:10:39開始
5.7 名稱空間、模板
補(bǔ)充:內(nèi)存對齊/字節(jié)對齊
(視頻課中從01:28:45開始)
為什么要有內(nèi)存對齊,因為加入CPU每次固定的讀4個字節(jié),這樣可以用空間來換取時間。
對齊模數(shù)必須是2的整數(shù)次冪。
內(nèi)存對齊的規(guī)則:
如果成員是結(jié)構(gòu)體變量,就把它里面最大的成員拿出來和對齊模數(shù)作比較,取小的那個的整數(shù)倍;
示例:
#include<iostream> using namespace std; //#pragma pack(show) //默認(rèn)的對齊模數(shù)是8 //#pragma pack(1) //如果把對齊模數(shù)改為1,就相當(dāng)于不存在內(nèi)存對齊了,結(jié)果就是所有的字節(jié)數(shù)加在一起的總和struct Student{//對齊模數(shù)是8int a; //0 ~ 3float b; //4 ~ 7 float大小是4,4和8相比,4小,從4的整數(shù)倍開始char c1; //8 ~ 8 15 char大小是1,1和8相比,1小,從1的整數(shù)倍開始double d; //9 16 ~ 23 double大小是8,8和8相比,8小,從8的整數(shù)倍開始(所以把9改成16,上面的8改成15//最后,最大的8和對齊模數(shù)8相比,8小,所以整個結(jié)構(gòu)體的大小是8的整數(shù)倍,結(jié)果是24 }; struct Student2{//對齊模數(shù)是8int a; //0 ~ 3 7Student stu;//4 8 ~ 31 結(jié)構(gòu)體中最大的是8,8和8相比,8小,所以從8的整數(shù)倍開始(把4改成8,上面的3改成7double d; //32 ~ 39 double大小是8,8和8相比,8小,所以從8的整數(shù)倍開始char e; //40 ~ 40 47 char的大小是1,1和8相比,1小,所以從1的整數(shù)倍開始//最后,最大的8和對齊模數(shù)8相比,8小,所以整個結(jié)構(gòu)體的大小是8的整數(shù)倍,結(jié)果是48(所以上面的40改成47 }; int main(){cout << "float的大小:" << sizeof(float) << endl;//4cout << "double的大小:" << sizeof(double) << endl;//8cout << "int的大小:" << sizeof(int) << endl;//4cout << "char的大小:" << sizeof(char) << endl;//1cout << "結(jié)構(gòu)體Student的大小:" << sizeof(Student) << endl;//24 如果把對齊模數(shù)改為1,結(jié)果是17cout << "結(jié)構(gòu)體Student2的大小:" << sizeof(Student2) << endl;//48 如果把對齊模數(shù)改為1,結(jié)果是30return 0; }名稱空間
(視頻課中從02:03:00開始)
C++ Primer Plus(嵌入式公開課)—第九章 內(nèi)存模型和名稱空間
作用域解析運(yùn)算符(兩個冒號::)
它的優(yōu)先級是運(yùn)算符中等級最高的,例如cat.Animals::name
它有三個作用:1.全局作用域符;2.類作用域符;3名稱空間作用域符
::前面沒有任何內(nèi)容,代表全局作用域符。
名稱空間
名稱空間中可以寫什么?(變量、函數(shù)、結(jié)構(gòu)體、類…)
using聲明和using編譯指令
using聲明和using編譯指令,是用來簡化對名稱空間中名稱的使用;
using聲明:使特定的標(biāo)識符可用;using std::cout; using std::endl;
using編譯指令:讓整個名稱空間中的名稱可用;using namespace std;
也可以不用using聲明,也不用using編譯指令:std::cout << ""<< std::endl;
有了using聲明或者using編譯指令,下面再使用時就不需要再加作用域了
注意:
using聲明:如果局部變量和using聲明同時使用會出現(xiàn)問題;
using編譯指令:如果出現(xiàn)同名的變量,不會報錯,使用就近原則。(局部名稱隱藏名稱空間名)
示例:
#include<iostream> using namespace std;//讓std這個名稱空間下的所有名稱都可用namespace fun{int num = 1;double d = 1.1; }namespace fun1{int num = 1;double d = 1.1; }int main(){cout << fun::num << endl;//1int num = 2; using namespace fun; //using編譯:讓fun名稱空間下的所有名稱都可用 如果出現(xiàn)同名的變量,不會報錯,使用就近原則。//using fun::num; //using聲明:只讓fun名稱空間下的變量num可用 如果和局部變量一起用會出錯//有了上面的using聲明或者using編譯指令,下面再使用時就不需要再加作用域了cout << num << endl; //2cout << num << endl; //有了上面的using聲明,下面再使用時就不需要再加作用域了cout << d << endl;return 0; }模板
(視頻課中從02:16:45開始)
C++筆記7:C++提高編程1:模板—[函數(shù)模板和類模板]
C++除了面向?qū)ο缶幊?/strong>思想,還有泛型編程思想。
泛型編程主要是利用模板技術(shù)來實現(xiàn)的。
函數(shù)模板
①函數(shù)模板語法
語法:
template<typename T> 函數(shù)聲明或定義其中:
template — 聲明創(chuàng)建模板
typename — 表面其后面的符號是一種數(shù)據(jù)類型,可以用class代替
T — 通用的數(shù)據(jù)類型,名稱可以替換,通常為大寫字母
②函數(shù)模板和普通函數(shù)的區(qū)別
函數(shù)模板不允許自動類型轉(zhuǎn)換;
普通函數(shù)能夠自動類型轉(zhuǎn)換;(char --> int)
③函數(shù)模板調(diào)用規(guī)則
C++編譯器優(yōu)先考慮普通函數(shù);
可以通過空模板實例參數(shù)列表的語法限定編譯器只能通過模板匹配;
函數(shù)模板可以重載;
如果函數(shù)模板可以產(chǎn)生一個更好的匹配,那么優(yōu)先選擇模板。
④模板實現(xiàn)機(jī)制及局限性
函數(shù)模板通過具體類型產(chǎn)生不同的函數(shù)。
通過函數(shù)模板產(chǎn)生的函數(shù)稱為模板函數(shù)。
類模板
①類模板基礎(chǔ)語法
template<typename T>//typename可以用class代替 類②類模板中成員函數(shù)的創(chuàng)建時機(jī)
類模板中成員函數(shù)并不是一開始創(chuàng)建,而是在使用的時候才生成,在替換T后生成。
#include<iostream> using namespace std;template<class T> class testClass{ public:void func1(){obj.show1();}void fun2(){obj.show2();}T obj; }; class Person1{ public:void show1(){cout << "調(diào)用show1()函數(shù)" << endl;} }; class Person2{ public:void show2(){cout << "調(diào)用show2()函數(shù)" << endl;} }; int main(){testClass<Person1> tc;//testClass<Person2> tc;//編譯錯誤tc.func1();return 0; }③類模板做函數(shù)參數(shù)
④類模板派生類
⑤類模板成員函數(shù)類內(nèi)實現(xiàn)
⑥類模板成員函數(shù)類外實現(xiàn)
⑦類模板分文件編寫(類模板文件 .hpp)
把類的聲明(.h)和實現(xiàn)(.cpp)寫在一起放到.hpp文件中,一看.hpp文件就知道是類模板文件。
⑧模板類碰到友元函數(shù)
⑨模板案例—數(shù)組類封裝
5.8 STL(標(biāo)準(zhǔn)模板庫)
C++筆記8:C++提高編程2:STL—標(biāo)準(zhǔn)模板庫
SRL六大組件:容器、算法、迭代器、仿函數(shù)、適配器、空間配置器
容器:各種數(shù)據(jù)結(jié)構(gòu),vector、list、deque、set、map,用來存放數(shù)據(jù),是一種類模板;
算法:各種常用的算法,sort、find、copy、for_each,是一種函數(shù)模板;
迭代器:相當(dāng)于指針,是一種將operator*,operator->,operator++,operator–等指針相關(guān)操作予以重載的類模板(運(yùn)算符重載?);
仿函數(shù):重載函數(shù)調(diào)用運(yùn)算符()的類或者類模板;
適配器:用來修飾容器、仿函數(shù)、迭代器的接口
空間配置器:負(fù)責(zé)空間的配置與管理;比如vector容器的擴(kuò)容就是這個空間配置器來完成的。
容器:序列式容器(vector、deque、list)、關(guān)聯(lián)式容器(map、set);
算法:質(zhì)變算法(拷貝、替換、刪除)、非質(zhì)變算法(查找、計數(shù)、遍歷);
迭代器:輸入迭代器(只讀)、輸出迭代器(只寫)、前向迭代器、雙向迭代器、隨機(jī)訪問迭代器。
vector擴(kuò)容:
size()是容器中的元素個數(shù)、capacity()是容器的容量;
reserve(int len);//預(yù)留len個元素長度;
vector與普通數(shù)組區(qū)別:數(shù)組是靜態(tài)空間,而vector可以動態(tài)擴(kuò)展。
(補(bǔ)充:動態(tài)擴(kuò)展并不是在原空間之后續(xù)接新空間,而是找更大的內(nèi)存空間,然后將原數(shù)據(jù)拷貝至新空間,釋放原空間;并且原有的迭代器會失效。)
另外,vector容器的迭代器是支持隨機(jī)訪問的迭代器!!!
總結(jié) 常用容器的排序的區(qū)別:
主要看下面兩個地方的總結(jié):
我的C++八股文中的筆記9最大的收獲;
C++筆記8:C++提高編程2:STL—標(biāo)準(zhǔn)模板庫中的☆☆☆總結(jié) 常用容器的排序的區(qū)別
總結(jié):
0.自定義排序規(guī)則的實現(xiàn)方式
自定義排序規(guī)則:(全局函數(shù))
- mySort0()//內(nèi)置數(shù)據(jù)類型
- mySort0()//自定義數(shù)據(jù)類型
自定義排序規(guī)則:(仿函數(shù))
- mySort2()//內(nèi)置數(shù)據(jù)類型和自定義數(shù)據(jù)類型
具體代碼:
//自定義排序規(guī)則:(全局函數(shù)) bool mySort0(int a, int b){//降序if(a > b)return 1;else return 0; } bool C(const Person& p1, const Person& p2){//降序if(p1.age > p2.age)//按年齡降序return 1;else if(p1.age == p2.age){//如果年齡相等,就按身高升序return p1.height < p2.height;}else return 0; } //自定義排序規(guī)則:(仿函數(shù)) class mySort2{ public://重載函數(shù)調(diào)用運(yùn)算符()bool operator()(int a, int b){return a > b;}bool operator()(const Person& p1, const Person& p2) {//constif(p1.age > p2.age)//按年齡降序return 1;else if(p1.age == p2.age){//如果年齡相等,就按身高升序return p1.height < p2.height;}else return 0;} };1.vector容器,deque容器;&& 2.list容器
對于內(nèi)置數(shù)據(jù)類型:
如果想自定義排序規(guī)則,可以使用全局函數(shù)和仿函數(shù)和內(nèi)建仿函數(shù)來實現(xiàn):
對于自定義數(shù)據(jù)類型:
如果容器中存放的是自定義數(shù)據(jù)類型,就必須要指明自定義的排序規(guī)則,不能使用默認(rèn)的排序規(guī)則。
3.set容器,map容器:
沒有sort(),因為此容器會自動排序,默認(rèn)是升序,因此可以利用仿函數(shù)實現(xiàn)自定義排序規(guī)則,不能通過全局函數(shù)來實現(xiàn)。
對于內(nèi)置數(shù)據(jù)類型:
如果想自定義排序規(guī)則,可以使用仿函數(shù)和內(nèi)建仿函數(shù)來實現(xiàn):
對于自定義數(shù)據(jù)類型:
如果容器中存放的是自定義數(shù)據(jù)類型,在創(chuàng)建容器的時候就要指明自定義的排序規(guī)則,不能使用默認(rèn)的排序規(guī)則。
注意:這里的排序規(guī)則寫的是類名,后面不用加括號,跟上面的vector、deque、list不一樣,注意區(qū)分!!!
set容器:
map容器:
map<double, double> m1;//默認(rèn)排序規(guī)則---按照Key值從小到大排 map<double, double,MySort2> m2;//自定義排序規(guī)則---在創(chuàng)建容器的時候就指明排序規(guī)則(按Key值降序) map<double, double, greater<>> m5;//使用內(nèi)建仿函數(shù)(大于仿函數(shù)greater<>()):按Key值降序map<double, Person> m3;//默認(rèn)排序規(guī)則:按Key值升序 map<double, Person,MySort2> m6;//利用仿函數(shù)自定義排序規(guī)則:按Key值降序 map<double, Person, greater<>> m7;//內(nèi)建仿函數(shù)(大于仿函數(shù)greater<>()):按Key值降序4.匯總
| vector容器、deque容器、list容器 (內(nèi)置數(shù)據(jù)類型) | √ | √ | √ | √ | 全局函數(shù)后不加括號;類名后面加括號 |
| vector容器、deque容器、list容器 (自定義數(shù)據(jù)類型) | × | × | √ | √ | 全局函數(shù)后不加括號;類名后面加括號 |
| set容器 (內(nèi)置數(shù)據(jù)類型) | √ | √ | × | √ | 不能用全局函數(shù)實現(xiàn),只能用仿函數(shù)實現(xiàn) 并且是用仿函數(shù)時類名后面不加括號 |
| set容器 (自定義數(shù)據(jù)類型) | × | × | × | √ | 只能用仿函數(shù)實現(xiàn) 并且是用仿函數(shù)時類名后面不加括號 |
| map容器 (內(nèi)置數(shù)據(jù)類型) | √ 按照key值從小到大排序 | √ 按照key值從小到大排序 | × | √ | 不能用全局函數(shù)實現(xiàn),只能用仿函數(shù)實現(xiàn) 并且是用仿函數(shù)時類名后面不加括號 |
| map容器 (自定義數(shù)據(jù)類型) | √ 按照key值從小到大排序 | √ 按照key值從小到大排序 | × | √ | 不能用全局函數(shù)實現(xiàn),只能用仿函數(shù)實現(xiàn) 并且是用仿函數(shù)時類名后面不加括號 |
函數(shù)對象 & 謂詞
函數(shù)對象基本概念:
重載函數(shù)調(diào)用操作符()的類,其對象常稱為函數(shù)對象;
函數(shù)對象在使用重載的函數(shù)調(diào)用操作符()時,行為類似函數(shù)調(diào)用,所以也叫仿函數(shù)。
注意:函數(shù)對象(仿函數(shù))是一個類,不是一個函數(shù)。(和普通函數(shù)類似,又超出普通函數(shù)的概念)
謂詞基本概念:
返回bool類型的仿函數(shù)稱為謂詞;
如果operator()接受一個參數(shù),那么叫做一元謂詞;
如果operator()接受兩個參數(shù),那么叫做二元謂詞。
內(nèi)建函數(shù)對象
需要引入頭文件:
#include<functional>關(guān)系仿函數(shù)中最常用的就是greater<>大于,
因為默認(rèn)排序規(guī)則都時升序,想要實現(xiàn)降序排序規(guī)則,就要是用大于仿函數(shù)greater<>()。
STL—常用算法
可以看C++筆記9:C++提高編程3:STL—函數(shù)對象&標(biāo)準(zhǔn)算法中的5、STL—常用算法
5.9 C++新特性
見筆記③:牛客校招沖刺集訓(xùn)營—C++工程師
總結(jié)
以上是生活随笔為你收集整理的笔记②:牛客校招冲刺集训营---C++工程师(面向对象(友元、运算符重载、继承、多态) -- 内存管理 -- 名称空间、模板(类模板/函数模板) -- STL)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 某页式虚拟存储器,若某用户空间为16个界
- 下一篇: C++ Primer Plus(嵌入式公