繼承的概念及定義
繼承的概念
繼承(Inheritance)機制作為面向對象程序設計中最核心的代碼復用方式,它不僅允許開發人員在保留基礎類特性的前提下進行功能擴展(從而創建新的派生類),更重要的是體現了面向對象程序設計的分層架構理念,這種架構完美地映射了從簡單到復雜的認知過程。與傳統函數級別的復用相比,繼承提升到了類設計層次的復用,為軟件系統的可擴展性和可維護性提供了更強大的支持。
class Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "peter"; // 存儲姓名信息int _age = 18; // 存儲年齡信息
};/*
通過繼承機制,基類 Person 的所有成員(包括數據成員和成員函數)
都會成為子類的組成部分。以下示例展示了 Student 和 Teacher 類
如何有效地復用 Person 類的成員。開發者可以通過調試工具觀察
Student 和 Teacher 對象,直觀地驗證數據成員的復用情況。
*/class Student : public Person
{
protected:int _stuid; // 存儲學號信息
};class Teacher : public Person
{
protected:int _jobid; // 存儲工號信息
};int main()
{Student s;Teacher t;s.Print();t.Print();return 0;
}
繼承定義
定義格式
在面向對象系統中,Person
?作為父類(也稱為基類),而?Student
?扮演子類(也稱為派生類)的角色,這種層級關系構成了面向對象程序設計的基礎架構。
繼承關系和訪問限定符
繼承基類成員訪問方式的變化
-
基類的?
private
?成員在派生類中具有不可訪問性(無論采用何種繼承方式)。這種不可見性并不意味著派生類對象沒有繼承這些成員,而是從語法層面禁止了派生類對象(無論是在類內部還是外部)的訪問權限。 -
基類的?
private
?成員在派生類中無法直接訪問。若希望基類成員在派生類中可訪問,同時避免類外訪問,應當使用?protected
?訪問限定符。由此可見,protected
?限定符的出現正是為了滿足繼承機制的特殊需求。 -
總結訪問規則可以發現:基類的私有成員在子類中始終不可見,而基類其他成員在子類中的訪問權限取決于
Min(成員在基類的訪問限定符,繼承方式)
其中?
public > protected > private
。 -
在 C++ 中,使用?
class
?關鍵字定義類時,默認繼承方式是?private
;使用?struct
?時,默認繼承方式則為?public
。為了代碼可讀性和維護性,強烈建議顯式聲明繼承方式。 -
在工程實踐中,
public
?繼承是最常用的繼承方式,而?protected
/private
?繼承的應用場景相對較少。這主要是因為?protected
/private
?繼承限制了派生類外部的可訪問性,降低了代碼的維護性和擴展性。
?基類和派生類對象賦值轉換
class Person
{
public:void Print(){cout << _name << endl;}
protected:string _name; // 存儲姓名
private:int _age; // 存儲年齡
};// class Student : protected Person
// class Student : private Personclass Student : public Person
{
protected:int _stunum; // 存儲學號
};/*
在面向對象系統中,派生類對象可以賦值給基類對象、基類指針或基類引用,
這一特征常被形象地稱為“切片”或“切割”,比喻將派生類中屬于父類的部分
“切出”進行賦值。反之,基類對象不能直接賦值給派生類對象。基類指針或引用可以通過強制類型轉換賦值給派生類指針或引用,但需要注意:
只有當基類指針確實指向派生類對象時,這種轉換才是安全的。在多態類型中,
可以使用 RTTI (Run-Time Type Information) 的 dynamic_cast 進行安全轉換
(具體實現將在后續章節講解)。
*/class Person
{
protected:string _name; // 存儲姓名string _sex; // 存儲性別int _age; // 存儲年齡
};class Student : public Person
{
public:int _No; // 存儲學號
};void Test()
{Student sobj;// 1. 子類對象可以賦值給父類對象/指針/引用Person pobj = sobj;Person* pp = &sobj;Person& rp = sobj;// 2. 基類對象不能賦值給派生類對象sobj = pobj; // 錯誤操作// 3. 基類指針可以通過強制類型轉換賦值給派生類的指針pp = &sobj;Student* ps1 = (Student*)pp; // 安全轉換ps1->_No = 10;pp = &pobj;Student* ps2 = (Student*)pp; // 非安全轉換,可能引發越界訪問ps2->_No = 10;
}
繼承中的作用域
-
在繼承體系中,基類和派生類各自擁有獨立的作用域。
-
當子類和父類中存在同名成員時,子類的成員會屏蔽父類對該同名成員的直接訪問,這種現象叫做隱藏,也稱重定義。在子類成員函數中,可以通過?
基類::成員
?的方式顯式訪問被隱藏的基類成員。 -
成員函數的隱藏只需函數名相同即可構成隱藏,無需參數列表完全一致。
-
實際工程中,建議避免在繼承體系中定義同名成員,避免帶來混淆。例如:
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; // 學號,隱藏了基類的_num
};void Test()
{Student s1;s1.Print();
}
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);
}
?派生類的默認成員函數
C++中的6個默認成員函數指的是編譯器在無顯式定義時自動生成的函數。派生類中這些函數的生成及行為:
-
構造函數
派生類構造函數必須調用基類的構造函數來初始化基類部分。如果基類沒有默認構造函數,則派生類構造函數須在初始化列表中顯式調用相應基類構造函數。 -
拷貝構造函數
派生類的拷貝構造函數必須調用基類的拷貝構造函數來完成基類部分的復制初始化。 -
賦值運算符(operator=)
派生類的賦值運算符必須調用基類的賦值運算符完成基類成員的復制。 -
析構函數
派生類析構函數執行完畢后,自動調用基類的析構函數,保證銷毀順序從派生部分到基類部分。 -
對象初始化順序
實例化派生類對象時,先調用基類構造,再調用派生類構造。 -
對象銷毀順序
銷毀派生類對象時,先調用派生類析構,再調用基類析構。 -
析構函數重寫注意
因后續實現多態常重寫析構函數,編譯器會對析構函數名做特殊處理,形成隱藏關系。若基類析構函數未標記virtual
,則子類析構函數和基類析構函數構成隱藏關系,可能導致析構不完全。
class Person
{
public:Person(const char* name = "peter"): _name(name) {cout << "Person()" << endl;}Person(const Person& p): _name(p._name){cout << "Person(const Person& p)" << endl;}Person& operator=(const Person& p){cout << "Person operator=(const Person& p)" << endl;if (this != &p)_name = p._name;return *this;}~Person(){cout << "~Person()" << endl;}
protected:string _name; // 姓名
};class Student : public Person
{
public:Student(const char* name, int num): Person(name), _num(num){cout << "Student()" << endl;}Student(const Student& s): Person(s), _num(s._num){cout << "Student(const Student& s)" << endl;}Student& operator=(const Student& s){cout << "Student& operator=(const Student& s)" << endl;if (this != &s){Person::operator=(s);_num = s._num;}return *this;}~Student(){cout << "~Student()" << endl;}
protected:int _num; // 學號
};void Test()
{Student s1("jack", 18);
}
繼承與友元
友元關系不能繼承,即基類中聲明為友元的類或函數,不能訪問子類的私有或保護成員。
class Student;
class Person
{
public:friend void Display(const Person& p, const Student& s);
protected:string _name; // 姓名
};
class Student : public Person
{
protected:int _stuNum; // 學號
};
void Display(const Person& p, const Student& s)
{cout << p._name << endl;cout << s._stuNum << endl;
}
void main()
{Person p;Student s;Display(p, s);
}
繼承與靜態成員
基類定義的?static
?靜態成員變量在整個繼承體系中只有一份實例。無論有多少派生類對象,都共享同一個靜態成員。
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;Student ::_count = 0;cout <<" 人數 :"<< Person ::_count << endl;
}
菱形繼承的問題
單繼承:一個子類只有一個直接父類時稱這個繼承關系為單繼承
多繼承:一個子類有兩個或以上直接父類時稱這個繼承關系為多繼承
菱形繼承:菱形繼承是多繼承的一種特殊情況。
菱形繼承的問題:從下面的對象成員模型構造,可以看出菱形繼承有數據冗余和二義性的問題。 在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"; // 二義性,編譯器無法確認訪問哪個基類的_name// 需明確指定訪問路徑解決二義性,但數據冗余依然存在a.Student::_name = "xxx";a.Teacher::_name = "yyy";
}
虛擬繼承
為解決菱形繼承中數據冗余和二義性問題,可采用虛擬繼承:
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"; // 無二義性,只有一份 Person 成員
}
繼承的總結和反思
-
C++語法的復雜性,部分來源于多繼承及菱形繼承的底層實現。虛擬繼承雖能解決菱形繼承問題,但底層實現復雜且有一定性能開銷,因此一般不建議設計多繼承結構,更不要設計菱形繼承。
-
多繼承被視為 C++ 的缺陷之一,很多后續面向對象語言(如 Java)均不支持多繼承。
-
繼承與組合關系:公有繼承(
public inheritance
)是is-a關系,即每個派生類對象都是一個基類對象。組合是has-a關系,表示一個類內部包含另一個類的對象。例如:
// is-a 關系
class Car
{
protected:string _colour = "白色"; // 顏色string _num = "陜ABIT00"; // 車牌號
};class BMW : public Car
{
public:void Drive() { cout << "好開-操控" << endl; }
};class Benz : public Car
{
public:void Drive() { cout << "好坐-舒適" << endl; }
};// has-a 關系
class Tire
{
protected:string _brand = "Michelin"; // 品牌size_t _size = 17; // 尺寸
};class Car
{
protected:string _colour = "白色"; // 顏色string _num = "陜ABIT00"; // 車牌號Tire _t; // 輪胎,組合關系
};
-
建議優先使用組合以降低耦合度,提高代碼維護性。繼承破壞了基類封裝,變化基類實現對派生類有較大影響,導致耦合度高。
-
繼承適用于表達“是一個”關系,且需要實現多態時;組合適用于“擁有一個”關系,是更安全、更靈活的復用手段。
筆試面試題舉例
什么是菱形繼承?其問題有哪些?
菱形繼承(Diamond Inheritance)?是指在多繼承中出現的一種特殊繼承結構,其中一個派生類同時繼承自兩個有共同基類的父類,構成菱形結構。具體表現為:
A/ \B C\ /D
派生類?D
?繼承自?B
?和?C
,而?B
、C
?又都繼承自同一個基類?A
。
問題:
- 數據冗余:派生類?
D
?會擁有兩個獨立的基類?A
?子對象,導致內存中有兩個?A
?成員變量,相當于數據重復。 - 二義性:在訪問基類?
A
?的成員時,如?D
?中調用?A
?的成員時是通過?B
?繼承得到的,還是通過?C
?繼承得到的?編譯器無法確定,導致訪問沖突。
什么是菱形虛擬繼承?如何解決數據冗余和二義性?
菱形虛擬繼承(Virtual Diamond Inheritance)?是解決菱形繼承問題的技術手段。通過在所有繼承公共基類的路徑上使用virtual
關鍵字,確保派生類沿各條路徑共享同一個基類子對象,而不是創建多個獨立副本。
例如:
class A { ... };class B : virtual public A { ... };class C : virtual public A { ... };class D : public B, public C { ... };
解決方案:
- 數據冗余解決:通過虛擬繼承,派生類?
D
?只保留一個共享的基類?A
?實例,消除多份數據冗余。 - 二義性解決:訪問基類?
A
?的成員不再因為多繼承路徑而產生歧義,編譯器明確且唯一地解析其位置,無需顯式指定路徑,避免訪問沖突。
虛擬繼承底層通過虛基表(VBT)和虛基表指針(VBPtr)實現,維護偏移量以正確定位唯一基類子對象。
繼承與組合的區別?何時使用繼承,何時使用組合?
特性 | 繼承(is-a 關系) | 組合(has-a 關系) |
---|---|---|
關系語義 | “是一個”關系,例如學生是人 | “擁有一個”關系,例如車有輪胎 |
封裝性 | 破壞部分封裝,派生類依賴基類實現 | 封裝良好,只依賴公開接口 |
耦合度 | 高,基類變化影響派生類 | 低,修改部件不影響整體 |
靈活性 | 較低,類型固定 | 高,可動態組合不同部件 |
多態支持 | 支持多態,允許重寫基類接口 | 一般不支持多態,但可通過接口實現類似效果 |
使用場景 | 需表達“是一個”的類型繼承關系,且關注行為重用 | 組合復雜功能,靈活構建系統,關注模塊化和擴展性 |
何時使用繼承?
- 當類之間存在明確的“是一個”關系。
- 需要通過多態達到動態綁定和接口統一。
- 想重用或擴展基類行為。
何時使用組合?
- 當組件之間是“擁有”的關系。
- 需降低耦合,提高代碼靈活性和可維護性。
- 希望功能通過組合多個對象實現,便于擴展和替換。
總結:
優先推薦使用組合來實現代碼復用,只有在合理且明確的“是一個”關系且多態需求明確時,才采用繼承。