草庐IT

c++对象模型 拷贝、构造、虚构

chenglixue 2023-04-19 原文

抽象基类

​ 现有如下代码:

class Abstract_base
{
public:
    virtual ~Abstract_base() = 0;
    virtual void interface() const = 0;
    virtual const char* mumble() const { return _mumble; }
    
protected:
    char* _mumble;
}

​ 以上抽象基类声明有几个问题:

  1. 即使class被声明为抽象基类,其依然需要explicit constructor来初始化protected data member _mumble,否则derived class无法决定_mumble初值
  2. 抽象基类的virtual destructor不要声明为pure。因为每个derived class destructor会被编译器扩张,以静态方式调用每个virtual base class和上一层base class的destructor
  3. mumble()不应声明为virtual function,因为其定义的内容和类型无关,derived class并不会改写此函数
//合理的声明
class Abstract_base
{
public:
    virtual ~Abstract_base();
    virtual void interface() = 0;
    const char* mumble() const { return _mumble; }
    
protected:
    Abstract_base( char* pc = 0 );
    char* _mumble;
}
  • 一般而言,class的data member应被初始化,且只在constructor中或在class的其他member function中指定初值。其他操作都会破坏封装性质,让class的维护和修改变得愈加困难
  • 我们可以静态地定义和调用一个pure virtual function,但不能经由虚拟机制
  • c++保证继承体系中每个class object的destructors都能被调用,编译期不可压抑这一操作,且编译器并没有足够知识合成pure virtual destructor函数定义
  • 虚拟基类中,不要把所有的member functions都声明为virtual function,再靠编译器的优化把非必要的虚拟调用去除
  • 虚拟基类中,virtual function不使用const

对象构造

不继承

​ 现有如下片段:

typedef struct
{
    float x, y, z;
}Point;

Point global;

Point foobar()
{
    Point local;
    Point* heap = new Point;
    *heap = local;
    delete heap;
    return local;
}
  • 对于Point这样的声明,在c++会被贴上Plain OI' Data标签。编译器并不会为其声明default constrcutor、destructor、copy constructor、copy assignment operator

  • 对于 Point global; 这样的定义,在c++中members并没有被定义或调用,行为和c如出一辙。编译器并不会调用constructor和destructor。除非在c中,global被视作临时性定义

    临时性定义:因为没有显示初始化操作,一个临时性定义可以在程序多次发生,但编译器最终会将这些实例链接折叠起来,只留下一个实例,放在data segment中"保留给未初始化的global object使用的"空间
    
    但在c++中并不支持临时性定义,对于此例,会阻止后续的定义
    
  • c++中所有全局对象都被以初始化过的数据对待

  • 对于 Point* heap = new Point;编译器并不会调用default constructor,只是 Point* heap = __new( sizeof(Point) )。delete亦是如此

  • 对于*heap = local;编译器并不会调用copy assignment operator做拷贝,但只是像c那样做简单的bitwise

  • return操作也是,只是简单的bitwise,并没有调用copy constructor

​ 现有如下片段:

class Point
{
    public:
    	Point( float x = 0.0, float y = 0.0, float z = 0.0 ) : _x(x), _y(y), _z(z) { }
    	//没有copy constructor,copy operator,destructor
    
    private:
    	float _x, _y, _z;
}

void do()
{
    Point local1 = {1.0, 1.0, 1.0};
}
  • 对于将class中成员设定常量值,使用explicit initialization list更有效率。因为当函数的活动记录(activation record)被放进堆栈,initialization list的常量即可放入local1内存中

    活动记录过程的调用是过程的一次活动,当过程语句(及其调用)结束后,活动生命周期结束。变量的生命周期为其从被定义后有效存在的时间
    
  • 但explicit initialization list也有不足:

    • class member需要为public
    • 只能指定常量,因为其常量在编译器即可求值
    • 编译器并没有自动施行它,初始化很可能失败
  • 若调用之前的例子,放在现在编译器也不会调用destructor,因为并没有显示地提供destructor

    delete heap;
    

​ 现有如下片段:

class Point
{
    public:
    	Point( float x = 0.0, float y = 0.0, float z = 0.0 ) : _x(x), _y(y), _z(z) { }
    	virtual float z();
    	//没有copy constructor,copy operator,destructor
    
    private:
    	float _x, _y;
}
  • 导入了virtual functions会引发编译器对class Point的膨胀:

    • 定义的constructor附加一些代码,以便将vptr初始化。这些代码附加在任何base class constructors调用后,user code之前
    • 合成一个copy constructor 和 一个assignment operator,且不再是有用的(trivial),但implicit desctructor仍为有用的
  • 这种情况下,编译器在优化状态下可能会把object的连续内容拷贝到另一个object上,且不是memberwise。因此,编译器会尽量延迟nontrivial members的合成操作,直到遇到合适场合

  • 例如之前的例子,此时就很有可能合成copy assignment operator,以及inline expansion。对于return,又因为合成copy constructor,函数内部又需改写;但若编译器支持NRV,内部会改写为constructor,此时将不用调用copy constructor

    *heap = local;
    
    return local;
    

继承

T object;

对于以上定义,编译器会扩充每一个constructor。一般而言扩充如下:

  • 所有virtual base class constructors必须被调用,从左到右,从最深到最浅
    • 若class被列于member initialization list,如果有任何显示指定的参数,都应传过去。若没有列于list,而class有default constructor,则调用此
    • 此外,class中的每个virtual base class subobject的offset必须在执行期可被存取
    • 若 class object 是最底层的class,其constructors可能被调用
  • 所有上层的base class constructors必须被调用,以base class的声明顺序
    • 若class被列于member initialization list,如果有任何显示指定的参数,都应传过去。若没有列于list,而class有default constructor,则调用此
    • 若base class是多重继承下的第二或后继base class,那么this指针需调整
  • 若class object有virtual table pointers,其需指定初值
  • 记录在member initialization list的data member初始化会被放进constructor函数本体,以members声明顺序为顺序。若有一个member没有出现在member initialization list,但其有default constructor,那么该default constructor必须被调用

​ 实例:

class Point
{
public:
    Point( float x = 0.0, float y = 0.0 );
    Point( const Point& );
    Point& operator=( const Point& );
    
    virtual ~Point();
    virtual float z() { return 0.0; }
    
protected:
    float _x, _y;
};

class Line
{
    Point _begin, _end;
public:
    Line( float = 0.0, float = 0.0, float = 0.0, float = 0.0 );
    Line( const Point&, const Point& ) : _end(end), _begin(begin);
    
    draw();
    ...
};

//扩充
Line* Line::Line( Line* this, const Point&, const Point& )
{
    this->begin.Point::Point( begin );
    this->end.Point::Point( end );
    return this;
}

//合成隐式的Line destructor。若Line派生自Point,合成的将会是virtual
Line a;
  • 对于虚拟继承,以上constructor扩张方式将不再支持,这是因为virtual base class的共享性,否则会导致多次virtual base class的constructor。针对虚拟继承,每个derived class constructor扩张方式需发生改变,添加 bool __most_derived来判断是否为派生最底层,若为最底层则调用virtual base class consturctor

vptr初始化

​ 现有如下片段:

//假设每个class都定义了virtual function size(),传回class大小,每个constrcutor中调用size()
Point(x,y);
Point3d(x,y,z);
Vertex(x,y,z);
Vertex3d(x,y,z);
PVertex(x,y,z);

//当我们定义PVertex object,前五个constructor各自调用自己的size()
  • 做到如上机制,我们需要在执行一个constructor时,必须限制一组virtual functions候选名单。通过控制vptr的初始化和设定
  • vptr的初始化应在base class constructors调用后,在member initialization list所列member初始化操作前

​ 此时,constructor也需改变:

  • 在derived class constructor中,调用所有virtual base classes及上一层base class 的constructors
  • 然后,初始化vptr,指向相关virtual table
  • 若有member initialization list,将在constructor展开,但必须在vptr被设定后才施行
  • 最后,执行user code
//改变后的constructor扩张
PVertex* PVertex::PVertex( PVertex* this, bool __most__derived, float x, float y, float z )
{
    //调用virtual base constructor
    if( __most_derived != false ) this->Point::Point(x,y);
    
    //调用上层base class
    this->Vertex3d::Vertex3d(x,y,z);
        
    //初始化vptr
    this->__vptr_PVertex = __vtbl_PVertex;
    this->__vptr_Point__PVertex = __vtbl_Point__PVertex;
    
    //size()
    ...
        
    return this;
}

​ 当然以上方案并不完美:

Point::Point( float x, float y ) : _x(x), _y(y) { }
Point3d::Point3d( float x, float y, float z ) : Point(x,y), _z(z) { }

​ 此时,若声明PVertex,由于对其base class constructor的最新定义,其vptr将不再需要在每个base class constructor中被设定。因此,我们需要把constructor分裂为一个完整的object实例和sunobject实例

  • 在class的constructor的member initialization list中调用该class 的virtual functions,在语意上可能不安全,因为函数本身可能得依赖未被设立初值的members

对象复制

​ 设计一个class,并以一个class object指定给另一个class object,我们有三种选择:

  1. 什么都不做,实施默认行为
  2. 提供一个explicit copy assignment operator
  3. 显示拒绝把class object指定给另一个class object。也就是将copy assignment operator声明为private,且不提供定义
  • 只有在默认的member wise copy行为不安全或不正确时,才需要设计一个copy assignment operator。且如果class有bitwise copy,隐式的assignment operator不会合成

​ class对于default copy assignment operator,在以下情况,不会表现bitwise copy:

  1. 当class 内含member object,而其class有一个copy assignment operator
  2. 当class的base class有一个copy assignment operator
  3. 当class声明了任何virtual functions。一定别拷贝右边class object的vptr地址,它很有可能是derived class object
  4. 当class继承自virtual base class
  • copy assignment operators并不表示bitwise copy是nontrivial。只有nontrivial instances才被合成

  • 即使赋值由bitwise copy完成,并没有调用copy assignment operator,但还是需要提供一个copy constructor,以此打开NRV优化

  • 尽可能不要允许一个virtual base class的拷贝操作。不要在任何virtual base class中声明数据

对象效能

  • 对于单一继承和多重继承,若class使用bitwise copy,一般不会合成copy constructor,就不会增加效率成本

  • 对于虚拟继承,bitwise copy不再支持,而是合成copy assignment operator和inline copy constructor,导致成本大大增加。且继承体系复杂度增加,对象拷贝和构造的成本也会增加

对象析构

  • 若class没定义destructor,只有在class内含member object含有destructor时,编译器才会合成destructor
  • 若base class不含desturctor,那么derived class也不需要desturctor

​ destructor被扩展的方式。与constructor相似,但顺序相反:

  • destructor函数本体先被执行
  • 若class含有member class object,而后者含有destructors,他们会以其声明顺序的相反顺序被调用
  • 若object内含vptr,现需被重新指定,指向适当的base class的virtual table
  • 若有任何直接的nonvirtual base classes含有destructor,它们会以其相反的声明顺序被调用
  • 若由任何virtual base classes含有destructor,如之前的PVertex例子,会以其原来的构造顺序的相反顺序调用

有关c++对象模型 拷贝、构造、虚构的更多相关文章

  1. ruby - 如何从 ruby​​ 中的字符串运行任意对象方法? - 2

    总的来说,我对ruby​​还比较陌生,我正在为我正在创建的对象编写一些rspec测试用例。许多测试用例都非常基础,我只是想确保正确填充和返回值。我想知道是否有办法使用循环结构来执行此操作。不必为我要测试的每个方法都设置一个assertEquals。例如:describeitem,"TestingtheItem"doit"willhaveanullvaluetostart"doitem=Item.new#HereIcoulddotheitem.name.shouldbe_nil#thenIcoulddoitem.category.shouldbe_nilendend但我想要一些方法来使用

  2. ruby-on-rails - Rails - 子类化模型的设计模式是什么? - 2

    我有一个模型:classItem项目有一个属性“商店”基于存储的值,我希望Item对象对特定方法具有不同的行为。Rails中是否有针对此的通用设计模式?如果方法中没有大的if-else语句,这是如何干净利落地完成的? 最佳答案 通常通过Single-TableInheritance. 关于ruby-on-rails-Rails-子类化模型的设计模式是什么?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.co

  3. ruby-on-rails - 按天对 Mongoid 对象进行分组 - 2

    在控制台中反复尝试之后,我想到了这种方法,可以按发生日期对类似activerecord的(Mongoid)对象进行分组。我不确定这是完成此任务的最佳方法,但它确实有效。有没有人有更好的建议,或者这是一个很好的方法?#eventsisanarrayofactiverecord-likeobjectsthatincludeatimeattributeevents.map{|event|#converteventsarrayintoanarrayofhasheswiththedayofthemonthandtheevent{:number=>event.time.day,:event=>ev

  4. ruby-on-rails - Rails - 一个 View 中的多个模型 - 2

    我需要从一个View访问多个模型。以前,我的links_controller仅用于提供以不同方式排序的链接资源。现在我想包括一个部分(我假设)显示按分数排序的顶级用户(@users=User.all.sort_by(&:score))我知道我可以将此代码插入每个链接操作并从View访问它,但这似乎不是“ruby方式”,我将需要在不久的将来访问更多模型。这可能会变得很脏,是否有针对这种情况的任何技术?注意事项:我认为我的应用程序正朝着单一格式和动态页面内容的方向发展,本质上是一个典型的网络应用程序。我知道before_filter但考虑到我希望应用程序进入的方向,这似乎很麻烦。最终从任何

  5. ruby-on-rails - 在混合/模块中覆盖模型的属性访问器 - 2

    我有一个包含模块的模型。我想在模块中覆盖模型的访问器方法。例如:classBlah这显然行不通。有什么想法可以实现吗? 最佳答案 您的代码看起来是正确的。我们正在毫无困难地使用这个确切的模式。如果我没记错的话,Rails使用#method_missing作为属性setter,因此您的模块将优先,阻止ActiveRecord的setter。如果您正在使用ActiveSupport::Concern(参见thisblogpost),那么您的实例方法需要进入一个特殊的模块:classBlah

  6. ruby-on-rails - 如何验证非模型(甚至非对象)字段 - 2

    我有一个表单,其中有很多字段取自数组(而不是模型或对象)。我如何验证这些字段的存在?solve_problem_pathdo|f|%>... 最佳答案 创建一个简单的类来包装请求参数并使用ActiveModel::Validations。#definedsomewhere,atthesimplest:require'ostruct'classSolvetrue#youcouldevencheckthesolutionwithavalidatorvalidatedoerrors.add(:base,"WRONG!!!")unlesss

  7. ruby-on-rails - form_for 中不在模型中的自定义字段 - 2

    我想向我的Controller传递一个参数,它是一个简单的复选框,但我不知道如何在模型的form_for中引入它,这是我的观点:{:id=>'go_finance'}do|f|%>Transferirde:para:Entrada:"input",:placeholder=>"Quantofoiganho?"%>Saída:"output",:placeholder=>"Quantofoigasto?"%>Nota:我想做一个额外的复选框,但我该怎么做,模型中没有一个对象,而是一个要检查的对象,以便在Controller中创建一个ifelse,如果没有检查,请帮助我,非常感谢,谢谢

  8. Ruby 写入和读取对象到文件 - 2

    好的,所以我的目标是轻松地将一些数据保存到磁盘以备后用。您如何简单地写入然后读取一个对象?所以如果我有一个简单的类classCattr_accessor:a,:bdefinitialize(a,b)@a,@b=a,bendend所以如果我从中非常快地制作一个objobj=C.new("foo","bar")#justgaveitsomerandomvalues然后我可以把它变成一个kindaidstring=obj.to_s#whichreturns""我终于可以将此字符串打印到文件或其他内容中。我的问题是,我该如何再次将这个id变回一个对象?我知道我可以自己挑选信息并制作一个接受该信

  9. ruby-on-rails - 如果 Object::try 被发送到一个 nil 对象,为什么它会起作用? - 2

    如果您尝试在Ruby中的nil对象上调用方法,则会出现NoMethodError异常并显示消息:"undefinedmethod‘...’fornil:NilClass"然而,有一个tryRails中的方法,如果它被发送到一个nil对象,它只返回nil:require'rubygems'require'active_support/all'nil.try(:nonexisting_method)#noNoMethodErrorexceptionanymore那么try如何在内部工作以防止该异常? 最佳答案 像Ruby中的所有其他对象

  10. ruby-on-rails - 未在 Ruby 中初始化的对象 - 2

    我在Rails工作并有以下类(class):classPlayer当我运行时bundleexecrailsconsole然后尝试:a=Player.new("me",5.0,"UCLA")我回来了:=>#我不知道为什么Player对象不会在这里初始化。关于可能导致此问题的操作/解释的任何建议?谢谢,马里奥格 最佳答案 havenoideawhythePlayerobjectwouldn'tbeinitializedhere它没有初始化很简单,因为你还没有初始化它!您已经覆盖了ActiveRecord::Base初始化方法,但您没有调

随机推荐