C++ 多态


目录:

思维导图

一·多态概念

在我们日常生活中,买票这个行为对于我们来说再熟悉不过了。同样都是买票这一个行为,对于学生,成人,军人,残疾人……产生的结果却是不一样的

其实这就是多态的一个体现。

多态,广义上讲,是多种形态;狭义上,是不同的主体执行相同的动作,最终产生的结果是不一样

二· 多态的定义和实现

2.1构成条件

第一个条件:必须通过基类(父类)的指针或者引用来调用

第二个条件:虚函数重写:要求被调用的是虚函数,同时在子类里面已经对基类的虚函数进行了重

2.2 虚函数 

 虚函数:对类的成员函数使用关键字 virtual 进行修饰。

2.3 虚函数重写(覆盖)

在子类的成员函数里面,有和父类的虚函数完全一样符合三同:函数名字,函数的参数类型,函

数返回值类型)的一个虚函数,此时这个虚函数就完成了父类虚函数的重写

注意:当父类的虚函数的参数采用缺省值的形式,在子类里面的虚函数的缺省值与父类不同,此时

也是符合虚函数重写的,但此时的缺省值使用的是父类的

因为函数重写:只是继承父类的接口,重写的是函数体里面的具体实现的内容

 2.3.1 虚函数重写的2个特例 

第一个:协变

协变:父类域子类的虚函数的返回值类型不同:父类虚函数返回值类型是父类的指针或者父类 的

引用,子类的虚函数返回值类型是子类的指针或者引用

第二个:析构函数

在继承体系里面,父子类的析构函数是构造虚函数重写的无论子类的析构函数是否加上关键字 virtual 。

虽然此时析构函数的名字不一样(返回值类型,参数类型都保持一样),但在多态里面,编译器会

把析构函数处理成2部分:首先会把析构函数名字处理成 destructer(此时符合函数重写条件),

之后去调用 operator delete()函数,进行资源释放。这样可以保证对指针指向对象进行正确的释

放。

 这也恰好说明析构函数调用为什么需要先子后父

对父类指针进行堆空间申请的时候,new  子类对象,调用operator delete (),进行资源释

放,是根据指针类型进行free 的,此时free 的是一个父类指针,但new 的对象是一个子类的,因此

造成内存泄漏

2.4 关键字 override ,final

override ,final 是在C++11 ,新出来的关键字

C++对函数重写要求比较严格,当我们在函数重写不小心把函数名字  的字母顺序写反,就不能得

到预期的结果,但是编译器是不能检测出来的,直到运行结束后,通过对结果的分析,才能逐一确

定问题。

 C++11提供了override和final两个关键字,可以帮助用户检测是否重写

final :修饰虚函数,表示该虚函数不能被重写。 

 override : 检查子类的某个虚函数是否对父类的某个虚函数进行重写,若没有,编译报错。

2.5 重载,重写,重定义的区别

函数重载:必须在同一个作用域里面,要求函数名字一样,参数类型 或者参数的个数 或者参数的

顺序不同

函数重写(覆盖):2个函数分别在父类和子类里面;要求三同;对虚函数完成重写

函数重定义(隐藏):2个函数分别在父类和子类里面;只要求函数名字一样。

三· 抽象类

3.1 概念

虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口

),抽象类不能实例化出对象。派生类继承后也不能实例化出对象

只有重写纯虚函数,派生类才能实例化出对象。

3.2 函数的接口继承 和函数的实现继承

对于普通函数,子类继承的只是函数的实现;

对于虚函数的继承,子类继承的接口,对于虚函数的实现需要自己进行重写,从而实现多态。

所以说,要没有实现多态,不要把函数定义成虚函数。

四· 多态原理

4.1 虚函数表

对于这个问题,想必各位初始的答案也是 4字节吧!

借助调试,发现此时b 对象里面不仅仅存了-b这个成员,还有一个 _vfptr ,对应的类型是 void** 类

型的指针 。

最后结合对齐规则,Base 类型的大小就是8字节

对齐规则不太了解的,可以康康此篇博客。对齐规则

_vfptr 是一个虚函数表指针,一个有虚函数的类至少有一个虚函数表指针,虚表用来存放虚函数地址的 。

4.2 多态的原理
 4.2.1单继承的虚表

此时通过切片p 变为一个父类 Person 类型对象,在进行BuyTicket()函数调用的时候,编译器是如

何确定调用的是 子类的函数而不是父类的函数

验证分析:

 打开监视窗口,我们可以看到当前虚函数的地址以及对应具体哪一个类域的,同时借助内存窗

口,对当前的对象进行取地址(&s),就可以看到一个虚函数表指针,借助这个虚函数指针找到虚

,进而找到对应的虚函数地址(注意在VS 下,对于一个虚函数指针数组的存放默认以0结束或

者是nullptr

 1)父类的虚表与子类的虚表是不同的

2)父类显示写了虚函数,子类没有显示的写虚函数,此时2者的虚表也不一样。

 3)对于单继承来说,子类的虚表只有一个。

4)同一个类的虚函数表是一样的,不同类的虚函数表不同

5)多态的条件之一是父类的指针或者引用不能是父类的对象

假设是父类的对象调用,此时在进行传参的时候,也会把子类的虚函数表拷贝过去,这时候不能保

证这个虚函数表一定全部就是父类的虚函数

4.3 动态绑定与静态绑定

1. 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比

如:函数重载
2. 动态绑定又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行

为,调用具体的函数,也称为动态多态

五· 虚函数表

5.1 多继承的虚函数表

对于多继承,子类的虚表是有一个还是多个?

子类的虚表:有多个;有几个父类同时每一个父类都有虚函数,那么就有几个虚表

分析:

// 多继承
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;
};
typedef void (*VFUNC)(); // 对虚函数指针进行重命名为 VFUNC
//打印一下虚函数表的地址
void Print(VFUNC* p)
{
	for (int i = 0; p[i] != 0; i++)
	{
		printf("[%d]%p->", i, p[i]);//虚函数地址
		VFUNC f = p[i];
		f();//进一步验证是否为当前类型的虚函数
	}
	cout << endl;
}

 借助监视窗口,发现确实是有2张虚表。

但是此时子类的虚函数为什么没有,是不是子类的虚函数没有进行存储?

其实不是这样的:编译器为了方便用户的观察,进行了优化。这时需要借助内存窗口来进一步探究。

 我们方向此时b 对象确实是有3个虚函数地址,这也就说明了,子类的虚函数确实是存在的

 5.1.1 为何相同函数的地址不一样

 分析:

从画图角度分析:

 从汇编角度分析:

eax 是一个寄存器:在当前情况下,存放的是一个地址 


 

其实在底层调用的是同一个对象的函数(Derive::func1()) 

5.2 菱形继承和虚拟继承

对于菱形继承以及菱形虚拟继承在底层实现较为复杂,而且访问父类的成员在性能上代价较大,在

实际应用上用途不大,这里就不做详细讲解,感兴趣的友友们,可以康康以下大佬的博客

C++虚函数表详解

六· 多态想关的面试题

1. inline 函数可以是虚函数吗?

可以;普通函数调用具有inline 属性;多态调用,不具有inline 属性。

2. static 函数 可以是虚函数吗?

不可以;static 的函数没有this 指针,通过类域进行调用,无法实现多态。

3. 构造函数可以是虚函数吗?

不可以;编译报错;对象的虚表指针是在调用构造函数的时候进行初始化的,构造函数如果支持多

态调用,就需要对虚表指针进行初始化。

4.析构函数一定是虚函数吗?

不一定,但是最好析构函数是虚函数;当一个父类指针 = new 子类对象的时候,此时析构函数只

有支持虚函数 的重写,才能正确进行资源释放。

5. 虚函数表存在哪里,又是在什么阶段完成的?

存放在代码段,在编译阶段完成的。

注意虚函数表指针在调用构造函数阶段完成的;虚函数表指针存放在代码段。

6. 普通函数调用快还是多态调用快?

都是普通函数调用的时候,效率一样;当多态调用的时候,多态要满一些,需要到虚表里面找到对

应的虚函数地址。

  • 25
    点赞
  • 27
    收藏
    觉得还不错? 一键收藏
  • 17
    评论
C++中的多态(Polymorphism)是指在父类和子类之间的相互转换,以及在不同对象之间的相互转换。 C++中的多态性有两种:静态多态和动态多态。 1. 静态多态 静态多态是指在编译时就已经确定了函数的调用,也称为编译时多态C++中实现静态多态的方式主要有函数重载和运算符重载。 函数重载是指在同一作用域内定义多个同名函数,但它们的参数列表不同。编译器根据传递给函数的参数类型和数量来确定调用哪个函数。例如: ```c++ void print(int num) { std::cout << "This is an integer: " << num << std::endl; } void print(double num) { std::cout << "This is a double: " << num << std::endl; } int main() { int a = 10; double b = 3.14; print(a); // 调用第一个print函数 print(b); // 调用第二个print函数 } ``` 运算符重载是指对C++中的运算符进行重新定义,使其能够用于自定义的数据类型。例如: ```c++ class Complex { public: Complex(double real, double imag) : m_real(real), m_imag(imag) {} Complex operator+(const Complex& other) const { return Complex(m_real + other.m_real, m_imag + other.m_imag); } private: double m_real; double m_imag; }; int main() { Complex a(1.0, 2.0); Complex b(3.0, 4.0); Complex c = a + b; // 调用Complex类中重载的+运算符 } ``` 2. 动态多态 动态多态是指在运行时根据对象的实际类型来确定调用哪个函数,也称为运行时多态C++中实现动态多态的方式主要有虚函数和纯虚函数。 虚函数是在父类中定义的可以被子类重写的函数,使用virtual关键字声明。当一个对象的指针或引用指向一个子类对象时,调用虚函数时会根据实际的对象类型来确定调用哪个函数。例如: ```c++ class Shape { public: virtual void draw() { std::cout << "Drawing a shape." << std::endl; } }; class Circle : public Shape { public: void draw() override { std::cout << "Drawing a circle." << std::endl; } }; int main() { Shape* shape_ptr = new Circle(); shape_ptr->draw(); // 调用Circle类中重写的draw函数 } ``` 纯虚函数是在父类中定义的没有实现的虚函数,使用纯虚函数声明(如virtual void func() = 0;)。父类中包含纯虚函数的类称为抽象类,抽象类不能被实例化,只能作为基类来派生子类。子类必须实现父类的纯虚函数才能实例化。例如: ```c++ class Shape { public: virtual void draw() = 0; }; class Circle : public Shape { public: void draw() override { std::cout << "Drawing a circle." << std::endl; } }; int main() { Shape* shape_ptr = new Circle(); shape_ptr->draw(); // 调用Circle类中重写的draw函数 } ```
评论 17
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值