首页 > 编程语言 >C++类和对象_多态

C++类和对象_多态

时间:2023-08-08 22:40:03浏览次数:37  
标签:函数 对象 子类 多态 C++ 父类 重写

虚函数

virtual修饰的成员函数被称为虚函数,虚函数的地址会被纳入类的虚函数表(virtual function table)。inline 和 virtual不会同时生效,用virtual修饰内联函数时,编译器会忽视函数的内联属性,此时函数不再是内联。虚函数一定不是内联函数。

虚函数的重写

子类继承父类,并有一个与父类形式相同(函数名、参数、返回值)的虚函数,称子类重写了父类的虚函数。虚函数被重写后,产生一个新的函数地址,子类中被重写的虚函数与父类的虚函数地址不同。虚函数的重写本质上重写的是函数体的内容,函数形式完全相同。

虚函数重写的条件是子类与父类中的虚函数的函数名、参数、返回值相同,但有两个例外情况:协变和虚拟析构函数。

  • 协变:子类和父类虚函数的返回值可以不同,但是必须为父子类关系的指针或者引用。
  • 虚拟析构函数:用virtual修饰析构函数,构成虚函数重写,这是因为在C++中,析构函数在编译后会被统一处理成destructor(),符合重写的基本条件。将析构函数统一处理并使其支持多态,可以避免通过指向子类的父类指针调用析构时的不完全析构问题,从而避免内存泄漏。在实际中,应尽量对析构函数使用virtual修饰。

在C++11中,使用final关键字修饰虚函数,不允许虚函数被重写;使用override关键字修饰虚函数,可以帮助子类检查虚函数是否完成重写。

C++类和对象_多态_虚函数表

虚函数表

所有虚函数都会被纳入类对象的虚函数表,在存在虚函数的类中,对象会有一个虚函数表指针指向这个表。虚函数表存储的是虚函数的地址,子类重写父类的虚函数后,会将新的虚函数地址纳入子类的虚函数表。因为静态成员函数没有this指针,不能拿到虚函数表,所以静态成员函数不能被virtual修饰

虚函数重写的本质其实是子类重写了父类虚函数的实现,而使用父类虚函数的函数名、参数、返回值。可以认为重写是一种接口继承,重写 = 拷贝(父类的虚函数表) + 覆盖(父类的虚函数地址) + 追加(自己的虚函数地址)。

虚函数表的存在,可以使编译器忽略函数调用者(的指针或引用)的具体类型,而直接访问虚函数表拿到函数地址并调用对应函数。下文会看到,虚函数表的存在是多态实现的关键支持因素。

虚函数表是类层面的,同类对象共用虚函数表,虚函数表存储在常量区中,这是因为虚函数表不允许被修改。

C++多态

两种多态

当进行某种行为时,不同的对象会产生不同的不同的结果,这便是多态(polymorphism)。例如针对“买票”的行为,不同的对象(人群)票价不尽相同。

在C++种,大体上有两种类型的多态:静态多态和动态多态。静态多态又称编译时多态,这种多态在编译时就确定了具体的行为,例如函数重载,在编译时确定函数地址。动态多态又称运行时多态,这种多态的具体行为无法在编译时确定,而是需要在运行时进行动态绑定,从虚函数表中寻找函数地址并执行。多态调用看的是被引用/被指向的对象类型,而普通调用看的是调用者的类型。下文讨论的即是动态多态及其原理。

多态的两个条件

C++多态有两个条件:在继承体系中,调用的函数必须是被重写的虚函数;调用者必须是父类的指针或者引用。

反观虚函数表

了解多态的行为后,这里探究多态的底层实现原理。首先观察当进行虚函数重写后,子类的虚函数表发生了什么变化:

class A
{
public:
	virtual void func()
	{
		cout << "A::func()" << endl;
	}

	virtual void func_2()
	{
		cout << "A::func_2()" << endl;
	}
};

class B : public A
{
public:
	virtual void func()
	{
		cout << "B::func()" << endl;
	}
};

C++类和对象_多态_虚函数_02

正如上文所说的,当虚函数被重写后,子类会用新的虚函数地址在虚函数表中进行覆盖,而未被重写的虚函数依然保持与原来的地址相同。至此,已经可以大概认识到多态的实现机制:如果调用的是虚函数,则不管调用者是谁,而是在对象的虚函数表中查找虚函数的地址,通过地址调用对应的函数。

通过观察多态调用的汇编代码(为了便于观察,下文用32位机器模拟),可以证明事实确实如此:

void function(A* p)
{
	p->func();
}

int main()
{
	A a;
	B b;
	function(&a);
	function(&b);
	return 0;
}

C++类和对象_多态_重写_03

了解虚函数表和汇编后,可以梳理出多态调用的原理如下:

C++类和对象_多态_虚函数_04

至此可以解释多态的诸多条件:

  • 调用的函数为什么必须是重写的虚函数?进行虚函数重写后,子类的会将重写的虚函数地址进行覆盖,以与父类的虚函数地址进行区分,进而进行区别调用。
  • 为什么不能通过子类的指针或引用调用虚函数?这是因为父类指针即可以维护父类对象,也可以维护子类对象,对于父类对象,直接通过虚函数表指针拿去虚函数表即可,对于子类对象,父类指针或引用也可以方便地访问父类部分,拿取虚函数表指针。子类指针或引用则不具备这种能力。
  • 为什么不能通过父类对象调用而必须通过指针或引用?这是因为当子类对象赋值给父类对象时,虚函数表不进行拷贝,父类对象接受的虚函数表依然是父类的虚函数表。对象赋值时虚函数表不拷贝的目的是为了避免父类的虚函数表被污染,而通过指针或引用接受对象则不会发生拷贝,可以直接维护传来的对象的虚函数表。

多继承中的多态

在多继承中,子类会有多个虚函数表,每个父类部分含一个虚函数表。子类独有的虚函数追加到第一个父类的虚函数表中。

C++类和对象_多态_虚函数表_05

由于在实际中应尽量避免进行菱形继承,所以菱形继承的对象模型在这里不予讨论。

纯虚函数和抽象类

在虚函数后面加上= 0,这个函数即为纯虚函数。包含纯虚函数的类叫做抽象类。抽象类不能实例化出对象,只有在继承抽象类,并且重写抽象类的纯虚函数后,才能用子类实例化出对象。

class train
{
public:
	virtual void ride() = 0;
};

class bullet_train : public train
{
public:
	virtual void ride()
	{
		cout << "cheap" << endl;
	}
};

class high_speed_train : public train
{
public:
	virtual void ride()
	{
		cout << "fast" << endl;
	}
};

void test()
{
	train* p_bullet_train = new bullet_train();
	p_bullet_train->ride();
	train* p_high_speed_train = new high_speed_train();
	p_high_speed_train->ride();
}

抽象类又称接口类,往往对行为进行定义,抽象类体现出了接口继承关系。抽象类强制了对虚函数的重写。

标签:函数,对象,子类,多态,C++,父类,重写
From: https://blog.51cto.com/158SHI/7012955

相关文章

  • 可迭代对象,迭代器对象,for循环本质
    可迭代对象#可迭代对象#数据对象有__iter__方法的都称为可迭代对象1.内置方法通过加点的方式可以调用的方法2.__iter__读作:双下iter对象3.不可迭代对象:int,float4.可迭代对象:str,list,dict,tuple,set,f.__iter__文件对象5.可迭代的含义"""迭代:每一次更新......
  • C++ Primer Plus 第6版 读书笔记(8)第 8章 函数探幽
    第8章函数探幽本章内容包括:内联函数。引用变量。如何按引用传递函数参数。默认参数。函数重载。函数模板。函数模板具体化。通过第7章,您了解到很多有关C++函数的知识,但需要学习的知识还很多。C++还提供许多新的函数特性,使之有别于C语言。新特性包括内联函数、......
  • 事件对象
    事件对象介绍事件对象:当事件发生的时候,浏览器会创建一个事件对象,这个对象包含了当前事件发生时的所有信息事件对象是一个全局对象,在事件发生时,浏览器会创建一个事件对象,并把它作为实参传递给事件处理函数,事件处理函数通过事件对象,可以获取到事件发生时的相关信息,如鼠标位置......
  • 【C++第三方库】Windows下编译和使用websocketpp
    应用场景:使用C++开发一个支持websocket协议的服务进程,可与HTML5(浏览器js文件)通信。来实现替换基于firebreath框架的跨浏览器插件开发。当前,讲述websocketpp开源库的应用。目的是为了实现C++进程,支持websocket协议。但WebSocketpp是一个开源库,依赖于Boost和OpenSSL资源准......
  • c++ std::hash<std::string> 字符串哈希函数
    msvc采用了FNV-1a的哈希算法//众所周知std::string就是一个basic_string<char>template<class_Elem,class_Traits,class_Alloc>structhash<basic_string<_Elem,_Traits,_Alloc>>{_CXX17_DEPRECATE_ADAPTOR_TYPEDEFStypedefbasic_string<_......
  • C++STL 学习笔记
    C++STL学习笔记STL补充List链表list<int>mylist={}链表定义和初始化voidpush_front(constT& val)将val插入链表最前面voidpop_front()删除链表最前面的元素list.push_back() 增加一个新的元素在list的尾端list.pop_back() 删除list的......
  • 《VTK图形图像开发进阶》第3章VTK基本数据结构——数据对象和数据集
    3.1可视化数据的基本特点离散性数据具有规则或不规则的结构(结构化与非结构化)数据具有维度3.2数据对象和数据集vtkDataObject下图为vtkDataObject类的继承图vtkDataSetVTK里与数据集对应的类是vtkDataSet,该类从vtkDataObject直接派生。vtkDataSet由两个部分组成,即组织......
  • history对象
      ......
  • 使用ceph的对象存储
    Ceph对象存储Ceph对象存储使用Ceph对象网关守护进程(radosgw),它是个与Ceph存储集群交互的FastCGI模块。因为它提供了与OpenStackSwift和AmazonS3兼容的接口,RADOS要有它自己的用户管理。Ceph对象网关可与CephFS客户端或Ceph块设备客户端共用一个存储集群。S......
  • location对象常见属性
       ......