C++的虚函数是其实现多态的基础,今天在这里分享一下我对C++虚函数相关知识的系统总结,技术有限,如有不当,欢迎指正。
在将内容前,将大致涉及到的内容图解如下:
1. 有无虚函数在继承中的区别
//-- Zuo add on 2018-04-07
class A
{
public:
virtual void fun(){ std::cout << "A::fun"; }
void fun1(){ std::cout << "A::fun1"; }
};
class B : public A
{
public:
virtual void fun(){ std::cout << "B::fun"; }
void fun1(){ std::cout << "B::fun1"; }
};
void main(char argc, char** argv)
{
A *a = new A;
A *b = new B;
a->fun();
b->fun();
a->fun1();
b->fun1();
}
输出结果:
A::fun
B::fun
A::fun1
A::fun1
可以看到,如果是虚函数,在继承的时候,如果子类有重写,就会被覆盖(由子类的实现代替父类的实现),如果是普通成员函数,就不会被子类的实现覆盖,这也就是多态的原型。
例子很简单,想必大家很容易看懂,但是这里我提出两个问题大家思想下,后面会有答案:
1.1. 如果将上文的A *a = new B;
修改成 B *a = new B;
结果会怎样变化?
1.2. 如果虚函数有缺省值,缺省值会有什么特点?
2. 虚函数的本质
虚函数的本质是因为C++给有虚函数的类增加了一张虚函数表,用来存储所有虚函数的入口地址。在子类继承父类的过程中,首先继承的是这张虚函数表Virtual-Table(以下简称VT),如果子类有对父类虚函数的重写,那么就会在VT中覆盖对应的函数地址。这样就可以实现同样的调用,在不同的子类里,有不同的实现,这也就是多态。
注意一个问题,VT是跟着类走的,也就是说,如果是上文中1.1提到的,那么VT的覆盖就不会发生,因为A *a = new B;
等价于A *a = (A*)new B;
正因为VT是跟着类走的,如果是B *a = new B;
,那就无任何特别,普通的创建对象,如果是A *a = new B;
,那在将B类转换成A类的时候,也就是VT合并的时候。所以上文的1.1,输出值就和class A
毫无关系了。
除了上面讲到的,还有两点特性:
2.1. 只有虚函数的入口地址才会被存储在VT中,如果是普通成员函数,当然不会存储在里面。
2.2. 为了提高虚函数的调用效率,VT的地址被存放在类的最前面。
我从网上找了一张图比较明了:
在继承的过程在VT被子类重写的虚函数地址覆盖父类的虚函数地址,也就是同一个指针在不同的对象中可以指向不同的函数实现,这也就是虚函数的动态绑定实现多态的过程了。这个可以和普通函数的静态绑定相对比,普通函数是在编译期就静态绑定了,而虚函数是在运行期通过VT存储的函数地址实现动态绑定。
3. 纯虚函数
纯虚函数是一种比虚函数更加极端的函数。它的形式如下:
virtual void fun() = 0;
它存在的目的是为了规范接口,使得子类必须要实现对应的接口,如果子类没有实现接口,则会编译报错。
使用纯虚函数要注意一点,包含纯虚函数的类被称为抽象类,一般被设计为基类,且抽象类不能被实例化(因为有未实现的纯虚函数)。
4. 安全性-访问non-public虚函数
VT的存在固然为实现C++的多态立下汗马功劳,但是凡事都有双面性,它的到来,也引入了C++的一些安全上的不足。上文我们说到了,为了提高多态调用的性能,C++将VT地址存放在类空间的段首位置,所以我们通过获取类的地址可以找到VT的地址,也就是可以得到一个类所有虚函数的地址,那如果,这里面的虚函数有是non-public的,那就破坏了C++的封装属性了。Show Code:
//-- Zuo add on 2018-04-07
class A
{
private:
virtual void fun(){ qDebug() << "A fun"; }
};
void main(char argc, char** argv)
{
A *a = new A;
typedef void(*fun)(void);
std::cout << "虚函数表地址 = " << (int*)(a) << std::endl;
std::cout << "第一个虚函数地址 = " << (int*)(*(int*)a << std::endl;
//-- 将虚函数地址转换为void fun(void)函数指针
fun f = (fun)*((int*)(*(int*)a));
f();
}
可以看到,这里可以无报错的访问到原本为private的虚函数。
5. 虚函数的缺省值不能被覆盖
虚函数虽然可以被子类所覆盖以形成多态,但是有一个细节还是要注意,虚函数的缺省值是不能被覆盖的,还是上面的代码:
//-- Zuo add on 2018-04-07
class A
{
public:
virtual void fun(int a = 1){ std::cout << "A::fun && a = " << a; }
};
class B : public A
{
public:
virtual void fun(int a = 2){ std::cout << "B::fun && a = " << a; }
};
void main(char argc, char** argv)
{
A *a = new A;
A *b = new B;
a->fun();
b->fun();
}
输出结果:
A::fun && a = 1
B::fun && a = 1
可以看到这里的a
没有被改变。