C++中的虚函数通俗解释和用法示例

2021年3月27日17:49:13 发表评论 1,193 次浏览

虚函数是在基类中声明的成员函数, 并由派生类重新定义(重写)。当使用指针或对基类的引用来引用派生类对象时, 可以为该对象调用虚拟函数并执行该派生类的函数版本。

  • 虚函数可确保为对象调用正确的函数, 而不管用于函数调用的引用(或指针)的类型如何。
  • 它们主要用于实现运行时多态
  • 函数用虚拟基类中的关键字。
  • 函数调用的解析在运行时完成。

虚函数规则

  1. 虚函数不能是静态的, 也不能是另一个类的朋友函数。
  2. 应该使用基类类型的指针或引用来访问虚拟函数, 以实现运行时多态。
  3. 虚拟函数的原型在基类和派生类中都应相同。
  4. 它们始终在基类中定义, 而在派生类中被覆盖。不必强制派生类重写(或重新定义虚拟函数), 在这种情况下, 将使用函数的基类版本。
  5. 一堂课可能有虚拟析构函数但它不能具有虚拟构造函数。

虚函数的编译时(早期绑定)与运行时(后期绑定)行为

考虑以下显示虚函数运行时行为的简单程序。

// CPP program to illustrate
// concept of Virtual Functions
  
#include <iostream>
using namespace std;
  
class base {
public :
     virtual void print()
     {
         cout << "print base class" << endl;
     }
  
     void show()
     {
         cout << "show base class" << endl;
     }
};
  
class derived : public base {
public :
     void print()
     {
         cout << "print derived class" << endl;
     }
  
     void show()
     {
         cout << "show derived class" << endl;
     }
};
  
int main()
{
     base* bptr;
     derived d;
     bptr = &d;
  
     // virtual function, binded at runtime
     bptr->print();
  
     // Non-virtual function, binded at compile time
     bptr->show();
}

输出如下:

print derived class
show base class

说明:运行时多态只能通过基类类型的指针(或引用)来实现。同样, 基类指针可以指向基类的对象以及派生类的对象。在上面的代码中, 基类指针" bptr"包含派生类的对象" d"的地址。

后期绑定(运行时)是根据指针的内容(即指针指向的位置)完成的, 而早期绑定(编译时)是根据指针的类型完成的, 因为print()函数是用virtual关键字声明的, 因此将在运行时绑定(输出为打印派生类因为指针指向派生类的对象), 并且show()是非虚拟的, 所以它将在编译时绑定(输出为显示基类因为指针是基本类型)。

注意:如果我们在基类中创建了虚函数, 并且在派生类中对其进行了覆盖, 那么在派生类中就不需要virtual关键字, 那么在派生类中, 函数会自动视为虚函数。

虚函数的工作(VTABLEVPTR的概念)

如前所述这里, 如果一个类包含一个虚函数, 那么编译器本身会做两件事:

  1. 如果创建了该类的对象, 则虚拟指针(VPTR)作为类的数据成员插入, 以指向该类的VTABLE。对于创建的每个新对象, 将插入一个新的虚拟指针作为该类的数据成员。
  2. 不管对象是否被创建, 功能指针的静态数组, 称为VTABLE其中每个单元格包含该类中包含的每个虚函数的地址。

考虑下面的示例:

C++中的虚函数1
// CPP program to illustrate
// working of Virtual Functions
#include <iostream>
using namespace std;
  
class base {
public :
     void fun_1() { cout << "base-1\n" ; }
     virtual void fun_2() { cout << "base-2\n" ; }
     virtual void fun_3() { cout << "base-3\n" ; }
     virtual void fun_4() { cout << "base-4\n" ; }
};
  
class derived : public base {
public :
     void fun_1() { cout << "derived-1\n" ; }
     void fun_2() { cout << "derived-2\n" ; }
     void fun_4( int x) { cout << "derived-4\n" ; }
};
  
int main()
{
     base* p;
     derived obj1;
     p = &obj1;
  
     // Early binding because fun1() is non-virtual
     // in base
     p->fun_1();
  
     // Late binding (RTP)
     p->fun_2();
  
     // Late binding (RTP)
     p->fun_3();
  
     // Late binding (RTP)
     p->fun_4();
  
     // Early binding but this function call is
     // illegal(produces error) becasue pointer
     // is of base type and function is of
     // derived class
     // p->fun_4(5);
}

输出如下:

base-1
derived-2
base-3
base-4

说明:最初, 我们创建一个基类类型的指针, 并使用派生类对象的地址对其进行初始化。当我们创建派生类的对象时, 编译器将创建一个指针, 作为包含该派生类的VTABLE地址的类的数据成员。

类似的概念后期绑定如上例所示。对于fun_1()函数调用, 该函数的基类版本被调用, fun_2()在派生类中被覆盖, 因此派生类版本被调用, fun_3()在派生类中不被覆盖, 它是虚函数, 因此该基类版本被调用, 同样, fun_4()不会被覆盖, 因此将调用基类版本。

注意:派生类中的fun_4(int)与基类中的虚拟函数fun_4()不同, 因为这两个函数的原型都不同。

如果发现任何不正确的地方, 或者想分享有关上述主题的更多信息, 请写评论。

被认为是行业中最受欢迎的技能之一, 我们拥有自己的编码基础C++ STL通过激烈的问题解决过程来训练和掌握这些概念。

木子山

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: