虚函数实现多态的原理

***本文章从别处复制而来,作一个笔记学习,非常收益***

在C++里面,实现多态有两种方式:

动态多态,即以虚函数实现
静态多态:基于函数重载、模板实现
本期,主要讲解基于虚函数实现的动态多态。

多态:由基类指针,调用基类或者子类的虚成员函数
动态,即无法在编译期确定调用的是基类对象还是子类对象的虚成员函数vf,需要等待执行期才能确定。而将vf设置为虚成员函数,只需要在vf的声明前,加上virtual关键字。
比如,在下面的demo中,在成员函数print前面加上了virtual关键字,print函数就变成了虚成员函数。然后通过基类指针对象base来调用print函数,即base->print(),print最终输出的是 Base 还是 Derived取决于基类指针对象base指向的是基类对象还是子类对象。

class Base {
public:
  Base() = default;
  virtual 
  void print()       { std::cout <<"Actual Type:  Base" << std::endl; }
  void PointerType() { std::cout <<"Pointer Type: Base" << std::endl;}
  virtual ~Base()    { std::cout <<"base-dtor"<< std::endl;}
};

class Derived : public Base{
public:
  Derived() = default;
  void print()       { std::cout <<"Actual Type:  Derived" << std::endl; }
  void PointerType() { std::cout <<"Pointer Type: Derived" << std::endl;}
  ~Derived()         { std::cout <<"derived-dtor ";}
private:
   int random_{0};
};

int main(int argc, char const *argv[]) {
  Base* base = new Derived;  // base指向子类对象
  base->print();
  base->PointerType();
  delete base;

  std::cout<<"---"<<std::endl;

  base = new Base;          // base指向基类对象
  base->print();
  base->PointerType();
  delete base;
  return 0;
}
输出如下:

$ g++ virtual.cc  -o v && ./v
Actual Type: Derived
Pointer Type: Base
derived-dtor base-dtor
~~~
Actual Type: Base
Pointer Type: Base
base-dtor
从输出可以看出,只有加上了virtual关键字的print函数,才具有多态性质,即base->print调用的可能是基类的Base::print,也可能是子类的Derived::print,具有是哪个,由执行期base指向的对象确定。

而没有加上virtual关键字的PointerType函数,在编译期就能确定是Base::PointerType。

vtbl  & vptr
那么,编译器是如何让实现动态多态?

虚函数表(Virutal Function Table,vtbl)、虚函数指针(Virutal Function Pointer,vtpr)。

编译器会为每个存在虚函数(包括继承而来的虚函数)的类对象中插入一个指针vtpr,该vtpr指向存储虚函数的虚函数表vtbl。vtbl就是个数组,每个槽存储的都是虚函数的地址。

图片

以上面的Base、Derived类为例,他们都是空类,但是会因为vtpr的存在,对象大小变成一个指针大小(X64平台为8个字节)。

int main(int argc, char const *argv[]) {
    
  std::cout<< sizeof(Base)<< std::endl;
  std::cout<< sizeof(Derived)<< std::endl;
  return 0;
}
编译输出如下:

$ g++ virtual.cc  -o v && ./v
8
8
现在,我们知道了动态多态是通过vtpr、vtbl实现的。下面我们进一步讨论下,动态多态从编译到运行,哪些任务是在编译期完成,哪些任务是在执行期决议的。

我们仍然以上面的一段demo为例:

 Base* base = new Derived; 
 base->print();
base->print()会被编译器大致转换为:

(*base->vptr[0])(base)
vtpr是指向虚函数表的指针
0是虚函数print在vtbl中的索引
编译期确定:索引0在编译期就能确定,即在编译期就能确定待调用函数print在vtbl中位置,进而取出print的地址。这个索引,就是按照虚函数声明的顺序确定,比如print是第一个声明的,那么它在vtbl中的索引位置就是0。

执行期确定:真正在执行期才能确定的是base指向的对象,是 Base类的对象,还是 Derived类的对象。

「by the way」

转换后的结果中的第二个base,代表的是this指针,因为任何类的成员函数都是要转换为非成员函数,因此要在成员函数的第一个参数位置插入this指针。

这个现在不理解没关系,下一期的函数重载部分会详细讲解。

void PointerType();

// 转换后,会在第一个参数位置插入this指针
// 函数名也会经过name mangleing操作
void PointType__base(Base* this);
复现多态
下面,那我们就来获取具有虚函数的对象中的vtpr、vtbl,再来直接调用虚成员函数。

我们已经知道了vtbl是个表格数组,它的每个槽都存储的是虚函数的地址。在C/C++中,数组可以使用指针来表征,并且地址可以使用uintptr_t类型来表示。因此:

vtbl:vtbl的类型可以表达为uintptr_t*,表示vtbl是一个数组,数组的每个元素类型都是 uintptr_t;
vtpr:vtpr指向vtbl,因此 vtpr的类型是uintptr_t**,表示指针vtpr指向的类型是uintptr_t*。
另一方面,在GCC中,vtpr是被放置在内存模型中的第一个位置,即Derived对象的内存模型如下:

class Derived {
public:
 //...
    
private:  
  uintptr_t** vptr;
  int random_{0};
};
因此,vtpr的地址和Derived对象地址一致。因此,我们可以直接通过基类、子类对象来获取vtbl、vtpr并调用虚成员函数,更加详细地查看编译器的运行过程。

下面以虚成员函数print为例,通过 getVirutalFunc 函数来获取vtpr、vtbl进而调用print函数:

using FuncType = void (*)();  // print函数类型的 函数指针 

FuncType getVirutalFunc(Base* obj, uint64_t idx) { 

  uintptr_t** vptr  = reinterpret_cast<uintptr_t**>(obj);     // 1)先取出vtpr
  uintptr_t*  vtbl  = *vptr;                                  // 2)vptr指向的是vtbl,因此 vtbl 即 *vptr
  uintptr_t   func  = *vtbl;                                  // vtbl存储的第一个虚函数

  // 返回指定位置的虚函数
  return reinterpret_cast<FuncType>(func + idx);      // 3)      
}

int main(int argc, char const *argv[]) {
  Base* base = new Derived; 
  // 编译器完成调用 
  base->print();     
  // 我们自己调用
  auto print = getVirutalFunc(base, 0); // 指向print函数的函数指针
  print();  // 调用print函数

  delete base;
  return 0;
}
编译运行的输出:

$ g++ virtual.cc  -o v && ./v
Actual Type:  Derived
Actual Type:  Derived
derived-dtor base-dtor
我们来复盘下, getVirutalFunc 函数对应着编译器从编译到执行一个虚成员函数的过程,那getVirutalFunc函数的三步中哪些是在编译器完成的呢,哪些在执行期才能完成的呢?

编译期:很明显,getVirutalFunc函数的第二个参数idx在编译期就可以确定;
执行期:obj指向的是基类对象还是父类对象不确定。因此,根据obj取得的vtpr不知道是基类的还是子类的,这会对后续的vtbl产生影响。
为了验证这一观点,我们让Derived的构造函数输出Derived对象地址:

class Derived : public Base{
public:
  Derived() {std::cout<<"Derived: "<<this<<std::endl;};
 //...
}

int main(int argc, char const *argv[]) {
  Base* base = new Derived; 
  std::cout<<"base:    "<< base <<std::endl;

  base->print();
  getVirutalFunc(base, 0)(); // print()

  delete base;
  return 0;
}
输出如下:

$ g++ virtual.cc  -o v && ./v
Derived: 0x7fffc2c64eb0
base:    0x7fffc2c64eb0
Actual Type:  Derived
Actual Type:  Derived
derived-dtor base-dtor
发现什么没有?

main函数中创建的基类指针base指向的子类Derived对象的内存地址,这就使得 getVirutalFunc 函数中取得的vtpr、vtbl是子类的,那么就能调用子类的print函数,完成动态绑定。

我们再想一想,子类的vtbl和基类的vtbl真的不是同一个虚函数表vtbl吗?

回答这个问题很简单,只是需要输出子类和基类的vtbl地址即可。

class Base { 
public:
  Base() { Base::showVtbl(this, "Base   "); };
  virtual ~Base() =default;
    
  static void showVtbl(Base* obj, const char* type) { 
    uintptr_t** vptr  = reinterpret_cast<uintptr_t**>(obj);     
    uintptr_t*  vtbl  = *vptr;
    std::cout<<type<<"  vtbl: "<<vtbl<<std::endl;
  }
};

class Derived : public Base { 
public:
  Derived(){  Base::showVtbl(this, "Derived"); }
};

int main(int argc, char const *argv[]) {

  Derived derived{};
  return 0;
}
编译并执行输出:

$ g++ vir.cc -o v && ./v
Base     vtbl: 0x7fa2f3804d20
Derived  vtbl: 0x7fa2f3804cf8
从输出结果可以看出,确实不是一个。

override
最后,再提下C++11引入的关键字override。

在子类重写父类的虚函数vf时,可能会因为不小心导致子类重写的虚函数与基类的虚函数不完全一致,此时编译器会将写错(这里的错,是指函数名、参数等与基类的虚函数不一致)的函数,决议为新的函数,而不会报错,最终的结果是未预料的。

因此,为了确保子类重写的虚函数与基类的保持一致,C++11引入了override关键字,如果基类中没有这个虚函数,那么编译器就会报错。

比如下面的demo:

class Base { 
public:
  Base() = default;
  virtual ~Base() = default;

  virtual void func_1() { std::cout<<"base:::func_1"<<std::endl; }
  virtual void func_2(int i, double d) { std::cout<<"base:::func_2"<<std::endl; }
};

class Derived : public Base { 
public:
  Derived() = default;

  void func_1() override { std::cout<<"Derived::func_1"<<std::endl;}
  void func_2(int i, float f) override { std::cout<<"Derived::func_1"<<std::endl;}
};

int main(int argc, char const *argv[]) {
  Base* base = new Derived;
  delete base;
  return 0;
}
编译输出:

$ g++ vir.cc -o v && ./v
vir.cc:18:8: error: ‘void Derived::func_2(int, float)’ marked ‘override’, but does not override
   18 |   void func_2(int i, float f) override { std::cout<<"Derived::func_1"<<std::endl;}
      |        ^~~~~~
编译直接报错,基类中没有提供 func_2(int i, float f) 函数。

因此,一个良好的编码习惯,应该在每个子类重写的虚函数后,加上 override关键字,防止一些可预防的bug。
(https://imgconvert.csdnimg.cn/aHR0cHM6Ly9hdmF0YXIuY3Nkbi5uZXQvNy83L0IvMV9yYWxmX2h4MTYzY29tLmpwZw =30x30)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值