国产 无码 综合区,色欲AV无码国产永久播放,无码天堂亚洲国产AV,国产日韩欧美女同一区二区

【C++】繼承和多態(tài)

這篇具有很好參考價值的文章主要介紹了【C++】繼承和多態(tài)。希望對大家有所幫助。如果存在錯誤或未考慮完全的地方,請大家不吝賜教,您也可以點擊"舉報違法"按鈕提交疑問。

一、繼承

1. 繼承概念

繼承機制是面向?qū)ο蟪绦蛟O計使代碼可以復用的最重要的手段,它允許程序員在保持原有類特性的基礎上進行擴展,增加功能,這樣產(chǎn)生新的類,稱派生類/子類。繼承呈現(xiàn)了面向?qū)ο蟪绦蛟O計的層次結(jié)構(gòu),體現(xiàn)了由簡單到復雜的認知過程。以前我們接觸的復用都是函數(shù)復用,繼承是類設計層次的復用。

我們先簡單看一下繼承的使用,如以下代碼:

		class Person
		{
		public:
			void Print()
			{
				cout << "name:" << _name << endl;
				cout << "age:" << _age << endl;
			}
		
		protected:
			string _name = "Mike";
			int _age = 18;
		};
		
		
		// 繼承 Person 父類
		class Student : public Person
		{
		protected:
			string s_id;
		};
		
		// 繼承 Person 父類
		class Tercher : public Person
		{
		protected:
			string t_id;
		};

繼承后,父類的 Person 的成員(成員函數(shù)+成員變量)都會變成子類的一部分。這里體現(xiàn)出了 StudentTeacher 復用了 Person 的成員。下面我們使用監(jiān)視窗口查看 StudentTeacher 對象,可以看到變量的復用;調(diào)用 Print 可以看到成員函數(shù)的復用;如下代碼:

			int main()
			{
				Person p;
				Student s;
				Tercher t;
				p.Print();
				s.Print();
				t.Print();
				return 0;
			}

我們先觀察監(jiān)視窗口:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

我們可以看到,st 都繼承了父類的成員變量;再嘗試調(diào)用父類的成員函數(shù):

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

2. 繼承定義

(1)繼承的格式定義

繼承的定義格式如下,Person父類/基類,Student子類/派生類public繼承方式

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

(2)繼承父類成員訪問方式的變化

繼承父類成員訪問方式的變化如下表:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

其中以 public 繼承方式我們稱父類和子類是一種 is-a 關系,也就是“我是一個你”。

總結(jié):

  1. 父類 private 成員在子類中無論以什么方式繼承都是不可見的。這里的不可見是指父類的私有成員還是被繼承到了派生類對象中,但是語法上限制子類對象不管在類里面還是類外面都不能去訪問它。
  2. 父類 private 成員在子類中是不能被訪問,如果父類成員不想在類外直接被訪問,但需要在子類中能訪問,就定義為 protected??梢钥闯霰Wo成員限定符是因繼承才出現(xiàn)的。
  3. 父類的私有成員在子類都是不可見。父類的其他成員在子類的訪問方式 == min(成員在基類的訪問限定符,繼承方式),其中比較規(guī)則:public > protected > private.
  4. 使用關鍵字 class 時默認的繼承方式是 private,使用 struct 時默認的繼承方式是 public,不過最好顯示的寫出繼承方式。
  5. 在實際運用中一般使用都是 public 繼承,幾乎很少使用 protetced/private 繼承,也不提倡使用 protetced/private 繼承,因為 protetced/private 繼承下來的成員都只能在派生類的類里面使用,實際中擴展維護性不強。

下面演示 public 繼承關系下父類成員的各類型成員訪問關系的變化 :

		// Print() 函數(shù)在父類中是 pubilc 成員
		class Person
		{
		public:
			void Print()
			{
				cout << _name << endl;
			}
		protected:
			string _name = "Mike"; // 姓名
		};
		
		
		// public 繼承父類
		class Student : public Person
		{
		public:
			Student()
				:_stuid(000)
			{
				Print();
			}
		protected:
			int _stuid; // 學號
		};
		
		int main()
		{
			Student s;
			s.Print();
			return 0;
		}

如上,Print() 函數(shù)在父類中是 pubilc 成員,子類繼承方式也是 public,所以此時 Print() 在類內(nèi)類外都可訪問,如下:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

當我們將 Print() 的訪問限定符改為 protected 后,如下:

		// Print() 函數(shù)在父類中是 protected 成員
		class Person
		{
		protected:
			void Print()
			{
				cout << _name << endl;
			}
		protected:
			string _name = "Mike"; // 姓名
		};

那么此時 Print() 函數(shù)只能在子類中使用,在類外不可使用:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

3. 父類和子類對象賦值轉(zhuǎn)換

  1. 子類對象可以賦值給父類的對象 / 父類的指針 / 父類的引用。這里有個形象的說法叫切片或者切割。寓意把子類中父類那部分切來賦值過去,如下圖所示:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

  1. 父類對象不能賦值給子類對象,因為子類就是繼承父類下來的,父類有的子類也有。

如下示例:

			class Person
			{
			protected:
				string _name;
				int _age;
			};
			
			class Student : public Person
			{
			public:
				string s_id;
			};
			
			int main()
			{
				Student s;
				
				// 1.子類對象可以賦值給父類對象/指針/引用
				Person p = s;
				Person* pp = &s;
				Person& rp = s;
			
				// 2.基類的指針可以通過強制類型轉(zhuǎn)換賦值給派生類的指針
				pp = &s;
				Student* ps1 = (Student*)pp; // 這種情況轉(zhuǎn)換時可以
				ps1->s_id = 7;
			
				pp = &p;
				Student* ps2 = (Student*)pp; // 這種情況轉(zhuǎn)換時雖然可以,但是會存在越界訪問
				ps2->s_id = 3;
			
				return 0;
			}

4. 繼承中的作用域

  1. 在繼承體系中父類和子類都有獨立的作用域。
  2. 父類和子類中有同名成員,子類成員將屏蔽父類對同名成員的直接訪問,這種情況叫隱藏,也叫重定義。(在子類成員函數(shù)中,可以使用 父類::基類成員 顯示訪問)
  3. 需要注意的是如果是成員函數(shù)的隱藏,只需要函數(shù)名相同就構(gòu)成隱藏。
  4. 注意在實際中在繼承體系里面最好不要定義同名的成員。

例如以下示例:

		class A
		{
		public:
			void fun()
			{
				cout << "func()" << endl;
			}
		};
		
		class B : public A
		{
		public:
			void fun(int i)
			{
				A::fun();  // 可顯示調(diào)用 A 類的 fun 函數(shù)
				cout << "func(int i)->" << i << endl;
			}
		};
		
		int main()
		{
			B b;
			b.fun(10);  // 默認調(diào)的是 B 類的 fun 函數(shù)
			b.A::fun(); // 可顯示調(diào)用 A 類的 fun 函數(shù)
			return 0;
		}

B 中的 funA 中的 fun 不是構(gòu)成重載,因為不是在同一作用域,而是構(gòu)成隱藏,成員函數(shù)滿足函數(shù)名相同就構(gòu)成隱藏。

5. 子類的默認成員函數(shù)

6個默認成員函數(shù),“默認”的意思就是指我們不寫,編譯器會變我們自動生成一個,那么在子類中,這幾個成員函數(shù)是如何生成的呢?

  1. 子類的構(gòu)造函數(shù)必須調(diào)用父類的構(gòu)造函數(shù)初始化父類的那一部分成員。如果父類沒有默認的構(gòu)造函數(shù),則必須在子類構(gòu)造函數(shù)的初始化列表階段顯示調(diào)用。

  2. 子類的拷貝構(gòu)造函數(shù)必須調(diào)用父類的拷貝構(gòu)造完成父類的拷貝初始化。

  3. 子類的 operator= 必須要調(diào)用父類的 operator= 完成基類的復制。

  4. 子類析構(gòu)函數(shù)會在被調(diào)用完成后自動調(diào)用父類析構(gòu)函數(shù)清理父類成員。因為這樣才能保證子類對象先清理子類成員再清理父類成員的順序。

  5. 子類對象初始化先調(diào)用父類構(gòu)造再調(diào)子類構(gòu)造。

  6. 子類對象析構(gòu)清理先調(diào)用子類析構(gòu)再調(diào)父類的析構(gòu)。

  7. 因為后續(xù)一些場景析構(gòu)函數(shù)需要構(gòu)成重寫,重寫的條件之一是函數(shù)名相同(后面會講解)。那么編譯器會對析構(gòu)函數(shù)名進行特殊處理,處理成 destrutor(),所以父類析構(gòu)函數(shù)不加 virtual 的情況下,子類析構(gòu)函數(shù)和父類析構(gòu)函數(shù)構(gòu)成隱藏關系。

下面演示繼承中構(gòu)造函數(shù)和析構(gòu)函數(shù)的調(diào)用情況:

			class A
			{
			public:
				A() { cout << "A()" << endl; }
				~A() { cout << "~A()" << endl; }
			
			private:
				int _a;
				
			};
			
			
			class B : public A
			{
			public:
				B() { cout << "B()" << endl; }
				~B() { cout << "~B()" << endl; }
			};

上述代碼中,我們實例化B對象,編譯器會先調(diào)用父類的構(gòu)造,即先調(diào)A的構(gòu)造,然后再構(gòu)造子類B,析構(gòu)順序按照先子后父,例如下圖:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

6. 繼承與友元

友元關系不能繼承,也就是說父類友元不能訪問子類私有和保護成員。簡單一句話說就是,父類的友元并不是子類的友元,所以不能繼承下來。

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

父類定義了 static 靜態(tài)成員,則整個繼承體系里面只有一個這樣的成員。無論派生出多少個子類,都只有一個 static 成員實例。

8. 復雜的菱形繼承及菱形虛擬繼承

(1)繼承類型

1. 單繼承:一個子類只有一個直接父類時稱這個繼承關系為單繼承。

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

2. 多繼承:一個子類有兩個或以上直接父類時稱這個繼承關系為多繼承。

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

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

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

菱形繼承的問題:從下面的對象成員模型構(gòu)造,可以看出菱形繼承有數(shù)據(jù)冗余二義性的問題。以上圖為例,在 D 的對象中 A 成員會有兩份,如下圖:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

用代碼來說明如下代碼為四個類以以上的方式繼承:

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

我們嘗試實例化一個D類對象d,先嘗試修改d對象中的 _b 值:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

如上圖是沒有問題的,我們再嘗試修改d對象中的 _a 值:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

如上圖,可以看出編譯不通過,理由就是對 _a 的訪問不明確,就是因為存在二義性,在 d 中存有兩份 _a,編譯器不知道我們訪問的是哪一個;

但是我們可以指定訪問域,如下圖:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

因為在 B 中和 C 中各有一份 _a,所以我們可以指定 B 中的 _a,或者 C 中的 _a 進行指定操作,這樣就解決二義性問題了;但是數(shù)據(jù)冗余還沒有解決。

但是有一種方法可以既解決二義性,也解決數(shù)據(jù)冗余問題,就是虛擬繼承。虛擬繼承可以解決菱形繼承的二義性和數(shù)據(jù)冗余的問題。 如上面的繼承關系,在 BC 繼承 A 時使用虛擬繼承,即可解決問題。需要注意的是,虛擬繼承不要在其他地方去使用。

我們用代碼演示證明一下,首先我們在 BC 繼承 A 時加上 virtual 關鍵字,說明是虛擬繼承:

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

接下來我們直接實例化 D 對象,并訪問 _a,如下圖:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

如上二義性問題就解決了,那么我們看看 d 對象中 BC 中的 _a 的值 :

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

我們觀察可知,d 對象中繼承下來的類里面的 _a 都是 1,也就是說,這個 _a 在這個 d 對象中,只有一份!那就說明虛擬繼承也解決了數(shù)據(jù)冗余問題了!

(2)虛擬繼承解決數(shù)據(jù)冗余和二義性的原理

首先我們先通過調(diào)試觀察一下普通菱形繼承中,d 對象中的內(nèi)存分布,如下圖所示:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

我們可以觀察到 d 對象中有兩份 _a ,明顯的數(shù)據(jù)冗余。

接下來我們加上虛擬繼承,繼續(xù)觀察 d 對象中的內(nèi)存分布,如下圖:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

上圖是菱形虛擬繼承的內(nèi)存對象成員模型:這里可以分析出 D 對象中將 A 放到的了對象組成的最下面,這個 A 同時屬于 BC,那么 BC 如何去找到公共的 A 呢?這就和 BC 中多了兩個地址有關系了,這兩個地址是什么呢?我們可以取它們的地址到內(nèi)存窗口去觀察一下:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

這里是通過了 BC 的兩個指針,指向的一張表。這兩個指針叫虛基表指針,這兩個表叫虛基表。虛基表中存的是偏移量,通過偏移量可以找到下面的 A。例如上圖中 B 中的指針指向的虛基表中,它的偏移量是十六進制的 14 字節(jié),化作十進制就是 20 字節(jié),那么從虛基表指針開始,往下 5 個單位就是 A,每個單位 4 個字節(jié),那么就剛好是 20 個字節(jié)就能找到 A.

9. 繼承的總結(jié)

繼承和組合

  • public 繼承是一種 is-a 的關系。也就是說每個子類對象都是一個父類對象。

  • 組合是一種 has-a 的關系。假設 B 組合了 A,每個 B 對象中都有一個 A 對象。
    什么是組合呢?如下代碼就是組合,其中是 B 組合了 A

      			class A
      			{
      			public:
      				int _a;
      			};
      			
      			class B
      			{
      			public:
      				int _b;
      				A a;
      			};
    
  • 優(yōu)先使用對象組合,而不是類繼承 。

  • 繼承允許我們根據(jù)父類的實現(xiàn)來定義子類的實現(xiàn)。這種通過生成子類的復用通常被稱為白箱復用(white-box reuse)。術語 “白箱” 是相對可視性而言:在繼承方式中,父類的內(nèi)部細節(jié)對子類可見 。繼承一定程度破壞了父類的封裝,父類的改變,對子類有很大的影響。子類和父類間的依賴關系很強,耦合度高。

  • 對象組合是類繼承之外的另一種復用選擇。新的更復雜的功能可以通過組裝或組合對象來獲得。對象組合要求被組合的對象具有良好定義的接口。這種復用風格被稱為黑箱復用(black-box reuse),因為對象的內(nèi)部細節(jié)是不可見的。對象只以 “黑箱” 的形式出現(xiàn)。組合類之間沒有很強的依賴關系,耦合度低。 優(yōu)先使用對象組合有助于我們保持每個類被封裝。

  • 實際盡量多去用組合。組合的耦合度低,代碼維護性好。 不過繼承也有用武之地的,有些關系就適合繼承那就用繼承,另外要實現(xiàn)多態(tài),也必須要繼承。類之間的關系可以用繼承,可以用組合,就用組合。

二、多態(tài)

1. 多態(tài)的概念

多態(tài)的概念: 通俗來說,就是多種形態(tài),具體點就是去完成某個行為,當不同的對象去完成時會產(chǎn)生出不同的狀態(tài)。

比如買票這個行為,當普通人買票時,是全價買票;學生買票時,是半價買票;軍人買票時是優(yōu)先買票。

2. 多態(tài)的定義及實現(xiàn)

多態(tài)是在不同繼承關系的類對象,去調(diào)用同一函數(shù),產(chǎn)生了不同的行為。比如 Student 繼承了 Person。Person 對象買票全價,Student 對象買票半價。

(1)多態(tài)的構(gòu)成條件

那么在繼承中要構(gòu)成多態(tài)還有兩個條件:

  1. 必須通過父類的指針或者引用調(diào)用虛函數(shù);
  2. 被調(diào)用的函數(shù)必須是虛函數(shù),且子類必須對父類的虛函數(shù)進行重寫;

我們先簡單看一下多態(tài)的使用,如以下代碼:

			class Person
			{
			// 讓 BuyTicket 成為虛函數(shù) 
			public:
				virtual void BuyTicket()
				{
					cout << "買票-全價" << endl;
				}
			};
			
			class Student : public Person
			{
			// 對 BuyTicket 進行重寫
			public:
				virtual void BuyTicket()
				{
					cout << "買票-半價" << endl;
				}
			};
			
			// 父類的指針或者引用調(diào)用
			//void func(Person& p)
			void func(Person* pa)
			{
				pa->BuyTicket();
			}

下面開始多態(tài)的調(diào)用,如下結(jié)果:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

如上就是多態(tài)的簡單使用,下面開始詳細介紹多態(tài)的條件。

(2)虛函數(shù)

虛函數(shù): 即被 virtual 修飾的類成員函數(shù)稱為虛函數(shù)。

如下代碼中的 BuyTicket 函數(shù):

			class Person
			{
			public:
				virtual void BuyTicket()
				{
					cout << "買票-全價" << endl;
				}
			};

(3)虛函數(shù)的重寫

虛函數(shù)的重寫(覆蓋):子類中有一個跟父類完全相同的虛函數(shù)(即子類虛函數(shù)與父類虛函數(shù)的返回值類型、函數(shù)名字、參數(shù)列表完全相同),稱子類的虛函數(shù)重寫了父類的虛函數(shù)。

如下段代碼中,子類 Student 類對 BuyTicket 函數(shù)完成了重寫:

			class Person
			{
			public:
				virtual void BuyTicket()
				{
					cout << "買票-全價" << endl;
				}
			};
			
			class Student : public Person
			{
			public:
				// void BuyTicket()  // 子類對 BuyTicket 進行重寫時,也可以不加 virtual
				virtual void BuyTicket()
				{
					cout << "買票-半價" << endl;
				}
			};

在重寫父類虛函數(shù)時,子類的虛函數(shù)在不加 virtual 關鍵字時,雖然也可以構(gòu)成重寫(因為繼承后基類的虛函數(shù)被繼承下來了在派生類依舊保持虛函數(shù)屬性),但是該種寫法不是很規(guī)范,不建議。這也和下面要說的析構(gòu)函數(shù)的重寫有一定的關聯(lián)。

虛函數(shù)重寫的兩個例外:

  1. 協(xié)變(父類與子類虛函數(shù)返回值類型不同)

子類重寫父類虛函數(shù)時,與父類虛函數(shù)返回值類型不同。即父類虛函數(shù)返回父類對象的指針或者引用,子類虛函數(shù)返回子類對象的指針或者引用時,稱為協(xié)變。(了解即可)

如以下這段代碼中,也構(gòu)成多態(tài):

			class Person
			{
			public:
				virtual Person* BuyTicket()
				{
					cout << "買票-全價" << endl;
					return nullptr;
				}
			};
			
			class Student : public Person
			{
			public:
				virtual Student* BuyTicket()
				{
					cout << "買票-半價" << endl;
					return nullptr;
				}
			};
  1. 析構(gòu)函數(shù)的重寫(父類與子類析構(gòu)函數(shù)的名字不同)

如果父類的析構(gòu)函數(shù)為虛函數(shù),此時子類析構(gòu)函數(shù)只要定義,無論是否加 virtual 關鍵字,都與父類的析構(gòu)函數(shù)構(gòu)成重寫,雖然父類與子類析構(gòu)函數(shù)名字不同。這樣看起來違背了重寫的規(guī)則,其實不然,這里可以理解為編譯器對析構(gòu)函數(shù)的名稱做了特殊處理,編譯后析構(gòu)函數(shù)的名稱統(tǒng)一處理成 destructor .

如果在父類中沒有加上 virtual 即析構(gòu)函數(shù)不構(gòu)成多態(tài),當下面這種情景時,不能正確調(diào)用析構(gòu)函數(shù):

			// 析構(gòu)函數(shù)不構(gòu)成多態(tài)
			class Person
			{
			public:
				~Person()
				{
					cout << "~Person()" << endl;
				}
			};
			
			class Student : public Person
			{
			public:
				~Student()
				{
					cout << "~Student()" << endl;
				}
			};

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

如上圖,當我們 new 的是一個子類對象,將它賦值兼容給父類,會發(fā)生切片操作,此時我們釋放 p 指針,由于析構(gòu)函數(shù)不構(gòu)成多態(tài),因此只會調(diào)用父類的析構(gòu)函數(shù),此時子類部分的空間就沒有被釋放,就會發(fā)生內(nèi)存泄漏。

當我們在父類的析構(gòu)函數(shù)加上 virtual,此時就構(gòu)成多態(tài)了,子類的析構(gòu)加不加 virtual 都無所謂,就是為了防止這種情況,我們在子類中忘記對析構(gòu)函數(shù)進行重寫,所以才會有上面的例外,在子類中進行重寫時可以不加 virtual;所以我們在父類中加上 virtual 即可構(gòu)成多態(tài),如下段代碼:

			class Person
			{
			public:
				virtual ~Person()
				{
					cout << "~Person()" << endl;
				}
			};
			
			class Student : public Person
			{
			public:
				~Student()
				{
					cout << "~Student()" << endl;
				}
			};

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

(3)override 和 final

從上面可以看出,C++ 對函數(shù)重寫的要求比較嚴格,但是有些情況下由于疏忽,可能會導致函數(shù)名字母次序?qū)懛炊鵁o法構(gòu)成重載,而這種錯誤在編譯期間是不會報出的,只有在程序運行時沒有得到預期結(jié)果才來 debug 會得不償失,因此:C++11提供了 override 和 final 兩個關鍵字,可以幫助用戶檢測是否重寫。

  1. final:修飾虛函數(shù),表示該虛函數(shù)不能再被重寫;

如下段代碼:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

  1. override:檢查子類虛函數(shù)是否重寫了父類某個虛函數(shù),如果沒有重寫編譯報錯。

如下段代碼:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

此時將B類中的func函數(shù)的返回類型改為 void 即可通過編譯。

(4)重載、覆蓋(重寫)、隱藏(重定義)

重載、覆蓋(重寫)、隱藏(重定義)的對比如下圖所示:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

3. 抽象類

(1)概念

在虛函數(shù)的后面寫上 =0 ,則這個函數(shù)為純虛函數(shù)。包含純虛函數(shù)的類叫做抽象類(也叫接口類)抽象類不能實例化出對象。子類繼承后也不能實例化出對象,只有重寫純虛函數(shù),子類才能實例化出對象。純虛函數(shù)規(guī)范了子類必須重寫,另外純虛函數(shù)更體現(xiàn)出了接口繼承。

			class A
			{
			public:
				virtual void func() = 0
				{
					cout << "A::func()" << endl;
				}
			};
			
			class B : public A
			{
			public:
				virtual void func()
				{
					cout << "B::func()" << endl;
				}
			};
			
			class C : public A
			{
			public:
				virtual void func()
				{
					cout << "C::func()" << endl;
				}
			};

如上段代碼,A類是抽象類,不能實例化出對象,B類和C類繼承了A類,并完成重寫 func 函數(shù),所以B類和C類可以實例化對象;下面我們簡單使用一下:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

(2)接口繼承和實現(xiàn)繼承

普通函數(shù)的繼承是一種實現(xiàn)繼承,子類繼承了父類函數(shù),可以使用函數(shù),繼承的是函數(shù)的實現(xiàn)。

虛函數(shù)的繼承是一種接口繼承,子類繼承的是父類虛函數(shù)的接口,目的是為了重寫,達成多態(tài),繼承的是接口。所以如果不實現(xiàn)多態(tài),不要把函數(shù)定義成虛函數(shù)。

4. 多態(tài)的原理

(1)虛函數(shù)表

我們先看一下下面這個類的大小是多少?

			class A
			{
			public:
				virtual void Func()
				{
					cout << "Func()" << endl;
				}
			private:
				int _a = 3;
			};

根據(jù)我們以前學的知識,成員函數(shù)不占A類的空間,而是放在公共代碼區(qū),而成員變量則根據(jù)內(nèi)存對齊計算大小,所以 sizeof(A) = 4,這樣算對嗎?我們看一下結(jié)果:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

如上圖,答案是 8,為什么會是 8 呢?我們實例化一個對象出來觀察它里面到底有什么:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

通過上圖我們發(fā)現(xiàn),除了 _a 成員,還多一個 __vfptr 放在對象的前面(注意有些平臺可能會放到對象的最后面,這個跟平臺有關),對象中的這個指針我們叫做虛函數(shù)表指針(v代表virtual,f代表function)。一個含有虛函數(shù)的類中都至少都有一個虛函數(shù)表指針,因為虛函數(shù)的地址要被放到虛函數(shù)表中,虛函數(shù)表也簡稱虛表。那么子類中這個表放了些什么呢?我們接著往下分析;

我們寫一個 Base 類,Derive 類繼承Base,Base 類中Func1 和 Func2 是虛函數(shù),F(xiàn)unc3 不是虛函數(shù); Derive 中重寫 Func1函數(shù),如下段代碼:

			class Base
			{
			public:
				virtual void Func1()
				{
					cout << "Base::Func1()" << endl;
				}
				virtual void Func2()
				{
					cout << "Base::Func2()" << endl;
				}
				void Func3()
				{
					cout << "Base::Func3()" << endl;
				}
			private:
				int _b = 1;
			};
			
			
			class Derive : public Base
			{
			public:
				virtual void Func1()
				{
					cout << "Derive::Func1()" << endl;
				}
			private:
				int _d = 2;
			};

下面我們實例化 b 對象和 d 對象:

			int main()
			{
				Base b;
				Derive d;
				return 0;
			}

然后我們通過調(diào)試窗口觀察對象中的內(nèi)存:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

我們看到,兩個對象的虛函數(shù)指針是不一樣;我們再進一步觀察虛函數(shù)指針中虛表的內(nèi)容:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

通過觀察和測試,我們發(fā)現(xiàn)了以下幾點問題:

  1. 子類對象 d 中也有一個虛表指針,d 對象由兩部分構(gòu)成,一部分是父類繼承下來的成員和虛表指針,另一部分是自己的成員。
  2. 父類 b 對象和子類 d 對象虛表是不一樣的,這里我們發(fā)現(xiàn) Func1完成了重寫,所以 d 的虛表中存的是重寫的 Derive::Func1 的地址,所以虛函數(shù)的重寫也叫作覆蓋,覆蓋就是指虛表中虛函數(shù)地址的覆蓋。重寫是語法的叫法,覆蓋是原理層的叫法。
  3. 另外 Func2 繼承下來后是虛函數(shù),所以放進了虛表,Func3 也繼承下來了,但是不是虛函數(shù),所以不會放進虛表。
  4. 虛函數(shù)表本質(zhì)是一個存虛函數(shù)指針的指針數(shù)組,一般情況這個數(shù)組最后面放了一個nullptr。
  5. 總結(jié)一下子類的虛表生成:
    a.先將父類中的虛表內(nèi)容拷貝一份到子類虛表中;
    b.如果子類重寫了父類中某個虛函數(shù),用子類自己的虛函數(shù)覆蓋虛表中父類的虛函數(shù) ;
    c.子類自己新增加的虛函數(shù)按其在子類中的聲明次序增加到子類虛表的最后。

(2)多態(tài)的原理

有了上面的基礎,我們就可以分析一下多態(tài)的原理了;

首先我們有以下兩個類實現(xiàn)的多態(tài):

			class Base
			{
			public:
				virtual void Func1()
				{
					cout << "Base::Func1()" << endl;
				}
			private:
				int _b = 1;
			};
			
			
			class Derive : public Base
			{
			public:
				virtual void Func1()
				{
					cout << "Derive::Func1()" << endl;
				}
			private:
				int _d = 2;
			};
			
			void Test(Base* pa)
			{
				pa->Func1();
			}

接下來我們分別使用Base和Derive實例化出對象 b 和 d,再調(diào)用 Test 函數(shù):

			int main()
			{
				Base b;
				Test(&b);
			
				Derive d;
				Test(&d);
			
				return 0;
			}

下面我們從匯編的角度分析多態(tài)的原理,如下圖:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言
從上圖可以看出,多態(tài)調(diào)用是運行時,去虛表里面找到函數(shù)地址,確定地址再調(diào)用。

但如果是對象調(diào)用,如下代碼,就不構(gòu)成多態(tài),我們同樣在匯編角度分析:

			void Test(Base pa)
			{
				pa.Func1();
			}
			
			int main()
			{
				Base b;
				Test(b);
			
				Derive d;
				Test(d);
			
				return 0;
			}

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

從上圖可以看出,普通調(diào)用是編譯鏈接時確定地址。

所以,我們要達到多態(tài),有兩個條件,一個是虛函數(shù)覆蓋,一個是對象的指針或引用調(diào)用虛函數(shù);其中虛函數(shù)覆蓋就不多說了;

父類的指針或引用接收對象的指針或引用調(diào)用虛函數(shù),本質(zhì)就是父類指針指向子類對象中切割出來父類的那一部分,然后再取虛函數(shù)指針找到虛函數(shù)的地址。

那么我們使用對象調(diào)用為什么不能實現(xiàn)多態(tài)呢?其實使用子類對象賦值給父類對象,會切割出子類對象中父類那一部分成員拷貝給父類,但是不會拷貝虛函數(shù)指針。為什么呢?我們假設:子類對象會拷貝虛函數(shù)表指針給父類對象,我們看一下以下代碼:

			Person* p = new Person;
			
			Student s;
			*p = s;
			
			delete p;	

此時出現(xiàn)的問題就是,多態(tài)調(diào)用指向父類,調(diào)用的不一定是父類的虛函數(shù)了,因為我們中間將 s 賦給了 *p,現(xiàn)在 p 中的虛函數(shù)指針是 s 的;所以這種方法不可取。

(3)動態(tài)綁定與靜態(tài)綁定

  1. 靜態(tài)綁定又稱為前期綁定(早綁定),在程序編譯期間確定了程序的行為,也稱為靜態(tài)多態(tài),比如:函數(shù)重載;
  2. 動態(tài)綁定又稱后期綁定(晚綁定),是在程序運行期間,根據(jù)具體拿到的類型確定程序的具體行為,調(diào)用具體的函數(shù),也稱為動態(tài)多態(tài)。

5. 虛函數(shù)和虛表存在于哪里?

虛函數(shù)和虛表存在于哪里?有人會說虛函數(shù)存在虛表,虛表存在對象中。但是上面的回答的錯誤的。

想要知道虛函數(shù)和虛表存在于哪里,我們可以打出各個區(qū)域的地址觀察,觀察虛函數(shù)和虛表離哪個區(qū)域比較近,下面我們開始驗證,如下段代碼:

			class Base
			{
			public:
				virtual void func1()
				{
					cout << "Base::func1" << endl;
				}
				virtual void func2()
				{
					cout << "Base::func2" << endl;
				}
			private:
				int a;
			};
			
			void func()
			{
				cout << "void func()" << endl;
			}
			
			int main()
			{
				Base b1;
				static int a = 0;
				int b = 0;
				int* p1 = new int;
				const char* p2 = "hello";
			
				printf("靜態(tài)區(qū):%p\n", &a);
				printf("棧:%p\n", &b);
				printf("堆:%p\n", p1);
				printf("代碼段/常量區(qū):%p\n", p2);
				printf("虛表:%p\n", *((int*)&b1));
				printf("虛函數(shù)地址:%p\n", &Base::func1);
				printf("普通函數(shù)地址:%p\n", func);
			
				return 0;
			}

運行結(jié)果如下:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

我們可以看到,在 vs2019 中,虛表和虛函數(shù)的地址都是離代碼段/常量區(qū)最近的,所以我們認為,在 vs2019 中,它們是在代碼段中的。

6. 單繼承中的虛函數(shù)表

需要注意的是在單繼承和多繼承關系中,下面我們?nèi)リP注的是子類對象的虛表模型,因為父類的虛表模型前面我們已經(jīng)看過了,沒什么需要特別研究的。我們這里就只看單繼承的中的虛函數(shù)表。

例如有以下代碼:

			class Base 
			{
			public:
				virtual void func1() 
				{ 
					cout << "Base::func1" << endl; 
				}
				virtual void func2() 
				{ 
					cout << "Base::func2" << endl; 
				}
			private:
				int a;
			};
			
			class Derive :public Base 
			{
			public:
				virtual void func1() 
				{ 
					cout << "Derive::func1" << endl; 
				}
				virtual void func3() 
				{ 
					cout << "Derive::func3" << endl; 
				}
				virtual void func4() 
				{ 
					cout << "Derive::func4" << endl; 
				}
			private:
				int b;
			};
			
			
			int main()
			{
				Base b;
				Derive d;
			
				return 0;
			}

我們通過監(jiān)視窗口觀察子類對象中的虛表模型:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

觀察上圖中的監(jiān)視窗口中我們發(fā)現(xiàn)看不見 func3func4。這里是編譯器的監(jiān)視窗口故意隱藏了這兩個函數(shù),也可以認為是他的一個小 bug。那么我們?nèi)绾尾榭?d 的虛表呢?下面我們使用代碼打印出虛表中的函數(shù)。

			class Base 
			{
			public:
				virtual void func1() 
				{ 
					cout << "Base::func1" << endl; 
				}
				virtual void func2() 
				{ 
					cout << "Base::func2" << endl; 
				}
			private:
				int a;
			};
			
			class Derive : public Base 
			{
			public:
				virtual void func1() 
				{ 
					cout << "Derive::func1" << endl; 
				}
				virtual void func3() 
				{ 
					cout << "Derive::func3" << endl; 
				}
				virtual void func4() 
				{ 
					cout << "Derive::func4" << endl; 
				}
			private:
				int b;
			};
			
			
			// typedef 函數(shù)指針類型
			typedef void (*VFUNC)();
			void PrintVFT(VFUNC* a)
			{
				for (size_t i = 0; a[i] != 0; i++)
				{
					printf("[%d]:%p->", i, a[i]);
					VFUNC f = a[i];
					f(); // 相當于調(diào)(*f)(); 調(diào)函數(shù)指針對應的函數(shù)
				}
			}
			
			int main()
			{
				Base b;
				PrintVFT((VFUNC*)(*((int*)&b)));
				cout << endl;
				Derive d;
				PrintVFT((VFUNC*)(*((int*)&d)));
			
				return 0;
			}

打印的結(jié)果如下:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

此時我們就可以看到 d 對象中的虛函數(shù)了。

7. 多繼承中的虛函數(shù)表

假設一個類是多繼承下來的,這個類有幾個父類,如果父類都有虛函數(shù),則就會有幾張?zhí)摫恚@個類自身不會產(chǎn)生多余的虛表;如果這個類自身也有虛函數(shù)呢?那么這個虛函數(shù)將會放到第一個父類的虛表中的最后,其他父類的虛表中不需要存儲這個類的虛函數(shù),因為存儲了也不能調(diào)用。

例如我們有以下三個類,A,B,C,其中 C 繼承 A 和 B,屬于多繼承,A 和 B 中都有虛函數(shù),C 中也對 A 、B 中的虛函數(shù)進行重寫,C 中再增加自己的虛函數(shù);如下三個類:

		class A
		{
		public:
			virtual void func1()
			{
				cout << "A" << endl;
			}
		};
		
		class B
		{
		public:
			virtual void func1()
			{
				cout << "B" << endl;
			}
		};
		
		class C : public A, public B
		{
		public:
			virtual void func1()
			{
				cout << "C" << endl;
			}
			virtual void func2() { ; }
		};

此時我們觀察它們的虛表,探究它們的關系;先定義三個類的對象:

		int main()
		{
			A a;
			B b;
			C c;
		
			return 0;
		}

我們進入調(diào)式模式觀察每個對象中的內(nèi)存:

先觀察a、b 對象中的內(nèi)存,它們都分別有一個虛函數(shù)指針,對應的虛函數(shù)指針中都有它們各自的虛函數(shù)的地址:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

然后觀察 c 對象中的內(nèi)存,c對象中有兩個虛函數(shù)指針,即有兩個虛函數(shù)表,我們分別觀察這兩個虛函數(shù)表中的內(nèi)容:

【C++】繼承和多態(tài),C++,1024程序員節(jié),c++,開發(fā)語言

通過上面我們可以得出結(jié)論,c 對象中自己定義的虛函數(shù)最終會放在第一個繼承的父類中的虛函數(shù)表的最后,而不會放在其它父類的虛函數(shù)表中。文章來源地址http://www.zghlxwxcb.cn/news/detail-735205.html

到了這里,關于【C++】繼承和多態(tài)的文章就介紹完了。如果您還想了解更多內(nèi)容,請在右上角搜索TOY模板網(wǎng)以前的文章或繼續(xù)瀏覽下面的相關文章,希望大家以后多多支持TOY模板網(wǎng)!

本文來自互聯(lián)網(wǎng)用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權(quán),不承擔相關法律責任。如若轉(zhuǎn)載,請注明出處: 如若內(nèi)容造成侵權(quán)/違法違規(guī)/事實不符,請點擊違法舉報進行投訴反饋,一經(jīng)查實,立即刪除!

領支付寶紅包贊助服務器費用

相關文章

  • 好用且免費的CodeWhisperer,給1024程序員節(jié)送禮來了

    好用且免費的CodeWhisperer,給1024程序員節(jié)送禮來了

    ? ? ? 國慶期間沒有膽量去人從眾的景點,關在家里刷手機時意外在亞馬遜的User Group公眾號上發(fā)現(xiàn)了CodeWhisperer這么個好東西(bu yao qian),以后擼代碼也可以提高生產(chǎn)力(fang yang mo yu)了,這還不趕緊上手試一下??垂俜浇榻B說它支持流行的IDE開發(fā)工具,包括VS Code、Intelli

    2024年02月08日
    瀏覽(31)
  • 1024程序員節(jié)帶你玩轉(zhuǎn)圖片Exif信息獲取之JavaScript

    1024程序員節(jié)帶你玩轉(zhuǎn)圖片Exif信息獲取之JavaScript

    目錄 一、前言 二、背景 三、Exif.js ? ? ? ? ?1、Exif.js 簡介 2、Exif.js 引入 四、多場景展示數(shù)據(jù)獲取 1、原始圖片直接獲取 ?2、base64 編碼文件加載 ?3、文件上傳的方式加載 ?五、總結(jié) ? ? ? ?1024是2的十次方,二進制計數(shù)的基本計量單位之一。1G=1024M,而1G與1級諧音,也有一

    2024年02月20日
    瀏覽(98)
  • 1024程序員節(jié)特輯 | Spring Boot實戰(zhàn) 之 MongoDB分片或復制集操作

    1024程序員節(jié)特輯 | Spring Boot實戰(zhàn) 之 MongoDB分片或復制集操作

    Spring實戰(zhàn)系列文章: Spring實戰(zhàn) | Spring AOP核心秘笈之葵花寶典 Spring實戰(zhàn) | Spring IOC不能說的秘密? 國慶中秋特輯系列文章: 國慶中秋特輯(八)Spring Boot項目如何使用JPA 國慶中秋特輯(七)Java軟件工程師常見20道編程面試題 國慶中秋特輯(六)大學生常見30道寶藏編程面試題

    2024年02月08日
    瀏覽(44)
  • 1024程序員狂歡節(jié) | IT前沿技術、人工智能、數(shù)據(jù)挖掘、網(wǎng)絡空間安全技術

    1024程序員狂歡節(jié) | IT前沿技術、人工智能、數(shù)據(jù)挖掘、網(wǎng)絡空間安全技術

    一年一度的1024程序員狂歡節(jié)又到啦!成為更卓越的自己,堅持閱讀和學習,別給自己留遺憾,行動起來吧! 那么,都有哪些好書值得入手呢?小編為大家整理了前沿技術、人工智能、集成電路科學與芯片技術、新一代信息與通信技術、網(wǎng)絡空間安全技術,四大熱點領域近期

    2024年02月06日
    瀏覽(32)
  • 1024程序員節(jié)特輯 | ELK+ 用戶畫像構(gòu)建個性化推薦引擎,智能實現(xiàn)“千人千面”

    1024程序員節(jié)特輯 | ELK+ 用戶畫像構(gòu)建個性化推薦引擎,智能實現(xiàn)“千人千面”

    專欄集錦,大佬們可以收藏以備不時之需 Spring Cloud實戰(zhàn)專欄:https://blog.csdn.net/superdangbo/category_9270827.html Python 實戰(zhàn)專欄:https://blog.csdn.net/superdangbo/category_9271194.html Logback 詳解專欄:https://blog.csdn.net/superdangbo/category_9271502.html tensorflow專欄:https://blog.csdn.net/superdangbo/category_869

    2024年02月07日
    瀏覽(36)
  • 1024程序員節(jié)特輯 | 解密Spring Cloud Hystrix熔斷提高系統(tǒng)的可用性和容錯能力

    1024程序員節(jié)特輯 | 解密Spring Cloud Hystrix熔斷提高系統(tǒng)的可用性和容錯能力

    專欄集錦,大佬們可以收藏以備不時之需 Spring Cloud實戰(zhàn)專欄:https://blog.csdn.net/superdangbo/category_9270827.html Python 實戰(zhàn)專欄:https://blog.csdn.net/superdangbo/category_9271194.html Logback 詳解專欄:https://blog.csdn.net/superdangbo/category_9271502.html tensorflow專欄:https://blog.csdn.net/superdangbo/category_869

    2024年02月08日
    瀏覽(28)
  • 1024程序員節(jié)?我們整點AI繪圖玩玩吧,一文教你配置stable-diffusion

    1024程序員節(jié)?我們整點AI繪圖玩玩吧,一文教你配置stable-diffusion

    需提前準備:一臺高性能的電腦(尤其是顯存)、python、Git、梯子。 其實Github上有很多關于Stable diffusion的庫,綜合對比之后,我選取的是比較全面的AUTOMATIC1111這個,源碼鏈接:Stable-diffusion(Github) 找到安裝那塊的教程,此教程以windows為例。 ps:如果你電腦上已經(jīng)有了pyt

    2024年01月16日
    瀏覽(31)
  • 想轉(zhuǎn)行做程序員,該怎么選擇開發(fā)語言?哪個崗位工資最高?

    本文主要針對零基礎想了解或者轉(zhuǎn)行從事開發(fā)崗的同學。 我們收集了往屆畢業(yè)同學和一些正在咨詢的同學,發(fā)現(xiàn)大家在學習初期,對轉(zhuǎn)行互聯(lián)網(wǎng)做開發(fā),最多的疑問或者顧慮大體分為幾類: 現(xiàn)在哪門語言比較火? 學什么語言好找到工作? XX語言是不是飽和了? ... 其實語言

    2024年02月02日
    瀏覽(23)
  • 黑馬程序員——javase進階——day01——匿名對象 , 繼承 , 抽象類

    黑馬程序員——javase進階——day01——匿名對象 , 繼承 , 抽象類

    目錄: 面向?qū)ο蠡仡?面向?qū)ο蟮暮诵乃枷胧鞘裁? 現(xiàn)有的類還是先有的對象? Java類的創(chuàng)建? 類中的組成成分? 創(chuàng)建對象所使用的? 創(chuàng)建對象的格式? 調(diào)用對象的成員? 定義構(gòu)造方法的格式? 定義構(gòu)造方法的特點? 構(gòu)造方法的作用? 面向?qū)ο笕筇卣魇鞘裁? 封裝的思想及其作

    2024年01月24日
    瀏覽(23)
  • 【程序員必備】UE4 C++ 虛幻引擎:詳解JSON文件讀、寫、解析,打造高效開發(fā)!

    【程序員必備】UE4 C++ 虛幻引擎:詳解JSON文件讀、寫、解析,打造高效開發(fā)!

    ???♂? 作者:海碼007 ?? 專欄:UE虛幻引擎專欄 ?? 標題:【程序員必備】UE4 C++ 虛幻引擎:詳解JSON文件讀、寫、解析,打造高效開發(fā)! ?? 寄語:人生的意義或許可以發(fā)揮自己全部的潛力,所以加油吧! ?? 最后: 文章作者技術和水平有限,如果文中出現(xiàn)錯誤,希望大

    2024年02月03日
    瀏覽(24)

覺得文章有用就打賞一下文章作者

支付寶掃一掃打賞

博客贊助

微信掃一掃打賞

請作者喝杯咖啡吧~博客贊助

支付寶掃一掃領取紅包,優(yōu)惠每天領

二維碼1

領取紅包

二維碼2

領紅包