深入探究虚函数 (一)

在实现C++多态时会用到虚函数。使用虚函数目的是通过基类指针访问派生类定义的函数。

典型使用:

1
2
Base* ba = new Derive();
Derive *de = ba;

反射性地会说ba是个基类指针,指向派生类对象。但是运行程序竟然报错了: error: C2440: “初始化”: 无法从Base * 转换为 Derive * 从基类型到派生类型的强制转换需要 dynamic_cast 或 static_cast 。也就是需要向下转型,为什么?ba不是指向派生类对象吗?

这就是虚函数的动态绑定, 在运行期才指向派生类对象,而在编译期还是基类指针,不能赋给de,语法检查就过不了,当然报错了。

我们一般说ba的静态类型是Base*,动态类型是Derive*。如果第一行改为Base* ba,那么静态类型是Base*,没有动态类型。

  • 所谓静态多态是指通过模板或者函数重载实现的多态,其在编译期确定行为。动态多态是指通过虚函数技术实现在运行期动态绑定的技术。

  • 普通函数在编译时就确定了函数地址,但虚函数在运行时查询虚函数表,所以效率较低。

  • 要正确实现虚函数,只能通过基类的指针或引用来指向派生类对象

  • 派生类重新实现基类中的虚函数,这就叫覆盖

  • 成员虚函数不能有同名同参数的静态函数

  • 多个虚函数对sizeof的计算相当于一个,因为它们都存在虚函数表里,虚函数表只对应一个虚指针,所以不会扩大占用内存

纯虚函数

基类有纯虚函数时,就是抽象类,抽象类无法实例化,也无法声明指针。派生类可以不实现这个纯虚函数,但这样就没有意义了。 基类中的纯虚函数其实也可以实现,但没有必要,因为派生类都会重新实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
virtual void testPureVirtual() = 0;	 // 基类中声明纯虚函数

void Base::testPureVirtual() // 基类中实现纯虚函数
{
cout<<"base pure virtual test "<<endl;
}

void Derived::testPureVirtual() // 派生类中实现虚函数
{
Base::testPureVirtual();
cout<<"derived pure virtual test"<<endl;
}
Derived d;
d.testPureVirtual();

这里的代码在派生类中调用了基类中的纯虚函数,由于抽象类无法实例化,所以基类纯虚函数是不能直接调用的。

不要重新定义继承而来的虚函数的默认参数值

绝对不要重新定义一个继承而来的virtual函数的缺省参数值,因为缺省参数值都是静态绑定(为了执行效率),而virtual函数却是动态绑定。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class Base
{
public:
Base();
virtual ~Base();
virtual void test_virtual(int i=0); // 默认值为0
};
void Base::test_virtual(int i)
{
cout<<"base virtual "<<i<<endl;
}

class Derive : public Base
{
public:
Derive();
void test_virtual(int i=1); // 默认值改为1
};
void Derive::test_virtual(int i)
{
cout<<"derive virtual "<<i<<endl;
}

还是那个常用的调用:

1
2
Base* ba = new Derive();
ba->test_virtual();

结果竟然是:
1
derive virtual  0   // 函数为派生类的,默认值为基类的

原因在于虚函数是动态绑定的,但参数是静态绑定的, 覆盖虚函数时只能覆盖动态绑定的部分,所以不能改变参数。虚函数的效率较差,如果对参数也进行动态绑定,那么对执行速度和编译器简易度不利,所以C++做了分开的处理。

不能声明为虚函数的函数

构造函数

首先明确一点,在编译期,编译器完成了虚表的创建,而虚指针在构造函数期间被初始化

如果构造函数是虚函数,那必然需要通过虚指针来找到虚构造函数的入口地址,但是这个时候我们还没有把虚指针初始化。因此,构造函数不能是虚函数。

內联函数

编译期內联函数在调用处被展开,而虚函数在运行时才能被确定具体调用哪个类的虚函数。內联函数体现的是编译期机制,而虚函数体现的是运行期机制。 二者不是同一范畴的东西。

静态成员函数

静态成员函数和类有关,即使没有生成一个实例对象,也可以调用类的静态成员函数。而虚函数的调用和虚指针有关,虚指针存在于一个类的实例对象中,如果静态成员函数被声明成虚函数,那么调用成员静态函数时又如何访问虚指针呢。也就是说,静态成员函数与类有关,而虚函数与类的实例对象有关。 二者也不是同一范畴的东西。

多态

多态存在的3个必要条件:

  1. 继承
  2. 函数的重写
  3. 父类引用或指针指向子类对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class B0    //基类BO声明
{
public: //外部接口
virtual void display() //虚成员函数
{
cout << "B0 display0" << endl;
}
};
class B1 :public B0
{
public:
void display() { cout << "B1 display0" << endl; }
};

// 尝试形参改为 B0& ptr 或 B0* ptr
void fun(B0 ptr) //普通函数
{
ptr.display();
}
B0 b0; //声明基类对象
B1 b1; //声明派生类对象
fun(b0); //调用基类B0函数成员
fun(b1); //调用派生类B1函数成员

运行结果是B0 display0 B0 display0
虚函数的动态绑定仅在 基类指针或引用 绑定派生类对象时发生,fun的形参不是指针而是对象,所以调用哪个版本的函数在编译时就已经确定。

参考:
C++虚函数表剖析
深入C++对象模型&虚函数表