繼承的概念及定義
繼承機制是面向對象程序設計使代碼可以複用的最重要的手段,它允許程序員在保持原有類特性的基礎上進行擴展,增加功能,這樣產生新的類,稱派生類。繼承呈現了面向對象程序設計的層次結構,體現了由簡單到複雜的認知過程,繼承是類設計層次的複用。
定義格式如下:
繼承基類成員訪問方式的變化:
類成員/繼承方式 | public繼承 | protected繼承 | private繼承 |
---|---|---|---|
基類的public成員 | 派生類的public成員 | 派生類的protected成 員 | 派生類的private成 員 |
基類的protected成 員 | 派生類的protected成 員 | 派生類的protected成 員 | 派生類的private成 員 |
基類的private成員 | 在派生類中不可見 | 在派生類中不可見 | 在派生類中不可見 |
基類和派生類對象賦值轉換
- 派生類對象 可以賦值給 基類的對象 / 基類的指針 / 基類的引用
- 基類對象不能賦值給派生類對象
- 基類的指針可以通過強制類型轉換賦值給派生類的指針。但是必須是基類的指針是指向派生類對象時纔是安全的
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;
}
繼承的作用域
- 在繼承體系中基類和派生類都有獨立的作用域
- 子類和父類中有同名成員,子類成員將屏蔽父類對同名成員的直接訪問,這種情況叫隱藏,也叫重定義(在子類成員函數中,可以使用 基類::基類成員 顯示訪問)
- 需要注意的是如果是成員函數的隱藏,只需要函數名相同就構成隱藏
- 注意在實際中在繼承體系裏面最好不要定義同名的成員
// Student的_num和Person的_num構成隱藏關係
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; // 學號
};
void Test()
{
Student s1;
s1.Print();
};
派生類的默認成員函數
6個默認成員函數,“默認”的意思就是指我們不寫,編譯器會變我們自動生成一個,那麼在派生類中,這幾個
成員函數是如何生成的呢?
- 派生類的構造函數必須調用基類的構造函數初始化基類的那一部分成員。如果基類沒有默認的構造函數,則必須在派生類構造函數的初始化列表階段顯示調用
- 派生類的拷貝構造函數必須調用基類的拷貝構造完成基類的拷貝初始化
- 派生類的 operator = 必須要調用基類的operator=完成基類的複製
- 派生類的析構函數會在被調用完成後自動調用基類的析構函數清理基類成員。因爲這樣才能保證派生類對象先清理派生類成員再清理基類成員的順序
- 派生類對象初始化先調用基類構造再調派生類構造
- 派生類對象析構清理先調用派生類析構再調基類的析構
實現一個不能被繼承的類
// C++98中構造函數私有化,派生類中調不到基類的構造函數。則無法繼承
class NonInherit
{
public:
static NonInherit GetInstance()
{
return NonInherit();
}
private:
NonInherit()
{}
};
// C++11給出了新的關鍵字final禁止繼承
class NonInherit final
{};
友元
友元關係不能繼承,即基類友元不能訪問子類私有和保護成員
靜態成員
基類定義了 static 靜態成員,則整個繼承體系裏面只有一個這樣的成員。無論派生出多少個子類,都只有一個 static 成員實例
菱形繼承及菱形虛擬繼承
菱形繼承:菱形繼承是多繼承的一種特殊情況
菱形繼承的問題:從下面的對象成員模型構造,可以看出菱形繼承有數據冗餘和二義性的問題。在 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"; }
虛擬繼承可以解決菱形繼承的二義性和數據冗餘的問題。如上面的繼承關係,在 Student 和 Teacher 的繼承 Person 時使用虛擬繼承,即可解決問題
class A {
public:
int _a;
};
// class B : public A
class B : virtual public A {
public:
int _b;
};
// class C : public A
class C : virtual public A {
public:
int _c;
};
class D : public B, public C {
public:
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; }
小結
- C++語法複雜,多繼承就是一個體現。有了多繼承,就存在菱形繼承,有了菱形繼承就有菱形虛擬繼承,底層實現很複雜
- 多繼承可以認爲是C++的缺陷之一,很多後來的OO語言都沒有多繼承,如Java。
- 繼承和組合
public繼承是一種is-a的關係。也就是說每個派生類對象都是一個基類對象
組合是一種has-a的關係。假設B組合了A,每個B對象中都有一個A對象
優先使用對象組合,而不是類繼承
繼承允許根據基類的實現來定義派生類的實現。這種通過生成派生類的複用通常被稱爲白箱複用,術語“白箱”是相對可視性而言:在繼承方式中,基類的內部細節對子類可見 。繼承一定程度破壞了基類的封裝,基類的改變,對派生類有很大的影響。派生類和基類間的依賴關係很強,耦合度高
對象組合是類繼承之外的另一種複用選擇。新的更復雜的功能可以通過組裝或組合對象來獲得。對象組合要求被組合的對象具有良好定義的接口。這種複用風格被稱爲黑箱複用,因爲對象的內部細節是不可見的。對象只以“黑箱”的形式出現。 組合類之間沒有很強的依賴關係,耦合度低。優先使用對象組合有助於保持每個類被封裝
實際儘量多去用組合。組合的耦合度低,代碼維護性好。不過繼承也有用武之地的,有些關係就適合繼承那就用繼承,另外要實現多態,也必須要繼承。類之間的關係可以用繼承,可以用組合,就用組合