多态——c++

25 篇文章 0 订阅

多态


多态是面向对象语言中封装,继承外的第三个基本特征。(封装和继承都是为了多态服务的)

多态性提供了接口和具体实现间的一层隔离。

多态性改善了代码的可读性和组织性,同时使创建的程序具有可扩展性(项目不仅在最初创建时期扩展,当以后项目需要新功能的是时候也能扩展。)

多态分为两种:静态多态和动态多态,静态多态包括函数重载(函数名相同,但是由于参数不同,调用的函数就不同,但是接口只有一个)还有运算符重载等。静态多态和动态多态的区别就是函数地址是早绑定(静态联编)还是晚绑定(动态联编)



静态联编和动态联编


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

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

void doSpeak(Animal&animal)
{
	animal.speak();
}

int main()
{
	Cat cat;
	doSpeak(cat);//动物在说话
	return 0;地址
}

打印的其实是动物在说话,因为doSpeak函数中传进来的参数的类型就是动物类,所以无论传进来的是小猫对象还是小狗对象调用的都是父类中的speak函数。这种提前将地址绑定好的数据静态联编。

如果想调用小猫说话,那么就不能提前绑定地址,应该在程序进行的时候再绑定地址(动态联编)。

在函数面前加关键字:virtual

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

这样函数就变成了虚函数。 再运行就变成了小猫在说话。

这样就是动态多态。 动态多态的产生条件:先有继承的关系,父类中有虚函数,子类重写父类中的虚函数。

且父类中的指针或引用指向子类的对象。

子类重写虚函数的时候virtual加或不加都可。



虚函数


如果对上面的Animal(在添加关键字virtual之前)进行求字节:普通的成员函数是不算在类的字节中的,所以相当于空类。

在加上关键字之后,就比变成了四个字节。(因为多了一个指针vfptr)

v是virtual,f是function,ptr是pointer。虚函数指针。指向的是虚函数表(vftable)。

虚函数表内部记录的是虚函数入口地址。

Animal类的内部结构:

img

在cat子类还没有重写函数的时候,只是继承了父类Animal 时 的类内部结构:

img

当重写了以后(子类重新写父类中的虚函数)(重写:返回值相同,函数名相同,形参列表相同)(注意:重载和重写是不同的,重载不需要在意返回值和形参列表,只需要函数名相同,而重写要求都一样。)这个继承过来的虚函数入口地址也是要进行改变的,变成了&Cat::speak。

例如:

Animal*animal = new Cat;

animal->speak();//小猫叫

创建的是Cat类型的对象,只是用Animal类型的指针去接收,调用函数的时候调用的还是Cat中的函数。从子类中找speak的入口。

总结:当类内部出现虚函数的时候,类的内部本质发生了变化,类的内部多了一个vfptr的指针,会指向虚函数表。虚函数表中记录着虚函数的入口地址。所以虽然用的是父类的指针指向子类的对象,但是当调用虚函数的时候会从cat类的虚函数表中寻找speak 的入口。看的是指向的对象而不是指针的类型。创建(new)的对象是猫就从猫里找,是狗就从狗里找。

看一下Cat类中的内部结构:

img

有个虚函数指针,是从Animal父类中继承下来的,指向虚函数表。表中是speak等函数的接口,由于Cat类中已经将speak函数重写,所以Cat类中的入口就不再是继承下来的入口了,而是自己的函数入口。地址是0,如果还有其他的函数就以4字节为单位,地址变成2。

在上面的小猫叫代码中,可以直接利用speak()来调用。也可以利用指针偏移来找函数的地址。

指针animal就是函数的首地址,因为需指针就是在0地址的位置。

(int *)animal就是规定步长为4字节,不需要加1,因为已经是虚指针了。

*(int *)animal 解引用到了虚函数表的内部

(int*) *(int *)animal再进行强转是为了规定步长,也不需要+1,因为就一个函数正好是要找的函数地址。

* (int*) *(int *)animal 再解引用到函数speak的具体的地址了。(这个就是个地址0x001)

找到了函数的地址,怎么调用函数?

使用一个函数指针指向这个地址(这里的函数speak指针的返回值是void,形参列表是空的)

(void(*)(//形参列表是空的))//这就是函数指针

((void(*)())(*(int *) * (int * )animal))();这样就调用了。函数返回值+函数名(地址)+();

函数调用的本质是:通过地址偏移,找到虚函数地址入口,然后调用

如果再加个动物吃饭的虚函数,Cat类再重写:

img img

计算器案例


初始的案例:

class calculator
{
public:
	int getResult(string oper)
	{
		if (oper == "+")
		{
			return m_A + m_B;
		}
		else if (oper == "-")
		{
			return m_A - m_B;
		}
		else if (oper == "*")
		{
			return m_A * m_B;
		}

	}

	int m_A;
	int m_B;
};

int main()
{
	calculator c;
	c.m_A = 10;
	c.m_B = 10;
	cout << c.getResult("+") << endl;
	return 0;
}

这种写法不好,因为,如果算除法或者其他运算的时候有没有考虑到的特殊情况,发现后需要修改,那么就必须在类中(原码)上进行修改。从头开始找出错的位置。设计原则:开闭原则(对扩展进行开放,对修改进行关闭 )

所以最好利用多态来实现计算器。(写一个父类抽象计算器,然后里面写一个虚函数(getResult),以及两个属性ab。然后再将原先的加法等运算分别写成计算器类)

#define _CRT_SECURE_NO_WARNINGS 1
#include<iostream>
using namespace std;
#include<string>

class AbstractCalculator//抽象计算器类
{
public:
	virtual int getResult()
	{
		return 0;
	}
	int m_A;
	int m_B;
};
class AddCalculator :public AbstractCalculator//加法
{
public:
	virtual int getResult()
	{
		return m_A + m_B;
	}
};
class SubCalculator :public AbstractCalculator//减法
{
public:
	virtual int getResult()
	{
		return m_A - m_B;
	}
};
class MulCalculator :public AbstractCalculator//乘法
{
public:
	virtual int getResult()
	{
		return m_A * m_B;
	}
};


int main()
{
	AbstractCalculator* calculator = new AddCalculator;//父类的指针指向子类对象
	calculator->m_A = 200;
	calculator->m_B = 100;
	cout << calculator->getResult() << endl;//300
	delete calculator;
    
	calculator = new SubCalculator;
	calculator->m_A = 200;
	calculator->m_B = 100;
	cout << calculator->getResult() << endl;//100

	return 0;
}

这样虽然代码的量比刚才的要多,但是如果出了错或者要添加新的代码功能是十分方便 的。

这个calculate指针一直有,释放了加法计算器还可以让这个指针指向新创建的减法计算器。

个人总结:在同意层次上的不同的函数可以总结一个“抽象”类,类中写个函数的模板。将多个函数改写成不同的类,变成多态的形式。

猫吃饭,狗吃饭,人吃饭。就是一个层次的相似的函数。可以抽象出动物的抽象类,然后将猫,狗,人改写成不同的类,然后在类中对抽象类中的虚函数进行改写。



纯虚函数和抽象类


在上面的例子中,抽象计算机类中的虚函数getResult的返回值是用不到的,实现的内容也是用不到的,所以可以把这个getResult虚函数改成纯虚函数。

virtual int getResult() = 0;//纯虚函数

如果一个类中包含纯虚函数,那么这个类就无法实例化对象,这个类就称为抽象类。

抽象类的子类必须要重写父类的纯虚函数,否则也属于抽象类。

class drink
{
public:
	virtual void one() = 0;
	virtual void two() = 0;
	virtual void three() = 0;
	virtual void last() = 0;
	void MakeDrink()
	{
		one();
		two();
		three();
		last();
	}
};


class tea:public drink
{
	virtual void one()
	{
		cout << "准备茶叶" << endl;
	}
	virtual void two()
	{
		cout << "煮水" << endl;
	}
	virtual void three()
	{
		cout << "煮茶" << endl;
	}
	virtual void last()
	{
		cout << "喝茶" << endl;
	}
};

class coffee:public drink
{
	virtual void one()
	{
		cout << "准备咖啡" << endl;
	}
	virtual void two()
	{
		cout << "煮水" << endl;
	}
	virtual void three()
	{
		cout << "煮咖啡" << endl;
	}
	virtual void last()
	{
		cout << "喝咖啡" << endl;
	}
};

void DoBussiness(drink* drink)
{
	drink->MakeDrink();
	delete drink;
}

int main()
{
	drink* drink = new coffee;
    DoBussiness(drink);
    
    DoBussiness(new coffee)	
	return 0;
}


虚析构和纯虚析构


一个例子:

class Animal
{
public :
	Animal()
	{
		cout << "Animal的构造调用" << endl;
	}
	virtual void speak()
	{
		cout << "动物在说话" << endl;
	}
	~Animal()
	{
		cout << "Animal的析构调用" << endl;
	}
};

class Cat :public Animal
{
public:
	Cat(const char* name)
	{
		cout<< "Cat 的构造调用" << endl;
		this->m_name = new char[strlen(name) + 1];//在堆中为数组开辟空间,要在析构中释放
		strcpy(this->m_name, name);
	}
	virtual void speak()
	{
		cout << "小猫在说话" << endl;
	}

	~Cat()//在释放对象的同时将在堆中开辟的空间删除
	{
		cout << "Cat 的析构调用" << endl;
		if (this->m_name)
		{
			delete[] this->m_name;
			this->m_name = NULL;
		}
	}
	char* m_name;
};


int main()
{
	Animal* animal = new Cat("ketti");
	animal->speak();
	delete(animal);//需要手动释放,因为animal是开辟到堆上的。
	return 0;
}

输出的结果:

Animal的构造调用
Cat 的构造调用
小猫在说
Animal的析构调用

发现在手动释放对象的时候并没有调用小猫的析构(没有将起的名字的空间释放)

用多态的方式将子类的属性放在了堆区(子类中有指向堆区的属性),这样在释放父类指针(指向的对象)的时候,是不会调用子类的析构的。解决方法就是将父类中的析构改写成virtal虚析构。这样就会调用小猫的析构了。如果子类中的属性全是栈上的,那么就不需将父类中的析构写成虚析构。


纯虚析构

将父类中的析构改写成:

virtual ~Animal() = 0;

但是纯虚析构也是需要有实现的,不像上面的父类中的纯虚函数只需要声明,因为子类会进行实现,但是这里的析构父类也可以调用,如果父类指针指向的对象调用析构呢(有一点父类中的东西要删除)?所以这里的纯虚析构既需要声明也需要实现(需要类内声明(类内没地方写了),类外实现。)

class Animal
{
public :
	Animal()
	{
		cout << "Animal的构造调用" << endl;
	}
	virtual void speak()
	{
		cout << "动物在说话" << endl;
	}
	virtual ~Animal() = 0;
};
Animal::~Animal()
{
	cout << "Animall的纯虚析构调用" << endl;
}

注意:如果一个类中,有了纯虚析构,那么这个类也是属于抽象类,无法实例化对象

这个和上面的父类对象调用析构不冲突,因为父类对象调用多态创建的子类对象,也就是说一个对象可以调用两个析构了,

总结:

原先不加virtual 的时候只能调用父类中的对象,加了virtual后两个都可以调用了。但是不是纯虚析构,不能保证父类是抽象类,所以如果要变成纯虚析构,那么就要类内只声明,类外实现(只类内声明不实现会报错)。



向上向下类型转换

Animal* animal = new Animal();
Cat* cat = (Cat*)animal;

这样的写法称为向下类型转换,会出现越界的情况。因为Animal申请的空间比Cat申请的空间要小。寻址范围也小。img

相反的就是向上类型转换,这个是没有问题的。

如果发生多态(父类指针指向子类对象),那么转换是永远安全的

Animal* animal = new Cat();
Cat* cat = (Cat*)animal;
//按照子类的空间申请的,所以不会越界
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

是小明同学啊

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

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

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

打赏作者

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

抵扣说明:

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

余额充值