WordPress的文本編輯器福建seo
目錄
1、繼承的概念與意義
2、繼承的使用
2.1繼承的定義及語法
2.2基類與派生類間的轉換
2.3繼承中的作用域
2.4派生類的默認成員函數
<1>構造函數
<2>拷貝構造函數
<3>賦值重載函數
<4析構函數
<5>總結
3、繼承與友元
4、繼承與靜態(tài)變量
5、菱形繼承及菱形虛擬繼承
6、繼承與組合
1、繼承的概念與意義
什么是繼承?
繼承(inheritance)機制是面向對象程序設計使代碼可以復用的最重要的手段,它允許我們在保持原有類特性的基礎上進行擴展,增加方法(成員函數)和屬性(成員變量),這樣產生新的類,稱派生類。
繼承呈現了面向對象程序設計的層次結構,體現了由簡單到復雜的認知過程。以前我們接觸的函數層次的復用,繼承是類設計層次的復用。
通過繼承聯系在一起的類構成了一種層次關系,在這種關系中有一個基類(base class),其他類則是直接或間接地從基類繼承過來的,這些繼承來的類可以稱為派生類(drived class)?;愅ǔS兄鴮哟侮P系中所有類共同擁有維護的成員,而每個派生類也有著自己各自特定的成員。
一個簡單的例子:一個學習管理系統(tǒng),那么成員必定有學生,老師等等,這些是身份,歸根到底是個人(基類)包含著名字、年齡、地址等基礎信息。這些需要共同維護的就是基類的成員。
//共同維護的成員部分->基類 class Person { public:// 進?校園/圖書館/實驗室刷?維碼等?份認證 void identity(){cout << "void identity()" << _name << endl;} protected:string _name = "qsy"; // 姓名 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; // 職稱 }; int main() {Student s;Teacher t;s.identity();t.identity();return 0; }
可以看到派生類可以訪問基類成員
如果沒有繼承這種結構關系的話?Student和Teacher 都有姓名/地址/ 電話/年齡等成員變量,都有identity身份認證的成員函數,設計到兩個類里面就是冗余的。更好地體現了繼承是類設計層次的復用。
2、繼承的使用
2.1繼承的定義及語法
這就是繼承的語法格式
繼承方式與訪問限定符號一樣有著三種,不同的繼承方式與不同的類成員組合會是不同的情況
總結一下規(guī)律:
<1>基類 private 成員在派生類中無論以什么方式繼承都是不可見的。這里的不可見是指基類的私有成員還是被繼承到了派生類對象中,但是語法上限制派生類對象不管在類里面還是類外面都不能去訪問它。
將年齡變?yōu)樗接序炞C一下是否繼承到了派生類對象
可以看到繼承下來了但是不可以訪問!!
<2>如果基類成員不想在類外直接被訪問,但需要在派生類中能訪問,就定義為 protected??梢钥闯霰Wo成員限定符是因繼承才出現的。
如果想要訪問 private 成員可以在基類中成員函數訪問,這樣派生類可以間接訪問到 private成員
<3>基類的私有成員在派生類都是不可見。基類的其他成員在派生類的訪問方式 == Min(成員在基類的訪問限定符,繼承方式),public??> protected >?private。
Tip:class默認繼承方式是 private,struct默認繼承方式是public。最好顯示寫出繼承方式
<4>在實際運用中?般使用都是 public 繼承,幾乎很少使用 protetced/private 繼承,也不提倡使用protetced/private繼承,因為protetced/private繼承下來的成員都只能在派生類的類里面使用,實 際中擴展維護性不強。
2.2基類與派生類間的轉換
基類與派生類之間是否有著類型的轉換呢?
答案是可以的! public繼承中有一個 is-a 概念:每個派生類都是一個特殊的基類對象
? public繼承的派生類對象 可以賦值給 基類的指針 / 基類的引用。這里有個形象的說法叫切片或者切割。寓意把派生類中基類那部分切出來,基類指針或引用指向的是派生類中切出來的基類那部分。
? 基類對象不能賦值給派生類對象
? 基類的指針或者引用可以通過強制類型轉換賦值給派生類的指針或者引用。但是必須是基類的指針是指向派生類對象時才是安全的。這里基類如果是多態(tài)類型,可以使用RTTI(Run-Time Type??Information)的dynamic_cast 來進行識別后進行安全轉換。
2.3繼承中的作用域
繼承體現中也有各自的作用域規(guī)則并且引出來一個隱藏概念,隱藏影響的只是編譯器查找規(guī)則
1. 在繼承體系中基類和派生類都有獨立的作用域。
2. 派生類和基類中有同名成員,派生類成員將屏蔽基類對同名成員的直接訪問,這種情況叫隱藏。 (在派生類成員函數中,可以使用 基類::基類成員顯示訪問)
3. 需要注意的是如果是成員函數的隱藏,只需要函數名相同就構成隱藏。(區(qū)分重載)
4. 注意在實際中在繼承體系里面最好不要定義同名的成員。
// Student的_num 和 Person的_num 構成隱藏關系,可以看出這樣代碼雖然能跑,但是?常容易混淆
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; // 學號
};
int main()
{Student s1;s1.Print();return 0;
};
訪問的是哪個 _num 呢?
可以看到派生類成員隱藏了基類的同名成員,直接訪問了派生類的 _num?
同理,函數也有隱藏的現象
A和B類中的 fun 兩個函數構成什么關系呢??根據前面的知識可以知道繼承體系中函數名相同就構成隱藏關系
2.4派生類的默認成員函數
6個默認成員函數,默認的意思就是指我們不寫,編譯器會自動生成?個,那么在派生類中,這 幾個成員函數是如何生成的呢?
四個常見默認成員函數:
<1>構造函數
派生類的構造函數必須調用基類的構造函數初始化基類的那?部分成員。
class Person { public:Person(const char* name="xxc") //全缺省函數,默認構造:_name(name){cout << "Person()" << endl;} protected:string _name;//姓名 };class Student :public Person { public://不顯示實現默認構造,編譯器生成的// 1. 內置類型->不確定// 2. 自定義類型->調用自定義類型的顯示寫的默認構造// 3. 基類成員看作一個整體,要求調用基類的默認構造 protected:int _num;//學號string _addrss;//地址 };int main() {Student s1;return 0; }
如果基類沒有默認的構造函數,則必須在派生類構造函數的初始化列表階段顯示調用。
寫一個 Student的構造函數
還是報錯!前面提到 需要把基類成員當成一個對象調用基類的構造函數
如何實現一個不能被繼承的類呢?
方法1:基類的構造函數私有,派生類的構成必須調用基類的構造函數,但是基類的構成函數私有化以后,派生類看不見就不能調用了,那么派生類就無法實例化出對象。
方法2:C++11新增了?個final關鍵字,final 修改基類,派生類就不能繼承了。
<2>拷貝構造函數
派生類的拷貝構造函數必須調用基類的拷貝構造完成基類的拷貝初始化。
可以看到,沒有資源申請的時候 Student 并不需要自己顯示實現拷貝構造,因為編譯器默認拷貝構造會調用基類的拷貝構造
那么怎么自己實現拷貝構造呢?(Tip:基類對象是最先聲明(內存順序)的,初始化列表中第一個初始化)
Person(const Person& p): _name(p._name)
{cout << "Person(const Person& p)" << endl;
}Student(const Student& s):Person(s),_num(s._num),_addrss(s._addrss)
{//深拷貝
}
Person(s) 這個 s 是派生類對象的引用為什么可以傳給基類呢? 涉及基類與派生類間的轉換概念——切片?
如果顯示寫了拷貝構造但是不顯示調用基類的拷貝構造的會,編譯器會自動調用默認構造而非調用基類的拷貝構造
補充一下缺省值構成默認構造,運行一下發(fā)現調用的就是默認構造而非拷貝構造
<3>賦值重載函數
派生類的operator=必須要調用基類的operator=完成基類的復制。
賦值重載與拷貝構造類似一般編譯器默認生成的就已經夠用了,如果有資源申請的話才需要顯示實現
Student& operator=(const Student& s)
{if (this != &s){operator=(s);//派生類切片基類成員_num = s._num;_addrss = s._addrss;}return *this;
}
棧溢出,無限遞歸調用,我們不是想要調用基類的賦值函數嗎?為什么調用了派生類的呢?
需要注意的是派生類的 operator= 隱藏了基類的operator= ,所以顯示調用基類的operator= ,需要指定基類作用域
Student& operator=(const Student& s)
{if (this != &s){//基類和派生類的賦值構成了隱藏關系 需要指定作用域Person::operator=(s);//派生類切片基類成員_num = s._num;_addrss = s._addrss;}return *this;
}
<4析構函數
析構函數可以顯示調用,那么可以在派生類顯示調用基類的析構函數來清理基類成員
可是為什么調不動呢?這里派生類和基類的析構函數構成了隱藏關系
因為多態(tài)中?些場景析構函數需要構成重寫,重寫的條件之一是函數名相同,那么編譯器會對析構函數名進行特殊處理,處理成destructor(),所以基類析構函數不加 virtual的情況下,派生類析構函數和基類析構函數構成隱藏關系。
想要調用就標明作用域:
Person::~Person()
但是像上述這樣寫,會有一個問題,基類的析構會調用兩次!!!
其實,派生類的析構函數會在被調用完成后自動調用基類的析構函數清理基類成員。因為這樣才能保證派生類對象先清理派生類成員再清理基類成員的順序。所以我們不必在派生類的析構函數中進行調用基類的析構函數,不然就會重復釋放同一塊空間,導致報錯!
可以怎么理解派生類析構自動調用基類的析構呢? 先子后父保證析構順序!顯示調用不一定保證先子后父的析構順序
<5>總結
派生類和基類的層次關系邏輯基礎還是類和對象
派生類的默認成員函數的注意事項:
<1>派生類的構造函數必須調用基類的構造函數初始化基類的那一部分成員。如果基類沒有默認的構造函數,則必須在派生類構造函數的初始化列表階段顯示調用。
<2>派生類的拷貝構造函數必須調用基類的拷貝構造完成基類的拷貝初始化。
<3>派生類的operator=必須要調用基類的operator=完成基類的復制。
<4>派生類的析構函數會在被調用完成后自動調用基類的析構函數清理基類成員(不需要顯示和調用基類析構)。因為這樣才能保證派生類對象先清理派生類成員再清理基類成員的順序。
<5>派生類對象初始化先調用基類構造再調派生類構造。派生類對象析構清理先調用派生類析構再調基類的析構。
<6>因為后續(xù)一些場景析構函數需要構成重寫,重寫的條件之一是函數名相同那么編譯器會對析構函數名進行特殊處理,處理成destrutor(),所以父類析構函數不加virtual的情況下,子類析構函數和父類析構函數構成隱藏關系
3、繼承與友元
友元關系不能繼承,也就是說基類友元不能訪問派生類私有和保護成員。比如爸爸的朋友可以說是你的朋友嗎?
class Student;//前置聲明
class Person
{
public:friend void Display(const Person& p, const Student& s);//需要前置聲明否則報錯招不到 Student
protected:string _name; // 姓名
};
class Student : public Person
{
protected:int _stuNum; // 學號
};
void Display(const Person& p, const Student& s)
{cout << p._name << endl;
}
int main()
{Person p;return 0;
}
如果訪問派生類的私有和保護成員呢?
可以看見是不可訪問的?在派生類同樣設置一個友元就可以解決這個問題了。
4、繼承與靜態(tài)變量
基類定義了 static 靜態(tài)成員,則整個繼承體系里面只有?個這樣的成員。無論派生出多少個派生類,都只有?個static 成員實例。
驗證一下:
class A
{
public: static int _a;int _aa;
};
class B :public A
{
public:int _b;
};
// static int _a = 1;報錯int A::_a = 1;//注意定義的方式
int main()
{A a;B b1;B b2;//這?的運行結果可以看到非靜態(tài)成員_aa的地址是不?樣的// 說明派生類繼承下來了,?類派生類對象各有?份 cout << &a._aa << endl;cout << &b1._aa << endl;cout << endl;// 這?的運行結果可以看到靜態(tài)成員 _a 的地址是?樣的 //說明派生類和基類共用同?份靜態(tài)成員 cout << &a._a << endl;cout << &b1._a << endl;cout << &b2._a << endl;cout << endl;//公有情況下 基類派生類都可以訪問靜態(tài)成員變量cout << a._a << endl;cout << b1._a << endl;cout << b2._a << endl;return 0;
}
也就說明他們共用一個_a變量,所以無論派生出多少個子類,都只有一個static成員實例
這個特性可以帶來一種思路統(tǒng)計實例化類的數量個數,只需在構造函數中加入一個增加該靜態(tài)變量的語句即可:
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()
{Student s1;Student s2;Student s3;cout << " 人數 :" << Person::_count << endl;Student::_count = 0;cout << " 人數 :" << Person::_count << endl;return 0;
}
這樣我們就可以知道該繼承體系中實例化了多少個類了!!!
5、菱形繼承及菱形虛擬繼承
首先聲明一下,由于C++的歷史緣故,其一致行走在語言發(fā)展的前端,一直在嘗試新的內容。在發(fā)展過程中,有些內容加入到C++的時候,還沒有發(fā)現其弊端。而后來發(fā)現的時候,為了向上兼容,只能打補丁,所以不開避免的不會有一些弊端,會有復雜的語法和復雜的特性??傄邢闰屨咦咔扒懊?#xff0c;而C++就是!!!
單繼承:?個派生類只有?個直接基類時稱這種繼承關系為單繼承
多繼承:?個派生類有兩個或以上直接基類時稱這種繼承關系為多繼承,多繼承對象在內存中的模型是,先繼承的基類在前面,后面繼承的基類在后面,派生類成員在放到最后面。
菱形繼承:菱形繼承是多繼承的?種特殊情況。
菱形繼承的問題,從上面的對象成員模型構造,可以看出菱形繼承有數據冗余和二義性的問題,在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 _majorCourse; // 主修課程
};
int main()
{// 編譯報錯:error C2385: 對“_name”的訪問不明確 二義性Assistant a;a._name = "peter";// 需要顯?指定訪問哪個基類的成員可以解決?義性問題,但是數據冗余問題?法解決 a.Student::_name = "xxx";a.Teacher::_name = "yyy";return 0;
}
那該如何解決數據冗余的問題呢??可以借用虛擬繼承!!
虛擬繼承(virtual)可以解決菱形繼承的二義性和數據冗余的問題。如上面的繼承關系,在Student和Teacher的繼承Person時使用虛擬繼承,即可解決問題。
這是什么原理呢?測試一下!
菱形繼承不虛擬繼承的情況
#include<iostream>
#include<string>using namespace std;class A
{
public:int _a;
};class B : public A
//class B : virtual public A
{
public:int _b;
};class C : public A
//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._b = 3;d._c = 4;d._d = 5;return 0;
}
調試一下:
通過這個逐語句調試的內存變化,我們可以確定大致的內存情況:
不使用虛擬繼承就是這樣的內存情況,也好理解為什么同名變量的兩份是如何儲存的了。
接下來我們來看虛擬繼承下的菱形繼承是怎么個情況:
內存分布:
a儲存在最下面,而B,C部分的原有儲存_a的位置現在是什么呢???
其實是個指針,那我們來看看指針指向的空間儲存著什么吧:
???怎么對應位置是00 00 00 00為什么是零?往下看看:
分別儲存著16進制數字14 0c轉換為10進制數字20 12,然后對應B,C原本的指針位置(006FFB6C)加上這個值(偏移量),都會指向到A _a的空間!!!這個00 00 00 00到多態(tài)的部分再來進行講解,知道原地址加上下面的值就是A _a的空間就可以了!!!
這里是通過了B和C的兩個指針,指向的一張表。這兩個指針叫虛基表指針,這兩個表叫虛基表。虛基表中存的偏移量。通過偏移量可以找到下面的A。
即原本B,C中_a的位置儲存這一個指針,指針指向的位置有一個偏移量,原位置的地址加上偏移量就會指向A的空間!!!
那這樣進行拷貝切片的時候是怎樣的呢?一樣是把D中B對象的部分切片,然后通過虛基表的方式來找到_a。但這樣也帶來了一些代價:(PS:內存中的儲存順序就是聲明的順序,先繼承誰,誰就在前面)
多繼承指針偏移問題(切片)
p1和p2指向哪里呢???
內存分布中,先繼承的放前面!
因為切片的概念p2指向 base2開始但是只能看見 base2 那一部分
6、繼承與組合
- public繼承是一種is-a(誰是什么)的關系。也就是說每個派生類對象都是一個基類對象。
- 組合是一種has-a(誰有什么)的關系。假設B組合了A,每個B對象中都有一個A對象(也就是把A作為B的成員變量)
-
繼承允許你根據基類的實現來定義派生類的實現。這種通過生成派生類的復用通常被稱為白箱復用(white-box reuse 能看見,不安全,耦合度高)。術語 “白箱”是相對可視性而言:在繼承方式中,基類的內部細節(jié)對子類可見 。繼承一定程度破壞了基類的封裝,基類的改變,對派生類有很大的影響。派生類和基類間的依賴關系很強,耦合度高。
-
對象組合是類繼承之外的另一種復用選擇。新的更復雜的功能可以通過組裝或組合對象來獲得。對象組合要求被組合的對象具有良好定義的接口。這種復用風格被稱為黑箱復用(black-box reuse 不能能看見,安全,耦合度低),因為對象的內部細節(jié)是不可見的。對象只以“黑箱”的形式出現。組合類之間沒有很強的依賴關系,耦合度低。優(yōu)先使用對象組合有助于你保持每個類被封裝。
有關繼承的經典面試題
<1>C++有多繼承,為什么java等語言沒有?
歷史原因!C++是先驅者(人的直覺認為多繼承很合理,我感覺正常人都會想到多繼承),并且c++中的多繼承處理起來十分復雜,訪問基類變量的過程就會很復雜!!!java等后來發(fā)展的語言見到c++中多繼承的復雜,就干脆放棄了。
<2>什么是菱形繼承?多繼承的問題是什么?
菱形繼承如字面意思(兩個父類的父類是同一個類就會發(fā)生菱形繼承),多繼承本身沒什么問題,真正的問題是有多繼承就可能發(fā)生菱形繼承。菱形繼承就有問題了:變量的二義性和繼承冗雜。解決辦法很簡單就是虛擬繼承,但是這樣就會大大降低效率。
<3>繼承和組合的區(qū)別?什么時候用繼承?什么時候用組合?
繼承:通過擴展已有的類來獲得新功能的代碼復用方法
組合:新類由現有類的對象合并而成的類的構造方式
如果二者間存在一個“是”的關系,并且一個類要對另外一個類公開所有接口,那么繼承是更好的選擇
如果二者間存在一個“有”的關系,那么首選組合
!能用組合就用組合!!!能用組合就用組合!!!能用組合就用組合!!!