【C++自學筆記】詳細解讀——C++面向對象之繼承(內含詳細理解菱形繼承和菱形虛擬繼承)

一、繼承的概念及定義

1、繼承的概念

繼承機制是面向對向程序涉及使代碼可以複用的最重要的手段,它允許程序猿在保持原有類特性的基礎上進行了擴展,增加功能,產生新的類,稱爲派生類(子類)。

繼承呈現了面向對象程序設計的層次結構,體現了由簡單到複雜的認知過程,繼承是類設計層次的複用。

2、繼承的定義

先看一個例子:

class Person {
public:
	void Print() {
		cout << "name:" << _name << endl;
		cout << "age:" << _age << endl;
	}
protected:
	//C++11特性:支持定義時進行初始化
	string _name = "Peter";
	int _age = 18;
};
class Student :public Person {
protected:
	int _stuid;
	int _major;
};

C++中繼承的定義格式爲:

其中繼承方式和訪問限定符遵循如下規定:

3、三種繼承方式:

public繼承:

  1. 派生類(子類)繼承基類(父類)的數據;
  2. 共有的繼承方式,可以訪問父類的共有成員(public)與保護成員(protected),但無法訪問私有成員(private);
  3. 基類中的public在派生類中也是public;
  4. 基類中的protected在派生類中也是protected;
  5. 基類中的私有成員,仍然歸父類所有,雖然子類繼承了,但是無法進行訪問(可以調用父類的共有方法來訪問)

protected繼承:

  1. 保護的繼承方式;
  2. 基類中的public到派生類中稱爲protected;
  3. 基類中的保護成員訪問權限不變,依舊是protected;
  4. 基類中的私有成員也是一樣,被繼承了,但是用不了;

private繼承:

  1. 私有的繼承方式;
  2. 在基類中所有的成員變量權限均變爲私有;

注意點:

  • 基類(父類)private 成員在派生類(子類)中無論以什麼方式繼承都是不可見的;(實際上被繼承了,但是語法限制無法訪問)
  • 保護成員限定符是因爲繼承纔出現的;
  • 基類的其他成員在子類中的訪問方式總是取最小的(public>protected>private);
  • 使用關鍵字 class 默認的繼承方式是 private,但是使用 struct 時默認的訪問方式爲 public;
  • 在實際運用的過程中一般都是 public 繼承,很少使用 protected/private 繼承;

二、基類和派生類對象賦值轉換

1、派生類對象何以賦值給 基類的對象/基類的指針/基類的引用 ;(切片/切割:把派生類中父類的那部分切來賦值)

class Preson {
protected:
	string _name;
	string _sex;
	int _age;
};
class Student :public Preson {
public:
	int _No;
};

void Test() {
	Student s;
	//子類對象賦值給父類對象/指針/引用
	Preson p = s;
	Preson* pp = &s;
	Preson& rp = s;

}

2、基類對象不能賦值給派生類對象;

3、基類的指針可以通過強制類型轉換賦值給派生類的指針,但是必須是基類的指針是指向派生類對象時纔是安全的;

void Test() {
	Student s;
	//子類對象賦值給父類對象/指針/引用
	Preson p = s;
	Preson* pp = &s;
	Preson& rp = s;

	//基類對象不能賦值給派生類對象
	//s = p;

	//基類的指針可以通過強制類型轉換賦值給派生類的指針
	pp = &s;
	Student* ps1 = (Student*)pp;
	ps1->_No = 10;

	pp = &p;
	Student* ps2 = (Student*)pp;//可能會存在越界訪問的問題
	ps2->_No = 10;
}

三、繼承中的作用域

在繼承體系中基類和派生類都有獨立的作用域;

如果父類和子類中由同名成員,子類成員將會屏蔽父類對同名成員的直接訪問,這種現象叫做隱藏,也叫重定義;(在子類成員函數中,可以使用 基類::基類成員 顯示訪問);

如果時成員函數的隱藏,只需要函數名相同就構成隱藏;

在實際運用過程中,繼承體系裏面最好不要定義同名的成員;

class Person {
protected:
	string _name = "xiaoxiao";//Name
	int _num = 111; //PersonID
};
class Student :public Person {
public:
	void Print() {
		cout << "Name = " << _name << endl;
		cout << "PersonID = " << _num << endl;
		cout << "StudentID = " << _num << endl;
	}
protected:
	int _num = 999; //StudentID
};
void Test() {
	Student s1;
	s1.Print();
}

那麼如果存在同名函數呢?

class A {
public:
	void fun() {
		cout << "func" << endl;
	}
};
class B :public A {
public:
	void fun(int i) {
		fun();
		cout << "fun(int i)-->" << i << endl;
	}
};
void Test1() {
	B b;
	b.fun(10);
}

這個代碼會報錯:

需要在B類fun中調用的fun前面叫上作用域限定符;

==>>

B中的fun和A中的fun並不是構成重載!!因爲兩者不在同一個作用域!

B中的fun和A中的fun構成隱藏,成員函數滿足函數名相同就構成隱藏;

四、派生類的默認成員函數

類中的6個默認成員函數,“默認”就意味着,就算我們不寫,編譯器也會自動生成,爲了詳細的理解,我們先上一段代碼:
 


class Preson {
public:
	Preson(const char* name = "peter")
		:_name(name)
	{
		cout << "Preson()" << endl;
	}
	Preson(const Preson& p) 
		:_name(p._name)
	{
		cout << "Preson(const Preson& p)" << endl;
	}
	Preson& operator=(const Preson& p) {
		cout << "Preson& operator=(const Preson& p)" << endl;
		if (this != &p) {
			_name = p._name;
		}
		return *this;
	}
	~Preson() {
		cout << "~Preson()" << endl;
	}
protected:
	string _name;
};
class Student :public Preson {
public:
	Student(const char* name, int num)
		:Preson(name)
		, _num(num)
	{
		cout << "Studetn()" << endl;
	}
	Student(const Student& s)
		:Preson(s)
		, _num(s._num)
	{
		cout << "Student(const Studetn& s)" << endl;
	}
	Student& operator=(const Student& s) {
		cout << "Student& operator=(const Student& s)" << endl;
		if (this != &s) {
			Preson::operator = (s);
			_num = s._num;
		}
		return *this;
	}
	~Student() {
		cout << "~Student()" << endl;
	}
protected: 
	int _num;
};
void Test() {
	Student s1("jack",18);
	Student s2(s1);
	Student s3("rose", 17);
	s1 = s3;
}

運行結果:

通過上面的代碼,可以得出結論:派生類的默認成員函數只是負責了自己的成員,繼承所來的基類的成員還是需要調用基類中的成員函數進行初始化;

總結一下:

  1. 派生類的構造函數:必須調用基類的構造函數初始化基類的那一部分成員,如果基類沒有默認的構造函數,則必須在派生類構造函數的初始化列表階段顯式調用;
  2. 派生類的拷貝構造函數:必須調用基類的拷貝構造函數完成基類的拷貝初始化;
  3. 派生類的賦值運算符重載函數:必須調用基類的opertor=完成基類的複製;
  4. 派生類的析構函數:會在被調用完成後自動調用基類的析構函數清理基類成員,因爲這樣才能保證派生類對象先清理派生類成員再清理基類成員的順序。

派生類對向初始化先調用基類構造在調用派生類構造;派生類對象析構清理先調用派生類西溝再調用基類的析構;

一道常見的面試題:實現一個無法被繼承的類

==>> (C++98)將該類的構造函數設爲 private (私有化);

==>> (C++11)再類名後面加上關鍵字 final;

五、繼承與友元

友元關係不能繼承!!!!(基類友元不能訪問子類私有和保護成員)

#if 1
//繼承與友元
class Student;
class Person {
public:
	friend void DisPlay(const Person& p, const Student& s);
protected:
	string _name;
};
class Student :public Person {
protected:
	int _stuNum;
};
void DisPlay(const Person& p,const Student& s) {
	cout << p._name << endl;
	cout << s._stuNum << endl;
}
#endif

以上代碼會報錯:

六、繼承與靜態成員

基類定義了 static 靜態成員,則整個繼承體系裏面只有一個這樣的成員!(無論派生出多好個子類,都只有一個 static 成員實例)

#if 1
//繼承與靜態成員
class Person {
public:
	Person() { ++_count; }
protected:
	string _name;
public:
	static int _count;
};
int Person::_count = 0;
class Student :public Person {
protected:
	int _num;
};
class Graduate :public Student {
protected:
	string _seminarCourse;
};
void Test(){
	Student s1;
	Student s2;
	Student s3;
	Graduate s4;
	cout << "人數" << Person::_count << endl;
	Student::_count = 0;

	cout << "人數" << Person::_count << endl;
}
#endif

七、複雜的菱形繼承及菱形虛擬繼承

1、單繼承:一個子類只有一個直接的父類時成這個繼承關係爲單繼承;

2、多繼承:一個子類有兩個或兩個以上直接父類時稱這個繼承關係爲多繼承;

3、菱形繼承:菱形繼承時多繼承的一種特殊情況;

菱形繼承的問題:從下面的對象成員構造模型構造,可以看出菱形繼承有 數據冗餘 和 二義性 的問題,在Assistant的對象成員中 Preson 成員會有兩份;

看一段代碼:

#if 1
//菱形繼承
class Person {
public:
	string _name;
};
class Student :public Person {
protected:
	int _num;
};
class Teacher :public Person {
protected:
	int _id;
};
class Assistant :public Student, public Teacher {
protected:
	string _major;
};
void Test() {
	Assistant a;
	a._name = "peter";
}
#endif

結果==

如果要解決菱形繼承的二義性問題,就必須顯示指定訪問那個父類的成員!但是數據冗餘的問題 無法解決;

void Test() {
	Assistant a;
	//a._name = "peter";
	a.Student::_name = "zhangsan";
	a.Teacher::_name = "lisi";
}

虛擬繼承可以解決領袖繼承的 二義性 和 數據冗餘 問題。如上面的繼承關係,在Student 和Teacher 的繼承 Person 時使用虛擬繼承,即可解決問題,需要注意的問題是,虛擬繼承不要在其他地方去使用。

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

虛擬繼承最主要的功能就是,使得在繼承過程中,不管派生類中有多少個共用基類,都只公用一個基類。因此在派生類中,前4個字節首先存了一個指針,這個指針指向一個類似數組的區域,裏面裝着要訪問基類對象時,該指針要偏移的偏移量。

#if 1
//探究虛擬繼承的原理
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;
};
void Test() {
	D d;
	d.B::_a = 1;
	d.C::_a = 2;

	d._b = 3;
	d._c = 4;
	d._d = 5;

	//cout << d.B::_a << endl;
	//cout << d.C::_a << endl;
	//cout << d._b << endl;
	//cout << d._c << endl;
	//cout << d._d << endl;

}
#endif

打開內存窗口(左:菱形繼承;右:菱形虛擬繼承):

1、當 d 對象被實例化完畢後:

     

 

2、執行 d.B::_a = 1後:

     

3、執行 d.C::_a = 1後:

    

4、執行完後面的語句(左邊可見 數據冗餘:A多次重複出現 ):

     

5、細論菱形虛擬繼承的內存:

被圈出的地方,實際上就是開始說過的4個字節存了一個指針,這個就是指向虛基表,通過虛基表中指定的偏移量,從而找到公共的那塊基類成員數據。在這張圖中就是地址 0x012FF704 這塊內存; 

 

結論:派生類 D 通過 B 和 C 兩個指針,指向的一張表。這兩個指針叫做虛基表指針,這兩個表叫做虛基表。虛基表中存的偏移量。通過偏移量就可以找到 A。  

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章