【C++】多态,虚函数表相关问题解决

这篇具有很好参考价值的文章主要介绍了【C++】多态,虚函数表相关问题解决。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

多态概念及其触发条件

  多态的概念:通俗来说,就是多种形态。具体点就是去完成某个行为,当不同的对象去完成时,会产生出不同的状态

多态的构成条件:
1.必须通过基类的指针或者引用调用虚函数(即被virtual修饰的类成员函数称为虚函数)
2.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写

重写和协变

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

虚函数重写的两个例外:
1. 协变(基类与派生类虚函数返回值类型不同)
  派生类重写基类虚函数时,与基类虚函数返回值类型不同。即基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用时,称为协变
2. 析构函数的重写(基类与派生类析构函数的名字不同)
  如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类析构函数名字不同。虽然函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

override和final两个关键字

【C++】多态,虚函数表相关问题解决,C++,c++,虚表,虚表位置,多继承虚函数表相关问题解决

(考点1)

这里强调一下,重写重写的是实现。看以下这个场景:(考点)

class A
{
public:
	A()
	{}
	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()
{

	A* p = new B();
	p->test();
	return 0;
}

  打印结果为B->1,说明调的是子类的func函数,但是缺省值用的却是父类,返回值,函数名,参数类型相同即构成重写,重写重写的是实现,壳子用的是父类的,写的内容自己控制

【C++】多态,虚函数表相关问题解决,C++,c++,虚表,虚表位置,多继承虚函数表相关问题解决

(考点2)

那为什么要把析构函数构成重写呢?看以下这个场景:(考点)


class Person {
public:
	 ~Person() { cout << "~Person()" << endl; }
};
class Student : public Person {
public:
	~Student() {
		cout << "~Student()" << endl;
		delete[] ptr;
	}
protected:
	int* ptr = new int[10];
};

int main()
{
	Person* p = new Person;
	delete p;

	p = new Student;
	delete p; 
	return 0;
}

  当我们用父类指针,指向子类对象时,期望析构的是子类对象,而不是父类对象。不构成重写的话,无论父类指针是指向子类对象还是父类对象,析构的都是父类对象,导致下面的 ptr 动态开辟的空间没有释放而内存泄漏

【C++】多态,虚函数表相关问题解决,C++,c++,虚表,虚表位置,多继承虚函数表相关问题解决

【C++】多态,虚函数表相关问题解决,C++,c++,虚表,虚表位置,多继承虚函数表相关问题解决

  当我们给父类析构函数加上 virtual,让其构成重写后。同时注意这里析构玩~Student后还会析构继承父类,照应上面的构造先父后子,"析构先子后父"

【C++】多态,虚函数表相关问题解决,C++,c++,虚表,虚表位置,多继承虚函数表相关问题解决

【C++】多态,虚函数表相关问题解决,C++,c++,虚表,虚表位置,多继承虚函数表相关问题解决

抽象类:
  在虚函数的后面写上 =0 ,则这个函数为纯虚函数。包含纯虚函数的类叫做抽象类(也叫接口类),抽象类不能实例化出对象。派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象。纯虚函数规范了派生类必须重写,另外纯虚函数更体现出了接口继承


【C++】多态,虚函数表相关问题解决,C++,c++,虚表,虚表位置,多继承虚函数表相关问题解决

动态绑定与静态绑定:

【C++】多态,虚函数表相关问题解决,C++,c++,虚表,虚表位置,多继承虚函数表相关问题解决

虚函数表及其位置

  一个含有虚函数的类中都至少都有一个虚函数表指针,因为虚函数的地址要被放到虚函数表中,虚函数表也简称虚表,通过下面这个例子来看对象模型

【C++】多态,虚函数表相关问题解决,C++,c++,虚表,虚表位置,多继承虚函数表相关问题解决

【C++】多态,虚函数表相关问题解决,C++,c++,虚表,虚表位置,多继承虚函数表相关问题解决

(考点3)

这时候我们再反过来思考,为什么一定是父类的指针或者引用,而不能是父类对象?

  首先我们可以看出,子类对象会先拷贝父类虚函数表,然后再对需要重写的虚函数进行地址修改。
  假如我们把子类对象赋值给父类对象,那么子类对象的虚函数表要不要拷贝给父类?如果虚函数表不拷贝,那么还是调用父类的函数,没有构成多态。
  如果拷贝了,那么父类对象的虚函数表存的是子类对象修改后的虚函数,如下图:此时我们无法再调用父类本身被重写的函数,因为无论我们传子类还是父类对象,调用的都是子类对象的函数,不能构成多态。
  因此多态的条件,一定是父类的指针或者引用,这样可以避免像下面这样拷贝带来的错误。

【C++】多态,虚函数表相关问题解决,C++,c++,虚表,虚表位置,多继承虚函数表相关问题解决

虚表位置

class Person {
public:
	virtual	void BuyTicket() const { cout << "成人-全价" << endl; }
};
class Student : public Person {
public:
	virtual void BuyTicket() const { cout << "学生-半价" << endl; }
};
int main()
{
	Person ps;
	Student st;
	int a = 0;
	printf("栈:%p\n\n", &a);
	static int b = 0;
	printf("静态区:%p\n\n", &b);
	int* p = new int;
	printf("堆:%p\n\n", p);
	const char* str = "hello world";
	printf("常量区:%p\n\n", str);
	printf("虚表1:%p\n", *((int*)&ps));
	printf("虚表2:%p\n", *((int*)&st));
	return 0;
}

  虚表存放在哪里呢?首先排除堆,虚表由编译器生成,不会自己去动态申请空间。其次排除栈,同类型对象公用一张虚表,栈都是伴随栈帧走的,不能函数调用结束,栈帧销毁,虚表就销毁了吧。我们用打印的方式来看一下虚表是存在哪里的
  看下面的代码和输出结果,我们可以发现,虚表是存在常量区的

【C++】多态,虚函数表相关问题解决,C++,c++,虚表,虚表位置,多继承虚函数表相关问题解决

多继承中的虚函数表

// 打印函数指针数组
typedef void(*FUNC_PTR) ();
void PrintVFT(FUNC_PTR* table)
{
	for (size_t i = 0; table[i] != nullptr; i++)
	{
		printf("[%d]:%p->", i, table[i]);
		FUNC_PTR f = table[i];
		f();//这个地址可以调用说明一定是函数
	}
	printf("\n");
}

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

int main()
{
	Derive d;
	int vft1 = *((int*)&d);
	Base2* ptr = &d;
	int vft2 = *((int*)ptr);
	printf("第一张虚表:\n");
	PrintVFT((FUNC_PTR*)vft1);
	printf("第二张虚表:\n");
	PrintVFT((FUNC_PTR*)vft2);
	return 0;
}

  先看上面这段代码,首先d对象有几张虚表呢?看下面的监视窗口,很明显发现d对象有两张虚表,但是d对象自己的虚函数func3去哪里了,其实它在第一张虚表中,我们可以通过上面的代码打印观察出来,f()这个地址可以调用,说明它一定是函数。这里是可以认为是编译器的监视窗口故意隐藏了func3函数,也可以认为是它的一个小bug

【C++】多态,虚函数表相关问题解决,C++,c++,虚表,虚表位置,多继承虚函数表相关问题解决

【C++】多态,虚函数表相关问题解决,C++,c++,虚表,虚表位置,多继承虚函数表相关问题解决

  可是细心一点发现,两张表中的func1地址不一样,它们不是都重写了func1函数吗?而且用父类指针调用会发现,它们调的是同一个函数,那么这里为什么地址不一样呢?

【C++】多态,虚函数表相关问题解决,C++,c++,虚表,虚表位置,多继承虚函数表相关问题解决

看下面这个场景

【C++】多态,虚函数表相关问题解决,C++,c++,虚表,虚表位置,多继承虚函数表相关问题解决
  
注意:这里你要调用的是派生类d对象的func1函数,this指针应该指向d对象,而这里的ptr1指针恰好指向d对象,不需要改动。而ptr2指向的却是Base2对象。调用d对象的func1函数要传d对象的this指针, 而不是Base2对象的this指针。所以这里第二张表的地址其实是"虚地址",多封装了几层是为了修正this指针

【C++】多态,虚函数表相关问题解决,C++,c++,虚表,虚表位置,多继承虚函数表相关问题解决

接下来我们通过汇编来看看ptr1和ptr2调用的区别,更好理解Base2的"虚地址"
ptr1调用


【C++】多态,虚函数表相关问题解决,C++,c++,虚表,虚表位置,多继承虚函数表相关问题解决

ptr2调用
【C++】多态,虚函数表相关问题解决,C++,c++,虚表,虚表位置,多继承虚函数表相关问题解决
文章来源地址https://www.toymoban.com/news/detail-615244.html

到了这里,关于【C++】多态,虚函数表相关问题解决的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 设计模式学习笔记 - 面向对象 - 2.封装、抽象、继承、多态分别用来解决哪些问题?

    封装 也叫作信息隐藏或者数据访问保护。类通过暴露有限的访问接口,授权外部仅能通过类提供的方法(或者叫作函数)来访问内部信息或数据。 下面这段代码是一个简化版的虚拟钱包的代码实现。在金融系统中,我们会给每个用户创建一个虚拟钱包,用来记录用户在我们

    2024年02月21日
    浏览(39)
  • 【C++进阶】继承、多态的详解(多态篇)

    作者:爱写代码的刚子 时间:2023.8.16 前言:本篇博客主要介绍C++中多态有关的知识,是C++中的一大难点,刚子将带你深入C++多态的知识。(该博客涉及到的代码是在x86的环境下,如果是在x86_64环境下指针的大小可能需要变成8bytes) 多态的概念 多态的概念:通俗来说,就是多

    2024年02月12日
    浏览(31)
  • 【C++进阶】继承、多态的详解(继承篇)

    作者:爱写代码的刚子 时间:2023.7.28 前言:本篇博客主要介绍C++进阶部分内容——继承,C++中的继承和多态是比较复杂的,需要我们认真去深挖其中的细节。 继承的概念及定义 继承的概念 继承(inheritance)机制是面向对象程序设计使代码可以复用的最重要的手段,它允许程序

    2024年02月13日
    浏览(32)
  • Python函数的重载、多态和继承

    Python重载函数是指函数可以接受不同数量或类型的参数,这样可以使得函数可以处理多种情况。函数重载是指有多个参数签名(即函数在定义时参数个数跟类型)并且有多个舍入函数体实现。也就是, 具有相同名称但实现多个不同的功能 。调用重载函数时,运行时首先评估

    2024年02月05日
    浏览(76)
  • 【C++】继承和多态

    继承机制是面向对象程序设计使代码可以 复用 的最重要的手段,它允许程序员在保持原有类特性的基础上进行 扩展 ,增加功能,这样产生新的类,称 派生类/子类 。继承呈现了面向对象程序设计的层次结构,体现了由简单到复杂的认知过程。以前我们接触的复用都是函数复

    2024年02月06日
    浏览(31)
  • 简单明了证明多态虚表是位于常量区

    证实虚表存储与常量区

    2024年02月16日
    浏览(32)
  • C++ 中的继承和多态

    继承允许我们 依据一个类来定义另一个类 ,这使得创建和维护一个应用程序变得更容易。这样做也达到了重用代码功能和提高执行效率的效果。 派生类的成员可以直接访问基类的保护成员(protected),但不能直接访问基类的私有成员(private) 。不过需要注意的是,派生类

    2024年02月06日
    浏览(32)
  • C# 类class、继承、多态性、运算符重载,相关练习题

    34.函数重载 35.几个相同的函数  print() ,用于打印不同的数据类型。   36.基类和派生类   37.基类的初始化   38.多重继承   39.动态多态性   40.抽象性和虚方法   41.通过虚方法 area() 来计算不同形状图像的面积   42.运算符重载的实现   @www.runoob.com 

    2024年02月09日
    浏览(36)
  • C++基础篇:07 继承与多态

            当遇到问题,先看一下现有的类是否能够解决一部分问题,如果有则继承,并在此基础上进行扩展来解决所有问题,以此缩短解决问题的时间(代码复用)         当遇到一个大而复杂的问题时,可以把复杂问题拆分成若干个小问题,为每个小问题的解决设计一

    2024年02月08日
    浏览(44)
  • c++面向对象之封装、继承、和多态

    把客观事物封装成类,而且可以把自己的数据和方法设置为只能让可信的类或者对象操作,对不可信的信息进行隐藏(利用public,private,protected,friend)实现 has-a :描述一个类由多个部件类构成,一个类的成员属性是另一个已经定义好的类。 use-a:一个类使用另一个类,通过类之间

    2024年02月02日
    浏览(38)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包