Maison > développement back-end > Tutoriel C#.Net > Présentation des classes de fonctions virtuelles de base liées à l'héritage et au polymorphisme en C++

Présentation des classes de fonctions virtuelles de base liées à l'héritage et au polymorphisme en C++

巴扎黑
Libérer: 2017-09-11 11:28:47
original
1504 Les gens l'ont consulté

Cet article vous présente principalement les informations pertinentes sur les classes de fonctions virtuelles de base de l'héritage et du polymorphisme en C++. L'article les présente en détail à travers un exemple de code. Il a une certaine valeur d'apprentissage de référence pour l'étude ou le travail de chacun. Amis, veuillez suivre l'éditeur pour apprendre ensemble.

Avant-propos

Cet article vous présente principalement le contenu pertinent sur les classes de fonctions virtuelles de base de l'héritage et du polymorphisme en C++. pour tout le monde. Veuillez vous y référer pour référence. Je n’en dirai pas plus ci-dessous. Jetons un coup d’œil à l’introduction détaillée.

Classe de fonctions virtuelles

En héritage, on évoque souvent l'héritage virtuel Explorons maintenant ce genre de fonction virtuelle, virtuelle If. le mot-clé virtual est ajouté devant une fonction membre d'une classe de fonctions, la fonction membre est appelée fonction virtuelle. Ne sous-estimez pas cette fonction virtuelle. Elle peut résoudre de nombreux problèmes épineux en matière d'héritage, et elle est encore plus importante pour le polymorphisme. . Sans cela, il n'y a pas de polymorphisme, donc ce point de connaissance est très important, et la table de fonctions virtuelles introduite plus tard est extrêmement importante. Vous devez la comprendre attentivement ~ Maintenant, en commençant à conceptualiser les fonctions virtuelles, un autre concept est introduit, à savoir la réécriture (. écrasement), chez un enfant Lorsqu'une classe définit une fonction virtuelle qui est exactement la même que la classe parent, on dit que la fonction de la sous-classe remplace (également appelée écrasement) la fonction virtuelle de la classe parent. Permettez-moi d'abord de mentionner la table de fonctions virtuelles. Comme nous le verrons plus tard, la réécriture consiste à remplacer toutes les adresses de fonctions remplacées de la classe parent dans la table de fonctions virtuelles de la sous-classe par les adresses des fonctions de sous-classe.

Fonction virtuelle pure

Écrivez =0 après le paramètre formel de la fonction membre, alors la fonction membre est une fonction virtuelle pure. Les classes qui contiennent des fonctions virtuelles pures sont appelées classes abstraites (également appelées classes d'interface)

Les classes abstraites ne peuvent pas instancier des objets. Ce n'est qu'après qu'une fonction virtuelle pure est redéfinie dans une classe dérivée que la classe dérivée peut instancier un objet.

Regardez un exemple :


class Person 
{ 
  virtual void Display () = 0; // 纯虚函数 
protected : 
  string _name ;   // 姓名 
}; 
 
class Student : public Person 
{};
Copier après la connexion

Résumons d'abord le concept :

1. Dérivé réutilisation de classe Pour implémenter le polymorphisme en écrivant une fonction virtuelle d'une classe de base, le nom de la fonction, la liste des paramètres et la valeur de retour doivent être exactement les mêmes. (Sauf pour la covariance)

2. Une fonction virtuelle est définie dans la classe de base, et la fonction conserve toujours les caractéristiques d'une fonction virtuelle dans la classe dérivée.

3. Seules les fonctions membres d'une classe peuvent être définies comme fonctions virtuelles.

4. Les fonctions membres statiques ne peuvent pas être définies comme des fonctions virtuelles.

5. Si vous définissez une fonction virtuelle en dehors de la classe, vous ne pouvez ajouter virtual que lors de la déclaration de la fonction. Vous ne pouvez pas ajouter virtual lors de la définition de la fonction en dehors de la classe.

6. N'appelez pas de fonctions virtuelles dans le constructeur et le destructeur, l'objet est incomplet et un comportement indéfini peut se produire.

7. Il est préférable de déclarer le destructeur de la classe de base comme une fonction virtuelle. (Pourquoi ? De plus, le destructeur est spécial car le nom du destructeur de la classe dérivée est différent de celui de la classe de base, mais il constitue un override. C'est parce que le compilateur a fait un traitement spécial)

8. Le constructeur ne peut pas être une fonction virtuelle. Bien que Operator= puisse être défini comme une fonction virtuelle, il est préférable de ne pas définir Operator= comme une fonction virtuelle car il est facile de semer la confusion lorsqu'il est utilisé.


Vous vous demandez peut-être pourquoi est-ce le concept ci-dessus ? Les réponses à ces contenus peuvent être trouvées dans les connaissances suivantes ~ D'accord, alors notre protagoniste d'aujourd'hui est fonction virtuelleApparence !!!

Qu'est-ce qu'une table de fonctions virtuelles ? On peut le découvrir en écrivant un programme et en ajustant une fenêtre de surveillance.

Ce qui suit est une classe avec des fonctions virtuelles :


#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; 
}
Copier après la connexion

On clique maintenant sur la fenêtre de surveillance de b1

Il y a un _vfptr ici, et ce que ce _vfptr pointe est notre protagoniste, la table de fonctions virtuelle. Tout le monde saura dans un instant qu'il s'agit d'un héritage unique ou d'un héritage multiple, même notre table de fonctions virtuelles d'héritage en forme de losange aura des formes différentes. La table de fonctions virtuelles est une chose très intéressante.

Étudions la disposition de la mémoire de l'héritage unique

Regardez attentivement le code suivant :


#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; 
};
Copier après la connexion

Pour la classe Derive, que pensons-nous qu'il y aura dans sa table virtuelle ?

Tout d'abord, le fun1() de la sous-classe réécrit le fun1() de la classe parent. La table virtuelle stocke le fun1() de la sous-classe, puis le fun2() de la classe parent. , la sous-classe Les fun3() et fun4() sont toutes des fonctions virtuelles, il y aura donc 4 éléments dans la table virtuelle, à savoir le fun1() de la sous-classe, le fun2() de la classe parent, le fun3() de la sous-classe et le fun4(). Ensuite, nous ouvrons la fenêtre de surveillance pour voir si ce que nous pensons est correct ?

我预计应该是看到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; 
}
Copier après la connexion

下图来说一下他的缘由:


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


//单继承 
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; 
}
Copier après la connexion

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


PrintVTable((int*)(*(int*)(&d1)));
Copier après la connexion

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

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



Présentation des classes de fonctions virtuelles de base liées à l'héritage et au polymorphisme en 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; 
}
Copier après la connexion

现在我们现在知道会有两个虚函数表,分别是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; 
}
Copier après la connexion

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

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


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


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


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

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

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Étiquettes associées:
source:php.cn
Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal