C++ 学习-多态

多态介绍

C++的三大特性之一
分类:

  • 静态多态:函数重载 和 运算符重载,即复用函数名;
  • 动态多态:派生类虚函数 实现运行时的多态

有了虚函数, 基类指针指向基类对象时就使用基类成员(包括变量和函数),指向派生类对象时就使用派生类的成员。即, 基类指针可以按照基类的方式做事,也可以按照派生类的方式做事,它有多种形态, 也称为“多态”

  • 虚函数的唯一用处就是构成多态

  • C++ 提供多态的目的就是 可以通过基类指针对所有派生类(包括直接派生或间接派生)的成员变量和成员函数进行“全方位” 的访问, 尤其是成员函数。如果没有多态,我们就只能访问成员变量

  • 虚函数的注意事项
    1)只需要在虚函数的声明处加上 virtual 关键字,函数定义处可以加也可加
    2) 为了方便,只需要将基类中的函数声明为 虚函数 即可, 这样所有派生类中具有遮蔽关系的同名函数都将自动成为 虚函数。
    3) 只有当派生类的虚函数覆盖基类的虚函数(函数原型相同)才能构成多态(通过基类指针访问派生类函数)。例如基类虚函数的原型为 virtual void func();派生类虚函数的原型为virtual void func(int);那么当基类指针 p 指向派生类对象时, 语句 p->func(100),将出错

  • 构成多态的条件
    1)必须存在继承关系
    2)继承关系中必须有同名的虚函数, 并且它们是覆盖关系(函数原型相同)
    3)存在基类指针, 通过该指针调用虚函数

区别:

  • 静态多态的函数地址早绑定 , 即 在编译阶段才确定函数地址
  • 动态多态的函数地址晚绑定 , 即 在运行阶段才确定函数地址
#include 
using namespace std;

class Animal
{
public:
	void speak()
	{
		cout <<"动物说话"<< endl;
	}
};
class cat :public Animal
{
public:
	void speak()
	{
		cout << "猫在说话" << endl;
	}

};
void dospeak(Animal &animal)
{
	animal.speak();
}
void test()
{
	cat cat;
	//使用子类调用本来参数是父类的函数,也可以,但是此时返回的值任然会是父类中的函数在执行
	//这是由于这种操作属于早绑定,编译阶段函数的地址就已经确定了
	dospeak(cat);
}

int main()
{
	test();
}

以上的情况,使用子类调用本来参数是父类的函数,也可以,但是此时返回的值任然会是父类中的函数在执行
这是由于这种操作属于早绑定,编译阶段函数的地址就已经确定了。

而如何改变上述情况,使得使用子类对象能调用包含父类参数的函数:
即:想让 cat 类执行,那么这个函数地址就不能提前绑定好,需要在运行阶段进行绑定,地址晚绑定。
解决:使用虚函数:

class Animal
{
public:
    //虚函数
	virtual void speak()
	{
		cout <<"动物说话"<< endl;
	}
};
class cat :public Animal
{
public:
	void speak()
	{
		cout << "猫在说话" << endl;
	}

};

解释:

  • 此时,当父类的函数声明了虚函数后,当子类需要调用自己与父类同名函数时,就会忽略父类的虚函数,使用相应子类的成员函数。

  • 动态多态的满足条件:
    1、有继承关系
    2、子类重写父类的虚函数(即与父类的虚函数的函数声明完全相同,子类不加virtual)

  • 动态多态的使用
    即:父类的指针或者引用 ,执行子类对象

class cat :public Animal
{
public:
	void speak()
	{
		cout << "猫在说话" << endl;
	}

};
void dospeak(Animal &animal) // Animal &animal = cat
{
	animal.speak();
}
void test()
{
	cat cat;
	dospeak(cat);//这里
}

多态的底层剖析

class Animal
{
public:
	void speak()
	{
		cout <<"动物说话"<< endl;
	}
};

首先:
在没有 virtual 关键字时,此时该类占用的是 1个字节
当加上 virtual 关键字后,此时该类占用的是4 个字节( )
(无论是什么指针都占4个字节)

class Animal
{
public:
    //虚函数
	virtual void speak()
	{
		cout <<"动物说话"<< endl;
	}
};

虚函数的操作流程解析:
C++ 学习-多态_第1张图片

多态案例(1)——计算器类

多态的优点:

  • 代码组织结构清晰
  • 可读性强
  • 利于前期和后期的扩展以及维护

应用场景:
(1)针对 真正开发中,对于需要对函数进行扩展新的功能时,尽量不修改原来的代码,只进行扩展开放,修改关闭的原则

案例:

//先定义一个计算器的基类(抽象基类)
	class AbcCalculater
	{
	public:
		virtual int calculater()
		{
			return 0;
		}
		int M_A;
		int M_B;
	};
	//扩展加法运算
	class ADDCalculater :public AbcCalculater
	{
	public:
		virtual int calculater()
		{
			return M_A + M_B;
		}

	};
	class SubCalculater :public AbcCalculater
	{
	public:
		virtual	int calculater()
		{
			return M_A - M_B;
		}

	};

	void test()
	{
		enum operation
		{
			add,//枚举默认从0开始
			sub
		};

		AbcCalculater * add_;

		operation oper(add);
	
		switch (oper)
		{
		case operation::add:
			add_ = new ADDCalculater;//使用指针方式创建堆区给计算器的子类的加法类
			add_->M_A = 10;
			add_->M_B = 10;
			cout << add_->calculater() << endl;
			delete add_;//创建堆区,必须手动销毁
			break;
		case operation::sub:
			add_ = new SubCalculater;//使用指针方式创建堆区给计算器的子类的减法类
			add_->M_A = 10;
			add_->M_B = 10;
			cout << add_->calculater() <<endl;
			delete add_;//创建堆区,必须手动销毁
			break;
		default:
			break;
		}
	}

纯虚函数 和 抽象类

纯虚函数
本身父类中的虚函数基本不会被调用,所以父类中的虚函数没有必要进行定义
只进行声明,对虚函数内部不进行定义,而在子类中才进行定义

virtual int calculater() = 0 ;//只进行声明,对虚函数内部不进行定义,而在子类中才进行定义

抽象类

  • 当类中存在了纯函数,这个类就叫做抽象类

特点:

  • 抽象类无法实例化对象
  • 子类必须重写抽象类中的纯虚函数,否则也属于抽象类

举例:

class Base
	{
	public:
		virtual int func() = 0;//定义纯虚函数
	};

	class child_1 :public Base
	{
	public:
		int func()
		{
			cout <<"儿子1"<< endl;
		}
	};

	void test4()
	{
		//Base b;//包含纯虚函数,属于抽象类,不能实例化对象
		Base * b;
		b = new child_1;
		b->func();
		delete(b);//创建了堆区就必须手动释放堆区
		//或者
		child_1 son;
		son.func();
	}

多态案例(2)——制作饮品

	class Drink
	{
	public:
		virtual void zhushui() = 0;
		virtual void jiachaye() = 0;
		virtual void chongpao() = 0;
		virtual void jiazuoliao() = 0;

		void make_drink() //该函数会被继承到子类
		{
			zhushui();
			jiachaye();
			chongpao();
			jiazuoliao();
		}
	};
	
	class Make_tea: public Drink
	{
	public:
		void zhushui()
		{
			cout <<"煮水" << endl;
		}
		void jiachaye()
		{
			cout << "加茶叶" << endl;
		}
		void chongpao()
		{
			cout <<"冲泡" <<endl;
		}
		void jiazuoliao()
		{
			cout << "加作料" << endl;
		}
	};

	void dowork(Drink *s)
	{
		s->make_drink();
		delete(s);
	}
	void test()
	{
		Drink *s = new Make_tea;//实例化子类,调用子类的make_drink()函数
		dowork(s);
	}

虚析构和纯虚析构

多态在使用时,如果子类中有属性开辟了堆区,那么父类指针在释放时无法调用到子类的析构函数
——解决方法:将父类中的析构函数改成 虚析构 或者 纯虚析构

虚析构 和 纯虚析构 共性

  • 可以解决父类指针释放子类对象
  • 都需要有具体的函数实现

虚析构 和 纯虚析构 的区别

  • 如果是纯虚析构,该类属于抽象类,无法实例化对象

虚析构例子:

class Animal
	{
	public:
		Animal()
		{
			cout <<"animal的构造" <<endl;
		}
		virtual ~Animal()//此时使用了虚析构这个方式,系统就会先去释放子类的虚构,再来释放父类这个析构
		{
			cout << "animal 的析构" << endl;
		}
		virtual void speak() = 0;
	};
		
	class cat :public Animal 
	{
	public:
		cat(string s)
		{
			cat_name = new string(s);//使用堆区去维护这个名字
			cout <<"猫的构造" << endl;
		}
		~cat() // 需要注意的是,如果父类的析构不使用虚析构,在使用的父类进行子类实例化对象后,子类的这个析构是不会被调用的
		{
			cout << "调用cat析构" << endl;
			if (cat_name != NULL) 
			{
				delete(cat_name);
				cat_name = NULL;
			}
		}
		void speak()
		{
			cout <<*cat_name << "在说话" << endl;
		}
		string *cat_name;
	};
	void test_work()
	{
		Animal *s = new cat("Tom");
		s->speak();
		delete(s);
	}

结果:
animal的构造
猫的构造
Tom在说话
调用cat析构
animal 的析构

  • 加上虚析构 或者 纯虚析构,系统就会先去释放子类的析构,再来释放父类这个析构
    不然在多态使用时,子类的析构是不会被调用的
virtual ~Animal()//此时使用了虚析构这个方式,系统就会先去释放子类的虚构,再来释放父类这个析构
		{
			cout << "animal 的析构" << endl;
		}
  • 纯虚析构:(更方便,但是使用了纯虚析构,父类就无法再实例化对象了)
class Animal
	{
	public:
		Animal()
		{
			cout <<"animal的构造" <<endl;
		}
		//virtual ~Animal()//此时使用了虚析构这个方式,系统就会先去释放子类的虚构,再来释放父类这个析构
		//{
		//	cout << "animal 的析构" << endl;
		//}
		virtual ~Animal() = 0;//纯虚析构(方便定义)
		virtual void speak() = 0;
	};
Animal::~Animal()
{
	cout << "animal 的纯虚析构" << endl;
}

你可能感兴趣的:(C/C++学习,c++,多态)