C++進(jìn)階繼承-創(chuàng)新互聯(lián)

作者:@小萌新
專欄:@C++進(jìn)階
作者簡介:大二學(xué)生 希望能和大家一起進(jìn)步!
本篇博客簡介:簡單介紹C++中繼承的概念

成都創(chuàng)新互聯(lián)堅(jiān)持“要么做到,要么別承諾”的工作理念,服務(wù)領(lǐng)域包括:做網(wǎng)站、成都做網(wǎng)站、企業(yè)官網(wǎng)、英文網(wǎng)站、手機(jī)端網(wǎng)站、網(wǎng)站推廣等服務(wù),滿足客戶于互聯(lián)網(wǎng)時代的太平網(wǎng)站設(shè)計(jì)、移動媒體設(shè)計(jì)的需求,幫助企業(yè)找到有效的互聯(lián)網(wǎng)解決方案。努力成為您成熟可靠的網(wǎng)絡(luò)建設(shè)合作伙伴!

繼承
  • 繼承的概念及定義
    • 繼承的概念
  • 繼承的定義
    • 定義格式
    • 繼承方式和訪問限定符
    • 默認(rèn)繼承方式
  • 基類和派生類的賦值轉(zhuǎn)換
  • 繼承中的作用域
  • 派生類的默認(rèn)成員函數(shù)
  • 繼承與友元
  • 繼承與靜態(tài)成員
  • 繼承方式
    • 單繼承
    • 多繼承
    • 菱形繼承
    • 菱形的虛擬繼承
    • 菱形虛擬繼承的原理
    • 繼承與組合
  • 筆試面試題
  • 總結(jié)

繼承的概念及定義 繼承的概念

繼承是一種面向?qū)ο缶幊痰母拍?,它指的是一個類(稱為子類)可以從另一個類(稱為父類)中繼承屬性和方法。這意味著子類可以獲得父類中定義的所有屬性和方法,并且可以在不改變父類代碼的情況下擴(kuò)展或修改這些屬性和方法。

那么這么做的優(yōu)點(diǎn)是什么呢?

很顯然的一點(diǎn) 可以增強(qiáng)代碼的復(fù)用性 減少冗余代碼

用代碼來舉個例子

class person
{public:
	void Print()
	{cout<< "name : "<< _name<< endl;
		cout<< "age : "<< _age<< endl;
	}
protected:
	string _name = "zhangsan";
	int _age = 18;
};

// 學(xué)生類
class student :public person
{public:
private:
	int _stuid;
};
// 教師類
class teacher :public person
{public:
private:
	int _jobid;
};

從而達(dá)到一個這樣子的效果

在這里插入圖片描述
繼承之后父類的所有成員 包括成員變量和方法 都會成為子類的一部分

繼承的定義 定義格式

繼承的定義方式如下
在這里插入圖片描述

繼承方式和訪問限定符

我們都知道 訪問限定符有三種

  1. public訪問
  2. protected訪問
  3. private訪問

繼承的方式也有三種

  1. public繼承
  2. protected繼承
  3. private繼承

基類當(dāng)中被不同訪問限定符修飾的成員,以不同的繼承方式繼承到派生類當(dāng)中后,該成員最終在派生類當(dāng)中的訪問方式將會發(fā)生變化。

如下圖

在這里插入圖片描述
實(shí)際上稍作觀察之后我們就能發(fā)現(xiàn)

在子類中的訪問訪問方式遵循以下規(guī)則

  1. 當(dāng)父類的訪問方式為protected或者public時 在子類中它會變成繼承方式和父類訪問方式中權(quán)限更小的值

什么意思呢? 比如說父類的訪問方式是public 子類使用protected繼承 那么它在子類中的訪問方式就變成protected了

如果父類的訪問方式是protected 子類使用public繼承 那么它在子類中的訪問方式還是protected

  1. 當(dāng)父類的訪問方式是privated時 不管子類使用何種繼承方式 都是不可見的

那么不可見又是什么意思呢?

我們寫出下面的一段代碼

class person
{public:
	void Print()
	{cout<< "name : "<< _name<< endl;
		cout<< "age : "<< _age<< endl;
	}
protected:
	string _name = "zhangsan";
	int _age = 18;
private:
	string _add = "chenghuadadao";
};

// 學(xué)生類
class student :public person
{public:
	void testerr()
	{cout<< this->_add<< endl;
	}

我們可以發(fā)現(xiàn) 在學(xué)生類中 我們是無法訪問父類中的_add的

在這里插入圖片描述
事實(shí)上這里的編譯器也直接給了我們紅線報錯

這里其實(shí)也從側(cè)面說明了protected訪問限定符為什么會出現(xiàn)

它的作用就是為了不想讓類外部訪問 而想讓子類訪問

但是 我們在實(shí)際寫代碼的過程中一般都是用public繼承

這也是C++被人詬病的語法缺點(diǎn)之一 后續(xù)的python語言甚

至都沒有繼承方式這一說了

默認(rèn)繼承方式

這里我們不推薦使用默認(rèn)繼承方式 所以也就不多講了

我們只需要知道兩點(diǎn)

class的默認(rèn)繼承方式是 private

struct的默認(rèn)繼承方式是 public

基類和派生類的賦值轉(zhuǎn)換

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

在這個過程當(dāng)中會發(fā)生基類和派生類對象之間的賦值轉(zhuǎn)換

我們來看代碼

class person
{public:
	string _name;
	string _sex;
	int age;
};

class student : public person
{private:
	int _stuid;
};

像上面的代碼 我們寫下下面這些操作全部是合法的

student s;
	person p = s;
	person* ptr = &s;
	person& ref = s;

對于我們上面的操作 C++中給了一個比較專業(yè)的名詞叫做切片

意思就是將子類中繼承基類的那部分切出來 賦值給基類

對象賦值
在這里插入圖片描述
指針賦值

在這里插入圖片描述
引用賦值

在這里插入圖片描述

那么這個時候我們再想一下 基類對象能否賦值給子類呢

在這里插入圖片描述
我們寫出上面的代碼 結(jié)果發(fā)現(xiàn)報錯了

其實(shí)想想也能明白 基類相對于子類來說會少一些東西

所以肯定是不能切片賦值的

但是子類的指針和引用可以通過強(qiáng)制類型轉(zhuǎn)換的方式來賦值

代碼和顯示效果如下

student* ptrs = (student *)&p;
	student& refs = (student&)p;

在這里插入圖片描述
同樣的 我們知道有這個方式存在就好 不建議使用!

繼承中的作用域

接下來我們要學(xué)習(xí)的是C++繼承中的又一大缺陷(bushi) 特性之一

還是一樣 我們先來看代碼

//父類
class Person
{protected:
	int _num = 111;
};
//子類
class Student : public Person
{public:
	void fun()
	{cout<< _num<< endl;
	}
protected:
	int _num = 999;
};
int main()
{Student s;
	s.fun(); 
	return 0;
}

我們?nèi)绻蒙仙厦娴囊欢未a 并且調(diào)用fun函數(shù)的話 由于函數(shù)的局部性原理 我們會得到子類中的num值 如下圖

在這里插入圖片描述

這個時候如果我們想要訪問父類中的num就需要使用域操作符

void fun()
	{cout<< Person::_num<< endl;
	}

在這里插入圖片描述

在繼承體系中的基類和派生類都有獨(dú)立的作用域。若子類和父類中有同名成員,子類成員將屏蔽父類對同名成員的直接訪問,這種情況叫隱藏,也叫重定義。

需要注意的是,如果是成員函數(shù)的隱藏,只需要函數(shù)名相同就構(gòu)成隱藏。

還是拿上面的代碼距離 我們給父類中加上一個fun函數(shù)

class Person
{public:
	void fun()
	{cout<< _num<< endl;
	}

像這樣 如果我們要調(diào)用父類里面的person函數(shù)只能加上一個域操作符 如下圖

在這里插入圖片描述

特別的 這兩個函數(shù)不構(gòu)成函數(shù)重載 因?yàn)闃?gòu)成函數(shù)重載的兩個函數(shù)一定要在同一作用域

我們在真正寫代碼的時候應(yīng)該避免重名的問題

派生類的默認(rèn)成員函數(shù)

在這里插入圖片描述

我們都知道 類有六大默認(rèn)成員函數(shù)

下面我們看看派生類當(dāng)中的默認(rèn)成員函數(shù),與普通類的默認(rèn)成員函數(shù)的不同之處。

其實(shí)這里只要記住一點(diǎn)就好

凡是與基類相關(guān)的部分 都要調(diào)用基類的相關(guān)函數(shù)

接下來我們來看代碼

這是基類person的代碼

//基類
class Person
{public:
	//構(gòu)造函數(shù)
	Person(const string& name = "peter")
		:_name(name)
	{cout<< "Person()"<< endl;
	}
	//拷貝構(gòu)造函數(shù)
	Person(const Person& p)
		:_name(p._name)
	{cout<< "Person(const Person& p)"<< endl;
	}
	//賦值運(yùn)算符重載函數(shù)
	Person& operator=(const Person& p)
	{cout<< "Person& operator=(const Person& p)"<< endl;
		if (this != &p)
		{	_name = p._name;
		}
		return *this;
	}
	//析構(gòu)函數(shù)
	~Person()
	{cout<< "~Person()"<< endl;
	}
private:
	string _name; //姓名
};

這是子類student的代碼

//派生類
class Student : public Person
{public:
	//構(gòu)造函數(shù)
	Student(const string& name, int id)
		:Person(name) //調(diào)用基類的構(gòu)造函數(shù)初始化基類的那一部分成員
		, _id(id) //初始化派生類的成員
	{cout<< "Student()"<< endl;
	}
	//拷貝構(gòu)造函數(shù)
	Student(const Student& s)
		:Person(s) //調(diào)用基類的拷貝構(gòu)造函數(shù)完成基類成員的拷貝構(gòu)造
		, _id(s._id) //拷貝構(gòu)造派生類的成員
	{cout<< "Student(const Student& s)"<< endl;
	}
	//賦值運(yùn)算符重載函數(shù)
	Student& operator=(const Student& s)
	{cout<< "Student& operator=(const Student& s)"<< endl;
		if (this != &s)
		{	Person::operator=(s); //調(diào)用基類的operator=完成基類成員的賦值
			_id = s._id; //完成派生類成員的賦值
		}
		return *this;
	}
	//析構(gòu)函數(shù)
	~Student()
	{cout<< "~Student()"<< endl;
		//派生類的析構(gòu)函數(shù)會在被調(diào)用完成后自動調(diào)用基類的析構(gòu)函數(shù)
	}
private:
	int _id; //學(xué)號
};

是不是發(fā)現(xiàn)完全符合我們上面的推論

這里有兩個特殊結(jié)論

派生類對象初始化時,會先調(diào)用基類的構(gòu)造函數(shù)再調(diào)用派生類的構(gòu)造函數(shù)。
派生類對象在析構(gòu)時,會先調(diào)用派生類的析構(gòu)函數(shù)再調(diào)用基類的析構(gòu)函數(shù)。

所以說我們在派生類的析構(gòu)函數(shù)中不需要在顯示調(diào)用基類的析構(gòu)函數(shù)了

在這里插入圖片描述
我們可以發(fā)現(xiàn) 假設(shè)我們顯示的調(diào)用基類的析構(gòu)函數(shù)的話 就會析構(gòu)兩次

這在一些情況(開辟釋放內(nèi)存)下會發(fā)生錯誤

此外還有以下三個注意點(diǎn)

  1. 派生類和基類的賦值運(yùn)算符重載函數(shù)因?yàn)楹瘮?shù)名相同構(gòu)成隱藏,因此在派生類當(dāng)中調(diào)用基類的賦值運(yùn)算符重載函數(shù)時,需要使用作用域限定符進(jìn)行指定調(diào)用。
  2. 由于多態(tài)的某些原因,任何類的析構(gòu)函數(shù)名都會被統(tǒng)一處理為destructor();。因此,派生類和基類的析構(gòu)函數(shù)也會因?yàn)楹瘮?shù)名相同構(gòu)成隱藏,若是我們需要在某處調(diào)用基類的析構(gòu)函數(shù),那么就要使用作用域限定符進(jìn)行指定調(diào)用。
  3. 在派生類的拷貝構(gòu)造函數(shù)和operator=當(dāng)中調(diào)用基類的拷貝構(gòu)造函數(shù)和operator=的傳參方式是一個切片行為,都是將派生類對象直接賦值給基類的引用。
繼承與友元

這里還是記住一點(diǎn)就好 友元關(guān)系不可以繼承

比如說

class Student;
class Person
{public:
 friend void Display(const Person& p, const Student& s);
protected:
 string _name; // 姓名
};
class Student : public Person
{protected:
 int _stuNum; // 學(xué)號
};
void Display(const Person& p, const Student& s)
{cout<< p._name<< endl;
 cout<< s._stuNum<< endl;
}
void main()
{Person p;
 Student s;
 Display(p, s);
}

我們這里Display函數(shù)不能訪問student
在這里插入圖片描述

要想訪問的話必須要在student中也聲明友元

class Student : public Person
{public:
	//聲明Display是Student的友元
	friend void Display(const Person& p, const Student& s);
protected:
	int _id; //學(xué)號
};

這個時候我們就可以使用dis函數(shù)訪問它們的內(nèi)容了

在這里插入圖片描述

繼承與靜態(tài)成員

我們都知道 靜態(tài)變量是儲存在靜態(tài)區(qū)里面的 那么不管我們派生出多少個子類 實(shí)際上它們的靜態(tài)變量儲存地址都是一樣的

我們可以使用下面的代碼來證明

class person
{public:
	static int _count ;
};

int person::_count = 0;
class student :public person
{public:
	void test()
	{_count++;
	}
};

我們試著調(diào)用子類中的函數(shù)讓_count++

之后打印子類和父類中的_count看看是不是都是1

在這里插入圖片描述

繼承方式 單繼承

單繼承:一個子類只有一個直接父類時稱這個繼承關(guān)系為單繼承。

比如下面這種方式

在這里插入圖片描述

多繼承

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

比如說下面這樣子

在這里插入圖片描述

菱形繼承

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

在這里插入圖片描述
從上面的圖我們不難看出 菱形繼承一定存在著數(shù)據(jù)冗余和二義性的問題

比如說我們來看下面的代碼

class Person
{public:
	string _name; //姓名
};
class Student : public Person
{protected:
	int _num; //學(xué)號
};
class Teacher : public Person
{protected:
	int _id; //職工編號
};
class Assistant : public Student, public Teacher
{protected:
	string _majorCourse; //主修課程
};
int main()
{Assistant a;
	a._name = "peter"; //二義性:無法明確知道要訪問哪一個_name
	return 0;
}

我們運(yùn)行上面的代碼之后就會發(fā)生一個這樣子的問題
在這里插入圖片描述
對于二義性問題 我們可以通過指定作用域來解決

Assistant a;
	a.Student::_name = "peter"; 
	a.Teacher::_name = "peter2";

在這里插入圖片描述
但是數(shù)據(jù)冗余的問題缺一定解決不了 如下圖

在這里插入圖片描述
這個時候就輪到我們的虛繼承出場了

菱形的虛擬繼承

在這里插入圖片描述
這種繼承方式就是為了專門解決菱形繼承的二義性還有數(shù)據(jù)冗余問題被發(fā)明出來的

我們只需要在上面的繼承方式前加上這段代碼

class Person
{public:
	string _name; //姓名
};
class Student : virtual public Person //虛擬繼承
{protected:
	int _num; //學(xué)號
};
class Teacher : virtual public Person //虛擬繼承
{protected:
	int _id; //職工編號
};
class Assistant : public Student, public Teacher
{protected:
	string _majorCourse; //主修課程
};
int main()
{Assistant a;
	a._name = "peter"; //無二義性
	return 0;
}

在這里插入圖片描述
這個時候我們便發(fā)現(xiàn)不會報錯了

此時 不管你是訪問teacher還是student中的name 都是訪問的一個地址

在這里插入圖片描述

菱形虛擬繼承的原理

我們首先看看 如果我們不適用菱形虛擬繼承 那么這幾個類在內(nèi)存中會是怎么樣分布的

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

接下來我們打開內(nèi)存窗口

在這里插入圖片描述
我們可以發(fā)現(xiàn) 它在內(nèi)存中的分布是這樣子的
(重點(diǎn)觀察_a 兩個a的地址不一樣)

在這里插入圖片描述
接下來我們看看虛繼承會是什么樣子的

#includeusing namespace std;
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;
}

還是一樣 我們來打開內(nèi)存窗口

在這里插入圖片描述

這個時候我們可以看到 _a被存放到了最后 而一開始存放_a的兩個數(shù)據(jù)則被兩個奇怪的數(shù)據(jù)代替了

實(shí)際上呢 這兩個奇怪的數(shù)據(jù)就是指針

它們被叫做虛基表指針

分別指向一個虛基表 而我們通過虛基表則能夠找到_a的地址

如下圖

在這里插入圖片描述

繼承與組合

很多人都說C++語法復(fù)雜,其實(shí)多繼承就是一個體現(xiàn)。有了多繼承,就可能存在菱形繼承,有了菱形繼承就有菱形虛擬繼承,底層實(shí)現(xiàn)就很復(fù)雜。所以一般不建議設(shè)計(jì)出菱形繼承,否則代碼在復(fù)雜度及性能上都容易出現(xiàn)問題,當(dāng)菱形繼承出問題時難以分析,并且會有一定的效率影響。

那么這個時候我們的組合就出現(xiàn)了

繼承和組合的區(qū)別有什么呢?

繼承是一種is-a的關(guān)系,也就是說每個派生類對象都是一個基類對象;而組合是一種has-a的關(guān)系,若是B組合了A,那么每個B對象中都有一個A對象。

比如說我們看下面的代碼

車和保時捷就是一種is a的關(guān)系 我們可以說 保時捷是一輛車

所以說這種情況使用繼承好一點(diǎn)

class  Car
{protected:
	string _colour; //顏色
	string _num; //車牌號
};
class Porsche : public Car
{;
};

但是呢 像車輪胎和保時捷 就是一種has a的關(guān)系了

我們只能說保時捷有車輪胎 不能說保時捷是車輪胎

class Tire
{protected:
	string _brand; //品牌
	size_t _size; //尺寸
};
class Car
{protected:
	string _colour; //顏色
	string _num; //車牌號
	Tire _t; // 這里就使用了一個包含關(guān)系
};

若是兩個類之間既可以看作is-a的關(guān)系,又可以看作has-a的關(guān)系,則優(yōu)先使用組合。

  1. 繼承允許你根據(jù)基類的實(shí)現(xiàn)來定義派生類的實(shí)現(xiàn),這種通過生成派生類的復(fù)用通常被稱為白箱復(fù)用(White-box
    reuse)。術(shù)語“白箱”是相對可視性而言:在繼承方式中,基類的內(nèi)部細(xì)節(jié)對于派生類可見,繼承一定程度破壞了基類的封裝,基類的改變對派生類有很大的影響,派生類和基類間的依賴性關(guān)系很強(qiáng),耦合度高。
  2. 組合是類繼承之外的另一種復(fù)用選擇,新的更復(fù)雜的功能可以通過組裝或組合對象來獲得。對象組合要求被組合的對象具有良好定義的接口,這種復(fù)用風(fēng)格被稱之為黑箱復(fù)用(Black-box reuse),因?yàn)閷ο蟮膬?nèi)部細(xì)節(jié)是不可見的,對象只以“黑箱”的形式出現(xiàn),組合類之間沒有很強(qiáng)的依賴關(guān)系,耦合度低,優(yōu)先使用對象組合有助于你保持每個類被封裝。
  3. 實(shí)際中盡量多使用組合,組合的耦合度低,代碼維護(hù)性好。不過繼承也是有用武之地的,有些關(guān)系就適合用繼承,另外要實(shí)現(xiàn)多態(tài)也必須要繼承。若是類之間的關(guān)系既可以用繼承,又可以用組合,則優(yōu)先使用組合。
筆試面試題
  1. 什么是菱形繼承?菱形繼承的問題是什么?

菱形繼承是多繼承的一種特殊情況,兩個子類繼承同一個父類,而又有子類同時繼承這兩個子類,我們稱這種繼承為菱形繼承。
菱形繼承因?yàn)樽宇悓ο螽?dāng)中會有兩份父類的成員,因此會導(dǎo)致數(shù)據(jù)冗余和二義性的問題。

  1. 什么是菱形虛擬繼承?如何解決數(shù)據(jù)冗余和二義性?
    在這里插入圖片描述

菱形虛擬繼承是指在菱形繼承的腰部使用虛擬繼承(virtual)的繼承方式,菱形虛擬繼承對于D類對象當(dāng)中重復(fù)的A類成員只存儲一份,然后采用虛基表指針和虛基表使得D類對象當(dāng)中繼承的B類和C類可以找到自己繼承的A類成員,從而解決了數(shù)據(jù)冗余和二義性的問題。

  1. 繼承和組合的區(qū)別?什么時候用繼承?什么時候用組合?

1 繼承是一種is-a的關(guān)系,而組合是一種has-a的關(guān)系。如果兩個類之間是is-a的關(guān)系,使用繼承;
2 如果兩個類之間是has-a的關(guān)系,則使用組合;
3 如果兩個類之間的關(guān)系既可以看作is-a的關(guān)系,又可以看作has-a的關(guān)系,則優(yōu)先使用組合。

總結(jié)

在這里插入圖片描述
本篇博客主要介紹了C++繼承的一些相關(guān)知識

你是否還在尋找穩(wěn)定的海外服務(wù)器提供商?創(chuàng)新互聯(lián)www.cdcxhl.cn海外機(jī)房具備T級流量清洗系統(tǒng)配攻擊溯源,準(zhǔn)確流量調(diào)度確保服務(wù)器高可用性,企業(yè)級服務(wù)器適合批量采購,新人活動首月15元起,快前往官網(wǎng)查看詳情吧

文章名稱:C++進(jìn)階繼承-創(chuàng)新互聯(lián)
瀏覽路徑:http://muchs.cn/article14/csphge.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供關(guān)鍵詞優(yōu)化網(wǎng)站營銷、用戶體驗(yàn)、自適應(yīng)網(wǎng)站外貿(mào)建站、移動網(wǎng)站建設(shè)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請盡快告知,我們將會在第一時間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

網(wǎng)站優(yōu)化排名