C++之多态篇(超详细版)

1.多态概念

多态就是多种形态,表示去完成某个行为时,当不同的人去完成时会有不同的形态,举个例子在车站买票,可以分为学生票,普通票,军人票,每种票的价格是不一样的,当你是不同的身份时去车站买票,就需要交不同的价钱,这个就是表示多态的行为。

2.多态的定义

cpp 复制代码
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票全价" << endl;
	}
};

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


void Func(Person& people)
{
	people.BuyTicket();
}

void test()
{
	Person Mike;
	Func(Mike);

	Student s;
	Func(s);
}
int main()
{
	test();
	return 0;
}

上面的代码就是简单的多态定义,对于初学者看到上面的代码可能会一脸懵,别着急,容我细细为你们分析!

3.虚函数的重写

在上面我们提到了虚函数,解释了什么是虚函数,那么如何重写虚函数呢?

(1)重写虚函数(也叫覆盖)是派生类中重写出一个和基类的虚函数完全相同的虚函数,什么是完全相同呢?(派生类的虚函数和基类的虚函数的返回类型和函数名和参数列表都相同).

是不是派生类中虚函数也有virtual关键字,如果我们把它去掉可以吗?

我们看看下面代码

cpp 复制代码
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票全价" << endl;
	}
};

class Student :public Person
{
public:
	void BuyTicket()
	{
		cout << "买票半价" << endl;
	}
};


void Func(Person& people)
{
	people.BuyTicket();
}

void test()
{
	Person Mike;
	Func(Mike);

	Student s;
	Func(s);
}
int main()
{
	test();
	return 0;
}

这里有老铁就会疑问了,为什么派生类虚函数可以没有virtual关键字呢?我们来调试一下代码吧

我们发现派生类继承下来了基类的虚函数,所以派生类也保持着虚函数的属性,所以程序没有问题,虽然程序没问题,但是这种写法不规范,不建议使用。

虚函数重写的两个特殊情况

1.协变:基类虚函数和派生类虚函数的返回值类型不同(基类返回的是基类对象的指针/引用;派生类返回的是派生类对象的指针/引用)

cpp 复制代码
class A {};
class B : public A {};
class Person {
public:
	virtual A* f() { return new A; }
};
class Student : public Person {
public:
	virtual B* f() { return new B; }
};

2.虚构函数的重写(基类和派生类的函数名不同)

如果基类虚函数是析构函数,那么派生类的虚构函数无论有没有virtual关键字都会对基类析构函数构成重写。

cpp 复制代码
class A {
public:
	virtual ~A()
	{
		cout << "~A()" << endl;
	}
};

class B : public A 
{
public:
	~B()
	{
		cout << "~B()" << endl;
	}
};

int main()
{

	A* p1 = new A;
	B* p2 = new B;
	delete p1;
	delete p2;
	
	return 0;
}

代码完全没问题

如果派生类和基类析构函数的虚函数的函数名不同会也能构成重写。这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor。

C++override和final关键字

看完上面的文章,我们知道C++的对函数的重写要求很严格,但在某些时候我们可能会出现写错函数名从而导致函数不能进行重载,这个错误编译阶段是不会报错的,所以如果我们debug就很难受了。所以C++11提供了override和final关键字来帮助我们检查是否完成重写。

我们来看看出现基类的虚函数和派生类的虚函数的函数名不同,看编译器会不会在编译阶段报错

cpp 复制代码
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票全价" << endl;
	}
};

class Student :public Person
{
public:
	virtual void BuyTickte()
	{
		cout << "买票半价" << endl;
	}
};


void Func(Person& people)
{
	people.BuyTicket();
}

void test()
{
	Person Mike;
	Func(Mike);

	Student s;
	Func(s);
}
int main()
{
	test();
	return 0;
}

编译阶段没有任何问题,我们再来看看运行结果,结果应该是买票全价和买票半价

###结果出错了,在我们不知情的情况下去debug就很难查找出原因了。

我们再加上override关键字试试

cpp 复制代码
class Person
{
public:
	virtual void BuyTicket()
	{
		cout << "买票全价" << endl;
	}
};

class Student :public Person
{
public:
	virtual void BuyTickte() override
	{
		cout << "买票半价" << endl;
	}
};


void Func(Person& people)
{
	people.BuyTicket();
}

void test()
{
	Person Mike;
	Func(Mike);

	Student s;
	Func(s);
}
int main()
{
	test();
	return 0;
}

我们再来看看编译结果

直接就报错没有重写基类,所以证明了override关键字可以帮助我们检查派生类是否和基类构成重写。

我们明白了override关键字的作用,那么final关键字作用是什么呢?

final关键字修饰虚函数,表示该虚函数不能再被重写了。

cpp 复制代码
class Person
{
public:
	virtual void BuyTicket() final
	{
		cout << "买票全价" << endl;
	}
};

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

看看编译结果

重载/重写/重定义三个概念进行对比

4.抽象类

在虚函数后面写上=0,就表示纯虚函数,包含纯虚函数的类叫抽象类(也叫接口类),抽象类不能实例化出对象

cpp 复制代码
//抽象类
class Person
{
public:x
	//纯虚函数
	virtual void BuyTicket()=0
	{
		cout << "买票全价" << endl;
	}
};
int main()
{
	Person Mike;
	return 0;
}

如果要实例化就直接报错了

那我们看看派生类继承了抽象类会怎么样?

cpp 复制代码
//抽象类
class Person
{
public:
	//纯虚函数
	virtual void BuyTicket()=0
	{
		cout << "买票全价" << endl;
	}
};

class Student : public Person
{
public:
	virtual void Ticket() 
	{
		cout << "买票半价" << endl;
	}
};
int main()
{
	Student s;
	return 0;
}

由此我们可知,就算我们派生类继承了抽象类也不能实例化出对象,只有重写基类的虚函数,派生类才能实例化出对象。

cpp 复制代码
//抽象类
class Person
{
public:
	//纯虚函数
	virtual void BuyTicket()=0
	{
		cout << "买票全价" << endl;
	}
};

class Student : public Person
{
public:
	virtual void BuyTicket() 
	{
		cout << "买票半价" << endl;
	}
};
int main()
{
	Student s;
	return 0;
}

代码没有任何问题。

接口继承和实例继承的区别

我们知道虚函数继承是接口继承,那什么是接口继承呢?接口继承是一个类从另一个类那里继承行为规范,但并不继承具体实现,接口继承就是一个契约,它规定了某个对象能做什么,但并没有规定要怎么做。

举个例子:假设你开了一个酒店,然后需要在酒店门口设置前台,为了确保前台能够为用户提供一致的服务体验,你创建了一个行为规范指南(这个就是接口)里面列出了前台服务员必须要给用户提供的服务体验。这个行为规范指南 就是一个接口,任何想要任职你酒店的前台就必须能够提供这些服务。

普通函数是一个实现继承,派生类继承的是基类的函数的实现。

5.多态的原理

看一下下面代码结果是什么。(Win32平台)

cpp 复制代码
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
private:
	int _b = 1;
};
int main()
{
	cout << sizeof(Base) << endl;
	return 0;
}

为什么是8字节呢?有老铁就疑惑了,不应该是4字节吗?那就和我一起来探索一下吧。

我们调试一下吧!

我们发现还有一个_vfptr指针,这个指针是干啥的呢?

这个_vfptr指针叫虚函数表指针,每一个含有虚函数的类中都至少有一个虚函数表指针,因为虚函数的地址需要放到虚函数表中,虚函数表也叫虚表。

我们调试下面的代码看看

cpp 复制代码
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;
}



通过上面的代码,我们知道每一个虚函数都在虚函数表中存在一个指针,指向这个虚函数,普通函数在虚表中没有指向自己的指针;在虚表里面的指针可以分为两部分,一部分是从基类继承下来的虚函数,如果在派生类重写基类虚函数,就会把派生类对象的虚表里面的指针给覆盖掉,生成新的指针。,另一部分是派生类自己的虚函数。

我们通过调试窗口可以看到_vfptr虚表是不是一个存放指针的数组,一般这个数组后面都会以nullptr为结尾,

那么虚函数存放在哪呢?虚函数表又存放在哪里呢?
虚函数是和普通函数一样存放在代码段中,虚函数表中存放的是指向虚函数的指针,并不是虚函数本身,vs下的虚函数表是存放在代码段中。

我们来认证一下,看看vs编译器下虚函数表是不是存放在代码段中。

cpp 复制代码
class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
};

int main()
{
	Base s;
	printf("虚函数表的地址:%p\n", *(int*)&s);//只取前四个字节的地址

	static int a = 0;
	printf("静态区地址:%p\n", &a);

	const char* ch = "hello";
	printf("常量区:%p\n", ch);
}

这证明了虚函数表在常量区中

下面我们将通过画图来理解多态工作的原理

我们以下面的代码为例

cpp 复制代码
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;
}


那么满足多态的函数调用是在编译阶段还是在运行阶段呢?
答案是运行阶段(但是虚表是在编译阶段就生成了),如果不满足多态的函数调用则是在编译阶段就调用对应的函数了。

动态绑定和静态绑定

动态绑定(后期绑定):在运行阶段,根据拿到的具体类型去确定程序的具体行为,调具体函数。
静态绑定:在编译阶段确定了程序行为(例如函数的重载)

单继承和多继承的虚函数表

我们知道派生类可以对基类进行单继承,也可以对基类进行多继承,那么两种继承方式的虚函数表有什么不同呢?

cpp 复制代码
class Base 
{
public:
	virtual void func1() 
	{ cout << "Base::func1" << endl; }
	virtual void func2() 
	{ cout << "Base::func2" << endl; }
private:
	int a;
};
class Derive :public Base 
{
public:
	virtual void func1() 
	{ cout << "Derive::func1" << endl; }
	virtual void func3() 
	{ cout << "Derive::func3" << endl; }
	virtual void func4() 
	{ cout << "Derive::func4" << endl; }
private:
	int b;
};

int main()
{
	Base b;
	Derive d;
	return 0;
}

我们来调试这段代码看看单继承的虚函数表

我们可以看到d对象继承了基类的虚函数,并重写了func1()函数,但是在d对象中应该还有func3和func4虚函数在虚表中,这里由于编译器隐藏起来了,所以我们看不到。

我们再来看看多继承的虚函数表

cpp 复制代码
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;
}

多继承的派生类的未重写的虚函数放在第一个继承基类部分虚函数表中

总结:

多态的概念比较晦涩难懂,希望各位老铁看完这篇文章能对多态有着清晰的理解!

相关推荐
秀聚2 分钟前
C++初始化列表 initializer_list 介绍
开发语言·c++
江奖蒋犟8 分钟前
【初阶数据结构】排序——归并排序
c语言·数据结构·算法
落雨便归尘10 分钟前
c++进阶篇——初窥多线程(四) 线程同步的概念以及锁
c++·笔记·学习
无限大.18 分钟前
c语言实例 -- 循环链表
c语言·开发语言·链表
cdut_suye18 分钟前
STL之list篇(下)(从底层分析实现list容器,逐步剥开list的外表)
开发语言·数据结构·c++·学习·算法·stl·list
Stark、20 分钟前
异常处理【C++提升】(基本思想,重要概念,异常处理的函数机制、异常机制,栈解旋......你想要的全都有)
c语言·开发语言·c++·后端·异常处理
流星白龙40 分钟前
【C++算法】9.双指针_四数之和
开发语言·c++·算法
闻缺陷则喜何志丹40 分钟前
【C++差分数组】2381. 字母移位 II|1793
c++·算法·字符串·力扣·差分数组·移位·方向
李余博睿(新疆)40 分钟前
c++知识点总结
c++
什么鬼昵称42 分钟前
Pikachu-PHP反序列化
开发语言·javascript·php