虚函数表
C++中的虚函数的作用主要是实现了多态的机制。
多态:就是用父类型的指针指向其子类的实例。
虚函数是通过一张虚函数表(Virtual Table)实现的。在这个表中,主要是一个类的虚函数地址表,这张表解决了继承和覆盖等问题。就像一张地图一样,指明了指针实际所应该调用的函数。
举个例子:
class Base{
public:
virtual void f() {cout << "Base::f" << endl;};
virtual void g() {cout << "Base::g" << endl;};
virtual void h() {cout << "Base::h" << endl;};
}
typedef void(*Fun)(void);
Base b;
cout << "虚函数表地址" << (int*)(&b) << endl; // 获取虚函数表的地址
cout << "虚函数表第一个函数的地址"<< (int*)*(int*)(&b) << endl; // 获取虚函数表第一个函数的地址
pFun = (Fun)*((int*)*(int*)(&b));
pFun(); // 等价于Base::f()
(Fun)*((int*)*(int*)(&b + 0)); //Base::f()
(Fun)*((int*)*(int*)(&b + 1)); //Base::g()
(Fun)*((int*)*(int*)(&b + 2)); //Base::h()
1、 单继承无虚函数覆盖
class Base{
public:
virtual void func1();
virtual void func2();
virtual void func3();
}
class Derived : public Base{
public:
virtual void func4();
virtual void func5();
virtual void func6();
}
从虚函数表中我们可以看出:
(1)虚函数按照声明顺序依次放入表中。
(2)父类的虚函数放在子类的虚函数前面。
2、单继承又虚函数覆盖
class Base{
public:
virtual void func1();
virtual void func2();
virtual void func3();
}
class Derived: public Base{
public:
virtual void func1();
virtual void func5();
virtual void func6();
}
从虚函数表中我们可以看出:
(1)覆盖的 f() 函数被放到了虚表中原来父类虚函数的位置。
(2) 没有被覆盖的依次存放。
3、 多继承无函数覆盖图解
class Base1{
public:
virtual void func1();
virtual void func2();
virtual void func3();
}
class Base2{
public:
virtual void func1();
virtual void func2();
virtual void func3();
}
class Base3{
public:
virtual void func1();
virtual void func2();
virtual void func3();
}
class Derived: public Base1, Base2, Base3{
public:
virtual void func4();
virtual void func5();
virtual void func6();
}
我们可以看到:
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
4、 多继承有函数覆盖图解
class Base1{
public:
virtual void func1();
virtual void func2();
virtual void func3();
}
class Base2{
public:
virtual void func1();
virtual void func2();
virtual void func3();
}
class Base3{
public:
virtual void func1();
virtual void func2();
virtual void func3();
}
class Derived: public Base1, Base2, Base3{
public:
virtual void func1();
virtual void func5();
virtual void func6();
}
我们可以看到:
1) 每个父类都有自己的虚表,被覆盖的放在父类原来的位置。
2) 子类的成员函数被放到了第一个父类的表中。(所谓的第一个父类是按照声明顺序来判断的)
5、 虚函数常见问题
5.1、 虚函数表的代价
(1)带有虚函数的每个类会产生一个虚函数表,用来存储虚成员函数的指针。
(2)带有虚函数的每个类都会有一个指向虚函数表的指针。
5.2、 那些函数不能是虚函数
(1)构造函数:对象的虚函数表指针需要通过构造函数初始化。
(2)内联函数:内联函数可以在编译阶段进行函数体的替换,而虚函数需要在运行期间进行确定。
(3)静态函数:静态函数不属于对象而属于类,因为静态成员函数没有this指针,所以无法访问对象的虚表指针,也就无法访问类的虚函数表,将静态函数设置成虚函数也就没有任何意义,所以c++语法不支持将静态函数设置成虚函数。
(4)友元函数:友元函数不属于类,也不能被继承,没有继承特性的函数没有虚函数的说法
(5)类外的普通函数:类外普通函数不是类的成员函数,同样不具备继承特性,也就没有虚函数的说法
5.3 虚函数和纯虚函数的区别
1)纯虚函数只有定义,没有实现,虚函数既有定义,又有实现
2)含有纯虚函数的类不能定义对象,含有虚函数的类可以定义对象
5.4、 虚析构函数的作用?父类的析构函数为什么一定要设置成虚函数?
父类虚析构函数就是为了避免内存泄漏,防止子类内存得不到释放造成内存泄漏。
1.当父类的析构函数不声明成虚析构函数时,当子类继承父类,父类指针指向子类对象,delete掉父类指针,只会调动父类的析构函数,而不会调用子类的析构函数,从而造成子类对象内存泄漏
2.当父类的析构函数声明成虚析构函数时,当子类继承父类,父类指针指向子类对象,delete掉父类指针,先调动父类的析构函数,然后调用子类的析构函数,不存在子类对象内存泄漏的问题
只要存在继承关系,则父类的析构函数必须定义成虚函数!
5.5、 构造函数和析构函数中为什么不可以调用虚函数?
1.构造子类对象时,首先调用父类构造函数初始化对象的父类部分,在执行父类的构造函数时,对象的子类部分都是未初始化的,实际上此时对象还不是一个子类对象。
2.析构子类对象时,先析构子类部分,然后按照构造顺序逆序析构父类部分。
所以在运行子类的构造和析构函数时,对象都是不完整的,为了适应这种不完整,编译器视对象类型为当前构造或析构函数所在类的类型,由此造成的结构就是:在父类的构造或析构函数中,会将子类对象当作父类对象看待。
如果我们在父类的构造或析构函数中调用虚函数,调用的往往是当前类的虚函数,达不到多态的效果,跟普通函数调用没有区别。