C++基础梳理 2019.1.15(开闭原则,什么是多态,动态联编和静态联编,重写,编译器如何找函数地址的原理,抽象类和纯虚函数,虚析构和纯虚析构,向上类型转换向下类型转换)

多态

 

什么是多态?

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

 

C++支持编译时多态(静态多态——即运算符重载和函数重载)和运行时多态(动态多态——即派生类和虚函数)。

静态多态和动态多态的区别

函数地址是早绑定(静态联编)还是晚绑定(动态联编)。

  • 如果函数的调用,在编译节点就可以确定函数的调用地址,并产生代码,就是静态多态(编译时多态),就说地址是早绑定的;
  • 如果函数的调用地址不能编译不能在编译期确定,而需要在运行时才能决定,这就属于晚绑定(动态多态,运行时多态)。

 

静态联编的例子

下面代码中cat继承自animal类,对于函数doSpeak,他声明的参数是animal类型,因此调用doSpeak的时候,即使你传入的参数是cat类型,运行结果也是调用animal类型的speak函数

#include<iostream>
using namespace std;

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

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

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

void test01() {
	Cat cat;
	//如果发生了继承的关系,编译器允许进行类型转换
	//因此函数doSpeak里面的参数要求是Animal类型,传入子类cat类型也不会报错
	doSpeak(cat);
}

原因:

上述代码中,调用doSpeak的时候,会在编译阶段找参数的类型,因此调用的是animal类型的speak。speak函数的地址在编译时期就已经绑定好了——早绑定——静态联编。

如果想调用cat类型的speak,那就需要进行晚绑定,即在运行时确定函数地址,即动态联编

 

动态联编(讲到了重写的概念)

写法——将speak改成虚函数

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

};

修改后的运行结果

 

此时我们看一下animal的大小,执行以下程序获得类animal的大小为4

#include<iostream>
using namespace std;

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

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

void test02(){
	cout << sizeof(Animal) << endl;
}

原因:

animal内部的结构中有一个指针vfptr(虚函数指针 virtual function pointer),该指针指向了一张虚函数表中的animal::speak的地址。表中结构如下:

 

对于类cat,如果是如下写法:

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

class Cat :public Animal {
public:
	
};

则此时cat类的内部结构如下:

首先cat类会把animal类的vfptr指针继承下来,还会把父类的虚函数表继承下来。调用构造函数时,将所有虚函数表指针都指向自己的虚函数表,这个操作我们是看不到的

 

将speak函数写回cat中

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

则cat会用这句的speak覆盖掉存储在自己虚表中的父类的speak函数(父类的speak还在)。子类重新写父类中的虚函数speak——重写。重写的返回值、参数个数、类型、参数顺序都相同。

重写并不会改变父类中的东西,只是把子类的虚表中的存有的父类的东西进行了覆盖。

此时cat的内部结构如下:

则此时运行如下代码时,发生了多态,使得父类指针指向子类,调用的是cat里面的speak函数

Animal *animal = new Cat;
animal->speak();

那么父类指针指向子类是怎么实现的?(下面讲解通过函数指针调用cat里面的speak函数)

1、Animal *animal  是一个父类指针,找到了地址指向图中箭头所指的位置。

找到地址之后,做了一个类型的强制转换,以此来设置步长

(int*)animal

之后对改地址进行取 操作,从而找到了自己的虚函数表。虚函数表内部也是一个数组,数组的类型也是int*,因此代码变为:

(int)*(int*)animal

对于上侧代码再一次进行取 * 操作即可获得函数地址

*(int)*(int*)animal

由于函数指针可以指向函数的地址,因此可得如下函数指针

((void(*)()) (*(int)*(int*)animal))()//后面跟着的()起调用函数的作用

因此,这两句的作用是相同的。

animal->speak();

((void(*)()) (*(int*)*(int*)animal))();

 

 

练习,如下代码,调用cat类里面的eat函数

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

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

	virtual void eat() {
		cout << "动物在吃饭" << endl;
	}
};

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

	virtual void eat() {
		cout << "小猫在吃鱼" << endl;
	}
};

void test02(){

	//父类指针指向子类对象 多态
	Animal * animal = new Cat;

	//animal->speak();
	// *(int*)*(int*)animal 函数地址
	//((void(*)()) (*(int*)*(int*)animal))();

	//  *((int*)*(int*)animal+1)猫吃鱼的地址

	//((void(*)()) (*((int*)*(int*)animal + 1)))();
}

int main() {
	test02();

	system("pause");
	return EXIT_SUCCESS;
}

 

上侧已经找到了cat的speak的地址

(int*)*(int*)animal

将该指针+1,再对整体取地址,则找到了eat的地址

*((int*)*(int*)animal+1)

通过函数指针即可调用cat类的eat函数

((void(*)()) (*((int*)*(int*)animal + 1)))();

 

 

做一个计算器

 

第一版——每次有新功能,都要修改类中的代码,难以维护和扩展

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

class Calculator{
public:

	void setv1(int v){
		this->val1 = v;
	}

	void setv2(int v){
		this->val2 = v;
	}


	int getResult(string oper){
		if (oper == "+"){
			return val1 + val2;
		}
		else if (oper == "-"){
			return val1 - val2;
		}
	}
private:
	int val1;
	int val2;
};

void test01(){
	Calculator cal;
	cal.setv1(10);
	cal.setv2(10);
	cout << cal.getResult("+") << endl;
	cout << cal.getResult("-") << endl;
}

 

开闭原则

对扩展开放,对修改关闭。

 

第二版——利用多态

//利用多态实现计算器
class abstractCalculator {
public:

	virtual int getResult() { return 0; };

	void setv1(int v) {
		this->val1 = v;
	}

	void setv2(int v) {
		this->val2 = v;
	}

public:
	int val1;
	int val2;
};

//加法计算器
class PlusCalculator :public abstractCalculator{
public:
	virtual int getResult() {
		return val1 + val2;
	};
};

class SubCalculator : public abstractCalculator {
public:
	virtual int getResult() {
		return val1 - val2;
	};
};

class ChengCalculator :public abstractCalculator {
public:
	virtual int getResult() {
		return val1 * val2;
	};

};

void test02() {
	abstractCalculator * abc;
	//加法计算器
	abc = new PlusCalculator;

	abc->setv1(10);
	abc->setv2(20);

	cout << abc->getResult() << endl;

	delete abc;

	abc = new SubCalculator;
	abc->setv1(10);
	abc->setv2(20);
	cout << abc->getResult() << endl;

	delete abc;

	abc = new ChengCalculator;
	abc->setv1(10);
	abc->setv2(20);
	cout << abc->getResult() << endl;

}

 

 

 

抽象类和纯虚函数

上面例子中,父类的这个return 0 ,没有任何意义

virtual int getResult() { return 0; };

因此可以改一下

virtual int getResult()  = 0;

改完这一句之后,两种代码的运行结果是一样的,那么他改变了什么?

  • 如果父类中有了纯虚函数,那么子类继承父类就必须实现所有的纯虚函数,否则子类也是抽象类
  • 如果子类中有了纯虚函数,那么父类无法实例化对象了。
  • 一个类有了纯虚函数,则这个类叫抽象类——抽象类无法实例化对象
  • 纯虚函数不需要做实现,但是在虚表中会为其保留一个位置,但是这个位置不放地址

 

 

 

虚析构和纯虚析构

 

虚析构

 

观察下面代码

#include "pch.h"
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

class Animal
{
public:

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

	 ~Animal();
};

class Cat :public Animal
{
public:
	Cat(const char * name)
	{
		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 != NULL)
		{
			delete[] this->m_Name;
			this->m_Name = NULL;
		}
	}

	char * m_Name;

};


void test01()
{
	Animal * animal = new Cat("TOM");
	animal->speak();

	delete animal;

}


int main() {

	test01();

	system("pause");
	return EXIT_SUCCESS;
}

小猫的析构函数并没有被调用。因为普通的析构不会调用子类的析构函数。

原因:

基类指针指向了派生类对象,而基类中的析构函数却是非virtual的,之前讲过,虚函数是动态绑定的基础。现在析构函数不是virtual的,因此不会发生动态绑定,而是静态绑定,指针的静态类型为基类指针,因此在delete时候只会调用基类的析构函数,而不会调用派生类的析构函数。

因此需要将析构函数改写成虚析构函数

#include "pch.h"
#define _CRT_SECURE_NO_WARNINGS
#include<iostream>
using namespace std;

class Animal
{
public:

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

	//普通析构 是不会调用子类的析构的,所以可能会导致释放不干净
	//利用虚析构来解决这个问题
	virtual ~Animal()
	{
		cout << "Animal的析构调用" << endl;
	}

};


class Cat :public Animal
{
public:
	Cat(const char * name)
	{
		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 != NULL)
		{
			delete[] this->m_Name;
			this->m_Name = NULL;
		}
	}

	char * m_Name;

};


void test01()
{
	Animal * animal = new Cat("TOM");
	animal->speak();

	delete animal;

}


int main() {

	test01();

	system("pause");
	return EXIT_SUCCESS;
}

 

 

纯虚析构

virtual ~Animal() = 0;
  • 纯虚析构 ,需要声明 还需要实现 类内声明,类外实现
  • 如果函数中出现了 纯虚析构函数,那么这个类也算抽象类
  • 抽象类 不可实例化对象

 

虚析构不影响实例化,纯虚析构不可实例化。

 

 

 

向上类型转换向下类型转换

 

没有多态的情况下

下面的代码就不安全(animal是cat的父类):

new Animal,申请的空间如下图中左侧,cat类型的指针寻址能力如下图中的右侧:

因此上侧的指针类型转换是不安全的。所谓的安全不安全指的是指针的寻址范围。

new一个animal指针,其寻址范围就是animal类的区域。如果强转成cat类,cat类的范围会更大,容易操作到不是自己的内容

 

总结:

  • 类转派生不安全——向转换——指针寻址范围超过申请的空间
  • 派生类转基类安全——向转换——指针寻址范围不会超过申请的空间

 

 

发生多态的情况

如果发生多态,就总是安全的。

对于下侧代码:

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

我们 new 了一个cat那么大的空间,而指针的寻址范围是animal那么大的,因此指针不会操作不属于自己的空间,因此是安全的。

哪怕把animal指针强转成cat类型,我们申请的空间就已经有cat那么大了,因此不用担心指针操作不属于自己的空间。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值