C++ 虚函数 virtual
虚函数是一种成员函数,在基类中声明(delcare),在派生类中覆写(override).当用基类类型的指针或引用指向派生类对象时,这样就可以调用这个对象的虚函数,然后执行派生类的函数.
- 用来实现运行时多态(runtime polymorphism)
- 在基类中把关键字virtual写在函数的前面
- 运行时确定所调用的函数
使用虚函数的规则
- 虚函数不可以是static类型
- 虚函数可以是另一个类的friend 函数
- 应该使用基类类型的指针或引用来访问虚函数,从而达到运行时多态的目的
- 派生类中的虚函数原型应该和基类中的相同.
- 通常都是在基类中声明一个虚函数,然后在派生类中进行覆写(overide).当然派生类不是必须得进行覆写,如果没覆写,调用的就是基类中的函数.
- 一个类中,或许它的析构函数是虚函数,但是它的构造函数一定不是虚函数.
虚函数的编译时绑定和运行时绑定行为
参考下面的代码,展示了虚函数的运行时绑定和运行时绑定
// filename: virtual_function.cpp
// CPP program to illustratem 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;
base& bref = d;
// virtual function, binded at runtime
bptr->print();
bref.print();
d.print();
// Non-virtual function, binded at compile time
bptr->show();
bref.show();
return 0;
}
输出
$ ./virtual_function
print derived class
show base class
解释
只有使用指向基类的指针或者引用时才能体现运行时动态绑定的特性。基类类型的指针既可以指向基类,也可以指向派生类。在上述代码中,指针'bptr'包含着派生类对象’d‘的地址。
运行时绑定依照指针的内容,编译时绑定依照指针的类型。因为print()函数声明时带有virtual关键字,所以它是在运行时绑定。而show()不是虚函数,所以它是编译时绑定.
注意
如果我们在基类中创建了虚函数并在派生类中进行了覆写(override),那么在派生类中就不需要再写virtual关键字,在派生类中这个函数会自动被认为是虚函数。
虚函数的原理,虚表(VTABLE)和虚指针(VPTR)
如果一个类中有虚函数,编译器会做两件事:
- 当这个类创建出一个对象时,虚指针(VPTR)会被添加到类的数据成员中,虚指针(VPTR)会指向这个类的虚表(VTABLE).每当一个对象创建出来,一个新的虚指针(VPTR)就会添加到类的数据成员中。
- 不论对象是否被创建,都存在一张虚表(VTABLE),虚表(VTABLE)是函数指针组成的静态数组,每一个数组元素中包含的是这个类中虚函数的地址。
参考下面的示例
// filename: vtable_vptr.cpp
// CPP program to illustrate working of Virtual Functions
#include <iostream>
using namespace std;
class base {
public:
void func_1() {
cout << "base-1" << endl;
}
virtual void func_2() {
cout << "base-2" << endl;
}
virtual void func_3() {
cout << "base-3" << endl;
}
virtual void func_4() {
cout << "base-4" << endl;
}
};
class derived : public base {
void func_1() {
cout << "derived-1" << endl;
}
void func_2() {
cout << "derived-2" << endl;
}
void func_4(int x) {
cout << "derived-4" << endl;
}
};
int main()
{
base* p;
derived obj1;
p = &obj1;
// Early binding because func1() is non-virtual
// in base
p->func_1();
// Late binding (RTP)
p->func_2();
// Late binding (RTP)
p->func_3();
// Late binding (RTP)
p->func_4();
// Early binding but this function call is
// illegal(produces error) becasue pointer
// is of base type and function is of
// derived class
// p->func_4(5)
return 0;
}
/*
********** p->func_4(5); ***********
g++ vtable_vptr.cpp -o vtable_vptr
vtable_vptr.cpp: In function ‘int main()’:
vtable_vptr.cpp:55:16: error: no matching function for call to ‘base::func_4(int)’
55 | p->func_4(5)
| ^
vtable_vptr.cpp:16:18: note: candidate: ‘virtual void base::func_4()’
16 | virtual void func_4() {
| ^~~~~~
vtable_vptr.cpp:16:18: note: candidate expects 0 arguments, 1 provided
*/
输出
$ ./vtable_vptr
base-1
derived-2
base-3
base-4
解释
一开始我们给一个基类类型的指针赋了派生类对象的地址。当我们创建出一个派生类的对象,编译器创建出一个指针作为类的数据成员,这个指针指向派生类的VTABLE.
上述示例中我们同样使用了运行时绑定和编译时绑定的概念。对于函数func_1(),调用的是基类的版本;func_2()在子类中覆写(override)了,所以调用的是子类的版本;func_3()是虚函数但是没有被覆写(override),所以调用的是基类的版本;func_4()也没有被覆写(override)所以调用的也是基类的版本。
注意
派生类中的func_4(int)和虚函数func_4()是不同的,因为它们的原型不一致。i此处发生的是重载(overload).
This article is contributed by Yash Singla.