1.什么是虚函数?
答:在C++的类中,使用virtual修饰的函数。
例如: virtual void speak() const { std::cout << "Mammal speak! "; }
2.虚函数有什么作用?
答:虚函数的作用是允许在派生类中重新定义与基类同名的函数,并且可以通过基类指针或引用来访问基类和派生类中的同名函数。
例如:定义一个父类Mammal类
1: class Mammal
2: {3: public:
4: Mammal():age(1) { std::cout << "Mammal constructor ... "; }
5: ~Mammal() { std::cout << "Mammal destructor ... "; }
6: void move() const { std::cout << "Mammal, move one step "; }7: virtual void speak() const { std::cout << "Mammal speak! "; }8:9: protected:
10: int age;
11: };
再定义一个子类Dog类,继承Mammal类,子类Dog类中重新定义了speak()函数
1: class Dog : public Mammal2: {3: public:
4: Dog() { std::cout << "Dog constructor ... "; }
5: ~Dog() { std::cout << "Dog destructor .. "; }
6: void wagTail() { std::cout << "Wagging tail ... "; }7: virtual void speak() const { std::cout << "Woof! "; }8: void move() const { std::cout << "Dog moves 5 steps ... "; }9: };
现在通过指针来访问基类和子类中的同名函数
1: int main()
2: {3: Mammal *pMam = new Mammal;
4: Mammal *pDog = new Dog;
5: pMam->speak();6: pDog->move();7: pDog->speak();8: return 0;
9: }
运行结果:
3.虚函数的使用?
首先,明确一点,虚函数是用来实现多态的,如果类继承中无需实现多态,请不要使用虚函数,后面会讲到,使用虚函数实际上会增加开销。
3.1单继承的形式
由Mammal派生出多个子类,每个子类唯一的继承Mammal父类,可以看到,每个子类都有一个虚函数 void speak()重写了父类中的虚函数,这是因为就这种动物类而言,每一种子类的发声都不同,需要重新定义。
1: #include <iostream>2:3: class Mammal
4: {5: public:
6: Mammal():age(1) { }7: ~Mammal() { }8: virtual void speak() const { std::cout << "Mammal speak! "; }9: protected:
10: int age;
11: };12:13: class Dog : public Mammal14: {15: public:
16: void speak() const { std::cout << "Woof! "; }17: };18:19: class Cat : public Mammal20: {21: public:
22: void speak() const { std::cout << "Meow! "; }23: };24:25: class Horse : public Mammal26: {27: public:
28: void speak() const { std::cout << "Whinny! "; }29: };30:31: class Pig : public Mammal32: {33: public:
34: void speak() const { std::cout << "Oink! "; }35: };
访问各类:
1: int main()
2: {3: Mammal* array[5];4: Mammal* ptr;5: int choice, i;
6: for (i = 0; i < 5; i++)
7: {8: std::cout << "(1) dog (2) cat (3) horse (4) pig: ";
9: std::cin >> choice;10: switch (choice)
11: {12: case 1:
13: ptr = new Dog;
14: break;
15: case 2:
16: ptr = new Cat;
17: break;
18: case 3:
19: ptr = new Horse;
20: break;
21: case 4:
22: ptr = new Pig;
23: break;
24: default:
25: ptr = new Mammal;
26: break;
27: }28: array[i] = ptr;29: }30: for (i=0; i < 5; i++)
31: {32: array[i]->speak();33: }34: return 0;
35: }
访问结果:
小结:
- 当在类中引入虚函数的时候,这个类的对象必须跟踪它,每个对象会添加 vptr,其指向的一个虚拟函数表v-table,从而增加额外的空间。
- 当出现继承关系时,虚拟函数表可能需要改写,即当用基类的指针指向一个派生类的实体地址,然后通过这个指针来调用虚函数。这里要分两种情况,当派生类已经改写同名虚函数时,那么此时调用的结果是派生类的实现;而如果派生类没有实现,那么调用依然是基类的虚函数实现,而且只在多态、虚函数上表现。
- 多态仅仅在虚函数上表现,意即倘若同样用基类的指针指向一个派生类的实体地址,那么这个指针将不能访问和调用派生类的成员变量和成员函数。
- 对于上述代码,在编译阶段,无法知道将创建什么类型的对象,因此无法知道将调用哪个speak()。ptr指向的对象是在运行阶段确定的,这被称为后期绑定或运行阶段绑定,与此相对的是静态绑定或编译阶段绑定。
3.2多继承的情况
C既继承了A,也继承了B,类定义的代码如下:
1: #include <iostream>2: using namespace std;3: class A
4: {5: public:
6: A() { cout << "A construction" << endl; }
7: virtual ~A() { cout << "A destruction" << endl; }8: int a;
9: void fooA() {}
10: virtual void func(){ cout << "A func." << endl; };11: virtual void funcA() { cout << "funcA." << endl; }12: };13:14: class B
15: {16: public:
17: B() { cout << "B construction" << endl; }
18: virtual ~B() { cout << "B destruction" << endl; }19: int b;
20: void fooB() {}
21: virtual void func() { cout << "B func." << endl; };22: virtual void funcB() { cout << "funcB." << endl; }23: };24:25: class C : public A, public B26: {27: public:
28: C() { cout << "C construction" << endl; }
29: virtual ~C() { cout << "C destruction" << endl; }30: int c;
31: void fooC() {}
32: virtual void func() { cout << "C func." << endl; };33: virtual void funcC() { cout << "funcC." << endl; }34: };35:36: int main()
37: {38: A *pa = new A();
39: pa->func();40:41: B *pb = new B();
42: pb->func();43:44: C *pc = new C();
45: pc->func();46:47: A *pac = new C();
48: pac->func();49:50: system("pause");
51: return 0;
52: }
可以看到A,B,C三个类的构造函数和虚函数都不同,下面测试一下,创建对象以及调用虚函数时,派生类及父类的函数是如何执行的。
运行一下,观察结果:
分析小结:
- 当对一个多继承的类实例化的时候,调用了多个父类的构造函数,且也调用了子类的构造函数。
- 当出现继承关系时,子类的虚拟函数直接覆盖了父类的续写函数,相当于重写了该函数,实际上,在编译的时候,子类的虚拟函数列表指针就直接把继承的父类的虚拟函数的地址覆盖掉了。
4.总结
创建第一个虚函数时候就会创建一个v-table,包含虚函数成员的类必须维护v-table,因此会带来一些开销。如果类很小,并且不打算从它派生出其他类,就根本没必要使用虚函数。