C++多态

请先大致浏览以下内容:
虚函数指针与虚函数表的内容
指向子类对象的父类指针的使用
虚析构函数的讲解

本篇博客主要记载以下内容:
(1)多态的基本概念(计算器代码示例)
(2)纯虚函数和抽象类(制作饮品示例)
(3)虚析构函数和纯虚析构函数
对于虚函数指针和虚函数表以及虚基类指针和虚基类表在另一篇博客中详细记录(点击此处查看)。

一、多态的基本概念

1.多态的基本概念

多态分为两类:静态多态和动态多态。
静态多态: 函数重载和运算符重载都属于静态多态。
动态多态: 派生类和虚函数实现运行时多态。
静态多态区别:静态多态的函数地址早绑定,编译阶段确定函数地址。动态多态的函数地址晚绑定,运行阶段确定函数地址。
动态多态满足条件:
(1)有继承关系。
(2)子类要重写父类的虚函数(指函数返回类型和函数名形参列表必须完全相同)。
(3)要通过指向子类对象的父类指针调用重写后的虚函数才会形成动态多态。
用例子发现问题解决问题:

class Animal 
{
public:
	void Speek()
	{
		cout << "动物在叫" << endl;
	}
};

class Cat :public Animal
{
public:
	void Speek()
	{
		cout << "猫在叫" << endl;
	}
};

void doSpeek(Animal& ani)
{ 
	ani.Speek();
}

void test()
{
	Cat cat;
	doSpeek(cat);
}

如上,调用test()函数的时候,ani调用Animal的Speek还是Cat的Speek?运行结果:
在这里插入图片描述
发现调用的是动物的Speek,这是因为普通函数地址早绑定,Animal类型的指针去调用Animal的成员函数。
怎么样让它调用子类的函数呢?有个虚函数的概念。即在成员函数的前面加个关键字virtual。当父类有虚函数时,子类继承父类时,子类有权重写父类的虚函数,与其说修改,不如说是覆盖,用子类的同名函数(返回类型函数名形参列表必须都相同)覆盖到父类上面,其实这个覆盖并不是真正意义上的覆盖。
当父类有虚函数,子类继承父类,子类会包含一个虚函数指针,虚函数指针指向一个虚函数表,而父类的虚函数的地址就放在这个虚函数表里面。通过虚指针查找虚函数表,找到父类的虚函数地址,而当子类重写父类的虚函数时,重写后的函数的地址值就会覆盖掉虚函数表中父类虚函数的地址值。看以下代码,及对应的图(用vs的开发者工具可以看到类中的结构)。
代码:

class Animal 
{
public:
	virtual void Speek()
	{
		cout << "动物在叫" << endl;
	}
};

class Cat :public Animal
{
//public:
//	void Speek()
//	{
//		cout << "猫在叫" << endl;
//	}
};

在这里插入图片描述
以上代码中,父类函Speek是一个虚函数,父类中包含一个虚函数指针,指向一个虚函数表,虚函数表中放着父类的虚函数 Speek的地址,我们这时候调用Speek肯定是父类的虚函数。因为子类根本没有重写父类的虚函数Speek。Speek也只有父类才有。如果子类也写一个Speek函数呢?如下代码;

class Animal 
{
public:
	virtual void Speek()
	{
		cout << "动物在叫" << endl;
	}
};

class Cat :public Animal
{
public:
	void Speek()
	{
		cout << "猫在叫" << endl;
	}
};

void doSpeek(Animal& ani)
{ 
	ani.Speek();
}

void test()
{
	Cat cat;
	doSpeek(cat);
}

父类Animal:
可以看见包含了一个虚函数指针,虚指针指向的是虚函数表,虚函数表中存放的是虚函数Speek的地址。
在这里插入图片描述

子类(Cat):
可以看见子类继承了父类的东西,包括虚指针,虚指针指向虚函数表,虚函数表中本来存放着Animal::Speek的地址,但是由于子类对父类的Speek函数重写了,所以重写后的函数的地址覆盖虚函数中
在这里插入图片描述
发现,vfptr是(virtural function pointer)虚函数指针的意思,指向的是虚函数表(vftable),可以看见虚函数表中放着的变成了子类的Speek函数了。这个时候调用Speek函数,会先通过虚函数指针找到虚函数表,在虚函数表中找到要调用的函数的地址,然后调用Speek函数,这个时候虚函数表中本来存放的父类的Animal::Speek函数由于子类重写Speek函数,所以子类重写的Speek函数覆盖了父类的Speek函数地址。
通过以下代码结合上面的类代码执行:

void doSpeek(Animal& ani)
{ 
	ani.Speek();//通过虚函数指针到虚函数表中查找Speek函数的地址,然后调用。
	ani.Animal::Speek();//编译器通过直接到栈中的代码段找到父类的Speek并调用。
}

void test()
{
	Cat cat;
	doSpeek(cat);
}

int main()
{
	test();
	return 0;
}

执行结果:
在这里插入图片描述
doSpeek()函数里面有两种调用Speek()的方式,第一种方式就是通过虚函数指针,然后在虚函数表中查找Speek函数的地址,然后调用。第二种方式是直接到栈的代码区中找到Animal的Speek函数,然后调用。
在本例中,将父类的Speek函数写成虚函数,这样父类就会生成一个虚函数指针,和一个虚函数表。虚函数表中一开始放着的是父类的虚函数的地址,子类重写父类的虚函数的时候,虚函数表中的函数地址就会替换成子类重写过后的函数的地址。
将函数写成虚函数的形式,会使函数地址晚绑定,即在运行时才绑定,在运行时通过查虚函数表的方式找到要调用的函数。
总结:
形成多态的条件:
(1)要有继承关系。
(2)要有重写。
(3)父类指针(引用)指向(引用)子类对象。
多态的优点:
(1)代码组织结构清晰。
(2)可读性强。
(3)利于前期和后期的维护。

2.计算器案例

根据多态写一个计算器案例。
代码:

class AbstractCalculator
{
public:
	virtual int getResult(){ return 0; }
	int _num1;
	int _num2;
};

//加法计算器
class AddCalculator :public AbstractCalculator//继承
{
public:
	AddCalculator(int num1, int num2)
	{
		_num1 = num1;
		_num2 = num2;
	}
	int getResult()//重写父类虚函数
	{
		return _num1 + _num2;
	}
};

//减法计算器
class SubCalculator :public AbstractCalculator//继承
{
public:
	SubCalculator(int num1, int num2)
	{
		_num1 = num1;
		_num2 = num2;
	}
	int getResult()//重写父类虚函数
	{
		return _num1 - _num2;
	}
};

void test()
{
	AbstractCalculator* abc = new AddCalculator(20, 10);//父类指针或者指针指向子类对象
	cout << abc->getResult() << endl;
	delete abc;

	abc = new SubCalculator(20, 10);
	cout << abc->getResult() << endl;
	delete abc;
	abc = NULL;
}

int main()
{
	test();
	return 0;
}

有关父类指针(引用)指向(引用)子类对象的讲解,请点击此处
运行结果:
在这里插入图片描述
这里可以看见多态带来的好处,对扩展开放,对修改关闭。如果想要添加乘法除法运算,就再写两个类继承抽象计算器类,对里面的虚函数进行重写,当需要修改某一个运算时,只需要将实现这个运算的类的代码修改即可,而不用修改其他类的代码。

二、纯虚函数和抽象类

1.纯虚函数和抽象类的概念

多态中,通常父类中的虚函数的实现是毫无意义的,通常父类将成员函数设置成虚函数是为了让子类重写,让子类根据自己的需求将虚函数重写为子类需要的实现方式。
所以,通常父类可以将这些虚函数写为纯虚函数。
语法: virtual 返回值类型 函数名(参数列表) = 0;
含有纯虚函数的类,被称为抽象类
抽象类的特点:
(1)无法实例化对象
(2)子类必须重写父类的虚函数,否则子类也是一个抽象类。

2.制作饮品案例

考虑制作饮品步骤:
(1)烧水
(2)冲泡(咖啡粉,茶叶,等)
(3)加辅料
对于烧水,如果所有饮品都是用泉水制作的,那么烧水可以写成一个普通函数
对于冲泡,假设每个饮品都必须经过这个步骤,而且具体冲泡什么和饮品种类有关,那么这个就可以写成纯虚函数
对于加辅料,有的饮品需要加,有的不需要,所以这个不适合写成纯虚函数,适合写成虚函数,需要加辅料的可以重写,不需要加辅料的可以不重写,而且不耽误你实例化对象(制作饮品)。

class AbstractDringking
{
	//对外只提供制作方式这一个接口
public:
	//将制作步骤整合
	void makeDrink()//制作饮品
	{
		Boil();
		Brew();
		AddSomething();
	}


	//比如制作饮品步骤都是烧水、冲泡、加辅料这三个步骤
private:
	//比如如果饮品都是用山泉水制作的,那么可以在抽象类里写成普通成员函数
	void Boil()//烧水
	{
		cout << "将泉水烧沸腾" << endl;
	}

	//假设冲泡是必须步骤,冲泡什么要根据饮品而定,可能是冲泡咖啡粉,可能是冲泡茶叶
	virtual void Brew() = 0;                //冲泡

	//加入辅料根据饮品而定,可能是加牛奶,可能是加柠檬,也可能不加辅料,所以将加辅料写成虚函数
	virtual void AddSomething(){}//加入辅料
	
};

class Coffe :public AbstractDringking 
{
private:
	//将父类的纯虚函数进行重写
	void Brew() 
	{
		cout << "冲泡咖啡粉" << endl;
	}
	void AddSomething() 
	{
		cout << "加入牛奶" << endl;
	}
};

class Tea :public AbstractDringking
{
private:
	//将父类的纯虚函数进行重写
	void Brew()
	{
		cout << "冲泡茶叶" << endl;
	}
	void AddSomething()
	{
		cout << "加入柠檬" << endl;
	}
};

void test()
{
	AbstractDringking* aptr = new Coffe();
	aptr->makeDrink();
	delete aptr;

	aptr = new Tea();
	aptr->makeDrink();
	delete aptr;
}

int main()
{
	test();
	return 0;
}

运行结果:
在这里插入图片描述
根据上面步骤可以发现普通成员函数,虚函数,纯虚函数具体设置情况。
(1)如果是子类实现功能的必须步骤,而且每个子类该步骤都一样,那么我们可以在父类中写成普通成员函数,这样每个子类继承后都不用重写了。
(2)如果是子类实现的必须步骤,但是每个子类的该步骤的具体细节不一定一样,那么我们就写成纯虚函数,让每个子类自行实现步骤的细节。
(3)如果不是每个子类实现的具体步骤,即有的子类实现功能需要这一步,有的子类实现功能不需要这一步,那么我们就将这一步设置成虚函数,需要这一步的子类自行重写,不需要这一步的子类,不用重写,也不会影响子类创建对象。

三、虚析构函数和纯虚析构函数

1.构造函数不允许被写虚函数或者纯虚函数

析构函数可以写成虚函数,但是构造函数不允许被写成虚函数或者纯虚函数。虚函数的地址是存放到虚函数表中的,虚函数指针指向虚函数表,具有虚函数的类实例化的对象含有虚函数指针。捋一下逻辑,调用构造函数是为了创建对象,如果把构造函数写成虚函数的形式,那么是想让对象查虚函数表调用构造函数吗?那肯定不是。
虚函数表是通过虚函数指针找到虚函数表的地址,而虚函数指针是存放到对象里的。而对象是通过构造函数创建的,如果再把构造函数写成虚函数,是不是发现形成一个死循环了?
所以构造函数不能被写成虚函数,纯虚函数是虚函数的特例,所以也不能被写成纯虚函数。

2.虚析构函数和纯虚析构函数

多态使用时,如果子类中有属性开辟到堆区,那么由于父类指针无法调用子类的析构函数,会造成堆区无法释放,内存泄漏问题。(对于为什么父类指针无法调用子类的析构函数,看解释请点击此处)。
虚析构函数和纯虚析构函数的共性:
(1)都可以解决父类指针释放子类对象的问题
(2)都需要有具体的函数实现
虚析构函数和纯虚析构函数的区别:
如果含有纯虚析构函数,那么该类为抽象类,无法实例化对象。
先来看看动态过程中析构函数存在的问题。

class A
{
public:
	A() 
	{
		aptr = nullptr;
		cout << "A的构造函数" << endl;
	}
	~ A()
	{
		if (aptr != nullptr)
		{
			cout << "A的析构函数" << endl;
			delete aptr;
		}
	}
public:
	int* aptr;
};

class B :public A
{
public:
	B() 
	{ 
		bptr = new int();
		cout << "B的构造函数" << endl; 
	}
	~B()
	{
		if (bptr != nullptr)
		{
			cout << "B的析构函数" << endl;
		}
	}
public:
	int* bptr;
};

void test()
{
	A* p = new B();
	p->aptr = (int*)malloc(sizeof(int) * 10);
	delete p;
}

int main()
{
	test();
	return 0;
}

运行结果:
在这里插入图片描述
发现指向子类对象的父类指针只调用了父类的析构函数,没有调用子类的析构函数。也就是bptr指向的堆空间没有被释放,这样会造成内存泄漏。原因就是父类指针无法访问子类的成员(除了子类对父类重写的函数,这部分讲解请点击此处 )
(1)虚析构函数
在使用多态的过程中,那么如何使每个类都管理自己的指针呢?换句话说,如何使子类只需要管理自己本身的指针(本例即不需要管理aptr)就好了呢?虚函数可以解决此类问题。只需要将父类的析构函数写成虚析构函数即可。如下图:

class A
{
public:
	A() 
	{
		aptr = nullptr;
		cout << "A的构造函数" << endl;
	}
	virtual~ A()
	{
		if (aptr != nullptr)
		{
			cout << "A的析构函数" << endl;
			delete aptr;
		}
	}
public:
	int* aptr;
};

为什么这样做呢?原因就是虚析构函数会被放进虚函数表里面,而子类的析构函数,编译器会认为是对父类的析构函数的重写(尽管函数名称不一样),子类的析构函数会对虚函数表中父类的虚析构函数进行覆盖,而调用子类的析构函数时,子类里面会调用父类的析构函数。(详细博客点击此处
那这样的话,完美。既能让指向子类对象的父类指针调用子类的析构函数,子类的析构函数里又会调用父类的析构函数。
运行结果:
在这里插入图片描述
(2)纯虚析构函数
纯虚析构函数和其他普通纯虚函数不同,其他普通纯虚函数有声明即可,但是纯虚析构函数必须要有定义。

class A
{
public:
	A() 
	{
		aptr = nullptr;
		cout << "A的构造函数" << endl;
	}
	virtual~A() = 0;//纯虚析构函数
public:
	int* aptr;
};

//对A类纯虚析构函数的定义
A::~A()
{
	{
		if (aptr != nullptr)
		{
			cout << "A的析构函数" << endl;
			delete aptr;
		}
	}
}

class B :public A
{
public:
	B() 
	{ 
		bptr = nullptr;
		cout << "B的构造函数" << endl; 
	}
	~B()
	{
		if (bptr != nullptr)
		{
			cout << "B的析构函数" << endl;
		}
	}
public:
	int* bptr;
};

所以发现纯虚析构函数和虚析构函数没多大差别,唯一差别就是写成纯虚析构函数的类不允许实例化对象。我们说子类必须对父类的纯虚函数进行重写,否则子类不能创建对像。但是对于父类的纯虚析构函数函数来说,子类不写析构函数也可以创建对象,原因是如果子类没有实现析构函数,编译器会自动生成析构函数,就相当于对父类的纯虚析构函数重写。

总结:
(1)对于含有成员指针变量的类,最好把析构函数写成虚析构函数
(2)最好实现父类的析构函数对父类的指针成员变量进行判空释放堆空间,子类的析构函数只需要对子类的指针成员变量进行判空释放堆空间。也就是说每个类的析构函数都考虑也只考虑好自己的指针成员变量可能指向的堆空间的释放。

  • 22
    点赞
  • 76
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
C++中的多态(Polymorphism)是指在父类和子类之间的相互转换,以及在不同对象之间的相互转换。 C++中的多态性有两种:静态多态和动态多态。 1. 静态多态 静态多态是指在编译时就已经确定了函数的调用,也称为编译时多态C++中实现静态多态的方式主要有函数重载和运算符重载。 函数重载是指在同一作用域内定义多个同名函数,但它们的参数列表不同。编译器根据传递给函数的参数类型和数量来确定调用哪个函数。例如: ```c++ void print(int num) { std::cout << "This is an integer: " << num << std::endl; } void print(double num) { std::cout << "This is a double: " << num << std::endl; } int main() { int a = 10; double b = 3.14; print(a); // 调用第一个print函数 print(b); // 调用第二个print函数 } ``` 运算符重载是指对C++中的运算符进行重新定义,使其能够用于自定义的数据类型。例如: ```c++ class Complex { public: Complex(double real, double imag) : m_real(real), m_imag(imag) {} Complex operator+(const Complex& other) const { return Complex(m_real + other.m_real, m_imag + other.m_imag); } private: double m_real; double m_imag; }; int main() { Complex a(1.0, 2.0); Complex b(3.0, 4.0); Complex c = a + b; // 调用Complex类中重载的+运算符 } ``` 2. 动态多态 动态多态是指在运行时根据对象的实际类型来确定调用哪个函数,也称为运行时多态C++中实现动态多态的方式主要有虚函数和纯虚函数。 虚函数是在父类中定义的可以被子类重写的函数,使用virtual关键字声明。当一个对象的指针或引用指向一个子类对象时,调用虚函数时会根据实际的对象类型来确定调用哪个函数。例如: ```c++ class Shape { public: virtual void draw() { std::cout << "Drawing a shape." << std::endl; } }; class Circle : public Shape { public: void draw() override { std::cout << "Drawing a circle." << std::endl; } }; int main() { Shape* shape_ptr = new Circle(); shape_ptr->draw(); // 调用Circle类中重写的draw函数 } ``` 纯虚函数是在父类中定义的没有实现的虚函数,使用纯虚函数声明(如virtual void func() = 0;)。父类中包含纯虚函数的类称为抽象类,抽象类不能被实例化,只能作为基类来派生子类。子类必须实现父类的纯虚函数才能实例化。例如: ```c++ class Shape { public: virtual void draw() = 0; }; class Circle : public Shape { public: void draw() override { std::cout << "Drawing a circle." << std::endl; } }; int main() { Shape* shape_ptr = new Circle(); shape_ptr->draw(); // 调用Circle类中重写的draw函数 } ```

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

孟小胖_H

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值