中國(guó)建設(shè)銀行寧波分行網(wǎng)站杭州seo首頁(yè)優(yōu)化軟件
🥁作者: 華丞臧
📕????專欄:【C++】
各位讀者老爺如果覺得博主寫的不錯(cuò),請(qǐng)諸位多多支持(點(diǎn)贊+收藏+關(guān)注
)。如果有錯(cuò)誤的地方,歡迎在評(píng)論區(qū)指出。推薦一款刷題網(wǎng)站 👉LeetCode
文章目錄
- 一、繼承的概念及其定義
- 1.1 繼承的概念
- 1.2 繼承的定義
- 1.2.1 定義格式
- 1.2.2 繼承關(guān)系和訪問限定符
- 1.2.3繼承基類成員訪問方式的變化
- 二、基類和派生類對(duì)象賦值轉(zhuǎn)換
- 三、繼承中的作用域
- 四、派生類的默認(rèn)成員函數(shù)
- 五、繼承--友元和靜態(tài)成員
- 5.1 繼承與友元
- 5.2 繼承與static靜態(tài)成員
- 六、復(fù)雜的菱形繼承及菱形虛擬繼承
- 6.1 單繼承和多繼承
- 6.2 菱形繼承
- 6.3 虛擬繼承解決數(shù)據(jù)冗余和二義性的原理
- 七、繼承的總結(jié)
一、繼承的概念及其定義
1.1 繼承的概念
繼承機(jī)制是面向?qū)ο蟪绦蛟O(shè)計(jì)實(shí)現(xiàn)代碼復(fù)用最重要的手段,繼承允許程序員在保持原有類特性的基礎(chǔ)上進(jìn)行擴(kuò)展,增加功能,這樣產(chǎn)生新的類,稱為派生類,原有類稱為基類。派生類也稱為子類,基類也稱為父類。
繼承呈現(xiàn)了面向?qū)ο蟪绦蛟O(shè)計(jì)的層次結(jié)構(gòu),體現(xiàn)了由簡(jiǎn)單到復(fù)雜的認(rèn)知過(guò)程。以前我們學(xué)習(xí)接觸到的復(fù)用都是函數(shù)復(fù)用,繼承是類設(shè)計(jì)層次的復(fù)用。
來(lái)看下面的例子:
class Person
{
public:void Print(){cout << "name:" << _name << endl;cout << "age:" << _age << endl;}protected:string _name = "peter"; // 姓名int _age = 18; // 年齡
};class Student : public Person
{
protected:int _stuid; // 學(xué)號(hào)
};class Teacher : public Person
{
protected:int _jobid; // 工號(hào)
};int main()
{Student s;Teacher t;s.Print();t.Print();return 0;
}
繼承后父類的Person的成員(成員函數(shù)+成員變量)都會(huì)變成子類的一部分。這里體現(xiàn)出了Student和Teacher復(fù)用了Person的成員。
調(diào)用Print函數(shù)可以看到函數(shù)的復(fù)用:
1.2 繼承的定義
1.2.1 定義格式
下面代碼中,Student是子類,也稱為派生類;Person是父類,也稱為基類;其中public表示繼承方式。
1.2.2 繼承關(guān)系和訪問限定符
1.2.3繼承基類成員訪問方式的變化
類成員/繼承方式 | public繼承 | protected繼承 | private繼承 |
---|---|---|---|
基類的public成員 | 派生類的public成員 | 派生類的protected成員 | 派生類的private成員 |
基類的protected成員 | 派生類的protected成員 | 派生類的protected成員 | 派生類的private成員 |
基類的private成員 | 在派生類中不可見 | 在派生類中不可見 | 在派生類中不可見 |
// 實(shí)例演示三種繼承關(guān)系下基類成員的各類型成員訪問關(guān)系的變化
class Person
{
public :void Print (){cout<<_name <<endl;}
protected :// public/protected繼承在子類中可見,private繼承在子類中不可見string _name ;
private :int _age ; // 在子類中不可見,但是被繼承了
};//class Student : protected Person
//class Student : private Person
class Student : public Person
{
protected :int _stunum ; // 學(xué)號(hào)
};
總結(jié):
- 基類private成員在子類中無(wú)論什么方式繼承都是不可見的。這里的不可見是指基類的私有成員還是被繼承到了派生類對(duì)象中,但是語(yǔ)法上限制派生類對(duì)象不管在類里面還是類外面都不能去訪問它。
- 基類private成員在派生類中是不能被訪問的,如果基類成員不想在類外直接被訪問,但需要在派生類中能訪問,就定義為protected,可以看出保護(hù)成員限定符就是因繼承才出現(xiàn)的。
- 基類的其他成員在子類的訪問方式 == Min(成員在基類的訪問限定符,繼承方式),public > protected > private。
- 使用關(guān)鍵字class的默認(rèn)繼承方式是private,使用struct默認(rèn)繼承方式是public,不過(guò)最好顯式的寫出繼承方式。
- 在實(shí)際運(yùn)用中一般使用public繼承,很少使用protected/private繼承,也不提倡使用protetced/private繼承,因?yàn)閜rotetced/private繼承下來(lái)的成員都只能在派生類的類里面使用,實(shí)際中擴(kuò)展維護(hù)性不強(qiáng)。(繼承本就是為了復(fù)用,私有代碼就不能服用了)
二、基類和派生類對(duì)象賦值轉(zhuǎn)換
- 派生類對(duì)象可以賦值給
基類的對(duì)象
、基類的指針
、基類的引用
。 在派生類對(duì)象賦值轉(zhuǎn)換時(shí),有個(gè)形象的說(shuō)法叫切片或者切割,即把派生類中的父類那部分切來(lái)賦值過(guò)去。 - 基類對(duì)象不能賦值給派生類對(duì)象。
- 基類的指針或者引用可以通過(guò)強(qiáng)制類型轉(zhuǎn)換賦值個(gè)派生類的指針或者引用;但是必須是基類的指針且指向派生類對(duì)象時(shí)才是安全的。這里基類如果是多態(tài)類型,可以使用RTTI(RunTime Type Information)的dynamic_cast 來(lái)進(jìn)行識(shí)別后進(jìn)行安全轉(zhuǎn)換。(這里只需先了解一下)
class Person
{
public:Person(string name = "", int age = 0){_name = name;_age = age;}void Print(){cout << _name << endl;}protected:string _name; // 姓名int _age; // 年齡
};class Student : public Person
{
public:Student(int stunum = 0):Person(){_stunum = stunum;}void Show(){cout << _name << endl;cout << _age << endl;cout << _stunum << endl;}protected:int _stunum; // 學(xué)號(hào)
};int main()
{Student s;s.Show();Person* per = &s; //發(fā)生切片//Person per = s;//Person& per = s;per->Print();// 3.基類的指針可以通過(guò)強(qiáng)制類型轉(zhuǎn)換賦值給派生類的指針per = &s;Student* ps1 = (Student*)per; // 這種情況轉(zhuǎn)換時(shí)可以的。per = &pobj;Student* ps2 = (Student*)per; // 這種情況轉(zhuǎn)換時(shí)雖然可以,但是會(huì)存在越界訪問的問題return 0;
}
三、繼承中的作用域
- 在繼承體系中基類和派生類都有獨(dú)立的作用域。
- 子類和父類中有同名成員,子類成員將屏蔽父類對(duì)同名成員的直接訪問,這種情況稱為隱藏,也叫重定義。
- 注意:如果是成員函數(shù)的隱藏,只需要函數(shù)名相同就可以構(gòu)成隱藏。(在子類成員函數(shù)中,可以使用 基類::基類成員 顯示訪問)
- 實(shí)際中,在繼承體系里最好不要定義同名成員。
class Person
{
protected:string _name = "小李子"; // 姓名int _num = 111; // 身份證號(hào)
};class Student : public Person
{
public:void Print(){cout << " 姓名:" << _name << endl;cout << " 身份證號(hào):" << Person::_num << endl;cout << " 學(xué)號(hào):" << _num << endl;}
protected:int _num = 999; // 學(xué)號(hào)
};
Student的_num和Person的_num構(gòu)成隱藏關(guān)系,可以看出這樣代碼雖然能跑,但是非常容易混淆;子類中必須顯示訪問才能使用隱藏成員。
再來(lái)看看成員函數(shù)的隱藏:
class A
{
public:void fun(){cout << "func()" << endl;}
};class B : public A
{
public:void fun(int i){A::fun();cout << "func(int i)->" << i << endl;}
};void Test()
{B b;b.fun(10);
};
B中的fun和A中的fun不是構(gòu)成重載,因?yàn)椴皇窃谕蛔饔糜?#xff1b;B中的fun和A中的fun構(gòu)成隱藏,成員函數(shù)滿足函數(shù)名相同就構(gòu)成隱藏。
四、派生類的默認(rèn)成員函數(shù)
6個(gè)默認(rèn)成員函數(shù),“默認(rèn)”的意思就是指我們不寫,編譯器會(huì)變我們自動(dòng)生成一個(gè),那么在派生類中,這幾個(gè)成員函數(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ù)會(huì)在被調(diào)用完成后自動(dòng)調(diào)用基類的析構(gòu)函數(shù)清理基類成員。因?yàn)檫@樣才能保證派生類對(duì)象先清理派生類成員再清理基類成員的順序。
- 派生類對(duì)象初始化先調(diào)用基類構(gòu)造再調(diào)派生類構(gòu)造。
- 派生類對(duì)象析構(gòu)清理先調(diào)用派生類析構(gòu)再調(diào)基類的析構(gòu)。
- 因?yàn)楹罄m(xù)一些場(chǎng)景析構(gòu)函數(shù)需要構(gòu)成重寫,重寫的條件之一是函數(shù)名相同(暫時(shí)先了解)。那么編譯器會(huì)對(duì)析構(gòu)函數(shù)名進(jìn)行特殊處理,處理成destrutor(),所以父類析構(gòu)函數(shù)不加virtual的情況下,子類析構(gòu)函數(shù)和父類析構(gòu)函數(shù)構(gòu)成隱藏關(guān)系。
通過(guò)上述說(shuō)明,我們能總結(jié)一下幾點(diǎn):
- 派生類當(dāng)中的基類部分是使用自己的構(gòu)造函數(shù)初始化,子類調(diào)用其默認(rèn)構(gòu)造函數(shù),沒有默認(rèn)構(gòu)造就必須顯示調(diào)用父類構(gòu)造;對(duì)基類數(shù)據(jù)的操作一般調(diào)用基類的成員函數(shù)。
- 派生類的模型是基類在前,派生類的成員在后,且父類先構(gòu)造后析構(gòu),子類后構(gòu)造先析構(gòu);這是由于棧的特性所決定的,棧是先入后出的。
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: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 << _num;cout << ":~Student()" << endl;}protected:int _num; //學(xué)號(hào)
};
void Test()
{Student s1("jack", 18);Student s2(s1);Student s3("rose", 17);s1 = s3;
}int main()
{Test();return 0;
}
五、繼承–友元和靜態(tài)成員
5.1 繼承與友元
友元關(guān)系不能繼承,也就是說(shuō)基類友元不能訪問子類私有和保護(hù)成員 。
class Student; //聲明,Display函數(shù)向上查找Student
class Person
{
public:friend void Display(const Person& p, const Student& s);
protected:string _name; // 姓名
};
class Student : public Person
{
protected:int _stuNum; // 學(xué)號(hào)
};void Display(const Person& p, const Student& s)
{cout << p._name << endl;cout << s._stuNum << endl;
}
void test()
{Person p;Student s;Display(p, s);
}
程序無(wú)法運(yùn)行成功,Student成員變量_stuNum是保護(hù)成員,在基類Person的友元中是無(wú)法被訪問的。
5.2 繼承與static靜態(tài)成員
基類定義了static靜態(tài)成員,則整個(gè)繼承體系里面只有一個(gè)這樣的成員。無(wú)論派生出多少個(gè)子類,都只有一個(gè)static成員實(shí)例 。
六、復(fù)雜的菱形繼承及菱形虛擬繼承
6.1 單繼承和多繼承
一個(gè)子類只有一個(gè)直接父類時(shí)稱這個(gè)繼承關(guān)系為單繼承。
前面我們看到的都是單繼承,單繼承并不是說(shuō)只有兩個(gè)類,而是類之間的基礎(chǔ)關(guān)系是線性的,即一個(gè)父親只有一個(gè)兒子,如下圖:
一個(gè)子類有兩個(gè)或兩個(gè)以上直接父類時(shí)稱這個(gè)繼承關(guān)系為多繼承。
6.2 菱形繼承
菱形繼承是多繼承的一種特殊情況。
class Person
{
protected:string _name; // 姓名
};class Student : public Person
{
protected:int _stuNum; // 學(xué)號(hào)
};class Teacher : public Person
{
protected:int _id; // 職工編號(hào)
};class Assistant : public Student, public Teacher{
protected:string _majorCourse; // 主修課程
};
通過(guò)監(jiān)視窗口觀察到,子類對(duì)象中存在兩份Person數(shù)據(jù),如下圖:
在C++中支持多繼承,因此就會(huì)存在菱形繼承;菱形繼承就是子類繼承的多個(gè)父類當(dāng)中繼承了相同的類。這里就存在一個(gè)問題:當(dāng)子類繼承的多個(gè)父類中繼承了相同的類,那么就子類應(yīng)該繼承那一個(gè)呢,還是說(shuō)兩個(gè)繼承。
從下面的對(duì)象成員模型構(gòu)造,可以看出菱形繼承有數(shù)據(jù)冗余和二義性的問題,在Assistant的對(duì)象中Person成員會(huì)有兩份。
C++提供了virtual關(guān)鍵字,用來(lái)定義虛擬函數(shù)和虛繼承;虛擬繼承可以解決菱形繼承的二義性和數(shù)據(jù)冗余問題。如上面的繼承關(guān)系,在Student和Teacher繼承Person時(shí)使用虛擬繼承即可解決問題。注意:虛擬繼承不要在其他地方去使用。
class Person
{
public:string _name; // 姓名
};
class Student : virtual public Person //虛擬繼承
{
protected:int _num; //學(xué)號(hào)
};
class Teacher : virtual public Person
{
protected:int _id; // 職工編號(hào)
};
class Assistant : public Student, public Teacher
{
protected:string _majorCourse; // 主修課程
};
6.3 虛擬繼承解決數(shù)據(jù)冗余和二義性的原理
為了研究虛擬繼承原理,我們給出了一個(gè)簡(jiǎn)化的菱形繼承繼承體系,再借助內(nèi)存窗口觀察對(duì)象成員的模型。
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;
}
下圖是菱形繼承的內(nèi)存對(duì)象成員模型,可以看到數(shù)據(jù)冗余:
下圖是菱形虛擬繼承的內(nèi)存對(duì)象成員模型:這里可以分析出D對(duì)象中將A放到的了對(duì)象組成的最下面,這個(gè)A同時(shí)屬于B和C,那么B和C如何去找到公共的A呢?這里是通過(guò)了B和C的兩個(gè)指針,指向的一張表。這兩個(gè)指針叫虛基表指針,這兩個(gè)表叫虛基表。虛基表中存的偏移量。通過(guò)偏移量可以找到下面的A。
B和C通過(guò)對(duì)象中的虛基表指針找到偏移量,再通過(guò)偏移量找到A。
下面是上面的Person關(guān)系菱形虛擬繼承的原理解釋:
七、繼承的總結(jié)
-
C++語(yǔ)法復(fù)雜,多繼承就是一個(gè)體現(xiàn)。有了多繼承,就存在菱形繼承,有了菱形繼承就有菱形虛擬繼承,底層實(shí)現(xiàn)就很復(fù)雜。所以一般不建議設(shè)計(jì)出多繼承,一定不要設(shè)計(jì)出菱形繼承,否則在復(fù)雜度及性能上都有問題。
-
繼承和組合
- public繼承是一種is-a的關(guān)系。也就是說(shuō)每個(gè)派生類對(duì)象都是一個(gè)基類對(duì)象。
- 組合是一種has-a的關(guān)系。假設(shè)B組合了A,每個(gè)B對(duì)象中都有一個(gè)A對(duì)象。
- 繼承允許你根據(jù)基類的實(shí)現(xiàn)來(lái)定義派生類的實(shí)現(xiàn)。這種通過(guò)生成派生類的復(fù)用通常被稱為白箱復(fù)用(white-box reuse)。術(shù)語(yǔ)“白箱”是相對(duì)可視性而言:在繼承方式中,基類的內(nèi)部細(xì)節(jié)對(duì)子類可見 。繼承一定程度破壞了基類的封裝,基類的改變,對(duì)派生類有很大的影響。派生類和基類間的依賴關(guān)系很強(qiáng),耦合度高。
- 對(duì)象組合是類繼承之外的另一種復(fù)用選擇。新的更復(fù)雜的功能可以通過(guò)組裝或組合對(duì)象來(lái)獲得。對(duì)象組合要求被組合的對(duì)象具有良好定義的接口。這種復(fù)用風(fēng)格被稱為黑箱復(fù)用(black-box reuse),因?yàn)閷?duì)象的內(nèi)部細(xì)節(jié)是不可見的。對(duì)象只以“黑箱”的形式出現(xiàn)。組合類之間沒有很強(qiáng)的依賴關(guān)系,耦合度低。優(yōu)先使用對(duì)象組合有助于你保持每個(gè)類被封裝。
- 實(shí)際盡量多去用組合。組合的耦合度低,代碼維護(hù)性好。不過(guò)繼承也有用武之地的,有些關(guān)系就適合繼承那就用繼承,另外要實(shí)現(xiàn)多態(tài),也必須要繼承。類之間的關(guān)系可以用繼承,可以用組合,就用組合。
// Car和BMW Car和Benz構(gòu)成is-a的關(guān)系
class Car
{
protected:string _colour = "白色"; // 顏色string _num = "陜ABIT00"; // 車牌號(hào)
};class BMW : public Car
{
public:void Drive() {cout << "好開-操控" << endl;}
};class Benz : public Car
{
public:void Drive() {cout << "好坐-舒適" << endl;}
};// Tire和Car構(gòu)成has-a的關(guān)系class Tire
{
protected:string _brand = "Michelin"; // 品牌size_t _size = 17; // 尺寸};class Car
{
protected:string _colour = "白色"; // 顏色string _num = "陜ABIT00"; // 車牌號(hào)Tire _t; // 輪胎
};