http://blog.csdn.net/alps1992/article/details/45052403
虚函数
虚函数就是用virtual
来修饰的函数。虚函数是实现C++多态的基础。
虚表
每个类都会为自己类的虚函数创建一个表,来存放类内部的虚函数成员。
虚函数表指针
每个类在构造函数里面进行虚表和虚表指针的初始化。
下面看一段代码:
1 // 2 // main.cpp 3 // VirtualTable 4 // 5 // Created by Alps on 15/4/14. 6 // Copyright (c) 2015年 chen. All rights reserved. 7 // 8 9 #include <iostream> 10 using namespace std; 11 12 class Base{ 13 public: 14 virtual void func(){ 15 printf("Base "); 16 } 17 virtual void hunc(){ 18 printf("HBase "); 19 } 20 private: 21 virtual void gunc(){ 22 printf("Base Private "); 23 } 24 }; 25 26 class Derive: public Base{ 27 public: 28 virtual void func(){ 29 printf("Derive "); 30 } 31 }; 32 33 class DeriveSecond: public Base{ 34 public: 35 void func(){ 36 printf("Second! "); 37 } 38 }; 39 40 class DeriveThird: public Base{ 41 }; 42 43 class DeriveForth: public Base{ 44 public: 45 void gunc(){ 46 printf("Derive Forth "); 47 } 48 }; 49 50 int main(int argc, const char * argv[]) { 51 Derive d; 52 Base *pb = &d; 53 pb->func(); 54 // 1 输出:Derive 55 56 DeriveSecond sec; 57 pb = &sec; 58 pb->func(); 59 // 2 输出:Derive Second 60 61 DeriveThird thi; 62 pb = &thi; 63 pb->func(); 64 //3 输出:Base 65 66 DeriveForth forth; 67 pb = &forth; 68 // pb->gunc(); 69 // 4 报错 70 return 0; 71 }
在这个里面我创建了一个基类Base还有其他派生类。
-
首先
// 1
部分,表示了虽然我们声明的是一个Base类的指针,但是指向的是派生类的实例,所以调用的就是派生类的函数。 -
其次
// 2
部分,表示的和1差不多,只不过在// 2
里不是虚函数了,覆盖了父类的虚函数。但还是存放在派生类的虚表里。 -
在
// 3
的代码里可以看到,派生类没有覆盖父类的虚函数的时候,虽然指向的是派生类的实例,但是调用的是父类的方法,是因为在继承时候,子类也有一个虚表,里面存放了父类的虚函数表。 -
在
// 4
里是私有的虚函数是不能直接被外部调用的。
虚表详解
先看如下代码:代码来源:RednaxelaFX,编程语言厨此人我觉得很厉害,这里借用一下他的代码,无任何商用,如果有问题,请联系我删除。
1 #include <string> 2 #include <iostream> 3 4 class Object { 5 int identity_hash_; 6 7 public: 8 Object(): identity_hash_(std::rand()) { } 9 10 int IdentityHashCode() const { return identity_hash_; } 11 12 virtual int HashCode() { return IdentityHashCode(); } 13 virtual bool Equals(Object* rhs) { return this == rhs; } 14 virtual std::string ToString() { return "Object"; } 15 }; 16 17 class MyObject : public Object { 18 int dummy_; 19 20 public: 21 int HashCode() override { return 0; } 22 std::string ToString() override { return "MyObject"; } 23 }; 24 25 int main() { 26 Object o1; 27 MyObject o2; 28 std::cout << o2.ToString() << std::endl 29 << o2.IdentityHashCode() << std::endl 30 << o2.HashCode() << std::endl; 31 } 32 33 /* 34 Object vtable 35 -16 [ offset to top ] __si_class_type_info 36 -8 [ typeinfo Object ] --> +0 [ ... ] 37 --> +0 [ vptr ] --> +0 [ &Object::HashCode ] 38 +8 [ identity_hash_ ] +8 [ &Object::Equals ] 39 +12 [ (padding) ] +16 [ &Object::ToString ] 40 41 MyObject vtable 42 -16 [ offset to top ] __si_class_type_info 43 -8 [ typeinfo MyObject ] --> +0 [ ... ] 44 --> +0 [ vptr ] --> +0 [ &MyObject::HashCode ] 45 +8 [ identity_hash_ ] +8 [ &Object::Equals ] 46 +12 [ dummy_ ] +16 [ &MyObject::ToString ] 47 48 */
这里最主要的是我认为R大的这个虚表画的实在是好看。所以直接借用了,一看就比我上面自己写的代码好看多了(T T)。
首先我们学习的时候,可以暂时先无视小于0的虚表内容。从+0开始存放了vptr
这个虚表指针指向了类的虚表。可以很清楚的看到在MyObject
的虚表里其中HashCode 和 ToString
函数已经是派生类的虚函数了,把父类的函数重写了。
所以这两个R大画的类已经很清楚的说明了类的虚表虚函数的操作。
那么有没有比较暴力的办法强行自己来控制虚表呢。其实这个来源于当时我做的一个阿里笔试题,做完当天我就看到知乎的R大已经做了详细的解释,这里还是引用他的代码好了。
虚表和虚函数地址
以下代码同出自R大之手:RednaxelaFX,编程语言厨
1 #include <iostream> 2 using namespace std; 3 4 class animal 5 { 6 protected: 7 int age_; 8 animal(int age): age_(age) { } 9 10 public: 11 virtual void print_age(void) = 0; 12 virtual void print_kind() = 0; 13 virtual void print_status() = 0; 14 }; 15 16 class dog : public animal 17 { 18 public: 19 dog(): animal(2) { } 20 ~dog() { } 21 22 virtual void print_age(void) { 23 cout << "Woof, my age = " << age_ << endl; 24 } 25 26 virtual void print_kind() { 27 cout << "I'm a dog" << endl; 28 } 29 30 virtual void print_status() { 31 cout << "I'm barking" << endl; 32 } 33 }; 34 35 class cat : public animal 36 { 37 public: 38 cat(): animal(1) { } 39 ~cat() { } 40 41 virtual void print_age(void) { 42 cout << "Meow, my age = " << age_ << endl; 43 } 44 45 virtual void print_kind() { 46 cout << "I'm a cat" << endl; 47 } 48 49 virtual void print_status() { 50 cout << "I'm sleeping" << endl; 51 } 52 }; 53 54 void print_random_message(void* something) { 55 cout << "I'm crazy" << endl; 56 } 57 58 int main(void) 59 { 60 cat kitty; 61 dog puppy; 62 animal* pa = &kitty; 63 64 intptr_t* cat_vptr = *((intptr_t**)(&kitty)); 65 intptr_t* dog_vptr = *((intptr_t**)(&puppy)); 66 67 intptr_t fake_vtable[] = { 68 dog_vptr[0], // for dog::print_age 69 cat_vptr[1], // for cat::print_kind 70 (intptr_t) print_random_message 71 }; 72 *((intptr_t**) pa) = fake_vtable; 73 74 pa->print_age(); // Woof, my age = 1 75 pa->print_kind(); // I'm a cat 76 pa->print_status(); // I'm crazy 77 78 return 0; 79 }
我们可以看到R大干了什么!!丧心病狂的把vtable自己伪造了一个,然后放到虚表指针后面!简直佩服。看到这个代码我也是才明白,虚表可以这么操作。
虚表地址和虚函数地址
虚函数表的地址(int*)&classname)
与虚函数的地址(int*)*(int*)(&classname)
实际按照R大的说法,这里的int
应该改成intptr_t
才更好,这样能够防止在LP64模型下,函数指针是8个字节。而地址获取不全。
虚函数表的地址和虚函数地址的关系类似于: x 和 *x
的关系。