【C++学习笔记】虚函数和虚函数表

1、虚函数定义

什么是虚函数?

虚函数就是在函数声明时使用关键字virtual修饰的成员函数。

为什么使用虚函数?

在实现C++的多态时使用虚函数,使用虚函数的核心目的是通过基类访问派生类的函数。为了提高程序的可读性,建议后代的虚函数都加上virtual关键字。

虚函数是在基类中使用关键字virtual声明的成员函数,它允许派生类对其进行重写,实现运行时多态,当通过基类指针或引用调用虚函数时,实际调用的是对象类型对应的派生类中的函数。

代码示例:基类指针调用正常函数和虚函数的区别

#include<iostream>
using namespace std;


class Base{
  public:
  Base(){cout<<__func__<<__LINE__<<endl;}
  void func1(){cout<<"Base::func1"<<endl;}
  virtual void func2(){cout<<"Base::func2"<<endl;}
};


class Derived:public Base{
  public:
  Derived(){cout<<__func__<<__LINE__<<endl;}
  void func1(){cout<<"Derived::func1"<<endl;}
  void func2(){cout<<"Derived::func2"<<endl;}
  
};


int main()
{
  Base* pb = new base;//基类指针指向基类对象
  pb->func1();
  pb->func2();
  cout<<endl;
  
  pb = new derived;//基类指针指向派生类对象
  pd->func1();
  pb->func2();
  cout<<endl; 
  
  delete pb;
  
  return 0;
}

运行结果:

Base6

Base::func1

Base::func2

Base6

Derived13

Base::func1

Derived::func2

注意:

  1. 静态成员函数不能写为虚函数
    1. static成员不属于任何类对象或类实例,所以即使给此函数加上virtual也是没有任何意义的。
    2. 静态与非静态成员函数之间有一个主要的区别。那就是静态成员函数没有this指针。 虚函数依靠vptr和vtable来处理。vptr是一个指针,在类的构造函数中创建生成,并且只能用this指针来访问它,因为它是类的一个成员,并且vptr指向保存虚函数地址的vtable。  对于静态成员函数,它没有this指针,所以无法访问vptr. 这就是为何static函数不能为virtual
    3. 虚函数的调用关系:this -> vptr -> vtable ->virtual function
  2. 只有类的成员函数才能说明为虚函数
  3. 内联函数不能为虚函数
    1. 原因:机制冲突。内联函数需要在编译时就确定函数的实现,而虚函数是在运行时才能 确定。
  4. 构造函数不能为虚函数
    1. 虚函数的机制是对象构造过程中建立的,因此对象构造期间虚函数机制无法正常工作;并且构造函数的目的是初始化对象的状态,而不是实现多态性
  5. 析构函数可以写成虚函数
    1. 为了确保在删除派生类对象时正确地调用析构函数;如果析构函数不是虚函数, 基类指针只会调用基类的析构函数,无法析构派生类对象,可能会造成资源泄露或未能 正确释放资源。

代码示例:

#include <iostream>
using namespace std;


class base
{
  public:
  base()
  {
    cout<<__func__<<__LINE__<<endl;
  }
  
  virtual void func1()
  {
    cout<<"Base::func1"<<endl;
  }
  
  // 编译错误:static成员函数不能声明为virtual
  //virtual static void func2(){}
  
  virtual ~base()//虚析构函数
  {
    cout<<__func__<<__LINE__<<endl;
  }
};


class derived:public base
{
  public:
  derived()
  {
    cout<<__func__<<__LINE__<<endl;
  }
  
  virtual void show()
  {
    cout<<"derived class show"<<endl;
  }
  
  ~derived()
  {
    cout<<__func__<<__LINE__<<endl;
  }
};


int main()
{
  base*b = new derived;
  b->func1();
  delete b;//正确调用派生类析构函数
  return 0;
}

运行结果:

base9

derived31

derived::func1

~derived41

~base22

(基类析构函数是虚函数,先调用派生类析构函数,再调用基类析构函数)

如果基类析构函数不加virtual,输出:

base9

derived31

derived::func1

~base22

2、实现原理—虚函数表

虚函数的实现原理基于虚函数表。

虚表指针

每个使用虚函数的类(包括基类和派生类)都有一个虚函数表,该表是一个函数指针数组,存储了指向类的虚函数的指针,每个虚函数指针指向相应的虚函数地址,类的每个实例都包含一个指向其虚函数表的指针,虚表指针(在32位系统占用4个字节,64位系统占用8个字节)。

所以,当一个类存在虚函数,那么它就会多占用一个指针大小的内存。这个指针会存在类对象的最前面。

代码演示:

#include <iostream>
using namespace std;


class A_NULL{  //空类
  
};


class A{      //基类有一个虚函数
  public:
  virtual void func(){};
};


class B:public A{//派生类,继承了基类的虚函数
  
};


int main()
{
  A_NULL pan;
  A pa; 
  B pb;
  cout<<sizeof(pan)<<endl;
  cout<<sizeof(pa)<<endl;
  cout<<sizeof(pb)<<endl;
  return 0;
}

运行结果:

1

8

8

虚函数表

虚表是在编译时创建的,并且对于每个类只有一个。

虚表是类的元数据,它记录了该类的虚函数及其位置,当类的对象被创建时,一个指向虚表的指针会添加到对象的内存布局中。

当派生类重写基类的虚函数时,派生类的虚函数表中相应位置的函数指针会被更新为指向派生类的函数。如果派生类没有重写虚函数,则派生类的虚函数表中会保留指向基类虚函数的指针

虚表指针:在含有虚函数的类实例化对象时,对象地址的前四个字节存储的指向虚表的指针,它是在构造函数中被初始化的。

#include<iostream>
using namespace std;


class Base{
  public:
  virtual void func1(){cout<<"Base::func1"<<endl;}
  virtual void func2(){cout<<"Base::func2"<<endl;}


  private:
  int num;
};


class Derived : public Base{
  public:
  virtual void func2() override{cout<<"Derived::func2"<<endl;}
  virtual void func3(){cout<<"Derived::func3"<<endl;}


  private:
  int score;
};


//定义一种 返回值为void,形参为void,这种类型的函数指针
//并为这种指针取别名为PFUN
//typedef 返回值类型 (*别名)(形参类型);
typedef void(*PFUN)(void);//定义函数指针类型


int main()
{
  Base pb;
  Derived pd;
  //获取虚表指针,类对象第一个指针大小的内存里的值
  unsigned long* vptr_Base=(unsigned long*)(*((unsigned long*)(&pb)));
  unsigned long* vptr_Derived=(unsigned long*)*(unsigned long*)&pd;
  //打印虚函数表各个虚函数的地址
  cout<<"虚表指针:"<<vptr_Base<<endl;
  for(int i=0;i<2;i++)
  {
    cout<<"Base虚函数表第"<<i+1<<"个虚函数地址:"<<*vptr_Base<<endl;
    PFUN pfun=(PFUN)*vptr_Base;
    pfun();
    vptr_Base++;
  }
  cout<<endl;
  cout<<"虚表指针:"<<vptr_Derived<<endl;
  for(int i=0;i<3;i++)
  {
    cout<<"Derived虚函数表第"<<i+1<<"个虚函数地址:"<<*vptr_Derived<<endl;
    PFUN pfun=(PFUN)*vptr_Derived;
    pfun();
    vptr_Derived++;
  }
  return 0;
}

输出:

虚表指针:0x400ec0

Base虚函数表第1个虚函数地址:4197514

Base::func1

Base虚函数表第2个虚函数地址:4197558

Base::func2

虚表指针:0x400e98

Derived虚函数表第1个虚函数地址:4197514

Base::func1

Derived虚函数表第2个虚函数地址:4197602

Derived::func2

Derived虚函数表第3个虚函数地址:4197646

Derived::func3

3、虚函数的重写

虚函数重写是面向对象编程中实现多态性的一种方式。

虚函数允许派生类根据需要改变或扩展基类中的行为。

一旦某个函数在基类被声明成虚函数,则在所有派生类中它都是虚函数

  1. 定义
    1. 虚函数重写指的是派生类中提供一个函数,该函数的与基类中具有相同名称、相同返回类型、相同参数列表的虚函数相匹配,const属性也与基类的虚函数声明一致。通过这种方式,派生类可以提供自己特定的实现,替换,扩展基类的行为。
  2. 规则
    1. 函数名匹配:要重写基类中的虚函数,派生类中的基函数必须具有相同的名称、返回类型、参数列表。
    2. 基类函数必须是虚函数:只有虚函数可以被重写、如果基类中的函数不是虚函数,派生类中相同名字的函数会隐藏(而非重写)基类中打函数。
    3. 访问权限可以不同:虚函数在派生类中的访问级别(public、protected、private)可以与基类中的不同,但这会影响到函数的访问性。
    4. 使用override关键字,C++11及以上,以告诉编译器,我们写的这个函数是为了重写基类的虚函数,如果 函数名,参数列表,const属性,返回值 这些不一致,就给我报错
  3. 注意
    1. 构造函数不能是虚函数
      • 因为构造函数是用来创造对象的,而虚函数的调用需要通过对象的虚函数表,这在对象构造阶段还未完全建立。
    1. 析构函数应该是虚的
      • 如果一个类有可能被继承,并且通过基类指针来删除派生类对象,那么基类的析构函数应该是虚的。
    1. 可以使用final关键字
      • 阻止类的进一步派生,和虚函数的进一步重写。
#include <iostream>
using namespace std;


class base
{
  public:
  base()
  {
    cout<<__func__<<__LINE__<<endl;
  }
  
  virtual void show() const
  {
    cout<<"Base::show"<<endl;
  }
  
  virtual void finalfunc() const final
  {
    cout<<"Base::finalfunc"<<endl;
  }
  
  virtual ~base()//虚析构函数
  {
    cout<<__func__<<__LINE__<<endl;
  }
};


class derived:public base
{
  public:
  derived()
  {
    cout<<__func__<<__LINE__<<endl;
  }
  
  void show() const override//使用override确保正确重写
  {
    cout<<"derived::show"<<endl;
  }
  //virtual void finalfunc() const{}//基类指定为final了,重写会报错
  
  ~derived()
  {
    cout<<__func__<<__LINE__<<endl;
  }
};


int main()
{
  base*b = new derived;
  b->show();
  b->finalfunc();
  delete b;//正确调用派生类析构函数
  return 0;
}

输出:

base9

derived33

derived::show

Base::finalfunc

~derived44

~base24

4、基类和派生类的虚函数表

  1. 基类:在基类中,编译器会为其创建一个虚函数表,这个表包含了基类中所有虚函数的地址。如果派生类没有覆盖(重写)这些虚函数,派生类对象的虚函数表会复制基类虚函数表中相应的地址。
  2. 派生类::当派生类覆盖(重写)基类中的虚函数时,派生类的虚函数表中对应位置的函数指针会被更新为指向派生类中的函数实现。如果派生类引入了新的虚函数,这些新的虚函数也会被加入到派生类的虚函数表中。
  3. 多重继承:在多重继承的情况下,每个基类都会有自己的虚函数表。派生类对象会包含多个虚函数表指针,每个指针指向对应基类的虚函数表。如果派生类覆盖了某个基类的虚函数,那么相关基类虚函数表中的条目会被更新为指向派生类中的实现。
#include <iostream>
using namespace std;


class base
{
  public:
  virtual void func1(){cout<<"base func1"<<endl;}
  virtual void func2(){cout<<"base func2"<<endl;}
  virtual ~base(){cout<<__func__<<__LINE__<<endl;}
};


class derived:public base
{
  public:
  virtual void func1(){cout<<"derived func1"<<endl;}
  virtual void func3(){cout<<"derived func3"<<endl;}
  ~derived(){cout<<__func__<<__LINE__<<endl;}
};


int main()
{
  base*b = new derived();
  b->func1();
  b->func2();
  delete b;
  return 0;
}

输出:

derived func1

base func2

~derived17

~base9

int main()
{
  base*b = new derived();
  derived d;
  b->func1();
  b->func2();
  d->func3();
  delete b;
  return 0;
}

输出:

derived func1

base func2

derived func3

~derived17

~base9

~derived17

~base9

  • base类有自己的虚函数表,包含func1,func2
  • derived类有自己的虚函数表,其中func1的条目会被更新为指向derived::func1,func2保持不变,并且会添加一个新的条目指向func3.

5、纯虚函数

  • 纯虚函数是在基类中声明但不实现的虚函数,其声明方式是在函数声明的结尾处添加=0.
  • 类中如果包含至少一个纯虚函数,则该类成为抽象类,不能实例化对象。
  • 纯虚函数的主要作用是定义接口规范,强制要求派生类必须实现这些函数,从而实现接口的同一和标准化。
#include <iostream>
using namespace std;


class base
{
  public:
  base(){cout<<__func__<<__LINE__<<endl;}
  virtual void func1() = 0;//纯虚函数
  virtual ~base(){cout<<__func__<<__LINE__<<endl;}
};


class derived:public base
{
  public:
  derived(){cout<<__func__<<__LINE__<<endl;}
  virtual void func1(){cout<<"hello world"<<endl;}
  ~derived(){cout<<__func__<<__LINE__<<endl;}
};


int main()
{
  base*b = new derived();
  b->func1();
  delete b;
  return 0;
}

输出:

base7

derived15

hello world

~derived17

~base9

6、抽象类

包含纯虚函数的类被称为抽象类。

抽象类是一种特殊的类,它是为了抽象和设计的目的建立的,它处于继承层次结构的交上曾。

  1. 作用
    • 抽象类的主要作用是将有关的操作作为结果接口组织在一个继承层次结构中, 由它来为派生类提供一 个公共的根,派生类将具体实现在其基类中作为接口的操作。所以派生类实际上刻画了一组子类的操 作接口的通用语义,这些语义也传给子类,子类可以具体实现这些语义,也可以再将这些语义传给自 己的子类。
  1. 注意
    • 抽象类只能作为基类来使用, 其纯虚函数的实现由派生类给出。 如果派生类中没有重新定义纯虚 函数,而只是继承基类的纯虚函数,则这个派生类仍然还是一个抽象类。如果派生类中给出了基 类纯虚函数的实现,则该派生类就不再是抽象类了,它是一个可以建立对象的具体的类。
    • 抽象类是不能定义对象的。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值