文章目錄
- 一、繼承概念
- 二、繼承定義
- 定義格式
- 繼承后基類成員訪問方式的變化
- 類模板的繼承
- 三、基類和派?類間的轉換(賦值兼容轉換)
- 四、繼承中的作用域
- 隱藏規則
- 兩道筆試常考題
- 五、派生類的默認成員函數
- 四個常見默認成員函數
- 實現?個不能被繼承的類
- 六、繼承與友元
- 七、繼承與靜態成員
- 八、多繼承及其菱形繼承問題
- 繼承模型
- 虛繼承
- 九、繼承和組合
一、繼承概念
繼承(inheritance)機制是?向對象程序設計使代碼可以復?的最重要的?段,它允許我們在保持原有類特性的基礎上進?擴展,增加?法(成員函數)和屬性(成員變量),這樣產?新的類,稱派?類。繼承呈現了?向對象程序設計的層次結構,體現了由簡單到復雜的認知過程。以前我們接觸的函數層次的復?,繼承是類設計層次的復?。
下面小編來舉個例子,如果要創建兩個類teacher和student,我們就可以把類公有的信息或者函數比如姓名/地址/電話/年齡等成員變量,都有identity?份認證的成員函數放在父類里,創建teacher和student時直接繼承父類里的內容,把類獨有的定義在各自的子類里,這樣就可以簡化代碼,避免重復定義。
//父類
class Person
{
public:// 進?校園/圖書館/實驗室刷?維碼等?份認證void identity(){cout << "void identity()" << _name << endl;}
protected:string _name = "張三"; // 姓名string _address; // 地址string _tel; // 電話int _age = 18; // 年齡
};//子類
class Student : public Person
{
public:// 學習void study(){// ...}
protected:int _stuid; // 學號
};//子類
class Teacher : public Person
{
public:// 授課void teaching(){//...}
protected:string title; // 職稱
};
二、繼承定義
定義格式
下?我們看到Person是基類,也稱作?類。Student是派?類,也稱作?類。
繼承方式有三種,和我們之前介紹的訪問限定符同名。
繼承后基類成員訪問方式的變化
我們前面介紹了繼承方式和訪問限定符,那么父類的訪問限定符里的內容繼承到子類后在子類中的訪問方式是怎樣的呢?我們首先要清楚一共有9中排列組合的方式,因父類中的一種訪問方式有三種繼承方式,詳情見下圖:
回顧:類中的public成員是類里類外都可以訪問,protect和private成員是類外不可訪問,類里可以訪問。
1、我們先看表格中最特別的最后一行,在基類的private成員不論什么繼承方式在子類中都不可見,不可見是比private更高一級的限制,它是指不可見的內容在派生類的類里類外都無法訪問,當然這里的無法訪問不是絕對的,可以通過派生類里的共有或者保護成員函數間接訪問,在實踐中我們是很少把基類成員定義為私有的。
2、剩下的六種成員在派生類里的訪問方式是將成員在基類的訪問限定符和繼承方式相比較取小,大小關系如下:public > protected > private
3、基類private成員在派?類中是不能被訪問,如果基類成員不想在類外直接被訪問,但需要在派?類中能訪問,就定義為protected。可以看出保護成員限定符是因繼承才出現的。
4、繼承方式也可以像訪問限定符一樣不顯示寫,使?關鍵字class定義派生類時默認的繼承?式是private,使?struct時默認的繼承?式是public,不過最好顯?的寫出繼承?式。
5、在實際運?中?般使?都是public繼承,?乎很少使?protetced/private繼承,也不提倡使?protetced/private繼承,因為protetced/private繼承下來的成員都只能在派?類的類??使?,實際中擴展維護性不強。
類模板的繼承
在此之前小編先科普一下復用,復用有兩種方式,一種的組合,也就是我們熟悉的容器適配器模式,類里面直接包含,我直接包含你比如stack類直接包含deque,有些大佬稱之為has-a,還有一種就是繼承,一個類繼承于另一個類,我是一個特殊的你,這是is-a。
小編回到主題來講類模板的繼承,我們前面實現的普通類型父類繼承到子類后子類是可以直接調用父類的函數的,但如果是類模板的話,子類是無法直接調用父類的成員函數的,因為父類是模板沒有實例化成具體代碼,所以編譯器無法確定父類成員的合法性,需要顯示聲明函數的來源才能調用,比如下面通過訪問限定符:
namespace bit
{//template<class T>//class vector//{};// stack和vector的關系,既符合is-a,也符合has-atemplate<class T>class stack : public vector<T>{public:void push(const T& x){// 基類是類模板時,需要指定?下類域,// 否則編譯報錯:error C3861: “push_back”: 找不到標識符// 因為stack<int>實例化時,也實例化vector<int>了// 但是模版是按需實例化,push_back等成員函數未實例化,所以找不到//push_back(x);vector<T>::push_back(x);}void pop(){vector<T>::pop_back();}const T& top(){return vector<T>::back();}bool empty(){return vector<T>::empty();}};
三、基類和派?類間的轉換(賦值兼容轉換)
首先我們要明確兩個普通類是無法像下面三種方式一樣轉換的。
1、通常情況下我們把一個類型的對象賦值給另一個類型的指針或者引用時,不一般會發生構造+拷貝構造,存在類型轉換,中間會產生臨時對象,所以需要加 const,如:int a = 1; const double& d = a;。 在C++中,完全獨立的類型是不支持隱式類型轉換的,除非兩個類有繼承關系。public 繼承中,就是一個特殊處理的例外,派生類對象可以賦值給基類的指針 / 基類的引用,而不需要加 const,(意味著沒有產生臨時對象)這里的指針和引用綁定是派生類對象中的基類部分,如下圖所示。也就意味著一個基類的指針或者引用,可能指向基類對象,也可能指向派生類對象。
2、除了給引用和指針,子類對象也可以直接賦值給父類對象。派生類對象賦值給基類對象是通過基類的拷貝構造函數或者賦值重載函數完成的
(這兩個函數的細節后面小節會細講),這個過程就像派生類自己定義部分成員切掉了一樣,所以也被叫做切割或者切片,如下圖中所示。
3、基類對象不能賦值給派生類對象。
4、基類的指針或者引用可以通過強制類型轉換賦值給派生類的指針或者引用。但是必須是基類的指針是指向派生類對象時才是安全的。這里基類如果是多態類型,可以使用RTTI (Run-Time Type Information) 的 dynamic_cast 來進行識別后進行安全轉換。(我們后面在多態章節再細講)
四、繼承中的作用域
隱藏規則
- 在繼承體系中基類和派?類都有獨?的作?域,所以基類和派生類中可以定義同名成員,在派生類成員中訪問同名成員時默認會先訪問派生類的,派生類沒有才回去訪問基類的,如果只想訪問基類的同名成員可以指定作用域訪問。
- 派?類和基類中有同名成員,派?類成員將屏蔽基類對同名成員的直接訪問,這種情況叫隱藏。成員變量隱藏的底層邏輯是作用域查找規則,因為訪問一個變量時會去找它的定義,查找順序是默認會先在派生類查找,派生類沒有才會去基類找。(在派?類成員函數中,可以使? 基類::基類成員 顯?訪問)
- 需要注意的是如果是成員函數的隱藏,只需要函數名相同就構成隱藏,成員函數隱藏的底層邏輯是函數隱藏規則,當派生類定義了一個與基類同名的函數(不管參數列表是否相同),基類中的同名函數就會被隱藏。我們以下面例題的代碼為例,一旦在派生類中定義了 void fun(int i),基類中的 void fun() 在派生類的作用域內就不再可見,并且也編譯器也不會主動再到基類里查找是否有匹配的函數。(除非使用作用域解析運算符 :: 顯式指定調用基類版本 )。
- 注意在實際中在繼承體系??最好不要定義同名的成員。
兩道筆試常考題
1、A和B類中的兩個func構成什么關系()
A. 重載 B. 隱藏 C.沒關系
2、下?程序的編譯運?結果是什么()
A. 編譯報錯 B.運?報錯 C. 正常運?
class A
{
public:void func(){cout << "func()" << endl;}
};class B : public A
{
public:void func(int i){cout << "func(int i)" << i << endl;}
};int main()
{B b;b.func(10);b.func();return 0;
};
首先要明確函數重載要求在同一作用域,這里顯然在兩個作用域,又因為兩個函數同名,符合隱藏規則的第三點,所以第一題選B。
第二題我們根據隱藏規則的第三點,當調用 b.fun() 時,因為函數隱藏規則,編譯器只會在派生類 B 的作用域內查找匹配的函數。由于在 B類中只定義了 void fun(int i) ,沒有 void fun() ,所以編譯器找不到匹配的函數,就會報錯,選A。
五、派生類的默認成員函數
基類的默認成員函數符合我們在類和對象章節介紹的規則,這里我們來探討派生類的默認成員函數。學習默認成員函數要從以下幾個方面入手:我們不寫編譯器會不會自動生成、怎么生成?默認生成的符不符合我們預期?不符合預期話我們自己寫要如何寫?
四個常見默認成員函數
我們首先要認識到默認成員函數是用來處理成員變量的,派生類的默認成員函數的行為和普通類的默認成員函數高度相類似,只是派生類的成員變量有兩部分,派生類的默認成員函數會把兩個部分的成員變量分開處理,一個是繼承自父類的基類成員,另一個是派生類自己定義的派生類成員,其中派生類成員的處理方式和普通類一樣,并且是由派生類自己的的默認成員函數處理。基類成員會被打包看成一個整體,然后調用基類的默認成員函數,統一由基類的默認成員函數處理。可以簡單理解成派生類相比普通類多了一個自定義類型成員。
小記:const成員、引用成員,沒有默認構造的自定義類型成員都必須顯示在構造函數的初始化列表初始化。(默認構造對其他成員可以隨便給一個值,反正在類里還可以賦值修改,const成員和引用成員無法做到)
const成員、引用成員在類的作用域里只有一次給值也就是初始化的機會,引用成員規定不能先定義再初始化,因為引用只能引用一個成員無法變更去引用其他成員,const成員一旦定義后就無法更改了所以它倆都必須在定義時就初始化,在類里變量都要最先走初始化列表初始化,所以const成員、引用成員只能顯示在初始化列表初始化。
在初始化列表階段編譯器會自動調用自定義類型成員的默認構造函數,如果這個自定義類型成員沒有默認構造函數,編譯器就無法完成自動初始化,構造函數體內部的代碼是在所有成員都完成初始化之后才執行的,如果成員的類沒有默認構造函數,編譯器在進入函數體之前就會因無法初始化該成員而報錯,(語法規定允許內置類型在進入構造函數體之前不初始化,自定義類型成員必須在進入構造函數體之前初始化)所以沒有默認構造的自定義類型成員也必須在初始化列表顯示初始化。
接下來我們以下面這段代碼為例依次介紹派生類的其中4個最重要的默認構造函數:
class Person
{
public: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; //學號string _address;
};int main()
{Student s1;return 0;
}
- 構造函數
派?類的構造函數必須調?基類的默認構造函數初始化基類的那?部分成員。如果基類沒有默認的構造函數,就相當于我們上面小記里的第三種特殊情況,則必須在派?類構造函數的初始化列表階段顯?調?。
上面這段代碼因為基類有默認構造函數初始化派生類里的基類成員,代碼不會報錯,如果基類沒有默認構造那么我們就需要自己顯示寫構造函數來初始化基類成員了,基類成員必須調用基類的構造函數初始化,并且調用只能走派生類構造函數的初始化列表,因為系統默認先初始化父類的成員(通過父類的構造函數),再初始化子類的成員,最后執行子類構造函數體的代碼,(如果有成員未初始化是不會進入構造函數體內部的)而且只有這樣可以避免先初始化子類成員導致訪問未初始化的父類資源,引發未定義行為。這也和我們下面介紹的析構函數順序對稱。
class Student : public Person
{
public:Student(const char* name, int num, const char* address)// 顯示調用基類構造函數初始化基類成員 :Person(name) //:_name(name) 錯誤寫法,_num(num),_address(address){ }protected:int _num; //學號string _address;
};
(自己想的補充:兩個獨立的類之間,不能像子類調用父類構造函數那樣
“跨類初始化成員”,但可以在一個類的初始化列表中調用另一個類的構造函數,來初始化自身包含的該類類型成員。)
- 拷貝構造
派?類的拷?構造函數必須調?基類的拷?構造完成基類的拷?初始化。一般拷貝構造都不需熬我們自己寫,除非需要深拷貝。構造函數一般都要自己寫,因為需要傳參去構造。
派?類的拷?構造函數拷貝父類成員就需要我們在派生類中傳一個父類對象的引用去調用父類的拷?構造函數,派生類調用基類拷貝構造過程中不會產生臨時對象,首先賦值兼容轉換不會產生,然后調用基類拷貝構造時因為是同類型直接構造也不會產生臨時對象。
下面的例子其實不用手動寫編譯器默認生成的就夠用了,這里小編只是演示一下如果需要自己手動寫要如何寫。
Student(const Student& s): Person(s) //調用父類拷貝構造(賦值兼容轉換支持), _num(s._num), _address(s._address)
{//編譯器自動生成的就夠用了//存在深拷貝才自己寫
}
- 賦值運算符重載
- 派?類的operator=必須要調?基類的operator=完成基類的復制。需要注意的是派?類和基類的賦值運算符重載是同名函數,所以派?類的operator=隱藏了基類的operator=,所以顯?調?基類的operator=,需要指定基類作?域。
//因為要判斷避免自己給自己賦值所以不走初始化列表
Student& operator=(const Student& s)
{if (this != &s){Person::operator=(s); //同名函數必須指定作用域_num = s._num;_address = s._address;}return *this;
}
- 析構函數
這里要注意一點派生類的析構函數規則和前面三個成員函數不同,派生類的析構函數只用顯示釋放派生類自己創建的資源,如果有資源的話。因為派生類的析構執行完畢后會自動調用基類的析構,如果在派生類的析構函數中再顯示釋放基類資源的話就會釋放兩次。
析構資源的順序和構造正好相反,先析構子類,再析構父類。如果先析構父類那么就有可能因為在子類中訪問父類被析構的成員而報錯,若先析構子類的話父類是訪問不到子類的成員的。
所以這里我們也明白了為什么基類析構函數是自動調用。系統為了保證先析構子類,再析構父類這個順序,所以在執行完派生類構造函數后會去自動調用基類的構造函數,如果在派生類構造函數中顯示析構父類和派生類的話這個順序就無法保證。
實現?個不能被繼承的類
?法1:基類的構造函數私有,派?類的基類成員必須調?基類的構造函數初始化,但是基類的構造函數私有化以后,派?類看不?無論是顯示還是編譯器自動都無法調用到基類的構造函數了,那么派?類就?法實例化出對象。(語法規定實例化對象必須經過兩個步驟:分配內存和初始化成員)這樣將基類的構造函數私有后基類也無法實例化出對象了。
?法2:C++11新增了?個final關鍵字,final修改基類,派?類就不能繼承了。
// C++11的?法
class Base final
{
public:void func5() { cout << "Base::func5" << endl; }
protected:int a = 1;
private:// C++98的?法//Base()//{}
};class Derive :public Base
{void func4() { cout << "Derive::func4" << endl; }
protected:int b = 2;
};int main()
{Base b;Derive d;return 0;
}
六、繼承與友元
友元關系不能繼承,也就是說基類友元不能訪問派?類私有和保護成員。解決方法就是讓基類友元也成為派?類的友元。
//前置聲明
class Student;class Person
{
public:friend void Display(const Person& p, const Student& s);
protected:string _name; // 姓名
};
class Student : public Person
{//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;// 編譯報錯:error C2248: “Student::_stuNum”: ?法訪問 protected 成員// 解決?案:Display也變成Student 的友元即可Display(p, s);return 0;
}
七、繼承與靜態成員
靜態成員會被繼承。基類定義了static靜態成員,則整個繼承體系??只有?個這樣的成員。?論派?出多少個派?類,都只有?個static成員實例。
class Person
{
public:string _name;static int _count;
};
int Person::_count = 0;
class Student : public Person
{
protected:int _stuNum;
};
int main()
{Person p;Student s;// 這?的運?結果可以看到?靜態成員_name的地址是不?樣的// 說明派?類繼承下來了,?派?類對象各有?份cout << &p._name << endl;cout << &s._name << endl;// 這?的運?結果可以看到靜態成員_count的地址是?樣的// 說明派?類和基類共?同?份靜態成員cout << &p._count << endl;cout << &s._count << endl;// 公有的情況下,?派?類指定類域都可以訪問靜態成員cout << Person::_count << endl;cout << Student::_count << endl;return 0;
}
八、多繼承及其菱形繼承問題
繼承模型
單繼承:?個派?類只有?個直接基類時稱這個繼承關系為單繼承
多繼承:?個派?類有兩個或以上直接基類時稱這個繼承關系為多繼承,多繼承對象在內存中的模型是,先繼承的基類在前?,后?繼承的基類在后?,派?類成員在放到最后?。
菱形繼承:菱形繼承是多繼承的?種特殊情況。菱形繼承的問題,從下?的對象成員模型構造,可以看出菱形繼承有數據冗余和?義性的問題,在Assistant的對象中Person成員會有兩份。?持多繼承就?定會有菱形繼承,像Java就直接不?持多繼承,規避掉了這?的問題,所以實踐中我們也是不建議設計出菱形繼承這樣的模型的。
數據冗余是指同一個基類的數據在派生類中存在多份拷貝,浪費內存空間,也可能導致數據不一致。
?義性小編用下面的例子解釋,當Assistant對象訪問基類Person對象成員_name時編譯器無法確定應該訪問來自Student的_name還是Teacher的。
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()
{// 編譯報錯:error C2385: 對“_name”的訪問不明確Assistant a;//a._name = "peter";// 需要顯?指定訪問哪個基類的成員可以解決?義性問題,但是數據冗余問題?法解決a.Student::_name = "xxx";a.Teacher::_name = "yyy";return 0;
}
虛繼承
虛繼承是用來解決菱形繼承數據冗余和?義性的。
很多?說C++語法復雜,其實多繼承就是?個體現。有了多繼承,就存在菱形繼承,有了菱形繼承就有菱形虛擬繼承,底層實現就很復雜,性能也會有?些損失,所以最好不要設計出菱形繼承。多繼承可以認為是C++的缺陷之?,后來的?些編程語?都沒有多繼承,如Java。
虛繼承格式是在繼承方式符前面加virtual,例子如下。虛繼承應在父類派生多個子類時加,多個父類派生一個子類是正常的多繼承,不用加。
虛繼承的底層原理小編大致說一下,就相當于把person 從student和teacher里拿出來,放在Assistant對象整體的開頭或者結尾,具體看編譯器。
class Person
{
public:string _name; // 姓名/*int _tel;* int _age;string _gender;string _address;*/// ...
};// 使?虛繼承Person類
class Student : virtual public Person
{
protected:int _num; //學號
};// 使?虛繼承Person類
class Teacher : virtual public Person
{
protected:int _id; // 職?編號
};// 教授助理
class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修課程
};int main()
{// 使?虛繼承,可以解決數據冗余和?義性Assistant a;a._name = "peter";return 0;
}
九、繼承和組合
繼承和組合都是復用思想的體現。
1、public繼承是?種is-a的關系。也就是說每個派?類對象都是?個基類對象。
2、組合是?種has-a的關系。假設B組合了A,每個B對象中都有?個A對象。例如容器適配器,stack里有一個deque。
3、繼承允許你根據基類的實現來定義派?類的實現。(因為一般都是共有繼承,子類能訪問父類成員)這種通過?成派?類的復?通常被稱為?箱復?(white-box reuse)。術語“?箱”是相對可視性??:在繼承?式中,基類的內部細節對派?類可?
。繼承?定程度破壞了基類的封裝,基類的改變,對派?類有很?的影響。派?類和基類間的依賴關系很強,耦合度?。
4、對象組合是類繼承之外的另?種復?選擇。新的更復雜的功能可以通過組裝或組合對來獲得。對象組合要求被組合的對象具有良好定義的接?。這種復??格被稱為?箱復(black-box reuse),因為對象的內部細節是不可?的。對象只以“?箱”的形式出現。
組類之間沒有很強的依賴關系,耦合度低。優先使?對象組合有助于你保持每個類被封裝。
5、優先使?組合,?不是繼承。實際盡量多去?組合,組合的耦合度低,代碼維護性好。不過也不太那么絕對,類之間的關系適合繼承(is-a)那就?繼承,另外要實現多態,也必須要繼承。類之間的關系既適合?繼承(is-a)也適合組合(has-a),就?組合。
以上就是小編分享的全部內容了,如果覺得不錯還請留下免費的關注和收藏如果有建議歡迎通過評論區或私信留言,感謝您的大力支持。
一鍵三連好運連連哦~~