1.多态和虚函数


【1】通过案例了解多态

  • 案例:父类Animal,2个子类Dog和Cat,实现speak方法
  • 用父类指针指向各对象,调用各方法看效果,记下来
#ifndef  __TEST_HPP__
#define  __TEST_HPP__

class Animal
{
public:
	void speak(void);
	
};

class Dog:public Animal
{
public:
	void speak(void);
	
};

class Cat:public Animal
{
public:
	void speak(void);
	
};
#endif 

#include "test.hpp"
#include <iostream>

using namespace std;

void Animal::speak(void)
{
	cout << "Animal speak" << endl;
}

void Dog::speak(void)
{
	cout << "wang wang wang" << endl;
}

void Cat::speak(void)
{
	cout << "miao miao miao" << endl;
}
int main(void)
{
	Animal A;
	Dog dog;
	Cat cat;
	
	Animal* p = &A;
	p->speak();          //打印Animal speak
	
	Animal* p1 = &dog;
	p1->speak();         //打印Animal speak
	
	Animal* p2 = &cat;
	p2->speak();         //打印Animal speak
	
	return 0;
}

  • 将父类speak方法声明为virtual,再用父类指针调用各方法看效果,记下来

class Animal
{
public:
	virtual void speak(void);
};


int main(void)
{
	Animal A;
	Dog dog;
	Cat cat;
	
	Animal* p = &A;
	p->speak();          //打印Animal speak
	
	Animal* p1 = &dog;
	p1->speak();         //打印wang wang wang
	
	Animal* p2 = &cat;
	p2->speak();         //打印miao miao miao
	
	return 0;
}

【2】什么是多态

polymorphism,多态,面向对象的三大特征之一。

从宏观讲,多态就是要实现一套逻辑多种具体适配的执行结果。猫就应该是猫的叫声,狗就应该是狗的叫声。

从微观讲,多态就是要一套代码在运行时根据实际对象的不同来动态绑定/跳转执行相匹配的具体函数
函数声明前加virtual的即是虚函数
虚函数是C++实现多态特性的基础,从语法上讲多态特性的基类方法必须是虚函数

【3】 多态中的override 覆盖

基类中方法声明为virtual,派生类中重新实现同名方法以实现多态,这就叫override(中文为覆盖,或重写)
注意区分override和redefining,微观上最大区别就是是否有virtual,宏观上最大区别就是是否表现为多态

【4】多态一定要通过面向对象和override来实现吗

宏观上的多态是一种编程效果,微观上的多态是一种C++支持的编程技术,微观是为了去实现宏观

不用C++的virtual和override,也可以实现宏观上的多态,C中我们就经常这么干。
C中实现多态的模拟案例:
enum Animal
{
	Dog = 0,
	Cat = 1,
}

void catspeak(void)
{

}

void dogspeak(void)
{

}

void speak(enum Animal a)
{
	if(a == Dog)
	{
		dogspeak();
	}
	else if (a == Cat)
	{
		catspeak();
	}
}

int main(void)
{
	enum Animal a;
	a = Dog;            //当然可以设置为根据用户输入确定a的类型,程序运行时自动去匹配并绑定对应的函数
	speak();

	return 0;
}


C++源生支持多态,实现起来更容易,后续修改和维护更容易,架构复杂后优势更大。

【5】对比下三个概念
中文名称 英文名称 意义
重载 overload 同一个类里面的多个方法,函数名相同, 但参数列表不同
重定义,隐藏 redifining 继承中子类再次实现父类中同名方法然后,把父类方法隐藏掉
覆盖,重写 override 继承中子类去实现父类中同名virtual方法, 然后实现多态特性

2.纯虚函数与抽象类
【1】纯虚函数

纯虚函数就是基类中只有原型没有实体的一种虚函数
纯虚函数形式:virtual 函数原型=0;

class Animal
{
public:
	virtual void speak(void) = 0;
};
纯虚函数为什么没有实体?因为语义上不需要
纯虚函数是否占用内存?不会,因为纯虚函数所在的类根本无法实例化对象

【2】抽象类(abstract type)

至少带有一个纯虚函数的类成为抽象类,允许有其他不是虚函数的成员。抽象类只能作为基类来派生新类,不可实例化对象。
派生类必须实现基类的纯虚函数后才能用于实例化对象
抽象类的作用:将有关的数据和行为组织在一个继承层次结构中,保证派生类具有要求的行为。对应暂时无法实现的函数,可以声明为纯虚函数,留给派生类去实现。这种机制可以让语法和语义保持一致。
抽象类的子类必须实现基类中的纯虚函数,这样子类才能创建对象,否则子类就还是个抽象类

【3】接口(interface)

接口是一种特殊的类,用来定义一套访问接口,也就是定义一套规约
接口类中不应该定义任何成员变量
接口类中所有成员函数都是公有且都是纯虚函数
有些高级语言中直接提供关键字interface定义接口,接口其实就是个纯粹的抽象基类

3.虚析构函数

【1】什么是虚析构函数

析构函数前加virtual,则析构函数变为虚析构函数
规则:基类有1个或多个虚函数时(注意不要求是纯虚函数),则其析构函数应该声明为virtual
【2】为什么需要虚析构函数

代码演示:父子类各自添加析构函数,用2种分配和回收对象的方式分别实验,观察析构函数被调用的规律

#ifndef  __TEST_HPP__
#define  __TEST_HPP__

class Animal
{
public:
	virtual void speak(void) = 0;
	
	~Animal();      //没声明成虚析构函数
};

class Dog:public Animal
{
public:
	void speak(void);
	~Dog();	
};

class Cat:public Animal
{
public:
	void speak(void);	
};
#endif 


#include "test.hpp"
#include <iostream>

using namespace std;
Animal::~Animal()
{
	cout << "~Animal" << endl;
}

Dog::~Dog()
{
	cout << "~Dog" << endl;
}
void Dog::speak(void)
{
	cout << "wang wang wang" << endl;
}

void Cat::speak(void)
{
	cout << "miao miao miao" << endl;
}
int main(void)
{
	/*
	Dog d;           //对象是dog类对象,分配在栈上
	Animal* p = &d;  //析构时释放的是对象,而不是指针
	p->speak();      //实际执行时只执行了父类Animal的析构函数,并没有执行子类Dog的析构函数
	*/
	
	Animal* p = new Dog();  //对象是dog类对象,分配在堆上
	p->speak();
	delete p;       实际执行时只执行了子类Dog的析构函数,并没有执行父类Animal的析构函数
	
	return 0;
}




class Animal
{
public:
	virtual void speak(void) = 0;
	
	virtual ~Animal();     //把含有虚函数的类里面的析构函数声明为虚析构函数
};


结论:虚析构函数在各种情况下总能调用正确的(和对象真正匹配的)析构函数。

【3】分析和总结

其实虚函数的virtual的价值,就是让成员函数, 在运行时动态解析和绑定具体执行的函数,这是RTTI机制的一部分。
析构函数也是成员函数,加virtual的效果和普通成员函数加virtual没什么本质差异
加virtual是有开销的,运行时动态绑定不如编译时静态绑定效率高资源消耗优,但是可以多态。