首頁 後端開發 C#.Net教程 介紹有關C++中繼承與多型態的基礎虛函數類

介紹有關C++中繼承與多型態的基礎虛函數類

Sep 11, 2017 am 11:28 AM
函數 基礎 繼承

這篇文章主要給大家介紹了關於C++中繼承與多態的基礎虛函數類的相關資料,文中透過範例程式碼介紹的非常詳細,對大家的學習或工作具有一定的參考學習價值,需要的朋友們下面隨著小編來一起學習學習吧。

前言

本文主要為大家介紹了關於C++中繼承與多態的基礎虛函數類別的相關內容,並分享出來供大家參考學習,下面話不多說了,來一起看看詳細的介紹吧。

虛函數類別

在繼承中我們經常提到虛擬繼承,現在我們來探究這種的虛函數,虛函數類別的成員函數前面加virtual關鍵字,則這個成員函數稱為虛函數,不要小看這個虛函數,他可以解決繼承中許多棘手的問題,而對於多態那他更重要了,沒有它就沒有多態,所以這個知識點非常重要,以及後面介紹的虛函數表都極其重要,一定要認真的理解~ 現在開始概念虛函數就又引出一個概念,那就是重寫(覆蓋),當在子類別的定義了一個與父類別完全相同的虛擬函數時,則稱子類別的這個函數重寫(也稱為覆蓋)了父類別的這個虛擬函數。這裡先提一下虛函數表,後面會講到的,重寫就是將子類別裡面的虛函數表裡的被重寫父類別的函數位址全都改成子類別函數的位址。

純虛函數

在成員函數的形參後面寫上=0,則成員函數為純虛函數。包含純虛函數的類別叫做抽象類別(也叫介面類別)

抽象類別不能實例化出物件。純虛函數在衍生類別中重新定義以後,衍生類別才能實例化出物件。

看一個例子:


class Person 
{ 
  virtual void Display () = 0; // 纯虚函数 
protected : 
  string _name ;   // 姓名 
}; 
 
class Student : public Person 
{};
登入後複製

先總結一下概念:

1.衍生類別重寫基類別的虛擬函數實現多態,要求函數名稱、參數列表、傳回值完全相同。 (協變除外)

2.基底類別中定義了虛函數,在衍生類別中該函數始終保持虛函數的特性。

3.只有類別的成員函數才能定義為虛函數。

4.靜態成員函數不能定義為虛函數。

5.如果在類別外定義虛擬函數,只能在宣告函數時加上virtual,類別外定義函數時不能加上virtual。

6.不要在建構子和析構函式裡面呼叫虛函數,在建構子和析構函式中,物件是不完整的,可能會發生未定義的行為。

7.最好把基底類別的析構函數宣告為虛函數。 (why?另外析構函數比較特殊,因為衍生類別的析構函式跟基底類別的析構函式名稱不一樣,但是構成覆寫,這裡是因為編譯器做了特殊處理)

#8.建構函數不能為虛擬函數,雖然可以將operator=定義為虛函數,但是最好不要將operator=定義為虛函數,因為容易使用時容易造成混淆.


上面概念大家可能都會問一句為什麼要這樣? 這些內容在接下來的知識裡都能找到答案~ 好了那麼我們今天的主角虛函數#登場!!!!

何為虛函數表,我們寫一個程序,調一個監視視窗就知道了。

下面是一個有虛擬函數的類別:


#include<iostream> 
#include<windows.h> 
using namespacestd; 
 
class Base 
{ 
public: 
   virtual void func1() 
   {} 
 
   virtual void func2() 
   {} 
 
private: 
   inta; 
}; 
 
void Test1() 
{ 
   Base b1; 
} 
 
int main() 
 
{ 
   Test1(); 
   system("pause"); 
   return0; 
}
登入後複製

我們現在點開b1的監視視窗

這裡面有一個_vfptr,而這個_vfptr指向的東西就是我們的主角,虛函數表。一會兒大家就知道了,無論是單繼承還是多繼承甚至於我們的菱形繼承虛函數表都會有不同的形態,虛函數表是一個很有趣的東西。

我們來研究單繼承的記憶體格局

##仔細看下面程式碼:


#include<iostream> 
#include<windows.h> 
using namespace std; 
 
 
class Base 
{ 
public: 
   virtual void func1() 
   { 
     cout<< "Base::func1"<< endl; 
   } 
 
   virtual void func2() 
   { 
     cout<< "Base::func2"<< endl; 
   } 
 
private: 
   inta; 
}; 
 
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; 
};
登入後複製

對於Derive類別來說,我們覺得它的虛表裡會有什麼?


首先子類別的

fun1()重寫了父類別的fun1() ,虛表裡存的是子類別的 fun1() ,接下來父類別的fun2() ,子類別的fun3() , fun4()都是虛函數,所以虛表裡會有4個元素,分別為子類別的fun1() ,父類別fun2() ,子類別fun3() ,子類fun4() 。然後我們調出監視視窗看我們想的到底對不對呢?

 

#

我预计应该是看到fun1() ,fun2() ,fun3() ,fun4()的虚函数表,但是呢这里监视窗口只有两个fun1() , fun2() ,难道我们错了????

这里并不是这样的,只有自己靠得住,我觉得这里的编译器有问题,那我们就得自己探索一下了。 但是在探索之前我们必须来实现一个可以打印虚函数表的函数。


typedef void(*FUNC)(void); 
void PrintVTable(int* VTable) 
{ 
   cout<< " 虚表地址"<<VTable<< endl; 
 
   for(inti = 0;VTable[i] != 0; ++i) 
   { 
     printf(" 第%d个虚函数地址 :0X%x,->", i,VTable[i]); 
     FUNC f = (FUNC)VTable[i]; 
     f(); 
   } 
 
   cout<< endl; 
} 
 
 
int main() 
{ 
   Derive d1; 
   PrintVTable((int*)(*(int*)(&d1))); 
   system("pause"); 
   return0; 
}
登入後複製

下图来说一下他的缘由:


我们来使用这个函数,该函数代码如下:


//单继承 
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 void(*FUNC)(void); 
void PrintVTable(int* VTable) 
{ 
   cout<< " 虚表地址"<<VTable<< endl; 
  
   for(inti = 0;VTable[i] != 0; ++i) 
   { 
     printf(" 第%d个虚函数地址 :0X%x,->", i,VTable[i]); 
     FUNC f = (FUNC)VTable[i]; 
     f(); 
   } 
  
   cout<< endl; 
} 
  
  
int main() 
{ 
   Derive d1; 
   PrintVTable((int*)(*(int*)(&d1))); //重点 
   system("pause"); 
   return0; 
}
登入後複製

这里我就要讲讲这个传参了,注意这里的传参不好理解,应当细细的"品味".


PrintVTable((int*)(*(int*)(&d1)));
登入後複製

首先我们肯定要拿到d1的首地址,把它强转成int*,让他读取到前4个字节的内容(也就是指向虚表的地址),再然后对那个地址解引用,我们已经拿到虚表的首地址的内容(虚表里面存储的第一个函数的地址)了,但是此时这个变量的类型解引用后是int,不能够传入函数,所以我们再对他进行一个int*的强制类型转换,这样我们就传入参数了,开始函数执行了,我们一切都是在可控的情况下使用强转,使用强转你必须要特别清楚的知道内存的分布结构。

最后我们来看看输出结果:



介紹有關C++中繼承與多型態的基礎虛函數類到底打印的对不对呢? 我们验证一下:

这里我们通过&d1的首地址找到虚表的地址,然后访问地址查看虚表的内容,验证我们自己写的这个函数是正确的。(这里VS还有一个bug,当你第一次打印虚表时程序可能会崩溃,不要担心你重新生成解决方案,再运行一次就可以了。因为当你第一次打印是你虚表最后一个地方可能没有放0,所以你就有可能停不下来然后崩溃。)我们可以看到d1的虚表并不是监视器里面打印的那个样子的,所以有时候VS也会有bug,不要太相信别人,还是自己靠得住。哈哈哈,臭美一下~

我们来研究一下多继承的内存格局

探究完了单继承,我们来看看多继承,我们还是通过代码调试的方法来探究对象模型

看如下代码:


class Base1 
{ 
public: 
 virtual void func1() 
 { 
  cout << "Base1::func1" << endl; 
 } 
 
 virtual void func2() 
 { 
  cout << "Base1::func2" << endl; 
 } 
 
private: 
 int b1; 
}; 
 
class Base2 
{ 
public: 
 virtual void func1() 
 { 
  cout << "Base2::func1" << endl; 
 } 
 
 virtual void func2() 
 { 
  cout << "Base2::func2" << endl; 
 } 
 
private: 
 int b2; 
}; 
 
 
class Derive : public Base1, public Base2 
{ 
public: 
 virtual void func1() 
 { 
  cout << "Derive::func1" << endl; 
 } 
 
 virtual void func3() 
 { 
  cout << "Derive::func3" << endl; 
 } 
 
private: 
 int d1; 
}; 
 
typedef void(*FUNC) (); 
void PrintVTable(int* VTable) 
{ 
 cout << " 虚表地址>" << VTable << endl; 
 
 for (int i = 0; VTable[i] != 0; ++i) 
 { 
  printf(" 第%d个虚函数地址 :0X%x,->", i, VTable[i]); 
  FUNC f = (FUNC)VTable[i]; 
  f(); 
 } 
 cout << endl; 
} 
 
 
void Test1() 
{ 
 Derive d1; 
 //Base2虚函数表在对象Base1后面 
 int* VTable = (int*)(*(int*)&d1); 
 PrintVTable(VTable); 
 int* VTable2 = (int *)(*((int*)&d1 + sizeof (Base1) / 4)); 
 PrintVTable(VTable2); 
} 
int main() 
{ 
 Test1(); 
 system("pause"); 
 return 0; 
}
登入後複製

现在我们现在知道会有两个虚函数表,分别是Base1和Base2的虚函数表,但是呢!我们的子类里的fun3()函数怎么办?它是放在Base1里还是Base2里还是自己开辟一个虚函数表呢?我们先调一下监视窗口:


监视窗口又不靠谱了。。。。完全没有找到fun3().那我们直接看打印出来的虚函数表。


现在很清楚了,fun3()在Base1的虚函数表中,而Base1是先继承的类,好了现在我们记住这个结论,当涉及多继承时,子类的虚函数会存在先继承的那个类的虚函数表里。记住了!

我们现在来看多继承的对象模型:


现在我们来结束一下上面我列的那么多概念现在我来逐一的解释为什么要这样.

1.为什么静态成员函数不能定义为虚函数?

因为静态成员函数它是一个大家共享的一个资源,但是这个静态成员函数没有this指针,而且虚函数变只有对象才能能调到,但是静态成员函数不需要对象就可以调用,所以这里是有冲突的.

2.为什么不要在构造函数和析构函数里面调用虚函数?

构造函数当中不适合用虚函数的原因是:在构造对象的过程中,还没有为“虚函数表”分配内存。所以,这个调用也是违背先实例化后调用的准则析构函数当中不适用虚函数的原因是:一般析构函数先析构子类的,当你在父类中调用一个重写的fun()函数,虚函数表里面就是子类的fun()函数,这时候已经子类已经析构了,当你调用的时候就会调用不到.

现在我在写最后一个知识点,为什么尽量最好把基类的析构函数声明为虚函数??

现在我们再来写一个例子,我们都知道平时正常的实例化对象然后再释放是没有一点问题的,但是现在我这里举一个特例:

我们都知道父类的指针可以指向子类,现在呢我们我们用一个父类的指针new一个子类的对象。


//多态 析构函数 
class Base 
{ 
public: 
 virtual void func1() 
 { 
  cout << "Base::func1" << endl; 
 } 
 
 virtual void func2() 
 { 
  cout << "Base::func2" << endl; 
 } 
 
 virtual ~Base() 
 { 
  cout << "~Base" << endl; 
 } 
 
private: 
 int a; 
}; 
 
class Derive :public Base 
{ 
public: 
 virtual void func1() 
 { 
  cout << "Derive::func1" << endl; 
 } 
 virtual ~Derive() 
 { 
  cout << "~Derive"<< endl; 
 } 
private: 
 int b; 
}; 
 
void Test1() 
{ 
 Base* q = new Derive; 
 delete q; 
} 
int main() 
{ 
 Test1(); 
 system("pause"); 
 return 0; 
}
登入後複製

这里面可能会有下一篇要说的多态,所以可能理解起来会费劲一点。

注意这里我先让父类的析构函数不为虚函数(去掉virtual),我们看看输出结果:


这里它没有调用子类的析构函数,因为他是一个父类类型指针,所以它只能调用父类的析构函数,无权访问子类的析构函数,这种调用方法会导致内存泄漏,所以这里就是有缺陷的,但是C++是不会允许自己有缺陷,他就会想办法解决这个问题,这里就运用到了我们下次要讲的多态。现在我们让加上为父类析构函数加上virtual,让它变回虚函数,我们再运行一次程序的:


诶! 子类的虚函数又被调用了,这里发生了什么呢??  来我们老方法打开监视窗口。


刚刚这种情况就是多态,多态性可以简单地概括为“一个接口,多种方法”,程序在运行时才决定调用的函数,它是面向对象编程领域的核心概念。这个我们下一个博客专门会总结多态.

当然虚函数的知识点远远没有这么一点,这里可能只是冰山一角,比如说菱形继承的虚函数表是什么样?然后菱形虚拟继承又是什么样子呢? 这些等我总结一下会专门写一个博客来讨论菱形继承。虚函数表我们应该已经知道是什么东西了,也知道单继承和多继承中它的应用,这些应该就足够了,这些其实都是都是为你让你更好的理解继承和多态,当然你一定到分清楚重写,重定义,重载的他们分别的含义是什么. 这一块可能有点绕,但是我们必须要掌握.

以上是介紹有關C++中繼承與多型態的基礎虛函數類的詳細內容。更多資訊請關注PHP中文網其他相關文章!

本網站聲明
本文內容由網友自願投稿,版權歸原作者所有。本站不承擔相應的法律責任。如發現涉嫌抄襲或侵權的內容,請聯絡admin@php.cn

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.聊天命令以及如何使用它們
1 個月前 By 尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SublimeText3漢化版

SublimeText3漢化版

中文版,非常好用

禪工作室 13.0.1

禪工作室 13.0.1

強大的PHP整合開發環境

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

SublimeText3 Mac版

SublimeText3 Mac版

神級程式碼編輯軟體(SublimeText3)

C++ 函式繼承詳解:如何在繼承中使用「基底類別指標」和「衍生類別指標」? C++ 函式繼承詳解:如何在繼承中使用「基底類別指標」和「衍生類別指標」? May 01, 2024 pm 10:27 PM

在函數繼承中,使用「基底類別指標」和「衍生類別指標」來理解繼承機制:基底類別指標指向派生類別物件時,執行向上轉型,只存取基底類別成員。派生類別指標指向基底類別物件時,執行向下轉型(不安全),必須謹慎使用。

golang函數動態建立新函數的技巧 golang函數動態建立新函數的技巧 Apr 25, 2024 pm 02:39 PM

Go語言提供了兩種動態函數創建技術:closures和反射。 closures允許存取閉包作用域內的變量,而反射可使用FuncOf函數建立新函數。這些技術在自訂HTTP路由器、實現高度可自訂的系統和建置可插拔的元件方面非常有用。

C++ 函數命名中參數順序的考慮 C++ 函數命名中參數順序的考慮 Apr 24, 2024 pm 04:21 PM

在C++函數命名中,考慮參數順序至關重要,可提高可讀性、減少錯誤並促進重構。常見的參數順序約定包括:動作-物件、物件-動作、語意意義和遵循標準函式庫。最佳順序取決於函數目的、參數類型、潛在混淆和語言慣例。

如何在Java中寫出高效和可維護的函數? 如何在Java中寫出高效和可維護的函數? Apr 24, 2024 am 11:33 AM

編寫高效且可維護的Java函數的關鍵在於:保持簡潔。使用有意義的命名。處理特殊情況。使用適當的可見性。

excel函數公式大全 excel函數公式大全 May 07, 2024 pm 12:04 PM

1. SUM函數,用於對一列或一組單元格中的數字進行求和,例如:=SUM(A1:J10)。 2、AVERAGE函數,用於計算一列或一組儲存格中的數字的平均值,例如:=AVERAGE(A1:A10)。 3.COUNT函數,用於計算一列或一組單元格中的數字或文字的數量,例如:=COUNT(A1:A10)4、IF函數,用於根據指定的條件進行邏輯判斷,並返回相應的結果。

C++ 中繼承和多態性如何影響類別的耦合度? C++ 中繼承和多態性如何影響類別的耦合度? Jun 05, 2024 pm 02:33 PM

繼承和多態性會影響類別的耦合度:繼承會增加耦合度,因為衍生類別依賴基底類別。多態性可以降低耦合度,因為物件可以透過虛擬函數和基底類別指標以一致的方式回應訊息。最佳實踐包括謹慎使用繼承、定義公共介面、避免在基底類別中新增資料成員,以及透過依賴注入解耦類別。實戰案例顯示如何使用多態性和依賴注入來降低銀行帳戶應用程式中的耦合度。

自訂 PHP 函數和預定義函數之間有什麼區別? 自訂 PHP 函數和預定義函數之間有什麼區別? Apr 22, 2024 pm 02:21 PM

自訂PHP函數與預定義函數的差異在於:作用域:自訂函數僅限於其定義範圍,而預定義函數可在整個腳本中存取。定義方式:自訂函數使用function關鍵字定義,而預先定義函數則由PHP核心定義。參數傳遞:自訂函數接收參數,而預先定義函數可能不需要參數。擴充性:自訂函數可以根據需要創建,而預定義函數是內建的且無法修改。

C++ 函式繼承詳解:如何偵錯繼承中出現的錯誤? C++ 函式繼承詳解:如何偵錯繼承中出現的錯誤? May 02, 2024 am 09:54 AM

繼承錯誤調試技巧:確保正確的繼承關係。使用偵錯器逐步執行程式碼,檢查變數值。確保正確使用virtual修飾符。檢查隱藏的繼承帶來的菱形繼承問題。檢查抽象類別中未實現的純虛函數。

See all articles