面试的时候,常常宣称自己是一个C++程序员,不免就会被问一些有关C++的问题,例如虚函数的实现原理;析构函数是否能定义为虚函数等等。很多时候都回答不上来,就总会以时间久了忘了,搪塞过去,面试完,可能回去查查答案,但也总是不求甚解。
既然有了博客,我就准备看看这些方面的内容,希望能记录下来,有所积累。于是就到网上查,或在书上找找,后来发现里面的内容太多了,不是一篇两篇博客就能写完的。于是没有办法,就只有硬着头皮写一个系列了。今天是第一部分虚函数和虚继承。
C++的关键字virtual只能用在两个地方,一是定义类的成员函数为虚函数,二是定义类的继承关系为虚继承。这两点的用处大相径庭,但是在设计思想上还是有一定的共同性的。这篇文章先讲虚函数,下一篇讲虚继承。
虚函数
虚,不实也。也可以理解为看到的和实际的不一样。虚函数存在的目的只有一个,那就是实现多态。关于多态,可以去参考各种教科书,上面都有详细的说明。虚函数在实现多态时,通过一种间接的运行时(而不是编译时)的机制激活(调用)的函数。下面看一个多态的简单例子。
多态的简单例子 view raw 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 * polymorphism.cc: example of polymorphism */ #include <iostream> using namespace std ;class base {public : virtual void f () { cout << "in base:f" << endl;} }; class derived1: public base {public : virtual void f () { cout << "in derived1:f" << endl;} }; class derived2: public base {public : virtual void f () { cout << "in derived2:f" << endl;} }; void call_func (base* pb) { pb->f(); } int main (void ) { base* pb = new base(); base* pd1 = new derived1(); base* pd2 = new derived2(); call_func(pb); call_func(pd1); call_func(pd2); }
运行结果如下:
从运行结果来看,函数call_func()
最后实际调用的f()
要到运行时,根据传入的参数,才能确定调用的是哪个函数。
那么虚函数的这种性质是如何实现的呢?答案是虚表(vtable)。具体的做法是,在对象的存储空间里面开辟一个指针,指针指向一个存放着虚函数地址的函数指针表。编译器在生成调用虚函数的指令时,按照偏移量,从虚表中取相应的函数指针进行调用。子类的虚函数会覆盖父类中对应虚函数在虚表中的位置,所以在调用的时候,就调用到了子类的函数了。也许这段话没有说太清楚,下面看一段代码或许会有帮助。
测试虚表的例子 view raw 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 * test virtual functions */ #include <iostream> using namespace std ;class base {public : virtual void f () { cout << "in base:f" << endl;} }; class derived: public base {public : virtual void f () { cout << "in derived:f" << endl;} }; typedef void (*fun) (void ) ;#define VTAB(pclass) ((long*)(*(long*)(pclass))) int main (void ) { base* pb = new base(); pb->f(); base* pd = (base*) new derived(); pd->f(); cout << "--- call through vtable ---" << endl; fun pfun = (fun)*VTAB(pd); pfun(); pfun = (fun)*VTAB(pb); pfun(); derived* pd_fake = (derived*)pb; pfun = (fun)*VTAB(pd_fake); pfun(); pfun = (fun)*VTAB(pd); cout << "value of *pfun is: " << *pfun << endl; pfun = (fun)*(VTAB(pd) + 1 ); cout << "value of *pfun is: " << *pfun << endl; }
输出结果如下:
上面这段代码的第20行,通过定义指针的方式,直接访问虚表,发现它的输出与通过对象调用是一直的,而且指针的类型无关。由上面的代码也可以看出虚表在对象的内存空间中是怎么分布的。对象的起始地址,就是虚表指针。虚表的最后一项为0,代表虚表结束。上面程序的最后一项输出可以看出来。
用一张图来说明情况。
在多个虚函数的情况下,子类仅仅覆盖在子类重载的虚函数,而子类新定义的虚函数,加入到虚表的最后。下面这个例子就是多个虚函数的情况。
多个虚函数 view raw 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 #include <iostream> using namespace std ;class base {public : virtual void f () { cout << "in base:f" << endl;} virtual void f0 () { cout << "in base:f0" << endl;} }; class derived: public base {public : virtual void f () { cout << "in derived:f" << endl;} virtual void f1 () { cout << "in derived:f1" << endl;} }; typedef void (*fun) (void ) ;#define VTAB(pclass) ((long*)(*(long*)(pclass))) int main (void ) { base* pb_real = new base(); pb_real->f(); base* pb = (base*) new derived(); pb->f(); pb->f0(); cout << "--- call function through vtable ---" << endl; fun pfun = (fun)*VTAB(pb); pfun(); pfun = (fun)*(VTAB(pb)+1 ); pfun(); pfun = (fun)*(VTAB(pb)+2 ); pfun(); }
输出结果:
虚表的组织结构如下:
由图中可以看出,子类的f()
覆盖了基类的f()
,而基类的f0()
依然存在,子类的f1()
添加到了虚表的最后。
当有多个基类时,子类会为每个基类添加一个虚表指针,指针的顺序按照类定义时的声明顺序。下面这个例子就是这样的。
虚函数多重继承 view raw 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 #include <iostream> using namespace std ;class father_b1 {public : virtual void f () { cout << "in father of base1:f" << endl;} virtual void fbf () { cout << "in father of base1:fbf" << endl;} }; class father1_b1 {public : virtual void f () { cout << "in father1 of base1:f" << endl;} virtual void f1bf () { cout << "in father1 of base1:f1bf" << endl;} }; class base {public : virtual void f () { cout << "in base:f" << endl;} virtual void bf0 () { cout << "in base:f0" << endl;} }; class base1: public father_b1, public father1_b1{public : virtual void f () { cout << "in base1:f" << endl;} virtual void b1f0 () { cout << "in base1:f0" << endl;} }; class derived: public base1, public base {public : virtual void f () { cout << "in derived:f" << endl;} virtual void f1 () { cout << "in derived:f1" << endl;} }; typedef void (*fun) (void ) ;#define VTAB(pclass) ((long*)(*(long*)(pclass))) #define VTAB1(pclass) ((long*)(*(((long*)(pclass)) + 1 ))) #define VTAB2(pclass) ((long*)(*(((long*)(pclass)) + 2 ))) int main (void ) { derived* pd = new derived(); cout << "--- vtable ---" << endl; fun pfun = (fun)*VTAB(pd); pfun(); pfun = (fun)*(VTAB(pd)+1 ); pfun(); pfun = (fun)*(VTAB(pd)+2 ); pfun(); pfun = (fun)*(VTAB(pd)+3 ); pfun(); cout << endl; cout << "--- vtable + 1 ---" << endl; pfun = (fun)*VTAB1(pd); pfun(); pfun = (fun)*(VTAB1(pd)+1 ); pfun(); cout << endl; cout << "--- vtable + 2 ---" << endl; pfun = (fun)*VTAB2(pd); pfun(); pfun = (fun)*(VTAB2(pd)+1 ); pfun(); }
输出结果:
这是一个比较复杂的继承关系了,可以看出最后的derived
类里面已经有3个虚表指针了,也就是说,子类会讲所有的父类的虚表继承下来,并将自己的虚函数添加到第一个虚表的最后。还有一点,就是子类的虚函数会覆盖所有基类的对应虚函数,图中的derived::f()
就覆盖了3处。其组织结构如下:
构造函数和析构函数的调用顺序
这个问题本来和虚函数没有关系,但是为了后面解释关于构造函数为什么不能声明为virtual,添加到这里的。
构造函数的调用顺序是,先调用父类的构造函数,然后调用成员变量的构造函数,最后调用子类自身的构造函数,多个父类时,按照父类的继承时的声明顺序调用,成员变量的构造函数也按照声明顺序调用。
析构函数的调用顺序与构造函数的正好相反,先调用子类自身的析构函数,然后是成员变量的析构函数,最后是父类的构造函数,多个父类时,按照父类的继承时的声明的相反顺序调用,成员变量的构造函数也按照声明的相反顺序调用。
下面是一段测试调用顺序的代码
构造和析构函数的调用顺序 view raw 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 * 研究构造函数的调用顺序 * 1. 首先调用基类的构造函数(如果有基类);如果有多个基类,则按基类被列出的顺序调用; * 2. 调用这个类的成员对象的构造函数(如果有的话);如果有多个成员对象,则按成员对象定义的顺序被调用(与参数列表中列出的顺序无关); * 3. 最后调用这个类自身的构造函数; * 4. 析构函数正好相反; * 注意:如果有虚基类,则先调用虚基类的构造函数。在调用基类的构造函数,如果有多个虚基类,则按列出的顺序调用; */ #include <iostream> using namespace std ;class father_base_a{public : father_base_a(){cout <<"This is father_base_a;" <<endl;} virtual ~father_base_a() { cout << "This is ~father_base_a" << endl; } }; class base_a: public father_base_a{public : base_a(){cout <<"This is base_a;" <<endl;} virtual ~base_a() { cout << "This is ~base_a" << endl; } }; class base_b{public : base_b(){cout <<"This is base_b;" <<endl;} virtual ~base_b() { cout << "This is ~base_b" << endl; } }; class member_a{public : member_a(){cout <<"This is meber_a;" <<endl;} virtual ~member_a(){cout <<"This is ~meber_a;" <<endl;} }; class member_b{public : member_b(){cout <<"This is member_b;" <<endl;} virtual ~member_b(){cout <<"This is ~meber_b;" <<endl;} }; class derived : public base_b, public base_a{ public : member_a ma; derived(){ cout <<"This is derived;" <<endl; } virtual ~derived() { cout << "this is ~derived()" << endl; } private : member_b mb; }; int main (void ) { derived de; cout << "---------" << endl; }
输出结果
从输出结果可以看出,先调用了base_b
的构造函数,然后调用base_a
的构造函数(但得先调用base_a
的父类father_base_a
),然后是成员member_a
和member_b
的构造函数,最后是derived
自己的构造函数。析构的过程正好相反。
构造函数不能声明为虚函数
这也是面试中常被问道的一个问题。原因应该有两个
在构造函数之前,类的对象是不存在的,那么也没有vtable,也无法通过vtable找到虚函数。所有无法定义为虚函数。
构造函数是先调用父类,最后才是子类的构造,因为子类的内存布局是基于父类。如果使用虚构函数,那么在调用父类的构造函数时,实际调用的是子类的构造函数,那么就无法完成对象的构造。
虚函数的主要目的是多态,运行时确定调用那个函数,对象的构造过程是确定的,使用虚函数没有意义。
实际上,如果将构造函数声明为虚函数,编译时,gcc会报错。
析构函数声明为虚函数
在基类的声明中一般都应该将析构函数声明为虚函数。首先由于析构函数没有构造函数的问题,所以是可以声明为虚函数的。其次,可能程序需要利用析构函数为虚函数的特性,才能讲资源释放完全。
首先来看一个代码例子
析构函数为虚函数 view raw 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 #include <iostream> using namespace std ;class base{public : virtual ~base(){ cout << "in base::~base()" << endl; } }; class derived: public base{public : virtual ~derived(){ cout << "in derived::~derived" << endl; } }; class base1 {public : ~base1(){ cout << "in base1::~base1()" << endl; } }; class derived1: public base1{public : ~derived1(){ cout << "in derived1::~derived1" << endl; } }; int main (void ) { base *pbase = new derived(); delete pbase; cout << "--------" << endl; base1 *pbase1 = new derived1(); delete pbase1; }
运行结果
由结果可以看出,由于base1
的析构函数没有声明为虚函数,在delete pbase1
时,没有调用derived1
的析构函数,那么在derived1
中申请的资源就无法释放。而因为base
类中的修改函数声明为了虚函数,就没有这样的问题。
什么时候使用虚函数
包含了虚函数的成员函数的对象,由于需要存放虚表地址,那么就比不包含虚函数的对象,多出了一块空间,32位为4个字节,64位为8个字节。如果没有用到多态,而且有大量的对象存在时,需要考虑这个开销。
在需要使用多态,或在定义接口时,需要使用虚函数。特别是定义接口时,最好使用纯虚函数。
另外,将私有成员函数声明为虚函数,可以通过对象的虚表指针的方式访问,造成了封装的不严密。例如,通过在前面的例子中定义的VTAB
宏,就可以直接获取函数指针,然后调用函数,这种调用是需要检查这个成员函数的性质的。
同名函数隐藏规则
在父类和子类中如果存在同名的函数,那么一定要小心了,这时需要重载父类中所有同名的函数。首先来看一个关于同名函数的例子
诡异的同名函数 view raw 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 * 测试c++隐藏规则: * (1)如果派生类的函数与基类的函数同名,但是参数不同。 * 不论有无virtual关键字,基类的函数将被隐藏(注意别与重载混淆)。 * (2)如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数 * 没有virtual关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。 */ #include <iostream> using namespace std ;class Base {public : virtual void f (float x) { cout << "Base::f(float) " << x << endl; } void g (float x) { cout << "Base::g(float) " << x << endl; } void h (float x) { cout << "Base::h(float) " << x << endl; } void m (float x) { cout << "Base::m(float) " << x << endl; } }; class Derived : public Base {public : virtual void f (float x) { cout << "Derived::f(float) " << x << endl; } void g (int x) { cout << "Derived::g(int) " << x << endl; } void h (float x) { cout << "Derived::h(float) " << x << endl; } void m (int x) { cout << "Derived::m(int) " << x << endl; } }; int main (void ) { Derived d; Base *pb = &d; Derived *pd = &d; pb->f(3.14f ); pd->f(3.14f ); pb->g(3.14f ); pd->g(3.14f ); pb->h(3.14f ); pd->h(3.14f ); pb->m(3.14f ); pd->m(3.14f ); }
输出结果
注意在用不同类型的类指针调用g()
,h()
,m()
三个函数时,程序实际的行为。
这样会带来一个问题,就是在继承后,重载了父类的一个函数,或在定义了一个函数,那么父类中与这个函数同名的函数都会被隐藏,子类的对象无法调用这些函数。
下面是一个例子
诡异的同名函数 view raw 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 #include <iostream> using namespace std ;class Base{public : virtual void f (float x) { cout << "Base::f(float) " << x << endl; } virtual void f (float x, float y) { cout << "Base::f(float, float) " << x << endl; } }; class Derived : public Base{public : virtual void f (float x) { cout << "Derived::f(float) " << x << endl; } }; int main (void ) { Derived d; Base *pb = &d; Derived *pd = &d; pb->f(3.14f , 3.14f ); pd->f(3.14f , 3.14f ); }
在编译的时候会报这样的错误
总结
虚函数是通过虚表实现运行时绑定。虚表存放在对象的起始位置。子类继承父类时,会将所有的虚表就继承下来,所有子类可能有多个虚表指针。如果子类重载了父类的某个虚函数,那么子类将重写所有虚表 中该函数的地址,子类将自己的虚函数添加到第一个虚表的最后。
构造函数不能声明为虚函数,析构函数可以声明为虚函数,当类包含是虚函数的成员函数时,析构函数必须声明为虚函数 。
当子类和父类有同名函数时,父类的所有与这个函数同名的函数都将被隐藏,子类的对象无法访问到。
参考
本文参考了以下文章:
陈皓.C++ 虚函数表解析
C++ Virtual详解