本节研究虚函数的相关问题;
虚函数表
无继承
代码片段
class Animal { public: Animal(int age) : _age(age) { } virtual void f() { cout << "Animal::f " << _age << endl; } virtual void g() { cout << "Animal::g " << _age << endl; } private: int _age; }; int main(void) { Animal a(10); cout << "Animal size: "<< sizeof(a) << endl; cout << "_age address: "<< ((int*)&a+1) << " value: " << *((int*)&a+1)<< endl; cout << "virtual fun address: "<< (int*)&a << endl; cout << "virtual fun[1] address: "<< (int*)*(int*)&a << endl; cout << "virtual fun[2] address: "<< ((int*)*(int*)&a+1) << endl; typedef void (*Fun)(Animal*); Fun pf = (Fun)*((int*)(*(int*)&a)); pf(&a); typedef void (*Fun2)(); Fun2 pg = (Fun2)*((int*)(*(int*)&a)+1); pg(); }
输出如下:
Animal size: 8 _age address: 0xbf8347d4 value: 10 virtual fun address: 0xbf8347d0 virtual fun[1] address: 0x8048c18 virtual fun[2] address: 0x8048c1c Animal::f 10 Animal::g 134515314
说明几点:
(1)在GCC中(vptr放在对象头部),因此Animal的结构体大小为8,第一个字节存放的虚函数指针(vptr),第二个字节存放的是_age数据;
(2)所有的Animal的对象的vptr都是一样的,vptr为Animal虚函数表的指针,虚函数表存放各个虚函数的函数地址;
(3)在void (*Fun)(Animal*);中我们在pf函数参数中传入a地址,通过pf函数指针也是可以进行Animal成员函数的;
虚函数示意图如下
单继承(无virtual覆盖)
代码片段
class Animal { public: Animal(int age) : _age(age) { } virtual void f() { cout << "Animal::f " << _age << endl; } virtual void g() { cout << "Animal::g " << _age << endl; } private: int _age; }; class Dog : public Animal { public: Dog(int age, int id) : Animal(age), _id(id) { } private: int _id; }; Dog d(10, 20); Animal a(10); int main(void) { cout << "Dog ---------------- " << endl; cout << "Dog size: "<< sizeof(d) << endl; cout << "_age address: "<< ((int*)&d+1) << " value: " << *((int*)&d+1)<< endl; cout << "_id address: "<< ((int*)&d+2) << " value: " << *((int*)&d+2)<< endl; cout << "virtual fun address: "<< (int*)&d << endl; cout << "virtual fun[1] address: "<< (int*)*(int*)&d << endl; cout << "virtual fun[2] address: "<< ((int*)*(int*)&d+1) << endl; typedef void (*Fun)(Animal*); Fun pf = (Fun)*((int*)(*(int*)&d)); pf(&d); typedef void (*Fun2)(); Fun2 pg = (Fun2)*((int*)(*(int*)&d)+1); pg(); cout << "Aniaml ---------------- " << endl; cout << "virtual fun address: "<< (int*)&a << endl; cout << "virtual fun[1] address: "<< (int*)*(int*)&a << endl; cout << "virtual fun[2] address: "<< ((int*)*(int*)&a+1) << endl; Animal c(10); cout << "virtual fun address: "<< (int*)&c << endl; cout << "virtual fun[1] address: "<< (int*)*(int*)&c << endl; cout << "virtual fun[2] address: "<< ((int*)*(int*)&c+1) << endl; }
输出如下:
Dog ---------------- Dog size: 12 _age address: 0x804a338 value: 10 _id address: 0x804a33c value: 20 virtual fun address: 0x804a334 virtual fun[1] address: 0x8048f00 virtual fun[2] address: 0x8048f04 Animal::f 10 Animal::g 14073600 Aniaml ---------------- virtual fun address: 0x804a340 virtual fun[1] address: 0x8048f10 virtual fun[2] address: 0x8048f14 virtual fun address: 0xbfed4890 virtual fun[1] address: 0x8048f10 virtual fun[2] address: 0x8048f14
地址示意图如下:
单继承(有virtual覆盖)
代码片段
class Animal { public: Animal(int age) : _age(age) { } virtual void f() { cout << "Animal::f " << _age << endl; } virtual void g() { cout << "Animal::g " << _age << endl; } private: int _age; }; class Dog : public Animal { public: Dog(int age, int id) : Animal(age), _id(id) { } void f() { cout << "Dog::f hello" << endl; } virtual void h() { cout << "Dog::h hello" << endl; } private: int _id; };
地址示意图如下:
虚函数与重载
代码片段(提示:此代码段有陷阱)
#include <iostream> using namespace std; class Point { public: Point(int x = 1, int y = 3) : _x(x), _y(y) { } virtual void print() { cout << "Point::print" << endl; } private: int _x, _y; }; class Point3D: public Point { public: void print() const { cout << "Point3D::print" << endl; } private: int _z; }; void print(Point a, Point* b, Point& c) { a.print(); (*b).print(); b->print(); c.print(); } int main(void) { Point3D point; print(point, &point, point); return 0; }
说明几点:
(1)void print(Point a, Point* b, Point& c)函数中的,4个输出均为Point::print;
(2)请注意Point::print() 表示传入的this为一个指向常量的指针,this本身是一个常量指针,因此传入函数的是一个指向常量的常量指针;而对于print中a,b,c并不发生virtual机制,对于a,发生切割,表现为ADT行为,一定调用的是Point::print() 函数,对于b,c指向地址空间只是Point3D的中Point部分(指针的不同,主要表现为所寻址的object的不同),只能解释Point所覆盖范围的部分,由于Point::print()和Point32::print()
const不是同一函数,因此不会发生virtual调用,只会Point::print() const;
(3)注意如果将将print函数修改为void print(Point a, const Point* b, const Point& c),此例子是无法编译通过的,因为b和c调用所传入的this指针均为const * Point const this;而对于Point::print()不为const成员函数,故将发生将指向常量的指针转换成普通指针的编译错误;
(4)修改print函数如下,此时,p输出的内容为Point3D::print;主要原因此时p所指向的地址空间为整个Point3D的地址空间,是一个显式的向下类型转换,无论如何都会变成一个Point3D类型指针的;请注意p转换成const Point3D*类型,否则不能编译通过,主要是因为Point3D提供的print函数为const函数;
void print(Point a, Point* b, Point& c) { a.print(); (*b).print(); b->print(); c.print(); const Point3D* p = static_cast<const Point3D *>(b); p->print(); }
(5)修改main函数如下,此时并不会出现段错误,原因是在p->print()在调用Point3D中的void print() const将被解释成xx_print_xx(const Point3D * const p),而由于在此函数xx_print_xx中p并没有访问相关的成员变量,如果访问将会出现段错误;
int main(void) { Point3D* p = NULL; p->print(); return 0; }
虚函数与默认实参
代码片段如下
#include <iostream> using namespace std; class A { public: virtual int X(int x = 1) { return x; } }; class B : public A { public: int X(int x = 5) { cout << x << endl; return x; } }; int main(void) { B b; cout << b.X() << endl; cout << "------------------------\n"; A* a = &b; cout << a->X() << endl; return 0; }
说明几点:
(1)对于B b并不会发生多态调用,因为C++中只有通过指针和引用才能进行动态调用,就因此b.X()就编译时已经确定好了,如果B中未重新定义X(int x=5),那么根据域的查找原则,可以调用父类的X();
(2)对于a->X()其实也是调用的B中的X(),X()中cout会执行,但是虚函数的默认实参是由本次调用的静态类型A决定的,也就是x会等于1;
(3)假设将A类去除掉virtual,修改为如下:
class A { public: int X(int x = 1) { return x; } };
(3.1)那么a->X()将会调用父类的X(),而不会发生动态调用;因为对非虚函数的调用是编译时决定的;名字查找,首先确定静态类型,找到与静态类型对应的类(或父类)中找到,找到以后,判断调用是否是虚函数,若是函数,编译器产生的代码将在运行时决定该虚函数的哪个版本;否则将是一次常规的函数调用;而本例对应后一种情况,不会发生虚函数调用;
虚函数与构造(析构)函数
代码片段如下
#include <iostream> using namespace std; class A { public: A() { cout << "----------\n"; X(); cout << "A ctor\n"; } virtual void X() { cout << "A::X()" << endl; } virtual ~A() { cout << "----------\n"; X(); cout << "A dtor\n"; } }; class B : public A { public: void X() { cout << "B::X()" << endl; } ~B() { cout << "----------\n"; X(); cout << "B dtor\n"; } }; int main(void) { A* a = new B; delete a; return 0; }
说明几点:
(1)A的析构函数,应该声明为virtual,因为delete a时将会执行A的析构函数,如果A的析构函数不是virtual,将不会发生动态调用,使得B对象处于半死状态;所以A的析构函数应该声明为析构函数,这样首先调用B的析构函数,继而调用A的析构函数;
(2)在A的构造函数和析构函数调用虚函数,并不会发生动态调用;因为执行到A的构造函数时,B的除A的其他部分还未初始化;而执行到A的析构函数时,B除A的其他部分已经销毁掉了;因此执行父类中虚函数时,整个对象处于未完成状态;而编译器将会将不会进行动态调用,而是执行所属类型的对应版本;