? 本篇文章會對c++中的繼承進行講解。其中講解了繼承的概念及定義、基類和派生類對象賦值轉換、繼承中的作用域、派生類的默認成員函數 和 復雜的菱形繼承及菱形虛擬繼承等內容。希望本篇文章會對你有所幫助。
文章目錄
一、繼承的概念及定義
1、1 繼承的概念
1、2 繼承的定義
二、基類和派生類對象賦值轉換
三、繼承中的作用域
四、派生類的默認成員函數
五、繼承與友元
六、繼承與靜態(tài)成員
七、菱形繼承和菱形虛擬繼承
7、1 單繼承、多繼承和菱形繼承
7、2 菱形繼承存在的問題
7、3 菱形虛擬繼承
7、3、1?菱形虛擬繼承的認識
7、3、2?菱形虛擬繼承的實現原理
八、繼承的總結和反思
九、總結
???♂??作者:@Ggggggtm????♂?
???專欄:C++? ??
???標題:list講解??
????寄語:與其忙著訴苦,不如低頭趕路,奮路前行,終將遇到一番好風景?????
一、繼承的概念及定義
1、1 繼承的概念
??C++是一種支持面向對象編程(Object-Oriented Programming)的編程語言,面向對象的三大特性:封裝、繼承、多態(tài)。繼承(inheritance)是C++中重要的特性之一。
? 繼承(inheritance)機制是面向對象程序設計使代碼可以復用的最重要的手段,它允許程序員在保持原有類特性的基礎上進行擴展,增加功能,這樣產生新的類,稱派生類。繼承呈現了面向對象程序設計的層次結構,體現了由簡單到復雜的認知過程。以前我們接觸的復用都是函數復用,繼承是類設計層次的復用。
? C++中的繼承可以通過派生類(derived class)從基類(base class)中派生出來?;愂且呀浂x好的類,而派生類則是基于基類的定義創(chuàng)建出來的新類。(派生類又名子類,基類又名父類)。
? 舉個例子:假如我們要對學生和老師信息進行管理,我們分別需要創(chuàng)建class Student和 class Teacher 類。但是我們發(fā)現這兩個類有很多共同的屬性(_name,_age)。能不能把共同的屬性和方法提出到一個類中呢?答案是可以的!
? ?我們把具有共同的屬性和方法的類稱為基類,class Student 和 class Teacher 類這兩個類再次繼承基類。這兩個類中也有基類的屬性和方法。具體也可結合如下代碼理解:
class Person { public: void Print() { cout << "name:" << _name << endl; cout << "age:" << _age << endl; } protected: string _name = "peter"; // 姓名 int _age = 18; ?// 年齡 }; // 繼承后父類的Person的成員(成員函數+成員變量)都會變成子類的一部分。這里體現出了 // Student和Teacher復用了Person的成員。下面我們使用監(jiān)視窗口查看Student和Teacher對象,可 // 以看到變量的復用。調用Print可以看到成員函數的復用。 class Student : public Person { protected: int _stuid; // 學號 }; class Teacher : public Person { protected: int _jobid; // 工號 };
1、2 繼承的定義
? 在上述代碼中,我們初始了繼承。相信大家都有很多疑惑。繼承定義的格式是什么?都有代表的什么意義呢?我們接著往下看,都會做出解釋。
? 上述代碼中,Person是父類,也稱作基類。Student和Teacher是子類,也稱作派生類。
? 其中,繼承方式不僅僅只有public,還有protected和private繼承。具體如下圖:
? 不同的繼承方式會決定派生類對基類成員的訪問權限。具體我們也可看下表:
? 我們只看上表,似乎不容易記住。但是是有規(guī)律的,通過上述的繼承總結出以下幾點:
基類private成員在派生類中無論以什么方式繼承都是不可見的。這里的不可見是指基類的私有成員還是被繼承到了派生類對象中,但是語法上限制派生類對象不管在類里面還是類外面都不能去訪問它。 基類private成員在派生類中是不能被訪問,如果基類成員不想在類外直接被訪問,但需要在派生類中能訪問,就定義為protected??梢钥闯霰Wo成員限定符是因繼承才出現的。 實際上面的表格我們進行一下總結會發(fā)現,基類的私有成員在子類都是不可見?;惖钠渌蓡T在子類的訪問方式 == Min(成員在基類的訪問限定符,繼承方式),public > protected > private。 使用關鍵字class時默認的繼承方式是private,使用struct時默認的繼承方式是public,不過最好顯示的寫出繼承方式。 在實際運用中一般使用都是public繼承,幾乎很少使用protetced/private繼承,也不提倡 使用protetced/private繼承,因為protetced/private繼承下來的成員都只能在派生類的類里 面使用,實際中擴展維護性不強
二、基類和派生類對象賦值轉換
??派生類對象 可以賦值給 基類的對象 / 基類的指針 / 基類的引用。這里有個形象的說法叫切片或者切割。寓意把派生類中父類那部分切來賦值過去。具體如下:
class Person { protected: string _name; // 姓名 string _sex; // 性別 int _age; // 年齡 }; class Student : public Person { public: int _No; // 學號 }; int main() { Student sobj; //注意,以下并不是隱式類型轉換,就是語法支持的 Person pobj = sobj; Person* pp = &sobj; Person& rp = sobj; return 0; }
? 我們也可結合調試看其對象,具體如下圖:
? 指針和引用也是如此。指針指向的內容也會對派生類進行切斷,使其指向的成員只包含父類的成員。但是,基類對象不能賦值給派生類對象。
? 基類的指針或者引用可以通過強制類型轉換賦值給派生類的指針或者引用。但是必須是基類的指針是指向派生類對象時才是安全的。
三、繼承中的作用域
? 在繼承體系中基類和派生類都有獨立的作用域。子類和父類中有同名成員,子類成員將屏蔽父類對同名成員的直接訪問,這種情況叫隱藏,也叫重定義。(在子類成員函數中,可以使用 基類::基類成員 顯示訪問)
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(); };
? 上述代碼就是在調用子類的Print()函數,此時的 _num 是子類中的成員變量。打印父類中的 _num 時,使用的是 基類::基類成員 顯示訪問:Person::_num。
? 需要注意的是如果是成員函數的隱藏,只需要函數名相同就構成隱藏。 我們可結合如下代碼理解:
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); };
? 運行結果如下:
? 我們看到,B中的fun和A中的fun不是構成重載,因為不是在同一作用域。B中的fun和A中的fun構成隱藏,成員函數滿足函數名相同就構成隱藏。
? 通過第一個例子中,Student的_num和Person的_num構成隱藏關系,可以看出這樣代碼雖然能跑,但是非常容易混淆。所以在實際中在繼承體系里面最好不要定義同名的成員。
四、派生類的默認成員函數
? 我們知道一個類會有六個默認成員函數,如下圖:
? 所謂默認,“默認”的意思就是指我們不寫,編譯器會變我們自動生成一個,那么在派生類
中,這幾個成員函數是如何生成的呢?? ?我們先看如下代碼:
class Person { public: //Person(const char* name = "peter") 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: protected: int _num; }; int main() { Student s1; return 0; }
? 派生類Student中,我們自己并沒有寫任何構造函數?,F在,我們定義了一個?Student 對象s1。此時s1對象會自動調用默認的生成的構造函數和析構函數。我們看如下運行結果:
? ?我們看到,這里調用了基類的構造函數和析構函數!那大概就知道派生類默認生成的構造函數會做哪些事情了:派生類的構造函數必須調用基類的構造函數初始化基類的那一部分成員。如果基類沒有默認的構造函數,則必須在派生類構造函數的初始化列表階段顯示調用。
? 那么派生類的默認生成的拷貝構造都會做哪些事情呢?我們看如下代碼:
class Person { public: //Person(const char* name = "peter") Person(const char* name) : _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; } protected: int _num; }; int main() { Student s1("zhagnsan",18); Student s2(s1); return 0; }
? 上述代碼中,我們先定義了一個 s1 對象,然后用 s2?對象去調用默認拷貝構造函數,用s1?對象來初始化。我們看如下運行結果:
? ?我們通過上述的運行結果可發(fā)現,派生類的拷貝構造函數必須調用基類的拷貝構造完成基類的拷貝初始化。
? 通過上述的兩個例子測試,我們不難發(fā)現,派生類默認生成的六個構造函數是有一定規(guī)律的,很初學類和對象生成的六個默認構造很相似。這里直接給出大家結論:
- ?派生類的構造函數必須調用基類的構造函數初始化基類的那一部分成員。如果基類沒有默認的構造函數,則必須在派生類構造函數的初始化列表階段顯示調用。
- ?派生類的拷貝構造函數必須調用基類的拷貝構造完成基類的拷貝初始化。
- ?派生類的operator=必須要調用基類的operator=完成基類的復制。
- ?派生類的析構函數會在被調用完成后自動調用基類的析構函數清理基類成員。因為這樣才能保證派生類對象先清理派生類成員再清理基類成員的順序。
- ?派生類對象初始化先調用基類構造再調派生類構造。
- ?派生類對象析構清理先調用派生類析構再調基類的析構。
因為后續(xù)一些場景析構函數需要構成重寫,重寫的條件之一是函數名相同(這個我們后面會講 解)。那么編譯器會對析構函數名進行特殊處理,處理成destrutor(),所以父類析構函數不加virtual的情況下,子類析構函數和父類析構函數構成隱藏關系。 在派生類所生成的默認構造,會依然保持原有的在類和對象的功能。
? 這里再強調一下派生類中的析構函數。先看如下代碼:
~Student() { ~Person(); cout<<"~Student()" <<endl; }
? 我們上面的派生類 Student 的析構函數會調用基類的析構函數,但是上面的寫法并不正確。因為基類的析構函數和派生類的析構函數構成了隱藏。怎么名字不同也能構成隱藏?其實真正原因是:由于后面多態(tài)的需要,析構函數名字會統(tǒng)一處理成destructor()。
? 當然,我們想要顯式調用時,我們可以使用:Person::~Person()。其實我們也不需要自己去調用基類的析構,每個子類析構函數后面,會自動調用父類析構函數,這樣才能保證先析構子類,再析構父類。
五、繼承與友元
??友元關系不能繼承,也就是說基類友元不能訪問子類私有和保護成員。如下代碼:
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); }
? 上述代碼是不正確的。有緣并不能繼承,所以 s 對象并不能訪問其保護成員。應在 Student 類中聲明友元函數。正確代碼如下:
class Student; class Person { public: friend void Display(const Person& p, const Student& s); protected: string _name="zhangsan"; // 姓名 }; class Student : public Person { friend void Display(const Person& p, const Student& s); protected: int _stuNum=111; // 學號 }; void Display(const Person& p, const Student& s) { cout << p._name << endl; cout << s._stuNum << endl; } int main() { Person p; Student s; Display(p, s); return 0; }
六、繼承與靜態(tài)成員
? 基類定義了static靜態(tài)成員,則整個繼承體系里面只有一個這樣的成員。無論派生出多少個子類,都只有一個static成員實例。? 我們看如下代碼:
class Person { public: Person() { ++_count; } //protected: string _name; // 姓名 public: static int _count; // 統(tǒng)計人的個數。 }; int Person::_count = 0; class Student : public Person { protected: int _stuNum; // 學號 }; int main() { Person p; Student s; p._name = "張三"; cout << s._name << endl; cout << Student::_count << endl; ++Person::_count; cout << Student::_count << endl; cout << &Person::_count << endl; cout << &Student::_count << endl; return 0; }
? 我們在基類中定義了一個 static 變量,該變量可用來統(tǒng)計人數。因為不管是定義派生類對象,還是基類對象,都會調用基類的構造函數。但實例化出來的 _count 只有一份。
七、菱形繼承和菱形虛擬繼承
7、1 單繼承、多繼承和菱形繼承
? 單繼承:一個派生類只能繼承自一個基類,也就是一個子類只有一個直接父類時稱這個繼承關系為單繼承。
? 單繼承的主要特點和優(yōu)點包括:
- 簡單清晰:派生類與基類之間的關系清晰明確,代碼易于理解和維護。
- 避免二義性:沒有多個基類之間的成員重名問題,因此避免了成員函數和變量的二義性。
- 對象模型簡單:派生類對象的內存布局也相對簡單,只需要按照繼承層次從上到下進行構造和析構。
? 多繼承:一個派生類可以同時繼承自多個基類,也就是一個子類有兩個或以上直接父類時稱這個繼承關系為多繼承。
? 多繼承的使用需謹慎,因為它可能引發(fā)一些問題:
- 成員二義性:當派生類直接或間接地從多個基類繼承相同的成員函數或變量時,容易引發(fā)二義性問題。需要通過作用域解析運算符(::)來指明具體的繼承類。
- 破壞封裝性:多繼承可能導致類的設計變得復雜,增加模塊之間的耦合性并破壞封裝性。
? 菱形繼承:菱形繼承是多繼承中的一種特殊情況,指派生類同時繼承自兩個或更多個基類,并且這些基類之間存在共同的基類。?
7、2 菱形繼承存在的問題
? 我們首先看如下一段代碼:
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; // 主修課程 }; int main() { Assistant at; // 菱形繼承的二義性問題 // 數據冗余 //at._name = "張三"; at.Student::_name = "張三"; at.Teacher::_name = "李四"; return 0; }
? ?我們看到,上述代碼就是一個菱形繼承。實際上,Assistant對象的結構就是如下圖:
? ?我們通過調試也很容易觀察出其結構,如下圖:
? 我們不難從上述結構中發(fā)現,Assistant對象成員模型構造,可以看出菱形繼承有數據冗余和二義性的問題。 在Assistant的對象中Person成員會有兩份。同時還有數據冗余的問題。
? 當然,我們訪問 _name 成員變量時,需要顯示指定訪問哪個父類的成員可以解決二義性問題,但是數據冗余問題無法解決。
? 針對上述問題,我們這里引出菱形虛擬繼承。
7、3 菱形虛擬繼承
7、3、1?菱形虛擬繼承的認識
? 在菱形虛擬繼承中,派生類通過 virtual 關鍵字聲明對基類的繼承關系。我們先看一份代碼:
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; // 主修課程 }; int main() { Assistant at; // 菱形虛擬繼承解決了二義性和數據冗余 at._name = "小張"; at.Student::_name = "張三"; at.Teacher::_name = "李四"; return 0; }
? 上述就是菱形虛擬繼承。在上述代碼中,類Student和類Teacher使用virtual關鍵字進行了虛繼承,這樣類Assistant繼承自Student和類Teacher時,只會包含一份類Person的成員變量。我們再通過調試觀察一下,如下:
??虛擬繼承解決了菱形繼承中存在的二義性問題,并減少了資源的重復使用。但需要注意,虛擬繼承會引入虛基表(vtable)以及虛基表指針(vpointer)等額外的開銷。我們在下面講解菱形虛擬繼承的原理中會講到虛基表和虛基表指針。
7、3、2?菱形虛擬繼承的實現原理
? 注:以下測試環(huán)境位vs2019。
? 我們想要搞懂菱形虛擬繼承的實現原理,少不了去了解他在內存中的存儲結構是怎么樣的。為了清晰的觀察,我們采用下面的代碼進行調試:
class A { public: int _a; }; class B : virtual public A { public: int _b; }; 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._a = 0; d._b = 3; d._c = 4; d._d = 5; return 0; }
? 我們通過調試,看數據和內存如下圖:
? 通過內存我們發(fā)現,類 B 和類 C 中不在存儲A對象,而是把A對象放到了D對象組的最底下。類 B 和類 C中多出來的值是什么呢?好像是一段地址,我們不妨看看。如下圖:
? 我們把上述的十六進制轉換為十進制后,發(fā)現一個是20,一個是14。其實我們對比不難發(fā)現,這個就是偏移量。該指針就是虛基表指針,指向的就是虛基表。虛基表中存儲的就有偏移量。我們通過虛基表指針,再加上虛基表中所存儲的偏移量,就可以輕松的找到A對象。
? 通過剖析完菱形虛擬繼承在內存中的存儲結構后,我們對菱形虛擬繼承會有一個更加清楚的認知。會對第一個菱形虛擬繼承例子有一個更加清楚的結構認識,如下圖:
八、繼承的總結和反思
? 對繼承的總結如下:
- 很多人說C++語法復雜,其實多繼承就是一個體現。有了多繼承,就存在菱形繼承,有了菱形繼承就有菱形虛擬繼承,底層實現就很復雜。所以一般不建議設計出多繼承,一定不要設 計出菱形繼承。否則在復雜度及性能上都有問題。
- 多繼承可以認為是C++的缺陷之一,很多后來的語言都沒有多繼承,如Java。
??繼承和組合 :
- 組合是另一種代碼復用機制,它描述了對象之間的整體與部分的關系。在組合關系中,一個類(稱為容器類)包含另一個類的對象(稱為成員對象),并且容器類可以通過成員對象來調用其方法和訪問其屬性。
- 繼承可以實現類之間的"is-a"關系,即派生類是基類的一種特殊類型。通過繼承,可以實現代碼重用、多態(tài)性和層次結構等特性。
- 通過組合,可以實現類之間的"has-a"關系,即容器類具有成員對象作為其一部分。組合提供了更大的靈活性,因為可以在運行時動態(tài)地更改成員對象。
優(yōu)先使用對象組合,而不是類繼承。 繼承允許你根據基類的實現來定義派生類的實現。這種通過生成派生類的復用通常被稱為白箱復用(white-box reuse)。術語“白箱”是相對可視性而言:在繼承方式中,基類的內部細節(jié)對子類可見 。繼承一定程度破壞了基類的封裝,基類的改變,對派生類有很大的影響。派生類和基類間的依賴關系很強,耦合度高。 對象組合是類繼承之外的另一種復用選擇。新的更復雜的功能可以通過組裝或組合對象來獲得。對象組合要求被組合的對象具有良好定義的接口。這種復用風格被稱為黑箱復用(black-box reuse),因為對象的內部細節(jié)是不可見的。對象只以“黑箱”的形式出現。組合類之間沒有很強的依賴關系,耦合度低。優(yōu)先使用對象組合有助于你保持每個類被封裝。 實際盡量多去用組合。組合的耦合度低,代碼維護性好。不過繼承也有用武之地的,有些關系就適合繼承那就用繼承,另外要實現多態(tài),也必須要繼承。類之間的關系可以用繼承,可以用組合,就用組合。
? 我們也可結合如下代碼一起理解上述概念:
// Car和BMW Car和Benz構成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; } }; // Tire和Car構成has-a的關系 class Tire { protected: string _brand = "Michelin"; ?// 品牌 size_t _size = 17; ? ? ? ? // 尺寸 }; class Car { protected: string _colour = "白色"; // 顏色 string _num = "陜ABIT00"; // 車牌號 Tire _t; // 輪胎 };
九、總結
? 繼承是C++一個重要特性之一。我們因該熟知繼承的概念,并且熟練的掌握繼承的用法。相對其它語言,C++的繼承想對復雜,但是我們也應該多練,達到孰能生巧的地步。文章來源:http://www.zghlxwxcb.cn/news/detail-596762.html
? 本篇文章的講解就到這里,感謝閱讀ovo~?文章來源地址http://www.zghlxwxcb.cn/news/detail-596762.html
到了這里,關于【C++】繼承(通俗易懂,超級詳細)的文章就介紹完了。如果您還想了解更多內容,請在右上角搜索TOY模板網以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網!