本文最后更新于:2021年1月27日 晚上
                
              
            
            
              概览:C++面向对象之多态,虚函数、动态联编(动态绑定)、虚析构函数、纯虚函数以及抽象类,以及多态的原理——虚函数指针和虚函数表。
多态是C++面向对象三大特性之一。
向不同的对象发送同一个消息(即调用函数),不同对象会产生不同的响应(函数实现)。通过多态性可以实现“一个接口,多种方法”。
形式有:
需求——“千人千面”
需求:在函数void play(Animal *a);中传递不同的对象,传入什么对象,就调用其对应的函数。
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
   | class Animal { public: 	void talk() { 		cout << "动物发出声音"<<endl; 	} };
  class Cat:public Animal { public: 	void talk() { 		cout << "小猫喵喵喵" << endl; 	} };
  class Dog :public Animal { public: 	void talk() { 		cout << "小狗汪汪汪" << endl; 	} };
  void play(Animal *a) { 	a->talk(); }
  void test() {
  	Animal *a = new Animal(); 	play(a); 	delete a;
  	Cat * c = new Cat(); 	play(c); 	delete c;
  	Dog* d = new Dog(); 	play(d); 	delete d; }
 
  | 
 
输出结果为:
- 首先,无论在派生方式是
public的方式下, 派生类的对象是可以赋值给基类指针的。所以上述函数play()的调用是没有问题的。 
- 然后从结果来看,函数无论传递的是基类还是派生类,最终调用的一定是基类方法。静态联编,(编译器默认做了一个安全处理,它认为,不管传递基类还是派生类对象,如果统一执行基类方法,一定可以成功)。
 
上述代码中,基类与派生类的方法是同名的方法,而我们的本意是根据传递对象的不同来显示不同的结果。最普通的方式那就是写三个同名函数,根据参数不同构成重载来解决这个问题,但是设想有1000个这样的不同类呢?
对于这种有继承关系的类,可以使用虚函数来解决这个问题。
定义虚函数
在基类的方法上添加关键字virtual,然后派生了对于同名方法进行各自的重写,然后再统一根据基类指针来作为参数传递时,就可以达到我们想要的效果,“千人千面”,这就是多态。
- 语法:基类中
virtual 函数返回值 函数名(参数),这样就是一个虚函数。 
virtual关键只需要在类内声明时写,在类外实现时不需要。 
- 而对于派生类,如果它重写了基类的虚函数,关键字
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 37 38 39 40 41 42
   | class Animal { public:
  	 	virtual void talk() { 		cout << "动物发出声音"<<endl; 	} };
  class Cat:public Animal { public: 	 	void talk() { 		cout << "小猫喵喵喵" << endl; 	} };
  class Dog :public Animal { public: 	void talk() { 		cout << "小狗汪汪汪" << endl; 	} };
  void play(Animal *a) { 	a->talk(); }
  void test() {
  	Animal *a = new Animal(); 	play(a); 	delete a;
  	Cat * c = new Cat(); 	play(c); 	delete c;
  	Dog* d = new Dog(); 	play(d); 	delete d; }
 
  | 
 
执行结果:
静态联编与动态联编
联编:指一个程序模块、代码之间相互关联的过程。
静态联编就是在编译阶段就确定好了函数的地址,也称为早期匹配、早绑定。
对于第一块代码,它就属于静态联编。在编译时,编译器会自动地根据指针类型来判断指向的是一个什么样的对象,所以编译器认为基类指针指向地是基类对象。而程序并没有运行,所以不可能知道基类指针指向地具体是基类对象还是派生类对象。从程序安全地角度考虑,编译结果选择调用基类的成员函数,这种特性就是静态联编。
重载函数和运算符重载就是使用的静态联编的方式。
而虚函数可以使得程序在运行阶段具体决定调用哪个类的方法,而不是按编译阶段绑定的基类方法执行,这称为动态联编,又称为晚期绑定、迟绑定、动态绑定。
当我们使用基类的引用或者指针时调用一个虚函数时,将会发生动态绑定。
多态发生的条件
基于虚函数的多态:
- 要有继承
 
- 要有虚函数以及派生类对虚函数的重写。
 
- 要有基类指针或者引用来指向派生类对象。
 
当基类的「指针」或者「引用」指向了基类对象或者派生类对象,调用哪个虚函数,取决于引用的对象是哪种类型的对象,这就是多态。
多态的作用
通过看两段代码来做对比
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
   |  class Calcutor {
  public:
  	int getResult(string op) { 		if (op == "+") { 			return m_numA + m_numB; 		} 		else if (op == "-") { 			return m_numA - m_numB; 		} 		else if (op == "*") { 			return m_numA * m_numB; 		} 		else return INFINITY; 	}
  	int m_numA; 	int m_numB;
  };
  void test2() {
  	
  	Calcutor c; 	c.m_numA = 10; 	c.m_numB = -10;
  	cout << c.m_numA << " + " << c.m_numB << " = " << c.getResult("+") << endl; 	cout << c.m_numA << " - " << c.m_numB << " = " << c.getResult("-") << endl; 	cout << c.m_numA << " * " << c.m_numB << " = " << c.getResult("*") << endl;
  }
 
  | 
 
- 当这个计算器要增加新的功能时,则需要对其源代码进行修改。而代码维护的原则是尽量不修改原有代码。
 
- 而且这个代码后期维护起来并不方便。
 
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
   |  class AbstractCalculator { public:
  	virtual int getResult() { 		return 0; 	}
  	int m_Num1; 	int m_Num2; };
 
  class AddCalculator:public AbstractCalculator { public: 	int getResult() { 		return m_Num1 + m_Num2; 	} };
 
  class SubCalculator :public AbstractCalculator { public: 	int getResult() { 		return m_Num1 - m_Num2; 	} };
 
  class MulCalculator :public AbstractCalculator { public: 	int getResult() { 		return m_Num1 * m_Num2; 	} };
  void test1() { 	
  	cout << "加法测试:" << endl;
  	AbstractCalculator * cal = new AddCalculator; 	cal->m_Num1 = 10; 	cal->m_Num2 = 20; 	cout << cal->m_Num1 << " + " << cal->m_Num2 << " = " << cal->getResult() << endl; 	delete cal;	
  	cout << "减法测试:" << endl;
  	cal = new SubCalculator; 	cal->m_Num1 = 10; 	cal->m_Num2 = 20; 	cout << cal->m_Num1 << " - " << cal->m_Num2 << " = " << cal->getResult() << endl; 	delete cal;
  	cout << "乘法测试:" << endl;
  	cal = new MulCalculator; 	cal->m_Num1 = 10; 	cal->m_Num2 = 20; 	cout << cal->m_Num1 << " * " << cal->m_Num2 << " = " << cal->getResult() << endl; 	delete cal;
  }
 
  | 
 
- 而通过多态实现的计算器,前面构造一个抽象的计算器类,定义好虚函数。
 
- 增加新的功能只需要在原有基础上构建一个新的派生类即可。
 
优点:
- 代码组织结构清晰
 
- 可读性强
 
- 利于前期和后期的扩展以及维护
 
一个更易懂的例子:LOL英雄联盟游戏:https://mp.weixin.qq.com/s/CeCuXuCjYROgNLmUiLtlHA
在普通成员函数中的多态
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
   | class Base { public: 	void func1() {          		this->func2(); 	}
  	virtual void func2() { 		cout << "Base::func2虚函数" << endl; 	}
  };
  class Derived :public Base { public: 	virtual void func2() { 		cout << "Derived::func2函数" << endl; 	} };
  void test1() {
  	
  	Base * pBase = new Derived(); 	pBase->func1();			 	delete pBase;
  }
 
  | 
 
pBase指针实际指向了派生类的对象,而派生类中并没有显式的func1()函数,但是它有继承自基类的函数,在Base::func1()之中执行this->func2(),this基类指针指向了派生类对象,调用了虚函数,发生了多态。
- 结论:在类中的非构造、非析构函数中调用虚函数,依旧是多态。
 
构造函数与析构函数能否实现多态?
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57
   | class Father { public:
  	Father() { 		cout << "Father构造函数" << endl; 		this->hello(); 	}
  	virtual ~Father() { 		cout << "Father析构函数" << endl; 		this->bye(); 	}
  	virtual void hello() { 		cout << "hello in Father" << endl; 	}
  	virtual void bye() { 		cout << "bye in Father" << endl; 	} };
  class Son :public Father { public:
  	Son() { 		cout << "Son构造函数" << endl; 		this->hello(); 	}
  	~Son() { 		cout << "Son析构函数" << endl; 		this->bye(); 	}
  	void hello() { 		cout << "hello in Son" << endl; 	}
  	void bye() { 		cout << "bye in Son" << endl; 	}
  };
  void test2() {
  	
  	Father * father = new Son();
  	cout << "----" << endl; 	father->hello(); 	cout << "----" << endl;
  	delete father; }
 
  | 
 
执行结果为:
1 2 3 4 5 6 7 8 9 10 11
   | Father构造函数 hello in Father Son构造函数 hello in Son ---- hello in Son ---- Son析构函数 bye in Son Father析构函数 bye in Father
 
  | 
 
从结果可见,在构造和析构函数中调用虚函数时,不会发生多态。
在编译时就会确定,调用的是自己类的函数还是基类的函数,不会发生动态联编。
原因:触发派生类的构造函数必然先触发基类的构造函数,而这时派生类的部分还没有构造,怎么可能能用虚函数实现动态绑定派生生类对象呢,所以构造B基类部分的时候,调用的基类的函数bar;同样的道理,当调用继承层次中某一层次的类的析构函数时,往往意味着其派生类部分已经析构掉,所以也不会呈现出多态。
https://blog.csdn.net/yesyes120/article/details/79627028
虚析构函数——引例
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
   | class Base { public: 	Base() { 		cout << "Base构造函数" << endl; 	} 	~Base() { 		cout << "Base析构函数"<<endl; 	} 	 	virtual void func() = 0; };
  class Son :public Base { public: 	Son() { 		cout << "Son构造函数" << endl; 	} 	~Son() { 		cout << "Son析构函数" << endl; 	} 	virtual void func() { 		cout << "Son func函数" << endl; 	} };
  void dosome(Base* b) { 	b->func(); 	delete b;	 }
  void test3() { 	dosome(new Son()); }
 
  | 
 
调用test3()函数,输出结果为:
1 2 3 4
   | Base构造函数 Son构造函数 Son func函数 Base析构函数
 
  | 
 
- 首先,派生类的构造函数在构造对象时一定会调用基类的构造函数。
 
- 但是在代码28行,
delete释放空间时,仅仅调用了基类的析构函数,并没有调用派生类的构造函数。 
危害:如果派生类中比基类中额外有一些存放于堆中的数据,由于无法调用派生类的析构函数,会存留一些数据无法释放,从而可能造成内存泄漏等问题。
解决方式:将基类中的析构函数修改为虚析构函数或者纯虚析构函数。
纯虚析构函数见纯虚函数那一小节。
解决方案——虚析构函数
把基类的析构函数声明为virtual即可。
1 2 3 4 5 6 7 8 9 10 11
   | class Base { public: 	Base() { 		cout << "Base构造函数" << endl; 	} 	virtual ~Base() { 		cout << "Base析构函数"<<endl; 	} 	 	virtual void func() = 0; };
 
  | 
 
再次调用test3()可得输出结果:
1 2 3 4 5
   | Base构造函数 Son构造函数 Son func函数 Son析构函数 Base析构函数
 
  | 
 
通过这样的方式,当通过基类指针释放派生类的对象时,首先调用派生类的析构函数,然后再调用基类的析构函数,依旧遵循继承中的「先构造,后虚构」的规则。
虚析构函数的使用习惯
- 如果一个类定义了虚函数,则应当将析构函数也定义成为虚函数。
 
- 一个类打算作为基类使用时,也应该将析构函数定义成虚函数。
 
- 构造函数不能定义为虚函数。
 
纯虚函数与抽象类
在多态中,通常基类中虚函数的实现是毫无意义的,主要都是调用派生类重写的内容,因此我们可以将基类中的虚函数修改成为纯虚函数。
纯虚函数语法:virtual 返回值类型 函数名(参数) = 0;
当类中有了纯虚函数时,这个类也就成为了抽象类。
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
   | class Animal { public: 	 	virtual void talk() = 0; };
  class Cat :public Animal { public: 	 	void sleep() { 		cout << "猫咪睡觉" << endl; 	} };
  void test1() { 	 	 	 }
  class Dog :public Animal { public: 	 	void talk() { 		cout << "小狗发出了汪汪汪的声音" << endl; 	} };
  void test2() { 	Dog d;		 	d.talk();	
  	Animal* animal = new Dog(); 	animal->talk(); }
 
  | 
 
抽象类特点
一旦拥有了纯虚函数,那这个类就属于抽象类,抽象类负责定义接口,而其余类负责覆盖接口。
- 抽象类不能够实例化对象
 
- 派生类继承抽象类之后,如果不重写基类的纯虚函数,那这个派生类依旧是抽象类。
 
- 抽象类的指针和引用可以指向由抽象类派生出去的类的对象。
 
- 拥有了纯虚析构函数的类也属于抽象类,也不能够实例化对象。
 
纯虚析构函数
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
   | class Animal { public: 	Animal() { 		m_id = nullptr; 		cout << "Animal无参构造函数" << endl; 	} 	Animal(int *id) :m_id(id) { 		cout << "Animal有参构造函数" << endl; 	}
  	 	virtual ~Animal() = 0;
  	 	virtual void func() = 0;
  	int* m_id; };
  Animal::~Animal() { 	 	if (m_id != nullptr) { 		delete m_id; 		m_id = nullptr; 	} 	cout << "Animal析构函数" << endl; }
  class Cat :public Animal { public: 	Cat() { 		cout << "Cat无参构造函数" << endl; 	}
  	Cat(int *id) :Animal(id) { 		cout << "Cat有参构造函数" << endl; 	}
  	~Cat() { 		cout << "Cat析构函数" << endl; 	}
  	 	virtual void func() { 		if(m_id != nullptr) 			cout << "m_id: "<<*m_id << endl; 	}
  };
  void test() { 	int *id = new int(20); 	Animal *animal = new Cat(id); 	animal->func(); 	delete animal;
  	cout << "--------------" << endl;
  	animal = new Cat(); 	animal->func(); 	delete animal; }
 
  | 
 
输出结果为:
1 2 3 4 5 6 7 8 9 10
   | Animal有参构造函数 Cat有参构造函数 m_id: 20 Cat析构函数 Animal析构函数 -------------- Animal无参构造函数 Cat无参构造函数 Cat析构函数 Animal析构函数
 
  | 
 
注意:
- 对于纯虚析构函数,类内按照普通的方式书写,但是类外需要写其实现,尤其是基类对象中含有指针时。
 
- 对于派生类中要给基类中的某些数据赋值时,调用基类的有参构造要在初始化列表的位置。
 
多态的实现原理
- 当类中声明虚函数时,编译器会在类中生成一个虚函数表。
 
- 虚函数表是一个存储类成员函数指针的数据结构。
 
- 虚函数表时由编译器自动生成和维护的。
 
- virtual成员函数会被编译器放入虚函数表中。
 
- 当存在虚函数时,每个对象中都有一个指向虚函数表的指针。
 
- 通过虚函数表指针调用重写函数是在程序运行时进行的,需要通过寻址操作才能确定真正确定应当调用的函数。在效率上是要低于普通的成员函数的。
 
当类含有虚函数时其类的大小
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
   | class Cat { public: 	int age; 	void func() {} };
  class Dog { public: 	int age; 	virtual void func() {} };
  class Fox { public: 	virtual void func() {} };
  void test() {										 	cout << "Cat sizeof = " << sizeof(Cat) << endl;	 	cout << "Dog sizeof = " << sizeof(Dog) << endl;	 	cout << "Fox sizeof = " << sizeof(Fox) << endl;	 }
 
  | 
 
编译器:VS2017

在编译器中选择x86的按钮,即可选择位数,x86就是32位,其对应的指针就是32位,4字节。
x64就是64位,其对应的指针就是64位,8字节。
- Dog在64位下,sizeof为16的原因是,4字节int,8字节虚函数指针,以及4字节用于对齐。
 
以下的代码都基于x86模式下来使用。
虚函数指针与虚函数表
每一个有虚函数的类或者是「含有虚函数类的」派生类都有一个虚函数表,该类的任何对象中都会存放着虚函数表的指针,而寻函数表中会有该类的虚函数的地址。
故上面类的大小中,比普通函数多出来的那些字节就是用来存放「虚函数表的地址」。
看一段代码
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
   | class Base { public: 	int i; 	virtual void Print() {} };
  class Derived :public Base { public: 	int n; 	virtual void Print() {} };
  class DerOther :public Base { 	 };
 
  | 
 
然后使用VS开发人员命令工具,cl /d1 reportSingleClassLayoutBase "17 虚函数表.cpp"来查看类的构造。
使用链接:http://www.colourso.top/vs-use/
类Base的结构与虚函数表:
1 2 3 4 5 6 7 8 9 10 11 12
   | class Base      size(8):         +---  0      | {vfptr}  4      | i         +---
  Base::$vftable@:         | &Base_meta         |  0  0      | &Base::Print
  Base::Print this adjustor: 0
 
  | 
 
类Derived的结构与虚函数表:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
   | class Derived   size(12):         +---  0      | +--- (base class Base)  0      | | {vfptr}  4      | | i         | +---  8      | n         +---
  Derived::$vftable@:         | &Derived_meta         |  0  0      | &Derived::Print
  Derived::Print this adjustor: 0
 
  | 
 
类DerOther的结构与虚函数表
1 2 3 4 5 6 7 8 9 10 11 12
   | class DerOther  size(8):         +---  0      | +--- (base class Base)  0      | | {vfptr}  4      | | i         | +---         +---
  DerOther::$vftable@:         | &DerOther_meta         |  0  0      | &Base::Print
 
  | 
 
- vfptr —— virtual function pointer,即虚函数指针。指向了虚函数表。
 
- vftable —— virtual function table,即虚函数表。表内会记录虚函数的地址。
 
- &Base::Print —— 前面的取地址符加上
Base::Print表示该函数的地址。 
看上述例子,类DerOther原样继承了类Base,DerOther的虚函数表是原样地继承了Base的虚函数表,故表内的虚函数地址是Base的虚函数的地址。
而看类Derived,它继承了类Base,并且重写了它的虚函数,这时子类中的虚函数内部的虚函数地址会替换为子类的虚函数地址。

案例与图片来自:https://mp.weixin.qq.com/s/CeCuXuCjYROgNLmUiLtlHA【公众号:小林coding】
当父类的指针或者引用指向了子类对象时候,会发生多态。当调用Print()函数时,会通过虚函数表来确定其对应的虚函数地址,从而会有不同的对象不同的行为。
B站视频链接:https://www.bilibili.com/video/BV1et411b73Z?p=136【黑马】
证明虚函数指针的作用
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
   | class Parent { public: 	virtual void func() { 		cout << "Parent::func" << endl; 	} };
  class Son :public Parent { public: 	virtual void func() { 		cout << "Son::func" << endl; 	} };
  void test() { 	Parent * pptr = new Son(); 	pptr->func();	
  	Parent parent;
  	int *p1 = (int*)&parent; 	int *p2 = (int*)pptr;	
  	*p2 = *p1;		 	pptr->func();		
  }
 
  | 
 
最终的输出结果为
类Parent的结构与虚函数表
1 2 3 4 5 6 7 8 9
   | class Parent    size(4):         +---  0      | {vfptr}         +---
  Parent::$vftable@:         | &Parent_meta         |  0  0      | &Parent::func
 
  | 
 
类Son的结构与虚函数表
1 2 3 4 5 6 7 8 9 10 11
   | class Son       size(4):         +---  0      | +--- (base class Parent)  0      | | {vfptr}         | +---         +---
  Son::$vftable@:         | &Son_meta         |  0  0      | &Son::func
 
  | 
 
int *p1 = (int*)&parent;是将类Parent的头4个字节 也就是「虚函数表指针」存储到了p1指针中。
x86下,指针4字节
int *p2 = (int*)pptr;是将类Son的头4个字节「虚函数表指针」存储到了p2指针中。
*p2 = *p1;就是将Parent的虚函数表的地址赋值给Son的虚函数表的地址。
再次调用函数,依据虚函数表里的函数最终访问到了基类的函数。
参考链接:掌握了多态的特性,写英雄联盟的代码更少啦!【小林coding】