一、引言
? ? ? ? 代碼的復用對于代碼的質量以及程序員的代碼設計上都是非常重要的,C++中的許多特性都體現了這一點,從函數復用、模板的引入到今天我們將一起學習的:繼承
二、什么是繼承?
? ? ? ? 1、繼承的概念
????????繼承(inheritance)機制是面向對象程序設計使代碼可以復用的最重要的手段,它允許程序員在保持原有類特性的基礎上進行擴展,增加功能,這樣產生新的類,稱派生類。繼承呈現了面向對象 程序設計的層次結構,體現了由簡單到復雜的認知過程。以前我們接觸的復用都是函數復用,繼 承是類設計層次的復用、
? ? ? ? 注:接下來我們將多次用到下面的繼承場景,即人、老師與學生三者之間的關系:
????????
class Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}
protected:string _name = "zhao"; // 姓名int _age = 18; // 年齡
};class Student : public Person
{
protected:int _stuid; //學號
};class Teacher : public Person
{
protected:int _jobid; // 工號
};
? ? ? ? 2、繼承的定義
? ? ? ? ? ? ? ? (1).定義繼承的格式
? ? ? ? ? ? ? ? 注:括號內為對于其前元素的說明
class Student(派生類/子類) : public(繼承方式) Person
{
public://子類的成員函數列表//子類的成員變量列表
};
? ? ? ? ? ? ? ? (2).繼承關系與訪問限定符
? ? ? ? ? ? ? ? 繼承方式有三種,分別對應了三種訪問限定符,它們分別是:
????????????????
? ? ? ? ? ? ? ? 不同的繼承方式與不同的訪問限定符組合會產生九種不同的效果:
?????????????????? ? ? ? ? ? ? ? 可以發現,基類的私有成員在派生類中一定不可見(在類外與類內都不可訪問,可以認為沒有這個成員),對于其他情況來說,一個成員的權限是訪問限定符與繼承方式中權限更小的那一個,所以在可能用到繼承的類中,我們更傾向于使用保護作為基類的訪問限定符
三、父子類之間的對象賦值轉換
? ? ? ? ? ?父子類之間支持子類對象、指針和引用向父類進行賦值轉換,這時會發生切片賦值,也就是會將子類中屬于父類成員的那一部分賦值給父類對象,同時切掉獨屬于子類的那一部分,對于引用和指針也是類似的,引用賦值時,父類的引用類型是子類中屬于父類對象的別名;指針賦值時,父類的指針類型直接指向了子類中屬于父類對象的那一部分
? ? ? ? 需要注意的是,父類對象不能,轉換賦值給子類對象,這點是很好理解的,因為父類對象中不包含子類對象的內容
? ? ? ? 上面幾點可以通過下面的代碼及其運行結果說明,為了方便演示,我將上面提供過的類中的protected成員換成了public成員:
? ? ? ? 代碼:
????????
class Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}string _name = "zhao"; // 姓名int _age = 18; // 年齡
};class Student : public Person
{
public:int _stuid; //學號
};class Teacher : public Person
{
public:int _jobid; // 工號
};
int main()
{Student s;s._age = 10;s._name = "mei";Teacher t;Person p = s;Person* pp = &s;Person& rp = s;p.Print();pp->Print();rp.Print();
}
? ? ? ? 運行結果:
????????
四、繼承中的作用域
? ? ? ? 1、在繼承體系中基類和派生類都有獨立的作用域
? ? ? ? 2、子類和父類中如果有同名成員,那么子類的成員將會屏蔽掉父類中與它同名的成員,使其不能直接訪問,這種情況叫做隱藏,也叫做重定義
? ? ? ? 下面的代碼和運行結果可以說明2中的問題:
? ? ? ? 代碼:
????????
class Person
{
protected:string _name = "zhao"; // 姓名int _num = 1010; // 身份證號
};class Student : public Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "num:" << _num << endl;}
protected:int _num = 10; //學號
};
int main()
{Student s;s.Print();
}
? ? ? ? 運行結果:
????????
? ? ? ? 3、需要注意的是,函數要構成隱藏,只需要函數名相同就構成隱藏
? ? ? ? 下面的代碼及運行結果可以說明3中的問題:
? ? ? ? 代碼:
????????
class Person
{void Print(int x = 10){cout << "Person" << endl;}
protected:string _name = "zhao"; // 姓名int _num = 1010; // 身份證號
};class Student : public Person
{
public:void Print(){cout << "Student" << endl;}
protected:int _num = 10; //學號
};
int main()
{Student s;s.Print();
}
? ? ? ? 運行結果:
????????
? ? ? ? 4、注意在實際應用中最好不要在繼承體系中定義同名成員,很容易混淆
五、派生類中的默認成員函數
? ? ? ? 每一個類中都有6個默認成員函數,“默認”是指我們不寫,編譯器會自動生成的函數,在這里對普通類中的默認成員函數不多做贅述,如果想要了解關于默認成員函數的詳細內容可以跳轉到以下鏈接:
????????C++?類和對象(中)!!!-CSDN博客https://blog.csdn.net/2501_90507065/article/details/147402717?spm=1001.2014.3001.5501? ? ? ? 接下來我們將主要討論一個繼承體系中派生類的默認成員函數都有哪些特點
? ? ? ? 1、構造函數
? ? ? ? 構造函數用于初始化類中的成員變量,對于派生類的構造函數,在初始化列表部分會自動調用基類的默認構造函數,如果基類沒有提供默認構造函數,我們需要主動調用它的構造函數,函數的調用類似于定義一個基類匿名對象,這是很合理的一種寫法,顯然,我們不可以在初始化列表部分初始化基類的成員,但是可以進行函數體內賦值,兩者并不沖突
? ? ? ? 2、拷貝構造函數
? ? ? ? 派生類的拷貝構造函數必須調用基類的拷貝構造函數完成對基類成員的拷貝構造,對于拷貝構造函數傳入的派生類對象引用,我們可以直接以類似定義基類匿名對象的形式將派生類對象的引用傳入,這匹配了上文講過的引用類型的切片賦值,與構造函數相同,上面的限制都是在初始化列表,對于函數體內賦值我們不做限制
? ? ? ? 3、operator=賦值運算符重載
? ? ? ? 派生類的operator=函數體中必須調用基類的operator=完成對于基類成員的賦值,需要注意的是,由于派生類與基類中的operator=函數名一致,這時候會造成函數名相同時派生類對于基類同名成員的隱藏,這時候我們需要指定類域,否則會在派生類的operator=中調用自己,形成死遞歸,最終造成程序崩潰
? ? ? ? 4、析構函數
? ? ? ? 由于繼承體系的特殊性,派生類中一定會先定義基類部分,這就意味著初始化列表中一定會先初始化基類部分,再初始化其他部分,那么在析構函數中我們就必須保證先釋放其它部分再釋放基類部分,很明顯這是確定的,所以編譯器接管了這一部分任務,我們需要在析構函數中完成對于其它部分的釋放,在析構函數的末尾,編譯器會自動調用基類的析構函數完成對于基類的析構
? ? ? ? 需要注意的是,由于多態部分的一些情況,析構函數需要構成重寫,一個條件是函數名必須相同(以后會討論到),所以編譯器對這個部分做了特殊處理,析構函數名被統一處理成了destrutor,所以基類與派生類的析構函數構成了隱藏,與operator=類似,如果要在派生類的析構函數中調用基類的析構函數需要指定類域,但是這種情況一般不會出現,在這里只是做一個特殊說明
? ? ? ? 5、注意
? ? ? ? 還有兩個默認成員函數分別是取地址運算符重載和const修飾的取地址運算符重載,但是這兩個函數編譯器自動生成的完全夠用,所以在這里不做過多贅述
? ? ? ? 了解了上面幾個默認成員函數的相關定義以及它們分別的注意點,下面利用Person類與Student類的做演示:
????????
//基類
class Person
{
public://構造函數Person(int x)//只是為了演示構造函數有傳參的情況{}//拷貝構造函數Person(Person& rp){_name = rp._name;_code = rp._code;}//賦值運算符重載Person& operator=(Person& rp){_name = rp._name;_code = rp._code;return *this;}//析構函數~Person()//沒有空間釋放{}
protected:string _name = "zhao"; // 姓名int _code = 1010; // 身份證號
};//派生類
class Student : public Person
{
public://構造函數Student(int x):Person(x){}//其余成員給缺省值//拷貝構造函數Student(Student& rs):Person(rs)//調用基類的拷貝構造,引用類型進行切片賦值{_num = rs._num;}//賦值運算符重載Student& operator=(Student& rs){Person::operator=(rs);_num = rs._num;return *this;}~Student(){_num = 0;}
protected:int _num = 10; //學號
};
六、繼承與友元
? ? ? ? 友元關系不能繼承,也就是說基類友元不能訪問子類私有和保護成員
? ? ? ? 所以對于如下情況,Display函數并不能訪問stuNum
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靜態成員,則整個繼承體系里面只有一個這樣的成員。無論派生出多少個子 類,都只有一個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;
}
int main()
{TestPerson();return 0;
}
? ? ? ? 運行結果:
????????
八、復雜的菱形繼承及菱形虛擬繼承
? ? ? ? 1、引入
? ? ? ? ? ? ? ? (1).單繼承
? ? ? ? ? ? ? ? 一個子類只有一個父類的繼承體系稱為單繼承,如下圖所示:
????????????????
? ? ? ? ? ? ? ? (2).多繼承
? ? ? ? ? ? ? ? 一個子類繼承了多個父類的情況稱為多繼承,如下圖所示:
????????????????
? ? ? ? ? ? ? ? (3).菱形繼承
? ? ? ? ? ? ? ? 菱形繼承是一種特殊的多繼承,可以理解為一個多繼承歸根結底有一個(多個)
類被重復繼承,最常見的情況如下圖所示:
????????????????
? ? ? ? ? ? ? ? 很明顯,菱形繼承是存在一些問題的,就是:在Assistant類對象中,Person類的成員被保存了兩份,這導致了Person類的數據早成冗余,同時Assistant的Person屬性會出現分歧,這兩個問題被稱為菱形繼承的數據冗余及數據二義性,下圖是Assistant類對象模型,可以很明顯的感受到菱形繼承的兩個問題:
????????????????
? ? ? ? 2、菱形繼承產生問題的解決與虛擬繼承
? ? ? ? ? ? ? ? (1).探究如何解決菱形繼承產生的問題
? ? ? ? ? ? ? ? 首先,對于二義性的問題是比較好解決的,我們可以對于兩個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";}
? ? ? ? ? ? ? ? (2).虛擬繼承
? ? ? ? ? ? ? ? 上面的方法只是暫時性的解決了菱形繼承產生的問題,但是并沒有從根本上解決這個問題,這時候我們引入了虛擬繼承,虛擬繼承可以解決菱形繼承的二義性和數據冗余的問題。如上面的繼承關系,在Student和 Teacher繼承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";}
? ? ? ? 3、虛擬繼承為什么可以解決數據冗余和二義性的問題
????????為了研究虛擬繼承原理,我們給出了一個簡化的菱形繼承體系,再借助內存窗口觀察對象成員的模型,如下:
????????
class A{
public:int _a;};// class B : public Aclass B : virtual public A{public:int _b;};// class C : public Aclass 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;}
? ? ? ? 下圖是內存中存儲的d對象模型(未進行虛擬繼承處理的):
????????? ? ? ??
? ? ? ? 下圖是菱形虛擬繼承的內存對象成員模型:這里可以分析出D對象中將A放到的了對象組成的最下 面,這個A同時屬于B和C,那么B和C如何去找到公共的A呢?這里是通過了B和C的兩個指針,指 向的一張表。這兩個指針叫虛基表指針,這兩個表叫虛基表。虛基表中存的偏移量。通過偏移量 可以找到下面的A?:
????????
? ? ? ? 在這里需要注意的是D類對象中的B、C都要分別去找屬于它們自己的A成員,否則在平常的情況不會有問題,但是在跨類的切片賦值時會出問題
九、繼承學習的總結與反思
? ? ? ? 1、盡量不使用多繼承,不使用菱形繼承,菱形繼承改變了類對象模型,性能上存在問題
? ? ? ? 2、繼承與組合
? ? ? ? ? ? ? ? (1).繼承是is-a的關系,描述的是A是B
? ? ? ? ? ? ? ? (2).組合是has-a的關系,描述的是A擁有B
? ? ? ? ? ? ? ? (3).能使用組合不使用繼承
? ? ? ? ? ? ? ? (4).繼承是一種白箱復用,將基類細節暴露了出來,同時提高了類之間的耦合度
? ? ? ? ? ? ? ? (5).組合是一種黑箱復用,對象內部更多的是封裝的,類之間耦合度低,組合類之間沒有很強的關系
十、結語
? ? ? ? 這就是本期關于繼承的所有內容了,期待各位于晏、亦菲和我一起學習、進步!
????????? ? ·????????