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*的强制类型转换,这样我们就传入参数了,开始函数执行了,我们一切都是在可控的情况下使用强转,使用强转你必须要特别清楚的知道内存的分布结构。

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

到底打印的对不对呢? 我们验证一下: 

这里我们通过&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,让它变回虚函数,我们再运行一次程序的:

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

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

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

总结

以上就是对虚函数的一点简单见解,希望本文的内容对大家的学习或者工作具有一定的参考学习价值,如果有疑问大家可以留言交流,谢谢大家对我们的支持。

(0)

相关推荐

  • C++自定义封装socket操作业务类完整实例

    本文实例讲述了C++自定义封装socket操作业务类.分享给大家供大家参考,具体如下: Linux下C++封装socket操作的工具类(自己实现) socketconnector.h #ifndef SOCKETCONNECTOR_H #define SOCKETCONNECTOR_H #include "global.h" using namespace std; class SocketConnector { public: typedef enum { ENormal, EOth

  • 详解C++的String类的字符串分割实现

    详解C++的String类的字符串分割实现 功能需求,输入一个字符串"1-2-3"切割出"1"."2"."3".在Java下直接用String的split函数就可以了.c++下String没有直接提供这个函数,需要自己写. 网上给出的解决方案是这里的三种方法.但我是通过JNI访问的,在里面用这些vector可能不中,自己封装了个,仅供参考: String recogScop = "01-02-03"; co

  • C++ 类的继承与派生实例详解

     C++ 类的继承与派生实例详解 继承性是面向对象程序设计最重要的特性之一,使软件有了可重用性,C++提供的类的继承机制. 继承与派生的概念 一个新类从已有的类那里获得已有的特性,这种现象称为类的继承.同样也可以说成已有的类派生出来了新的类.类A继承自类B也就是类B派生了类A.所以继承和派生的关系就像小学时把字句和被字句的造句一样.有了继承与派生后,就有了父类/基类与子类/派生类,C++中将类B称为父类/基类,将类A称为子类/派生类. 派生类的声明: #include <iostream> u

  • C++中的聚合类定义与用法分析

    本文实例讲述了C++中的聚合类.分享给大家供大家参考,具体如下: 聚合类是一种没有用户定义的构造函数,没有私有(private)和保护(protected)非静态数据成员,没有基类,没有虚函数.这样的类可以由封闭的大括号用逗号分隔开初始化列表.下列的代码在 C 和 C++ 具有相同的语法: struct C { int a; double b; }; struct D { int a; double b; C c; }; // initialize an object of type C wit

  • C++类继承之子类调用父类的构造函数的实例详解

    C++类继承之子类调用父类的构造函数的实例详解 父类HttpUtil: #pragma once #include <windows.h> #include <string> using namespace std; class HttpUtil { private: LPVOID hInternet; LPVOID hConnect; LPVOID hRequest; protected: wchar_t * mHostName; short mPort; string send

  • C++ 中类的拷贝、赋值、销毁的实例详解

    C++ 中类的拷贝.赋值.销毁的实例详解 本篇文章我们一共讲解一下几个知识点: 类的拷贝构造函数. 类的拷贝赋值运算符. 类的析构. 好了one by one 如果我们没有定义类的拷贝构造函数的话,那么编译器会为我们合成默认拷贝构造函数----合成拷贝构造函数. 和成拷贝构造函数的操作是将其参数的各个成员拷贝到正在创建的对象中去,每个成员的类型决定了他是如何被拷贝的:对类类型的成员,会使用其拷贝构造函数,内置类型的成员则是直接拷贝,虽然我们不能直接拷贝一个数组,但是合成拷贝构造函数会逐个的拷贝一

  • C++中继承与多态的基础虚函数类详解

    前言 本文主要给大家介绍了关于C++中继承与多态的基础虚函数类的相关内容,分享出来供大家参考学习,下面话不多说了,来一起看看详细的介绍吧. 虚函数类 继承中我们经常提到虚拟继承,现在我们来探究这种的虚函数,虚函数类的成员函数前面加virtual关键字,则这个成员函数称为虚函数,不要小看这个虚函数,他可以解决继承中许多棘手的问题,而对于多态那他更重要了,没有它就没有多态,所以这个知识点非常重要,以及后面介绍的虚函数表都极其重要,一定要认真的理解~ 现在开始概念虚函数就又引出一个概念,那就是重写(覆

  • C/C++中虚基类详解及其作用介绍

    目录 概述 多重继承的问题 虚基类 初始化 例子 总结 概述 虚基类 (virtual base class) 是用关键字 virtual 声明继承的父类. 多重继承的问题 N 类: class N { public: int a; void display(){ cout << "A::a=" << a <<endl; } }; A 类: class A : public N { public: int a1; }; B 类: class B :

  • C++ 中const修饰虚函数实例详解

    C++ 中const修饰虚函数实例详解 [1]程序1 #include <iostream> using namespace std; class Base { public: virtual void print() const = 0; }; class Test : public Base { public: void print(); }; void Test::print() { cout << "Test::print()" << end

  • Python基础之元类详解

    1.python 中一切皆是对象,类本身也是一个对象,当使用关键字 class 的时候,python 解释器在加载 class 的时候会创建一个对象(这里的对象指的是类而非类的实例) class Student: pass s = Student() print(type(s)) # <class '__main__.Student'> print(type(Student)) # <class 'type'> 2.什么是元类 元类是类的类,是类的模板 元类是用来控制如何创建类的,

  • Java基础之Object类详解

    object类的介绍 object是所有类的直接父类或者是间接父类,为什么这么说呢? 可以查询java8的API帮助文档: 可见在这样的一个类树中,所有的类的根还是Object类 在IDEA中新建一个类,系统会默认继承Object类 public class Pet extends Object{ } 那么Dog继承了Pet类的属性和行为方法,还会继承Object类的属性和行为方法了吗?这一点是肯定的,Pet类作为Object类的子类,Dog类作为Pet类的子类,所以说Object是Dog类的间

  • C++ 虚函数的详解及简单实例

    C++ 虚函数的详解 虚函数的使用和纯虚函数的使用. 虚函数是在基类定义,然后子类重写这个函数后,基类的指针指向子类的对象,可以调用这个函数,这个函数同时保留这子类重写的功能. 纯虚函数是可以不用在基类定义,只需要声明就可以了,然后因为是纯虚函数,是不能产生基类的对象,但是可以产生基类的指针. 纯虚函数和虚函数最主要的区别在于,纯虚函数所在的基类是不能产生对象的,而虚函数的基类是可以产生对象的. // pointers to base class #include <iostream> usi

  • 对python中的six.moves模块的下载函数urlretrieve详解

    实验环境:windows 7,anaconda 3(python 3.5),tensorflow(gpu/cpu) 函数介绍:所用函数为six.moves下的urllib中的函数,调用如下urllib.request.urlretrieve(url,[filepath,[recall_func,[data]]]).简单介绍一下,url是必填的指的是下载地址,filepath指的是保存的本地地址,recall_func指的是回调函数,下载过程中会调用可以用来显示下载进度. 实验代码:以下载cifa

  • Python类的继承、多态及获取对象信息操作详解

    本文实例讲述了Python类的继承.多态及获取对象信息操作.分享给大家供大家参考,具体如下: 继承 类的继承机制使得子类可以继承父类中定义的方法,拥有父类的财产,比如有一个Animal的类作为父类,它有一个eat方法: class Animal(object): def __init__(self): print("Animal 构造函数调用!") def eat(self): print("Animal is eatting!") 写两个子类,Cat和Dog类,继

  • C++ 虚函数与纯虚函数代码详解

    目录 什么是虚函数: 虚函数的注意事项: 存虚函数 总结 什么是虚函数: 虚函数 是在基类中使用关键字 virtual 声明的函数,在C++ 语言中虚函数可以继承,当一个成员函数被声明为虚函数之后,其派生类中的同名函数都自动生成为虚函数, 虚函数主要体验C++的多态方面,(多态是参数个数和类型相同而实现功能不同的函数) 为了更好的里面虚函数请看下面的demo #include <iostream> #include <string> using namespace std; cla

  • 一些php项目中比较通用的php自建函数的详解

    以下一些php函数是我们it动力最常用的项目开发函数,这些函数还算是在比较多的项目中使用到的,也是比较通用的.1.请求接口的处理函数 复制代码 代码如下: /**  * curl访问程序接口  * @param string  * @return array  */  function getCurlDate($url, $datas, $key) {      $datas['time'] = $_SERVER['REQUEST_TIME'] + 300;      $post_data['p

随机推荐