目錄
一、繼承的概念
1.繼承的基本概念
2.繼承的定義和語法
3.繼承基類成員訪問方式的變化
?編輯?4.總結
二、基類和派生類對象賦值轉換
三、繼承中的作用域
四、派生類的默認成員函數
1.派生類中的默認構造函數
2.派生類中的拷貝構造函數
3.派生類中的移動構造函數
4.派生類的拷貝賦值運算符
?5.派生類的移動賦值運算符
6.派生類的析構函數
為什么基類析構函數需要virtual關鍵字修飾?
理由:多態性和正確的析構順序
問題:非虛析構函數導致的資源泄漏
總結
五、繼承和友元
六、繼承與靜態成員
?七、復雜的菱形繼承和菱形虛擬繼承
1.單繼承
2.多繼承
3.菱形繼承
?菱形繼承的問題
4.菱形虛擬繼承
5.虛擬繼承解決數據冗余和二義性的原理
虛基表的工作機制
一、繼承的概念
在C++中,繼承是一種面向對象編程的重要特性,它允許一個類(稱為派生類或子類)從另一個類(稱為基類或父類)繼承屬性和行為(成員變量和成員函數)。通過繼承,派生類不僅可以擁有基類的所有成員,還可以擴展或修改這些成員以提供更具體或特殊的功能。
1.繼承的基本概念
- 基類(Base Class):提供基礎屬性和行為的類。
- 派生類(Derived Class):從基類繼承并擴展或修改其功能的類。
- 訪問控制(Access Control):
- Public 繼承:基類的public和protected成員在派生類中保持其訪問級別不變,public成員依然是public,protected成員依然是protected。
- Protected 繼承:基類的public和protected成員在派生類中都變為protected成員。
- Private 繼承:基類的public和protected成員在派生類中都變為private成員。
- 構造函數和析構函數:派生類的構造函數在執行前會先調用基類的構造函數,析構函數的調用順序則相反,先調用派生類的析構函數,再調用基類的析構函數。
- 多重繼承(Multiple Inheritance):C++允許一個派生類從多個基類繼承。
2.繼承的定義和語法
class Base {
public:int baseValue;void baseFunction() {// 基類成員函數}
};class Derived : public Base {
public:int derivedValue;void derivedFunction() {// 派生類成員函數}
};
3.繼承基類成員訪問方式的變化
?4.總結
- 基類private成員在派生類中無論以什么方式繼承都是不可見的。這里的不可見是指基類的私有成員還是被繼承到了派生類對象中,但是語法上限制派生類對象不管是在類內還是在類外都不能去訪問它。
- 基類private成員在派生類中是不能被訪問的,如果基類成員不想在類外直接被訪問,但需要在派生類中能訪問,就定義為protected。可以看出保護成員限定符是繼承才出現的。
- 實際上面的表格我們進行一下總結就能發現,基類的私有成員在子類中都是不可見的。基類的其他成員在子類的訪問方式 == Min(成員在基類的訪問限定符,繼承方式),public > protected > private。
- 使用關鍵字class時默認的繼承方式是private,使用struct時默認的繼承方式是public,不過最好顯示的寫出繼承方式。
- 在實際運用中一般都是使用public繼承,幾乎很少使用protected/private繼承,也不提倡使用protected/private繼承,因為?protected/private繼承下來的成員都只能在派生類的類里使用,實際中擴展維護性不強。
二、基類和派生類對象賦值轉換
- 派生類對象 可以賦值給 基類的對象 / 基類的指針 / 基類的引用。這里有個形象的說法叫切片或者切割。寓意把派生類中父類那部分切來賦值過去。
- 基類對象不能賦值給派生類對象。
- 基類的指針或者引用可以通過強制類型轉換賦值給派生類的指針或者引用。但是必須是基類的指針是指向派生類對象時才是安全的。
class Base {
public:int baseValue;virtual void display() {std::cout << "Base class" << std::endl;}
};class Derived : public Base {
public:int derivedValue;void display() override {std::cout << "Derived class" << std::endl;}
};Base baseObj;
Derived derivedObj;baseObj = derivedObj; // 對象切割發生
baseObj.display(); // 輸出 "Base class"
在上面的例子中,盡管derivedObj賦值給了baseObj,但baseObj只保留了Base類的部分,派生類的derivedValue被切割掉了,調用display函數時也只會調用基類的版本。
此外,C++允許使用基類的指針或引用來指向派生類對象,這可以實現多態性。多態性允許你通過基類接口調用派生類的重載函數。
Base* basePtr = &derivedObj;
basePtr->display(); // 輸出 "Derived class"(多態性)Base& baseRef = derivedObj;
baseRef.display(); // 輸出 "Derived class"(多態性)
?在上面代碼中,basePtr和baseRef都指向Derived對象,并且調用display方法時,會調用派生類Derived中的版本,這是因為display函數被聲明為virtual。(virtual關鍵字我們下面會講)
另外還有類型轉換:static_cast和dynamic_cast,感興趣的可以去了解下。
三、繼承中的作用域
1. 在繼承體系中 基類 和 派生類 都有 獨立的作用域 。2. 子類和父類中有同名成員, 子類成員將屏蔽父類對同名成員的直接訪問,這種情況叫隱藏,也叫重定義。 (在子類成員函數中,可以 使用 基類 :: 基類成員 顯示訪問 )3. 需要注意的是如果是成員函數的隱藏,只需要函數名相同就構成隱藏。4. 注意在實際中在 繼承體系里 面最好 不要定義同名的成員 。
// Student的_num和Person的_num構成隱藏關系,可以看出這樣代碼雖然能跑,但是非常容易混淆
class Person
{
protected :string _name = "小李子"; // 姓名int _num = 111; ? // 身份證號
};
class Student : public Person
{
public:void Print(){cout<<" 姓名:"<<_name<< endl;cout<<" 身份證號:"<<Person::_num<< endl;cout<<" 學號:"<<_num<<endl;}
protected:int _num = 999; // 學號
};
void Test()
{Student s1;s1.Print();
};
// B中的fun和A中的fun不是構成重載,因為不是在同一作用域
// B中的fun和A中的fun構成隱藏,成員函數滿足函數名相同就構成隱藏。
class A
{
public:void fun(){cout << "func()" << endl;}
};
class B : public A
{
public:void fun(int i){A::fun();cout << "func(int i)->" <<i<<endl;}
};
void Test()
{B b;b.fun(10);
};
四、派生類的默認成員函數
在之前的學習中, 我們知道類可以自動生成一些默認的成員函數,這些成員函數包括默認構造函數、拷貝構造函數、移動構造函數、拷貝賦值運算符、移動賦值運算符和析構函數。而對于派生類,這些默認成員函數的生成和行為有一些特殊的規則和注意事項,下面我講詳細介紹。
1.派生類中的默認構造函數
默認構造函數在沒有用戶定義的構造函數時自動生成。對于派生類的默認構造函數,它會調用基類的默認構造函數(如果存在),然后初始化派生類的成員。而如果基類沒有默認構造函數,則必須在派生類構造函數的初始化列表階段顯示調用。
class Base {
public:Base() {std::cout << "Base default constructor" << std::endl;}
};class Derived : public Base {
public:Derived() {std::cout << "Derived default constructor" << std::endl;}
};int main() {Derived d; // 輸出:Base default constructor// Derived default constructorreturn 0;
}
2.派生類中的拷貝構造函數
拷貝構造函數在沒有用戶定義的情況下自動生成,用于創建類的對象副本。派生類的拷貝構造函數會首先調用基類的拷貝構造函數,然后復制派生類的成員。
class Base {
public:Base(const Base&) {std::cout << "Base copy constructor" << std::endl;}
};class Derived : public Base {
public:Derived(const Derived& other) : Base(other) {std::cout << "Derived copy constructor" << std::endl;}
};int main() {Derived d1;Derived d2 = d1; // 輸出:Base copy constructor// Derived copy constructorreturn 0;
}
3.派生類中的移動構造函數
移動構造函數在沒有用戶定義的情況下自動生成,用于移動資源所有權。派生類的移動構造函數會首先調用基類的移動構造函數,然后移動派生類的成員。
class Base {
public:Base(Base&&) noexcept {std::cout << "Base move constructor" << std::endl;}
};class Derived : public Base {
public:Derived(Derived&& other) noexcept : Base(std::move(other)) {std::cout << "Derived move constructor" << std::endl;}
};int main() {Derived d1;Derived d2 = std::move(d1); // 輸出:Base move constructor// Derived move constructorreturn 0;
}
4.派生類的拷貝賦值運算符
拷貝賦值運算符在沒有用戶定義的情況下自動生成,用于將一個對象的內容賦值給另一個對象。派生類的拷貝賦值運算符會首先調用基類的拷貝賦值運算符,然后賦值派生類的成員。
class Base {
public:Base& operator=(const Base&) {std::cout << "Base copy assignment operator" << std::endl;return *this;}
};class Derived : public Base {
public:Derived& operator=(const Derived& other) {Base::operator=(other);std::cout << "Derived copy assignment operator" << std::endl;return *this;}
};int main() {Derived d1, d2;d1 = d2; // 輸出:Base copy assignment operator// Derived copy assignment operatorreturn 0;
}
?5.派生類的移動賦值運算符
移動賦值運算符在沒有用戶定義的情況下自動生成,用于將一個對象的內容移動到另一個對象。派生類的移動賦值運算符會首先調用基類的移動賦值運算符,然后移動派生類的成員。
class Base {
public:Base& operator=(Base&&) noexcept {std::cout << "Base move assignment operator" << std::endl;return *this;}
};class Derived : public Base {
public:Derived& operator=(Derived&& other) noexcept {Base::operator=(std::move(other));std::cout << "Derived move assignment operator" << std::endl;return *this;}
};int main() {Derived d1, d2;d1 = std::move(d2); // 輸出:Base move assignment operator// Derived move assignment operatorreturn 0;
}
6.派生類的析構函數
析構函數在沒有用戶定義的情況下自動生成,用于清理對象。派生類的析構函數會首先調用派生類的析構函數,然后調用基類的析構函數。
class Base {
public:virtual ~Base() {std::cout << "Base destructor" << std::endl;}
};class Derived : public Base {
public:~Derived() {std::cout << "Derived destructor" << std::endl;}
};int main() {Base* b = new Derived();delete b; // 輸出:Derived destructor// Base destructorreturn 0;
}
為什么基類析構函數需要virtual關鍵字修飾?
基類的析構函數需要加virtual關鍵字是為了確保在刪除派生類對象時能夠正確調用析構函數。這是一個非常重要的概念,尤其是在使用多態性和通過基類指針或引用操作派生類對象時。
理由:多態性和正確的析構順序
class Base {
public:virtual ~Base() {std::cout << "Base destructor" << std::endl;}
};class Derived : public Base {
public:~Derived() {std::cout << "Derived destructor" << std::endl;}
};
當基類的析構函數是虛函數時,通過基類指針刪除派生類對象時,C++會首先調用派生類的析構函數,然后再調用基類的析構函數。這確保了派生類中分配的資源可以先被正確釋放,再釋放基類中分配的資源。
int main() {Base* b = new Derived();delete b; // 輸出順序:Derived destructor// Base destructorreturn 0;
}
問題:非虛析構函數導致的資源泄漏
如果基類的析構函數不是虛函數,則通過基類指針刪除派生類對象時,只會調用基類的析構函數,而不會調用派生類的析構函數。這會導致派生類中的資源沒有被正確釋放,造成資源泄漏。
class Base {
public:~Base() {std::cout << "Base destructor" << std::endl;}
};class Derived : public Base {
public:~Derived() {std::cout << "Derived destructor" << std::endl;}
};int main() {Base* b = new Derived();delete b; // 只輸出:Base destructorreturn 0;
}
在上述代碼中,由于 Base
類的析構函數不是虛函數,刪除 b
時只調用了 Base
的析構函數,Derived
類的析構函數沒有被調用,這會導致 Derived
類中的資源沒有被正確釋放。
總結
為了確保在使用多態性時派生類對象可以被正確地銷毀,避免資源泄漏,基類的析構函數應該聲明為虛函數。這一做法可以保證刪除派生類對象時,派生類和基類的析構函數都能被正確調用。以下是總結的要點:
- 多態性支持:使用基類指針或引用操作派生類對象時,確保正確調用派生類的析構函數。
- 正確的析構順序:先調用派生類的析構函數,再調用基類的析構函數,確保資源正確釋放。
- 避免資源泄漏:防止派生類中的資源沒有被釋放,導致內存泄漏或其他資源泄漏。
五、繼承和友元
友元關系是單向的和局部的,友元關系不能繼承!也就是說基類友元不能訪問子類私有和保護成員。
#include <iostream>// 基類
class Base {
private:int basePrivateVar;
protected:int baseProtectedVar;
public:int basePublicVar;Base() : basePrivateVar(1), baseProtectedVar(2), basePublicVar(3) {}friend void baseFriendFunction(Base &obj);
};// 基類的友元函數
void baseFriendFunction(Base &obj) {std::cout << "Base Private Var: " << obj.basePrivateVar << std::endl;std::cout << "Base Protected Var: " << obj.baseProtectedVar << std::endl;
}// 派生類
class Derived : public Base {
private:int derivedPrivateVar;
protected:int derivedProtectedVar;
public:int derivedPublicVar;Derived() : derivedPrivateVar(4), derivedProtectedVar(5), derivedPublicVar(6) {}friend void derivedFriendFunction(Derived &obj);
};// 派生類的友元函數
void derivedFriendFunction(Derived &obj) {// 基類的友元不能訪問派生類的私有或保護成員// std::cout << "Derived Private Var: " << obj.derivedPrivateVar << std::endl; // 錯誤// std::cout << "Derived Protected Var: " << obj.derivedProtectedVar << std::endl; // 錯誤std::cout << "Derived Public Var: " << obj.derivedPublicVar << std::endl;
}int main() {Base baseObj;Derived derivedObj;baseFriendFunction(baseObj); // 可以訪問Base類的私有和保護成員derivedFriendFunction(derivedObj); // 可以訪問Derived類的公共成員// 基類的友元函數不能訪問派生類的私有和保護成員// baseFriendFunction(derivedObj); // 錯誤return 0;
}
六、繼承與靜態成員
基類定義了static成員,則整個繼承體系里面只有一個這樣的成員。無論派生出多少個子類,都只有一個static成員實例。
靜態成員變量需要在類外進行定義和初始化。靜態成員函數則不需要在類外定義。
#include <iostream>// 基類
class Base {
public:static int staticVar; // 聲明靜態成員變量static void staticFunction() { // 聲明并定義靜態成員函數std::cout << "Static Function in Base" << std::endl;}
};// 定義靜態成員變量
int Base::staticVar = 10;// 派生類
class Derived : public Base {
public:void display() {std::cout << "Base staticVar: " << staticVar << std::endl; // 訪問基類的靜態成員變量staticFunction(); // 調用基類的靜態成員函數}
};int main() {Derived obj;obj.display();// 靜態成員可以通過類名直接訪問Base::staticVar = 20;Derived::staticVar = 30;std::cout << "Base staticVar after modification: " << Base::staticVar << std::endl;std::cout << "Derived staticVar after modification: " << Derived::staticVar << std::endl;return 0;
}
靜態成員的特點
- 類共享性:所有類的對象共享同一個靜態成員變量。
- 類作用域:靜態成員變量和靜態成員函數在類作用域內,但可以通過類名直接訪問。
- 內存管理:靜態成員變量在程序啟動時分配內存,程序結束時釋放內存。
注意:
- 靜態成員函數:靜態成員函數不能訪問非靜態成員變量和非靜態成員函數,因為它們屬于類本身,而不是類的某個對象。但是靜態成員函數可以訪問靜態成員變量和其他靜態成員函數。
?七、復雜的菱形繼承和菱形虛擬繼承
1.單繼承
一個子類只有一個直接父類時稱這個繼承關系為單繼承。
2.多繼承
一個子類有兩個或以上直接父類時稱這個繼承關系為多繼承
3.菱形繼承
菱形繼承(也稱鉆石繼承)是指一種特殊的多繼承情況,其中一個類從兩個基類繼承,而這兩個基類又繼承自同一個祖先類。這種繼承關系形成了一個菱形結構。
?菱形繼承的問題
?從上面的對象成員模型構造,可以看出菱形繼承有數據冗余和二義性的問題。在Assistant的對象中Person成員會有兩份。
#include <iostream>// 祖先類
class A {
public:int value;A() : value(0) {}
};// 兩個派生類繼承自 A
class B : public A {};
class C : public A {};// 派生類 D 同時繼承自 B 和 C
class D : public B, public C {};int main() {D obj;// obj.value; // 錯誤:二義性問題,不知道是從 B 繼承的 A 還是從 C 繼承的 A// 解決方法之一是明確指定路徑obj.B::value = 1;obj.C::value = 2;std::cout << "obj.B::value: " << obj.B::value << std::endl;std::cout << "obj.C::value: " << obj.C::value << std::endl;return 0;
}
4.菱形虛擬繼承
為了解決上面菱形繼承所帶來的問題,我們可以使用虛擬繼承。虛擬繼承確保在菱形繼承結構中只存在一個基類的實例。
如在上面的代碼中,我們可以在B和C繼承A的時候使用虛擬繼承,即
class B : virtual public A {};
class C : virtual public A {};
需要注意的是,虛擬繼承不要在其他地方去使用。
5.虛擬繼承解決數據冗余和二義性的原理
class A
{
public:int _a;
};
// class B : public A
class B : virtual public A
{
public:int _b;
};
// class C : public A
class C : virtual public A
{
public:int _c;
};
class D : public B, public C
{
public:int _d;
};
int main()
{D d;d.B::_a = 1;d.C::_a = 2;d._b = 3;d._c = 4;d._d = 5;return 0;
}
下面是菱形繼承的內存對象成員模型:這里可以看到數據冗余
下面是菱形虛擬繼承的內存對象成員模型:
?
這里可以分析出D對象中將A放到了D對象組成的最下面,這個A同時屬于B和C,那么B和C如何去找到公共的A呢?
這里是通過了B和C的兩個指針,指向的一張表。這兩個指針叫虛基表指針,這兩個表叫虛基表。虛基表中存儲的是偏移量,通過偏移量就能找到A?。
總結:
虛擬繼承通過確保每個虛擬基類在派生類中只有一個共享實例,從而避免了重復實例化和二義性問題。為了實現這一點,編譯器會使用虛基表來跟蹤和管理虛擬基類的實例。
虛基表的工作機制
虛基表的引入: 每個使用虛擬繼承的類會包含一個虛基表指針。這個指針指向一個虛基表,該表包含虛擬基類的指針。
共享基類實例: 在派生類(如
D
)的對象中,虛基表指針確保所有虛擬基類實例都指向同一個實際基類實例。這意味著D
中只有一個A
類的實例。成員訪問的重定向: 在訪問基類成員時,編譯器使用虛基表來正確地定位基類成員,確保訪問的是唯一的基類實例。
所以,當一個類虛擬繼承另一個類時,編譯器在對象布局中插入一個虛基表指針(vbptr)。這個指針指向一個虛基表(vbtbl),而虛基表中包含指向虛擬基類的偏移量或地址。通過這種方式,每個派生類能夠正確地定位并訪問唯一的虛擬基類實例。
上面就是我們對C++繼承的全部理解了~