【ONE·C++ || 多态】

news2025/1/14 2:21:12

总言

  主要介绍多态相关内容。

文章目录

  • 总言
  • 1、多态介绍
    • 1.1、多态是什么
    • 1.2、构成多态的两个必备条件
      • 1.2.1、虚函数介绍
      • 1.2.2、基类的指针或者引用调用虚函数
      • 1.2.3、演示多态条件的破坏(两个特例说明)
        • 1.2.3.1、不符合条件演示
        • 1.2.3.2、特例演示
    • 1.3、一道例题讲解
  • 2、多态原理
    • 2.1、虚函数表
    • 2.2、条件破坏
    • 2.3、再识虚函数
      • 2.3.1、析构函数与virtual重写
      • 2.3.2、C++11两个关键字:final && override
      • 2.3.3、概念比较:函数重写、函数重载、函数重定义
  • 3、抽象类、接口继承
    • 3.1、抽象类介绍
    • 3.2、接口继承和实现继承
  • 4、单继承和多继承关系中的虚函数表
    • 4.1、单继承中虚函数表
    • 4.2、多继承中虚函数表

  

1、多态介绍

1.1、多态是什么

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

1.2、构成多态的两个必备条件

  1)、总述
  多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。在继承中要构成多态还有两个条件
  1、必须通过基类的指针或者引用调用虚函数
  2、被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写
  

1.2.1、虚函数介绍

  1)、多态条件之一:虚函数概念
  虚函数: 即被关键字virtual修饰的类成员函数,称为虚函数。

  需要注意:
  1、virtual关键字有两用:其一,在继承关系中用于表示虚继承;其二,修饰类成员函数用于表示虚函数。

class Person
{
public:
	string _name;
};

class Student : virtual public Person//此处表示虚继承
{
protected:
	int _num; 
};
class Person {
public:
	virtual void BuyTicket() //类成员函数:虚函数
	{ 
		cout << "买票-全价" << endl; 
	}
};

  2、虚函数必须是类成员函数才行。
  
  
  2)、虚函数重写

  虚函数的重写(覆盖): 派生类中有一个跟基类完全相同的虚函数,称子类的虚函数重写了基类的虚函数。
  注意达成重写的条件: 派生类虚函数与基类虚函数的返回值类型、函数名字、参数列表完全相同。(若是不构成重写,当函数名相同时,父子类该函数关系为隐藏。)

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

  1、有父子类:说明有继承关系,可以看到此处Student、Solider继承了Person
  2、子类的虚函数重写了父类的虚函数:virtual void BuyTicket(),是虚函数、函数名、参数、返回值相同,构成重写。
  
  
  3)、虚函数的两个特例
  1、特例一:重写基类虚函数时,派生类的虚函数在不加virtual关键字时,也可以构成重写。

  2、特例二:重写协变。当派生类重写基类虚函数时,若要在与基类虚函数返回值类型不同的情况下保持虚函数重写关系,则要求返回类型必须是父子关系的指针或者引用,即:基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用,将其称为协变。
  
  此两个特例将在后续1.2.3中演示。
  
  

1.2.2、基类的指针或者引用调用虚函数

  1)、多态条件之一:基本说明
  以下述代码为例:void Func(Person& p):可看到此处形参p的类型是父类引用。

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& p)
{
	p.BuyTicket();
}

int main()
{
	Person ps;
	Student st;
	Soldier sl;
	Func(ps);
	Func(st);
	Func(sl);
	return 0;
}

  
  
  2)、结果演示
  在构成多态的情况下:当具备多态的两个条件时,我们去调用相关函数Func,结果如下:根据我们传入的不同类型,ps、st、sl分别得到不同结果,这就是多态,不同继承关系的类对象,去调用同一函数,产生了不同的行为。
在这里插入图片描述
在这里插入图片描述

  假如没有多态,单看下述这个函数,我们使用ps、st、sl去调用,由于p的类型为Person &st、sl调用时存在切片行为,因此我们将得到父类中的BuyTicket

void Func(Person& p)
{
	p.BuyTicket();
}

在这里插入图片描述

  
  

1.2.3、演示多态条件的破坏(两个特例说明)

1.2.3.1、不符合条件演示

  以下述代码为例:

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& p)
{
	p.BuyTicket();
}

void test01()
{
	Person ps;
	Student st;
	Soldier sl;
	Func(ps);
	Func(st);
	Func(sl);
}

  
  1)、演示一:破坏基类指针/引用

void Func(Person p)
{
	p.BuyTicket();
}

  可看到多态被破坏。
在这里插入图片描述

  
  

  2)、演示二:虚函数参数不同

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

class Student : public Person {
public:
	virtual void BuyTicket(char) { cout << "买票-半价" << endl; }
};

class Soldier : public Person {
public:
	virtual void BuyTicket(size_t) { cout << "买票-优先" << endl; }
};

void Func(Person& p)
{
	p.BuyTicket(1);
}

  可看到多态被破坏。

在这里插入图片描述
  
  

1.2.3.2、特例演示

  3)、演示三:子类虚函数不加virtual关键字
  在重写基类虚函数时,派生类的虚函数在不加virtual关键字时,也可以构成重写。因为虚函数的继承是一种接口继承,派生类继承的是基类虚函数的接口,故而基类的虚函数被继承下来,在派生类依旧保持虚函数属性。但是该种写法并不规范,一般不建议这样使用。

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

class Student : public Person {
public:
	//virtual void BuyTicket() { cout << "买票-半价" << endl; }
	void BuyTicket() { cout << "买票-半价" << endl; }

};

class Soldier : public Person {
public:
	//virtual void BuyTicket() { cout << "买票-半价" << endl; }
	void BuyTicket() { cout << "买票-优先" << endl; }
};

  可以看到多态仍旧成立。
在这里插入图片描述
  
  
  4)、演示四:虚函数返回类型不同,使用父子关系指针/引用
  重写协变:当派生类重写基类虚函数时,若要在与基类虚函数返回值类型不同的情况下保持虚函数重写关系,则要求返回类型必须是父子关系的指针或者引用,即:基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用,将其称为协变。

class Person {
public:
	virtual Person* BuyTicket() 
	{ 
		cout << "买票-全价" << endl; 
		return this;
	}
};

class Student : public Person {
public:
	virtual Student* BuyTicket() 
	{ 
		cout << "买票-半价" << endl; 
		return this;
	}
};

class Soldier : public Person {
public:
	virtual Soldier* BuyTicket() 
	{ 
		cout << "买票-优先" << endl;
		return this;
	}
};

在这里插入图片描述

  
  这里返回的父子类型指针/引用也可以是其它类的:

class A
{};

class B : public A
{};

class C:public A
{};

class Person {
public:
	virtual A* BuyTicket()
	{
		cout << "买票-全价" << endl;
		return nullptr;
	}
};

class Student : public Person {
public:
	virtual B* BuyTicket()
	{
		cout << "买票-半价" << endl;
		return nullptr;
	}
};

class Soldier : public Person {
public:
	virtual C* BuyTicket()
	{
		cout << "买票-优先" << endl;
		return nullptr;
	}
};

在这里插入图片描述
  
  
  
  

1.3、一道例题讲解

  1)、基本例题讲解
  以下程序输出结果是什么()

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;
}
A: A->0 
B: B->1 
C: A->1 
D: B->0 
E: 编译出错 
F: 以上都不正确

  
  阶段分析一:B* p = new B;p->test();,p指针指向类型为B,该指针调用了函数test()。virtual void test() { func(); },test是A类的成员函数,对应的其隐藏的this指针为A*,实际上这里实参属于B*,形参属于A*,发生了切片行为。test()函数中调用了func()函数,实则是this->func(),A* this 进行调用,满足多态条件之一,父类指针调用虚函数。
  
  阶段分析二:父子类中都函数func,根据特例情况,其满足虚函数重写。那么达成多态的必备两条件,故此处属于多态调用。多态调用,要看它实际实参行为,B* p = new B;,这里的p指向的是B类,故其会调用B类中的func。
  
  阶段分析三:virtual void func(int val = 1)void func(int val = 0),这里A、B两类中,func的缺省参数不同,但实际上,虚函数,其在子类中的属于接口型继承,即子类接口完全继承自父类,重写的是函数实现,函数体内那一部分。
  
  因此,此题选择B。
在这里插入图片描述

  
  
  2)、相关衍生
  假如我们做出如下修改,结果如何?
  将B* p = new B;修改为A* p = new B;

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[])
{
	A* p = new B;
	p->test();
	return 0;
}

  分析结果如下:
在这里插入图片描述

  
  
  
  

2、多态原理

2.1、虚函数表

  1)、问题引入:在存在虚函数的情况下,一个类有多大?

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

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

  演示结果如下:
在这里插入图片描述

  通过调试窗口,我们发现,Base中除了基本的成员变量_b、_ch外,还多一个__vfptr。对象中的这个指针我们叫做虚函数表指针(vf即virtualfunction。该指针存储位置与平台有关,平台可能会放到对象的最后面)。
  这说明:一个含有虚函数的类中,至少都有一个虚函数表指针,该指针指向一个虚函数表(实际上是函数指针数组),其用于存放虚函数的地址,通常,虚函数表也简称虚表。
  
  
  2)、虚函数表基本说明
  再次举例演示:

class Person {
public:
	virtual void BuyTicket() { cout << "买票-全价" << endl; }//虚函数,且子类中实现了函数重写

	virtual void Func() { cout << "Func" << endl; }//虚函数,但未进行函数重写


	int _a = 0;
};

class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }

	int _b = 0;
};

void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	//验证多态:构成多态后,为什么传入对象不同,同一函数调用不同?
	Person Mike;
	Func(Mike);

	Student Johnson;
	Func(Johnson);

	return 0;
}

在这里插入图片描述

  派生类的虚表生成:
  1、先将基类中的虚表内容拷贝一份到派生类虚表中 。
  2、如果派生类重写了基类中某个虚函数,用派生类自己的虚函数覆盖虚表中基类的虚函数 。
  3、派生类自己新增加的虚函数按其在派生类中的声明次序增加到派生类虚表的最后。
  
  注意事项:
  1、虚函数表本质是一个存虚函数指针的指针数组,一般情况这个数组最后面放了一个nullptr(VS下)。
  2、虚表存的是虚函数指针,不是虚函数,虚函数和普通函数一样的,都是存在代码段的,只是他的指针又存到了虚表中。另外对象中的不是虚表,存的是虚表指针。(vs下,虚表存在代码段中)
  
  
  
  

2.2、条件破坏

  1)、父类指针/引用 && 非父类指针/引用
  以下述代码为例:

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

	int _a = 0;
};

class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }

	int _b = 0;
};


   如下所示分别调用:

void Func(Person p)
{
	p.BuyTicket();
}

void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	
	Person Mike;
	Func(Mike);

	Student Johnson;
	Func(Johnson);

	return 0;
}

  可看到结果如下:
  1、多态调用:运行时决议,即运行时到指向对象的虚表中调用虚函数地址,因此,指向对象是谁,就调用谁的虚函数。
  2、普通调用:不构成多态,编译时就能确定调用函数的地址,运行时直接调用。
在这里插入图片描述

  
  
  2)、破坏虚函数virtual
  情况一:以BuyTicket为例,父类无虚函数,子类设置虚函数。

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

	virtual void Func() { cout << "Func" << endl; }


	int _a = 0;
};

class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }

	int _b = 0;
};

void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person Mike;
	Func(Mike);

	Student Johnson;
	Func(Johnson);

	return 0;
}

  结果:非多态调用,BuyTicket不进入虚表中,编译时决议。
在这里插入图片描述

  
  
  情况二:以BuyTicket为例,父类有虚函数,子类无虚函数。

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

	virtual void Func() { cout << "Func" << endl; }


	int _a = 0;
};

class Student : public Person {
public:
	//virtual void BuyTicket() { cout << "买票-半价" << endl; }

	int _b = 0;
};

void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person Mike;
	Func(Mike);

	Student Johnson;
	Func(Johnson);

	return 0;
}

  结果:BuyTicket进入虚表中,只是父类和子类虚函数地址相同,说明调用时都是调用父类的虚函数,因为子类没有完成重写,故而得到结果相同。
在这里插入图片描述

  
  
  

2.3、再识虚函数

2.3.1、析构函数与virtual重写

  1)、析构函数构成重写说明
  建议在继承中将析构函数定义成虚函数:如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写。虽然基类与派生类析构函数名字不同,但编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor

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

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

  
  2)、为什么建议重写
  1、普通情况演示:

int main()
{
	Person p;
	Student s;

	return 0;
}

  如下图,普通情况下的调用,遵循之前我们学习的父子类析构规则,先构造的后析构,子类中先析构子类,再析构父类。
在这里插入图片描述

  
  
  但是这种父子类中析构函数不设置为重写就能调用无误的情况有不适用的场景:
  2、特殊情况演示:如下,我们创建一个父类的指针,分别调用父子类,然后析构。此时析构函数是否重写结果差异很大。

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

	Person* ptr2 = new Student;
	delete ptr2;

	return 0;
}

在这里插入图片描述

  
  
  

2.3.2、C++11两个关键字:final && override

  1)、final关键字
  说明:
  1、在继承中,如果一个类不想被继承,可加上关键字final

class A final
{
public:
	A(){}
	//……

protected:
	int _a;
};

class B : public A //error
{
	//……
};

  2、在多态中,如果一个类的虚函数不想被重写,可加上关键字final

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

class Student : public Person {
public:
	virtual void BuyTicket() //error
	{ 
		cout << "买票-半价" << endl; 
	}
};

class Soldier : public Person {
public:
	virtual void BuyTicket() //error
	{ 
		cout << "买票-优先" << endl; 
	}
};

在这里插入图片描述

  
  
  2)、overide关键字
  说明:检查派生类虚函数是否重写了基类某个虚函数,如果没有重写,则编译报错。

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

class Student : public Person {
public:
	virtual void BuyTicket() override
	{ 
		cout << "买票-半价" << endl; 
	}
};

class Soldier : public Person {
public:
	virtual void BuyTicket(int) override
	{ 
		cout << "买票-优先" << endl; 
	}
};

在这里插入图片描述

  
  

2.3.3、概念比较:函数重写、函数重载、函数重定义

在这里插入图片描述

  
  
  

3、抽象类、接口继承

3.1、抽象类介绍

  1)、纯虚函数和抽象类

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

在这里插入图片描述

class Car
{
public:
	virtual void Drive() = 0;//构成纯虚函数,则Car为抽象类
};

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

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

void Test()
{
	Car* pBenz = new Benz;
	pBenz->Drive();
	Car* pBMW = new BMW;
	pBMW->Drive();
}

  
  
  

3.2、接口继承和实现继承

  1)、相关介绍

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

4、单继承和多继承关系中的虚函数表

4.1、单继承中虚函数表

  1)、问题引入1:同一个父类,实例化对象,是否共用同一个虚表?
  验证代码如下:

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

int main()
{
	Person p1;
	Person p2;

	return 0;
}

  验证结果如下:可以看到二者共用一个虚表。同一个类型的对象共用一个虚表。
在这里插入图片描述

  
  

  2)、问题引入2:子类继承父类,实例化对象,是否共用同一个虚表?
  验证代码如下:

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

class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

int main()
{
	Person p1;
	Student s1;

	return 0;
}

  验证结果如下:在构成虚函数重写的情况下,子类会对相应的虚函数进行重写,且子类虚表应该存子类的虚函数,父类的虚表存父类的虚函数,故而二者有自己的虚表。
在这里插入图片描述

  
  但是需要注意,在之前破坏虚函数virtual里我们演示过,若子类没有完成虚函数重写,此时父子类虚表如下:
在这里插入图片描述
  
  

  3)、问题引入3:同一个子类,实例化对象,是否共用同一个虚表?
  验证代码如下:

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

class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

int main()
{
	Student s1;
	Student s2;

	return 0;
}

  验证结果如下:
在这里插入图片描述

  
  
  4)、问题引入4:当父子类有各自的虚函数存在时,虚表内呈现效果如何?
  验证代码如下:

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

};

class Student : public Person {
public:
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
	virtual void Func2(){}
};

int main()
{
	Person p1;
	Student s1;
	return 0;
}

  验证结果如下:
在这里插入图片描述

  可以看到,父类和子类虚表是否共用、虚函数重写时、不重写时结果与我们之前验证的一致。但是此处有一个问题,在监视窗口里,我们似乎没在虚表中看到子类的Func2?
  
  

在这里插入图片描述 解决方案一: 通过内存窗口初步判断

  如下述,我们在内存窗口中输入子类虚函数表指针,可以看到其虚表(函数指针数组)中内容如下:

0x002B9CF4  6a 14 2b 00  j.+.
0x002B9CF8  74 14 2b 00  t.+.
0x002B9CFC  6f 14 2b 00  o.+.

在这里插入图片描述
  但这种方法只能粗略验证,故而我们可以使用以下方法观察:
  
  

在这里插入图片描述 解决方案二: 写一个函数,用来打印虚函数表

  前提认识:

	void(*ptr)(); //创建一个函数指针变量
	typedef void(*ptr)(); //对一个函数指针变量进行重命名

  相关函数如下:

typedef  void (*VFPTR)();
void PrintVTable(VFPTR VTable[])
{
	cout << " 虚表地址>" << VTable << endl;
	for (size_t i = 0;  VTable[i] != nullptr; ++i)
	{
		printf("VTable[%d],地址为:%p,对应函数为:",i, VTable[i]);
		VTable[i]();
		//等价于:
		//VFPTR ptr=VTable[i];
		//ptr();//通过函数指针,调用函数
	}
	cout << endl;
}

  VFPTR VTable[],既然是要打印虚函数表,也就意味着要打印函数指针数组,这里形参使用的是一个函数指针。
  VTable[i] != nullptr,for循环中设置此为结束条件,是结合了VS下虚函数表最后会放置一个nullptr。
  VTable[i]();,这是函数调用,该部分涉及函数指针相关内容,可回顾C阶段所学:指针
  
  
  调用写法如下:

int main()
{
	Person p1;
	Student s1;

	VFPTR* Tablep1 = (VFPTR*)(*(int*)(&p1));
	PrintVTable(Tablep1);

	VFPTR* Tables1 = (VFPTR*)(*(int*)(&s1));
	PrintVTable(Tables1);

	return 0;
}

  我们要打印虚函数表中的函数地址,地址32位下实际上为四字节,因此假如我们直接传递指向整个类的指针,那么根据指针访问权限,其一次++、--跨步与我们的需求不一致。因此此处要进行处理。

  (int*)(&p1),首先要明确,直接使用类型强制转换不一定能成功,比如此处的自定义类型Student和Person, 我们不能直接将其转换为int类型。但是自定义类型的指针和内置类型的指针,同属于指针,可以互相转换。
  (*(int*)(&p1)),对int*类型的指针解引用,一次能访问4bytes,因此我们获得对象首4bytes的值,这个值就是指向虚表的指针。注意,这里之所以要解引用,是因为我们要获取的是对象中头四个字节的内容,而非是对象本身的地址,故而直接(int*)(&s1)是不能满足预期的。
  (VFPTR*)(*(int*)(&p1)),强转成VFPTR*,因为虚表就是一个存VFPTR类型(虚函数指针类型)的数组。
  
  
  演示结果如下:可以看到,子类中的虚函数表,实际上也存储了没有重写的虚函数 。
在这里插入图片描述

  
  
  
  
  

4.2、多继承中虚函数表

  1)、多态继承中虚函数表演示

  演示代码如下:
  Base1、Base2为基类,Derive分别继承二者。func1为重写的虚函数,func2Base1、Base2独自的虚函数,func3Derive自己内部的虚函数。

class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1 = 1;
};

class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
	int b2 = 2;
};

class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d = 3;
};

  
问题一:上述多继承中,子类大小?

int main()
{
	Derive d;
	cout << "Derive:" << sizeof(d) << endl;

	return 0;
}

  结果如下:根据继承,至少我们能够知道,子类Derive d中除了有相关成员变量,还有对应的虚表指针。有一个问题是,子类继承多个父类,多个父类共用一个虚表,还是彼此有独立的虚表?
在这里插入图片描述
  由上述结果,我们可以初步判断,多继承中,子类分别继承父类各自的虚表。接下来我们通过监视窗口来观察:
在这里插入图片描述  可以看到,1、父类在子类中有各自的虚表,因此子类实际存储了父类各自的虚表指针;2、对于重写的虚函数func1,在两个父类虚表中都存在,对于父类自己的虚函数func2,子类也将其继承下来,同样存储在父类的虚表中。
  同样,这里也存在两个问题:
  1、子类自己也有虚函数func3,而子类只有两个虚表,那么这个虚函数存储在哪个虚表中?
  2、既然func1构成虚函数重写,在对应的父类虚函数表中都指向同一个(由Derive::func1可知),为什么二者地址不同0x004a1230、0x004a133e
  
  

在这里插入图片描述对问题一:子类自己的虚函数存储位置

  解决方案:同之前类似,我们可以将子类中虚函数表打印出来观察。

typedef void(*VFPTR)();
void PrintVTable(VFPTR VFtable[])
{
	cout << "虚表地址>" << VFtable << endl;
	for (size_t i = 0; VFtable[i] != nullptr; ++i)
	{
		printf("VTable[%d],地址为:%p,对应函数为:", i, VFtable[i]);
		VFtable[i]();
	}
	cout << endl;
}

  1、打印Base1的虚表,PrintVTable((VFPTR*)(*(int*)(&d)));,直接对d对象进行转换即可。
  2、打印Base2的虚表,这个需要进行一定处理,我们要在d对象中找到Base2的起始位置,又或者说Base2的虚表指针,再根据虚表指针取地其虚表函数。
    ①方法一:(char*)&d+sizeof(Base1)):我们可以对d对象取地址,再跳过整个Base1类,但需要注意指针访问权限,(char*)&d,需要先将其强制类型转换为char*,这样指针±整数,单次访问的就是一个字节。
    ②方法二:除了上述方法,我们还可以使用切片思想Base2* ptr2 = &d;

int main()
{
	Derive d;
	cout << "Derive:" << sizeof(d) << endl;

	//打印Base1虚表:
	VFPTR* ptrBase1 = (VFPTR*)(*(int*)(&d));
	PrintVTable(ptrBase1);

	//打印Base2虚表:借助sizeof
	VFPTR* ptrBase2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));
	PrintVTable(ptrBase2);

	//对于打印Base2虚表的另一种方法:切片
	Base2* ptr2 = &d;
	PrintVTable((VFPTR*)(*(int*)ptr2));

	return 0;
}

在这里插入图片描述

  
  

在这里插入图片描述对问题二:为什么指向子类同一个虚函数,但二者地址不同?

  结论:
  
  整体演示代码如下:


class Base1 {
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }
private:
	int b1 = 1;
};

class Base2 {
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }
private:
	int b2 = 2;
};

class Derive : public Base1, public Base2 {
public:
	virtual void func1() { cout << "Derive::func1" << endl; }
	virtual void func3() { cout << "Derive::func3" << endl; }
private:
	int d = 3;
};

typedef void(*VFPTR)();
void PrintVTable(VFPTR VFtable[])
{
	cout << "虚表地址>" << VFtable << endl;
	for (size_t i = 0; VFtable[i] != nullptr; ++i)
	{
		printf("VTable[%d],地址为:%p,对应函数为:", i, VFtable[i]);
		VFtable[i]();
	}
	cout << endl;
}


int main()
{
	Derive d;
	//打印Base1虚表:
	VFPTR* ptrBase1 = (VFPTR*)(*(int*)(&d));
	PrintVTable(ptrBase1);

	//打印Base2虚表:借助sizeof
	VFPTR* ptrBase2 = (VFPTR*)(*(int*)((char*)&d+sizeof(Base1)));
	PrintVTable(ptrBase2);

	//对于打印Base2虚表的另一种方法:切片
	Base2* ptrBase22 = &d;
	PrintVTable((VFPTR*)(*(int*)ptrBase22));


	d.func1();

	Base1* ptr1 = &d;
	ptr1->func1();
	
	Base2* ptr2 = &d;
	ptr2->func1();

	return 0;
}

  该问题我们需要结合一点汇编来理解:
在这里插入图片描述

  
  
  
  
  

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/471196.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【高危】Spring Boot在Cloud Foundry中部署存在路由限制绕过风险(CVE-2023-20873)

漏洞描述 Spring Boot是用于构建Java应用程序的框架&#xff0c;Cloud Foundry是用于部署和管理应用程序的云平台&#xff0c;/cloudfoundryapplication也会路由至Spring Boot actuator。 当 Spring Boot 的受影响版本部署在 Cloud Foundry 上且具有处理 /cloudfoundryapplic…

计算机网络-应用层和传输层协议分析实验(PacketTracer)

实验三.应用层和传输层协议分析实验 一.实验目的 通过本实验&#xff0c;熟悉PacketTracer的使用&#xff0c;学习在PacketTracer中仿真分析应用层和传输层协议&#xff0c;进一步加深对协议工作过程的理解。 二.实验内容 从 PC 使用 URL 捕获 Web 请求&#xff0c;运行模拟并…

第06讲:为何各大开源框架专宠 SPI 技术?

在此前的内容中&#xff0c;已经详细介绍了 SkyWalking Agent 用到的多种基础技术&#xff0c;例如&#xff0c;Byte Buddy、Java Agent 以及 OpenTracing 中的核心概念。本课时将深入介绍 SkyWalking Agent 以及 OAP 中都会使用到的 SPI 技术。 JDK SPI 机制 SPI&#xff08…

Oracle-12c版本之后替换OCR磁盘组步骤

背景: 用户有一套Oracle12.2的RAC集群&#xff0c;在安装配置的时候&#xff0c;OCR磁盘只使用了单块磁盘external的模式&#xff0c;想替换成包含三块磁盘组成员normal模式的磁盘组 OCR磁盘组存储的对象: 在替换OCR磁盘之前&#xff0c;我们先确认需要迁移的OCR磁盘组存储的对…

图扑数字孪生助力智慧冷链园区实现大数据实时监控

前言 近年来&#xff0c;业界学者及企业就智慧冷链物流展开深入研究&#xff0c;2010 年 IBM 发布的《智慧的未来供应链》研究报告中提出智慧供应链概念&#xff0c;并由此延伸出智慧物流概念&#xff0c;即智慧物流是以信息化为依托并广泛应用物联网、人工智能、大数据、云计…

设置rocky Linux ip 与主机服务器处于同一网段内,并且能上网

第一步&#xff1a;查找主机服务器的 了解地址信息 第二步&#xff0c;设置rocky Linux 网络适配器连接&#xff0c;选择桥接模式 第三步&#xff1a;设置rocky Linux ip 第四步&#xff0c;设置完&#xff0c;重启Linux &#xff0c;验证ip是否修改过来&#xff0c;是否在同一…

【python】scikit-learn包:模型评估与优化

模型构建的目的 首先明确&#xff0c;模型拟合的目的&#xff1a; 不是对训练数据进行准确预测&#xff0c;而是对新数据进行准确预测 欠拟合 与 过拟合 欠拟合&#xff1a;可以通过训练数据及时发现&#xff0c;且可通过优化模型结果解决 过拟合&#xff1a;难以发觉&#x…

Golang基础----基于Goland编辑器快速体验Golang

【原文链接】Golang基础----基于Goland编辑器快速体验Golang &#xff08;1&#xff09;打开Goland&#xff0c;点击“New Project” &#xff08;2&#xff09;设置项目存放位置以及项目名&#xff0c;然后点击“Add SDK”&#xff0c;然后点击“Local” &#xff08;3&a…

vue3 的router跳转 - 页面同tab跳转和打开新tab跳转

vue3 的router跳转 - 页面同tab跳转和打开新tab跳转 vue3的路由基本知识 当前页需要的方法 主要是获得Vue Router实例暴露的一些方法&#xff0c;使用这些方法&#xff0c;进行路由操作引入 import { useRouter } from vue-router;调用 const router useRouter();目标页需…

Android jetpack Compose之约束布局

概述 我们都知道ConstraintLayout在构建嵌套层级复杂的视图界面时可以有效降低视图树的高度&#xff0c;使视图树扁平化&#xff0c;约束布局在测量布局耗时上比传统的相对布局具有更好的性能&#xff0c;并且约束布局可以根据百分比自适应各种尺寸的终端设备。因为约束布局确…

simulink simscape传感总结

1. 传感模块概述2. 可观测的传感量3. 传感模块3.1 运动传感模块3.1.1 旋转和平移细分旋转平移 3.2 力传感模块3.2.1 关节力和力矩细分 1. 传感模块概述 Simscape提供传感模块&#xff0c;通过改变模型的输入和输出&#xff0c;可以进行许多分析&#xff0c;比如可以进行机械臂…

中移链控制台对接4A平台功能验证介绍

中移链控制台具备单独的注册登录页面&#xff0c;用户可通过页面注册或者用户管理功能模块进行添加用户&#xff0c;通过个人中心功能模块进行用户信息的修改和密码修改等操作&#xff0c;因业务要求&#xff0c;需要对中移链控制台的用户账号进行集中管理&#xff0c;统一由 4…

2 文件IO

2.1 文件描述符 对于内核而言&#xff0c;所有打开文件都由文件描述符引用。文件描述符是一个非负整数。当打开 一个现存文件或创建一个新文件时&#xff0c;内核向进程返回一个文件描述符。当读、写一个文件时&#xff0c; 用open或creat返回的文件描述符标识该文件&#xff0…

QT 网络编程之主机信息查询(QHostInfo 和 QNetworkInterface类)简介

Qt 网络模块提供了用于编写 TCP/IP 客户端和服务器端程序的各种类&#xff0c;如用于 TCP 通信的QTcpSocket 和 QTcpServer&#xff0c;用于 UDP 通信的 QUdpSocket&#xff0c;还有用于实现 HTTP、FTP 等普通网络协议的高级类如 QNetworkRequest&#xff0c;QNetworkReply 和Q…

【Redis】Redis中的5种基础数据类型详解

文章目录 1. Redis数据结构2. 基础数据结构详解2.1 String字符串2.2 List列表2.3 Set集合2.4 Hash散列2.5 Zset有序集合 1. Redis数据结构 对于Redis来说&#xff0c;所有的key&#xff08;键&#xff09;都是字符串&#xff0c;我们一般所讨论的Redis基础数据类型指的是存储的…

ref在Vue2、Vue3中的使用

文章目录 前言一、ref在Vue2中的用法二、ref在Vue3中的用法 前言 记录一下ref在Vue2与Vue3中的使用&#xff0c;ref可以获取DOM元素&#xff0c;也可以获取子组件的数据、方法。 一、ref在Vue2中的用法 给元素绑定一个ref&#xff0c;然后在js中通过this.$refs获取DOM。 ref命…

@PostConstruct注解和@PreDestroy注解

前言 Bean注解指定初始化和销毁的方法&#xff0c;也介绍了使用InitializingBean和DisposableBean来处理bean的初始化和销毁。JDK中还提供了两个注解能够在bean创建完成并且属性赋值完成之后执行一些初始化工作和在容器销毁bean之前通知我们进行一些清理工作。 1.PostConstru…

java 获取当前线程的方法

我们知道&#xff0c;线程是程序运行的基本单元&#xff0c;是程序中各进程之间通信的桥梁&#xff0c;一个线程的创建和销毁直接影响整个程序的运行效率。 我们在 Java中经常使用 Runnable接口来获取当前线程&#xff0c;获取线程的主要目的就是为了快速地启动进程。但是&…

一键导出ChatGPT聊天记录:让备份更简单

重要性&#xff1a; 备份ChatGPT的聊天记录同样非常重要&#xff0c;因为这些记录可能包含了您与ChatGPT的交互记录&#xff0c;这些记录可能包含了您的个人信息、兴趣爱好、偏好和其他敏感信息。以下是备份ChatGPT聊天记录的一些重要性&#xff1a; 防止数据丢失&#xff1a;…

FT2000+ qemu kvm 64C64G 通过频繁设置CPU online 状态导致虚拟机红旗操作系统假死测试用例

宿主机配置 虚拟机配置文件 <domain typekvm> //如果是Xen&#xff0c;则type‘xen’<name>redflag1</name> //虚拟机名称&#xff0c;同一物理机唯一<uuid>44748c15-7c00-4817-8724-675a27c3f821</uuid> //同一物理机唯一&#xff0c;可用uu…