【C++ 繼承】—— 青花分水、和而不同,繼承中的“明明德”與“止于至善”

歡迎來到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 成員不可見不可見不可見
  1. 基類 private 成員在派生類中無論以什么方式繼承都是不可見。這里的不可見是指基類的私有成員雖然被繼承到了派生類對象中,但是語法上限制派生類對象無論在類里面還是類外面都無法訪問它。
  2. 基類 private 成員在派生類中是不能被訪問,如果基類成員不想在類外直接被訪問,但需要在派生類中能訪問,就定義為 protected。可以看出保護成員限定符是因繼承才出現的。
  3. 基類的私有成員在派生類都是不可見。基類的其他成員在派生類的訪問方式 == 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” 關系指的是一個類包含另一個類的對象作為成員變量,即聚合關系。

  • 對于 stackvector 的關系既符合 is - a,也符合 has - a

vector 本質是 std 命名空間中實現的類模板:

template<class T>
class vector{};
  • 我們可以基于 stackvectoris - 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_castdynamic_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新語法新特性,以及智能指針和異常級話題,后面會發布專門的文章為大家講解。感謝您的觀看!

如果你覺得這篇文章對你有所幫助,請為我的博客 點贊👍收藏?? 評論💬或 分享🔗 支持一下!你的每一個支持都是我繼續創作的動力?!🙏
如果你有任何問題或想法,也歡迎 留言💬 交流,一起進步📚!?? 感謝你的閱讀和支持🌟!🎉
祝各位大佬吃得飽🍖,睡得好🛌,日有所得📈,逐夢揚帆?!

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/899620.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/899620.shtml
英文地址,請注明出處:http://en.pswp.cn/news/899620.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Qt warning LNK4042: 對象被多次指定;已忽略多余的指定

一、常規原因&#xff1a; pro或pri 文件中源文件被多次包含 解決&#xff1a;刪除變量 SOURCES 和 HEADERS 中重復條目 二、誤用 對于某些pri庫可以使用如下代碼簡寫包含 INCLUDEPATH $$PWDHEADERS $$PWD/*.hSOURCES $$PWD/*.cpp但是假如該目錄下只有頭文件&#xff0c;沒…

Visual Studio Code 無法打開源文件解決方法

&#x1f308; 個人主頁&#xff1a;Zfox_ &#x1f525; 系列專欄&#xff1a;Linux &#x1f525; 系列專欄&#xff1a;C從入門到精通 目錄 一&#xff1a;&#x1f525; 突發狀況 二&#xff1a;&#x1f525; 共勉 一&#xff1a;&#x1f525; 突發狀況 &#x1f42c;…

js文字兩端對齊

目錄 一、問題 二、原因及解決方法 三、總結 一、問題 1.text-align: justify; 不就可以了嗎&#xff1f;但是實際測試無效 二、原因及解決方法 1.原因&#xff1a;text-align只對非最后一行文字有效。只有一行文字時&#xff0c;text-align無效&#xff0c;要用text-alig…

LeetCode算法題(Go語言實現)_20

題目 給你兩個下標從 0 開始的整數數組 nums1 和 nums2 &#xff0c;請你返回一個長度為 2 的列表 answer &#xff0c;其中&#xff1a; answer[0] 是 nums1 中所有 不 存在于 nums2 中的 不同 整數組成的列表。 answer[1] 是 nums2 中所有 不 存在于 nums1 中的 不同 整數組成…

每天認識一個設計模式-橋接模式:在抽象與實現的平行宇宙架起彩虹橋

一、前言&#xff1a;虛擬機橋接的啟示 使用過VMware或者Docker的同學們應該都接觸過網絡橋接&#xff0c;在虛擬機網絡配置里&#xff0c;橋接模式是常用的網絡連接方式。選擇橋接模式時&#xff0c;虛擬機會通過虛擬交換機與物理網卡相連&#xff0c;獲取同網段 IP 地址&…

java筆記02

運算符 1.隱式轉換和強制轉換 類型轉換的分類 1.隱式轉換&#xff1a; 取值范圍小的數值 轉換為 取值范圍大的數值 2.強制轉換&#xff1a; 取值范圍大的數值 轉換為 取值范圍小的數值隱式轉換的兩種提升規則 取值范圍小的&#xff0c;和取值范圍大的進行運算&#xff0c;小的…

Redis-07.Redis常用命令-集合操作命令

一.集合操作命令 SADD key member1 [member2]&#xff1a; sadd set1 a b c d sadd set1 a 0表示沒有添加成功&#xff0c;因為集合中已經有了這個元素了&#xff0c;因此無法重復添加。 SMEMBERS key: smembers set1 SCARD key&#xff1a; scard set1 SADD key member1 …

李飛飛、吳佳俊團隊新作:FlowMo如何以零卷積、零對抗損失實現ImageNet重構新巔峰

目錄 一、摘要 二、引言 三、相關工作 四、方法 基于擴散先前的離散標記化器利用廣告 架構 階段 1A&#xff1a;模式匹配預訓練 階段 1B&#xff1a;模式搜索后訓練 采樣 第二階段&#xff1a;潛在生成建模 五、Coovally AI模型訓練與應用平臺 六、實驗 主要結果 …

CSS3:現代Web設計的魔法卷軸

一、布局革命&#xff1a;從平面到多維空間 1.1 Grid布局的次元突破 星際戰艦布局系統 .galaxy {display: grid;grid-template-areas: "nav nav nav""sidebar content ads""footer footer footer";grid-template-rows: 80px 1fr 120p…

美觀快速的react 的admin框架

系統特色&#xff1a; - &#x1f3a8; 精心設計的UI主題系統&#xff0c;提供優雅的配色方案和視覺體驗 - &#x1f4e6; 豐富完整的組件庫&#xff0c;包含大量開箱即用的高質量組件 - &#x1f528; 詳盡的組件使用示例&#xff0c;降低開發者的學習成本 - &#x1f680…

【C++】 string底層封裝的模擬實現

目錄 前情提要Member functions —— 成員函數構造函數拷貝構造函數賦值運算符重載析構函數 Element access —— 元素訪問Iterator —— 迭代器Capacity —— 容量sizecapacityclearemptyreserveresize Modifiers —— 修改器push_backappendoperator(char ch)operator(const …

計算機網絡相關知識小結

計算機網絡 1.計算機網絡&#xff1a;獨立計算機&#xff0c;通信線路連接&#xff0c;實現資源共享 2.組成&#xff1a;資源子網和通信子網 3.拓撲分類 4.范圍&#xff1a;LAN, MAN. WAN 5、有線和無線 6.按照方向&#xff1a;單工、雙工&#xff0c;全雙工 7.傳輸對象方式&a…

16-CSS3新增選擇器

知識目標 掌握屬性選擇器的使用掌握關系選擇器的使用掌握結構化偽類選擇器的使用掌握偽元素選擇器的使用 如何減少文檔內class屬性和id屬性的定義&#xff0c;使文檔變得更加簡潔&#xff1f; 可以通過屬性選擇器、關系選擇器、結構化偽類選擇器、偽元素選擇器。 1. 屬性選擇…

【彈性計算】異構計算云服務和 AI 加速器(四):FPGA 虛擬化技術

《異構計算云服務和 AI 加速器》系列&#xff0c;共包含以下文章&#xff1a; 異構計算云服務和 AI 加速器&#xff08;一&#xff09;&#xff1a;功能特點異構計算云服務和 AI 加速器&#xff08;二&#xff09;&#xff1a;適用場景異構計算云服務和 AI 加速器&#xff08;…

Java進階——位運算

位運算直接操作二進制位&#xff0c;在處理底層數據、加密算法、圖像處理等領域具有高效性能和效率。本文將深入探討Java中的位運算。 本文目錄 一、位運算簡介1. 與運算2. 或運算異或運算取反運算左移運算右移運算無符號右移運算 二、位運算的實際應用1. 權限管理2. 交換兩個變…

OpenAI深夜直播「偷襲」谷歌!GPT-4o原生圖像生成:奧特曼帶梗圖,AGI戰場再燃戰火

引言&#xff1a;AI戰場的「閃電戰」 當谷歌剛剛發布「地表最強」Gemini 2.5 Pro時&#xff0c;OpenAI立即以一場深夜直播「閃電反擊」——GPT-4o的原生圖像生成功能正式上線&#xff01;從自拍變梗圖到相對論漫畫&#xff0c;奧特曼&#xff08;OpenAI團隊&#xff09;用一連…

鴻蒙harmonyOS:筆記 正則表達式

從給出的文本中&#xff0c;按照既定的相關規則&#xff0c;匹配出符合的數據&#xff0c;其中的規則就是正則表達式&#xff0c;使用正則表達式&#xff0c;可以使得我們用簡潔的代碼就能實現一定復雜的邏輯&#xff0c;比如判斷一個郵箱賬號是否符合正常的郵箱賬號&#xff0…

[首發]烽火HG680-KD-海思MV320芯片-2+8G-安卓9.0-強刷卡刷固件包

烽火HG680-KD-海思MV320芯片-28G-安卓9.0-強刷卡刷固件包 U盤強刷刷機步驟&#xff1a; 1、強刷刷機&#xff0c;用一個usb2.0的8G以下U盤&#xff0c;fat32&#xff0c;2048塊單分區格式化&#xff08;強刷對&#xff35;盤非常非常挑剔&#xff0c;usb2.0的4G U盤兼容的多&a…

Python-數據處理

第十五章 生成數據 安裝Matplotlib&#xff1a;通過pip install matplotlib命令安裝庫。繪制折線圖的核心語法為&#xff1a; import matplotlib.pyplot as plt x_values [1, 2, 3] y_values [1, 4, 9] plt.plot(x_values, y_values, linewidth2) plt.title(&quo…

Java基礎-23-靜態變量與靜態方法的使用場景

在Java中&#xff0c;static關鍵字用于定義靜態變量和靜態方法。它們屬于類本身&#xff0c;而不是類的某個實例。因此&#xff0c;靜態成員可以通過類名直接訪問&#xff0c;而無需創建對象。以下是靜態變量與靜態方法的常見使用場景&#xff1a; 一、靜態變量的使用場景 靜態…