当前位置: 首页 > article >正文

多态(c++)

目录

1.概念

2多态定义与实现

2.1特殊情况

2.2.c++11override+final

2.3. 重载、覆盖(重写)、隐藏(重定义)

 2.4例题

3.抽象类

 4.多态原理

4.1动态绑定和静态绑定 

5.单继承和多继承关系的虚函数表

5.1.单继承

5.2多继承

5.3菱形继承和菱形虚拟继承

6.小结


1.概念

形象的说,多种形态,当完成某个行为的时候,不同的对象去完成时会有不同的状态。

比如vip买物品价格和普通用户买同样物品的价格会不一样。

2多态定义与实现

构成多态有2个条件

1.被调用的函数必须是虚函数,且派生类必须对基类的虚函数进行重写 

2.必须通过基类的指针或引用调用虚函数

#include<iostream>
using namespace std;

class A {
public:
	virtual int name(int a) {
		cout << "111" << endl;
		return 1;
	}
};
class B :public A
{
public:
	virtual int name(int b) {
		cout << "222";
		return 2;
	}
};

void func(A&p) {
	p.name(2);
}
int main() {
	A a;
	B b;
	func(a);
	func(b);
	//111
	//222
	return 0;
}

virtual修饰的是类成员函数是虚函数。

虚函数的重写,就是派生类与基类有一个完全相同的虚函数(返回值类型,函数名字,参数列表(参数名可以不同,但类型和顺序要相同))。

重写虚函数的时候,派生类的虚函数virtual可以不写,基类的虚函数被继承下来后,虚函数的属性也保留,派生类少了virtual的虚函数(伪)也可以跟基类的虚函数构成重写。但这个点很离谱,是为下面的析构函数准备的。一般不建议这么写,不规范。

2.1特殊情况

1.协变,即虚函数重写中,基类和派生类的虚函数返回值类型可以不同,可以是父子类关系的指针或引用

#include<iostream>
using namespace std;
class c {
};
class d:public c{};
class A {
public:
	virtual c* name(int a) {
		cout << "111" << endl;
		return new c;
	}
};
class B :public A
{
public:
	virtual d* name(int b) {
		cout << "222";
		return new d;
	}
};

void func(A&p) {
	p.name(2);
}
int main() {
	A a;
	B b;
	func(a);
	func(b);
	//111
	//222
	return 0;
}

2.析构函数的重写

因为此时父类和子类的析构函数不构成重写,也就不是多态。

delete是调用了类的析构函数,是普通调用,看的是指针或引用或对象的类型,也就是说,此时只会调用A类的析构函数。

这样就可能会造成内存泄漏,因为子类的内容只清理了父类的,但是子类自身独有的没有被清理。

因此我们需要多态调用这里。

析构函数被编译器统一命名为destructor,这就符合的虚函数重写的一个条件,因此

结合前面说的一个不规范写法,这个不规范写法主要是为了析构函数准备的,因为只要父类的析构函数加了virtual,那子类就可以不加,但也可以满足多态调用。

2.2.c++11override+final

注意,如果想让类不能被继承,方法1:可以让父类的构造函数放在私有成员里

这样子类就无法直接构造了

方法2:final修饰的类,无法被继承

class c final{
};

final可以修饰虚函数,让虚函数无法被重写

virtual ~A() final{
		cout << "~A" << endl;
	}

override可以检查是否成功重写,不成功编译器报错

2.3. 重载、覆盖(重写)、隐藏(重定义)

重载:两个函数在同一作用域,函数名和参数相同。

重写:两个函数必须是虚函数,且分别在派生类和基类的作用域里,函数名、参数(参数类型,参数顺序。即参数列表)、返回值必须相同(协变例外)。

重定义:两个函数分别在派生类和基类的作用域里,且函数名相同。注意,基类和派生类的同名函数不构成重写就是重定义。

 2.4例题

 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。首先func构成了重写(上面不规范写法里的内容),其次func()调用时this->func();

this是什么,我一开始认为是B*,实际上是A*,因为成员函数的继承并不是写了一个新的函数在B类里,而是B类也有了A类成员函数的访问权,test(*this)里的this是A*,此时B类是通过切片的方式传递过来。因此此地的this是A*。

根据多态的条件,必须通过基类的指针或引用调用虚函数。此时this->func()满足了多态调用,因为p指针传递过去给test,test的this是A*,p是B*,此时是切片,切片是指向子类的一部分父类成员,也就是说指向的范围还是子类,那么此时func,是以B类身份调用,那根据多态,此时就是调用了B类成员里的func。因此是B->

为什么是1?

这里要明白,重写是实现重写,其实就是说,重写的只是函数体,函数头依旧是沿用父类的

,因此val此时缺省值是1。导致最后答案是B->1

3.抽象类

虚函数后面加上=0,这个函数被称为纯虚函数。包含纯虚函数的类叫抽象类(接口类),不能实例化对象。派生类继承后也不能实例化对象,只有重写纯虚函数,派生类才能实例化对象。注意,纯虚函数规定了派生类必须重写,并且纯虚函数体现了接口继承

\

注意,纯虚函数在父类里不能写函数体(本质就是为了让派生类依据需要必须重写纯虚函数)

抽象类如其名,可以理解一个概念性的类,比如面包就是一个抽象父类,而法棍面包就可以理解为面包这个父类的派生类。抽象类是没有实体的,只有派生类经过重写纯虚函数,才能实例化。

普通函数的继承是实现继承,派生类继承了基类的函数,可以使用函数,继承的是函数的实现(还有接口)。虚函数的继承是一种接口继承,派生类主要继承的是基类虚函数的接口,目的是为了重写,达成多态,当然实现也是有继承的,可以通过指定作用域访问父类的虚函数实现。如果不实现多态,就不要把函数定义成虚函数。

 4.多态原理

class x
{
public:
	virtual void aaaa() {
		cout << 1;
	}
private:
	int y = 1;
};
sizeof x等于多少?
一般来说是4,但是,实际测试出来是8(x86环境,x64是16,我找了半天)

我们会发现多了个vfptr指针,v是virtual,f是function。这个指针叫虚函数表指针,指向的是一块虚函数表(虚函数的地址放在虚函数表里),每个有虚函数的类都至少有一个虚函数表指针,一般放在最前面,有些编译器可能不同。虚函数表本质上就一个函数指针数组。虚函数表末尾默认存个nullptr指针。g++下没放

#include<iostream>
using namespace std;

class x
{
public:
	virtual void aaaa() {
		cout << 1;
	}
	virtual void bbbb() {
		cout << 1;
	}
private:
	int y = 1;
};
class k :public x
{
	virtual void aaaa() {
		cout << 2 << endl;
	}
};
int main() {
	x x1;
	k k1;
	return 0;
}

可以发现,派生类的父类的虚函数表里有2个虚函数地址,但子类重写了其中一个虚函数,子类的虚函数表,第一个虚函数地址变了(这也说明了重写在原理层是覆盖,重写是语法层的概念,实现重写)。注意只是多态调用的时候,函数头是看父类的,函数体才是看是子类还是父类。

重申一遍,有虚函数的类里有虚函数表指针,虚函数表指针指向的是虚函数表,虚函数表里存的是指针(虚函数地址)。而虚函数和普通函数,都是放在代码段的,不在任何栈堆里。

注意,派生类的虚函数表指针就是在派生类的父类成员区域里。

#include<iostream>
using namespace std;

class x
{
public:
	virtual void aaaa() {
		cout << 1 << endl;
	}
	virtual void bbbb() {
		cout << 1;
	}
private:
	int y = 1;
};
class k :public x
{
	virtual void aaaa() {
		cout << 2 << endl;
	}
};
void f(x& x2)
{
	x2.aaaa();
}
int main() {
	x x1;
	k k1;
	f(x1);
	f(k1);
	return 0;
}

关于多态如何实现,我们可以发现父类的指针或引用可能接受到的一个是原生的父类,一个是子类的切片
而在具体调用中,如果是原生的父类,那么会直接去调用原生父类的虚函数表指针,找到虚函数。
而如果是子类,且实现了虚函数重写。因为切片本质是子类的一部分,
且虚函数表会被继承在子类的父类成员范围内,那么编译器通过切片的虚函数表指针
,找到相应的虚函数,而因为重写,所以同样的虚函数表内位置,
地址存的是不同的虚函数。这就实现了多态调用

普通调用是根据调用者的类型确定函数地址。

普通调用是在编译时通过符号表找到函数地址。

多态调用时在运行时通过不同类的虚函数表找到虚函数地址

补充:

派生类的虚函数表生成:先将基类虚函数表复制到派生类,如果派生类有重写基类的某个虚函数,那就替换相应虚函数表里的地址,最后如果派生类自己有新的非重写虚函数,就按在派生类的声明顺序添加到派生类的虚函数表里。

同类型的对象有共同的虚函数表。

4.1动态绑定和静态绑定 

静态绑定(前期绑定、早绑定),是在程序编译期间确定了程序的行为,也称为静态多态,比如函数重载

动态绑定(后期绑定、晚绑定),是在运行时,根据拿到的类型确定程序的具体行为,调用具体的函数,即动态多态。

5.单继承和多继承关系的虚函数表

5.1.单继承

 我们会发现,虽然通过监视窗口,我们也看到了派生类的虚函数表,但是明明派生类自己也有个非重写的虚函数的,但是没有出现

我们找到内存中的虚函数表,可以看到,实际上B类是有2个虚函数的,只是vs的监视窗口有点毛病。

虚函数表在什么位置?

#include<iostream>
using namespace std;
class A {
public:
	virtual void aaa() {
		cout << 1 << endl;
	}
};
class B :public A
{
public:
	virtual void aaa() {
		cout << 2 << endl;
	}
	virtual void bbb() {
		cout << 3333 << endl;
	}
};
int main() {
	B b1;
	A a1;
	B* b2 = &b1;
	A* a2 = &a1;
	
	int a;
	static int b=1;
	int* c = new int;
	const char* d = "dwdawd";
	printf("栈区地址:%p\n", &a);
	printf("静态区地址:%p\n", &b);
	printf("堆区地址:%p\n", c);
	printf("常量区地址:%p\n", d);
	//注意,虚函数表地址怎么打印?
	//首先,类对象不能强转,所以我们要用指针来强转,因为指针的类型
	//只是决定了其解引用的范围。
	printf("A虚函数表地址:%p\n", *(int*)a2);
	printf("B虚函数表地址:%p\n", *(int*)b2);
//注意,x86环境是强转成int*,x64要long long*
	return 0;
}

我们可以看到,是接近于常量区的(window环境,linux比较新的,也是在常量区)。

虚函数表是在编译时产生的,对象里的虚函数表指针,则是在构造函数的初始化列表,第一个初始化的(赋予虚函数表的地址)

如何打印虚函数表

#include<iostream>
using namespace std;
class A {
public:
	virtual void aaa() {
		cout << 1 << endl;
	}
};
class B :public A
{
public:
	virtual void aaa() {
		cout << 2 << endl;
	}
	virtual void bbb() {
		cout << 3 << endl;
	}
};
typedef void(*VFPTR)();
//重命名函数指针

void PrintVFPTR(VFPTR* vfr)
{
	//vs下可以用nullptr判断,linux要灵活用别的
	//比如直接传几个虚函数
	//另外,我这边虚函数类型都是固定void函数,方便打印
	for (int i = 0; vfr[i] != nullptr; i++)
	{
                                            //vfr[i]
		printf("[第%d个虚函数]:%p\n", i + 1, *(vfr+i));
		VFPTR f = *(vfr + i);//vfr[i]
		f();
//(*f)()也可以
	}
}
int main() {
	A a1;
	B b1;
	cout << "A虚函数表:" << endl;
	//注意,最外面还要强转成函数指针,不强转还是int*类型,传参传不过去
	PrintVFPTR((VFPTR*)(*((int*)&a1)));
	cout << "B虚函数表:" << endl;
	PrintVFPTR((VFPTR*)(*((int*)&b1)));

	return 0;
}

5.2多继承

#include<iostream>
using namespace std;
class A {
public:
	virtual void aaa() {
		cout << 1 << endl;
	}
	virtual void aaaa() { cout << 11 << endl; };
	int _a = 1;
};
class B
{
public:
	virtual void aaa() {
		cout << 2 << endl;
	}
	virtual void aaaa() {
		cout << 22 << endl;
	}
	int _b = 1;
};
class C :public A, public B
{
public:
	virtual void aaa() {
		cout << 3 << endl;
	};
	virtual void bbbb() { cout << 33 << endl; };
	int _c = 1;
};

int main() {
	cout << sizeof C << endl;
	return 0;
}

注意,多继承下的虚函数表,是都会被继承下来的。


int main() {
	cout << sizeof C << endl;
	C c1;
	A* a1 = &c1;
	B* b1 = &c1;
	
	return 0;
}
为了节省篇幅,同样的部分就不复制进来了

可以看到,b1和a1存的地址是不同的,两者的差距,正好是一个对象的大小,即8byte

#include<iostream>
using namespace std;
class A {
public:
	virtual void aaa() {
		cout << 1 << endl;
	}
	virtual void aaaa() { cout << 11 << endl; };
	int _a = 1;
};
class B
{
public:
	virtual void aaa() {
		cout << 2 << endl;
	}
	virtual void aaaa() {
		cout << 22 << endl;
	}
	int _b = 1;
};
class C :public A, public B
{
public:
	virtual void aaa() {
		cout << 3 << endl;
	};
	virtual void bbbb() { cout << 33 << endl; };
	int _c = 1;
};
typedef void(*VFPTR)();
//重命名函数指针

void PrintVFPTR(VFPTR* vfr)
{
	//vs下可以用nullptr判断,linux要灵活用别的
	//比如直接传几个虚函数
	//另外,我这边虚函数类型都是固定void函数,方便打印
	for (int i = 0; vfr[i] != nullptr; i++)
	{
                                            //vfr[i]
		printf("[第%d个虚函数]:%p\n", i + 1, *(vfr+i));
		VFPTR f = *(vfr + i);//vfr[i]
		f();
//(*f)()也可以
	}
}
int main() {
	C c1;
	A* a1 = &c1;
	B* b1 = &c1;
	cout << "C类中的A类虚函数表:\n";
	PrintVFPTR((VFPTR*)*((int*)a1));
	cout << "C类中的B类虚函数表:\n";
	PrintVFPTR((VFPTR*)*((int*)b1));
	return 0;
}

由程序运行结果可以见到,C类的非重写虚函数是被添加到了A类的虚函数表上,而我试过改变C类继承的顺序,可以得出,这个添加的虚函数表是根据继承顺序觉得的。

class C:public A,public B是加到A类虚函数表上,class C:public B,public A是添加到B类虚函数表上。

我们会发现,明明是同一个函数,但是在虚函数表中的地址不同,因为D对象中A对象成员正好在前面,a1存的地址,即是整个D对象的开头地址,也是其中A类对象开头地址,因为aaa函数是派生类重写的,且切片本身还是指向D对象,所以调用aaa函数也是以D对象身份来调用,a1存的函数地址,是仅有的一个中间层的地址(通过汇编可以看到,D类call的是一个jump指令的地址,jump指令中再跳到函数地址),但是b1不同,b1虽然也是D对象的切片,但是,指向的地址不是D对象起始地址,而是后面一些,这样多态调用,就不能以D类对象调用了,所以在汇编视角,call了一次jump,但这个jump是跳到一个sub指令,正好减到了a1对象的jump,再调用这个jump,再跳到函数中。

5.3菱形继承和菱形虚拟继承

这部分不多描述,可以去看下专门的关于菱形继承和多态的文章。

class A {
public:
	virtual void aaa() {
		cout << 1 << endl;
	}
	int _a = 1;
};
class B:public A
{
public:
	
	int _b = 2;
};
class C :public A
{
public:
	int _c = 3;
};
class D :public B, public C
{
public:
	int _d = 4;
};

就存储模型来说,菱形继承和多继承的模型类似。

而对于菱形虚拟继承,有重写冲突的问题。

菱形虚拟继承之后,A类的内容只会保留一份,而B类和C类如果有重写A类的虚函数,那么这时候,覆盖的虚函数表是同一个,那么就会发生争议,编译器不知道到底覆盖谁的。

解决方法是直接在D类上重写即可。

如果出现下列情况:BC继承A,D继承BC。A类有虚函数,B、C有非重写的虚函数,D类有重写A类虚函数。那么D会变得更大。除了D类的虚函数表指针会依旧放在公共位置(里面存着重写了A类虚函数的新的虚函数地址),B类和C类都会有一个单独的虚函数表指针指向各自单独的虚函数表,跟公共位置的D类虚函数表指针指向的虚函数表不一样。

6.小结

1. inline函数可以是虚函数吗,可以。编译器在普通调用的时候,会遵循inline的属性,直接展开。但是如果是多态调用,会忽视inline属性,该虚函数会有地址,并且地址存在虚函数表中,走多态调用。

2.什么是多态。参考上文,静态多态和动态多态。

3.重载、重写、重定义。参考上文。

4.多态实现原因。参考上文

5.静态成员可以是虚函数吗?不能。因为静态成员函数没有this指针,而多态的条件之一就是由基类的指针或引用调用虚函数(而静态成员函数没有this指针意味着,使用类型::成员函数的方式调用静态成员函数无法满足多态的条件。),因此静态成员函数无法构成多态。

6.构造函数可以是虚函数吗?不能。因为虚函数表指针是在构造函数的初始化列表中才初始化的,而虚函数又必须在虚函数表中,由编译器在运行时通过虚函数表指针找到虚函数使用,初始化虚函数表指针又是要通过构造函数,这就死循环了。因此不能。

7.析构函数可以是虚函数吗?可以,建议基类析构函数都定义成虚函数。其他参考上文。

8.对象访问普通函数快还是虚函数快?如果虚函数是普通调用,则一样快。如果是多态调用,则普通函数快。因为多态调用,需要编译器在运行的时候通过虚函数表指针访问虚函数表,再找到虚函数地址,再调用虚函数,而普通函数,在编译的时候,已经通过类成员函数表,直接找到普通函数地址,运行的时候再调用普通函数。

9。虚函数表在什么阶段生成,存在哪?虚函数表在编译阶段就生成了,一般存在代码段(常量区)

10.C++菱形继承的问题?虚继承的原理。参考本文和继承文章。

11.什么是抽象类?抽象类的作用?参考上文。抽象类强制重写了虚函数,另外抽象类体现出了接口继承关系


http://www.kler.cn/news/302558.html

相关文章:

  • 怎样还原空白试卷?2024教你快速还原空白试卷的软件
  • Python 最小公倍数计算器:从基础到应用
  • 鸿蒙-沉浸式pc端失效
  • 深入理解全连接层:从线性代数到 PyTorch 中的 nn.Linear 和 nn.Parameter
  • Unity Shader实现简单的各向异性渲染(采用各向异性形式的GGX分布)
  • 优化销售流程:免费体验企元数智小程序合规分销系统!
  • Idea 2021.3 破解 window
  • vue3常见的bug 修复bug
  • 力扣每日一题:1372.二叉树中的最长交错路径
  • 腾讯云2024年数字生态大会开发者嘉年华(数据库动手实验)TDSQL-C初体验
  • 62. 不同路径
  • 户用光伏业务市场开发的步骤
  • 走进低代码报表开发(二):高效报表设计新利器
  • 基于SpringMVC的API灰度方案
  • SuperMap GIS基础产品FAQ集锦(20240911)
  • 使用AI大模型进行企业数据分析与决策支持
  • Redis 的标准使用规范之数据类型使用规范
  • MySQL总结(上)
  • 决策树(Decison Tree)—有监督学习方法、概率模型、生成模型、非线性模型、非参数化模型、批量学习
  • 如何测试你购买的IP的丢包率是否正常
  • 市场上便宜好用的量化交易软件-QMT!QMT系统函数之handlebar - 行情事件函数
  • Matlab simulink建模与仿真 第十一章(端口及子系统库)【下】
  • 力扣337-打家劫舍 III(Java详细题解)
  • mac安装swoole过程
  • 大模型的第一个杀手级应用场景出来了
  • SQL的优化和引擎有哪些
  • Win11 频繁蓝屏重启
  • GIS应届生不考研,不考公,不考编,未来要怎么安排?
  • 【规范】Git Commit 约定式提交规范
  • MySQL表操作