文章目錄
- 前言
- 一、實現一個不能繼承的類
- 二、友元與繼承
- 三、繼承與靜態成員
- 四、多繼承以及菱形繼承問題
- 1.繼承模型:
- 2.菱形繼承的問題
- 3.虛擬繼承解決數據冗余和二義性的原理
- 4.虛擬繼承的原理
- 五、繼承的總結和反思
- 1.繼承和組合
- 總結
前言
各位好呀!今天呢我們接著講繼承的相關知識,之前給大家已經分享了繼承一部分知識。那今天小編就來給繼承收個尾吧。來看看繼承的剩下的一部分。
一、實現一個不能繼承的類
想要實現一個不能被繼承的類的呢有兩種方法:
- 方法一:父類的構造函數私有,子類的構造必須調用父類的構造函數,但是父類的構造函數私有化以后呢,在子類中是不可見也不可調用的。那么 子類就無法實例化出對象,這樣就可以達到父類不能被繼承(C++98的方法)。
- 方法二:C++11新增了一個關鍵字final,用final修飾父類,那么子類就無法繼承父類了。
#include<iostream>
#include<string>
using namespace std;
class Person//C++11
{
public:
protected:string _name;
private:Person()//私有化構造函數{}
};
class student:public Person
{
public:
private:string ID;
};
int main()
{student s;//這里會報錯的,因為構造函數已經被私有化,子類是調不到父類的構造函數的//但是這里要注意的是:如果我們這里不定義,代碼是不會報錯的。return 0;
}#include<iostream>
#include<string>
using namespace std;
class Person final//C++11
{
public:Person()//私有化構造函數 {}
protected:string _name;
private:};
class student:public Person //像這樣用final修飾父類的話,父類也不能被子類繼承
{
public:
private:string ID;
};
int main()
{student s;return 0;
}
二、友元與繼承
注意:友元關系不能被繼承,也就是說,父類的友元不能訪問子類的私有成員和保護成員。
解決方法:在子類中加上友元就可以了。還有就是要注意一下需要前置聲明一下子類
#include<iostream>
#include<string>
using namespace std;
class student; //前置聲明
class Person
{friend void Print(const Person& p, const student& s); //編譯器在遇到一個變量和函數的時候,都只會向上查找(提高查找的效率),// 所以這里的student就會向上查找,但是上面沒有student,student在下面//還有就是student不能放在方面取,因為student要繼承Person。這兩者相互依賴。//為了解決這個問題呢我們會在上面加一個前置聲明
public:
protected:string _name="帥哥";
};
class student:public Person
{friend void Print(const Person& p, const student& s); //由于繼承關系不能被繼承下來,所以就訪問不到student中_num成員變量//解決這個問只需要像這樣,加一個友元就可以解決這個問題了。
public:
protected :string _num="123456";
};
void Print(const Person& p, const student& s)
{cout << p._name << endl; cout << s._num << endl;
}
int main()
{student v;Person c;Print(c,v); return 0;
}
三、繼承與靜態成員
#include<iostream>
#include<string>
using namespace std;
class Person
{
public:string _name; static int n;
};
int Person::n = 1;
class student :public Person
{
public:string _num;
};
int main()
{Person p;student s;//非靜態成員變量的地址 cout << &p._name << endl;cout << &s._name << endl; cout << endl; //靜態成員變量的地址cout << &p.n << endl;cout << &s.n << endl; return 0;
}
我們通過看到非靜態成員_name地址是不一樣,這說明了子類繼承下來的成員在子類和父類中各有一份。但是靜態成員是不是地址相同呀?這又說明父類定義了static靜態成員,則整個繼承體系里面只有一個這樣的成員。無論派生出多少個子類,都只有一個static成員實例。
還有一個就是在共有的情況下,父類和子類指定作用域就可以訪問靜態成員。這里突破作用域就可以訪問,因為它沒有存在對象里面,而是存在靜態區,它只是受到類域的限制而已。還有就可以把靜態成員理解成全局的。
四、多繼承以及菱形繼承問題
1.繼承模型:
繼承模型呢分為三種:單繼承,多繼承和菱形繼承
- 單繼承:一個子類只有一個直接父類時稱這種繼承關系為單繼承
- 多繼承:一個子類有兩個或以上直接父類時稱這個繼承關系為多繼承
- 菱形繼承:菱形繼承屬于一個特殊的多繼承,
2.菱形繼承的問題
菱形繼承主要時兩個問題,分別是 二義性和數據冗余
二義性:
首先,什么是二義性?二義性就是在訪問數據的時候發生歧義,不知道訪問那個。
示例:
#include<iostream>
#include<string>
using namespace std;
class A
{
public:string _name;int _age;
};
class B:public A
{
public:
protected:string _number;
};
class C :public A
{
public:
protected:string Gender;
};
class D :public B, public C
{
public:
protected:string ID;
};
int main()
{D d;d._name;//存在二義性
}
問題分析:
這里為什么會存在訪問不明確呢?結合之前的知識,子類繼承父類成員,那么子類和父類中是不是都分別有一份獨立的成員。但是現在B和C這兩個類都繼承了A,然而D又繼承B和C。也就是說A,B,C,D中分別都有一份_name,現在要訪問父類中的成員_name,但是這里的D是繼承了兩個類,兩個類中都有_name ,所以這里編譯器就不知道該訪問那個類里面的_name。這就是二義性。
解決方法:
怎樣解決這個問題呢?其實很簡單,只需要顯示指定訪問那個父類中_name就可以解決問題,但是不能解決數據冗余的問題
#include<iostream>
#include<string>
using namespace std;
class A
{
public:string _name;int _age;
};
class B:public A
{
public:
protected:string _number;
};
class C :public A
{
public:
protected:string Gender;
};
class D :public B, public C
{
public:
protected:string ID;
};
int main()
{D d;d.B::_name="張三";//指定要訪問的父類d.C::_name = "小張"; //指定要訪問的父類
}
數據冗余:
數據冗余可以理解成數據重復造成空間的浪費
示例:
菱形繼承還有個特別煩的點就是他會讓空間變大,一個它的父類在被幾個類繼承時,那它就有幾份。比如下列代碼中,A在B,C中各有一份。
#include<iostream>
#include<string>
using namespace std;
class A
{
public:string _name; int _age;
};
class B:public A
{
public:
protected:string _number;
};
class C :public A
{
public:
protected: string Gender;
};
class D :public B, public C
{
public:
protected:string ID;
};
class F:public A,public B
{
public:};
int main()
{D d;F f;cout << sizeof(d) << endl;//菱形繼承大小cout << sizeof(f) << endl;//多繼承的大小
}
可以看出兩者的空間大小相差的將近一倍了。所以,一般不建議創建菱形繼承,因為這樣有太多的問題,有時候還把握不住,建議不使用。
3.虛擬繼承解決數據冗余和二義性的原理
菱形繼承一般不建議使用,但是如果非要使用,那該怎樣解決二義性和數據冗余呢?這里就要引用一個新的關鍵字 virtual(虛擬繼承)。
那這個關鍵字該怎么用呢?該加在哪里呢?先看示例:
#include<iostream>
#include<string>
using namespace std;
class A
{
public:string _name;int _age;
};
class B :virtual public A
{
public:
protected:string _number;
};
class C :virtual public A
{
public:
protected:string Gender;
};
class D :public B, public C
{
public:
protected:string ID;
};
int main()
{D d;d._name = "張三";return 0;
}
通過上面的代碼可以看出:
virtual應該加在產生二義性和數據冗余繼承的地方,現在A是不是產生了二義性和數據冗余 ,那virtual就加在B和C繼承的哪里。這樣就解決了二義性和數據冗余的問題,這點我們可以通過監視窗口可以看出。
從監視窗口展示的原因,這里雖然看起來是三份,但其實是一份。這樣是不是就解決了數據冗余和二義性的問題啊。
注意:這里的virtual不能只加在B或者C,必須要同時加在B和C
大家看看這上圖,圖中的關系是不是菱形繼承呢?其實 上圖也時菱形繼承哦,大家不要對形狀太刻板了哦,認為菱形繼承那他的形狀就必須時菱形。形狀不是菱形但是有二義性和數據冗余的產生那他就是菱形繼承,
思考以及解決方法:
那這里的virtual該加在哪里呢?BC?還是DC?其實正確是應該是加在BC,這里是不是A產生二義性和數據冗余,那就要加BC呀!那這里可以不可以在BCD都加上virtual呢?這里就好比一個人只需要兩根拐棍,而你偏要給他三根是一樣的性質。在D那里都沒有產生二義性和數據冗余那就沒必要加。
還有就是大家在寫代碼的時候遇到上圖這種繼承的時候都加上virtual,這種情況呢屬于過度防范了。首先我們可以先看看B和C有沒有被同一個類繼承。如果沒有,可以先不用加;如果有,那再加上virtual是不是也不遲啊?所以大家在寫代碼的時候不要過度的防范二義性和數據冗余。
4.虛擬繼承的原理
#include<iostream>
#include<string>
using namespace std;
class A
{
public:A(const char*name,int age=18):_name(name),_age(age) {}string _name;int _age;
};
class B :virtual public A
{
public:B(const char*name,const char*number="1234567899"):A(name),_number(number) {}
protected:string _number;
};
class C :virtual public A
{
public:C(const char* name, const char* gender="男"):A(name), Gender(gender) {}
protected:string Gender;
};
class D :public B, public C
{
public:D(const char*name,const char*id="1263457"):B(name) ,C(name) //,A(naem)//這里必須顯示調用,不然會報錯,//還有就是這里初始化怎么多name,那到底以誰的為準呢?,ID(id) {}
protected:string ID;
};
int main()
{D d("張三"); return 0;
}
這里從我們之前學的知識來看這里代碼的邏輯應該時沒有 問題的呀。調用子類的構造函數先調用父類的默認構造函數嘛。但是這里說class A 不存在默認構造。那是怎么回事呢?這不得不就要看看虛擬繼承的原理了。
相比于普通的多繼承,虛擬繼承呢是要把class A拿出來放在最底下的一個類中。他就不像普通多繼承那樣class A分別存在class B 和class C中。因為他要解決二義性和數據冗余。與此同時,這里還要引入一個虛基表和虛基表指針,復雜的很,所以小編這里就沒有展示出來。小編這里主要是像讓大家看看這兩種繼承的有什么不同。回到上面的問題:由于這里A不在B和C里面了,而是一個單獨的父類,所以A也因該顯示調用。
總結:不要輕易使用菱形繼承和寫出菱形繼承的代碼,多繼承可以用
五、繼承的總結和反思
1. 很多人說C++語法復雜,其實多繼承就是一個體現。有了多繼承,就存在菱形繼承,有了菱形繼承就有菱形虛擬繼承,底層實現就很復雜。所以一般不建議設計出多繼承,一定不要設計出菱形繼承。否則在復雜度及性能上都有問題。
- 多繼承可以認為是C++的缺陷之一,很多后來的OO語言都沒有多繼承,如Java。
1.繼承和組合
- public繼承是一種is-a的關系。也就是說每個派生類對象都是一個基類對象。
- 組合是一種has-a的關系,。假設B組合了A,每個B對象中都有一個A對象。
示例:我們以實現一個簡單的棧來演示:
//stack和cin構成has-a的關系
#include<iostream>
#include<string>
#include<stdbool.h>
using namespace std;
class stack
{
public :void push(const int& x){cin.push_back(x);}void pop(){cin.pop_back();}const int& top()const {return cin.back();}bool empty(){return cin.empty();}
private: vector<int > cin;
};
int main()
{stack s;s.push(1);s.push(2);s.push(3);while (!s.empty()){cout << s.top() << " ";s.pop();}return 0;
}//stack和vector是is-a關系
#include<iostream>
#include<stdbool.h>
#include<vector>
using namespace std;
class stack:public std::vector<int>
{
public:void push(const int& x){vector<int> ::push_back(x);}void pop(){vector<int> ::pop_back();}const int& top()const{return vector<int> ::back();}bool empty(){return vector<int> ::empty(); }
};
int main()
{stack s; s.push(1); s.push(2);s.push(3); while (!s.empty()) {cout << s.top() << " "; s.pop();}return 0;
}
- 優先使用對象組合,而不是類繼承 。
- 繼承允許你根據基類的實現來定義派生類的實現。這種通過生成派生類的復用通常被稱為白箱復用(white-box reuse)。術語“白箱”是相對可視性而言:在繼承方式中,基類的內部細節對子類可見 。繼承一定程度破壞了基類的封裝,基類的改變,對派生類有很大的影響。派生類和基類間的依賴關系很強,耦合度高。
- 對象組合是類繼承之外的另一種復用選擇。新的更復雜的功能可以通過組裝或組合對象來獲得。對象組合要求被組合的對象具有良好定義的接口。這種復用風格被稱為黑箱復用(black-box reuse),因為對象的內部細節是不可見的。對象只以“黑箱”的形式出現。組合類之間沒有很強的依賴關系,耦合度低。優先使用對象組合有助于你保持每個類被封裝
- 實際盡量多去用組合。組合的耦合度低,代碼維護性好。不過繼承也有用武之地的,有些關系就適合繼承那就用繼承,另外要實現多態,也必須要繼承。類之間的關系可以用繼承,可以用組合,就用組合。
總結
到這里繼承的相關知識小編就基本分享完了咯,如果有什么疑問歡迎大家討論。那今天就到這里吧。