C++數(shù)據(jù)結(jié)構(gòu)繼承的概念與菱形繼承及虛擬繼承和組合
??博客代碼已上傳至gitee:https://gitee.com/byte-binxin/cpp-class-code
??繼承的概念
繼承:繼承機(jī)制是面向?qū)ο蟪绦蛟O(shè)計(jì)使代碼可以復(fù)用的最重要的手段,它允許程序員在保持原有類特性的基礎(chǔ)上進(jìn)行擴(kuò)展,增加功能,這樣產(chǎn)生新的類,稱派生類。繼承呈現(xiàn)了面向?qū)ο蟪绦蛟O(shè)計(jì)的層次結(jié)構(gòu),體現(xiàn)了由簡單到復(fù)雜的認(rèn)知過程。以前我們接觸的復(fù)用都是函數(shù)復(fù)用,繼承是類設(shè)計(jì)層次的復(fù)用。
??繼承的定義
語法:

說明: 派生類會(huì)將基類的成員變量和成員函數(shù)都繼承下來,但是訪問限定符會(huì)根據(jù)繼承方式而發(fā)生變化。
繼承方式有三種:
- public繼承
- protected繼承
- private繼承
訪問限定符:
- public訪問
- protected訪問
- private訪問
繼承基類成員的訪問方式的變化:
| 類成員/繼承方式 | public繼承 | protected繼承 | private繼承 |
|---|---|---|---|
| 基類的public成員 | 派生類的public成員 | 派生類的protected成員 | 派生類的private成員 |
| 基類的protected成員 | 派生類的protected成員 | 派生類的protected成員 | 派生類的private成員 |
| 基類的private成員 | 派生類中不可見 | 派生類中不可見 | 派生類中不可見 |
總結(jié):
- 基類的private成員在派生類中都是不可見的,這里的不可見是指基類的私有成員還是被繼承到了派生類對(duì)象中,但是語法上限制派生類對(duì)象不管在類里面還是類外面都不能去訪問它。
- 基類成員在父類中的訪問方式=min(成員在基類的訪問限定符,繼承方式),public>protected>private。
- 一般會(huì)把基類中不想讓類外訪問的成員設(shè)置為protecd成員,不讓類外訪問,但是讓派生類可以訪問。
??基類和派生類對(duì)象之間的賦值轉(zhuǎn)換
派生類對(duì)象會(huì)通過 “切片” 或 “切割” 的方式賦值給基類的對(duì)象、指針或引用。但是基類對(duì)象不能賦值給派生類對(duì)象。

實(shí)例演示:
class Person
{
public:
Person(const char* name = "")
:_name(name)
{}
void Print()
{
cout << "name:" << _name << " age:" << _age << endl;
}
protected:
string _name = "";
int _age = 1;
};
class Student : public Person
{
public:
Student()
:Person("xiaoming")
{}
void Print()
{
cout << "name:" << _name << " age:" << _age << " _stuid:" << _stuid << " _major:" << _major << endl;
}
private:
int _stuid = 0;// 學(xué)號(hào)
int _major = 0;// 專業(yè)
};
int main()
{
Student s;
// 子類對(duì)象可以賦值給父類的對(duì)象、指針和引用 反過來不行
// Student對(duì)象通過 “切片” 或 “切割” 的方式進(jìn)行賦值
Person p1 = s;
Person* p2 = &s;
Person& p3 = s;
p1.Print();
p2->Print();
p3.Print();
// 基類的指針可以通過強(qiáng)制類型轉(zhuǎn)換賦值給派生類的指針
Student* ps = (Student*)p2;
ps->Print();
return 0;
}

總結(jié):
- 派生類對(duì)象可以“切片”或“切割”的方式賦值給基類的對(duì)象,基類的指針或基類的引用,就是把基類的那部分切割下來。
- 基類對(duì)象不能給派生類對(duì)象賦值。
- 基類的指針可以通過強(qiáng)制類型轉(zhuǎn)換賦值給派生類的指針。但必須是基類的指針指向派生類的對(duì)象才是安全的,因?yàn)槿绻愂嵌鄳B(tài)類型,會(huì)引發(fā)多態(tài)。
??繼承中的作用域
在繼承體系中,基類和派生類對(duì)象都有獨(dú)立的作用域,子類中的成員(成員變量和成員函數(shù))會(huì)對(duì)父類的同名成員進(jìn)行隱藏,也叫重定義。
實(shí)例演示:
class Person
{
public:
Person(const char* name = "")
:_name(name)
{}
void Print()
{
cout << "name:" << _name << " age:" << _age << endl;
}
protected:
string _name = "";
int _age = 1;
};
class Teacher : public Person
{
public:
void Print()
{
cout << "name:" << _name << " age:" << _age << " jobid:" << _jobid << endl;
}
private:
int _jobid = 0;// 工號(hào)
};
int main()
{
Teacher t;
t.Print();
t.Person::Print();// 子類會(huì)隱藏(重定義)父類的同名成員(同名函數(shù)或同名成員變量) 可以通過指定域作用限定符訪問
return 0;
}
代碼運(yùn)行結(jié)果如下:

得出結(jié)論: 子類中的成員(成員變量和成員函數(shù))會(huì)對(duì)父類的同名成員進(jìn)行隱藏,如果相要訪問父類的同名成員,必須指定類域訪問。
看下面一個(gè)小問題: 請(qǐng)問A中的fun函數(shù)和B中的fun函數(shù)是構(gòu)成重載還是隱藏?
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);
};
答案: 兩個(gè)函數(shù)在不同的作用域,不可能構(gòu)成重載。因?yàn)闃?gòu)成重載的條件是兩個(gè)函數(shù)必須在同一作用域,而隱藏是要求在基類和派生類不同作用域的,所以這里同名成員是構(gòu)造隱藏。
??派生類的默認(rèn)成員函數(shù)
C++中的每個(gè)對(duì)象中會(huì)有6個(gè)默認(rèn)成員函數(shù)。默認(rèn)的意思就是我們不寫,編譯器會(huì)生成一個(gè)。那么在繼承中,子類的默認(rèn)成員函數(shù)是怎么生成的呢?
先看下面一個(gè)例子:
class Person
{
public:
Person(const char* name = "", int age = 1)
:_name(name)
,_age(age)
{
cout << "Person()" << endl;
}
Person(const Person& p)
:_name(p._name)
, _age(p._age)
{
cout << "Person(const Person& p)" << endl;
}
Person& operator=(const Person& p)
{
_name = p._name;
_age = p._age;
cout << "Person& operator=(const Person& p)" << endl;
return *this;
}
void Print()
{
cout << "name:" << _name << " age:" << _age << endl;
}
~Person()
{
cout << "~Person()" << endl;
}
protected:
string _name;
int _age;
};
class Student : public Person
{
public:
Student(const char* name, int age, int stuid = 0)
:Person(name, age)// 此處調(diào)用父類的構(gòu)造函數(shù)堆繼承下來的成員進(jìn)行初始化,不謝的話,編譯器調(diào)用父類的默認(rèn)構(gòu)造函數(shù)
, _stuid(stuid)
{
cout << "Student()" << endl;
}
Student(const Student& s)
:Person(s)// 子類對(duì)象可以傳給父類的對(duì)象、指針或引用
,_stuid(s._stuid)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator=(const Student& s)
{
cout << "Student& operator=(const Student& s)" << endl;
if (this != &s)
{
Person::operator=(s);// 先完成基類的復(fù)制
_stuid = s._stuid;
}
return *this;
}
void Print()
{
cout << "name:" << _name << " age:" << _age << " _stuid:" << _stuid << endl;
}
~Student()
{
// 基類和派生類的析構(gòu)函數(shù)的函數(shù)名都被編譯器處理成了destruction,構(gòu)成隱藏,是一樣指定域訪問
//Person::~Person();// 不需要顯示調(diào)用 編譯器會(huì)自動(dòng)先調(diào)用派生類的析構(gòu)函數(shù),然后調(diào)用基類的析構(gòu)函數(shù)
cout << "~Student()" << endl;
}
private:
int _stuid;// 學(xué)號(hào)
};
測試1:構(gòu)造函數(shù)和析構(gòu)函數(shù)
void test1()
{
Student s("小明",18,10);
}
代碼運(yùn)行結(jié)果如下:

總結(jié)1: 子類的構(gòu)造函數(shù)必須調(diào)用基類的構(gòu)造函數(shù)初始化基類的那一部分成員,如果基類沒有默認(rèn)構(gòu)造函數(shù),則必須在派生類構(gòu)造函數(shù)的初始化列表階段顯示調(diào)用。子類的析構(gòu)函數(shù)會(huì)在被調(diào)用完成后自動(dòng)調(diào)用基類的析構(gòu)函數(shù)清理基類的成員。不需要顯示調(diào)用。這里子類和父類的析構(gòu)函數(shù)的函數(shù)名會(huì)被編譯器處理成destructor,這樣兩個(gè)函數(shù)構(gòu)成隱藏。
測試2:拷貝構(gòu)造函數(shù)
void test2()
{
Student s1("小明", 18, 10);
Student s2(s1);
}
代碼運(yùn)行結(jié)果如下:

總結(jié)2: 子類的拷貝構(gòu)造必須代用父類的拷貝構(gòu)造完成父類成員的拷貝。
測試3:operator=

結(jié)論3: 子類的operator=必須調(diào)用基類的operator完成基類的賦值。
思考
如何設(shè)計(jì)一個(gè)不能被繼承的類? 把該類的構(gòu)造函數(shù)設(shè)為私有。如果基類的構(gòu)造函數(shù)是私有,那么派生類不能調(diào)用基類的構(gòu)造函數(shù)完成基類成員的初始化,則無法進(jìn)行構(gòu)造。所以這樣設(shè)計(jì)的類不可以被繼承。(后面還會(huì)將加上final關(guān)鍵字的類也不可以被繼承)
總結(jié):
- 子類的構(gòu)造函數(shù)必須調(diào)用基類的構(gòu)造函數(shù)初始化基類的那一部分成員,如果基類沒有默認(rèn)構(gòu)造函數(shù),則必須在派生類構(gòu)造函數(shù)的初始化列表階段顯示調(diào)用。
- 子類的拷貝構(gòu)造必須代用父類的拷貝構(gòu)造完成父類成員的拷貝。
- 子類的operator=必須調(diào)用基類的operator完成基類的賦值。
- 子類的析構(gòu)函數(shù)會(huì)在被調(diào)用完成后自動(dòng)調(diào)用基類的析構(gòu)函數(shù)清理基類的成員。不需要顯示調(diào)用。
- 子類對(duì)象會(huì)先調(diào)用父類的構(gòu)造在調(diào)用子類的構(gòu)造。
- 子類對(duì)象會(huì)先析構(gòu)子類的析構(gòu)再調(diào)用父類的析構(gòu)。
??繼承中的兩個(gè)小細(xì)節(jié)
??繼承和友元
友元關(guān)系不能被繼承。也就是說基類的友元不能夠訪問子類的私有和保護(hù)成員。
??繼承和靜態(tài)成員
基類定義的static靜態(tài)成員,存在于整個(gè)類中,不屬于某個(gè)類,無論右多少個(gè)派生類,都這有一個(gè)static成員。
實(shí)例演示:
class Person
{
public:
Person()
{
++_count;
}
// static成員存在于整個(gè)類 無論實(shí)例化出多少對(duì)象,都只有一個(gè)static成員實(shí)例
static int _count;
};
int Person::_count = 0;
class Student :public Person
{
public:
int _stuid;
};
int main()
{
Student s1;
Student s2;
Student s3;
// Student()._count = 10;
cout << "人數(shù):" << Student()._count - 1 << endl;
return 0;
}
代碼運(yùn)行結(jié)果如下:

??單繼承和多繼承(菱形繼承)
單繼承: 一個(gè)子類只有一個(gè)直接父類時(shí)稱這個(gè)繼承關(guān)系為單繼承。

多繼承: 一個(gè)子類有兩個(gè)或以上的直接父類時(shí)稱這個(gè)繼承關(guān)系為多繼承。

菱形繼承: 多繼承的一種特殊情況。

多繼承帶來的問題: 子類會(huì)得到兩份BenZ的數(shù)據(jù),會(huì)造成數(shù)據(jù)冗余和二義性。
??虛擬繼承
??概念
為了解決菱形繼承帶來的數(shù)據(jù)冗余和二義性的問題,C++提出來虛擬繼承這個(gè)概念。虛擬繼承可以解決前面的問題,在繼承方式前加椰果virtual的關(guān)鍵字即可。
class Person
{
public:
string _name;
};
// 不要在其他地方去使用。
class Student : virtual public Person
{
public:
int _num; //學(xué)號(hào)
};
class Teacher : virtual public Person
{
public:
int _id; // 職工編號(hào)
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修課程
};
??虛擬繼承的原理
先看下面一串代碼:
class A
{
public:
int _a;
};
class B :virtual public A
{
public:
int _b;
};
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 = 4;
d._c = 5;
d._d = 6;
return 0;
}
我們通過內(nèi)存窗口查看它的對(duì)象模型:

原理: 從上圖可以看出,A對(duì)象同時(shí)屬于B和C,B和C中分別存放了一個(gè)指針,這個(gè)指針叫虛基表指針,分別指向的兩張表,叫虛基表,虛基表中存的是偏移量,B和C通過偏移量就可以找到公共空間(存放A對(duì)象的位置)。

??組合與繼承
總結(jié)一下幾點(diǎn):
- 組合和繼承都屬于類層次的復(fù)用。
- public繼承是一種is-a的關(guān)系。也就是說每個(gè)派生類對(duì)象都是一個(gè)基類對(duì)象-。
- 組合是一種has-a的關(guān)系。假設(shè)B組合了A,每個(gè)B對(duì)象中都有一個(gè)A對(duì)象。
- 優(yōu)先使用對(duì)象組合,而不是類繼承 。
- 繼承允許你根據(jù)基類的實(shí)現(xiàn)來定義派生類的實(shí)現(xiàn)。這種通過生成派生類的復(fù)用通常被稱為白箱復(fù)用。術(shù)語“白箱”是相對(duì)可視性而言:在繼承方式中,基類的內(nèi)部細(xì)節(jié)對(duì)子類可見 。繼承一定程度破壞了基類的封裝,基類的改變,對(duì)派生類有很大的影響。派生類和基類間的依賴關(guān)系很強(qiáng),耦合度高。
- 對(duì)象組合是類繼承之外的另一種復(fù)用選擇。新的更復(fù)雜的功能可以通過組裝或組合對(duì)象來獲得。對(duì)象組合要求被組合的對(duì)象具有良好定義的接口。這種復(fù)用風(fēng)格被稱為黑箱復(fù)用,因?yàn)閷?duì)象的內(nèi)部細(xì)是不可見的。對(duì)象只以“黑箱”的形式出現(xiàn)。 組合類之間沒有很強(qiáng)的依賴關(guān)系,耦合度低。優(yōu)先使用對(duì)象組合有助于你保持每個(gè)類被封裝。
- 實(shí)際盡量多去用組合。組合的耦合度低,代碼維護(hù)性好。不過繼承也有用武之地的,有些關(guān)系就適合繼承那就用繼承,另外要實(shí)現(xiàn)多態(tài),也必須要繼承。類之間的關(guān)系可以用繼承,可以用組合,就用組合。
C++的缺陷之一:
多繼承就是一個(gè)。多繼承會(huì)帶來菱形繼承,菱形繼承又會(huì)帶來數(shù)據(jù)冗余和二義性,為了解決這個(gè)問題,又引入了虛擬繼承。進(jìn)而導(dǎo)致C++的底層結(jié)構(gòu)對(duì)象模型非常復(fù)雜,這樣會(huì)帶來一定的損失。所以說盡量不要設(shè)計(jì)出菱形繼承。
??總結(jié)
C++的繼承使我們變得更加的富有,其中多繼承也是C++的缺陷。我們要盡量避開不好的而選擇好的一面。這篇博客就介紹到這里了,喜歡的話,歡迎點(diǎn)贊。支持和關(guān)注~

到此這篇關(guān)于C++數(shù)據(jù)結(jié)構(gòu)繼承的概念與菱形繼承及虛擬繼承和組合的文章就介紹到這了,更多相關(guān)C++ 繼承內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C語言結(jié)構(gòu)體嵌套與對(duì)齊超詳細(xì)講解
這篇文章主要介紹了C語言結(jié)構(gòu)體嵌套與對(duì)齊,C語言中結(jié)構(gòu)體是一種構(gòu)造類型,和數(shù)組、基本數(shù)據(jù)類型一樣,可以定義指向該種類型的指針。結(jié)構(gòu)體指針的定義類似其他基本數(shù)據(jù)類型的定義2022-12-12
探究在C++程序并發(fā)時(shí)保護(hù)共享數(shù)據(jù)的問題
這篇文章主要介紹了探究在C++程序并發(fā)時(shí)保護(hù)共享數(shù)據(jù)的問題,也有利于大家更好地理解C++多線程的一些機(jī)制,需要的朋友可以參考下2015-07-07

