C++多态

●个人主页:你帅你先说.
●欢迎点赞关注收藏
●既选择了远方,便只顾风雨兼程。
●欢迎大家有问题随时私信我!
●版权:本文由[你帅你先说.]原创,CSDN首发,侵权必究。

为您导航

  • 1.多态的概念
    • 1.1概念
    • 1.2虚函数
    • 1.3虚函数的重写
    • 1.4多态的构成条件
    • 1.5C++11 override 和 final
    • 1.6重载、覆盖(重写)、隐藏(重定义)的对比
  • 2.抽象类
    • 2.1概念
    • 2.2接口继承和实现继承
  • 3.多态的原理
    • ️3.1虚函数表
    • ️3.2多态的原理
    • ️3.3通过地址调用虚函数
    • ️3.4多继承虚函数的存储方式

1.多态的概念

1.1概念

多态的概念:通俗来说,就是多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态。多态分为静态多态动态多态
静态多态:函数重载,调用同一个函数有不同行为,在编译时实现。
动态多态:一个父类的引用或指针去调用同一个函数,传递不同的对象,会调用不同的函数,在运行时实现。

1.2虚函数

虚函数:即被virtual修饰的类成员函数称为虚函数。

class Person 
{
public:
 virtual void BuyTicket() { cout << "买票-全价" << endl;}
};

1.3虚函数的重写

虚函数的重写(覆盖):派生类中有一个跟基类完全相同的虚函数(即派生类虚函数与基类虚函数的返回值类型、函数名、参数列表完全相同),称子类的虚函数重写了基类的虚函数。虚函数重写时,继承父类的虚函数接口,重写的是虚函数实现。

class Person 
{
public:
 virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person 
{
public:
 virtual void BuyTicket() { cout << "买票-半价" << endl; }
/*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后
基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使用*/
 /*void BuyTicket() { cout << "买票-半价" << endl; }*/
};
void Func(Person& p)
{
 	p.BuyTicket(); 
}
int main()
{
	 Person ps;
	 Student st;
	 
	 Func(ps);//传父类调用父类
	 Func(st);//传子类调用子类
	 return 0; 
}
class A{};
class B:public A{};
class Person 
{
public:
 virtual A* BuyTicket() 
 { 
 	cout << "买票-全价" << endl; 
 	return A();
 }
};
class Student : public Person 
{
public:
virtual B* BuyTicket() 
{ 
 	cout << "买票-半价" << endl;
 	return B();
}

注意:重写要求返回值有一个例外,叫做协变。当返回的是父子关系的指针或者引用时也可以构成重写。
在这里问大家一个问题:析构函数构成重写吗?
答案是构成。我们前面讲继承的时候讲过,析构函数名会被编译器处理成destructor,所以会构成重写。
为什么让析构函数构成重载?
我们来看一个场景。

#include
using namespace std;
class Person
{
public:
	~Person()
	{
		cout <<"~Person()"<< endl;
	}
};
class Student:public Person
{
public:
	~Student()
	{
		cout << "~Student()" << endl;
	}
};
int main()
{
	Person* p1 = new Person;
	Person* p2 = new Student;

	delete p1;
	delete p2;

	return 0;
}

C++多态_第1张图片
从运行结果我们就能发现,new出来的Student对象没有被释放,这就会造成内存泄漏,为什么会这样?因为p1和p2都是Person类型的,所以释放时也是调Person的析构函数。所以这种场景下只有虚函数能解决。
总而言之,当动态申请的对象,如果给了父类指针管理,那么需要析构函数是虚函数,因为父类不会自动调用子类析构函数,而如果给了子类管理,在析构的时候,会先析构子类对象,后析构父类对象。
在这里还需要补充的一点是:虚函数的重写允许,两个都是虚函数或父类是虚函数,再满足其它条件,就构成重写。虽然子类没写virtual,但是它是先继承了父类的虚函数属性,再完成重写,那么他也算是虚函数。

1.4多态的构成条件

在继承中要构成多态还有两个条件:
1.必须通过基类的指针或者引用调用虚函数
2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

1.5C++11 override 和 final

现在我们有这样一个需求,我们希望一个类不能被继承要怎么做?
我们只需要把它的构造函数设为私有就行,但我们还希望它自己本身可以定义对象,私有成员函数不能被外部所调用,但类内部可以调用。

class A
{
private:
	A(int a = 0){}
public:
	static A getObj(int a = 0)
	{
		return A(a);
	}
};
int main()
{
	A a = A::getObj(10);
}

在这里解释一下为什么要用静态成员函数,因为普通成员函数只能通过对象来调用,我们没法创建对象,而静态成员函数既可以用对象也可以用类来直接调用。
C++11给我们提供了一个方法可以直接实现以上需求。
1.final:修饰虚函数,表示该虚函数不能再被重写。修饰类,表示类不能被继承

//修饰虚函数
class Car
{
public:
 	virtual void Drive() final {}
};
class Benz :public Car
{
public:
 	virtual void Drive() {cout << "Benz-舒适" << endl;}
};
//修饰类
class Car final
{
public:
 	virtual void Drive() final {}
};

2.override: 检查派生类虚函数是否重写了基类某个虚函数,如果没有重写编译报错。

class Car
{
public:
 	virtual void Drive(){}
};
class Benz :public Car 
{
public:
 	virtual void Drive() override {cout << "Benz-舒适" << endl;}
};

1.6重载、覆盖(重写)、隐藏(重定义)的对比

三个概念的对比
重载
两个函数在同一作用域
函数名/参数不同,返回值没有要求
重写/覆盖
两个函数分别在基类和派生类的作用域
函数名/参数/返回值都必须相同,协变例外
两个函数必须是虚函数
重定义/因此
两个函数分别在基类和派生类的作用域
函数名相同
两个基类和派生类的同名函数不构成重写就是重定义

2.抽象类

2.1概念

在虚函数的后面写上 = 0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承。

class Car
{
public:
 	virtual void Drive() = 0;
};
class Benz :public Car
{
public:
 	virtual void Drive()
 {
 	cout << "Benz-舒适" << endl;
 }
};
class BMW :public Car
{
public:
 	virtual void Drive()
 {
 	cout << "BMW-操控" << endl;
 }
};
int main()
{
	 Car* pBenz = new Benz;
	 pBenz->Drive();
	 Car* pBMW = new BMW;
	 pBMW->Drive();
}

2.2接口继承和实现继承

普通函数的继承是一种实现继承,派生类继承了基类函数,可以使用函数,继承的是函数的实现。虚函数的
继承是一种接口继承,派生类继承的是基类虚函数的接口,目的是为了重写,达成多态,继承的是接口。所
以如果不实现多态,不要把函数定义成虚函数。

3.多态的原理

️3.1虚函数表

class Base
{
public:
 virtual void Func1()
 {
 	cout << "Func1()" << endl;
 }
private:
 	int _b;
 	char _c;
};

大家来算一算这个类是多大(32位下)?
有的人肯定啪的一下给我一个结果8。在这里与以前计算类的大小不同,因为有虚函数,所以类里面会多出一个虚函数表指针。且虚函数表指针是第一个计算,所以根据类的计算方法可以算出结果是12。它的本质是函数指针数组

️3.2多态的原理

class Person 
{
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person 
{
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
void Func(Person& p) 
{
	p.BuyTicket();
}
int main()
{
	Person Mike;
	Func(Mike);

	Student Johnson;
	Func(Johnson);
	return 0;
}

C++多态_第2张图片
通过监视窗口我们观察到,Mike是通过它的虚表指针调用的虚函数,而Johnson是通过从Person类里继承下来的虚表指针调用的Johnson的虚函数。
我们之前说过,如果子类赋值给父类会发生切片,会把子类中继承父类的给切出来,所以最后调用的还是父类的函数,但通过刚刚我们分析,如果对应的函数是虚函数,父类的指针或引用指向子类的切片,那么调用的还是子类的虚函数。
与this指针类似,所有对象共享一个虚表指针。普通的类成员函数和虚函数的存储是一样的,都在公共代码段。只是虚函数要把地址存到虚表,方便实现多态。

#include
using namespace std;
class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}
private:
	int _b = 1;
};
class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Derive d;
	return 0;
}
  1. 派生类对象d中也有一个虚表指针,d对象由两部分构成,一部分是父类继承下来的成员,虚表指针也就是存在部分的另一部分是自己的成员。
  2. 基类b对象和派生类d对象虚表是不一样的,这里我们发现Func1完成了重写,所以d的虚表中存的是重写的Derive::Func1,所以虚函数的重写也叫作覆盖,覆盖就是指虚表中虚函数的覆盖。重写是语法的叫法,覆盖是原理层的叫法。
  3. 另外Func2继承下来后是虚函数,所以放进了虚表,Func3也继承下来了,但是不是虚函数,所以不会放进虚表。
  4. 虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr。
  5. 总结一下派生类的虚表生成:a.先将基类中的虚表内容拷贝一份到派生类虚表中 b.如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 c.派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  6. 这里还有一个很容易混淆的问题:虚函数存在哪的?虚表存在哪的? 答:虚函数存在虚表,虚表存在对象中。注意上面的回答都是错的。但是很多人都是这样深以为然的。注意虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中存的不是虚表,存的是虚表指针。那么虚表存在哪的呢?实际我们去验证一下会发现vs下是存在常量区的。

通过监视窗口带大家对重写有更进一步的理解。
C++多态_第3张图片
我们发现被重写的虚函数地址不一样,没被重写的虚函数地址相同,换句话说,被重写的就是子类自身的虚表指针,没被重写的用的就是父类的虚表指针。
多继承与单继承类似,但当你在观察多继承时(在vs下)你们会发现,子类重写的父类的函数,但虚表中存的地址不一样,这是编译器做的封装,实际上最后调用的是同一个函数。
冷知识:虚表指针是在初始化列表初始化的。

️3.3通过地址调用虚函数

#include
using namespace std;
class base 
{
public:
	base()
	{
		a = 0;
	}
	virtual void func1() { cout << "base::func1" <<endl; }
	virtual void func2() { cout << "base::func2" << endl; }
private:
	int a = -1;
};

class derive :public base 
{
public:
	virtual void func1() { cout << "derive::func1" << endl; }
	virtual void func3() { cout << "derive::func3" << endl; }
	void func4() { cout << "derive::func4" << endl; }
private:
	int b;
};
typedef void(*VF_PTR)();
void PrintVFTable(VF_PTR* table)
{
	for (int i = 0; table[i] != nullptr; ++i)
	{
		printf("vft[%d]:%p->", i, table[i]);
		VF_PTR f = table[i];
		f();
	}
	cout << endl << endl;
}
int main()
{
	base b;
	PrintVFTable((VF_PTR*)(*(int*)&b));//取出前四个字节

	return 0;
}

这种方式调用的虚函数已经不受访问限定符的约束了,因为访问限定符是从语法上去检查,而我们直接通过地址去调用,语法是检查不出来的。
所以即使父类的虚函数变成私有属性,我们还是可以调到虚函数。
上面的(VF_PTR*)(*(int*)&b)也可以写成(VF_PTR*)(*(void**)&b))。可能有的人会疑惑,为什么这样也可以,我们在这边用强制类型转换不是随便都可以转的,它们必须是相关类型才可以转换。
比如
int char都是代表整数可以互相转
int double 表示数据大小也可以互相转
整型 指针 都是用整数来表示也可以互相转
我们来解释一下这个代码,首先&b就是一个base的类型,然后强转成void**,然后解引用得到void的类型,但这种类型还是不能传过去,再强转成VF_PTR*的类型。

️3.4多继承虚函数的存储方式

class Base1 
{
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1;
};
class Base2 
{
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
	int b2;
};
class Derive : public Base1, public Base2 
{
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d1;
};
int main()
{
	Base1 b1;
	
	Base2 b2;
	
	Derive d;
	
	return 0;
}

C++多态_第4张图片
通过调试窗口我们发现子类重写了父类的fun1,为什么它们的地址不一样?我们前面说过,这是vs进行过封装,实际上最后调的是同一个函数。可能就有人会疑惑,那fun3归谁管?怎么在调试窗口看不到,我们可以通过刚才的函数打印出虚函数表。
C++多态_第5张图片
通过打印我们发现fun3是存在先继承的父类的虚函数表里。

喜欢这篇文章的可以给个一键三连 点赞关注收藏

你可能感兴趣的:(C++,C++,后端)