前言
前面我們講了C語言的基礎(chǔ)知識,也了解了一些數(shù)據(jù)結(jié)構(gòu),并且講了有關(guān)C++的命名空間的一些知識點以及關(guān)于C++的缺省參數(shù)、函數(shù)重載,引用 和 內(nèi)聯(lián)函數(shù)也認(rèn)識了什么是類和對象以及怎么去new一個 ‘對象’ ,也了解了C++中的模版,以及學(xué)習(xí)了幾個STL的結(jié)構(gòu)也相信大家都掌握的不錯,接下來博主將會帶領(lǐng)大家繼續(xù)學(xué)習(xí)有關(guān)C++比較重要的知識點—— 繼承(基類、派生類和多態(tài)性)。下面話不多說坐穩(wěn)扶好咱們要開車了??
一、繼承的概念及定義
1. 繼承的概念
繼承(inheritance)機制是面向?qū)ο蟪绦蛟O(shè)計使代碼可以復(fù)用的最重要的手段,它允許程序員在保持原有類特性的基礎(chǔ)上進(jìn)行擴展,增加功能,這樣產(chǎn)生新的類,稱派生類。繼承呈現(xiàn)了面向?qū)ο蟪绦蛟O(shè)計的層次結(jié)構(gòu),體現(xiàn)了由簡單到復(fù)雜的認(rèn)知過程。以前我們接觸的復(fù)用都是函數(shù)復(fù)用,繼承是類設(shè)計層次的復(fù)用。
????注意:繼承是一種強關(guān)聯(lián)關(guān)系,因此在使用繼承時需要仔細(xì)設(shè)計類之間的關(guān)系,避免產(chǎn)生緊耦合和不必要的依賴關(guān)系。
2.繼承的定義
?定義格式
class 派生類名(子類): 訪問修飾符 基類名(父類)
{
// 子類的成員和方法
};
-
class
關(guān)鍵字用于聲明一個類。 -
派生類名
是你要定義的子類的名稱。 -
訪問修飾符
可以使用public
、protected
或private
,用于控制子類對父類成員的訪問權(quán)限。 -
基類名
是你希望子類繼承的父類的名稱。
?繼承關(guān)系和訪問限定符
?繼承基類成員訪問方式的變化
類成員/繼承方式 | public繼承 | protected繼承 | private繼承 |
---|---|---|---|
基類的public成員 | 派生類的public成員 | 派生類的protected成員 | 派生類的private成員 |
基類的protected成員 | 派生類的protected成員 | 派生類的protected成員 | 派生類的private成員 |
基類的private成員 | 在派生類中不可見 | 在派生類中不可見 | 在派生類中不可 |
【總結(jié)】
- 基類
private
成員在派生類中無論以什么方式繼承都是不可見的。這里的不可見是指基類的私有成員還是被繼承到了派生類對象中,但是語法上限制派生類對象不管在類里面還是類外面都不能去訪問它。 - 基類
private
成員在派生類中是不能被訪問,如果基類成員不想在類外直接被訪問,但需要在派生類中能訪問,就定義為protected
。可以看出保護(hù)成員限定符是因繼承才出現(xiàn)的。 - 實際上面的表格我們進(jìn)行一下總結(jié)會發(fā)現(xiàn),基類的私有成員在子類都是不可見。
基類的其他成員在子類的訪問方式 == Min(成員在基類的訪問限定符,繼承方式),public > protected> private
。 - 使用關(guān)鍵字
class
時默認(rèn)的繼承方式是private
,使用struct
時默認(rèn)的繼承方式是public
,不過最好顯示的寫出繼承方式。 -
在實際運用中一般使用都是
public
繼承,幾乎很少使用protetced/private
繼承,也不提倡使用protetced/private
繼承,因為protetced/private
繼承下來的成員都只能在派生類的類里面使用,實際中擴展維護(hù)性不強。
?下面是一個示例,演示如何定義一個子類 Square
繼承父類 Shape
:
class Shape
{
protected:
int width;
int height;
public:
void setWidth(int w)
{
width = w;
}
void setHeight(int h)
{
height = h;
}
};
class Square : public Shape
{
public:
int getArea()
{
return width * height;
}
};
在上述示例中,Square
繼承了 Shape
的屬性 width
和 height
,并且定義了自己的方法 getArea()
來計算正方形的面積。使用 public
訪問修飾符,使得 Square
類可以直接訪問 Shape
類中的公共成員。
二、基類和派生類對象賦值轉(zhuǎn)換
- 派生類對象 可以賦值給 基類的對象 / 基類的指針 / 基類的引用。這里有個形象的說法叫切片或者切割。寓意把派生類中父類那部分切來賦值過去。
- 基類對象不能賦值給派生類對象。
-
基類的指針或者引用可以通過強制類型轉(zhuǎn)換賦值給派生類的指針或者引用。如果要將基類對象轉(zhuǎn)換為派生類對象,可以使用
dynamic_cast
進(jìn)行類型轉(zhuǎn)換,并且可能需要在轉(zhuǎn)換之前進(jìn)行運行時類型檢查,以確保安全性。
class Person
{
protected :
string _name; // 姓名
string _sex; // 性別
int _age; // 年齡
};
class Student : public Person
{
public :
int _No ; // 學(xué)號
};
void Test ()
{
Student sobj ;
// 1.子類對象可以賦值給父類對象/指針/引用
Person pobj = sobj ;
Person* pp = &sobj;
Person& rp = sobj;
//2.基類對象不能賦值給派生類對象會報錯
sobj = pobj;//err
//3.基類的指針可以通過強制類型轉(zhuǎn)換賦值給派生類的指針
pp = &sobj
Student* ps1 = (Student*)pp; // 這種情況轉(zhuǎn)換時可以的。
ps1->_No = 10;
pp = &pobj;
Student* ps2 = (Student*)pp; // 這種情況轉(zhuǎn)換時雖然可以,但是會存在越界訪問的問題
ps2->_No = 10;
}
三、繼承中的作用域
- 在繼承體系中基類和派生類都有獨立的作用域。
- 子類的作用域包含父類的作用域。
- 父類的作用域不包含子類的作用域。
- 子類可以直接訪問父類的公共成員和受保護(hù)成員。
- 父類不能直接訪問子類的成員。(如果父類需要訪問子類的成員,可以通過公共接口或子類的方法來實現(xiàn))
- 子類和父類中有同名成員,子類成員將屏蔽父類對同名成員的直接訪問,這種情況叫隱藏,也叫重定義。(在子類成員函數(shù)中,可以使用 基類::基類成員 顯示訪問)
- 需要注意的是如果是成員函數(shù)的隱藏,只需要函數(shù)名相同就構(gòu)成隱藏。
- 注意在實際中在繼承體系里面最好不要定義同名的成員。
??總結(jié)起來,繼承中的作用域規(guī)則允許子類訪問父類的成員,但父類不能直接訪問子類的成員。這種作用域規(guī)則有助于實現(xiàn)封裝和信息隱藏,提高代碼的可維護(hù)性和安全性。
四、派生類的默認(rèn)成員函數(shù)
?6個默認(rèn)成員函數(shù),“默認(rèn)”的意思就是指我們不寫,編譯器會變我們自動生成一個,那么在派生類中,這幾個成員函數(shù)是如何生成的呢?下面我們來逐一分析:
- 派生類的構(gòu)造函數(shù)必須調(diào)用基類的構(gòu)造函數(shù)初始化基類的那一部分成員。如果基類沒有默認(rèn)的構(gòu)造函數(shù),則必須在派生類構(gòu)造函數(shù)的初始化列表階段顯示調(diào)用。
- 派生類的拷貝構(gòu)造函數(shù)必須調(diào)用基類的拷貝構(gòu)造完成基類的拷貝初始化。
- 派生類的
operator=
必須要調(diào)用基類的operator=
完成基類的復(fù)制。 - 派生類的析構(gòu)函數(shù)會在被調(diào)用完成后自動調(diào)用基類的析構(gòu)函數(shù)清理基類成員。因為這樣才能保證派生類對象先清理派生類成員再清理基類成員的順序。
- 派生類對象初始化先調(diào)用基類構(gòu)造再調(diào)派生類構(gòu)造。
- 派生類對象析構(gòu)清理先調(diào)用派生類析構(gòu)再調(diào)基類的析構(gòu)。
??注意:編譯器會對析構(gòu)函數(shù)名進(jìn)行特殊處理,處理成destrutor()
所以父類析構(gòu)函數(shù)不加virtual
的情況下,子類析構(gòu)函數(shù)和父類析構(gòu)函數(shù)構(gòu)成隱藏關(guān)系。
?這六個默認(rèn)成員函數(shù)在派生類中的生成規(guī)則與基類的可訪問性有關(guān)。需要注意的是,如果派生類顯式定義了上述任何一個成員函數(shù),編譯器將不會自動生成對應(yīng)的默認(rèn)成員函數(shù)。
五、繼承與友元
?友元關(guān)系不能繼承,也就是說基類友元不能訪問子類私有和保護(hù)成員
在C++中,友元關(guān)系允許一個類或函數(shù)訪問另一個類的私有成員或受保護(hù)成員。通過在類中聲明其他類或函數(shù)為友元,可以授予這些友元類或函數(shù)對私有成員的訪問權(quán)限。
然而,友元關(guān)系不會被繼承?;惖挠言P(guān)系僅適用于基類,不能自動擴展到派生類。這意味著基類的友元類不能直接訪問派生類的私有或受保護(hù)成員。
讓我們來看一個例子來說明這一點:
class Base {
private:
int privateMember;
friend class FriendClass; // 聲明 FriendClass 為 Base 的友元類
public:
void publicMemberFunc() {
privateMember = 10; // 在類的成員函數(shù)中可以訪問私有成員
}
};
class Derived : public Base {
private:
int derivedPrivateMember;
public:
void derivedMemberFunc() {
derivedPrivateMember = 20;
}
};
class FriendClass {
public:
void accessBaseMember(Base& obj) {
obj.privateMember = 30; // 可以訪問基類的私有成員
}
};
int main() {
Base baseObj;
FriendClass friendObj;
friendObj.accessBaseMember(baseObj); // 可以通過友元類訪問基類的私有成員
Derived derivedObj;
friendObj.accessBaseMember(derivedObj); // 但不能通過友元類訪問派生類的私有成員
return 0;
}
在上面的例子中,FriendClass
被聲明為 Base
的友元類,并且可以訪問 Base
類的私有成員 privateMember
。然而,FriendClass
無法訪問派生類 Derived
的私有成員 derivedPrivateMember
,即使 Derived
類是從 Base
類繼承而來。
因此,友元關(guān)系不會在繼承過程中自動傳遞。
??總結(jié)來說,繼承和友元是C++中的兩個不同的概念。繼承用于創(chuàng)建派生類從基類派生的關(guān)系,而友元用于授予其他類或函數(shù)對私有成員的訪問權(quán)限。友元關(guān)系不會被繼承,基類的友元類無法直接訪問派生類的私有成員。
六、繼承與靜態(tài)成員
?基類定義了static
靜態(tài)成員,則整個繼承體系里面只有一個這樣的成員。無論派生出多少個子類,都只有一個static
成員實例 。
靜態(tài)成員由所有該類的對象共享,并在類的所有實例之間保持唯一。當(dāng)在基類中定義一個靜態(tài)成員時,在繼承體系中的所有派生類中也只有一個實例。這意味著,無論有多少個派生類,靜態(tài)成員只有一個實例。無論是訪問、修改還是獲取靜態(tài)成員的值,都只會影響該唯一的實例。
以下示例說明了派生類繼承了基類的靜態(tài)成員的行為:
#include <iostream>
class Base {
public:
static int staticMember;
};
int Base::staticMember = 0;
class Derived1 : public Base {
};
class Derived2 : public Base {
};
int main() {
Derived1 d1;
Derived2 d2;
d1.staticMember = 10;
d2.staticMember = 20;
std::cout << d1.staticMember << std::endl; // 輸出: 20
std::cout << d2.staticMember << std::endl; // 輸出: 20
return 0;
}
在這個例子中,Base
類定義了一個靜態(tài)成員 staticMember
,默認(rèn)為 0。Derived1
和 Derived2
是從 Base
派生出來的兩個派生類。
d1.staticMember
和 d2.staticMember
都是訪問相同的靜態(tài)成員 Base::staticMember
。修改其中一個派生類的靜態(tài)成員的值,會同時影響到其他派生類和基類。
因此,無論有多少個派生類,都只有一個靜態(tài)成員實例,它們共享相同的靜態(tài)成員變量。這就是靜態(tài)成員在繼承體系中的行為。
七、復(fù)雜的菱形繼承及菱形虛擬繼承
?單繼承
單繼承:一個子類只有一個直接父類時稱這個繼承關(guān)系為單繼承
?多繼承
多繼承:一個子類有兩個或以上直接父類時稱這個繼承關(guān)系為多繼承
?菱形繼承
菱形繼承是一種多重繼承的情況,其中一個派生類同時從兩個基類直接或間接繼承,而這兩個基類又繼承自同一個基類。
這種繼承關(guān)系可能導(dǎo)致一些問題,其中最常見的問題是稱為"菱形繼承問題"或"鉆石繼承問題"。它主要涉及兩個方面:命名沖突和二義性。
1. 命名沖突問題
命名沖突問題指的是,如果派生類在兩個基類中都有相同名稱的成員,那么在派生類中訪問該成員將會產(chǎn)生沖突。編譯器無法判斷使用哪個基類的成員,導(dǎo)致編譯錯誤。
2. 二義性問題
二義性問題指的是,如果派生類調(diào)用一個在兩個基類中都有定義的函數(shù),編譯器無法確定要調(diào)用哪個基類的函數(shù),導(dǎo)致語義上的二義性。
3. 虛繼承(virtual)
為了解決菱形繼承問題,C++ 提供了虛繼承(virtual inheritance)的機制,通過使用關(guān)鍵字 virtual
來聲明基類繼承,以便消除重復(fù)基類而帶來的問題。虛繼承確保在繼承體系中只有一個共享的基類子對象。
下面是使用虛繼承解決菱形繼承問題的示例:
#include <iostream>
class Base {
public:
int value;
};
class Derived1 : virtual public Base { // 使用虛繼承
};
class Derived2 : virtual public Base { // 使用虛繼承
};
class Derived3 : public Derived1, public Derived2 {
public:
void setValue(int val) {
value = val; // 可以直接訪問 value,不會產(chǎn)生二義性
}
void printValue() {
std::cout << value << std::endl; // 可以直接訪問 value,不會產(chǎn)生二義性
}
};
int main() {
Derived3 d;
d.setValue(10);
d.printValue(); // 輸出: 10
return 0;
}
在上面的例子中,Derived1
和 Derived2
都使用了虛繼承從 Base
繼承。Derived3
從 Derived1
和 Derived2
多重繼承,并可以直接訪問共享的 value
成員,而不會產(chǎn)生二義性。
通過使用虛繼承,我們可以解決菱形繼承問題中的命名沖突和二義性。虛繼承確保只有一個共享的基類子對象,避免了重復(fù)繼承和二義性的問題。
需要注意的是,虛繼承引入了額外的開銷和復(fù)雜性,因此應(yīng)謹(jǐn)慎使用。一般來說,只有在確實需要共享基類子對象的情況下才應(yīng)使用虛繼承。在其他情況下,使用普通的多重繼承就可以滿足需求。
八、繼承的總結(jié)和反思
- 很多人說C++語法復(fù)雜,其實多繼承就是一個體現(xiàn)。有了多繼承,就存在菱形繼承,有了菱形繼承就有菱形虛擬繼承,底層實現(xiàn)就很復(fù)雜。所以一般不建議設(shè)計出多繼承,一定不要設(shè)計出菱形繼承。否則在復(fù)雜度及性能上都有問題。
- 多繼承可以認(rèn)為是C++的缺陷之一,很多后來的語言都沒有多繼承。
繼承和組合
- public繼承是一種is-a的關(guān)系。也就是說每個派生類對象都是一個基類對象。
- 組合是一種has-a的關(guān)系。假設(shè)B組合了A,每個B對象中都有一個A對象。
- 優(yōu)先使用對象組合,而不是類繼承(詳細(xì)介紹鏈接)
- 繼承允許你根據(jù)基類的實現(xiàn)來定義派生類的實現(xiàn)。這種通過生成派生類的復(fù)用通常被稱為白箱復(fù)用(white-box reuse)。術(shù)語“白箱”是相對可視性而言:在繼承方式中,基類的內(nèi)部細(xì)節(jié)對子類可見 。繼承一定程度破壞了基類的封裝,基類的改變,對派生類有很大的影響。派生類和基類間的依賴關(guān)系很強,耦合度高。
- 對象組合是類繼承之外的另一種復(fù)用選擇。新的更復(fù)雜的功能可以通過組裝或組合對象來獲得。對象組合要求被組合的對象具有良好定義的接口。這種復(fù)用風(fēng)格被稱為黑箱復(fù)用(black-box reuse),因為對象的內(nèi)部細(xì)節(jié)是不可見的。對象只以“黑箱”的形式出現(xiàn)。組合類之間沒有很強的依賴關(guān)系,耦合度低。優(yōu)先使用對象組合有助于你保持每個類被封裝。
- 實際盡量多去用組合。組合的耦合度低,代碼維護(hù)性好。不過繼承也有用武之地的,有些關(guān)系就適合繼承那就用繼承,另外要實現(xiàn)多態(tài),也必須要繼承。類之間的關(guān)系可以用繼承,可以用組合,就用組合。
九、筆試面試題
- 什么是菱形繼承?菱形繼承的問題是什么?
【答】菱形繼承是指在一個繼承體系中,派生類同時從兩個基類直接或間接繼承,并且這兩個基類又繼承自同一個基類。由于繼承關(guān)系形成了一個菱形的圖形,因此得名菱形繼承。菱形繼承會帶來一些問題,其中最常見的問題是命名沖突和二義性。
-
命名沖突:如果派生類
D
在兩個基類B
和C
中都有相同名稱的成員,那么在派生類D
中訪問該成員時會產(chǎn)生沖突。編譯器無法確定要使用哪個基類的成員,導(dǎo)致編譯錯誤。 -
二義性:如果派生類
D
調(diào)用一個在兩個基類B
和C
中都有定義的函數(shù)時,編譯器無法確定要調(diào)用哪個基類的函數(shù),從而產(chǎn)生語義上的二義性。
為了解決菱形繼承問題,C++ 提供了虛繼承(virtual inheritance)的機制。通過在繼承聲明中使用 virtual
關(guān)鍵字,可以消除重復(fù)基類而帶來的問題。虛繼承確保在繼承體系中只有一個共享的基類子對象,從而解決了命名沖突和二義性的問題。
- 什么是菱形虛擬繼承?如何解決數(shù)據(jù)冗余和二義性的?
【答】菱形虛擬繼承是一種使用虛擬繼承解決菱形繼承問題的技術(shù)。它通過在繼承聲明中使用虛擬繼承,消除了重復(fù)基類而帶來的數(shù)據(jù)冗余和二義性問題。
菱形虛擬繼承的主要目標(biāo)是確保在繼承體系中只有一個共享的基類子對象,從而避免數(shù)據(jù)冗余。通過虛擬繼承,派生類只保留一個基類子對象的副本,而不是多個副本。
此外,菱形虛擬繼承還解決了二義性問題。由于只有一個共享的基類子對象,派生類可以直接訪問該對象的成員,而不會產(chǎn)生二義性。
通過菱形虛擬繼承,我們可以解決菱形繼承問題中的數(shù)據(jù)冗余和二義性。虛擬繼承確保只有一個共享的基類子對象,從而避免了數(shù)據(jù)冗余。同時,派生類可以直接訪問共享的基類成員,而不會產(chǎn)生二義性。
- 繼承和組合的區(qū)別?什么時候用繼承?什么時候用組合?
【答】繼承和組合是面向?qū)ο缶幊讨袃煞N不同的關(guān)系建立方式。
繼承是一種"is-a"(是一個)的關(guān)系,它允許一個類(派生類)繼承另一個類(基類)的屬性和行為。通過繼承,派生類可以重用基類的代碼,并且可以添加、修改或覆蓋基類的成員。繼承用于表示類之間的一般化和特殊化關(guān)系,其中派生類是基類的一種特殊類型。
組合是一種"has-a"(有一個)的關(guān)系,它允許一個類(容器類)包含另一個類(成員類)的對象作為成員。通過組合,容器類可以使用成員類的功能,并且可以控制成員類的生命周期。組合用于表示類之間的整體與部分關(guān)系,其中容器類包含成員類作為其一部分。
-
繼承適合以下情況:
- 當(dāng)一個類是另一個類的特殊類型時,可以使用繼承來表示它們之間的關(guān)系。
- 當(dāng)需要重用基類的代碼,并在派生類中添加、修改或覆蓋成員時,可以使用繼承。
- 當(dāng)需要使用基類的指針或引用來操作派生類對象時,可以使用繼承。
-
組合適合以下情況:
- 當(dāng)一個類需要包含另一個類的對象作為其一部分時,可以使用組合來表示它們之間的關(guān)系。
- 當(dāng)需要控制成員對象的生命周期,并在容器對象的生命周期內(nèi)創(chuàng)建、使用和銷毀成員對象時,可以使用組合。
- 當(dāng)需要在容器對象中調(diào)用成員對象的功能時,可以使用組合。
??繼承和組合都是關(guān)系建立的方式,它們并不是互斥的。在實際的設(shè)計中,可以根據(jù)具體的需求和設(shè)計目標(biāo),靈活地使用繼承和組合來構(gòu)建類之間的關(guān)系。
溫馨提示
感謝您對博主文章的關(guān)注與支持!另外,我計劃在未來的更新中持續(xù)探討與本文相關(guān)的內(nèi)容,會為您帶來更多關(guān)于C++以及編程技術(shù)問題的深入解析、應(yīng)用案例和趣味玩法等。請繼續(xù)關(guān)注博主的更新,不要錯過任何精彩內(nèi)容!文章來源:http://www.zghlxwxcb.cn/news/detail-672696.html
再次感謝您的支持和關(guān)注。期待與您建立更緊密的互動,共同探索C++、算法和編程的奧秘。祝您生活愉快,排便順暢!文章來源地址http://www.zghlxwxcb.cn/news/detail-672696.html
到了這里,關(guān)于【C++入門到精通】C++入門 —— 繼承(基類、派生類和多態(tài)性)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關(guān)文章,希望大家以后多多支持TOY模板網(wǎng)!