歡迎來到ZyyOvO的博客?,一個關于探索技術的角落,記錄學習的點滴📖,分享實用的技巧🛠?,偶爾還有一些奇思妙想💡
本文由ZyyOvO原創??,感謝支持??!請尊重原創📩!歡迎評論區留言交流🌟
個人主頁 👉 ZyyOvO
本文專欄??C++ 進階之路
繼承中的“明明德”與“止于至善”
- 繼承
- 繼承的概念
- 基本語法
- 繼承類模板
- 基類和派生類的轉換
- 內存布局與繼承的關系
- 向上轉型
- 向下轉型
- 繼承中的作用域
- 作用域嵌套規則
- 隱藏規則
- 多層繼承的作用域鏈
- 派生類的默認成員函數
- 默認構造函數
- 拷貝構造函數
- 拷貝賦值運算符
- 析構函數
- 繼承和友元
- 繼承和靜態成員
- 靜態成員的可見性
- 靜態數據成員的初始化
- 靜態成員函數與多態(TODO)
- 同名靜態成員的隱藏
- 多繼承及其菱形繼承
- 單繼承
- 多繼承
- 菱形繼承
- I0庫中的菱形虛擬繼承
- 繼承和組合
- 思考題
- 寫在最后
繼承
繼承的概念
繼承是面向對象編程(OOP)中的一個核心概念,它允許一個類(派生類或子類)繼承另一個類(基類或父類)的屬性和方法。
繼承的核心思想是代碼復用和創建類之間的層次關系。通過繼承,我們可以定義一個通用的基類,包含一些通用的屬性和方法,然后派生出更具體的子類,這些子類可以繼承基類的特性,并添加自己獨特的屬性和方法。
下面我們舉一個例子來幫助大家理解:
想象一下你正在管理一個動物園,里面有各種各樣的動物。這些動物有一些共同的特征和行為,比如它們都需要吃東西、睡覺,同時不同種類的動物又有各自獨特的行為,像鳥兒會飛翔,魚兒會游泳。
為了更有條理地管理這些動物信息,我們可以先把動物們的共同特征和行為總結出來,形成一個通用的描述,然后再針對每種動物的獨特之處進行單獨描述。
- 這個通用的描述包含所有動物的共同特點和行為,用一個類
Animal
來實現,我們把這個類成為基類
或父類
。
class Animal {
public:Animal(const std::string& n) : name(n) {}void eat() {std::cout << name << " is eating." << std::endl;}void sleep() {std::cout << name << " is sleeping." << std::endl;}
private:std::string name;
};
- 鳥類是一種動物,包含了動物的所有公共特點,所以我們可以用一個類
Brid
來實現對Animal
類的繼承,這個Brid
類就叫做派生類
或者子類
,代表是在基類的基礎上繼承而來的,同時也可以包含鳥類獨有的特點和行為,比如翅膀,飛翔。
class Bird : public Animal {
public:Bird(const std::string& n) : Animal(n) {}void fly() {std::cout << getName() << " is flying." << std::endl;}
};
- 魚類同樣是動物的一種,
Fish
類也可以繼承Animal
類。魚類有自己獨特的行為,比如游泳。
class Fish : public Animal {
public:Fish(const std::string& n) : Animal(n) {}void swim() {std::cout << getName() << " is swimming." << std::endl;}
};
基本語法
class DerivedClassName : access-specifier BaseClassName {// 派生類成員定義
};
派生類 (Derived Class
)
- 新定義的類,繼承自基類
- 可以添加新成員,修改或擴展基類功能
基類 (Base Class
)
- 被繼承的現有類,也稱為父類或超類
訪問說明符 (access-specifier
)
- 控制基類成員在派生類中的訪問權限
可選值:public、protected、private
默認值:
- 對于
class
定義的類:private
- 對于
struct
定義的類:public
訪問限定符說明:
類成員 \ 繼承方式 | public 繼承 | protected 繼承 | private 繼承 |
---|---|---|---|
基類的 public 成員 | 派生類的 public | 派生類的 protected | 派生類的 private |
基類的 protected 成員 | 派生類的 protected | 派生類的 protected | 派生類的 private |
基類的 private 成員 | 不可見 | 不可見 | 不可見 |
- 基類
private
成員在派生類中無論以什么方式繼承都是不可見的。這里的不可見是指基類的私有成員雖然被繼承到了派生類對象中,但是語法上限制派生類對象無論在類里面還是類外面都無法訪問它。 - 基類
private
成員在派生類中是不能被訪問,如果基類成員不想在類外直接被訪問,但需要在派生類中能訪問,就定義為protected
。可以看出保護成員限定符是因繼承才出現的。 - 基類的私有成員在派生類都是不可見。基類的其他成員在派生類的訪問方式 ==
Min(成員在基類的訪問限定符,繼承?式),public > protected >private
。
- 代碼示例:
基類:
class Base {
public:int publicVar; // 基類 public 成員
protected:int protectedVar; // 基類 protected 成員
private:int privateVar; // 基類 private 成員(所有繼承方式均不可訪問)
};
- 公有繼承
class PublicDerived : public Base {
public:void access() {publicVar = 1; // 繼承為 public → 類外可訪問protectedVar = 2; // 繼承為 protected → 僅派生類內部可訪問}
};
- 保護繼承
class ProtectedDerived : protected Base {
public:void access() {publicVar = 1; // 繼承為 protected → 僅派生類內部可訪問protectedVar = 2; // 繼承為 protected → 僅派生類內部可訪問}
};
- 私有繼承
class PrivateDerived : private Base {
public:void access() {publicVar = 1; // 繼承為 private → 僅派生類內部可訪問protectedVar = 2; // 繼承為 private → 僅派生類內部可訪問}
};
- 在實際運用中?般使用都是
public
繼承,幾乎很少使用protetced/private
繼承,也不提倡使用protetced/private
繼承,因為protetced/private
繼承下來的成員都只能在派生類的類里面使用,實際中擴展維護性不強。
繼承類模板
在面向對象編程中,“is - a” 關系指的是類的繼承關系,即一個類(派生類)是另一個類(基類)的特殊化 ;“has -a” 關系指的是一個類包含另一個類的對象作為成員變量,即聚合關系。
- 對于 stack和 vector 的關系既符合
is - a
,也符合has - a
vector 本質是 std
命名空間中實現的類模板:
template<class T>
class vector{};
- 我們可以基于 stack 和 vector 的
is - a
關系,讓 stack 通過繼承 vector 類模板來實現其功能。
namespace test{template <class T>class stack : public std::vector<T> {public:void push(const T &x) {std::vector<T>::push_back(x); //要指定類域//或者this->push_back(x);}void pop(){std::vector<T>::pop_back();}const T &top(){return std::vector<T>::back();}bool empty(){return std::vector<T>::empty();}};
}
注意:模板類繼承另一個模板類時,基類的成員函數需要通過作用域限定符或this
指針訪問
- 基類是類模板時,需要指定一下類域來調用其成員,否則編譯報錯:
error C3861: “push_back”: 找不到標識符
這里涉及到對編譯器對C++類模板的編譯編譯過程:
兩階段名稱查找(Two-phase name lookup
)
C++模板的編譯分為兩個階段:
- 模板定義階段:編譯器解析模板的非依賴型名稱(Non-dependent Names),解析模板本身的語法,檢查不依賴模板參數的名稱
- 模板實例化階段:解析依賴型名稱(Dependent Names,即與模板參數相關的名稱),生成具體類型代碼時,檢查依賴模板參數的名稱
對于繼承自模板基類的成員訪問,需要顯式指明來源:
- 因為 基類 std::vector 的類型依賴于模板參數
T
,其成員函數 push_back() 屬于依賴型名稱(Dependent name
),編譯器在模板定義階段無法確定這些成員是否存在。 - 因為 stack 實例化時,也實例化 vector 了,但由于模板是按需實例化,push_back等成員函數未實例化,所以編譯器找不到 push_back 成員函數。
另一種解決方案是利用 this->push_back
替代
- this 指針的作用機制:將成員訪問變為依賴型名稱
依賴型名稱的標記
this
的類型是Derived<T>*
,與模板參數T
相關- this->push_back() 成為依賴型表達式,編譯器推遲其名稱查找
當 stack<int>
被實例化時:
// 實例化后的代碼等價形式
class stack<int> : public vector<int> {
public:void push(const T &x) {this->push_back(x);此時vecotr<int>已完全實例化}
};
- 編譯器在實例化后的
vector<int>
中查找push_back()
- 通過 this 指針訪問成員,使得表達式成為類型依賴表達式,符合延遲查找規則。
測試:
int main(){test::stack<int> st;st.push(1);st.push(2);st.push(3);while (!st.empty()) {std::cout << st.top() << " ";st.pop();}std::cout << std::endl;return 0;
}
輸出:
3 2 1
基類和派生類的轉換
public 繼承的派生類對象 可以賦值給 基類的指針 /基類的引用。這里有個形象的說法叫切片。寓意把派生類中基類那部分切出來,基類指針或引用指向的是派生類中切出來的基類那部分。
- 基類對象不能賦值給派生類對象。
- 基類的指針或者引用可以通過強制類型轉換賦值給派生類的指針或者引用。但是必須是基類的指針是指向派生類對象時才是安全的。
為什么呢?下面為大家分析基類和派生類之間的轉換是如何進行的,以及底層的原理。
內存布局與繼承的關系
首先我們需要了解基類和派生類中的成員變量是如何在內存中存儲的
基類和派生類的內存結構
- 基類對象:僅包含基類定義的成員變量。
- 派生類對象:在內存中先存儲基類部分,再存儲派生類新增的成員變量。
關鍵點
- 派生類對象的起始地址就是基類部分的起始地址。
- 基類指針/引用可以直接指向派生類對象的基類部分,無需任何偏移計算。
向上轉型
定義
- 將派生類指針/引用隱式轉換為基類指針/引用。
特點:
- 隱式轉換:無需手動強制類型轉換,編譯器自動完成。
- 安全:因為派生類對象必然包含基類的所有成員,轉換不會丟失基類部分的數據。
安全性問題:
- 注意:向上轉型是安全的,指的是派生類的指針或者引用轉換成基類的指針或引用是安全的!
這里會涉及到一個陷阱:
- 如果通過值傳遞將派生類直接賦值給基類,派生類特有的成員變量會被丟棄,也就是"切片"
| 基類成員 | 派生類新增成員 | → 值傳遞后 → | 基類成員 |
下面我們舉個例子來讓大家理解值傳遞向上轉型的安全問題:
基類:
// 基類
class Animal {
public:int age = 0;virtual void speak() { // 虛函數cout << "Animal sound (age: " << age << ")" << endl;}
};
- 有關虛函數在多態章節會詳細介紹
派生類:
// 派生類
class Cat : public Animal {
public:int lives = 9; // 派生類特有成員void speak() override { // 覆蓋虛函數cout << "Meow (lives: " << lives << ", age: " << age << ")" << endl;}
};
值傳遞:
// 值傳遞函數:參數為基類對象
void processByValue(Animal animal) {animal.speak(); // 調用虛函數animal.age = 100; // 修改基類成員
}
- 發生對象切片:Cat對象被強制轉換為Animal基類對象,丟失派生類特有成員lives
- 虛函數調用:由于切片后對象類型為Animal,調用基類的speak()。
引用傳遞:
// 引用傳遞函數:參數為基類引用
void processByRef(Animal& animal) {animal.speak();animal.age = 200;
}
- 保持多態性:通過基類引用操作派生類對象
- 虛函數調用:動態綁定到Cat::speak()。
main函數:
int main() {Cat cat;cat.age = 3;cat.lives = 9;cout << "----- 值傳遞 -----" << endl;processByValue(cat); // 值傳遞觸發對象切片cout << "值傳遞后 cat 的 age: " << cat.age << endl; // age 未被修改cout << "值傳遞后 cat 的 lives: " << cat.lives << endl; // lives 保持原值cout << "\n----- 引用傳遞 -----" << endl;processByRef(cat); // 引用傳遞保持多態性cout << "引用傳遞后 cat 的 age: " << cat.age << endl; // age 被修改return 0;
}
運行結果:
Animal sound (age: 3)
值傳遞后 cat 的 age: 3
值傳遞后 cat 的 lives: 9----- 引用傳遞 -----
Meow (lives: 9, age: 3)
引用傳遞后 cat 的 age: 200
向下轉型
定義:
- 向下轉型是將基類的指針或引用轉換為派生類指針或引用的操作。
特點:
- 方向性:與自然的向上轉型(派生類→基類)相反,向下轉型是逆向操作。
- 顯式強制:必須通過
static_cast
或dynamic_cast
顯式轉換,無法隱式完成。
為什么需要向下轉型:
當基類指針/引用實際指向的是派生類對象時,若需要訪問派生類特有的成員(方法或屬性),必須通過向下轉型恢復其原始類型才能訪問。
class Animal {}; // 基類
class Cat : public Animal {
public: void meow() { /* 派生類特有方法 */ }
};Animal* animalPtr = new Cat(); // 基類指針指向派生類對象
animalPtr->meow(); // 錯誤!基類指針無法直接訪問派生類方法
此時必須通過向下轉型操作:
Cat* catPtr = static_cast<Cat*>(animalPtr); // 向下轉型
catPtr->meow(); // 正確
向下轉型的兩種方式
static_cast
(靜態轉型)
特點:
- 在編譯期完成類型轉換。
- 不進行運行時類型檢查,若轉換錯誤可能導致未定義行為(如訪問非法內存)。
dynamic_cast
(動態轉型)
特點:
- 在運行時檢查類型是否合法(依賴RTTI)。
若轉換非法:
- 對指針返回
nullptr
。 - 對引用拋出
std::bad_cast
異常。
要求基類至少有一個虛函數(多態類型)。
安全性問題:
static_cast
的未檢查風險
- 本質:
static_cast
在編譯期完成類型轉換,但不驗證實際對象類型。
典型UB(未定義行為)場景:
class Animal {};
class Cat : public Animal { public: void meow() {} };
class Dog : public Animal {};Animal* animal = new Dog(); // 實際指向Dog對象
Cat* cat = static_cast<Cat*>(animal); // 編譯通過,但實際類型不匹配
cat->meow(); // 未定義行為!可能崩潰或破壞內存
dynamic_cast
的局限性
- 依賴
RTTI
:若基類無虛函數,dynamic_cast
無法使用
class Base {}; // 無虛函數
class Derived : public Base { public: void foo() {} };Base* base = new Derived();
Derived* derived = dynamic_cast<Derived*>(base); // 編譯錯誤!
繼承中的作用域
作用域嵌套規則
- 派生類作用域 嵌套在 基類作用域 內。
名字查找順序:
- 在派生類中訪問成員時,優先在 當前類作用域 查找,若未找到則向 直接基類作用域 逐層向上查找。
示例:
class Base {
public:int value = 10;void print() { cout << "Base: " << value << endl; }
};class Derived : public Base {
public:int value = 20; // 隱藏基類的value成員void print() { // 隱藏基類的print函數cout << "Derived: " << value << endl;cout << "Base::value: " << Base::value << endl; // 顯式訪問基類成員}
};
- 成員隱藏:派生類中定義的 value 和 print 會隱藏基類的同名成員。
- 顯式訪問:通過 Base::value 可繞過隱藏訪問基類成員。
隱藏規則
同名成員隱藏
- 規則:派生類中定義與基類同名的成員(數據或函數)會隱藏基類的成員,無論參數是否一致。
class Base {
public:void func(int x) { cout << "Base::func(int)" << endl; }
};class Derived : public Base {
public:void func(double x) { // 隱藏Base::func(int)cout << "Derived::func(double)" << endl;}
};Derived d;
d.func(5); // 輸出 "Derived::func(double)"(參數隱式轉換)
d.Base::func(5); // 顯式調用基類函數
虛函數與作用域
覆蓋(Override
)條件:
- 基類函數聲明為
virtual
。 - 派生類函數簽名完全相同(包括返回類型、參數、const修飾符)。
隱藏非虛函數:
class Base {
public:virtual void foo() { cout << "Base::foo" << endl; }void bar() { cout << "Base::bar" << endl; }
};class Derived : public Base {
public:void foo() override { cout << "Derived::foo" << endl; } // 正確覆蓋void bar(int) { cout << "Derived::bar(int)" << endl; } // 隱藏Base::bar()
};Derived d;
d.bar(); // 錯誤!Base::bar()被隱藏
d.Base::bar(); // 正確
- 使用 using
聲明解除隱藏
class Base {
public:void func(int) {}void func(double) {}
};class Derived : public Base {
public:using Base::func; // 引入基類所有重載版本的funcvoid func(const char*) {} // 添加新重載
};Derived d;
d.func(5); // 調用Base::func(int)
d.func("abc"); // 調用Derived::func(const char*)
多層繼承的作用域鏈
作用域逐層嵌套
class A { public: void f() {} };
class B : public A { public: void f(int) {} }; // 隱藏A::f()
class C : public B { public: void f() {} }; // 隱藏B::f(int)C c;
c.f(); // 調用C::f()
c.B::f(5); // 顯式調用B::f(int)
c.A::f(); // 顯式調用A::f()
虛函數的多層覆蓋
class A { public: virtual void f() { cout << "A::f" << endl; } };
class B : public A { public: void f() override { cout << "B::f" << endl; } };
class C : public B { public: void f() override { cout << "C::f" << endl; } };C c;
A* ptr = &c;
ptr->f(); // 輸出 "C::f"(動態綁定)
派生類的默認成員函數
6個默認成員函數,默認的意思就是指我們不寫,編譯器會變我們自動生成?個,那么在派生類中,這幾個成員函數是如何生成的呢?
- 派生類的構造函數必須調用基類的構造函數初始化基類的那一部分成員。如果基類沒有默認的構造函數,則必須在派生類構造函數的初始化列表階段顯式調用基類的構造函數。
- 派生類的拷貝構造函數必須調用基類的拷貝構造函數完成基類的拷貝初始化。
- 派生類的
operator=
必須調用基類的 operator= 完成基類的復制。需要注意的是,派生類的 operator= 會隱藏基類的operator=,因此顯式調用基類的 operator= 時,需指定基類作用域(例如Base::operator=)。- 派生類的析構函數會在被調用完成后自動調用基類的析構函數清理基類成員。這是為了保證派生類對象先清理派生類成員、再清理基類成員的順序。
- 派生類對象初始化時,先調用基類構造函數,再調用派生類構造函數。
- 派生類對象析構清理時,先調用派生類析構函數,再調用基類析構函數。
由于多態中一些場景下析構函數需要構成重寫(重寫條件之一是函數名相同,具體在多態章節講解),編譯器會對析構函數名進行特殊處理,統一處理為 destructor()
。因此,若基類析構函數未加 virtual
,派生類析構函數與基類析構函數構成隱藏關系(而非重寫)。
默認構造函數
(Derived() = default;)
基類構造規則:
- 自動調用 基類的默認構造函數。
- 若基類無默認構造函數,必須顯式調用基類的其他構造函數。
成員初始化:
- 對派生類新增的成員變量,按默認初始化規則處理(內置類型不初始化,類類型調用默認構造函數)。
示例:
class Base {
public:Base(int x) : value(x) {} // 無默認構造函數
private:int value;
};class Derived : public Base {
public:// 錯誤!基類無默認構造函數,必須顯式調用// Derived() = default; // 正確:顯式調用基類構造函數Derived() : Base(0) {}
};
拷貝構造函數
(Derived(const Derived&) = default;)
基類拷貝規則:
- 調用 基類的拷貝構造函數。
成員拷貝規則:
- 對派生類新增成員,執行 成員拷貝初始化(淺拷貝)。
示例:
class Base {
public:Base(const Base&) { cout << "Base copy" << endl; }
};class Derived : public Base {
public:int* data;// 默認拷貝構造函數行為:// 1. 調用 Base::Base(const Base&)// 2. 拷貝 data 指針(淺拷貝)Derived(const Derived&) = default;
};Derived d1;
d1.data = new int(10);
Derived d2 = d1; // 調用默認拷貝構造函數,data 指針被淺拷貝
拷貝賦值運算符
(Derived& operator=(const Derived&) = default;)
基類賦值規則:
- 調用 基類的拷貝賦值運算符。
成員賦值規則:
- 對派生類新增成員,執行 成員拷貝賦值。
示例:
class Base {
public:Base& operator=(const Base&) {cout << "Base copy assign" << endl;return *this;}
};class Derived : public Base {
public:string str;Derived& operator=(const Derived&) = default; // 自動調用基類拷貝賦值
};Derived d1, d2;
d1 = d2; // 調用 Base::operator= 和 string::operator=
析構函數
(~Derived() = default;)
析構順序:
- 先執行 派生類析構函數體。
- 然后按成員聲明逆序銷毀 派生類新增成員。
- 最后自動調用 基類析構函數。
虛析構函數:
- 若基類析構函數為
virtual
,則派生類析構函數自動成為虛函數。
示例:
class Base {
public:virtual ~Base() { cout << "~Base" << endl; }
};class Derived : public Base {
public:~Derived() { cout << "~Derived" << endl; }
};Base* ptr = new Derived();
delete ptr; // 輸出:~Derived → ~Base
注意事項:
顯式調用基類版本
- 在自定義派生類成員函數中,需手動調用基類對應函數:
class Derived : public Base {
public:Derived(const Derived& d) : Base(d) { // 調用基類拷貝構造函數// 拷貝派生類成員...}Derived& operator=(const Derived& d) {Base::operator=(d); // 調用基類拷貝賦值// 賦值派生類成員...return *this;}
};
繼承構造函數(C++11)
- 使用
using Base::Base
; 繼承基類構造函數:
class Base {
public:Base(int x) {}
};class Derived : public Base {
public:using Base::Base; // 繼承 Base(int x)
};Derived d(5); // 合法
繼承和友元
C++繼承體系中,友元函數是不可被繼承的。基類的友元函數不會自動成為派生類的友元。也就是說基類友元不能訪問派生類私有和保護成員 。除非派生類也聲明該函數為友元函數。
class Base {friend void foo(Base&);
private:int a;
};class Derived : public Base {
private:int b;
};void foo(Base& b) {b.a = 42; // 合法:foo是Base的友元// b.b = 42; // 錯誤:無法訪問Derived的私有成員
}
- foo能訪問Base的私有成員a,但無法訪問Derived新增的私有成員b,除非在Derived中顯式聲明foo為友元。
基類的友元函數可以通過基類引用/指針,訪問派生類對象中繼承自基類的私有成員。
Derived d;
foo(d); // 合法:傳遞派生類對象給基類引用
- 雖然d是Derived類型,但foo通過Base&訪問的是其基類部分的a,這是允許的。
派生類需顯式聲明友元
- 若派生類需要允許外部函數訪問其私有成員,必須獨立聲明友元。
class B {friend class A;
private:int secret;
};class A {};
class C : public A {};void test() {C c;// c.secret = 42; // 錯誤:C不是B的友元
}
繼承和靜態成員
靜態成員的可見性
靜態成員屬于定義它的類,不會被派生類繼承,但可以通過作用域運算符 (
::
) 訪問。基類定義了static
靜態成員,則整個繼承體系里面只有?個這樣的成員。無論派?出多少個派生類,都只有?個static
成員實例。派生類可以直接訪問基類的 公有(
public
)或保護(protected
)靜態成員。
示例:
class Base {
public:static int count; // 靜態成員聲明static void print() { cout << "Base: " << count << endl; }
};
int Base::count = 0; // 靜態成員初始化class Derived : public Base {
public:void increment() { Base::count++; // 合法:訪問基類的公有靜態成員}
};int main() {Derived d;d.increment();Base::print(); // 輸出 "Base: 1"Derived::print(); // 同樣合法:調用基類的靜態函數
}
靜態數據成員的初始化
靜態數據成員必須在類外單獨初始化,且初始化位置不影響繼承。即使通過派生類訪問基類的靜態成員,初始化仍需在基類作用域中完成
class Base {
public:static int x;
};
int Base::x = 100; // 必須初始化class Derived : public Base {};int main() {Derived::x = 200; // 修改基類的靜態成員cout << Base::x; // 輸出 200
}
靜態成員函數與多態(TODO)
靜態成員函數不能是虛函數,因為它們不依賴于對象實例(沒有
this
指針)。即使派生類定義了同名的靜態函數,也不會覆蓋基類的靜態函數。
示例:
class Base {
public:static void foo() { cout << "Base::foo\n"; }
};class Derived : public Base {
public:static void foo() { cout << "Derived::foo\n"; }
};int main() {Derived::foo(); // 輸出 "Derived::foo"Derived::Base::foo(); // 輸出 "Base::foo"
}
同名靜態成員的隱藏
如果派生類定義了與基類同名的靜態成員,基類的靜態成員會被隱藏。
需要通過作用域運算符 (Base
::
) 顯式訪問基類的靜態成員。
class Base {
public:static int value;
};
int Base::value = 10;class Derived : public Base {
public:static int value; // 隱藏基類的靜態成員
};
int Derived::value = 20;int main() {cout << Base::value; // 輸出 10cout << Derived::value; // 輸出 20cout << Derived::Base::value; // 輸出 10(顯式訪問基類靜態成員)
}
多繼承及其菱形繼承
- 單繼承:當一個派生類只有一個直接基類時,這種繼承關系被稱為單繼承。
- 多繼承:當一個派生類有兩個或以上直接基類時,這種繼承關系被稱為多繼承
- 多繼承對象在內存中的模型是,先繼承的基類位于前面,后繼承的基類位于后面,派生類成員則放在最后。
- 菱形繼承:菱形繼承是多繼承的一種特殊情況。從下面的對象成員模型構造可以看出,菱形繼承存在數據冗余和二義性的問題。
單繼承
定義
- 一個派生類(Derived Class)只有一個直接基類(Base Class)的繼承關系。
內存模型
- 派生類對象的內存布局:基類成員在前,派生類新增成員在后。
- 指針轉換時,基類指針可以直接指向派生類對象(隱式向上轉型)。
class Animal {
public:int age;
};class Dog : public Animal { // 單繼承
public:int weight;
};int main() {Dog dog;dog.age = 2; // 訪問基類成員dog.weight = 10; // 訪問派生類成員Animal* ptr = &dog; // 合法:基類指針指向派生類對象return 0;
}
多繼承
定義
- 一個派生類有多個直接基類的繼承關系。
內存模型
- 基類按聲明順序排列,派生類成員在最后。
- 指針轉換時需顯式指定基類類型(避免二義性)。
class LandAnimal {
public:void walk() { cout << "Walking\n"; }
};class WaterAnimal {
public:void swim() { cout << "Swimming\n"; }
};class Frog : public LandAnimal, public WaterAnimal { // 多繼承
public:void jump() { cout << "Jumping\n"; }
};int main() {Frog frog;frog.walk(); // 調用 LandAnimal 方法frog.swim(); // 調用 WaterAnimal 方法frog.jump(); // 調用派生類方法// 顯式指定基類指針類型LandAnimal* landPtr = &frog;WaterAnimal* waterPtr = &frog;return 0;
}
菱形繼承
Diamond Inheritance
問題
- 數據冗余:派生類會包含多個基類的同一份成員。
- 二義性:訪問基類成員時需顯式指定路徑。
示例代碼(問題演示)
class Person {
public:string name;
};class Student : public Person {}; // 繼承 Person
class Teacher : public Person {}; // 繼承 Personclass Assistant : public Student, public Teacher {}; // 菱形繼承int main() {Assistant assistant;// assistant.name = "Alice"; // 錯誤:二義性(無法確定是 Student::name 還是 Teacher::name)assistant.Student::name = "Alice"; // 顯式指定路徑assistant.Teacher::name = "Bob"; // 數據冗余:Person 被存儲兩次return 0;
}
解決方案:虛繼承(Virtual Inheritance
)
- 使用
virtual
關鍵字聲明基類,確保公共基類在派生類中只保留一份。 - 初始化時必須直接調用公共基類的構造函數。
class Person {
public:string name;
};class Student : virtual public Person {}; // 虛繼承
class Teacher : virtual public Person {}; // 虛繼承class Assistant : public Student, public Teacher {};int main() {Assistant assistant;assistant.name = "Alice"; // 合法:Person 只保留一份return 0;
}
- 優先使用單繼承:避免復雜性。
- 慎用多繼承:僅在明確需要組合多個獨立功能時使用。
- 避免菱形繼承:如必須使用,務必通過虛繼承解決冗余問題。
I0庫中的菱形虛擬繼承
template<class CharT, class Traits = std::char_traits<CharT>>
class basic_ostream : virtual public std::basic_ios<CharT, Traits>
{};
template<class CharT, class Traits = std::char_traits<CharT>>
class basic_istream : virtual public std::basic_ios<CharT, Traits>
{};
繼承和組合
- public 繼承是一種
“is-a”
的關系。也就是說每個派生類對象都是一個基類對象。- 組合是一種
“has-a
” 的關系。假設 B 組合了 A,那么每個 B 對象中都有一個 A 對象。- 繼承允許你根據基類的實現來定義派生類的實現。這種通過生成派生類的復用通常被稱為白箱復用(
white - box reuse
)。術語“白箱”是相對于可視性而言的:在繼承方式中,基類的內部細節對派生類可見。繼承在一定程度上破壞了基類的封裝性,基類的改變會對派生類產生很大的影響。派生類和基類之間的依賴關系很強,耦合度高。- 對象組合是類繼承之外的另一種復用選擇。新的、更復雜的功能可以通過組裝或組合對象來獲得。對象組合要求被組合的對象具有良好定義的接口。這種復用風格被稱為黑箱復用(
black - box reuse
),因為對象的內部細節是不可見的。對象只以 “黑箱” 的形式出現。組合類之間沒有很強的依賴關系,耦合度低。優先使用對象組合有助于保持每個類的封裝性。
優先使用組合,而不是繼承。實際上,應盡量多使用組合,因為組合的耦合度低,代碼維護性好。不過也不能過于絕對,如果類之間的關系適合繼承(is - a
),那就使用繼承;另外,要實現多態,也必須使用繼承。如果類之間的關系既適合用繼承(is - a
),也適合用組合(has - a
),則優先使用組合。
思考題
A和B類中的兩個func構成什么關系()
- A. 重載 B. 隱藏 C.沒關系
下面程序的編譯運行結果是什么()
- A. 編譯報錯 B. 運行報錯 C. 正常運行
#include <iostream>
using namespace std;class A
{
public:void fun(){cout << "func()" << endl;}
};
class B : public A
{
public:void fun(int i){cout << "func(int i)" << i << endl;}
};
int main()
{B b;b.fun(10);b.fun();return 0;
};
多繼承中指針偏移問題?下?說法正確的是( )
- A:p1 == p2 == p3 B:p1 < p2 < p3 C:p1 == p3 != p2 D:p1 != p2 != p3
class Base1
{
public:int _b1;
};
class Base2
{
public:int _b2;
};
class Derive : public Base1, public Base2
{
public:int _d;
};
int main()
{Derive d;Base1 *p1 = &d;Base2 *p2 = &d;Derive *p3 = &d;return 0;
}
寫在最后
本文到這里就結束了,有關C++更深入的講解,如多態,C++11新語法新特性,以及智能指針和異常級話題,后面會發布專門的文章為大家講解。感謝您的觀看!
如果你覺得這篇文章對你有所幫助,請為我的博客 點贊👍收藏?? 評論💬或 分享🔗 支持一下!你的每一個支持都是我繼續創作的動力?!🙏
如果你有任何問題或想法,也歡迎 留言💬 交流,一起進步📚!?? 感謝你的閱讀和支持🌟!🎉
祝各位大佬吃得飽🍖,睡得好🛌,日有所得📈,逐夢揚帆?!