一文帶你吃透C++繼承
??1、繼承的概念及定義
??1.1繼承的概念
繼承(inheritance)機制是面向?qū)ο蟪绦蛟O(shè)計使代碼可以復(fù)用的最重要的手段,它允許程序員在保持原有類特性的基礎(chǔ)上進行擴展,增加功能,這樣產(chǎn)生新的類,稱派生類。繼承呈現(xiàn)了面向?qū)ο蟪绦蛟O(shè)計的層次結(jié)構(gòu),體現(xiàn)了由簡單到復(fù)雜的認(rèn)知過程。以前我們接觸的復(fù)用都是函數(shù)復(fù)用,繼承是類設(shè)計層次的復(fù)用。
繼承&組合(也稱合成): 是C++實現(xiàn)代碼重用的2種主要方法。
??1.2圖解繼承例子
代碼示例:
class Person { public: void Print() { cout << "name:" << _name << endl; cout << "age:" << _age << endl; } protected: string _name = "jojo"; // 姓名 int _age = 18; // 年齡 }; // 繼承后父類的Person的成員(成員函數(shù)+成員變量)都會變成子類的一部分。這里體現(xiàn)出了Student和 //Teacher復(fù)用了Person的成員。下面我們使用監(jiān)視窗口查看Student和Teacher對象,可以看到變量的復(fù)用。 //調(diào)用Print可以看到成員函數(shù)的復(fù)用。 class Student : public Person { private: int _stuid; // 學(xué)號 }; class Teacher : public Person { protected: int _jobId; // 工號 }; int main() { Student s; Teacher t; s.Print(); t.Print(); return 0; }
??1.3 繼承的語法形式
??1.3.1繼承基類成員訪問方式的變化
總結(jié):
- 基類private成員在派生類中無論以什么方式繼承都是不可見的。這里的不可見是指基類的私有成員還是被繼承到了派生類對象中,但是語法上限制派生類對象不管在類里面還是類外面都不能去訪問它。
- 基類private成員在派生類中是不能被訪問,如果基類成員不想在類外直接被訪問,但需要在派生類中能訪問,就定義為protected。可以看出保護成員限定符是因繼承才出現(xiàn)的
- 使用關(guān)鍵字class時默認(rèn)的繼承方式是private,使用struct時默認(rèn)的繼承方式是public,不過最好顯示的寫出繼承方式。
- 在實際運用中一般使用都是public繼承,幾乎很少使用protetced/private繼承。
- 如果不想讓一個類作為其它類的基類,可以用final關(guān)鍵字阻止它被繼承。
??2、 基類&派生類的關(guān)系
通過繼承,派生類擁有了基類的數(shù)據(jù)成員和函數(shù)成員。
??派生類在基類的基礎(chǔ)上可實施以下改變??:
- 增加新的成員(數(shù)據(jù)+函數(shù))。
- 重載(overload)基類的函數(shù)成員。
- 重定義(override,覆蓋)基類已有的函數(shù)成員。
- 改變基類成員在派生類中的訪問屬性。
??派生類不能繼承基類的以下內(nèi)容??:
1)析構(gòu)函數(shù)。
2) 基類的友元函數(shù)。
3)靜態(tài)成員(數(shù)據(jù)+函數(shù))
4)針對基類定義的一些特殊運算符,如new等。
注意:
派生類繼承了基類的所有成員,但派生類能夠直接訪問從基類繼承來的公有和保護成員,且只能通過這兩類成員訪問從基類繼承來的私有成員。
??3、基類和派生類對象賦值兼容轉(zhuǎn)換
- 派生類對象 可以賦值給 基類的對象 / 基類的指針 / 基類的引用。這里有個形象的說法叫切片或者切割。寓意把派生類中父類那部分切來賦值過去。
- 基類對象不能賦值給派生類對象。
- 基類的指針可以通過強制類型轉(zhuǎn)換賦值給派生類的指針。但是必須是基類的指針是指向派生類對象時才是安全的。這里基類如果是多態(tài)類型,可以使用RTTI(Run-Time Type Information)的dynamic_cast 來進行識別后進行安全轉(zhuǎn)換。
- 基類和派生類的賦值轉(zhuǎn)換的基礎(chǔ)是建立在公有繼承的基礎(chǔ)上的
- 引用類型通常是用.來訪問的,指針類型的訪問就要通過->
代碼示例:
class Person { protected: string _name; // 姓名 string _sex; // 性別 int _age; // 年齡 }; class Student : public Person { public: int _No; // 學(xué)號 }; int main() { Student sobj; // 1.子類對象可以賦值給父類對象/指針/引用 Person pobj = sobj; Person* pp = &sobj; Person& rp = sobj; //2.基類對象不能賦值給派生類對象 //sobj = pobj;這邊會報錯 // 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; }
??4、繼承中的作用域
- 在繼承體系中基類和派生類都有獨立的作用域。
- 子類和父類中有同名成員,子類成員將屏蔽父類對同名成員的直接訪問,這種情況叫隱藏,也叫重定義。(在子類成員函數(shù)中,可以使用 基類::基類成員 顯示訪問)
- 需要注意的是如果是成員函數(shù)的隱藏,只需要函數(shù)名相同就構(gòu)成隱藏。
- 注意在實際中在繼承體系里面最好不要定義同名的成員。
隱藏關(guān)系代碼舉例:
class Base { //基類 public: Base(int a = 0) { k = a; } int k; //為演示,將數(shù)據(jù)成員聲明為公有 void fn1() { cout << "Base::fn1()" << endl; } void fn2() { cout << "Base::fn2()" << endl; } }; class Derived : public Base { //派生類 public: void fn1() { cout << "Devired::fn1()" << endl; }//構(gòu)成隱藏 void fn2() { cout << "Devired::fn2()" << endl; } //如果我們想訪問基類的fn1函數(shù)可以指定作用域 }; int main() { Derived d; cout << "k=" << d.k << endl; d.fn1(); d.fn2(); d.Base::fn1(); }
??5、派生類的默認(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)。
已知父類代碼Person如下:
class Person { public: Person(const char* name = "peter")//有參的構(gòu)造函數(shù) : _name(name) { cout << "Person()" << endl; } Person(const Person& p)//Person給拷貝構(gòu)造函數(shù) : _name(p._name) { cout << "Person(const Person& p)" << endl; } Person& operator=(const Person& p)//operator重載= { cout << "Person operator=(const Person& p)" << endl; if (this != &p) _name = p._name; return *this; } ~Person()//析構(gòu)函數(shù) { cout << "~Person()" << endl; } protected: string _name; // 姓名 };
如何實現(xiàn)子類的拷貝構(gòu)造?派生類的拷貝構(gòu)造函數(shù)必須調(diào)用基類的拷貝構(gòu)造完成基類的拷貝初始化
子類有一個Int 類型的成員_num。
子類完整代碼示例:
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; //學(xué)號 };
- 子類里面要不要顯示調(diào)用父類析構(gòu)函數(shù)的問題?
答案是不用,因為父子類的析構(gòu)函數(shù)構(gòu)成隱藏關(guān)系,子類會自動調(diào)用父類的析構(gòu)函數(shù)。
析構(gòu)名稱會被統(tǒng)一處理成destructor()
- 如何設(shè)計一個不能被繼承的類?
答案:構(gòu)造函數(shù)私有,這樣子類就無法初始化基類對象,從而不能被繼承。
- 派生類對象的構(gòu)造和析構(gòu)順序
派生類對象的構(gòu)造順序:
步驟1:先構(gòu)造基類;
步驟2:再構(gòu)造對象成員;
步驟3:最后構(gòu)造派生類自身;
- 派生類什么時候可以不定義構(gòu)造函數(shù)?
當(dāng)基類 和 所有 對象成員具有無參構(gòu)造函數(shù)時!
無參構(gòu)造函數(shù)細分為3種情況: (1)沒有定義任何構(gòu)造函數(shù); (2)具有[重定義的]無參構(gòu)造函數(shù); (3)具有缺省參數(shù)的構(gòu)造函數(shù)。
- 派生類什么時候“必須”定義構(gòu)造函數(shù)?
答案:當(dāng)基類或?qū)ο蟪蓡T所屬類只含有帶參數(shù)的構(gòu)造函數(shù)時。即使派生類本身沒有數(shù)據(jù)成員要初始化,它也必須定義構(gòu)造函數(shù)!以“構(gòu)造函數(shù)初始化列表”的方式向基類和對象成員的構(gòu)造函數(shù)傳遞參數(shù),以實現(xiàn)基類子對象和對象成員的初始化。
??6、繼承與友元、靜態(tài)函數(shù)
友元關(guān)系不能繼承,也就是說基類友元不能訪問子類私有和保護成員
**如果一個類繼承了其它類,則它聲明的友元也只能訪問它自己的全體成員,以及從基類繼承到的public和protected成員。**而它的基類和派生類并不認(rèn)可這種友元關(guān)系,按照規(guī)則只能訪問公有成員。
繼承和靜態(tài)的關(guān)系:
- 基類定義了static靜態(tài)成員,則整個繼承體系里面只有一個這樣的成員。無論派生出多少個子類,都只有一個static成員實例 。
- 基類靜態(tài)成員為繼承層次結(jié)構(gòu)所有類共享;
代碼示例:
class Person { public : Person () {++ _count ;} protected : string _name ; // 姓名 public : static int _count; // 統(tǒng)計人的個數(shù)。 }; int Person :: _count = 0; class Student : public Person { protected : int _stuNum ; // 學(xué)號 }; class Graduate : public Student { protected : string _seminarCourse ; // 研究科目 }; void TestPerson() { Student s1 ; Student s2 ; Student s3 ; Graduate s4 ; cout <<" 人數(shù) :"<< Person ::_count << endl; Student ::_count = 0; cout <<" 人數(shù) :"<< Person ::_count << endl; }
??7、復(fù)雜的菱形繼承及菱形虛擬繼承
單繼承:一個子類只有一個直接父類時稱這個繼承關(guān)系為單繼承
多繼承:一個子類有兩個或以上直接父類時稱這個繼承關(guān)系為多繼承
菱形繼承:菱形繼承是多繼承的一種特殊情況。
菱形繼承的問題:從下面的對象成員模型構(gòu)造,可以看出菱形繼承有數(shù)據(jù)冗余和二義性的問題。在Assistant的對象中Person成員會有兩份。
需要顯示指定訪問哪個父類的成員可以解決二義性問題
class Person { public: string _name; // 姓名 }; class Student : public Person { protected: int _num; //學(xué)號 }; class Teacher : public Person { protected: int _id; // 職工編號 }; class Assistant : public Student, public Teacher { protected: string _majorCourse; // 主修課程 }; void Test() { // 這樣會有二義性無法明確知道訪問的是哪一個 Assistant a; //a._name = "peter"; // 需要顯示指定訪問哪個父類的成員可以解決二義性問題,但是數(shù)據(jù)冗余問題無法解決 a.Student::_name = "xxx"; a.Teacher::_name = "yyy"; }
解決二義性的方法除了指定訪問的父類,也可以通過虛擬繼承.
虛擬基類:使得派生類中只存在同一基類的一份拷貝,解決了基類 數(shù)據(jù)成員的二義性問題;
??虛擬繼承的定義??
舉例:
class Student: virtual public Person{……} //Person為虛基類 class Employee: virtual public Person{……} //Person為虛基類 class StuEmployee:public Student,public Employee{……}
代碼示例:
class A { public: void vf() { cout<<"I come from class A"<<endl; } }; class B: virtual public A{ }; class C: virtual public A{ }; class D: public B, public C{ }; void main(){ D d; d.vf(); // 正確 }
繼承關(guān)系如圖所示——虛基類!
??總結(jié)
本文和大家總結(jié)了C++繼承的幾個要點,從繼承的概念、定義出發(fā),再到基類和派生類之間的關(guān)系,賦值兼容轉(zhuǎn)換,再到繼承中的作用域、成員函數(shù)等,再淺談了C++的復(fù)雜菱形繼承等,希望本文對大家有所幫助!
以上就是一文帶你吃透C++繼承的詳細內(nèi)容,更多關(guān)于C++ 繼承的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
C++實現(xiàn)簡單的生產(chǎn)者-消費者隊列詳解
這篇文章主要為大家詳細介紹了如何利用C++實現(xiàn)一個簡單的生產(chǎn)者-消費者隊列,文中的示例代碼講解詳細,感興趣的小伙伴可以了解一下2023-04-04C++實現(xiàn)LeetCode(111.二叉樹的最小深度)
這篇文章主要介紹了C++實現(xiàn)LeetCode(111.二叉樹的最小深度),本篇文章通過簡要的案例,講解了該項技術(shù)的了解與使用,以下就是詳細內(nèi)容,需要的朋友可以參考下2021-07-07解析Linux下的時間函數(shù):設(shè)置以及獲取時間的方法
本篇文章是對Linux下的時間函數(shù):設(shè)置以及獲取時間的方法進行了詳細的分析介紹,需要的朋友參考下2013-05-05