第六章 多态、虚函数、虚函数表

多态

父类指针、子类指针

  1. 父类指针可以指向子类对象,是安全的,开发中经常用到(继承方式必须是public)
  2. 子类指针指向父类对象是不安全的

多态

  1. 默认情况下,编译器只会根据指针类型调用对应的函数,不存在多态

     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()
    
  2. 多态是面向对象非常重要的一个特性
    1. 同一操作作用于不同的对象,可以有不同的解释,产生不同的执行结果
    2. 运行时,可以识别出真正的对象类型,调用对应子类中的函数
  3. 多态的要素
    1. 子类重写父类的成员函数(override)(父类的成员函数必须是虚函数!)
    2. 父类指针指向子类对象
    3. 利用父类指针调用重写的成员函数

虚函数

  1. C++中的多态通过虚函数(virtual function)来实现
  2. 虚函数:被virtual修饰的成员函数
  3. 只要在父类中声明为虚函数,子类中重写的函数也自动变成虚函数(也就是说子类中可以省略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()
    
  4. 注意:你要使用哪个父类指针做多态,那么至少在那个父类指针或父类的父类用virtual修饰
    1. 意思就是如果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()
       	}
      
  5. 父类指针可以指向子类对象,继承方式必须是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();
    

虚函数表

  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 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;
     }
    
    1. 反汇编查看cat->speak();cat->run();这两句代码
      1. 将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)  
        
        1. 可以看出上面调用的结果分别是固定地址,静态调用call 地址
      2. 将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  
        
        1. 可以看出调用过程是动态调用的。call 寄存器
    2. 有虚函数对象的内存

       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;    
      
      1. 虚函数对象会多4个字节的内存,而且是在最前面,用于存储虚表的地址值,根据这个地址值能找到一张虚表。
  2. 内存图(x86环境的图)

    图1

    1. 对象的前4个字节用于存储虚表的地址值
    2. 虚表里面前4个字节用于存储Cat的speak成员函数地址值,后4个字节用于存储run成员函数的地址值
    3. 所有的Cat对象(不管在全局区、栈、堆)共用同一份虚表
    4. cat->speak();调用
      1. 找到cat对象的首地址
      2. 然后拿到前4个字节存储的地址值
      3. 根据地址值找到虚表
      4. 找到speak函数的首地址,然后调用。
  3. 虚表汇编分析

    1. 调用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  
      
    2. 调用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使用小知识点

  1. VS的内存窗口打开
    1. 调试->窗口->内存->内存1

子类没有重写父类成员函数

  1. 如果子类对象没有重写父类的虚函数,那么子类对象调用该函数时,首先到当前类中找,找不到在到父类中找
  2. 本质:子类仍然有虚表,子类的虚表存的是父类的虚函数地址

     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();
    

调用父类的成员函数实现

  1. 有一种情况子类重写父类的成员函数,要保留父类的成员函数功能
    1. 重写父类的成员函数
    2. 在子类成员函数内部先调用父类的成员函数,然后在新增自己的功能
    3. 相当于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;
     	}
     };
    

虚析构函数

  1. 含有虚函数的类,应该将析构函数声明为虚函数(虚析构函数)
    1. 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;
    
Table of Contents