C++_多态

C++_多态

多态的概念

通俗来讲,就是多种形态。多态分为编译时多态(静态多态)运行时多态(动态多态)

编译时多态 主要就是函数重载函数模板,他们传不同类型的参数就可以调用不同的函数,通过参数不同达到多种形态,之所以叫编译时多态,是因为他们实参传给形参的参数匹配是在编译时完成的,编译时一般归为静态,运行时一般归为动态。

运行时多态,具体点就是去完成某个行为,可以传不同的对象就会完成不同的行为,就达到多种形态。比如买票这个行为,普通人是全价买票;学生是优惠买票;军人是优先买票。再比如动物叫,小猫是喵喵🐱,小狗是汪汪🐶。

多态的定义及实现

多态的构成条件

多态是一个继承关系下的类对象,去调用同一函数,产生了不同的行为。比如Student继承Person,前者优惠买票,后者全价买票。

实现多态的两个重要条件

  • 必须用指针或者引用调用函数
  • 被调用的函数必须是虚函数(virtual)

要实现多态效果,

  1. 必须是基类的指针或引用,因为只有基类的指针或引用才能既指向派生类对象,也指向基类对象。
  2. 派生类必须对基类的虚函数重写/覆盖,重写/覆盖了,派生类才能有不同的函数,多态的不同形态效果才能达到。

虚函数

类成员函数前面加virtual修饰,那么这个成员函数被称为虚函数。

非成员函数不能加virtual修饰。

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

虚函数的重写/覆盖

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

在重写虚函数时,派生类的虚函数可以不加virtual,但不建议。(继承后基类的虚函数被继承下来在派生类中依旧保持虚函数属性)

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

一个选择题

下面的程序输出结果是什么()

A: A->0 B: B->1 C: A->1 D: B->0

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

虚函数重写的问题

  • 协变

派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或引用,派生类虚函数返回派生类对象的指针或引用时,称为协变。

cpp 复制代码
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,所以基类的析构函数加了virtual,派生类的析构函数就构成重写。

下面的代码我们看到,如果~A()不加virtual,那么delete p2时只调用了A的析构函数,没有调用B的析构函数,就会导致内存泄漏问题,因此在~B()中释放资源。

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

override 和 final 关键字

C++对函数重写的要求比较严格,但是有些情况下由于疏忽,比如函数名写错参数写错等导致无法构成重载,而这种错误在编译阶段是不会报错的,只有在程序运行时没有得到预期结果,再去debug就得不偿失了。

因此C++11提供了关键字override,可以帮助用户检查是否重写,如果我们不想让派生类去重写这个虚函数,则需要final关键字。

cpp 复制代码
// error C3668: "Benz::Drive": 包含重写说明符"override"的方法没有重写任何基类方法
class Car {
public:
	virtual void Dirve()
	{}
};
class Benz :public Car {
public:
	virtual void Drive() override
    { 
        cout << "Benz-舒适" << endl;
    }
};
int main()
{
	return 0;
}
cpp 复制代码
// error C3248: "Car::Drive": 声明为"final"的函数无法被"Benz::Drive"重写
class Car
{ 
public:
	virtual void Drive() final {}
};
class Benz :public Car
{ 
public:
	virtual void Drive() 
    {
        cout << "Benz-舒适" << endl; 
    }
};
int main()
{
	return 0;
}

重载/重写/隐藏 的对比

纯虚函数和抽象类

在虚函数的后面写上=0,则这个函数为纯虚函数,纯虚函数不需要定义实现(实现没啥意义,因为要被派生类重写,但是语法上可以实现),只要声明即可。

包含纯虚函数的类为抽象类,抽象类不能实例化出对象,如果派生类继承后不重写纯虚函数,则派生类也为抽象类。

纯虚函数某种程度上强制了派生类重写虚函数,因为不重写则无法实例化出对象。

cpp 复制代码
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 car;
	Car* pBenz = new Benz;
	pBenz->Drive();
	Car* pBMW = new BMW;
	pBMW->Drive();
    
	return 0;
}

多态的原理

虚函数表指针

下面程序在32位环境下运行结果是什么()

A.编译报错 B.运行报错 C.8 D.12

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

答案为12bytes,除了_b_ch成员还多了一个__vfptr放在对象前(也可能放在对象后,与编译器有关)。

对象中的这个指针我们叫做虚函数表指针,一个含有虚函数的类中都至少含有一个虚函数表指针,⼀个类所有虚函数的地址都要被放到这个类对象的虚函数表中,虚函数表也简称虚表。

多态是如何实现的

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

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

动态绑定和静态绑定

  • 对不满足多态条件的函数调用是在编译时绑定,也就是编译时确定调用函数的地址,叫做静态绑定
  • 满足多态条件的函数调用是在运行时绑定,也就是在运行时到指向对象的虚函数表中找到调用函数的地址,也就是动态绑定
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)

虚函数表

  • 基类对象的虚函数表中存放着基类所有虚函数的地址
  • 派生类由两部分构成,继承下来的基类和自己的成员。一般情况下,继承下来的基类中有虚函数表指针,自己就不会再生成虚函数表指针。但是要注意,这里继承下来的虚函数表指针和基类对象的虚函数表指针并非同一个,就像基类对象的成员和派生类对象中的基类对象成员都独立存在一样
  • 派生类中重写的基类的虚函数,派生类的虚函数表指针中的虚函数地址就会被覆盖成重写后的虚函数地址
  • 派生类的虚函数表中包括:基类的虚函数地址,派生类重写的虚函数地址,派生类自己的虚函数地址
  • 虚函数表本质是一个存放虚函数指针的指针数组,一般情况下这个数组最后放了一个0x00000000的标志。(由编译器决定)
  • 虚函数也存在代码段,只是地址被放到了虚函数表中
  • 虚函数表存在的位置C++并没有规定,但在vs中存放于代码段
相关推荐
Algorithm15761 分钟前
云原生相关的 Go 语言工程师技术路线(含博客网址导航)
开发语言·云原生·golang
shinelord明10 分钟前
【再谈设计模式】享元模式~对象共享的优化妙手
开发语言·数据结构·算法·设计模式·软件工程
Monly2117 分钟前
Java(若依):修改Tomcat的版本
java·开发语言·tomcat
boligongzhu18 分钟前
DALSA工业相机SDK二次开发(图像采集及保存)C#版
开发语言·c#·dalsa
Eric.Lee202118 分钟前
moviepy将图片序列制作成视频并加载字幕 - python 实现
开发语言·python·音视频·moviepy·字幕视频合成·图像制作为视频
小俊俊的博客19 分钟前
海康RGBD相机使用C++和Opencv采集图像记录
c++·opencv·海康·rgbd相机
7yewh20 分钟前
嵌入式Linux QT+OpenCV基于人脸识别的考勤系统 项目
linux·开发语言·arm开发·驱动开发·qt·opencv·嵌入式linux
waicsdn_haha32 分钟前
Java/JDK下载、安装及环境配置超详细教程【Windows10、macOS和Linux图文详解】
java·运维·服务器·开发语言·windows·后端·jdk
_WndProc34 分钟前
C++ 日志输出
开发语言·c++·算法
薄荷故人_35 分钟前
从零开始的C++之旅——红黑树及其实现
数据结构·c++