对多态是什么并不陌生,但是之前只知道其作用和一般的使用方法,对其具体实现原理并不清楚。今天在解决遇到的问题时,参考了一些相关的文章,在这里总结一下。
多态(Polymorphisn)
多态性是OOP的核心概念。直观的说就是基类在操作它的多态类对象时,会根据不同的类对象,调用其相应的函数。
C++的多态性是通过虚函数来实现的,也就是以virtual关键字修饰的函数。先看个例子:
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 Human { public : virtual void eat() { cout << "Human eat food." << endl; } void speak() { cout << "Human can speak." << endl; } }; class Baby : public Human { public : void eat() { cout << "Baby drink milk." << endl; } void speak() { cout << "Baby can't speak yet." << endl; } }; int main() { Human *ph = new Baby; ph->eat(); ph->speak(); return 0; } |
这段程序的运行结果是:
Baby drink milk.
Human can speak.
只需在把基类的成员函数设为virtual,其派生类的相应的函数也会自动变为虚函数。因此Baby类中的eat()函数也是虚函数,其前面的virtual可省略。
对一般的函数来说,不论基类指针指向的是哪个具体的派生类对象,最终被调用的还是基类成员函数。所以,用基类指针ph调用speak()时,基类的speak()被调用。
但对于虚函数来说,会根据具体的派生类对象,调用相应派生类的函数。所以,ph调用eat()时,最终是它所指向的Baby类对象的成员函数被调用。
同时也注意到,要实现多态,还有一个关键之处是就是一切用指向基类的指针或引用来操作对象。
实现原理
当一个类中有虚函数存在时,编译器就会为它插入一段数据,并为之创建一个表。那段数据叫做虚表指针(vptr),指向那个表,那个表叫做虚表 (vtbl)。每个类都有自己的vtbl,vtbl的作用就是保存自己类中虚函数的地址,可以把vtbl形象地看成一个数组,这个数组的每个元素存放的就 是虚函数的地址。
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 | #include <iostream> using namespace std; class A { public : virtual void fun1() { cout << "A::fun1()" << endl; } virtual void fun2() { cout << "A::fun2()" << endl; } }; class B : public A { public : void fun1() { cout << "B::fun1()" << endl; } void fun2() { cout << "B::fun2()" << endl; } }; int main() { A *pa = new A; pa->fun1(); delete pa; return 0; } |
毫无疑问,调用了A::fun1(),但是A::fun1()不是像普通函数那样直接找到函数地址而执行的。真正的执行方式是:首先取出vptr的 值,这个值就是vtbl的地址,由于调用的函数A::fun1()是第一个虚函数,所以取出vtbl第一个表项里的值,这个值就是A::fun1()的地 址了,最后调用这个函数。因此只要vptr不同,指向的vtbl就不同,而不同的vtbl里装着对应类的虚函数地址,所以这样虚函数就可以完成它的任务, 多态就是这样实现的。
而对于class A和class B来说,他们的vptr指针存放在何处?其实这个指针就放在他们各自的实例对象里。由于class A和class B都没有数据成员,所以他们的实例对象里就只有一个vptr指针。通过上面的分析,可以用下面一段代码来描述这个带有虚函数的类的简单模型。
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 | #include <iostream> using namespace std; class A { public : virtual void fun1() { cout << "A::fun1()" << endl; } virtual void fun2() { cout << "A::fun2()" << endl; } }; class B : public A { public : void fun1() { cout << "B::fun1()" << endl; } void fun2() { cout << "B::fun2()" << endl; } }; int main() { void (*fun1)(A *); A *p = new B; long lVptrAddr; memcpy (&lVptrAddr, p, 4); memcpy (&fun1, reinterpret_cast < long *>(lVptrAddr), 4); fun1(p); delete p; return 0; } |
一步一步开始分析:
1. void (*fun)(A *); 这段定义了一个函数指针名字叫做fun,而且有一个A*类型的参数,这个函数指针待会用来保存从vtbl里取出的函数地址。
2. A *p = new B; new B是向内存申请一个内存单元的地址然后隐式地保存在一个指针中。然后把这个地址附值给A类型的指针p.
3. long lVptrAddr; 这个long类型的变量用来保存vptr的值。
4. memcpy(&lVptrAddr, p, 4); 前面讲了,他们的实例对象里只有vptr指针,所以我们就放心大胆地把p所指的4bytes内存里的东西复制到lVptrAddr中,所以复制出来的 4bytes内容就是vptr的值,即vtbl的地址。
5. memcpy(&fun, reinterpret_cast(lVptrAddr), 4); 取出vtbl第一个表项里的内容,并存放在函数指针fun里。需要注意的是lVptrAddr里面是vtbl的地址,但lVptrAddr不是指针,所以要把它先转变成指针类型。
6. fun(p); 这里就调用了刚才取出的函数地址里的函数,也就是调用了B::fun()这个函数。至于为什么会有参数p,其实类成员函数调用时,会有个this 指针,这个p就是那个this指针,只是在一般的调用中编译器自动处理了而已,而在这里则需要自己处理。
7. delete p; 释放由p指向的自由空间;
如果调用B::fun2()怎么办?那就取出vtbl的第二个表项里的值就行了。
1 2 3 | memcpy (&fun, reinterpret_cast < long *>(lVptrAddr + 4), 4); //OR memcpy (&fun, reinterpret_cast < long *>(lVptrAddr) + 1, 4); |
第一种方法加4是因为一个指针的长度是4bytes,所以加4。第二种方法更符合数组的用法,因为lVptrAddr被转成了long*型,所以加1就是往后移sizeof(long)的长度。