C 语言

关注公众号 jb51net

关闭
首页 > 软件编程 > C 语言 > C++ 虚函数

深入理解C++中的虚函数机制

作者:SeeDoubleU

本文描述了C++中的虚函数机制,以及类模板和函数模板中是否能使用虚函数,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧

多态

要了解虚函数机制,首先要了解多态。多态是指同一方法调用在不同对象上表现出不同的行为。它是面向对象编程的核心特性之一。

c++中的多态性包含静态多态、动态多态。
静态多态在编译阶段进行决议,常见的实现方式有重载、模板;
动态多态在运行阶段进行决议,常见的实现方式是重写。

虚函数

虚函数是被virtual关键字所修饰的类的成员函数,通过虚函数可以实现重写,以此来完成动态多态。

通过函数重写来实现多态:

// 基类
class Base {
public:
    Base() {}

    virtual void test(int x) {}  // 基类虚函数
};

// 派生类
class Derive : public Base {
public:
    Derive() {}

    void f1(int x) {}  // 子类中重写基类的虚函数,子类函数自动继承虚函数特性
};

// 调用
Base * base_class = new Derive();  // 基类指针持有子类对象
base_class->test(10);  // 调用 Derive::test(int),会根据实际类型调用对应函数,实现多态

当某一个类的成员函数被定义为虚函数,则由该类派生出来的所有子类中,与该函数同名,同参,同返回值类型(或返回类型协变)的函数也会保持虚函数的特征。

静态成员函数不能是虚函数,因为静态成员函数是所有该类类型对象所共有的,不会有多态特性。
内联函数不能是虚函数,因为内联函数在每个实例中都有一个拷贝,无映射关系,不会有多态特性。
构造函数不能是虚函数,因为在调用构造函数时对象还没有完成实例化。
析构函数一般会定义为虚函数,因为其子类析构时需要通过多态调用自身的析构函数。

虚函数表指针

虚函数表指针(vfptr,virtual fun point table)的生成条件——只要类中有虚函数,那么该类的实例化对象的内存布局中就会创建一个隐含的成员,即虚函数表指针,且该类的派生类对象也会创建自身的虚函数表指针。

无论虚函数有多少个,虚函数表指针只有一个。

虚函数表

虚函数表指针指向的就是虚函数表(virtual table),又叫虚表,虚表存放在内存中的只读数据段。

虚函数表的构造发生在代码的编译阶段。
多态发生时,即调用时选择哪个表由对象的运行时类型决定。

虚函数表中包含以下内容:

  1. 不同虚函数表的偏移量;(只在多重继承时使用)
  2. RTTI运行时类型信息;(用来确定对象的运行时类型)
  3. 类中所有虚函数的函数地址。(用来确定需要调用的函数入口)

子类在生成自身的虚函数表的时候,会用自身重写的函数替换掉基类中的虚函数。
所有同类对象共享一张虚表。

通过虚函数表实现多态

示例:

// 基类
class Base {
public:
    Base() {}

    virtual void f1(int x) {}  // 基类虚函数
    virtual void f2(int x) {}  // 基类虚函数
};

// 派生类
class Derive : public Base {
public:
    Derive() {}

    void f1(int x) {}  // 子类中重写基类的虚函数,子类函数自动继承虚函数特性
};

// 调用
Base * base_class = new Derive();  // 基类指针持有子类对象
base_class->f1(10);  // 调用 Derive::test(int),会根据实际类型调用对应函数,实现多态
base_class->f2(10);  // 调用 Base::test(int)

在编译阶段:

  1. 编译器看到virtual关键字,给类生成一张虚函数表;
  2. 基类 Base 生成虚函数表,在表中填入基类的虚函数地址&Base::f1、&Base::f2;
  3. 子类 Derive 生成自己的虚函数表,包含自身的虚函数以及基类的虚函数,使用自身重写的函数 &Derive::f1替换&Base::f1,而f2未被重写,沿用父类&Base::f2;
  4. 给类增加隐藏成员vptr虚函数表指针;
  5. 对象构造的时候,构造函数把vptr虚函数表指针赋值为本类的虚函数表地址。

在运行时阶段:

  1. 实例对象调用函数f1;
  2. 从对象Base * base_class的内存起始位置取出vptr虚函数表指针;
  3. 通过vptr虚函数表指针找到虚函数表;
  4. 根据函数f1查找虚函数表中,获取需要调用的函数指针;
  5. 跳转调用这个函数指针指向的函数。

通过上述的流程,完成运行时多态。

类模板的成员函数可以是虚函数(模板在类上)

模板是在编译期阶段生成类,所以实例化出来的具体类,就和普通类一样,可以有虚函数、参与多态、产生虚函数表:

template class<T>
class Base
{
public:
  virtual void f1() {}  //虚函数
  virtual void f2() = 0 {}  //纯虚函数
};

Base<T>本身不是一个类,是类模板;实例化后的Base<int>才是真正的类,才有虚函数表。
Base<int>和Base<string>是两个互不相关的类型,两张独立虚函数表。

成员函数模板不能是虚函数(模板在函数上)

虚函数表需要在编译期就确定,而函数模板的具体类型是在运行时产生了实例之后才确定的,函数模板可以被实例化成无数种,每实例化一次就生成一份全新函数,所以编译器无法预知会实例化多少版本,虚函数表无法预先确定。

虚表是固定大小数组;模板实例化数量是无限可能,二者矛盾。
虚表编译期就要定好全部函数指针;函数模板实例化可以发生在任意地方,编译器预判不了全部实例。

template<typename T>  // 无论类是不是模板类
class Base
{
public:
    template<typename U>
    virtual void test(U u) {} // 编译报错,函数模板不能使用 virtual
};

正确的类模板,带成员函数模板的实现:

template<typename T>  // 无论类是不是模板类
class Base
{
public:
    template<typename U>
    void test(U u) {}  // 正确,函数模板未使用 virtual
};

// 调用
Base<int> b_class;  // 类定义时,已经可以确定类模板的具体类型了(虚表是属于类的)
b_class.test<string>("haha");  // 实例对象,无法通过实例来确定函数模板类型(虚表不是属于对象的)

到此这篇关于深入理解C++中的虚函数机制的文章就介绍到这了,更多相关C++ 虚函数内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

您可能感兴趣的文章:
阅读全文