一、繼承的基本概念
本質:代碼復用+類關系建模(是多態的基礎)
class Person { /*...*/ };
class Student : public Person { /*...*/ }; // public繼承
- 派生類繼承基類成員(數據+方法),可以通過監視窗口檢驗成員復用。
二、繼承中的訪問權限控制
訪問權限變化表
基類成員訪問限定符/繼承方式 | public繼承 | protected繼承 | private繼承 |
public成員 | ->派生類public | ->派生類protected | ->派生類private |
protected成員 | ->派生類protected | ->派生類protected | ->派生類private |
private成員 | 不可見(但存在) | 不可見(但存在) | 不可見(但存在) |
關鍵規則
- private成員:在派生類中始終不可訪問(但存在于對象中)
- protected成員:專為繼承設計,派生類可訪問,外部不可訪問。
- 訪問權限計算:Min(成員在基類的權限,繼承方式),權限等級:public>protected>private
- 默認繼承方式:class默認private繼承;struct默認public繼承。
- 實際開發:優先使用public繼承(占實際使用90%以上),慎用protected/private繼承。
三、對象賦值轉換規則
允許的操作
Student s;
Person p = s; // 對象切片(調用拷貝構造)
Person& rp = s; // 直接引用基類部分
Person* pp = &s; // 直接指向基類部分
禁止的操作
// Person p;
// Student s = p; // 錯誤!基類無法賦給派生類
關鍵注意
- 對象切片:派生類->基類賦值時,丟失派生類特有成員。
- 引用轉換原理:派生類對象包含完整的基類子對象,無臨時變量生成。
? ? ? ? 類型系統對比:
int i = 0;const double& rd = i; // 需要const引用(臨時變量具有常性)
四、繼承中的作用域
核心規則
- 獨立作用域:基類和派生類擁有獨立的作用域。
- 隱藏/重定義:派生類成員與基類同名時,隱藏基類成員,包括成員變量和函數(無論參數是否一致)。
class Base { public:void func(int) {} };class Derived : public Base { public:void func() { Base::func(42); // 必須顯式指定作用域} };
重要細節
- 函數隱藏與重載:派生類會隱藏基類所有同名函數(即使參數不同)
- 訪問被隱藏成員:使用作用域解析符
Base::member
- 設計建議:避免定義同名非虛函數
五、派生類默認成員函數
1. 構造函數
-
規則
1.當基類存在默認構造函數時:如果基類有隱式或顯式的無參構造函數(即默認構造函數),派生? ? ?類的構造函數初始化列表不需要顯式調用基類構造函數。編譯器會自動隱式調用基類的默認構造? ? ?函數。示例:
class Person {
public:Person() {} // 默認構造函數(可以隱式生成)
};class Student : public Person {
public:Student() {} // 隱式調用 Person::Person()
};
2.當基類沒有默認構造函數時:如果基類的構造函數需要參數,且沒有定義無參構造函數,則派生類必須在初始化列表中顯示調用基類的某個構造函數,否則會編譯報錯。示例:
class Person {
public:Person(int x) {} // 沒有默認構造函數
};class Student : public Person {
public:Student() : Person(42) {} // 必須顯式調用基類構造函數
};
-
原理:基類成員初始化順序優先于派生類成員
?2. 拷貝構造函數
-
規則:需顯式調用基類拷貝構造,完成基類部分的深拷貝
-
代碼示例:
Student(const Student& s): Person(s) // 切片調用基類拷貝構造, _id(s._id) {}
3. 賦值運算符重載?
-
規則:需要顯式調用基類賦值運算符,處理自賦值情況
-
代碼示例:
Student& operator=(const Student& s) {if (this != &s) {Person::operator=(s); // 顯式調用基類賦值_id = s._id;}return *this; }
4. 析構函數?
-
規則:
-
析構順序:派生類->基類(自動調用基類析構)
-
禁止顯式調用基類析構函數
-
-
代碼示例:
~Student() {// 自動調用Person::~Person()delete _ptr; // 先清理派生類資源 }
六、繼承關系與友元?
-
規則:基類友元不能訪問派生類私有成員,需要額外聲明
-
代碼示例:
class Student; // 前向聲明 class Person {friend void Display(const Person&, const Student&); }; class Student : public Person {friend void Display(const Person&, const Student&); }; void Display(const Person& p, const Student& s) {cout << p._name << endl; // 訪問基類保護成員cout << s._stuNum << endl; // 訪問派生類保護成員 }
七、靜態成員與繼承?
-
特性:
-
基類靜態成員被所有派生類共享
-
繼承的是訪問權而非副本
-
靜態成員不被包含在對象中,它放在靜態存儲器。
-
-
代碼示例:
class Person { public :Person () {++ _count ;} protected :string _name ; // 姓名 public :static int _count; // 統計人的個數。 }; int Person :: _count = 0;class Student : public Person { protected :int _stuNum ; // 學號 };class Graduate : public Student { protected :string _seminarCourse ; // 研究科目 };void TestPerson() {Student s1 ;Student s2 ;Student s3 ;Graduate s4 ;cout <<" 人數 :"<< Person ::_count << endl; //輸出:人數 :4Student ::_count = 0;cout <<" 人數 :"<< Person ::_count << endl; //輸出:人數 :0 }
八、復雜繼承模型?
- 單繼承:一個子類只有一個直接父類時稱這個繼承關系為單繼承
- 多繼承:一個子類有兩個或以上直接父類時稱這個繼承關系為多繼承
菱形繼承:多繼承的一種特殊情況
問題
數據冗余和二義性(從下面的對象成員模型構造,可以看出菱形繼承有數據冗余和二義性的問題。在Assistant的對象中Person成員會有兩份。)
class Person
{
public :string _name ; // 姓名
};
class Student : public Person
{
protected :int _num ; //學號
};
class Teacher : public Person
{
protected :int _id ; // 職工編號
};
class Assistant : public Student, public Teacher
{
protected :string _majorCourse ; // 主修課程
};void Test ()
{// 這樣會有二義性無法明確知道訪問的是哪一個Assistant a ; a._name = "peter"; // 需要顯示指定訪問哪個父類的成員可以解決二義性問題,但是數據冗余問題無法解決a.Student::_name = "xxx";a.Teacher::_name = "yyy";
}
解決方案
虛擬繼承(在Assistant的對象中Person成員只有一份。)
class Person
{
public :string _name ; // 姓名
};
class Student : virtual public Person
{
protected :int _num ; //學號
};
class Teacher : virtual public Person
{
protected :int _id ; // 職工編號
};
class Assistant : public Student, public Teacher
{
protected :string _majorCourse ; // 主修課程
};void Test ()
{Assistant a ;a._name = "peter";
}
虛擬繼承解決數據冗余和二義性的原理
講解
為了研究虛擬繼承原理,我們給出了一個簡化的菱形繼承繼承體系,再借助內存窗口觀察對象成員的模型。
class A { int _a; };
class B : public A { int _b; };
class C : public A { int _c; };
class D : public B, public C { 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;
}


- 疑問1:為什么D中B和C部分要去找屬于自己的A?
????????那么大家看看當下面的賦值發生時,d是不是要去找出B/C成員中的A才能賦值過去?
- 疑問2: 為什么要在B和C中存指針,而不直接存距離A的偏移量呢?
? ? ? ? ①存儲內容多樣性:
? ? ? ? ? ? - 多偏移量:菱形繼承中,虛基表除存當? ? ? ? ? ? ? ? ? ?前類到虛基類偏移量,在多重繼承、復? ? ? ? ? ? ? ? ? ?雜模板實例化等場景,還需存不同條件? ? ? ? ? ? ? ? ? ?下訪問虛基類的其他偏移量,用于正確? ? ? ? ? ? ? ? ? ?定位成員。
? ? ? ? ? ? - 輔助信息:虛基表存儲虛基類構造、析? ? ? ? ? ? ? ? ? ?構函數指針等輔助信息,確保在對象構? ? ? ? ? ? ? ? ? ?構造、析構及函數調用時正確操作。僅? ? ? ? ? ? ? ? ? ?用偏移量無法存儲這些信息,易致錯誤。
? ? ? ? ②支持復雜偏移關系:
? ? ? ? ? ? - 適應結構變化:使用虛基表指針,面對? ? ? ? ? ? ? ? ? ?復雜繼承結構變化(如 B、C 繼承路徑? ? ? ? ? ? ? ? ? ? ?新增虛繼承層次)時,虛基表可添加新? ? ? ? ? ? ? ? ? ?偏移量或信息,指針仍能正確指向,保? ? ? ? ? ? ? ? ? ?證虛基類成員訪問。直接用偏移量,結? ? ? ? ? ? ? ? ? ?構變化時需多處修改,維護擴展困難。

九、繼承與組合的選擇
特征 | 繼承 | 組合 |
---|---|---|
關系性質 | "is-a"關系 | "has-a"關系 |
可見性 | 白箱復用(了解實現細節) | 黑箱復用(接口隔離) |
耦合度 | 高耦合 | 低耦合 |
多態支持 | 支持 | 不支持 |
代碼復用方式 | 垂直復用(擴展功能) | 水平復用(功能組合) |
使用建議:
-
優先使用對象組合
-
需要多態特性時使用繼承
-
避免過度使用多繼承