C++繼承與菱形繼承詳細(xì)介紹
繼承的概念和定義
繼承機(jī)制是面向?qū)ο蟪绦蛟O(shè)計(jì)的一種實(shí)現(xiàn)代碼復(fù)用的重要手段,它允許程序員在保持原有類(lèi)特性的基礎(chǔ)上進(jìn)行拓展,增加其他的功能,在此基礎(chǔ)上也就產(chǎn)生了一個(gè)新的類(lèi),稱(chēng)為派生類(lèi)。繼承呈現(xiàn)了面向?qū)ο蟪绦蛟O(shè)計(jì)的層次結(jié)構(gòu),是類(lèi)設(shè)計(jì)層次的復(fù)用。
//以下代碼就是采用了繼承機(jī)制的一個(gè)場(chǎng)景
class person
{
protected:
char _name[28];
int _age;
char _id[30];
};
//繼承是代碼復(fù)用的一種重要手段
class student :public person
{
protected:
char _academy[50]; //學(xué)院
};繼承的格式

在前面的例子中,person是基類(lèi),student是派生類(lèi),繼承方式是public. 這是很容易記憶的,person是基礎(chǔ)的類(lèi),student是在person這個(gè)類(lèi)的基礎(chǔ)之上派生出來(lái)的。這就非常地像父子關(guān)系,所以基類(lèi)又可以稱(chēng)為父類(lèi),派生類(lèi)又可為子類(lèi)。子類(lèi)的后面緊跟著:,是:后面這個(gè)類(lèi)派生出來(lái)的。
繼承關(guān)系和訪(fǎng)問(wèn)限定符
繼承的幾種方式和訪(fǎng)問(wèn)限定符是相似的。
三種繼承方式:public繼承、protected繼承、private繼承。
三種訪(fǎng)問(wèn)限定符:public訪(fǎng)問(wèn)、protected訪(fǎng)問(wèn)、private訪(fǎng)問(wèn)。
基類(lèi)類(lèi)成員的訪(fǎng)問(wèn)權(quán)限和派生類(lèi)繼承基類(lèi)的繼承方式, 關(guān)系到了基類(lèi)被繼承下來(lái)的類(lèi)成員在派生類(lèi)中的情況。ps:這句話(huà)起始很好理解地,就是這句話(huà)寫(xiě)起來(lái)就變得繞口和復(fù)雜了,哈哈哈??.
| 基類(lèi)成員/繼承方式 | public繼承 | protected繼承 | private繼承 |
|---|---|---|---|
| public成員 | 在派生類(lèi)中為public成員 | 在派生類(lèi)中為protected成員 | 在派生類(lèi)中為private成員 |
| protected成員 | 在派生類(lèi)中為protected成員 | 在派生類(lèi)中為protected成員 | 在派生類(lèi)中為private成員 |
| private成員 | 在派生類(lèi)中不可見(jiàn) | 在派生類(lèi)中不可見(jiàn) | 在派生類(lèi)中不可見(jiàn) |
這里的不可見(jiàn)指的是:基類(lèi)中的private成員也是被繼承下來(lái)了的,只是在語(yǔ)法上,在派生類(lèi)的類(lèi)里和類(lèi)外都不能夠訪(fǎng)問(wèn)。
記住這個(gè)特殊的點(diǎn),那么其他的就可理解為“權(quán)限問(wèn)題”,這里“權(quán)限只能縮小,不能放大”。例如,基類(lèi)的public成員以private繼承方式繼承下來(lái),為“權(quán)限小的那個(gè)”,也就是繼承下來(lái)后在派生類(lèi)中是private成員。
class person
{
protected:
char _name[28];
char _id[30];
private:
int _age;
};
class teacher :public person
{
public:
teacher()
:_age(0) //基類(lèi)的private成員在派生類(lèi)里不能訪(fǎng)問(wèn)
{
}
protected:
char _jodid[20]; //工號(hào)
};
int main(void)
{
teacher t1;
t1._age; //基類(lèi)的private成員在類(lèi)外不能訪(fǎng)問(wèn)
return 0;
}基類(lèi)和派生類(lèi)之間的賦值
派生類(lèi)的對(duì)象可以賦值給其基類(lèi)的對(duì)象、基類(lèi)的指針、基類(lèi)的引用。

就像上面這樣,取基類(lèi)需要被賦值的值過(guò)去即可。

派生類(lèi)賦值給基類(lèi)的對(duì)象、基類(lèi)的指針、基類(lèi)的引用。在派生類(lèi)中取基類(lèi)需要的,就像把派生類(lèi)給切割了一樣、所以這里有一個(gè)形象的稱(chēng)呼:切割/切片
class Person
{
protected:
string _name; // 姓名
string _sex; // 性別
int _age; // 年齡
};
class Student : public Person
{
public:
int _id; // 學(xué)號(hào)
};
int main(void)
{
//可以將派生類(lèi)賦值給基類(lèi)的對(duì)象、指針、引用
Person p;
Student s;
p = s;
Person* Pptr = &s;
Person& Refp = s;
//注意不能將將基類(lèi)對(duì)象給派生類(lèi)對(duì)象
//s = p;
//允許將基類(lèi)指針賦值給派生類(lèi)指針,但是需要強(qiáng)制轉(zhuǎn)換
Student* sPtr = (Student*)Pptr;
return 0;
}【注意】
1、不允許基類(lèi)對(duì)象賦值給派生類(lèi)對(duì)象
2、允許基類(lèi)指針賦值給派生類(lèi)指針, 但是需要強(qiáng)制轉(zhuǎn)化。這種轉(zhuǎn)化雖然可以,但是會(huì)存在越界訪(fǎng)問(wèn)的問(wèn)題。
繼承中的作用域
基類(lèi)和派生類(lèi)都有獨(dú)立的作用域。繼承下來(lái)的基類(lèi)成員在一個(gè)作用域,派生類(lèi)的成員在另一作用域。
//以下代碼的運(yùn)行結(jié)果是什么?
class Person
{
protected:
string _name = "楊XX"; // 姓名
int _num = 12138; // 身份證號(hào)
};
class Student : public Person
{
public:
void Print()
{
cout <<_num << endl;
}
protected:
int _num = 52622; // 學(xué)號(hào)
};
void Test()
{
Student s1;
s1.Print();
};
基類(lèi)中有一個(gè)_num 給了缺省值“12138”, 派生類(lèi)中也有一個(gè)_name,給了缺省值“52622”,那么在派生類(lèi)里直接使用_name,使用的具體是哪一個(gè)類(lèi)里的?

使用的是派生類(lèi)Student里的。
總結(jié):基類(lèi)和派生類(lèi)中如果有同名成員,派生類(lèi)將屏蔽基類(lèi)對(duì)同名成員的直接訪(fǎng)問(wèn),這種情況稱(chēng)為隱藏 , 或者稱(chēng)為重定義。
如果想要訪(fǎng)問(wèn),則使用基類(lèi)::基類(lèi)成員顯示的訪(fǎng)問(wèn)。
class Person
{
protected:
string _name = "楊XX"; // 姓名
int _num = 12138; // 身份證號(hào)
};
class Student : public Person
{
public:
void Print()
{
cout << "身份證號(hào):" << Person::_num << endl;
cout << "學(xué)號(hào):" << _num << endl;
}
protected:
int _num = 52622; // 學(xué)號(hào)
};
void Test()
{
Student s1;
s1.Print();
};
int main(void)
{
Test();
return 0;
}運(yùn)行結(jié)果

我們已經(jīng)了解了什么是隱藏。那么來(lái)看一下下面這些代碼。
//以下的兩個(gè)函數(shù)構(gòu)成隱藏還是重載?
class A
{
public:
void func()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void func(int num)
{
cout << "func(int num)" << endl;
}
};
void Test()
{
B b;
b.func(10);
}
函數(shù)重載要求在同一作用域,而被繼承下來(lái)的基類(lèi)成員和派生類(lèi)成員在不同的作用域,所以構(gòu)成的是隱藏。
```cpp
//以下代碼的運(yùn)行結(jié)果是什么?
class A
{
public:
void func()
{
cout << "func()" << endl;
}
};
class B : public A
{
public:
void func(int num)
{
cout << "func(int num)" << endl;
}
};
void Test()
{
B b;
b.func();
}
因?yàn)?code>func()函數(shù)隱藏了,在派生類(lèi)的作用域內(nèi)沒(méi)有func()函數(shù),所以會(huì)出現(xiàn)編譯報(bào)錯(cuò)。
派生類(lèi)的默認(rèn)成員函數(shù)
類(lèi)有8個(gè)默認(rèn)成員函數(shù),這里只說(shuō)重點(diǎn)的四個(gè)默認(rèn)成員函數(shù):構(gòu)造函數(shù)、析構(gòu)函數(shù)、拷貝構(gòu)造函數(shù)、賦值重載函數(shù)
如果我們不寫(xiě)派生類(lèi)的構(gòu)造函數(shù)和析構(gòu)函數(shù),編譯器會(huì)做如下的事情:
1、基類(lèi)被繼承下來(lái)的部分會(huì)調(diào)用基類(lèi)的默認(rèn)構(gòu)造函數(shù)和析構(gòu)函數(shù)
2、派生類(lèi)自己也會(huì)生成默認(rèn)構(gòu)造和析構(gòu)函數(shù),派生類(lèi)自己的和普通類(lèi)的處理一樣
如果我們不寫(xiě)派生類(lèi)的賦值構(gòu)造函數(shù)和拷貝構(gòu)造函數(shù),編譯器會(huì)做如下的事情
3、基類(lèi)被繼承下來(lái)的部分會(huì)調(diào)用基類(lèi)的默認(rèn)拷貝構(gòu)造函數(shù)和賦值構(gòu)造函數(shù)。
4、派生類(lèi)自己也會(huì)生成默認(rèn)賦值拷貝構(gòu)造函數(shù)和賦值函數(shù),和普通類(lèi)的處理一樣。
什么情況下需要自己寫(xiě)?
1、父類(lèi)沒(méi)有合適的默認(rèn)構(gòu)造函數(shù),需要自己顯示地寫(xiě)
2、如果子類(lèi)有資源需要釋放,就需要自己顯示地寫(xiě)析構(gòu)函數(shù)
3、如果子類(lèi)存在淺拷貝的問(wèn)題,就需要自己實(shí)現(xiàn)拷貝構(gòu)造和賦值函數(shù)解決淺拷貝的問(wèn)題。
如果需要自己寫(xiě)派生類(lèi)的這幾個(gè)重點(diǎn)成員函數(shù),那么該如何寫(xiě)?
//如果需要自己實(shí)現(xiàn)派生類(lèi)的幾個(gè)四個(gè)重點(diǎn)默認(rèn)成員函數(shù),需要如何實(shí)現(xiàn)?該注意什么?
class Person
{
public:
Person(const char* name)
:_name(name)
{
cout << "Person(const char* name)" << endl; //方便查看它什么被調(diào)用了
}
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
{
protected:
int _id; //學(xué)號(hào)
int* _ptr = new int[10]; //給一個(gè)需要自己實(shí)現(xiàn)默認(rèn)成員函數(shù)場(chǎng)景用以舉例
};
1、實(shí)現(xiàn)派生類(lèi)的構(gòu)造函數(shù):需要調(diào)用基類(lèi)的構(gòu)造函數(shù)初始化被繼承下來(lái)的基類(lèi)部分的成員。如果基類(lèi)沒(méi)有合適的默認(rèn)構(gòu)造函數(shù),就需要在實(shí)現(xiàn)派生類(lèi)構(gòu)造函數(shù)的初始化列表階段顯示調(diào)用。
2、實(shí)現(xiàn)派生類(lèi)的析構(gòu)函數(shù):派生類(lèi)的析構(gòu)函數(shù)會(huì)在被調(diào)用完成后自動(dòng)調(diào)用基類(lèi)的析構(gòu)函數(shù)清理被繼承下來(lái)的基類(lèi)成員。這樣可以保證派生類(lèi)自己的成員的清理先于被繼承下來(lái)的基類(lèi)成員。ps:析構(gòu)函數(shù)名字會(huì)被統(tǒng)一處理成destructor(),所以被繼承下來(lái)的基類(lèi)的析構(gòu)函數(shù)和派生類(lèi)的析構(gòu)函數(shù)構(gòu)成隱藏。
3、實(shí)現(xiàn)派生類(lèi)的拷貝構(gòu)造函數(shù):需要調(diào)用基類(lèi)的拷貝構(gòu)造函數(shù)完成被繼承下來(lái)的基類(lèi)成員的拷貝初始化。
4、實(shí)現(xiàn)派生類(lèi)的operator=:需要調(diào)用基類(lèi)的operator=完成被繼承下來(lái)的基類(lèi)成員的賦值。
5、派生類(lèi)對(duì)象初始化先調(diào)用基類(lèi)構(gòu)造再調(diào)用派生類(lèi)構(gòu)造。
class Student : public Person
{
public:
Student(const char* name, int id)
: Person(name)
, _id(id)
{
cout << "Student()" << endl;
}
Student(const Student& s)
: Person(s)
, _id(s._id)
{
cout << "Student(const Student& s)" << endl;
}
Student& operator = (const Student& s)
{
cout << "Student& operator= (const Student& s)" << endl;
if (this != &s)
{
Person::operator =(s);
_id = s._id;
}
return *this;
}
~Student()
{
cout << "~Student()" << endl;
}
protected:
int _id; //學(xué)號(hào)
};菱形繼承
繼承可分為單繼承和多繼承。
單繼承:一個(gè)派生類(lèi)只有一個(gè)直接基類(lèi)

多繼承:一個(gè)派生類(lèi)有兩個(gè)或兩個(gè)以上的直接基類(lèi)。

而多繼承中又存在著一種特殊的繼承關(guān)系,菱形繼承

它們之間的繼承關(guān)系邏輯上就類(lèi)似一個(gè)菱形,所以稱(chēng)為菱形繼承。菱形繼承相對(duì)于其他繼承關(guān)系是復(fù)雜的。
B中有一份A的成員,C中也有一份A的成員,D將B和C都繼承了,那么D中被繼承下來(lái)的A的成員不就有兩份了嗎?不難看出,菱形繼承有數(shù)據(jù)冗余和二義性的問(wèn)題。
class Person
{
public:
string _name; // 姓名
};
class Student : public Person
{
public:
int _num; //學(xué)號(hào)
};
class Teacher : public Person
{
public:
int _id; // 職工編號(hào)
};
class Assistant : public Student, public Teacher
{
public:
string _majorCourse; // 主修課程
};
int main()
{
// 二義性、數(shù)據(jù)冗余
Assistant a;
a._id = 1;
a._num = 2;
// 這樣會(huì)有二義性無(wú)法明確知道訪(fǎng)問(wèn)的是哪一個(gè)
a._name = "peter";
return 0;
}上面的繼承關(guān)系如下:

此時(shí)Assitant中有兩份_name.存在數(shù)據(jù)冗余和二義性的問(wèn)題。
二義性的問(wèn)題是比較好解決的,使用::指定就可以了,但是并不能解決數(shù)據(jù)冗余的問(wèn)題。
int main()
{
// 二義性、數(shù)據(jù)冗余
Assistant a;
a._id = 1;
a._num = 2;
a.Student::_name = "小張";
a.Teacher::_name = "張老師";
return 0;
}
虛擬繼承可以解決繼承的數(shù)據(jù)冗余和二義性的問(wèn)題。如上面所畫(huà)的邏輯繼承關(guān)系。在開(kāi)始可能產(chǎn)生數(shù)據(jù)冗余和二義性的地方使用虛擬繼承,即可解決,但是在其他地方不要去使用虛擬繼承。
虛擬繼承格式

虛擬繼承解決數(shù)據(jù)冗余和二義性的原理
為了更好地研究,在這里給出一個(gè)比較簡(jiǎn)單的菱形繼承體系
class A {
public:
int _a;
};
class B : public A{
public:
int _b;
};
class C : 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;
}

B和C中都有一份A的數(shù)據(jù)可以看出數(shù)據(jù)的冗余。
現(xiàn)在增加虛擬繼承機(jī)制,解決數(shù)據(jù)冗余和二義性。
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 = 3;
d._c = 4;
d._d = 5;
return 0;
}
再次調(diào)式調(diào)用內(nèi)存窗口,會(huì)發(fā)現(xiàn)

和沒(méi)有采用虛擬繼承的內(nèi)存窗口有較大的變化。
B中的地址0x00677bdc里有什么?C中的地址0x00677be4里有什么?

從內(nèi)存窗口可看出,菱形虛擬繼承,內(nèi)存中只在對(duì)象組成的最高處地址保存了一份A,A是B、C公共的。而B(niǎo)和C里分別保存了一個(gè)指針,該指針指向一張表。這張表稱(chēng)為虛基表,而指向虛基表的指針?lè)Q虛基指針。虛基表中保存的值,是到A地址的偏移量,通過(guò)這個(gè)偏移量就能夠找到A了。
繼承和組合的區(qū)分與聯(lián)系
在沒(méi)有學(xué)習(xí)繼承之前,我們其實(shí)頻繁地使用組合。
class head
{
private:
int _eye;
int _ear;
int _mouth;
};
class hand
{
private:
int _arm;
int _fingers;
};
class Person
{
//組合
//一個(gè)人由手、頭等組合
hand _a;
head _b;
};
- 繼承是一種is-a的關(guān)系, 每一個(gè)派生類(lèi)是基類(lèi),例如,Student是一個(gè)Person, Teacher 是一個(gè)Person
- 組合是一種has-a的關(guān)系,Person組合了head, hand, 每一個(gè)Person對(duì)象中都有一個(gè)head、hand對(duì)象。
- 如果某種情況既可以使用繼承又可以使用組合,那么優(yōu)先使用對(duì)象組合,而不是類(lèi)繼承。
其余注意事項(xiàng)
- 友元關(guān)系不能被繼承,好比父親的朋友不一定是你的朋友。
- 如果基類(lèi)中定義了靜態(tài)成員,當(dāng)這個(gè)基類(lèi)被實(shí)例化后出現(xiàn)了一份,那么整個(gè)繼承體系中都只有這一份實(shí)例。
到此這篇關(guān)于C++繼承與菱形繼承詳細(xì)介紹的文章就介紹到這了,更多相關(guān)C++繼承 內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C語(yǔ)言中數(shù)組作為函數(shù)的參數(shù)以及返回值的使用簡(jiǎn)單入門(mén)
這篇文章主要介紹了C語(yǔ)言中數(shù)組作為函數(shù)的參數(shù)以及返回值的使用簡(jiǎn)單入門(mén),這里以一維數(shù)組作為基本條件進(jìn)行例子講解,需要的朋友可以參考下2015-12-12
C語(yǔ)言實(shí)現(xiàn)簡(jiǎn)單職工信息管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C語(yǔ)言實(shí)現(xiàn)簡(jiǎn)單職工信息管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-07-07
C語(yǔ)言之浮點(diǎn)數(shù)的表示與儲(chǔ)存方式
這篇文章主要介紹了C語(yǔ)言之浮點(diǎn)數(shù)的表示與儲(chǔ)存方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2025-03-03
C語(yǔ)言 if else 語(yǔ)句詳細(xì)講解
本文主要介紹C語(yǔ)言中的if else,這里詳細(xì)介紹了if else 語(yǔ)句并提供了簡(jiǎn)單的示例代碼,希望能幫助編程入門(mén)的小伙伴學(xué)習(xí)2016-07-07
C++實(shí)現(xiàn)圖的遍歷算法(DFS,BFS)的示例代碼
本文給大家?guī)?lái)的是圖遍歷的算法,DFS(深度優(yōu)先遍歷),BFS(廣度優(yōu)先遍歷)。這兩個(gè)算法是比較重要和常用的算法,但是在圖中的實(shí)現(xiàn)只是最基本的操作,快跟隨小編一起學(xué)習(xí)一下吧2022-07-07
C語(yǔ)言超詳細(xì)講解數(shù)據(jù)結(jié)構(gòu)中雙向帶頭循環(huán)鏈表
帶頭雙向循環(huán)鏈表:結(jié)構(gòu)最復(fù)雜,一般用在單獨(dú)存儲(chǔ)數(shù)據(jù)。實(shí)際中使用的鏈表數(shù)據(jù)結(jié)構(gòu),都是帶頭雙向循環(huán)鏈表。另外這個(gè)結(jié)構(gòu)雖然結(jié)構(gòu)復(fù)雜,但是使用代碼實(shí)現(xiàn)以后會(huì)發(fā)現(xiàn)結(jié)構(gòu)會(huì)帶來(lái)很多優(yōu)勢(shì),實(shí)現(xiàn)反而簡(jiǎn)單2022-04-04
在vs2010中,輸出當(dāng)前文件路徑與源文件當(dāng)前行號(hào)的解決方法
本篇文章是對(duì)在vs2010中,輸出當(dāng)前文件路徑與源文件當(dāng)前行號(hào)的解決方法進(jìn)行了詳細(xì)的分析介紹,需要的朋友參考下2013-05-05

