• C++对象模型学习笔记之关于对象


    C++对象在内存中如何存储?

    把这个问题称为C++对象模型(C++ Object Model)。下面对C++对象模型,进行说明:

    要存储的内容

    C++对象包括数据成员和函数成员。其中,
    数据成员分为:static data members(静态数据成员),nonstatic data members(非静态数据成员);
    成员函数分为:static function members(静态函数成员),nonstatic functions members(非静态函数成员),virtual functions(虚函数);
    以class Point为例,

    class Point {
    public:
    	Point(float xval);
    	virtual ~Point();
    	
    	float x() const;
    	static int PointCount();
    	
    protected:
    	virtual ostream& print(ostream &os) const;
    	float _x;
    	static int _point_count;
    };
    

    class Point在机器中如何存储data members和function members?

    如何存储?

    在C++对象模型中,nonstatic data members被配置于每个class object之内,static data members则存放在所有class object之外。static 和 nonstaic function members也存放在所有class object之外。virtual functions则以2个步骤支持:

    1. 每个class参数一堆指向virtual functions的指针,放在表格中。该表格称为virtual table(vtbl);
    2. 每个class object被添加了一个指针,指向virtual table。通常该指针称为vptr。vptr的设置和重置都由每个class的constructor/destructor(构造器、销毁器)和copy assignment(拷贝、赋值)运算符自动完成。每个class所关联的type_info object(用来支持runtime type identification, RTTI),也经由virtual table被指出,放在表格第一个slot处;

    Point object的存储模型见下:
    image

    可以看到,
    1)static data member放到了class object之外,nonstatic data放到了class object之内;
    2)所有的static/nonstatic function members都放到了class object之外;
    3)virtual functions都放到了vtbl之中,而vtbl通过class object中插入一个编译器生成的指针vptr(图中__vptr_Point)指向;
    4)每个vtbl都包含一个type_info object指针,存放在vtbl[0],指向type_info object(描述对象类型信息);

    模型优点:
    空间和存取时间效率高;

    模型缺点:
    如果应用程序代码本身未改变,但用到的class objects的nonstatic data members修改了,那些应用程序代码同样得重新编译。因为nonstatic data members是直接存储在class object中,而class object被应用程序所包含,也不是通过指针指向。

    加上继承,如何存储?

    • 单一继承
    class Library_materiasl {...};
    class Book : public Library_materiasl {...};
    class Rental_book : public Book {...};
    
    • 多重继承
    // 早期iostream实现
    class iostream : public istream, public ostream {...};
    class istream : virtual public ios {...};
    class ostream : virtual public ios {...};
    

    早期iostream及其基类关系如下图:
    image

    • 继承模型
      在存在继承关系时,C++的继承模型又是什么样呢?
      这里需要分为两种情况:普通继承,虚继承。

    • 普通继承
      在普继承情况下(非虚继承),base class subobject的data members被直接存放到derived class object中。
      优点:提供对base class members最紧凑而有效的存取;
      缺点:base class members的任何改变(增加、移除,或改变类型等),将使得所用到“此base class或其derived class的objects”者都必须重新编译。

    • 虚拟继承
      在虚拟继承情况下,base classes不管在串链(chain)中被派生(derived)多少次,永远只会有一个实体(subobject)。如上面的iostream 只有一个virtual ios base classes的一个实体(继承而来的ios的成员只会有一份),而不会因为多重继承而出现多个virtual ios base classes的实体。

    • 虚拟继承下,derived class如何模塑(model)其base class的实体(subobject)?
      每个base class可以被derived class object产生出来的base class table内的slot指出,因为每个slot内含一个base class地址。而derived class object中指向这个base class table(基类表)的指针称为bptr(基类表指针)。
      这类似于,每个class object都有一个vptr,指向一个vtbl(虚函数表)。不过,这里的vptr换成了bptr,vtbl换成了bptr。

    优点:
    1)每个class object中对于继承都有一致的表项方式,每个class object都应该在某个固定的位置上安放一个base table指针,与base classes的大小或数目无关;
    2)不需要改变class objects本身,就可以放大、缩小,或更改base class table。

    缺点:
    1)由于间接性而导致的空间和存取时间上的额外负担,串链的深度会导致多次访问基类对象成员;

    多重继承iostream对象模型见下:
    image

    关键词带来的差异 A keyword distinction

    当语言无法区分是声明,还是表达式时,需要一个超越语言范围的规则。
    例如,下面的代码是声明(declaration),还是调用(invocation)?

    // 不知道是declaration还是invocation, 直到看到整数常量1024才能决定:这是一个invocation
    int (*pf)(1024);
    
    // meta-language rule:
    // pq的一个declaration,而非invocation
    int (*pq)();
    

    关键词困扰 struct, class

    • 一致性用法
      C支持的struct和C++支持的class之间,有一个观念上的重要差异:struct代表data的集合,class还有data的相应操作(member function)。但关键词本身并不提供这种差异,这依赖于程序员对程序的约定。实际上,在使用上,struct与class没有区别,除了struct的data默认public,class的data默认private。
      例如,下面的东西可以说是struct,也可以说是个class。两种声明观念上的意义,取决于对“声明”本身的检验。
    // struct 名称(或clas名称)暂时省略
    {
    public:
    	operator int();
    	virtual void foo();
    	// ...
    protected:
    	static int object_count;
    	// mumble
    };
    

    所谓“取决于对“声明”本身的检验”,是指真正的问题并不在于“使用者自定义类型”(struct/class)的声明是否必须使用相同的关键词,而在于使用class或struct关键词是否给予“类型的内部声明”以某种承诺。
    比如,如果用struct实现C的数据萃取的观念,class实现C++的ADT(Abstract Data Type,抽象数据类型)观念,那么“不一致性”是一种错误的语言用法。
    例如,下面的代码合法吗?

    // 不合法吗?合法,只不过是不一致
    class node;
    struct node { ... };
    

    当struct表示数据萃取观念时,如果包含member functions,那就是不一致。

    struct A
    {
    	int n;
    	void get_n() { return n; }
    };
    

    PS:一致性的用法 是一种风格上的问题,而非语法的问题。

    • template不兼容struct
      template不打算与C兼容,struct是C的内容,class是C++的内容。如果C++要支持C程序代码,就不得不支持struct,但template不必支持struct。
    // 不合法
    template <struct T>
    class mumble { ...};
    
    // 没问题:明白地使用了class关键词
    template <class T>
    struct mumble { ... };
    

    策略性正确的struct

    • struct, class的数据成员内存布局
      struct能保证data members以其声明次序出现在内存布局中,因为它们都默认处于public的access section中。
      对于C++,处于同一access section的数据,也能保证以其声明次序出现在内存布局中,多个access sections中的数据则不一定。
    // 能保证age, name在内存中布局按其声明次序
    struct stumble 
    {
    	int age;
    	char name[1];
    };
    
    // 不能保证age, name在内存中布局按其声明次序
    class stumble
    {
    public:
    	char name[1];
    	// public operations ...
    protected:
    	// protected operations ...
    private:
    	int age;
    };
    
    • 基类和派生类的数据成员内存布局
      base classess和derived classes的data members的布局也没有规定谁先谁后。

    因此,程序不要依赖不同access section、base classes、derived claess之间的data members的顺序。

    如果程序需要一个复杂C++ class的某部分数据,拥有C声明的那种样子,那么那一部分最好抽取出来成为一个独立的struct声明。
    将C和C++结合在一起的方法:
    1)从C struct 中派生C++部分(不推荐,部分编译器不支持)

    struct C_point { ... };
    class Point : public C_point { ... };
    
    // C和C++两种方法都可获得支持
    extern void draw_line(Point, Point);
    extern "C" void draw_rect(C_point, C_point);
    
    draw_line(Point(0, 0), Point(100, 100));
    draw_rect(Point(0, 0), Point(100, 100));
    

    2)将C和C++组合到一起,而非继承(推荐)
    struct声明可以将数据封装起来,并保证拥有于C兼容的空间布局。不过,这项保证只在组合(composition)的情况下,才存在。如果是继承关系,而非组合,编译器会决定是否应该有额外的data members被安插到base struct subobject之中。

    struct C_point {...};
    
    class Point {
    public:
    	operator C_point() { return _c_point; }
    	// ...
    private:
    	C_point _c_point;
    	// ...
    };
    

    对象的差异 An object distinction

    C++程序设计模型直接支持三种程序设计典范(programming paradigms):

    1. 程序模型 (procedural model)
      过程式程序设计,比如处理字符串,使用字符数组作为参数,确定需要哪些过程,选择合适的算法(这里是标准C函数库中的str*\函数集)
    char boy[] = "Danny";
    char *p_son;
    ...
    p_son = new char[strlen(boy) + 1];
    if (!strcmp(p_son, boy))
    	take_to_disneyland(boy);
    
    1. 抽象数据类型模型(abstract data type model, ADT)
      将一组逻辑上相关的数据和操作(public接口)封装到一起提供。如下面的String class:
    class String
    {
    	char *str;
    	String(){...}
    	String(const String &s) {...}
    	operator=(){...}
    	...
    };
    
    String girl = "Anna";
    String daughter;
    ...
    // String::operator=();
    daughter = girl;
    ...
    // String::operator=();
    if (girl == daughter)
    	take_to_disneylan(girl);
    
    1. 面向对象模型(object-oriented model)
      此模型中,有一些彼此相关的类型,通过一个抽象的base class(提供共通的接口)被封装起来。
      例如,Library_materials class的例子中,subtypes如Book、Video、Compact_Disc、Puppet、Laptop等都可以从Library_materials派生而来。而凡是基类对象可以出现的地方,派生类对象都能出现。
    void check_in(Library_materials *pmat)
    {
    	if (pmat->late())
    		pmat->fine();
    	pmat->check_in();
    	
    	if (Lender *plend = pmat->reserved())
    		pmat->notify(plend);
    }
    

    在面向对象模型中,只有通过pointer或reference(引用)的间接处理,才能支持OO程序设计所需要的多态特性。如果是直接使用对象本身进行存取,会丧失子类扩展的那部分数据和功能。
    比如,下面例子中,直接将Book对象转化为基类Library_materials对象,会导致Book对象book被裁剪,值保留Library_materials那部分。

    Library_materials thing1;
    // class Book: public Library_materials { ... };
    Book book;
    
    // book会被裁剪,只保留Library_materials那部分
    thing1 = book;
    
    // 调用的是Library_materials::check_in(), 而非book::check_in()
    thing1.check_in();
    

    如果是通过base class的pointer或reference来完成多态:

    // OK: thing2参考到book
    Library_materials &thing2 = book;
    
    // OK: 调用的是Book::check_in()
    thing2.check_in();
    

    注意:void *指针也可以支持多态,但并不是语言级别的支持,需要程序员通过明确的转型操作类管理。

    • C++如何实现支持多态?
      步骤:
      1)经由一组隐含的转化操作,如把一个derived class指针转化为一个指向其public base type的指针;
      2)经由virtual function机制,调用实际对象绑定的虚函数;
      3)经由dynamic_cast和typeid运算符,将基类指针转化为派生类指针;
    class Shape {
    public:
    	virtual void rotate();
    };
    class Circle : public shape {
    	public:
    	virtual void rotate() override;
    };
    // 步骤1)
    shape *ps = new Circle;
    // 步骤2)
    ps->rotate();
    // 步骤3)
    if (circle *pc = dynamic_cast<circle *>(ps)) ...
    

    注意:dynamic_cast可以将基类指针转换为派生类指针或引用,前提是这个基类指针或引用 指向的对象实际上是派生类对象,否则转换会导致未定义行为。

    需要多少内存才能表现一个class object?

    一般而言,要有:

    • 其nonstatic data members的总和大小;
    • 加上任何由于alignment(字对齐)的需求而填补上去的空间(可能存在于members之间,也可能存在于集合体边界);
      alignment是将数值调整到某数的整数倍。在32bit计算机上,通常aligment为4byte,以使bus“运输量”达到最高效率。
    • 加上为了支持virtual而由内部产生的任何额外负担(overhead);
      通常指虚函数表指针vptr,虚基类指针bptr。

    指针的类型 The Type of a Pointer

    一个指向类对象的指针,与一个指向int的指针或者数组指针,有何不同?
    从内存角度来说,没有任何不同。三者都需要足够内存来存放一个机器地址(通常是个word,不同机器上长度可能不同),差异也不在于指针表示方法,或指针值不同,而是所寻址出来的object类型不同,i.e. “指针类型”会告诉编译器如何解释某个特定地址中的内存内容极其大小。

    例如,下面ZooAnimal指针指向的class object类型是ZooAnimal类型对象,编译器会根据其类型解释特定地址1000开始的内存、长为16byte的空间为ZooAnimal class object空间。

    class String {
    	int len;
    	char *str;
    	String(){...}
    	String(const String &s) {...}
    	operator=(){...}
    };
    
    class ZooAnimal {
    public:
    	ZooAnimal();
    	virtual ~ZooAnimal();
    	//...
    	virtual void rotate();
    private:
    	int loc;
    	String name;
    };
    
    ZooAnimal za("Zoey");
    ZooAnimal *pza = &za;
    

    image

    加上多态之后对象的差异

    例,定义一个Bear,作为一种ZooAnimal。可以通过public继承完成。

    class Bear : public ZooAnimal {
    public:
    	Bear();
    	~Bear();
    	// ...
    	void rotate(); // 继承ZooAnimal的virtual
    	// ...
    protected:
    	enum Dances {...};
    	
    	Dances dances_known;
    	int cell_block;
    };
    
    Bear b("Yogi");
    Bear *pb = &b;
    Bear &rb = *pb;
    

    b、pb、rb会是怎样的内存需求和布局?
    pointer,reference都只需要一个word空间(32位机上是4byte)。Bear object需要24bytes:ZooAnimal的16bytes + Bear定义的8bytes。

    Derived class(Bear)的object(b)和pointer(pb)、reference(rb)可能的内存布局:
    image

    假设Bear object放在地址1000处,一个Bear指针和一个ZooAnimal指针有何不同?
    如下面代码中&b 为1000,那么指向同一个地址&b的pz和pb有什么区别?

    Bear b;
    ZooAnimal *pz = &b;
    Bear *pb = &b;
    

    相同点是pz和pb都指向Bear object的第一个byte,差别是:pb涵盖的地址范围包括整个Bear object,pz涵盖的地址范围只包括Bear object中的ZooAnimal subobject部分。
    i.e. 2个指针能访问的范围不同,取决于指针类型。通过pz无法访问属于Bear那部分(成员数据和成员函数),只能访问属于ZooAnimal的那部分。

    参考

    [1]李普曼侯捷. 深度探索C++对象模型#:#Inside the C++ object model[M]. 电子工业出版社, 2012.

  • 相关阅读:
    存储过程调用API
    Visual Studio 2019 添加不了区域 解决办法
    .NET Core 3.1 IIS其它网站出现HTTP503无法访问解决方法
    串口数据处理分包处理
    树莓派实践总和
    mysql定期任务
    Newtonsoft.Json.Linq 序列化 反序列化等知识
    使用IDbCommandInterceptor解决EF-CORE-3.x-使用MYSQL时,未正常的生成LIKE查询语句
    .Net Core自定义读取配置文件信息appsettings.Json
    .Net Core Cors跨域
  • 原文地址:https://www.cnblogs.com/fortunely/p/15544593.html
Copyright © 2020-2023  润新知