目錄
1、封裝
2、繼承
繼承方式:
(1)公有繼承;public
(2)保護繼承;protected
(3)私有繼承;private
菱形繼承:?
同名隱藏?
含義:
產生原因:
?同名覆蓋?(函數重寫)
定義
作用
?3、多態
(1)多態的分類
(2)虛表:
(3)代碼示例?
指針:?
引用實現:
(1)派生類對象可以給基類,但基類不能給派生類。?
(2)強制類型轉換后,查的仍然是Base的虛表:
?(3)定義obj類型的對象,訪問的仍是Obj的虛表,
?(4)繼承關系中,動態創建派生類對象,但是是拿基類對象指向的,Object * op = new Base();在delete *op時,調用派生類的析構函數,解決辦法是將基類的析構函數設為虛函數,之后,就可以先調用~Base();再調基類的析構是為什么?
原理分析
1、封裝
封裝是面向對象編程(OOP)的四大基本特性之一(另外三個是繼承、多態和抽象),它是一種將數據(屬性)和操作這些數據的方法(行為)捆綁在一起,并對外部隱藏對象的內部實現細節的機制。
class?類名:繼承方式 基類名1,繼承方式 基類名2,。。。繼承方式 基類名n
{
???派生類成員定義
};
2、繼承
單繼承:一個類只有一個直接基類。
多繼承:一個類擁有多個直接基類。
繼承方式:
(1)公有繼承;public
- 基類的私有成員在派生類中不能直接訪問。
- 基類的保護成員只能在派生類內部訪問,不能在派生類外部訪問, 在派生類中,繼承而來的基類保護成員依然是protected。
- 基類的公有成員在派生類內部和外部都可以被訪問得到,在派生類中,繼承而來的基類的公有成員依然是public。
(2)保護繼承;protected
- 基類的私有成員在派生類中不能直接訪問。
- 基類的保護成員只能在派生類內部訪問,不能在派生類外部訪問, 在派生類中,繼承而來的基類保護成員依然是protected。
- 基類的公有成員在派生類內部可以被訪問得到,在派生類中,繼承而來的基類的公有成員變成了是protected。
(3)私有繼承;private
- 基類的私有成員在派生類中不能直接訪問。
- 基類的保護成員只能在派生類內部訪問,不能在派生類外部訪問, 在派生類中,繼承而來的基類保護成員是private。
- 基類的公有成員在派生類內部可以被訪問得到,在派生類中,繼承而來的基類的公有成員變成了private。
注:基類的私有成員在派生類中時存在的,但是不能在派生類中直接訪問,即無論通過何種方式繼承,都無法在派生類內部直接訪問繼承自基類的私有成員。只能通過基類中的公共函數,來訪問基類的私有成員。絕大多數情況下的繼承是公有繼承。
菱形繼承:?
C在繼承了B1類和B2類之后對于B1和B2中同樣來自于A類的數據就會造成訪問二義性問題。?會造成數據冗余。來自A的數據有兩份。
解決辦法:使用虛繼承
派生類訪問間接基類的數據時,實際上訪問的是該類對象中的虛基表指針,通過虛基表指針訪問到了虛基表,而虛基表中存儲的內容是當前虛基表指針位置到間接基類成員的地址偏移量。那么這樣子就能夠在使用派生類訪問間接基類成員時,通過偏移量直接找到繼承而來的間接基類的成員。所以在內存中只用保留一份間接基類的成員就行 。
同名隱藏?
含義:
同名隱藏指在繼承關系里,當派生類定義了和基類中同名的成員(包含成員變量和成員函數)時,基類的同名成員會被派生類的成員隱藏。這意味著在派生類的作用域內,若直接使用該成員名,默認訪問的是派生類的成員,基類的同名成員就好像 “被隱藏” 了,若要訪問基類的同名成員,需要使用作用域解析運算符(
::
)。產生原因:
這種機制源于 C++ 等語言在處理繼承時的名稱查找規則。當在派生類中使用一個名稱時,編譯器會先在派生類的作用域內查找該名稱,若找到就使用該名稱對應的成員,不再去基類的作用域中查找;若在派生類的作用域內沒找到,才會去基類的作用域中查找。
?同名覆蓋?(函數重寫)
在面向對象編程中,同名覆蓋(也常被稱為函數重寫,Override)是一種重要的多態性機制,主要發生在具有繼承關系的類之間。以下是關于它的詳細介紹:
定義
當派生類中定義了一個與基類中虛函數具有相同簽名(函數名、參數列表、返回值類型)的函數時,就發生了同名覆蓋。此時,派生類的對象在調用該函數時,會執行派生類中重寫的版本,而不是基類中的版本。
作用
同名覆蓋是實現多態性的關鍵手段之一。通過它,我們可以在不修改基類代碼的情況下,在派生類中根據具體需求對基類的虛函數進行重新定義,從而實現不同的行為。這樣,當使用基類指針或引用指向不同的派生類對象時,調用相同的函數名可以產生不同的效果,提高了代碼的可擴展性和可維護性。
?3、多態
(1)多態的分類
?????編譯時多態,在程序編譯時確定同名操作和具體的操作對象。(早期綁定)
? ? ? ? ? ? ? ?強制多態—強制類型轉換
? ? ? ? ? ? ? ?重載多態—函數重載和運算符重載
? ? ? ? ? ? ? ?參數化多態—類模板及函數模板
?????運行時多態,在程序運行時才會確定同名操作和具體的操作對象。通過類繼承關系和虛函數來實現。
? ? ? ? ? ? ? ? ? 包含多態—虛函數重寫
虛函數的重寫:三同:函數名、返回類型、參數列表
(2)虛表:
在 C++ 里,只要類包含虛函數,編譯器就會為該類創建一個虛表(Virtual Table,簡稱 VTable)。虛表本質上是一個存儲類的虛函數地址的指針數組,這個數組的首元素上存儲RTTI(運行時類型識別信息的指針),從數組下標0開始依次存儲虛函數地址。最后面放了一個nullptr。類的每個對象中都有一個指向該類虛表的指針(虛表指針,vptr)。
指針數組是指一個數組,其元素的類型為指針。也就是說,指針數組中的每個元素都存儲著一個內存地址
虛函數地址表在 .data 區。
運行時多態:必須用指針或引用調用虛函數,對象.虛函數,這是編譯時,不是運行時多態。?
(3)代碼示例?
#include<stdio.h>
#include<iostream>
#include <cassert>
using namespace std;class Object
{
private:int value;
public:Object(int x = 0) :value(x){}~Object(){}virtual void add() { cout << "Object::add" << endl; }virtual void func() { cout << "Object::func" << endl; }virtual void print()const { cout << "Object::printf" << endl; }
};class Base :public Object
{
private:int num;
public:Base(int x=0):Object(x),num(x+10){}//重寫虛函數virtual void add() { cout << "Base::add" << endl; }virtual void func() { cout << "Base::func" << endl; }virtual void show() { cout << "Base::show" << endl; }};class Test :public Base
{
private:int count;
public:Test(int x=0):Base(x),count(x+10){}virtual void add() { cout << "Test::add" << endl; }virtual void show() { cout << "Test::show" << endl; }virtual void print()const { cout << "Test::printf" << endl; }};void funcPobj(Object* pobj)
{assert(pobj != nullptr);pobj->add();pobj->func();pobj->print();
}
int main()
{Test test(10);funcPobj(&test);return 0;
}
以上代碼,在內存中的虛表大致如下:
sizeof(Object):8;int+一個指向虛表的指針(32位操作系統)?
指針:?
通過虛表指針,訪問Test類的虛表
引用實現:
(1)派生類對象可以給基類,但基類不能給派生類。?
(2)強制類型轉換后,查的仍然是Base的虛表:
?(3)定義obj類型的對象,訪問的仍是Obj的虛表,
訪問obj的虛表,obj中沒有派生類的show方法,執行到“000000”報錯。這種強轉可以理解為:無效的。
(Base*) & obj
?和?(Test*) & obj
)只是簡單地改變了指針的類型,而不會改變對象本身的實際類型。obj
?實際上是?Object
?類型的對象,盡管你把它的指針強制轉換為?Base*
?或?Test*
?類型,但對象的內存布局和實際類型依舊是?Object
。
- 虛函數調用:
Base
?和?Test
?類繼承自?Object
?類,并且有各自的虛表。當你把?Object
?類型的指針強制轉換為?Base*
?或?Test*
?類型并調用虛函數時,程序會依據轉換后的指針類型去訪問相應的虛表。然而,obj
?實際上是?Object
?類型的對象,它只有?Object
?類的虛表,這就會導致程序訪問錯誤的虛表,從而引發未定義行為。- 成員訪問:
Base
?和?Test
?類可能包含?Object
?類沒有的成員變量和成員函數。當你通過強制轉換后的指針訪問這些額外的成員時,程序會嘗試訪問不存在的內存位置,這也會導致未定義行為。
?(4)繼承關系中,動態創建派生類對象,但是是拿基類對象指向的,Object * op = new Base();在delete *op時,調用派生類的析構函數,解決辦法是將基類的析構函數設為虛函數,之后,就可以先調用~Base();再調基類的析構。
在繼承關系里,當使用基類指針指向動態創建的派生類對象,并且基類的析構函數不是虛函數時,在執行?
delete
?操作時只會調用基類的析構函數,這可能會造成派生類對象的部分資源無法正確釋放,進而引發內存泄漏等問題。當基類的析構函數不是虛函數時,
delete
?操作依據指針的靜態類型來決定調用哪個析構函數。由于指針類型是基類指針,所以只會調用基類的析構函數,派生類的析構函數不會被調用。?
?
基類析構不是虛函數示例代碼如下:?
#include <iostream>class Object {
public:~Object() {std::cout << "Object::~Object()" << std::endl;}
};class Base : public Object {
public:~Base() {std::cout << "Base::~Base()" << std::endl;}
};int main() {Object* op = new Base();delete op; return 0;
}
當把基類的析構函數設為虛函數后,
delete
?操作會依據對象的實際類型來決定調用哪個析構函數。因為對象的實際類型是派生類,所以會先調用派生類的析構函數,然后再調用基類的析構函數。
#include <iostream>class Object {
public:virtual ~Object() {std::cout << "Object::~Object()" << std::endl;}
};class Base : public Object {
public:~Base() {std::cout << "Base::~Base()" << std::endl;}
};int main() {Object* op = new Base();delete op; return 0;
}
?Base::~Base()
Object::~Object()
原理分析
- 虛表機制:當基類的析構函數被聲明為虛函數時,編譯器會為基類和派生類分別創建虛表。在對象的內存布局中,會有一個虛表指針指向對應的虛表。當執行?
delete
?操作時,程序會通過對象的虛表指針找到對應的虛表,然后從虛表中獲取析構函數的地址并調用。由于對象的實際類型是派生類,所以會先調用派生類的析構函數。- 析構順序:在 C++ 里,析構函數的調用順序與構造函數的調用順序相反。當創建派生類對象時,會先調用基類的構造函數,再調用派生類的構造函數;而在銷毀對象時,會先調用派生類的析構函數,再調用基類的析構函數,以此確保對象的資源能夠被正確釋放。
(5)運行時多態是怎么實現的??
運行時多態主要基于繼承和虛函數實現。當基類指針或引用指向派生類對象時,通過該指針或引用調用虛函數,程序會在運行時根據對象的實際類型來決定調用哪個類的虛函數,從而實現不同的行為。
用一個指針指向一個對象,調用函數的時候,指向對象虛表的地址給edx,調用第幾個函數就(edx+偏移量 4n)?
運行時多態怎么實現的(匯編)?例:
#include <iostream>class Base {
public:virtual void func1() {std::cout << "Base::func1()" << std::endl;}virtual void func2() {std::cout << "Base::func2()" << std::endl;}
};class Derived : public Base {
public:void func1() override {std::cout << "Derived::func1()" << std::endl;}void func2() override {std::cout << "Derived::func2()" << std::endl;}
};int main() {Base* ptr = new Derived();ptr->func1();ptr->func2();delete ptr;return 0;
}
當創建?
Derived
?類的對象并讓?Base
?類型的指針?ptr
?指向它時,Derived
?對象的內存布局起始位置會有一個虛表指針,該指針指向?Derived
?類的虛表。函數調用過程
- 獲取虛表指針:當執行?
ptr->func1()
?時,程序首先通過?ptr
?指針找到對象的內存地址,進而獲取對象的虛表指針,通常會把這個虛表指針的值存到某個寄存器(如你所說的?edx
)中。- 計算函數地址:虛表本質是一個存儲函數指針的數組,每個函數指針在虛表中按聲明順序排列,且每個指針占一定字節數(在 32 位系統中一般是 4 字節,64 位系統中是 8 字節)。要調用第?
n
?個虛函數,就需要在虛表指針的基礎上加上偏移量?4n
(32 位系統)或?8n
(64 位系統)來獲取該函數的地址。例如,調用?func1()
?時,偏移量為 0;調用?func2()
?時,偏移量為 4(32 位)或 8(64 位)。- 調用函數:獲取到函數地址后,程序就會跳轉到該地址處執行相應的函數代碼。
4、靜態聯編和動態聯編
靜態聯編:在編譯和鏈接階段,就將函數實現和函數調用關聯起來。
C語言中,所有的聯編都是靜態聯編。
C++語言中,函數重載和函數模版也是靜態聯編。
C++中,對象.成員運算符,去調用對象虛函數,也是靜態聯編。
動態聯編:程序執行的時候才將函數實現和函數調用關聯起來。
C++中,使用引用、指針->,則程序在運行時選擇虛函數的過程稱為動態聯編。
5、例題:memset對vptr的影響:
class Object
{
private:int value;
public:Object(int x = 0) :value(x){memset(this, 0, sizeof(Object));}void func(){ cout << "func" << endl; }virtual void add(int x) { cout << "obj add" << endl;}
};
int main()
{Object obj;Object* op = &obj;obj.add(1); //靜態聯編op->add(2); //報錯
}
??? ?op->add(2);編譯會報錯
原因:
1.?
memset
?對虛表指針的影響在 C++ 里,要是一個類包含虛函數,編譯器會為這個類創建虛表,并且在類的每個對象里插入一個虛表指針(
vptr
),此指針一般處于對象內存布局的起始位置。memset(this, 0, sizeof(Object));
?這個操作會把對象的整個內存區域都置為 0,這就包含了虛表指針。一旦虛表指針被置為 0,就無法正確指向對應的虛表。2. 虛函數調用機制
當借助基類指針(這里是?
op
)調用虛函數(像?op->add(2);
)時,程序會通過對象的虛表指針找到對應的虛表,再從虛表中獲取該虛函數的地址,最后調用這個函數。但由于虛表指針被?memset
?置為 0 了,程序就無法找到正確的虛表,從而引發運行時錯誤。3. 直接對象調用和指針調用的區別
obj.add(1);
:這是直接通過對象調用虛函數。在這種情形下,編譯器能夠在編譯時就確定要調用的函數,所以不會借助虛表指針,也就不會受到?memset
?操作的影響。op->add(2);
:這是通過指針調用虛函數,需要在運行時依靠虛表指針來確定要調用的函數。由于虛表指針被置為 0,程序就無法找到正確的虛表,進而導致運行時錯誤。
6、例題:
class Object
{
private:int value;
public:Object(int x = 0) :value(x){}void print() {cout << "obj::print" << endl;add(1);}virtual void add(int x) { cout << "obj::add"<<x << endl;}
};
class Base :public Object {
private:int num;
public :Base(int x = 0) :Object(x + 10), num(x){}void show() { cout << "Base::show" << endl; print(); //this->print();}virtual void add(int x) { cout << "base::add" << x << endl; }
};
int main()
{Base base;base.show();return 0;
}
?
類的成員函數在調用數據時有this,?
調用過程分析
main
?函數中調用?base.show()
:創建了一個?Base
?類的對象?base
,然后調用其?show
?方法。Base::show
?方法中調用?Base::show
?方法里調用了?Base
?類沒有重寫?Object
?的?this
?指針指向的是?Base
?類的對象?base
。Object::print
?方法中調用?add
?方法:在?Object::print
?方法中調用了?add(1)
。因為?add
?方法在?Object
?類中被聲明為虛函數(virtual void add(int x)
),并且?Base
?類重寫了該虛函數,所以在運行時會根據?this
?指針所指向對象的實際類型來決定調用哪個?add
?方法。由于?this
?指針指向的是?Base
?類的對象?base
,所以會調用?Base
?類中重寫的?add
?方法。
?如果在構造、析構函數里調用虛函數,調用誰的?答:調用自身類型的。不會查虛表。
7、動態+靜態聯編例題:
class Object
{
private:int value;
public:virtual void func(int a=10) { cout << "obj::func: a"<<a << endl; }
};
class Base :public Object {private:virtual void func(int b = 20) { cout << "Base::func: b"<<b << endl; }
};
int main()
{Base base;Object* op = &base;op->func();return 0;
}
1. 虛函數調用機制
在 C++ 里,當使用基類指針(如?
Object* op
)指向派生類對象(如?Base base
),并且通過該指針調用虛函數(如?op->func()
)時,會在運行時依據對象的實際類型來決定調用哪個類的函數版本。由于?op
?指向的是?Base
?類的對象?base
,所以會調用?Base
?類中重寫的?func
?函數。2. 默認參數的綁定規則
默認參數是在編譯時確定的,而不是運行時。當調用?
op->func()
?時,編譯器會查看指針的靜態類型(也就是?Object*
)來確定默認參數的值。在?Object
?類中,func
?函數的默認數?a
?被設定為 10,所以在調用?func
?函數時,默認參數的值會使用?Object
?類中定義的 10,而非?Base
?類中定義的 20。3. 總結
結合虛函數調用機制和默認參數的綁定規則,
op->func()
?會調用?Base
?類的?func
?函數,不過默認參數會使用?Object
?類中定義的 10,因此輸出結果為?Base::func: b 10
。
C++中,構造函數不能為虛。
構造函數的任務:設置虛表指針。?
構造函數的主要作用是初始化對象的成員變量,為對象分配內存并設置初始狀態。在創建對象時,編譯器已經明確知道要創建的對象類型,因此可以直接調用相應的構造函數,不需要通過虛函數機制在運行時動態確定。
構造函數執行時,對象還未完全創建好,虛表指針可能還未被正確初始化。如果構造函數是虛函數,就需要通過虛表指針來調用它,但此時虛表指針可能還沒指向正確的虛表,這會導致無法正確調用構造函數。