繼承
繼承的概念
繼承機制是面向對象程序設計使代碼可以復用的最重要的手段,它允許程序員在保持原有類特性的基礎上進行擴展,增加功能。繼承呈現了面向對象程序設計的層次結構,體現了由簡單到復雜的認知過程。
繼承的定義
上面的基類也可以叫父類,派生類也可以叫子類。
繼承關系和訪限定符
繼承方式
接下來用代碼測試上面的繼承方式
class Person { public : void Print () { cout<<_name <<endl; } protected : string _name = "張三" ; // 姓名 private : int _age = 18 ; // 年齡 }; class Student : public Person { protected : int _stunum = 22; // 學號 };
public繼承
上面是給的缺省值來測試沒寫構造函數
s就繼承了Person的name,age,基類中private的age在物理上繼承了但在語法上但是不能訪問的。
也可以調用基類的成員函數,但是不能直接訪問基類中private的成員,prootected可以在派生類中訪問,不能再在類外訪問
protected繼承
protected繼承,在類外連基類的public成員函數都不能用了,只能在派生類的類里面使用。
同樣基類中私有的不能訪問
private繼承就都是私有的了。
總結:
- 1.基類private成員在派生類中無論以什么方式繼承都是不可見的。這里的不可見是指基類的私有成員還是被繼承到了派生類對象中,但是語法上限制派生類對象不管在類里面還是類外面都不能去訪問它。
- 2.基類private成員在派生類中是不能被訪問,如果基類成員不想在類外直接被訪問,但需要在派生類中能訪問,就定義為protected??梢钥闯霰Wo成員限定符是因繼承才出現的.
- 3.基類的私有成員在子類中都是不可見的,其他成員在子類中等于權限最小的那個
- 4.class的默認繼承方式是private,struct默認的繼承方式是public,最好顯示的寫出繼承方式
- 5.在實際應用一般使用public繼承,很少使用protected和private。
父類和子類對象賦值轉化
class Person { protected: string _name; // 姓名 string _sex; // 性別 int _age; // 年齡 }; class Student : public Person { public: int _No; // 學號 };
子類可以給父類,父類不能給子類,不僅可以是子類的對象,也可以是指針和引用
Student s; Person p; p = s; Person *ptr = &s;//子類賦給父類指針 Person &ref = s;//子類賦給父類引用
派生類對象 可以賦值給 基類的對象 / 基類的指針 / 基類的引用。這里有個形象的說法叫切片或者切割。寓意把派生類中父類那部分切來賦值過去。
基類對象不能賦值給派生類對象
基類的指針可以通過強制類型轉換賦值給派生類的指針。但是必須是基類的指針是指向派生類對象時才是安全的。等到子類中的默認函數就會用到切片
繼承中的作用域
class Person { protected: string _name = "法外狂徒"; // 姓名 int _num = 11; // 身份證號 }; class Student : public Person { public: void Print() { cout << " 姓名:" << _name << endl; cout << " 身份證號:" << _num << endl; cout << " 學號:" << _num << endl; } protected: int _num = 2; // 學號 };
還有成員函數的隱藏
class A { public: void fun(double x) { cout << "fun()->x"<< x << endl; } }; class B : public A { public: void fun(int i) { cout << "fun()->" << i << endl; } }; int main() { B b; b.fun(10); b.A::fun(11.1);//加作用域 return 0; }
父類和子類函數名相同不是重載而是隱藏,函數重載是在同一作用域,不同的作用域是隱藏
在子類成員函數中,可以使用 基類::基類成員 顯示訪問
在寫代碼中最好不要定義同名的成員
子類的默認成員函數
在類和對象的時候講了6個默認的成員函數,現在子類中講4個,構造,拷貝構造,賦值和析構
class Person //父類 { public: 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; } //拷貝構造 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; //學號 };
總結:
- 派生類的構造函數必須調用基類的構造函數初始化基類的那一部分成員。如果基類沒有默認的構造函數,則必須在派生類構造函數的初始化列表階段顯示調用。
- 派生類的拷貝構造函數必須調用基類的拷貝構造完成基類的拷貝初始化。
- 派生類的operator=必須要調用基類的operator=完成基類的復制。
- 派生類的析構函數會在被調用完成后自動調用基類的析構函數清理基類成員。因為這樣才能保證派生類對象先清理派生類成員再清理基類成員的順序。
- 派生類對象初始化先調用基類構造再調派生類構造。
- 派生類對象析構清理先調用派生類析構再調基類的析構。
繼承與友元
友元關系不能繼承,父類友元不能訪問子類私有和保護成員
class Student; class Person { public: friend void Display(const Person& p, const Student& s); protected: string _name; // 姓名 }; class Student : public Person { public: friend void Display(const Person& p, const Student& s); protected: int _stuNum; // 學號 }; 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; }
繼承與靜態成員
基類定義了static靜態成員,則整個繼承體系里面只有一個這樣的成員。無論派生出多少個子類,都只有一個static成員實例 。
class Person { public: Person() { ++_count; } protected: string _name; // 姓名 public: static int _count; // 統計人的個數。 }; int Person::_count = 0; class Student : public Person { protected: int _id; // 學號 }; class Graduate : public Student { protected: string _Course; // 科目 }; int main() { Student s1; Student s2; Student s3; Graduate s4; cout << " 人數 :" << Person::_count << endl; cout << " 人數 :" << Student::_count << endl; cout << " 人數 :" << &Person::_count << endl; cout << " 人數 :" << &Student::_count << endl; return 0; }
再加上count的地址可以看出是同一個的count。計算出子類實例化了多少個對象就可以在父類中定義個count自加。
復雜的菱形繼承
單繼承:一個子類只有一個直接父類時稱這個繼承關系為單繼承
多繼承:一個子類有兩個或以上直接父類時稱這個繼承關系為多繼承
菱形繼承:菱形繼承是多繼承的一種特殊情況。
菱形繼承的問題:從下面的對象成員模型構造,可以看出菱形繼承有數據冗余和二義性的問題。在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 _Course; // 課程 }; int main() { // 這樣會有二義性無法明確知道訪問的是哪一個 Assistant a; //a._name = "peter"; // 顯示的調用解決了二義性,但數據冗余了 a.Student::_name = "蓋倫"; a.Teacher::_name = "亞索"; return 0; }
虛繼承
虛擬繼承可以解決菱形繼承的二義性和數據冗余的問題。如上面的繼承關系,在Student和Teacher的繼承Person時使用虛擬繼承,即可解決問題。需要注意的是,虛擬繼承不要在其他地方去使用。
在菱形的腰部加上virtual關鍵字可以解決冗余。那虛繼承是怎么解決的呢?先來看看不用虛繼承的
class A { public: int _a; }; class B : public A { public: int _b; }; class C : 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; }
我們可以通過內存窗口來觀察對象成員的模型
菱形繼承帶來了二義性和數據冗余。
再來看看虛繼承的
虛繼承就解決了數據冗余和二義性,B和C中多了地址,在用內存窗口看看這里的地址
2個指針叫虛基表指針指向虛基表,可以通過偏移量找到公共虛基類,此時A是在下面那為什么要找呢?
D d; d.B::_a = 1; d.C::_a = 2; d._b = 3; d._c = 4; d._d = 5; B b = d;//把d賦給b,把d切過去那此時要怎么找到A呢?,所以就要用虛基表找
B類中各個成員在內存中的分布:
通過偏移量找到虛基類。
還是不要用菱形繼承出現問題,虛繼承使得對象模型很復雜,并且會有效率的影響。
繼承的總結
- C++語法復雜,其實多繼承就是一個體現。有了多繼承,就存在菱形繼承,有了菱形繼承就有菱形虛擬繼承,底層實現就很復雜。所以一般不建議設計出多繼承,一定不要設計出菱形繼承。否則在復雜度及性能上都有問題。
- 多繼承可以認為是C++的缺陷之一.
組合
繼承是建立了父類與子類的關系,是一種“是”的關系,例如白貓是貓,組合是“有”的關系實際盡量多去用組合。組合的耦合度低,代碼維護性好。不過繼承也有用武之地的,有些關系就適合繼承那就用繼承,另外要實現多態,也必須要繼承。類之間的關系可以用繼承,可以用組合就用組合。
面試題
- 什么是菱形繼承?菱形繼承的問題是什么?
菱形繼承是多繼承的一種特殊繼承,兩個子類繼承同一個父類,而又有子類同時繼承這兩個子類??梢钥闯隽庑卫^承有數據冗余和二義性的問題。- 什么是菱形虛擬繼承?如何解決數據冗余和二義性的
在菱形繼承的腰部加上virtual,通過虛基表指針和虛基表中的偏移量可以找到虛基類,只存1份- 繼承和組合的區別?什么時候用繼承?什么時候用組合?
繼承是一種"是",組合是"有"的關系,父類和子類是的關系用繼承,是有的關系用組合。
以上就是C++繼承,由于作者水平有限,如有問題還請指出!
到此這篇關于C++繼承模式詳解的文章就介紹到這了。希望對大家的學習有所幫助,也希望大家多多支持服務器之家。
原文鏈接:https://blog.csdn.net/weixin_45599288/article/details/121706542?