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