【C++进阶】C++多态机制深度剖析

目录

前言

 1. 多态的概念

 2. 多态的定义及实现

 2.1 虚函数

2.2 虚函数的重写

 2.3 虚函数重写的两个特例

 3. 关于多态的两个关键字

 4. 多态的底层分析

  4.1 虚函数表

 4.2 多态的原理

 5. 抽象类

总结


前言

         多态也是面向对象程序设计的重要特性,它都是配合继承来使用,多态可以增加程序设计的灵活性;本文将深入探讨C++中的多态性,剖析其原理和实现方式;

a6c0473e16e249c2b9ca02e5b793f35e.gif#pic_center

 1. 多态的概念

       多态是指多种形态,具体点就是去完成某个行为,当不同的对象去完成时会产生出不同的状态;

 举个例子感受一下多态,比如:

买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票;军人买票时是优先买票同样是买票,不同的用户买票服务有所不同,这就是一种多态行为;

 2. 多态的定义及实现

         开始时提到,多态都是配合继承来使用,这是因为继承是实现多态的前提,如果对继承不太了解可以先去阅读:

深入了解C++继承机制

 多态是在继承体现中,不同的类对象调用同一函数,产生了不同的行为;比如Student继承了
Person。Person对象买票全价,Student对象买票半价;
在继承体系中,要构成多态需要添加两个条件:

  1. 必须通过基类的指针或者引用调用虚函数
  2. 被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

 2.1 虚函数

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

2.2 虚函数的重写

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

 注意:虚函数重写的是函数的实现!!

 简单介绍完我们来上手用一用:

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }
};
class Student : public Person {
public:
    //派生类重写的虚函数可以不加virtual(不加依然可以构成多态),但最好是加上;
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};
// 基类的引用或做指针调用,父类对象就无法构成多态
void Func(Person& p)
{
	p.BuyTicket();
}
int main()
{
	Person ps;
	Student st;
	Func(ps);
	Func(st);
	return 0;
}

 2.3 虚函数重写的两个特例

  •  协变(基类与派生类虚函数返回值类型不同)

 在虚函数重写时,返回类型可以不同,但前提是,基类的虚函数返回基类对象的指针或引用派生类的虚函数返回派生类对象的指针或引用

class A{};

class B : public A {};

class C 
{
public:
    virtual A* f() {return new A;}
};

class D : public C 
{
public:
    virtual B* f() {return new B;}
};
  • 析构函数(函数名不同)

         在继承里边我们就提到,派生类的析构和基类的析构会构成隐藏,为什么?这是因为析构函数在调用时,编译器对析构函数的名称做了特殊处理,函数名会被统一处理成destructor;

        虚函数重写时在基类的析构函数前加上virtual修饰,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然函数名不同,但最终会被统一处理成destructor;

class Person {
public:
	virtual ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	virtual ~Student() { cout << "~Student()" << endl; }
};

// 没有virtual调用析构时不会区分子类、父类
// 加上virtual可构成多态
int main()
{
	Person* p1 = new Person;//父类指针指向父类对象
	Person* p2 = new Student;//父类指针指向子类对象
    // 析构是虚函数,才能正确调用析构函数
    // p->destrutor() + operator delete(p)
	delete p1;
	delete p2;
	return 0;
}

         这个场景析构函数必须要使用多态;如果不是多态(去掉虚函数的virtual),调用析构时就会发现它不会调用派生类的析构函数,两次都调用了基类的析构函数(正常的应该是基类对象调基类析构,派生类对象调基类析构和自己的析构);C++为了让析构函数能够正确调用(构成多态),对析构函数做了特殊处理,将析构函数统一处理成destructor;

 3. 关于多态的两个关键字

在C++11中新添加了两个关键字override 和 final,其目的是为了防止重写时出现函数名错误的问题,帮助我们检查是否重写;

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

 它也可以修饰类,修饰类时,表示该类不能被继承

c8d646ec29ee47dfa52d0da853280662.png

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

f5c72e75c0c84817ba55cc388dc05865.png

 4. 多态的底层分析

  4.1 虚函数表

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

int main()
{
	Base b;
	cout << sizeof(b);
	return 0;
}

 这里sizeof (b)的大小是多少?如果你认为它是4那就错了,在32位机器下是8;在64位机器下是16;通过监视窗口我们会发现b里边除了有_b之外还有一个_vfptr的指针:

d5f45f732b954b3cab2e2b98a7502888.png

        这个指针也叫做虚函数表指针vfptr ( virtual function ptr );它是一个二级指针,准确的说它是一个虚函数指针 数组;一个含有虚函数的类中都至少都有一个虚函数表指针虚函数的地址要被放到虚函数表中,虚函数表也简称虚表;

 4.2 多态的原理

 调试下面这段代码,通过监视窗口观察_vfptr:

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;
}

021557fbb41f41ada62a6469c8ad4714.png

 通过监视窗口我们发现:

  1. 基类对象和派生类对象内部_vfptr地址不同
  2. 派生类对Func1进行了重写,在派生类的需表中Func1的地址发生了变化,Func2的地址没变

 得出以下结论:

  1. 基类和派生类它们用的不是同一个虚表
  2. 派生类对象的虚表是基类对象虚表的拷贝,当派生类对基类虚函数重写时,会替换原来虚表中的Func1(函数地址发生改变)

那它们是怎么实现多态的? p对象和s对象里的1和2是给的初始值,可以不用管;

105bb9e8497c4fb1927a12ffe9d67555.png

 原理其实也很简单:

  • p指向父类对象时,在父类对象中拿到父类的_vfptr(虚函数表指针),通过虚表调用父类的虚函数;
  • p指向子类对象时(会发生切片),在子类对象中拿到子类的_vfptr,通过虚表调用子类的虚函数;

 总结就是通过各自的虚表去调用各自的虚函数

 拓展结论:同一个类的不同对象,它们共用一个虚表

建议:私下想自己调试验证的话,建议在32位环境下,内存查看时显示4位,这样更为清晰

 5. 抽象类

  概念

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

 抽象类有以下特性:

  • 抽象类不能实例化出对象
  • 派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象
     

 纯虚函数规范了派生类必须重写;

 接口继承和实现继承

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

 注意:不管有没有多态体系,只要有虚函数就会有虚函数表;

拓展阅读:

多态的原理及底层较为复杂,联系到多继承,菱形继承更为复杂,感兴趣的可以去研读下面的文章;

多继承下的多态(含菱形继承的虚函数表研究)


总结

        继承和多态在校招笔试中考察的非常多,面试官很喜欢从继承和多态这里考察学生对C++的了解程度,所以这部分要耐心的去学习;好了以上便是本文的全部内容,希望可以对你有所帮助,最后感谢阅读!

  • 17
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值