首页 > 编程语言 >C++对象模型

C++对象模型

时间:2024-09-07 19:26:46浏览次数:10  
标签:函数 对象 子类 模型 C++ 继承 基类 父类

C++对象模型

在C++面向对象的类中,有两种数据成员和三种成员函数:

class Base{
public:
    Base(int i) :baseI(i){};
    int getI(){ return baseI;}
    static void countI(){};
    virtual void print(void){ cout << "Base::print()"; }
    virtual ~Base(){}
    
private:
    int baseI;
    static int baseS;
};

非继承下的对象模型

概述:在此模型下,non-static 数据成员被置于每一个对象中,而 static 数据成员被置于对象之外。static 与 non-static 函数也都放在对象之外,而对于 virtual 函数,则通过虚函数表和虚指针来支持,具体如下:

  • 每个生成一个表格,称为虚函数表或者虚表(virtual table,简称 vt)。虚表中存放着一堆指针,这些指针指向该类的每一个虚函数。虚表中的函数地址将按声明时的顺序排列(虚析构函数永远是第一个),不过当子类有多个重载函数时例外,后面会讨论。
  • 每个对象都拥有一个虚表指针(vtptr),由编译器为其生成。虚表指针的设定与重置皆由类的复制控制(也即是构造函数、析构函数、赋值操作符)来完成。vtptr 的位置为编译器决定,传统上它被放在所有显示声明的成员之后,不过现在许多编译器把 vtptr 放在一个对象的在内存中的最前端。关于数据成员布局的内容,在后面会详细分析。
  • 虚函数表的前面设置了一个指向 type_info 的指针,用以支持 RTTI(Run Time Type Identification,运行时类型识别)。RTTI 是为多态而生成的信息,包括对象继承关系,对象本身的描述等,只有具有虚函数的对象在会生成,因为是放在虚函数表的前面

在此模型下,Base 的对象模型如图:

继承下的对象模型

单继承

在之前的基础上,如果我们定义了派生类:

class Derive : public Base
{
public:
    Derive(int d) :Base(1000),      DeriveI(d){};
    
    //overwrite父类虚函数
    virtual void print(void) { cout << "Drive::Drive_print()" ; }
    
    // Derive声明的新的虚函数
    virtual void Drive_print() { cout << "Drive::Drive_print()" ; }
    
    virtual ~Derive(){}
private:
    int DeriveI;
};

一个派生类如何在机器层面上塑造其父类的实例呢?在 C++ 对象模型中:

  • 对于一般继承(这个一般是相对于虚继承而言),若子类重写(overwrite)了父类的虚函数,则子类虚函数将覆盖虚表中对应的父类虚函数(注意子类与父类拥有各自的一个虚函数表);若子类并没有 overwrite 父类虚函数,而是声明了自己新的虚函数,则该虚函数地址将扩充到虚函数表最后
  • 对于虚继承,若子类 overwrite 父类虚函数,同样地将覆盖从父类继承过来的虚函数表中的对应位置,若子类声明了自己新的虚函数,则编译器将为子类增加一个新的虚表指针 vtptr,这与一般继承不同
  • 子类虚析构函数会覆盖掉虚函数表中的分类的虚析构函数。
  • C++ 继承时,先调用父类构造函数生成一个父类对象,然后调用子类的构造函数生成一个子类的对象,其实这个对象在父类的对象上进行扩充
  • 子类继承了父类全部的成员,包括 private,只是子类没有访问权限。

继承导致重载函数的隐藏

  • 首先要说明的是,重载只能发生在同一个类中,子类和父类之间的同名函数(参数列表),无法构成重载, 子类的同名函数(无论参数列表是否相同),会覆盖所有父类(多继承情况下)的所有同名函数(包括虚函数)
  • 因为这种特性,也就导致了子类的同名函数会隐藏父类的重载函数。如果想用父类的重载函数,可以通过 using Base::foo 来声明继承所有的重载函数,然后重写特定参数列表的函数。如果不需要重写,则也可以通过使用父类作用域来显式地调用父类的重载函数。

多继承

一般的多重继承

单继承中(一般继承),子类会扩展父类的虚函数表。在多继承中,子类含有多个父类的子对象,该往哪个父类的虚函数表扩展呢?当子类 overwrite 了父类的函数,需要覆盖多个父类的虚函数表吗?

  • 子类继承所有父类的虚函数表,因此子类中拥有多个虚函数表指针和虚函数表。
  • 子类的新增的虚函数被放在声明的第一个基类的虚函数表中。
  • 虚函数被overwrite 时,所有基类的同名函数都被子类的同名函数覆盖。保证了父类指针指向子类对象时,总是能够调用到真正的函数。
  • 内存布局中,父类按照其声明顺序排列。
class Base{
public:
    Base(int i) :baseI(i){};
    virtual ~Base(){}
    int getI(){ return baseI; }
    static void countI(){};
    virtual void print(void){ cout << "Base::print()"; }
    
private:
    int baseI;
    static int baseS;
};

class Base_2{
public:
    Base_2(int i) :base2I(i){};
    virtual ~Base_2(){}
    int getI(){ return base2I; }
    static void countI(){};
    virtual void print(void){ cout << "Base_2::print()"; }
private:
    int base2I;
    static int base2S;
};
    
class Drive_multyBase :public Base, public Base_2{
public:
    Drive_multyBase(int d) :Base(1000), Base_2(2000) ,Drive_multyBaseI(d){};
    virtual void print(void){ cout << "Drive_multyBase::print" ; }
    virtual void Drive_print(){ cout << "Drive_multyBase::Drive_print" ; }
private:
    int Drive_multyBaseI;
};

菱形继承

菱形继承也称为钻石型继承或重复继承,它指的是基类被某个派生类简单重复继承了多次。这样,派生类对象中拥有多份基类实例(这会带来一些问题)。

class B{
public:
    int ib;
public:
    B(int i=1) :ib(i){}
    virtual void f() { cout << "B::f()" << endl; }
    virtual void Bf() { cout << "B::Bf()" << endl; }
};
    
class B1 : public B{
public:
    int ib1;
public:
    B1(int i = 100 ) :ib1(i) {}
    virtual void f() { cout << "B1::f()" << endl; }
    virtual void f1() { cout << "B1::f1()" << endl; }
    virtual void Bf1() { cout << "B1::Bf1()" << endl; }
};
    
class B2 : public B{
public:
    int ib2;
public:
    B2(int i = 1000) :ib2(i) {}
    virtual void f() { cout << "B2::f()" << endl; }
    virtual void f2() { cout << "B2::f2()" << endl; }
    virtual void Bf2() { cout << "B2::Bf2()" << endl; }
};
    
class D : public B1, public B2{
public:
    int id;
public:
    D(int i= 10000) :id(i){}
    virtual void f() { cout << "D::f()" << endl; }
    virtual void f1() { cout << "D::f1()" << endl; }
    virtual void f2() { cout << "D::f2()" << endl; }
    virtual void Df() { cout << "D::Df()" << endl; }
};

这时,根据单继承,我们可以分析出 B1,B2 类继承于 B 类时的内存布局。又根据一般多继承,我们可以分析出 D 类的内存布局。我们可以得出 D 类子对象的内存布局如下图:

D 类对象内存布局中,图中青色表示 b1 类子对象实例,黄色表示 b2 类子对象实例,灰色表示 D 类子对象实例。从图中可以看到,由于 D 类间接继承了 B 类两次,导致 D 类对象中含有两个 B 类的数据成员 ib,一个属于来源 B1 类,一个来源 B2 类。这样不仅增大了空间,更重要的是引起了程序歧义:

D d;
d.ib =1 ;               //二义性错误,调用的是B1的ib还是B2的ib?
d.B1::ib = 1;           //正确
d.B2::ib = 1;           //正确

虚继承

虚继承解决了菱形继承中最派生类拥有多个间接父类实例的情况。虚继承的派生类的内存布局与普通继承很多不同,主要体现在:

  • 虚继承的子类,如果本身定义了新的虚函数,则编译器为其生成一个虚函数表指针(vtptr)以及一张虚函数表。该 vtptr 位于对象内存最前面(非虚继承则是直接扩展父类虚函数表)。
  • 虚继承的子类也单独保留了父类的 vtptr 与虚函数表。这部分内容与子类内容以一个四字节的 0 来分界。
    • 这两条规则表明,如果虚继承的子类定义了新的虚函数,而且父类中已经有了虚函数,则子类对象中拥有多个虚表指针
  • 虚继承的子类对象中,含有 4 个字节的虚基类表指针偏移值。

虚基类表

在 C++ 对象模型中,虚继承而来的子类会生成一个隐藏的虚基类指针(vbptr),在 Microsoft Visual C++ 中,虚基类表指针总是在虚函数表指针(vtptr)之后,因而,对某个类实例来说,如果它有虚基类指针,那么虚基类指针可能在实例的 0 字节偏移处(该类没有 vtptr 时,vbptr 就处于类实例内存布局的最前面),也可能在类实例的4字节偏移处(有 vtptr)。

虚基类表的一些特性:

  • 一个类的虚基类指针指向虚基类表
  • 与虚函数表一样,虚基类表也由多个条目组成,条目中存放的是偏移值。第一个条目存放虚基类表指针(vbptr)所在地址到该类内存首地址的偏移值,由第一段的分析我们知道,这个偏移值为 0(类没有 vtptr)或者 -4(类有虚函数,此时有 vtptr)。
  • 虚基类表的第二、第三…个条目依次为该类的最左虚继承父类、次左虚继承父类…的内存地址相对于虚基类表指针的偏移值。

简单虚继承

如果我们的 B1 类虚继承于 B 类:

//类的内容与前面相同
class B{...}
class B1 : virtual public B

根据我们前面对虚继承的派生类的内存布局的分析,B1 类的对象模型应该是这样的:

注意上图,子类对象中有两个虚表指针,分别是子类的虚表指针和父类的虚表指针。如果子类重写父类的虚函数,则将父类的虚表中的虚函数地址替换为子类的虚函数地址

菱形虚继承

class B{...}
class B1: virtual public  B{...}
class B2: virtual public  B{...}
class D : public B1,public B2{...}

菱形虚拟继承下,最终派生类 D 类的对象模型又有不同的构成了。在D类对象的内存构成上,有以下几点:

  • 在 D 类对象内存中,基类出现的顺序是:先是 B1(最左父类),然后是 B2(次左父类),最后是 B(虚祖父类)。
  • D 类对象的数据成员 id 放在 B 类前面,两部分数据依旧以 0 来分隔。
  • 编译器没有为 D 类生成一个它自己的 vtptr,而是覆盖并扩展了最左父类的虚基类表,与简单继承的对象模型相同
  • 超类 B 的内容放到了 D 类对象内存布局的最后。

菱形虚拟继承下的C++对象模型为:

标签:函数,对象,子类,模型,C++,继承,基类,父类
From: https://www.cnblogs.com/sfbslover/p/18402033

相关文章

  • 4-网络安全体系与网络安全模型
    4.1网络安全体系概述1)概念一般而言,网络安全体系是网络安全保障系统的最高层概念抽象,是由各种网络安全单元按照一定的规则组成的,共同实现网络安全的目标。网络安全体系包括法律法规政策文件、安全策略、组织管理、技术措施、标准规范、安全建设与运营、人员队伍、教育培训、产......
  • C++继承基础
    虚函数默认实参C++默认实参是靠编译器实现的,因此默认实参不参与动态绑定,默认实参有静态类型决定。访问控制每个类分别控制自己的成员初始化过程,还分别控制其成员对于派生类来说是否可访问,友元不继承。成员:protected:派生类可见、自己、friend可见public:所有可见private:自己......
  • 【C++算法全真练习题】迷宫问题
    目录题目描述思路AC解答题目描述‌题目描述‌:‌给定一个二维迷宫,‌其中 0 表示可以走的路,‌1 表示障碍物。‌起点坐标为 (0,0),‌终点坐标为 (m-1,n-1),‌其中 m 和 n 分别是迷宫的行数和列数。‌你需要使用广度优先搜索(‌BFS)‌找到从起点到终点的一条路径......
  • VBA之Word应用第三章第一节:文档集合Documents 对象
    《VBA之Word应用》(版权10178982),是我推出第八套教程,教程是专门讲解VBA在Word中的应用,围绕“面向对象编程”讲解,首先让大家认识Word中VBA的对象,以及对象的属性、方法,然后通过实例让大家感受到WordVBA的妙处。这套教程是专门针对WORDVBA的教程,是VBA中的稀缺资源,我给这套教程分归为......
  • C++常见异常汇总(二): undefined reference to
    文章目录1、undefinedreferencetoA2、undefinedreferenceto`vtable2.1模版函数定义方案1:定义与实现均一起定义在头文件中2.2模版函数定义方案2:定义的同一个文件中,显示声明具体类型3、multipledefinitionof1、undefinedreferencetoA检查所有main相......
  • C++复习day06
    一、内存管理1.课件上关于内存分配的题目intglobalVar=1;staticintstaticGlobalVar=1;voidTest(){staticintstaticVar=1;intlocalVar=1;intnum1[10]={1,2,3,4};charchar2[]="abcd";constchar*pChar3="abcd";int*ptr1=(in......
  • windows C++-并行编程-转换使用异常处理的 OpenMP 循环以使用并发运行时
    此示例演示如何将执行异常处理的OpenMP并行for循环转换为使用并发运行时异常处理机制。在OpenMP中,在并行区域中引发的异常必须由同一线程在同一区域中捕获和处理。未处理的异常处理程序会捕获逃离并行区域的异常,默认情况下会终止进程。在并发运行时中,在传递给任务组(例......
  • c++元对象实现
    c++元对象实现在C++中,元对象技术通常指的是运行时检查类型信息和对象信息的能力。C++11标准引入了typetraits和reflection的概念,允许我们在编译时获取和使用类型信息。下面是一个简单的C++类,使用了C++11的typetraits和C++17的std::any来实现元对象:  #include<iostrea......
  • C++ 调用 C# - AOT 方案
    一些C#AOT编译的笔记,整体感觉:简单很方便,但限制也很多,适用于比较单一的功能点。跨语言调用C#代码的新方式-DllExport-InCerry-博客园在.NET8下,直接添加<PublishAot>true</PublishAot>就可以支持了,需要注意一些限制,这里比较相关的是,不能使用Newtonsoft.Json做序列......
  • C++ 调用 C# - DllExport 方案
    3F/DllExport:.NETDllExportwith.NETCoresupport(aka3F/DllExportakaDllExport.bat)目前还不支持.NET6以及后续的版本,看起来作者生活上遇到了一些麻烦,暂时也不打算处理了无法使用.NET6工作·问题#197·3F/DllExport---Unabletogetworkingusing.NET6......