第六章 多态、虚函数、虚函数表
多态
父类指针、子类指针
- 父类指针可以指向子类对象,是安全的,开发中经常用到(继承方式必须是public)
- 子类指针指向父类对象是不安全的
多态
-
默认情况下,编译器只会根据指针类型调用对应的函数,不存在多态
class Animal { public: void run() { cout << "Animal-run()" << endl; } }; class Cat: public Animal { public: void run() { cout << "Cat-run()" << endl; } }; class Dog : public Animal { public: void run() { cout << "Dog-run()" << endl; } }; //使用 Animal *cat = new Cat(); cat->run(); //打印结果,并不会调用Cat-run() //Animal-run()
- 多态是面向对象非常重要的一个特性
- 同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果
- 在运行时,可以识别出真正的对象类型,调用对应子类中的函数
- 多态的要素
- 子类重写父类的成员函数(override)(父类的成员函数必须是虚函数!)
- 父类指针指向子类对象
- 利用父类指针调用重写的成员函数
虚函数
- C++中的多态通过虚函数(virtual function)来实现
- 虚函数:被virtual修饰的成员函数
-
只要在父类中声明为虚函数,子类中重写的函数也自动变成虚函数(也就是说子类中可以省略virtual关键字)
class Animal { public: virtual void run() { cout << "Animal-run()" << endl; } }; class Cat: public Animal { public: void run() { cout << "Cat-run()" << endl; } }; class Dog : public Animal { public: void run() { cout << "Dog-run()" << endl; } }; void liu(Animal *animal) { animal->run(); } //调用 liu(new Dog()); liu(new Cat()); //打印 //Cat-run() //Dog-run()
- 注意:你要使用哪个父类指针做多态,那么至少在那个父类指针或父类的父类用virtual修饰
-
意思就是如果virtual修饰的是Dog的run,而你用Animal修饰,那就不行!!!
class Animal { public: void run() { cout << "Animal-run()" << endl; } }; class Dog : public Animal { public: virtual void run() { cout << "Dog-run()" << endl; } }; class Erha : public Dog { public: void run() { cout << "Erha-run()" << endl; } }; int main() { Animal *animal = new Animal(); animal->run(); Animal *animal1 = new Dog(); animal1->run(); Animal *animal2 = new Erha(); animal2->run(); //打印 //Animal-run() //Animal-run() //Animal-run() }
-
-
父类指针可以指向子类对象,继承方式必须是public
class Animal { public: void run() { cout << "Animal-run()" << endl; } }; //class 创建类默认继承是private class Dog : Animal { public: virtual void run() { cout << "Dog-run()" << endl; } }; // 下面使用会报错 Animal *animal1 = new Dog(); animal1->run();
虚函数表
-
虚函数的实现原理是虚表,这个虚表里面存储着最终需要调用的虚函数地址,这个虚表也叫虚函数表
class Animal { public: int m_age; virtual void speak() { cout << "Animal-speak()" << endl; } virtual void run() { cout << "Animal-run()" << endl; } }; class Cat : public Animal { public: int m_life; void speak() { cout << "Cat-speak()" << endl; } void run() { cout << "Cat-run()" << endl; } }; int main() { Animal *cat = new Cat(); cat->m_age = 20; cat->speak(); cat->run(); getchar(); return 0; }
- 反汇编查看
cat->speak();cat->run();
这两句代码-
将Animal类的2个virtual字段删掉,反汇编调用如下:
cat->speak(); //将当前对象的地址传给成员函数 00A92428 mov ecx,dword ptr [cat] //call:固定地址 00A9242B call Animal::speak (0A914C4h) cat->run(); 00A92430 mov ecx,dword ptr [cat] //call :固定地址 00A92433 call Animal::run (0A914C9h)
- 可以看出上面调用的结果分别是固定地址,静态调用。
call 地址
- 可以看出上面调用的结果分别是固定地址,静态调用。
-
将Animal类的2个virtual字段加上,反汇编调用如下:
cat->speak(); 002929F1 mov eax,dword ptr [cat] 002929F4 mov edx,dword ptr [eax] 002929F6 mov esi,esp 002929F8 mov ecx,dword ptr [cat] 002929FB mov eax,dword ptr [edx] //动态调用: call 寄存器 002929FD call eax cat->run(); 00292A06 mov eax,dword ptr [cat] 00292A09 mov edx,dword ptr [eax] 00292A0B mov esi,esp 00292A0D mov ecx,dword ptr [cat] 00292A10 mov eax,dword ptr [edx+4] //动态调用: call 寄存器 00292A13 call eax
- 可以看出调用过程是动态调用的。
call 寄存器
- 可以看出调用过程是动态调用的。
-
-
有虚函数对象的内存
Animal *cat = new Cat(); cat->m_age = 20; cat->speak(); cat->run(); //如果Animal中没有添加2个virtual,那么Cat占用内存为8个字节 //添加一个virtual,为12个字节;2个都加上,仍为12个字节 //说明一个问题:只要一个类存在虚函数不管有多少个,那么,这个类分配的内存就会增加4个字节 //而且这4个字节,在最前面,由反汇编得知 cout << sizeof(Cat) << endl;
- 虚函数对象会多4个字节的内存,而且是在最前面,用于存储虚表的地址值,根据这个地址值能找到一张虚表。
- 反汇编查看
-
内存图(x86环境的图)
- 对象的前4个字节用于存储虚表的地址值
- 虚表里面前4个字节用于存储Cat的speak成员函数地址值,后4个字节用于存储run成员函数的地址值
- 所有的Cat对象(不管在全局区、栈、堆)共用同一份虚表
cat->speak();
调用- 找到cat对象的首地址
- 然后拿到前4个字节存储的地址值
- 根据地址值找到虚表
- 找到speak函数的首地址,然后调用。
-
虚表汇编分析
-
调用speak
cat->speak(); //取出cat指针变量里面存储的地址值 //eax里面存储的是cat对象的地址值0x00E69B60 002929F1 mov eax,dword ptr [cat] //取出cat对象前面4个字节存储的数据(0x00B89B64)到edx //edx里面存储的是虚表的地址 : 0x00B89B64 002929F4 mov edx,dword ptr [eax] 002929F6 mov esi,esp //将cat对象的地址传递给虚函数内部的this指针 002929F8 mov ecx,dword ptr [cat] //取出虚表中前面4个字节存储的数据(就是0x00DC14F1)给eax //eax存放的就是Cat:: speak的函数地址 002929FB mov eax,dword ptr [edx] //动态调用: call 寄存器 002929FD call eax
-
调用run
cat->run(); //取出cat指针变量里面存储的地址值 //eax里面存储的是cat对象的地址值0x00E69B60 00292A06 mov eax,dword ptr [cat] //取出cat对象前面4个字节存储的数据(0x00B89B64)到edx //edx里面存储的是虚表的地址 00292A09 mov edx,dword ptr [eax] 00292A0B mov esi,esp //将cat对象的地址传递给虚函数内部的this指针 00292A0D mov ecx,dword ptr [cat] //取出虚表中后面4个字节存储的数据(就是0x00DC14CE)给eax //eax存放的就是Cat:: run的函数地址 00292A10 mov eax,dword ptr [edx+4] //动态调用: call 寄存器 00292A13 call eax
-
VS使用小知识点
- VS的内存窗口打开
- 调试->窗口->内存->内存1
子类没有重写父类成员函数
- 如果子类对象没有重写父类的虚函数,那么子类对象调用该函数时,首先到当前类中找,找不到在到父类中找
-
本质:子类仍然有虚表,子类的虚表存的是父类的虚函数地址
class Animal { public: int m_age; virtual void speak() { cout << "Animal-speak()" << endl; } virtual void run() { cout << "Animal-run()" << endl; } }; class Cat : public Animal { public: int m_life; void run() { cout << "Cat-run()" << endl; } }; //调用 Animal *cat = new Cat(); cat->m_age = 20; cat->speak();//直接回去调用父类的虚函数 cat->run();
调用父类的成员函数实现
- 有一种情况子类重写父类的成员函数,要保留父类的成员函数功能
- 重写父类的成员函数
- 在子类成员函数内部先调用父类的成员函数,然后在新增自己的功能
- 相当于OC的super功能
class Animal { public: int m_age; virtual void speak() { cout << "Animal-speak()" << endl; } }; class Cat : public Animal { public: int m_life; void speak() { //先调用父类的成员函数 Animal::speak(); cout << "Cat-speak()" << endl; } };
虚析构函数
- 含有虚函数的类,应该将析构函数声明为虚函数(虚析构函数)
- delete父类指针时,才会调用子类的析构函数,保证析构的完整性
class Animal { public: int m_age; //如果不加virtual,子类使用多态时,不会调用子类的析构函数 virtual ~Animal() { cout << "Animal~Animal()" << endl; } }; class Cat : public Animal { public: int m_life; ~Cat() { cout << "Cat~Cat()" << endl; } }; //调用 Animal *cat = new Cat(); cat->m_age = 20; //如果Animal的析构函数不是虚函数类型,那么他只会调用Animal的析构函数 //不会调用Cat的析构函数 delete cat;