前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >深度剖析C++多态

深度剖析C++多态

作者头像
小灵蛇
发布2024-06-06 21:29:03
830
发布2024-06-06 21:29:03
举报
文章被收录于专栏:文章部文章部

一.多态的概念

简单点来说,多态就是对于一个任务,不同的对象去完成会产生不同的效果。

举个栗子

对于买票这个行为,,学生去买就是半价,普通人去买就是全价,产生了不同的效果。

二.多态的定义和实现

2.1多态的构成条件

  1. 派生类对基类的虚函数实现重写
  2. 通过基类的指针或引用调用虚函数

虚函数是什么呢?

2.2虚函数

我们指被virtual修饰的函数为虚函数

代码语言:javascript
复制
class Person {

public:
 virtual void BuyTicket() { cout << "买票-全价" << endl;}
};

2.3虚函数重写

虚函数的重写(覆盖):派生类中有跟基类一模一样的虚函数(函数名,返回值,参数列表)

代码语言:javascript
复制
class Person {

public:
 virtual void BuyTicket() { cout << "买票-全价" << endl; }
};

class Student : public Person {

public:
 virtual void BuyTicket() { cout << "买票-半价" << endl; }
 
 /*注意:在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,虽然也可以构成重写(因
为继承后基类的虚函数被继承下来了在派生类依旧保持虚函数属性),但是该种写法不是很规范,不建议
这样使用*/

};

诶!我们这里还有两个例外哦:

2.3.1重写例外-》协变

什么叫做协变呢,就是派生类和基类的虚函数返回值不同,例如:

代码语言:javascript
复制
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.3.2重写的例外-》析构函数的重写

析构函数的重写即基类和派生类的析构函数名字不同。

代码语言:javascript
复制
class Person
{
public:
    virtual ~Person()
    {
        cout << "~Person()" << endl;
    }
    virtual void BuyTicket()
    {
        cout << "买票-全价" << endl;
    }
};
class Student :public Person
{
public:
    virtual ~Student()
    {
        cout << "~Student()" << endl;
    }
    virtual void BuyTicket()
    {
        cout << "买票-半价" << endl;
    }
};

这种情况也是构成重写的,是因为编译器对这种情况做了特殊处理,将析构函数的函数名转换为了destructor。

2.4override和final

在C++11中,引入了override和final

(1)override:放在派生类虚函数的后面,检测该虚函数是否重写了基类的虚函数,如果没有就报错

代码语言:javascript
复制
class Car
{
public:
	virtual void Drive() override
	{

	}
};

class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};
int main()
{
	return 0;
}

正确方式如下:

代码语言:javascript
复制
class Car
{
public:
	virtual void Drive()
	{

	}
};

class Benz :public Car
{
public:
	virtual void Drive() override
	{
		cout << "Benz-舒适" << endl;
	}
};
int main()
{
	return 0;
}

(2)final:放在虚函数的后面使该虚函数不能被重写

代码语言:javascript
复制
class Car
{
public:
	virtual void Drive() final
	{

	}
};

class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};

2.5重载,覆盖(重写),隐藏(重定义)对比

三.抽象类

3.1纯虚函数

在讲抽象类之前先讲讲纯虚函数,我们在虚函数的后面加上=0,这样的虚函数就叫纯虚函数。

下面的Drive就是纯虚函数。

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

3.2概念

包含纯虚函数的类就是我们的抽象类了,抽象类是不能实例化出对象的,而且他的派生类也不能,只有派生类重写了纯虚函数,那么派生类才能实例化对象。

代码语言:javascript
复制
class Car
{
public:
	virtual void Drive() = 0;
};
//间接强制了子类重写虚函数,因为不重写的话,子类依旧是抽象类,就实例化不出对象
class Benz :public Car
{
public:
	virtual void Drive()
	{
		cout << "Benz-舒适" << endl;
	}
};

class BWM :public Car
{
public:
	virtual void Drive()
	{
		cout << "BWM-操控" << endl;
	}
};

int main()
{
	//抽象类,实例不出对象
	//Car c1;
	Car* p = new Benz;
	p->Drive();

	p = new BWM;
	p->Drive();

	Benz b;
	b.Drive();
    return 0;
}

四.多态的原理

4.1虚函数表

对于下面的一段代码:

代码语言:javascript
复制
class Base

{

public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}

private:
	int _b = 1;
};
int main()
{
	Base bb;
	cout << sizeof(Base);
	return 0;
}

大家猜猜结果是多少?

哈哈其实是8哦!

为啥呢?

人狠话不多,直接上调试:

我们可以看见除了成员_b以外,还有一个指针_vfptr,这个指针就是我们的主角的引路人-》虚函数表指针,它指向一个虚函数表。

这就是虚函数表,简称虚表,里面的第一行就是函数Func1()的地址。

咱们再来看一下在继承中虚函数表是什么样的:

代码语言:javascript
复制
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;
	char _ch = 'a';
};

class Drive :public Base
{ 
public:
	virtual void Func1()
	{
		cout << "Drive::Func1()" << endl;
	}
private:
	int _d = 2;
};

int main()
{
	Base bb;

	Drive dd;
	return 0;
}

调试一下:

我们可以发现以下规律:

  1. 对于对象dd,也有一个虚表指针,dd对象有两部分,一部分是从基类继承的,另一部分是自己的。
  2. 可以看见dd和bb的虚表是不一样,因为派生类对象dd对基类对象bb重写了Func1(),所以dd的虚表里面变成了Drive::Func1()。所以虚函数的重写也叫做覆盖,即虚函数的覆盖。重写是语法层的叫法,覆盖是原理层的叫法。
  3. 我们可以观察到两个对象的虚表中都没有Func3(),因为Func3()并不是虚函数。

那我们的虚函数与虚表是存在哪的呢?

对于虚函数来说,本质也是个函数,所以存在于代码段中。那虚表呢?

代码语言:javascript
复制
class Base
{
public:
	Base()
		:_b(2)
	{}
	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 Drive :public Base
{
public:
	virtual void Func1()
	{
		cout << "Drive::Func1()" << endl;
	}
	virtual void Func3()
	{
		cout << "Drive::Func3()" << endl;
	}
private:
	int _d = 2;
};
int main()
{
	Base b;
	Base b1;
	Base b2;
	Drive d;

	int i = 0;
	static int j = 1;
	int* p1 = new int;
	const char* p2 = "xxxxxx";
	printf("栈:%p\n", &i);
	printf("静态区:%p\n", &j);
	printf("堆:%p\n", p1);
	printf("常量区:%p\n", p2);

	Base* p3 = &b;
	Drive* p4 = &d;
	printf("Base虚表地址:%p\n", *(int*)p3);
	printf("Drive虚表地址:%p\n", *(int*)p4);
	return 0;
}

结果如下:

可以看到虚表是在常量区的。

4.2多态的原理

上面噼里啪啦说了这么一大堆,那多态的原理到底是什么呢?

当p指向Mike对象时,p->BuyTicket()在虚表中调用的是Person::BuyTicket()

而当p指向Johnson对象时,p->BuyTicket()在虚表中调用的是Student::BuyTicket()

这就实现了不同的对象去完成同一个任务时,呈现出不同的效果。

通过对汇编代码的分析发现,满足多态的函数调用,是在运行后到对象中去找的,而不满足多态的函数调用,是在编译时就确认好的。

总结

好了,到这里今天的知识就讲完了,大家有错误一点要在评论指出,我怕我一人搁这瞎bb,没人告诉我错误就寄了。

祝大家越来越好,不用关注我(疯狂暗示)

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2024-06-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 二.多态的定义和实现
    • 2.1多态的构成条件
      • 2.2虚函数
        • 2.3虚函数重写
          • 2.3.1重写例外-》协变
          • 2.3.2重写的例外-》析构函数的重写
        • 2.4override和final
          • 2.5重载,覆盖(重写),隐藏(重定义)对比
          • 三.抽象类
            • 3.1纯虚函数
              • 3.2概念
              • 四.多态的原理
                • 4.1虚函数表
                  • 4.2多态的原理
                  领券
                  问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档