C++ | 12天学好C++ (Day 8)->结构图可视化、代码加通俗理解

国庆节快乐!为每天进步一点点而努力。

C++是计算机视觉的重要的一部分,虽然在初始学习时Python应用比较广,但是大多数公司还是C++做计算机视觉类的比较多,因为C++可加密,所以我们来一起探索吧!看了这系列博客,将会学到C++的基本操作!(如果不敲代码,可能会一知半解)

 第八天 - 221006

目录

chapter 9 虚拟函数(virtual function)和多态性

9.1.虚拟函数

虚拟函数规则:

C++ 中的虚拟函数示例:

纯虚拟函数:

纯虚函数可以定义为:

9.2.多态性

参考文献


chapter 9 虚拟函数(virtual function)和多态性

9.1.虚拟函数

c++作为面向对象的语言,主要有三大特性:继承、封装、多态。关于多态,简而言之就是用父类型的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时绑定,要么试图做到运行时绑定。因此C++的多态分为静态多态(编译时多态)和动态多态(运行时多态)两大类。静态多态通过重载、模板来实现;动态多态就是通过本文的主角虚拟函数来体现的。

C++ 中的虚拟函数是在派生类中重新定义的基类成员函数。virtual 关键字用于声明它。它告诉编译器该函数应该是动态链接的还是后期绑定的。需要使用单个指针来引用各种类的所有对象。结果生成了一个指向引用所有派生对象的基类的指针。但是,当派生类对象的地址包含在基类指针中时,总是执行基类函数。克服这个问题的唯一方法是使用 'virtual 函数。

- 'virtual' 关键字出现在函数的通常声明之前。

- 当一个函数变为虚函数时,C++ 使用基类指针引用的对象类型来确定应该在运行时调用哪个函数。

 9.1.1.虚拟函数的内存分布

虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数。因此有必要知道虚函数在内存中的分布。

class A {
  public:
    virtual void v_a(){}
    virtual ~A(){}
    int64_t _m_a;
};

int main(){
    A* a = new A();
    return 0;
}

 如以上代码所示,在C++中定义一个对象 A,那么在内存中的分布大概是如下图这个样子。

  • 首先在主函数的栈帧上有一个 A 类型的指针指向堆里面分配好的对象 A 实例。
  • 对象 A 实例的头部是一个 vtable 指针,紧接着是 A 对象按照声明顺序排列的成员变量。(当我们创建一个对象时,便可以通过实例对象的地址,得到该实例的虚函数表,从而获取其函数指针。)
  • vtable 指针指向的是代码段中的 A 类型的虚函数表中的第一个虚函数起始地址
  • 虚函数表的结构其实是有一个头部的,叫做 vtable_prefix ,紧接着是按照声明顺序排列的虚函数。
  • 注意到这里有两个虚析构函数,因为对象有两种构造方式,栈构造和堆构造,所以对应的,对象会有两种析构方式,其中堆上对象的析构和栈上对象的析构不同之处在于,栈内存的析构不需要执行 delete 函数,会自动被回收。
  • typeinfo 存储着 A 的类基础信息,包括父类与类名称,C++关键字 typeid 返回的就是这个对象。
  • typeinfo 也是一个类,对于没有父类的 A 来说,当前 tinfo 是 class_type_info 类型的,从虚函数指针指向的vtable 起始位置可以看出。

 C++ | 12天学好C++ (Day 8)->结构图可视化、代码加通俗理解_第1张图片

 C++ | 12天学好C++ (Day 8)->结构图可视化、代码加通俗理解_第2张图片

  • 紫色线框中的内容仅限于虚拟继承的情形(若无虚拟继承,则无此内容)
  • “offset to top”是指到对象起始地址的偏移值,只有多重继承的情形才有可能不为0,单继承或无继承的情形都为0。
  • “RTTI information”是一个对象指针,它用于唯一地标识该类型。
  • “virtual function pointers”也就是我们之前理解的虚函数表,其中存放着虚函数指针列表

 

虚拟函数规则:

  • 虚拟函数必须属于某个类。
  • 静态成员不能是虚拟函数。
  • 对象指针用于访问它们。
  • 他们可能是另一个班级的朋友。
  • 即使不使用,也必须在基类中指定虚拟函数。
  • 所有派生类的原型和基类的虚拟函数必须相同。C++ 会将两个同名但原型不同的函数视为重载函数。
  • 虚拟构造函数是不可能的,但虚拟析构函数是可能的

 示例1:

#include
using namespace std;
class Base {
	public:
		void f() { cout << "Base::f()called" << endl; }

};
class Derived :public Base {
public:
	void f() { cout << "Derived::f() called" << endl; }

};
void main() {
	Derived d, *pDer;
	pDer = &d;
	pDer->f();

	Base* pBase;
	pBase = pDer;
	pBase->f();
}

 结果:

示例2

#include   
using namespace std;  
class A  
{  
   int x=5;  
    public:  
    void display()  
    {  
        std::cout << "Value of x is : " << x<display();  
    return 0;  
}  

 结果:

C++ | 12天学好C++ (Day 8)->结构图可视化、代码加通俗理解_第3张图片

 - *a  是上例中的基类指针。指针只能访问基类的成员,不能访问派生类的成员。

- 尽管 C++ 允许基指针指向从基类派生的任何对象,但它不能直接访问派生类的成员。因此,需要一个虚函数来允许基指针访问派生类的成员。

C++ 中的虚拟函数示例:

#include     
using namespace std;
class A
{    
 public:    
 virtual void display()    
 {    
  cout << "Base class is invoked"<display();   //Late Binding occurs    
}   

结果:

C++ | 12天学好C++ (Day 8)->结构图可视化、代码加通俗理解_第4张图片

  虚拟函数,它用于在程序中调用派生类。

纯虚拟函数:

  • 虚拟函数不用于完成任何事情。它只是用作占位符。
  • 术语“无操作”函数是指没有定义的函数。
  • 纯虚拟函数是什么都不做的函数。纯虚拟函数是在基类中声明但在基类中没有定义的函数。
  • 抽象基类是包含纯虚拟函数但不能用于声明它们自己的对象的类。
  • 基类的主要目标是为派生类提供特征并建立允许运行时多态性的基指针。 

纯虚函数可以定义为:

virtual void display() = 0;

给虚函数加上=0并没有赋值,它只是表明虚函数是一个纯函数。

示例

#include   
using namespace std;
class Base
{
public:
    virtual void show() = 0;
};
class Derived : public Base
{
public:
    void show()
    {
        std::cout << "Derived class is derived from the base class." << std::endl;
    }
};
int main()
{
    Base* bptr;
    //Base b;  
    Derived d;
    bptr = &d;
    bptr->show();
    return 0;
}

 结果:

C++ | 12天学好C++ (Day 8)->结构图可视化、代码加通俗理解_第5张图片

 - 纯虚函数包含在前面示例中的基类中。因此,基类被称为抽象基类。无法创建基类的对象。

9.2.多态性

C++中有两种类型的多态:

  • 编译时多态性
  • 运行时多态性

 

 

 9.2.1.编译时多态(Static Polymorphism)

当一个 C++ 程序被执行时,它会分别从 main() 的顶部开始执行。如果遇到函数调用,则执行点跳转到被调用函数的开头。一旦程序被编译,编译器就会将 C++ 程序中的每条语句转换成一个或多个行的机器语言。机器语言的每一行都有自己唯一的顺序地址。这对于函数没有什么不同,它被转换成机器语言并给出下一个可用地址,因此每个函数最终都有一个唯一的地址。

绑定是指将标识符转换为地址的过程。对于每个变量和函数,此绑定已完成。绑定在编译时或运行时完成。

早期绑定也称为编译时多态性。顾名思义,编译器(或链接器)可以直接将地址与函数调用相关联。它用机器语言指令代替函数调用,该指令告诉 CPU 跳转到函数的地址。默认情况下,早期绑定发生在 C++ 中。

编译时多态也称为静态多态,由函数重载和运算符重载组成。

功能重载

函数重载是指两个或多个函数可以具有相同的名称,但参数不同。只有通过这些差异,编译器才能区分这两个重载函数。

C++中函数重载的优点

  • 功能重载节省了内存空间、一致性和可读性。
  • 代码维护很容易。
  • 它提供了基于参数类型加载类的方法。
  • 函数重载加速了程序的执行。
  • 它显示了多态的行为,允许我们获得不同的行为,即使会有一些链接使用同名的函数。

运算符重载

C++ 为运算符提供了一种数据类型的特殊含义,这种能力称为运算符重载,可以通过三种方法来完成,它们是“重载一元运算符”、“重载二元运算符”、“使用朋友重载二元运算符”功能'。

重载运算符是具有特殊名称的函数:关键字 'operator' 后跟正在定义的运算符的符号。与任何其他函数一样,重载运算符具有返回类型和参数列表。

9.2.2.动态多态(Run Time Polymorphism)

 当在运行时而不是编译时调用对象的方法时,将执行运行时多态性。它是通过方法覆盖实现的,也称为动态绑定或后期绑定。为了实现这一点,我们使用了基于指针的虚函数的概念。与编译时多态性相比,它的执行速度很慢,但操作起来更加灵活,因为所有东西都在运行时执行。

虚函数

虚函数允许我们创建一个基类指针列表并调用任何派生类的方法,甚至无需知道派生类对象的种类。它是使用 virtual 关键字声明的。当函数变为虚函数时,C++ 根据基类指针指向的对象类型确定在运行时调用哪个函数。

虚函数不能是静态成员,并且必须在基类中定义,即使它没有被使用。我们不能有一个虚拟构造函数,但我们可以有一个虚拟析构函数。虚函数不用于执行任何任务,它仅用作占位符。

编译器如何执行运行时解析?(如9.1所示)

编译器为此目的维护了两件事:

  1. vtable:函数指针表,按类维护。(vtable:虚拟表)
  2. vptr:指向 vtable 的指针,每个对象实例维护。(vptr:虚拟表指针)

编译器在两个地方添加了额外的代码来维护和使用 vptr。

  1. 每个构造函数中的代码。此代码设置正在创建的对象的 vptr。此代码将 vptr 设置为指向该类的 vtable。
  2. 每当进行多态调用时,编译器都会插入代码以首先使用基类指针或引用查找 vptr。一旦获取到 vptr,就可以访问子类的 vtable。

参考文献

【1】Virtual Function In C++ - LingarajTechHub

【2】C++虚函数的实现基本原理 - JackTang's Blog  

【3】https://medium.com/@derya.cortuk/polymorphism-in-c-5a7b188fa94f

【4】 C++ Polymorphism with Example

你可能感兴趣的:(-,编码理解,-,-,C++,-,c++,开发语言)