C++中的多态

1. 多态的概念

多态(polymorphism)的概念:通俗来说,就是多种形态。多态分为编译时多态(静态多态)和运⾏时多态(动态多态),这⾥我们重点讲运⾏时多态,编译时多态(静态多态)和运⾏时多态(动态多态)。编译时多态(静态多态)主要就是我们前⾯讲的函数重载和函数模板,他们传不同类型的参数就可以调⽤不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,我们把编译时⼀般归为静态,运⾏时归为动态。

运⾏时多态,具体点就是去完成某个⾏为(函数),可以传不同的对象就会完成不同的⾏为,就达到多种形态。⽐如买票这个⾏为,当普通⼈买票时,是全价买票;学⽣买票时,是优惠买票(5折或75折);军⼈买票时是优先买票。再⽐如,同样是动物叫的⼀个⾏为(函数),传猫对象过去,就是"(>^ω^<)喵",传狗对象过去,就是"汪汪"。

2. 多态的定义及实现

2.1 多态的构成条件

多态是⼀个继承关系的下的类对象,去调⽤同⼀函数,产⽣了不同的⾏为。⽐如Student继承了

Person。Person对象买票全价,Student对象优惠买票。

2.1.1 实现多态还有两个必须重要条件:

• 必须指针或者引⽤调⽤虚函数
• 被调⽤的函数必须是虚函数。

说明:要实现多态 效果,第⼀必须是基类的指针或引⽤ ,因为只有基类的指针或引⽤才能既指向派⽣类对象;第⼆派⽣类必须对基类的虚函数重写/覆盖,重写或者覆盖了,派⽣类才能有不同的函数,多态的不同形态效果才能达到。

2.1.2 虚函数

类成员函数前⾯加virtual修饰,那么这个成员函数被称为虚函数。注意⾮成员函数不能加virtual修

饰。

cpp 复制代码
class Person
{
public:
virtual void BuyTicket() { cout << "买票-全价" << endl;}
};
2.1.3 虚函数的重写/覆盖

虚函数的重写/覆盖:派⽣类中有⼀个跟基类完全相同的虚函数(即派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同),称派⽣类的虚函数重写了基类的虚函数。

注意:在重写基类虚函数时,派⽣类的虚函数在不加virtual关键字时,虽然也可以构成重写(因为继承后基类的虚函数被继承下来了在派⽣类依旧保持虚函数属性),但是该种写法不是很规范,不建议这样使⽤,不过在考试选择题中,经常会故意买这个坑,让你判断是否构成多态。

cpp 复制代码
#include<iostream>
using namespace std;
class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-打折" << endl; }
};
void Func(Person* ptr)
{
	// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
	// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
	ptr->BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(&ps);
	Func(&st);
	return 0;
}

此时就满足多态,指向谁调用谁

假如我把父类Person中的 BuyTicket()函数的修饰符virtual去掉 ,此时该函数就不是虚函数了,则不满足多态,该函数就存在栈上而不是在虚拟表中,函数调用不是去虚拟表中找,就实现不了多态

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

但是,反过来我把子类Student中的 BuyTicket()函数的virtual关键字去掉则不影响多态,因为Student继承于Person。Person中的BuyTicket()函数就是虚函数,子类中的BuyTicket()只是重写了父类中的BuyTicket()虚函数,virtual还是被继承下来了

cpp 复制代码
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-打折" << endl; }
};
2.2其他实现多态中的问题

1.传其他类型也不能实现多态,实现多态的条件必须是传引用或者指针

cpp 复制代码
#include<iostream>
using namespace std;
class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	 void BuyTicket() { cout << "买票-打折" << endl; }
};
void Func(Person s)//传类
{
	// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
	// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
	s.BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(ps);
	Func(st);
	return 0;
}

2.子类中实现多态部分传子类接受而不是父类会报错,子类不能接受父类会报错

cpp 复制代码
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-打折" << endl; }
};
void Func(Student* ptr)
{
	// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
	// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
	ptr->BuyTicket();
}

子类不能接受父类,所以实现多态得传父类,这样会形成切片,语法也合法

cpp 复制代码
#include<iostream>
using namespace std;
class Person {

};
class Student : public Person {

};

int main()
{
	Person ps;
	Student st;
	Student s = ps;
	return 0;
}
2.3实现一个动物叫声,满足多态
cpp 复制代码
#include<iostream>
using namespace std;
class Animal
{
public:
	virtual void talk() const
	{}
};
class Dog : public Animal
{
public:
	virtual void talk() const
	{
		std::cout << "汪汪" << std::endl;
	}
};
class Cat : public Animal
{
public:
	virtual void talk() const
	{
		std::cout << "(>^ω^<)喵" << std::endl;
	}
};
void letsHear(const Animal& animal)
{
	animal.talk();
}
int main()
{
	Cat cat;
	Dog dog;
	letsHear(cat);
	letsHear(dog);
	return 0;
}
2.4多态的面试题
cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
	virtual void test() { func(); }
};
class B : public A
{
public:
	void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{
	B* p = new B;
	p->test();
	return 0;
}

派⽣类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同(重写),指针或引用调用虚函数,所以满足多态

建立一个B类型的指针,调用了由A继承下来的函数text(),但是B里面没有text()函数,C++类函数里面的第一个参数是this* ,所以这里指向的是A*,调用的是父类中的func()函数,但是B有单独的虚拟表,B重写了虚拟函数func(),func()放在虚拟表中,所以在虚拟表中找到了B的func()函数,所以这里就是A类里面的函数声明+B里的func()函数重载构成新的函数实现调用。

2.4 虚函数重写的⼀些其他问题

• 协变(了解)

派⽣类重写基类虚函数时,与基类虚函数返回值类型不同 。即基类虚函数返回基类对象的指针或者引⽤,派⽣类虚函数返回派⽣类对象的指针或者引⽤时,称为协变。协变的实际意义并不⼤,所以我们了解⼀下即可。

虚函数的返回值类型不同,产生协变,也可以实现多态

cpp 复制代码
#include<iostream>
using namespace std;
class A {};
class B : public A {};
class Person {
public:
	virtual A* BuyTicket()
	{
		cout << "买票-全价" << endl;
		return nullptr;
	}
};
class Student : public Person {
public:
	virtual B* BuyTicket()
	{
		cout << "买票-打折" << endl;
		return nullptr;
	}
};
void Func(Person* ptr)
{
	ptr->BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(&ps);
	Func(&st);
	return 0;
}
  • 析构函数的重写

基类的析构函数 为虚函数,此时派⽣类析构函数只要定义,⽆论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派⽣类析构函数名字不同看起来不符合重写的规则,实际上编译器对析构函数的名称做了特殊处理 ,编译后析构函数的名称统⼀处理成destructor ,所以基类的析构函数加了vialtual 修饰,派⽣类的析构函数就构成重写

下⾯的代码我们可以看到,如果~A(),不加virtual,那么delete p2时只调⽤的A的析构函数,没有调⽤B的析构函数,就会导致内存泄漏问题,因为~B()中在释放资源。所以祖师爷在设计C++时就考虑好了这一点,加不加virtual,都默认加了,实现多态,先子后父,防止内存泄漏

cpp 复制代码
#include<iostream>
using namespace std;
class A
{
public:
	virtual ~A()
	{
		cout << "~A()" << endl;
	}
};
class B : public A {
public:
//构成重写
	~B()
	{
		cout << "~B()->delete:" << _p << endl;
		delete _p;
	}
protected:
	int* _p = new int[10];
};
// 只有派⽣类Student的析构函数重写了Person的析构函数,下⾯的delete对象调⽤析构函数,才能
//构成多态,才能保证p1和p2指向的对象正确的调⽤析构函数。
int main()
{
	A* p1 = new A;
	A* p2 = new B;
	delete p1;
	delete p2;
	return 0;
}
2.5 override 和 final关键字

从上⾯可以看出,C++对虚函数重写的要求⽐较严格,但是有些情况下由于疏忽,⽐如函数名写错参数写错等导致⽆法构成重写,⽽这种错误在编译期间是不会报出的,只有在程序运⾏时没有得到预期结果才来debug会得不偿失,因此C++11提供了override,可以帮助⽤⼾检测是否重写。如果我们不想让派⽣类重写这个虚函数,那么可以⽤final去修饰。

cpp 复制代码
#include<iostream>
using namespace std;
// error C3668: "Benz::Drive": 包含重写说明符"override"的⽅法没有重写任何基类⽅法
class Car {
public:
	virtual void Dirve()
	{}
};
class Benz :public Car {
public:
	virtual void Drive()  { cout << "Benz-舒适" << endl; }
};
int main()
{
	return 0;
}

这里我Dirve()函数和Drive()函数函数名不一样,不构成重写,编译器也不报错,但是加了override后可以检查是否被重写,没有就报错

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

final修饰的类为最终类不能被继承,修饰虚函数,该虚函数不能被重写

cpp 复制代码
#include<iostream>
using namespace std;
// error C3668: "Benz::Drive": 包含重写说明符"override"的⽅法没有重写任何基类⽅法
class Car {
public:
	virtual void Dirve()final
	{}
};
class Benz :public Car {
public:
	virtual void Dirve()  { cout << "Benz-舒适" << endl; }
};
int main()
{
	return 0;
}
2.6 重载/重写/隐藏的对⽐

3. 纯虚函数和抽象类

在虚函数的后⾯写上 =0 ,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义因为要被派⽣类重写,但是语法上可以实现),只要声明即可。包含纯虚函数的类叫做抽象类,抽象类不能实例化出对象,如果派⽣类继承后不重写纯虚函数,那么派⽣类也是抽象类。纯虚函数某种程度上强制了派⽣类重写虚函数,因为不重写实例化不出对象。

抽象类不能被实例化,包含纯虚函数

cpp 复制代码
class Car
{
public:
	virtual void Drive() = 0;
};

int main()
{
	// 编译报错:error C2259: "Car": ⽆法实例化抽象类
	Car car;
	
	return 0;
}

继承后也不行

cpp 复制代码
#include<iostream>
using namespace std;
class Car
{
public:
	virtual void Drive() = 0;
};
class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};
class BMW :public Car
{
public:
	
};
int main()
{
	// 编译报错:error C2259: "Car": ⽆法实例化抽象类
	
	BMW bmw;
	return 0;
}

但是重写子类抽象函数后就没有纯虚函数了,不属于抽象类,后就可以实例化子类了

cpp 复制代码
class BMW :public Car
{
public:
	virtual void Drive()
	{
		cout << "BMW-操控" << endl;
	}
};

通过抽象类来实现多态

cpp 复制代码
#include<iostream>
using namespace std;
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()
{
	// 编译报错:error C2259: "Car": ⽆法实例化抽象类
	
	Car* pBenz = new Benz;
	pBenz->Drive();
	Car* pBMW = new BMW;
	pBMW->Drive();
	return 0;
}

4. 多态的原理

4.1 虚函数表指针

cpp 复制代码
#include<iostream>
using namespace std;
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}
protected:
	int _b = 1;
	char _ch = 'x';
};
int main()
{
	Base b;
	cout << sizeof(b) << endl;
	return 0;
}

32位程序的运⾏结果

内存对齐,_b为int 类型,占四个字节,_ch为1个字节,但是32位下内存对齐4个字节,最大为四个字节,所以这里也分配4个字节来存放_ch,再加上一个虚拟表的指针32位下也是占4个字节,所以加起来就是12字节了上⾯题⽬运⾏结果12bytes,除了_b和_ch成员,还多⼀个__vfptr放在对象的前⾯(注意有些平台可能会放到对象的最后⾯,这个跟平台有关),对象中的这个指针我们叫做虚函数表指针(v代表virtual,f代表function)。⼀个含有虚函数的类中都⾄少都有⼀个虚函数表指针,因为⼀个类所有虚函数的地址要被放到这个类对象的虚函数表中,虚函数表也简称虚表。

4.2 多态的原理

cpp 复制代码
#include<iostream>
using namespace std;
class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-打折" << endl; }
};
class Soldier : public Person {
public:
	virtual void BuyTicket() { cout << "买票-优先" << endl; }
};
void Func(Person* ptr)
{
	// 这⾥可以看到虽然都是Person指针Ptr在调⽤BuyTicket
	// 但是跟ptr没关系,⽽是由ptr指向的对象决定的。
	ptr->BuyTicket();
}
int main()
{
	// 其次多态不仅仅发⽣在派⽣类对象之间,多个派⽣类继承基类,重写虚函数后
	// 多态也会发⽣在多个派⽣类之间。
	Person ps;
	Student st;
	Soldier sr;
	Func(&ps);
	Func(&st);
	Func(&sr);
	return 0;
}

4.2.1 多态是如何实现的

从底层的⻆度Func函数中ptr->BuyTicket(),是如何作为ptr指向Person对象调Person::BuyTicket,ptr指向Student对象调⽤Student::BuyTicket的呢?通过下图我们可以看到,满⾜多态条件后,底层不再是编译时通过调⽤对象确定函数的地址,⽽是运⾏时到指向的对象的虚表中确定对应的虚函数的地址,这样就实现了指针或引⽤指向基类就调⽤基类的虚函数,指向派⽣类就调⽤派⽣类对应的虚函数。第⼀张图,ptr指向的Person对象,调⽤的是Person的虚函数;第⼆张图,ptr指向的Student对象,调⽤的是Student的虚函数。

指向谁调用谁

指向哪个对象,运行时。到指向对象的虚函数表中找到对应虚函数的地址,进行调用

4.2.2 动态绑定与静态绑定

• 对不满⾜多态条件(指针或者引⽤+调⽤虚函数)的函数调⽤是在编译时绑定,也就是编译时确定调⽤函数的地址,叫做静态绑定。

• 满⾜多态条件的函数调⽤是在运⾏时绑定,也就是在运⾏时到指向对象的虚函数表中找到调⽤函数的地址,也就做动态绑定。

cpp 复制代码
// ptr是指针+BuyTicket是虚函数满⾜多态条件。
// 这⾥就是动态绑定,编译在运⾏时到ptr指向对象的虚函数表中确定调⽤函数地址
ptr->BuyTicket();
00EF2001 mov
eax,dword ptr [ptr]
00EF2004 mov
edx,dword ptr [eax]
00EF2006 mov
esi,esp
00EF2008 mov
ecx,dword ptr [ptr]
00EF200B mov
eax,dword ptr [edx]
00EF200D call
eax
// BuyTicket不是虚函数,不满⾜多态条件。
// 这⾥就是静态绑定,编译器直接确定调⽤函数地址
ptr->BuyTicket();
00EA2C91 mov
ecx,dword ptr [ptr]
00EA2C94 call
Student::Student (0EA153Ch)
4.2.3 虚函数表

• 基类对象的虚函数表中存放基类所有虚函数的地址。

• 派⽣类由两部分构成,继承下来的基类和⾃⼰的成员,⼀般情况下,继承下来的基类中有虚函数表指针,⾃⼰就不会再⽣成虚函数表指针。但是要注意的这⾥继承下来的基类部分虚函数表指针和基类对象的虚函数表指针不是同⼀个,就像基类对象的成员和派⽣类对象中的基类对象成员也独⽴

的。

• 派⽣类中重写的基类的虚函数,派⽣类的虚函数表中对应的虚函数就会被覆盖成派⽣类重写的虚函数地址。

• 派⽣类的虚函数表中包含,基类的虚函数地址,派⽣类重写的虚函数地址,派⽣类⾃⼰的虚函数地址三个部分。

• 虚函数表本质是⼀个存虚函数指针的指针数组,⼀般情况这个数组最后⾯放了⼀个0x00000000标记。(这个C++并没有进⾏规定,各个编译器⾃⾏定义的,vs系列编译器会再后⾯放个0x00000000标记,g++系列编译不会放)

• 虚函数存在哪的?虚函数和普通函数⼀样的,编译好后是⼀段指令,都是存在代码段的,只是虚函数的地址⼜存到了虚表中。

• 虚函数表存在哪的?这个问题严格说并没有标准答案C++标准并没有规定,我们写下⾯的代码可以对⽐验证⼀下。vs下是存在代码段(常量区)

b1和b2是同一个类实例化的不同对象,相同类共用同一个虚拟表,不同类型对象虚拟表各自独立,b1 和b2共用一个虚拟表

5,不同类型变量存放位置

cpp 复制代码
int main()
{
int i = 0;
static int j = 1;
int* p1 = new int;
const char* p2 = "xxxxxxxx";
printf("栈:%p\n", &i);
printf("静态区:%p\n", &j);
printf("堆:%p\n", p1);
printf("常量区:%p\n", p2);
Base b;
Derive d;
Base* p3 = &b;
Derive* p4 = &d;
printf("Person虚表地址:%p\n", *(int*)p3);
printf("Student虚表地址:%p\n", *(int*)p4);
printf("虚函数地址:%p\n", &Base::func1);
printf("普通函数地址:%p\n", &Base::func5);
return 0;
}
运⾏结果:
栈:010FF954
静态区:0071D000
堆:0126D740
常量区:0071ABA4
Person虚表地址:0071AB44
Student虚表地址:0071AB84
虚函数地址:00711488
普通函数地址:007114BF
相关推荐
binishuaio几秒前
Java 第11天 (git版本控制器基础用法)
java·开发语言·git
zz.YE2 分钟前
【Java SE】StringBuffer
java·开发语言
就是有点傻6 分钟前
WPF中的依赖属性
开发语言·wpf
洋24015 分钟前
C语言常用标准库函数
c语言·开发语言
进击的六角龙16 分钟前
Python中处理Excel的基本概念(如工作簿、工作表等)
开发语言·python·excel
wrx繁星点点17 分钟前
状态模式(State Pattern)详解
java·开发语言·ui·设计模式·状态模式
NoneCoder35 分钟前
Java企业级开发系列(1)
java·开发语言·spring·团队开发·开发
苏三有春35 分钟前
PyQt5实战——UTF-8编码器功能的实现(六)
开发语言·qt
脉牛杂德38 分钟前
多项式加法——C语言
数据结构·c++·算法
legend_jz40 分钟前
STL--哈希
c++·算法·哈希算法