C++繼承與菱形繼承詳細介紹

繼承的概念和定義

繼承機制是面向對象程序設計的一種實現代碼復用的重要手段,它允許程序員在保持原有類特性的基礎上進行拓展,增加其他的功能,在此基礎上也就產生瞭一個新的類,稱為派生類。繼承呈現瞭面向對象程序設計的層次結構,是類設計層次的復用。

//以下代碼就是采用瞭繼承機制的一個場景
class person
{
protected:
	char _name[28];
	int _age;
	char _id[30];
};
//繼承是代碼復用的一種重要手段
class student :public person
{
protected:
	char _academy[50]; //學院
};

繼承的格式

在前面的例子中,person是基類,student是派生類,繼承方式是public. 這是很容易記憶的,person是基礎的類,student是在person這個類的基礎之上派生出來的。這就非常地像父子關系,所以基類又可以稱為父類,派生類又可為子類。子類的後面緊跟著:,是:後面這個類派生出來的。

繼承關系和訪問限定符

繼承的幾種方式和訪問限定符是相似的。

三種繼承方式:public繼承、protected繼承、private繼承。

三種訪問限定符:public訪問、protected訪問、private訪問。

基類類成員的訪問權限和派生類繼承基類的繼承方式, 關系到瞭基類被繼承下來的類成員在派生類中的情況。ps:這句話起始很好理解地,就是這句話寫起來就變得繞口和復雜瞭,哈哈哈😁.

基類成員/繼承方式 public繼承 protected繼承 private繼承
public成員 在派生類中為public成員 在派生類中為protected成員 在派生類中為private成員
protected成員 在派生類中為protected成員 在派生類中為protected成員 在派生類中為private成員
private成員 在派生類中不可見 在派生類中不可見 在派生類中不可見

這裡的不可見指的是:基類中的private成員也是被繼承下來瞭的,隻是在語法上,在派生類的類裡和類外都不能夠訪問。

記住這個特殊的點,那麼其他的就可理解為“權限問題”,這裡“權限隻能縮小,不能放大”。例如,基類的public成員以private繼承方式繼承下來,為“權限小的那個”,也就是繼承下來後在派生類中是private成員。

class person
{
protected:
	char _name[28];
	char _id[30];
private:
	int _age;
};
class teacher :public person
{
public:
	teacher()
		:_age(0) //基類的private成員在派生類裡不能訪問
	{
	}
protected:
	char _jodid[20]; //工號
};
int main(void)
{
	teacher t1;
	t1._age; //基類的private成員在類外不能訪問
	return 0;
}

基類和派生類之間的賦值

派生類的對象可以賦值給其基類的對象、基類的指針、基類的引用。

就像上面這樣,取基類需要被賦值的值過去即可。

派生類賦值給基類的對象、基類的指針、基類的引用。在派生類中取基類需要的,就像把派生類給切割瞭一樣、所以這裡有一個形象的稱呼:切割/切片

class Person
{
protected:
	string _name; // 姓名
	string _sex; // 性別
	int _age; // 年齡
};
class Student : public Person
{
public:
	int _id; // 學號
};
int main(void)
{
	//可以將派生類賦值給基類的對象、指針、引用
	Person p;
	Student s;
	p = s;
	Person* Pptr = &s;
	Person& Refp = s;
	//註意不能將將基類對象給派生類對象
	//s = p;
	//允許將基類指針賦值給派生類指針,但是需要強制轉換
	Student* sPtr = (Student*)Pptr;
	return 0;
}

【註意】

1、不允許基類對象賦值給派生類對象

2、允許基類指針賦值給派生類指針, 但是需要強制轉化。這種轉化雖然可以,但是會存在越界訪問的問題。

繼承中的作用域

基類和派生類都有獨立的作用域。繼承下來的基類成員在一個作用域,派生類的成員在另一作用域。

//以下代碼的運行結果是什麼?
class Person
{
protected:
	string _name = "楊XX"; // 姓名
	int _num = 12138; // 身份證號
};
class Student : public Person
{
public:
	void Print()
	{
		cout <<_num << endl;
	}
protected:
	int _num = 52622; // 學號
};
void Test()
{
	Student s1;
	s1.Print();
};

基類中有一個_num 給瞭缺省值“12138”, 派生類中也有一個_name,給瞭缺省值“52622”,那麼在派生類裡直接使用_name,使用的具體是哪一個類裡的?

使用的是派生類Student裡的。

總結:基類和派生類中如果有同名成員,派生類將屏蔽基類對同名成員的直接訪問,這種情況稱為隱藏 , 或者稱為重定義。

如果想要訪問,則使用基類::基類成員顯示的訪問。

class Person
{
protected:
	string _name = "楊XX"; // 姓名
	int _num = 12138; // 身份證號
};
class Student : public Person
{
public:
	void Print()
	{
		cout << "身份證號:" << Person::_num << endl;
		cout << "學號:" << _num << endl;
	}
protected:
	int _num = 52622; // 學號
};
void Test()
{
	Student s1;
	s1.Print();
};
int main(void)
{
	Test();
	return 0;
}

運行結果

我們已經瞭解瞭什麼是隱藏。那麼來看一下下面這些代碼。

//以下的兩個函數構成隱藏還是重載?
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);
}

函數重載要求在同一作用域,而被繼承下來的基類成員和派生類成員在不同的作用域,所以構成的是隱藏。

```cpp
//以下代碼的運行結果是什麼?
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();
}

因為func()函數隱藏瞭,在派生類的作用域內沒有func()函數,所以會出現編譯報錯。

派生類的默認成員函數

類有8個默認成員函數,這裡隻說重點的四個默認成員函數:構造函數、析構函數、拷貝構造函數、賦值重載函數

如果我們不寫派生類的構造函數和析構函數,編譯器會做如下的事情:

1、基類被繼承下來的部分會調用基類的默認構造函數和析構函數

2、派生類自己也會生成默認構造和析構函數,派生類自己的和普通類的處理一樣

如果我們不寫派生類的賦值構造函數和拷貝構造函數,編譯器會做如下的事情

3、基類被繼承下來的部分會調用基類的默認拷貝構造函數和賦值構造函數。

4、派生類自己也會生成默認賦值拷貝構造函數和賦值函數,和普通類的處理一樣。

什麼情況下需要自己寫?

1、父類沒有合適的默認構造函數,需要自己顯示地寫

2、如果子類有資源需要釋放,就需要自己顯示地寫析構函數

3、如果子類存在淺拷貝的問題,就需要自己實現拷貝構造和賦值函數解決淺拷貝的問題。

如果需要自己寫派生類的這幾個重點成員函數,那麼該如何寫?

//如果需要自己實現派生類的幾個四個重點默認成員函數,需要如何實現?該註意什麼?
class Person
{
public:
	Person(const char* name)
		:_name(name)
	{
		cout << "Person(const char* name)" << 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
{
protected:
	int _id; //學號
	int* _ptr = new int[10]; //給一個需要自己實現默認成員函數場景用以舉例
};

1、實現派生類的構造函數:需要調用基類的構造函數初始化被繼承下來的基類部分的成員。如果基類沒有合適的默認構造函數,就需要在實現派生類構造函數的初始化列表階段顯示調用。

2、實現派生類的析構函數:派生類的析構函數會在被調用完成後自動調用基類的析構函數清理被繼承下來的基類成員。這樣可以保證派生類自己的成員的清理先於被繼承下來的基類成員。ps:析構函數名字會被統一處理成destructor(),所以被繼承下來的基類的析構函數和派生類的析構函數構成隱藏。

3、實現派生類的拷貝構造函數:需要調用基類的拷貝構造函數完成被繼承下來的基類成員的拷貝初始化。

4、實現派生類的operator=:需要調用基類的operator=完成被繼承下來的基類成員的賦值。

5、派生類對象初始化先調用基類構造再調用派生類構造。

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; //學號
};

菱形繼承

繼承可分為單繼承和多繼承。

單繼承:一個派生類隻有一個直接基類

多繼承:一個派生類有兩個或兩個以上的直接基類。

而多繼承中又存在著一種特殊的繼承關系,菱形繼承

它們之間的繼承關系邏輯上就類似一個菱形,所以稱為菱形繼承。菱形繼承相對於其他繼承關系是復雜的。

B中有一份A的成員,C中也有一份A的成員,D將B和C都繼承瞭,那麼D中被繼承下來的A的成員不就有兩份瞭嗎?不難看出,菱形繼承有數據冗餘和二義性的問題。

class Person
{
public:
	string _name; // 姓名
};
class Student : public Person
{
public:
	int _num; //學號
};
class Teacher : public Person
{
public:
	int _id; // 職工編號
};
class Assistant : public Student, public Teacher
{
public:
	string _majorCourse; // 主修課程
};
int main()
{
	// 二義性、數據冗餘
	Assistant a;
	a._id = 1;
	a._num = 2;
	// 這樣會有二義性無法明確知道訪問的是哪一個
	a._name = "peter";
	return 0;
}

上面的繼承關系如下:

此時Assitant中有兩份_name.存在數據冗餘和二義性的問題。

二義性的問題是比較好解決的,使用::指定就可以瞭,但是並不能解決數據冗餘的問題。

int main()
{
	// 二義性、數據冗餘
	Assistant a;
	a._id = 1;
	a._num = 2;
	a.Student::_name = "小張";
	a.Teacher::_name = "張老師";
	return 0;
}

虛擬繼承可以解決繼承的數據冗餘和二義性的問題。如上面所畫的邏輯繼承關系。在開始可能產生數據冗餘和二義性的地方使用虛擬繼承,即可解決,但是在其他地方不要去使用虛擬繼承。

虛擬繼承格式

虛擬繼承解決數據冗餘和二義性的原理

為瞭更好地研究,在這裡給出一個比較簡單的菱形繼承體系

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的數據可以看出數據的冗餘。

現在增加虛擬繼承機制,解決數據冗餘和二義性。

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;
}

再次調式調用內存窗口,會發現

和沒有采用虛擬繼承的內存窗口有較大的變化。

B中的地址0x00677bdc裡有什麼?C中的地址0x00677be4裡有什麼?

從內存窗口可看出,菱形虛擬繼承,內存中隻在對象組成的最高處地址保存瞭一份A,A是B、C公共的。而B和C裡分別保存瞭一個指針,該指針指向一張表。這張表稱為虛基表,而指向虛基表的指針稱虛基指針。虛基表中保存的值,是到A地址的偏移量,通過這個偏移量就能夠找到A瞭。

繼承和組合的區分與聯系

在沒有學習繼承之前,我們其實頻繁地使用組合。

class head
{
private:
	int _eye;
	int _ear;
	int _mouth;
};
class hand
{
private:
	int _arm;
	int _fingers;
};
class Person
{
	//組合
	//一個人由手、頭等組合
	hand _a;
	head _b;
};
  • 繼承是一種is-a的關系, 每一個派生類是基類,例如,Student是一個Person, Teacher 是一個Person
  • 組合是一種has-a的關系,Person組合瞭head, hand, 每一個Person對象中都有一個head、hand對象。
  • 如果某種情況既可以使用繼承又可以使用組合,那麼優先使用對象組合,而不是類繼承。

其餘註意事項

  • 友元關系不能被繼承,好比父親的朋友不一定是你的朋友。
  • 如果基類中定義瞭靜態成員,當這個基類被實例化後出現瞭一份,那麼整個繼承體系中都隻有這一份實例。

到此這篇關於C++繼承與菱形繼承詳細介紹的文章就介紹到這瞭,更多相關C++繼承 內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: