C++之多态

news2024/12/23 12:54:56

文章目录

  • 一、多态的理解
  • 二、多态的定义及实现
    • 1.多态的构成条件
    • 2.虚函数
    • 3.虚函数的重写/覆盖
    • 4. C++11 的 override 和 final
    • 5.重载、重写/覆盖、隐藏/重定义
  • 三、抽象类
  • 四、多态的原理
    • 1.虚函数表
    • 2.多态的原理
    • 3.动态绑定与静态绑定
  • 五、单继承和多继承关系的虚函数表
    • 1.单继承中的虚函数表
    • 2.多继承中的虚函数表
    • 3.菱形继承、菱形虚拟继承中的虚函数表

一、多态的理解

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

比如:买票这个行为,当普通人买票时,是全价买票;学生买票时,是半价买票。

// 静态的多态:函数重载,看起来调用同一个函数有不同的行为,编译时实现
// 动态的多态:一个父类的指针或引用去调用同一个函数,传递不同的对象,会调用不同的函数,运行时实现
// 本质:不同的对象去做同一件事情,行为不同

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

class Student : public Person 
{
public:
	// 子类中满足三同(函数名、参数、返回值)的虚函数,叫做重写/覆盖
	virtual void BuyTicket() { cout << "买票-半价" << endl; }
};

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

int main()
{
	Person ps;
	Student st;

	Func(ps);
	Func(st);

	return 0;
}

运行结果是:
买票-全价
买票-半价

二、多态的定义及实现

1.多态的构成条件

那么在继承中要构成多态有两个条件:

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

这两个条件缺一不可。

2.虚函数

虚函数:被virtual修饰的类的非静态成员函数称为虚函数。

其它函数不能成为虚函数。

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

3.虚函数的重写/覆盖

虚函数的重写/覆盖:派生类中有一个跟基类完全相同的虚函数,称派生类的虚函数重写/覆盖了基类的虚函数。

注:完全相同,指函数名、参数、返回值完全相同。

派生类虚函数重写时,继承的是基类虚函数的接口,重写的是虚函数的实现!

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

class Student : public Person 
{
public:
	// 子类中满足三同(函数名、参数、返回值)的虚函数,叫做重写/覆盖
	virtual void BuyTicket() { cout << "买票-半价" << endl; }

	// 注意:在重写基类虚函数时,若派生类的虚函数不加virtual关键字,也可以构成重写
	// 因为基类的虚函数被继承下来了,在派生类依旧保持虚函数属性
	// 若其访问限定符为非公有,也能调用,因为保持了基类虚函数的访问限定符
	// 重写的本质是重写了内容
	// 但是这种写法不是很规范,不建议这样使用
	//void BuyTicket() { cout << "买票-半价" << endl; }
};

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

int main()
{
	Person ps;
	Student st;

	Func(ps);
	Func(st);

	return 0;
}

运行结果是:
买票-全价
买票-半价


验证构成多态的条件:

① 破坏条件1,没有通过基类的指针或引用调用虚函数:

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

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

// 若构成多态,传的是哪个类的对象,调用的就是这个类的虚函数 -- 跟对象有关
// 若不构成多态,调用的就是p类的函数 -- 跟Func的参数类型有关
void Func(Person p)
{
	p.BuyTicket();
}

int main()
{
	Person ps;
	Student st;

	Func(ps);
	Func(st);

	return 0;
}

运行结果是:
买票-全价
买票-全价

② 破坏条件2,不是虚函数:

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

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

// 若构成多态,传的是哪个类的对象,调用的就是这个类的虚函数 -- 跟对象有关
// 若不构成多态,调用的就是p类的函数 -- 跟Func的参数类型有关
void Func(Person& p)
{
	p.BuyTicket();
}

int main()
{
	Person ps;
	Student st;

	Func(ps);
	Func(st);

	return 0;
}

运行结果是:
买票-全价
买票-全价

③ 破坏条件2,不构成重写。

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

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

// 若构成多态,传的是哪个类的对象,调用的就是这个类的虚函数 -- 跟对象有关
// 若不构成多态,调用的就是p类的函数 -- 跟Func的参数类型有关
void Func(Person& p)
{
	p.BuyTicket(10);
}

int main()
{
	Person ps;
	Student st;

	Func(ps);
	Func(st);

	return 0;
}

运行结果是:
买票-全价
买票-全价


虚函数重写的两个例外:

  1. 协变
    派生类重写基类虚函数时,与基类虚函数返回值类型可以不同,不过必须是基类虚函数返回基类对象的指针或者引用,派生类虚函数返回派生类对象的指针或者引用,称为协变。

测试代码1:

// 重写要求返回值相同有一个例外:协变 -- 返回值是父子关系的指针或引用
class Person 
{
public:
	virtual Person* BuyTicket() { cout << "买票-全价" << endl; return nullptr; }
};

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

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

int main()
{
	Person ps;
	Student st;

	Func(ps);
	Func(st);

	return 0;
}

测试代码2:

// 重写要求返回值相同有一个例外:协变 -- 返回值是父子关系的指针或引用
class A {};
class B : public A {};

class Person 
{
public:
	virtual A* f() { cout << "Person" << endl; return nullptr; }
};
class Student : public Person 
{
public:
	virtual B* f() { cout << "Student" << endl; return nullptr; }
};

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

int main()
{
	Person ps;
	Student st;
	
	Func(ps);
	Func(st);
	
	return 0;
}
  1. 析构函数的重写
    如果基类的析构函数为虚函数,此时派生类析构函数只要定义,无论是否加virtual关键字,都与基类的析构函数构成重写,虽然基类与派生类的析构函数名字不同。析构函数名不相同,看起来违背了重写的规则,其实不然,这里可以理解为编译器对析构函数的名称做了特殊处理,编译后析构函数的名称统一处理成destructor
// 若析构函数是虚函数,则构成重写
// 析构函数名被特殊处理了,处理成了destructor
class Person 
{
public:
	virtual ~Person() { cout << "~Person()" << endl; }
};

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

int main()
{
	// 普通对象,析构函数是否是虚函数,是否完成重写,都正确调用了
	//Person p;
	//Student s;

	// 动态申请的父子对象,如果给了父类指针管理
	// 那么需要析构函数是虚函数,子类完成重写,构成多态
	// 这样才能正确调用子类的析构函数
	Person* p1 = new Person;
	Person* p2 = new Student;

	delete p1;
	delete p2;
	// 若父类的析构函数不是虚函数
	// 那么delete p2时没有正确调用子类析构函数,导致内存泄漏


	// 其它场景,析构函数是否为虚函数,都可以正确调用析构函数
	// 比如上面的普通对象场景

	return 0;
}

4. C++11 的 override 和 final

  1. final
    ① 修饰类,表示该类不能被继承。
    ② 修饰虚函数,表示该虚函数不能被重写。
// C++11 final 修饰类,直接限制它不能被继承
class A final
{
protected:
	int _a;
};

class B : public A    // 编译报错
{

};

// C++11 final 修饰虚函数,限制它不能被子类中的虚函数重写
class C
{
public:
	virtual void f() final
	{
		cout << "C::f()" << endl;
	}
};

class D : public C
{
public:
	virtual void f()    // 编译报错
	{
		cout << "D::f()" << endl;
	}
};
  1. override:检查派生类虚函数是否重写了基类某个虚函数。如果没有重写,则会编译报错。
// C++11 override 放在子类虚函数后面,检查它是否重写了父类的某个虚函数
// 如果没有重写,则会编译报错

// 比如想让父类的某个函数被子类重写,但是忘了加virtual关键字
// 给子类虚函数加了override,就会检查到没有重写父类的某个虚函数,就会编译报错
class Car 
{
public:
	void Drive() 
	{}
};

class Benz :public Car 
{
public:
	virtual void Drive() override  // 编译报错
	{
		cout << "Benz-舒适" << endl;
	}
};

5.重载、重写/覆盖、隐藏/重定义

在这里插入图片描述

三、抽象类

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

override只是在语法上检查派生类虚函数是否重写了基类某个虚函数。

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

// 抽象 -- 在现实世界中没有对应的实物
// 一个类型,如果一般在现实世界中,没有具体的对应实物,就定义成抽象类比较好
class Car  // 抽象类
{
public:
	// 纯虚函数一般只声明不实现(可以实现,但是没有价值)
	virtual void Drive() = 0;
};

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

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

int main()
{
	//Car c;  // 编译报错,因为抽象类不能实例化出对象

	Car* p1 = new Benz;
	p1->Drive();

	Car* p2 = new BMW;
	p2->Drive();

	return 0;
}

四、多态的原理

1.虚函数表

先看下面的代码:

// 在32位平台下,sizeof(Base) == ?
class Base
{
public:
	virtual void Func1()
	{
		cout << "Func1()" << endl;
	}

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

protected:
	int _b = 1;
	char _ch = 'A';
};

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

	return 0;
}

在 32 位平台下,sizeof(Base) == 12 。
在这里插入图片描述
原因:
虚函数表指针(即 vfptr ,v 代表 virtual ,f 代表 function),指向虚函数表。
一个含有虚函数的类,其对象至少有一个虚函数表指针(在当前平台放在对象的最前面,注意有些平台可能会放到对象的最后面,这个跟平台有关)。
所以,加入虚函数表指针后再按照内存对齐规则计算,在 32 位平台下,该类的大小为 12 。

  • 一个虚函数表指针指向一张虚函数表(也简称为虚表),虚函数表里存放虚函数的地址,即虚函数表本质就是函数指针数组。

在这里插入图片描述

  • 虚函数表在编译阶段生成,存放在常量区(常量区是 C/C++ 语言角度,在操作系统角度常量区和代码段都是代码区)。

用一段代码来进行验证:

// 还是使用上面的代码
int main()
{
	int a = 0;
	printf("栈:%p\n", &a);

	int* p = (int*)malloc(4);
	printf("堆:%p\n", p);

	static int b = 0;
	printf("数据段:%p\n", &b);

	const char* str = "aaaaaa";
	printf("常量区:%p\n", str);

	printf("代码段:%p\n", &Base::Func1);

	Base bs;
	printf("虚函数表:%p\n", *((int*)&bs));  // 查看虚表指针内容(虚表的地址)

	return 0;
}

我们可以看到,虚表存放的位置离常量区最近,实际上虚表存放在常量区。
为什么虚表会存放在常量区呢?因为虚表创建好后是不会被修改的。
在这里插入图片描述

  • 虚函数的重写,也叫做覆盖。子类继承父类后,会先拷贝一份父类的虚表,然后再把子类重写的虚函数地址覆盖虚表中父类虚函数地址,形成子类的虚表。
    因此,可以认为重写是语法层的概念,覆盖是原理层的概念
class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}

protected:
	int _b = 1;
};

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

protected:
	int _d = 2;
};

int main()
{
	Base b;
	Derive d;

	return 0;
}

虚表里只存放虚函数的地址,非虚函数的地址不会被放进虚表。
在这里插入图片描述

2.多态的原理

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

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

protected:
	int _b = 2;
};

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

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

	Student Johnson;
	Func(Johnson);

	return 0;
}

解释多态的原理:在这里插入图片描述

多态是如何实现的?
 ① 普通函数的调用都是在编译时直接决定地址的,而满足多态的虚函数的调用是运行时才能决定地址的:先通过父类的指针或引用找到对象的虚表指针,再通过虚表指针找到其指向的虚表,然后在虚表中找对应函数的地址。
 ② 若传递的是父类对象,父类的指针或引用指向的是父类对象,它找父类对象的虚表,调用的是父类的虚函数。
 ③ 若传递的是子类对象,会发生切片,父类的指针或引用指向的是子类对象中的父类对象,它找父类对象的虚表,由于子类的虚函数完成了覆盖(子类虚函数地址覆盖了父类虚函数地址),调用的是子类的虚函数。
 ④ 所以满足多态的虚函数的调用过程中,执行相同的指令,调用的是不同的虚函数。

多态的实现依赖于虚函数的重写,虚函数重写了以后,父子类对象的虚表中存的就是不同的虚函数地址,就能实现多态。

为什么多态的实现必须是基类的指针或引用呢?

还是上面的代码,通过对比对象切片和引用切片就可以理解了:
在这里插入图片描述

 ① 引用切片,r1 是子类对象中父类对象的别名(指针的话,就是指向子类对象中的父类对象),指针或引用切片都没有出现对子类对象中的父类对象进行拷贝的行为。实现多态时,调用的虚函数一定是子类对象虚表中的虚函数。
 ② 对象切片,p 是 Johnson 子类对象中父类对象的拷贝,但唯独 p 的虚表指针不是 Johnson 子类对象中虚表指针的拷贝,而且 p 的虚表指针也不可能是 Johnson 子类对象中虚表指针的拷贝,父类对象的虚表指针一定指向父类虚表,子类对象的虚表指针一定指向子类虚表。
 ③ 因此,多态的实现必须是基类的指针或引用。


同类型的对象,虚表指针是相等的,指向同一张虚表。

// 还是使用上面的代码
int main()
{
	Person p1;
	Person p2;

	Student s1;
	Student s2;
	
	return 0;
}

在这里插入图片描述


普通函数和虚函数的存储位置是一样的,都在代码段。只是虚函数又把地址存到虚表中,方便实现多态。

  • 普通函数的调用,在编译时就能决定它的地址。
  • 满足多态的虚函数的调用,不是编译时就能决定它的地址的,而是运行时需要到对象的虚表中寻找,才能决定它的地址。
  • 不满足多态的虚函数的调用,跟普通函数的调用一样,在编译时就能决定它的地址,不需要在运行时到对象的虚表中寻找。

总结:在编译时能直接决定调用函数的地址的,一定会在编译时决定。满足多态的虚函数调用,在编译时是不能直接决定调用虚函数的地址的,只能在运行时去对象虚表中找虚函数的地址,这样才能决定。

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

public:
	int _a = 1;
};

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

protected:
	int _b = 2;
};

void Func(Person& p)
{
	p.BuyTicket();  // 满足多态,运行时决定函数地址
	p.f();          // 编译时决定函数地址
}

int main()
{
	Person Mike;
	Func(Mike);
	
	Student Johnson;
	Func(Johnson);
	
	Mike.BuyTicket();     // 编译时决定函数地址
	Johnson.BuyTicket();  // 编译时决定函数地址

	return 0;
}

在这里插入图片描述

3.动态绑定与静态绑定

  • 静态绑定又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。
  • 动态绑定又称为后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态

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

1.单继承中的虚函数表

我们都知道,派生类中的虚函数,无论是否重写,都会被放到虚表中:重写的虚函数会覆盖,自己新增加的虚函数会按其在派生类中的声明次序增加到虚表的最后。

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}

protected:
	int _b = 1;
};

class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
	virtual void Func4()
	{
		cout << "Derive::Func4()" << endl;
	}

protected:
	int _d = 2;
};

int main()
{
	Derive d;
	
	Base* p1 = &d;
	p1->Func1();

	return 0;
}

由于监视窗口隐藏了派生类虚表中未重写的虚函数,所以需要通过内存窗口观察派生类对象的虚表才能看到:
在这里插入图片描述


虚表中存的地址,严格上来说,其实也不是真正意义上的虚函数地址,而是在虚函数地址基础上封装了一层的地址。

这里虚函数的调用满足多态,eax 中存放的是从虚表中取出来的地址,这个就是封装过的虚函数地址。
执行 call 指令后,会到那个地址(jmp 指令的地址)。接着执行 jmp 指令,才会跳到真正意义上的虚函数地址。
换言之,虚表中存的是 jmp 指令的地址,而不是真正的虚函数地址,实际调用时相当于多跳了一层。在这里插入图片描述


我们也可以通过打印虚表的方式去查看虚表:

class Base
{
public:
	virtual void Func1()
	{
		cout << "Base::Func1()" << endl;
	}
	virtual void Func2()
	{
		cout << "Base::Func2()" << endl;
	}
	void Func3()
	{
		cout << "Base::Func3()" << endl;
	}

protected:
	int _b = 1;
};

class Derive : public Base
{
public:
	virtual void Func1()
	{
		cout << "Derive::Func1()" << endl;
	}
	virtual void Func4()
	{
		cout << "Derive::Func4()" << endl;
	}

protected:
	int _d = 2;
};

// 这里虚函数的类型都是void(*)(),方便我们打印虚表

// 将函数指针类型void(*)()重命名为VF_PTR,简化类型名称
typedef void(*VF_PTR)();

// 依次取虚表中的虚函数指针打印并调用
// 通过调用可以直观地看出存的是哪个虚函数
//void PrintVFTable(VF_PTR table[])    // 两种写法均可
void PrintVFTable(VF_PTR* table)
{
	for (int i = 0; table[i] != nullptr; ++i)  // VS在虚表的最后会放nullptr 
	{
		printf("vft[%d]:%p -> ", i, table[i]);
		table[i]();
	}
	cout << endl;
}

int main()
{
	Base b;
	// 为了拿到虚表指针的值,需要取b头上4/8(32位/64位)个字节的内容
	// 先强转成能看到b头上4/8个字节的指针类型int*/long long*
	// 解引用就能取到b头上4/8个字节的内容
	// 指针类型int*/long long*解引用后就是int/long long
	// 但这不是我们想要的类型,所以还需强转为VF_PTR*
	// 因为这里的虚表就是一个存VF_PTR类型(函数指针类型)的数组
	// 最后传参给PrintVFTable函数
	//PrintVFTable((VF_PTR*)(*(int*)&b));        // 32位平台
	//PrintVFTable((VF_PTR*)(*(long long*)&b));  // 64位平台
	// 上面的这种方式不能根据32位和64位平台自适应,不是很好
	// 其实最好的方式是先强转为二级指针
	// 因为二级指针能看到b头上的一个指针这么多个字节
	// 解引用就能取到b头上的一个指针大小的内容
	// 又因为指针的大小在32/64位下是4/8个字节
	// 所以这种方式在32位和64位平台下能自适应
	// 虽说是二级指针都可以,但是建议用void**,免得引起别人疑惑
	PrintVFTable((VF_PTR*)(*(void**)&b));

	Derive d;
	PrintVFTable((VF_PTR*)(*(void**)&d));

	return 0;
}

运行结果:
在这里插入图片描述

其实这种方式有点非法,因为可以乱调用虚表中的虚函数。所以说虚表是有安全小隐患的。当然,我们在这里只是以这种方式去查看虚表而已,实际中并不会这样用。

2.多继承中的虚函数表

多继承中,派生类未重写的虚函数,会被放到第一个继承基类的虚表中。

派生类中基类继承顺序的声明,就是派生类实际继承基类的顺序。

class Base1 
{
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }

protected:
	int _b1;
};

class Base2 
{
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }

protected:
	int _b2;
};

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

protected:
	int _d;
};

int main()
{
	Derive d;
	
	Base1* p1 = &d;
	p1->func1();
	Base2* p2 = &d;
	p2->func1();
	
	return 0;
}

通过内存窗口观察派生类对象的虚表:
在这里插入图片描述

在多继承中,子类重写了父类 Base1 和 Base2 的虚函数 func1() ,可是两个虚表中重写的 func1() 的地址不相同,这是因为虚表中存的是 jmp 指令的地址,而不是真正的虚函数地址,但最终都会跳到同一个真正的虚函数地址。这个可以通过调试反汇编来进行验证。

实际上,在多继承中,满足多态的情况下,相比于用第一个继承基类的指针或引用,用非第一个继承基类的指针或引用去调用派生类对象的虚函数时会多做一些准备工作,主要是修正 ecx 存放的值(this 指针),即指针的偏移。

在这里插入图片描述


通过打印虚表的方式去查看虚表:

class Base1
{
public:
	virtual void func1() { cout << "Base1::func1" << endl; }
	virtual void func2() { cout << "Base1::func2" << endl; }

protected:
	int _b1;
};

class Base2
{
public:
	virtual void func1() { cout << "Base2::func1" << endl; }
	virtual void func2() { cout << "Base2::func2" << endl; }

protected:
	int _b2;
};

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

protected:
	int _d;
};

typedef void(*VF_PTR)();

// 依次取虚表中的虚函数指针打印并调用
// 通过调用可以直观地看出存的是哪个虚函数
void PrintVFTable(VF_PTR* table)
{
	for (int i = 0; table[i] != nullptr; ++i)
	{
		printf("vft[%d]:%p -> ", i, table[i]);
		table[i]();
	}
	cout << endl;
}

int main()
{
	Base1 b1;
	PrintVFTable((VF_PTR*)(*(void**)&b1));
	
	Base2 b2;
	PrintVFTable((VF_PTR*)(*(void**)&b2));

	Derive d;
	// 打印Derive从Base1继承的虚表
	PrintVFTable((VF_PTR*)(*(void**)&d));
	// 打印Derive从Base2继承的虚表
	PrintVFTable((VF_PTR*)(*(void**)((char*)&d+sizeof(Base1))));
	
	// 打印Derive从Base2继承的虚表的另一种方式
	// 即利用切片时指针的自动偏移
	// 跟上面的方式本质是一样的
	//Base2* p = &d;
	//PrintVFTable((VF_PTR*)(*(void**)p));

	return 0;
}

运行结果:
在这里插入图片描述

3.菱形继承、菱形虚拟继承中的虚函数表

实际中我们不建议设计出菱形继承及菱形虚拟继承,它们再叠加上多态,就会非常复杂,所以我们就不看它们的虚表了,一般我们也不需要研究清楚,因为实际中很少用。

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

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

相关文章

【Git】IDEA 集成 Git

7、IDEA 集成 Git 7.1、配置 Git 忽略文件 1、Eclipse 特定文件 2、IDEA 特定文件 3、Maven 工程的 target 目录 问题 1:为什么要忽略他们&#xff1f; 答&#xff1a;与项目的实际功能无关&#xff0c;不参与服务器上部署运行。把它们忽略掉能够屏蔽 IDE 工具之间的差异。 …

联合变换相关器摄远物镜光学设计

联合变换相关器摄远物镜光学设计 联合变换相关器工作原理 随着科学技术的飞速发展&#xff0c;光学相关探测器件由最初的匹配滤波器发展到今天的联合变换相关器&#xff0c;联合变换相关器与范得耳-卢格特相关器相比&#xff0c;具有灵活性好、识别精度高等特点&#xff0c;所…

Media基础知识一

1.视频文件是什么&#xff1f; FLV, MKV, MP4是不同的视频后缀名&#xff0c;不同的视频格式就像一个容器。容器里封装的是音视频流。 FFmpeg&#xff1a;一款开源软件&#xff0c;用来处理音视频&#xff0c;对音视频进行编解码。要在Android中使用FFmpeg&#xff0c;需要下载…

vue-element-admin 换肤功能,登录后不同权限不同皮肤,刷新不会失效

一、拉vue-element-admin的代码跑起来 安装依赖时会遇到一些问题&#xff0c;tui-editor装不上&#xff0c;需要按照以下步骤删除它 1、vue-element-admin\package.json 删除‘tui-editor’&#xff1a;‘1.3.3’依赖项。 2、vue-element-admin\src\components 删除MarkdownE…

C++ 三种智能指针及其设计实现unique_ptr、 share_ptr 指针

0、差不多春节啦。。。。。 好久没有写博客&#xff0c;写一写吧。。。。。。 祝大家嗨皮&#xff0c;提前恭喜发财 1、三种智能指针的使用方法 C 有3种指针&#xff1a;share_ptr, unique_ptr, weak_ptr 1.1&#xff09;unique_ptr 指针 std::unique_ptr 是一种独占的智能指…

Sklearn标准化和归一化方法汇总(1):标准化 / 标准差归一化 / Z-Score归一化

Sklearn中与特征缩放有关的五个函数和类&#xff0c;全部位于sklearn.preprocessing包内。作为一个系列文章&#xff0c;我们将逐一讲解Sklearn中提供的标准化和归一化方法&#xff0c;以下是本系列已发布的文章列表&#xff1a; Sklearn标准化和归一化方法汇总(1)&#xff1a…

ESP32 (WIFI)-AP、STA模式(13)

提示&#xff1a;本博客作为学习笔记&#xff0c;有错误的地方希望指正 文章目录一、ESP32 WIFI模式概述二、ESP32 WIFI-AP初始化流程三、WIFI-AP示例四、ESP32 WIFI-STA初始化流程五、WIFI-STA示例一、ESP32 WIFI模式概述 参考资料&#xff1a;ESP IDF编程手册V4.4   WIFI主…

【GD32F427开发板试用】Systick系统定时器的使用

本篇文章来自极术社区与兆易创新组织的GD32F427开发板评测活动&#xff0c;更多开发板试用活动请关注极术社区网站。作者&#xff1a;HonestQiao 基于Arm Cortex-M系列内核的MCU&#xff0c;都包含了SysTick定时器。 所谓SysTick即为系统定时器&#xff0c;又称嘀嗒定时器&am…

Docker Swarm

Swarm 是什么&#xff1f; Docker Swarm 是Docker官方的跨节点的容器编排工具。用户只需要在单一的管理节点上操作&#xff0c;即可管理集群下的所有节点和容器。 主要解决什么问题 1. 解决docker server的集群化管理和部署。 2. Swarm通过对Docker宿主机上添加的标签信息来…

分享60个PHP源码,总有一款适合您

PHP源码 分享60个PHP源码&#xff0c;总有一款适合您 下面是文件的名字&#xff0c;我放了一些图片&#xff0c;文章里不是所有的图主要是放不下...&#xff0c; 60个PHP源码下载链接&#xff1a;https://pan.baidu.com/s/1SvjbzolwuMrQyhVb_byG5Q?pwdx831 提取码&#xff…

生物素点击标记试剂:DBCO-SS-PEG3-biotin,1430408-09-5,生物素PEG3二硫键DBCO

1、理论分析&#xff1a;中文名&#xff1a;生物素-三聚乙二醇-二硫-二苯并环辛炔&#xff0c;生物素-PEG3-二硫-DBCO &#xff0c;生物素PEG3-二硫二苯并环辛炔英文名&#xff1a;DBCO-S-S-PEG3-biotin&#xff0c;Biotin-PEG3-SS-DBCOCAS号&#xff1a;1430408-09-5化学式&am…

如何使用ArcGIS进行点抽稀

01 概述对于制图工作者而言&#xff0c;遇到大量的点要素&#xff08;POI、村名等&#xff09;标注的时候往往非常的麻烦&#xff0c;因为这些点往往都是十分的密集&#xff0c;直接标注很影响制图的美观。如果直接去处理原始的数据&#xff0c;会导致后续的数据更新维护麻烦&a…

pdf合并在线,大家都在用的工具

工作和生活当中都有很多场景需要我们提交PDF文档&#xff0c;而且有时候要求仅能提交一份&#xff0c;如果这时候刚好你的文档分成了几份&#xff0c;就得先合并之后才能提交。要在线合并PDF并不麻烦&#xff0c;关键是用对工具。下面就来给大家介绍几款热门的软件&#xff0c;…

Spring Boot(五十五):基于redis防止接口恶意刷新和暴力请求

下面的教程&#xff0c;通过intercept和redis针对urlip在一定时间内访问的次数来将ip禁用&#xff0c;可以根据自己的需求进行相应的修改&#xff0c;来达到自己的目的 下面只讲解大致步骤&#xff0c;不详细讲解&#xff0c;需要完整代码的可以自行下载。 https://download.c…

数据结构之查找详解

一、什么是查找表&#xff1f; 1.1 定义 查找表是由同一类型的数据元素构成的集合。例如电话号码簿和字典都可以看作是一张查找表。 1.2 查找表的几种操作&#xff1a; 1&#xff09;在查找表中查找某个具体的数据元素&#xff1b; 2&#xff09;在查找表中插入数据元素&am…

win10环境使用nvm安装多版本nodejs并配置环境变量

win10环境使用nvm安装多版本nodejs并配置环境变量nvm安装环境变量配置测试安装全局模块对于旧版本的node&#xff0c;手动安装npm最近使用node工程&#xff0c;需要多版本&#xff0c;并且进行切换&#xff0c;来回安装卸载不同版本的node比较麻烦&#xff0c;后面自己就简单捯…

MySQL 5.5版本的两个执行引擎

目录执行引擎引入MySQL执行引擎生成的文件MyIsamInnoDB聚簇索引与非聚簇索引稀疏索引回表覆盖索引执行引擎引入 我们真正的索引结构要去落地的时候呢&#xff0c;也就是MySQL底层BTree数据结构要去落地的话&#xff0c;那么一定要和我们的存储引擎相结合。接下来我们会说MySQL…

【游戏逆向】老飞飞怀恋魅力爱玩等老飞飞瞬移分析代码

【游戏逆向】老飞飞怀恋魅力爱玩等老飞飞瞬移分析代码 在游戏中&#xff0c;每个人物都有一个坐标。x坐标和y坐标。老飞飞也一样&#xff0c;可能有些朋友用ce找到当前的人物坐标。然后修改坐标就能达到瞬移到效果。不过有些老飞飞是无法实现的。只要瞬移就会掉客户端。今天就…

3>2,看看U.3升级了啥

关注企业级NVMe SSD的小伙伴对U.2接口一定不会感到陌生。然而&#xff0c;在U.2之外&#xff0c;还存在一种名为“U.3”的硬盘接口&#xff0c;二者外观完全相同&#xff0c;接口性能也都一样&#xff0c;甚至不少客户直接将U.3的NVMe SSD部署在U.2服务器上使用。但既然3&#…

分布式应用解决方案之一致性Hash

什么是一致性Hash 一致性Hash就是将整个hash值空间按照顺时针方向形成一个虚拟的环&#xff0c;整个环状结构就称之为Hash环。那为什么叫做一致性Hash环&#xff1f;一致性是由于Hash环应用场景一般在分布式应用服务中&#xff0c;各个服务提供者分布在hash环中&#xff0c;当某…