构造、解构、拷贝语意学

一 “无继承”情况下的对象构造

考虑下面程序片段:





1

2

3

4

5

6

7

8

9

10

11

Point glocal; //全局内存配置

Point foobar()

{

   Point local;//局部栈内存配置

   Point *heap=new
Point;//heap内存配置

   *heap=local;

   

   delete
heap;

   return
local;

}

1 把Point类写成c程序,c++标准说这是一种所谓的Plain OI Data

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

如果我们以C++来编译这段代码,观念上,编译器会为Point 声明一个trivial default
constructor、一个trival destructor、 一个trivial copy constructor,以及一个trivial copy
assignment operator。但实际上,编译器会分析这个声明,并为它贴上Plain OI
Data
标签。

1>
L1中,观念上Point的trivial default
constructor和trival
destructor都会产生并被调用,然而,事实上那些trivial
members要不是没有被定义,就是没有被调用,程序的行为一如它在c中的表现一样。

2> L5中的Point object
local,同样也是既没有被构造也没有被析构。如果local没有先经初始化,可能会称为一个潜在的程序漏洞-万一第一次使用就需要其初始值。

3> L6被转化为: Point
*heap=__new(sizeof(Point)); 并没有default constructor施行与new运算符所传回的Point
object身上。

4>
L7中,观念上该操作会触发trivial
copy assignment operator进行拷贝搬运操作。然而实际上,该对象被看作一个Plain
OI Data
,所以赋值操作将只是像C那样的纯粹位搬移操作

5>
L9中的操作被转化为:__delete(heap); destructor要不是没有产生就是没有调用。

6>
L10中,函数以传值的方式将local当作返回值传回,这在观念上会触发trivial
copy constructor,不过实际上return操作只是简单的位拷贝操作,因为对象是一个Plain
OI Data

2
抽象数据类型

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

我们并没有为Point定义一个copy cosntructor或copy operator,因为默认的位语意(default
bitwise semantics)已经足够。我们也不需要一个destructor,因为程序默认的内存管理方法也已经足够。

1> L1中,有了default
constructor作用与其上,由于global被定义在全局范围中,其初始化操作延迟到程序激活时才开始。

2> L5中,会被加上default
Point constructor的inline expansion;如下:

Point local;

Point local;

local._x=0.0;local._y=0.0;local._z=0.0;

3> L6配置heap Point object:

Point *heap=new Point; //现在被附加上一个对default
Point constructor的有条件调用。

Point *heap=__new(sizeof(Point));

if(Hadp!=0)

heap->Point::Point();

然后才被编译器进行inline
expansion操作,至于heap指针指向local object:

4> L7中: *heap=local;则保持着简单的位拷贝操作。

6> L10中,以传值方式传回local object,情况也是一样,保持着简单的位拷贝操作。

5> L9中的删除操作,并不会导致destructor被调用,仍采用默认的内存管理方法,因为我们并没有明确地提供一个destructor函数实体。

观念上,我们Point class有一个相关的default
copy constructor、copy operator和destructor,然而都是无关痛痒的(trivial),而且编译器实际上根本没有产生他们。

3 为继承做准备

class
Point{
public:
 
  Point(float
x=0.0,float y=0.0):_x(x),_y(y){}
    //no destructor,copy  cosntructor,or copy
operator defined...
    virtual float
z();
protected:
 
  float
_x,_y;
};

virtual函数引入带来的影响:

① 促使每一个Point
object拥有一个virtual table pointer,这个指针提供给我们virtual
接口的弹性,代价每一个object需要额外一个word的空间。

② 我们所定义的constructor被附加一些代码,以便初始化vptr,这些信息必须被附加在任何base class
cosntructors的调用之后,但必须在任何使用者(程序员)供应的码之前。

③ 合成一个copy constructor和一个copy assignment
operator,而且其操作不再是trivial。用在一个Point object被初始化或以一个derived class
object赋值,正确处理vptr指针。

1>
L1的gloabl初始化操作,L1的local初始化操作,L6的heap初始化操作以及L9的heap删除操作,都还和2中的一样。

2> L9中的赋值操作,很可能触发copy assignment
operator的合成,及其调用操作的一个inline expansion。

3> 最戏剧性的冲击发生在以传值方式传回local的那一行,由于copy
constructor的出现,foobar()很可能被转化为下面这样:

//用以支持copy constructor

Point foobar(Point &__result)
{
   
Point local;
    local.Point::Point(0.0,0.0);
    //heap的部分没变
 
  __result.Point::Point(local);
//copy constructor的应用
 
  //local对象的destructor将在这里发生
 
  //local.Point::~Point();
   
return ;
}

如果支持NRV优化,这个函数还会进一步转化为:

//以支持NRV优化

Point foobar(Point &__result)
{
   
__result.Point::Point(0.0,0.0);
 
  //heap的部分没变
 
  return ;
}

重要注意或提示:

一般而言,如果你的设计之中有有许多函数都要以传值方式传回一个local
class object。那么提供改一个copy
constructor就比较合理-深知即使default memberwise语义已经足够,它的出现会触发NRV。然而,就想上面的例子一样,NRV优化后将不再需要调用copy
constructor。

二 继承体系下的对象构造

当我们顶一个object如下:

T object;

时,如果T有一个constructor(不论是有user提供或是编译器合成),它会被调用。

constructor可能内含大量的隐藏码,因为编译器会扩充每一个constructor,扩充程度视class
T的继承体系而定,一般而言编译器所做的扩充大致如下:

1 所有virtual base class
constructors必须调用,从左到右,从最深到最浅。(正确处理虚基类对象的偏移量offset)

2 所有上一层base class cosntructors必须被调用,以base class的声明顺序为顺序。

3 如果class object有vptr,他们必须设定初值,指向适当的virtual tables。

4 记录在member initialization list中的data
members初始化操作会放进constructor函数本身,并以members的声明顺序为顺序。

5 如果有一个member 并没有出现在member initialization list之中,但它有一个default
constructor,那么default constructor必须被调用。

虚拟继承

存在虚拟继承时,必须保证虚基类子对象的初始化必须有最底层的派生类完成,否则可能会出现多次初始化。方法:

1> 增加一个用以指示virtual base class constructor应不应该调用的参数。

2>
把每一个constructor分类为二,一个针对完整的object,另一个针对subobject。"完整object"版无条件地调用虚基类构造函数;"subobject”版则步调用虚基类构造函数。

vptr初始化语义学

vptr应该在base class constructors调用之后,但是在程序员供应的码或是"member
initialization list"中所列的members初始化操作之前。编译器保证这一点。

这样解决了“在class中限制一组virtual function名单
”的问题,如果每一个constructor都一直的等待到base class constructors执行完之后才设置其对象的vptr,那么每次都能够调用正确的virtual
function实体。

constructor的执行算法通常如下:

1> 在派生类构造函数中,“所有基类构造函数”及“上一层基类“的构造函数会被调用。

2> 上述完成之后,对象的vptr被初始化,指向相关的虚函数表。

3> 如果有成员初始化列表的话,将在构造函数体内扩展开来,这必须在vptr设定之后才惊醒,以免一个虚函数调用。

4> 调用类成员对象的默认构造函数,如果有的话。

5> 最后,执行程序员所提供的码。

但其实不是每个基类构造函数都必须初始化vptr的。vptr必须设定的两种情况:

1> 当一个完整的对象被构造出来时。

2> 当一个subobject constructor调用了一个虚函数是(不论是直接调用或间接调用)

三 对象的赋值(copy assignment opertor)语义学

我们设计一个类,并以一个类对象指定给另一个类对象时,我们有三个选择:

1 什么都不做,因此得以实施默认行为(member copy or bitwise copy)。

如果我们不对Point类供应一个赋值操作符,而光是依赖默认的memberwise
copy,编译器会产生出一个实体吗?

这个答案和copy constructor的情况一样。实际上不会,因为此类已经有了bitwise
copy语义了,所以implicit copy
assignment operator视为无用的,也根本不会合成出来。

2 明确地拒绝一个class object指定给另一个class object。(将copy
assignment operator声明为private,并且不提供定义即可)

3 提供一个explicit copy assignment
operator。只有默认行为所导致的语义不安全或不正确是,我们才设计一个赋值操作符。

一个类对默认的赋值操作符,在一下情况不会表现出bitwise copy 语义

1> 当class内含一个member object,而其class有一个copy assignment opertor时。

2> 当一个class的base class 有一个copy assignment opertor时。

3> 当一个class声明了任何virtual functions(我们一定不能拷贝右端class
object的vptr地址,因为它可能是一个派生类对象)。

4> 当class 继承自一个virtual base class(不论base class 有没有copy assignment
opertor)时。

c++标准上说copy
assignment opertor并不表示bitwise copy semantics是nontrivial。实际上,只有nontivial
instances才会合成出来。

建议尽可能不要允许一个virtual base
class的拷贝操作,甚至提供一个比较奇怪的建议:不要在任何virtual base class 中声明数据。 应为copy
assignment opertor没有什么好的方法避免virtual base class 在派生层次中重复拷贝现象。

?四
解构(析构)语义学

如果class没有定义destructor,那么只有在class内带的member
object(或是class 自己的base class)拥有destructor的情况下,编译器才会自动合成出来一个来。否则,destructor会被视为不需要,也就不会合成(当然不会调用了),或者说是trivial的。

我们应该根据“需要”而不是“感觉”来提供destructor,更不应该因为不确定是否需要一个destructor,于是就提供它。

为了决定class是否需要一个程序层面的destructor(或是constructor),我们应该想一下class
object的声明在哪了结束(或开始)?需要什么操作才能宝座对象的完整。这是我们写程序应该了解的,也是constructor和destructor什么时候起作用的关键。

一个有程序员定义的destructor被扩展的方式类是constructor被扩展的方式,但顺序相反:

1 destructor的函数本身首先被执行。

2 如果class拥有member class objects,而后者拥有destructor,那么会以其声明顺序的相反顺序被调用。

3 如果内带一个vptr,则现在重新设定,指向适当之base class的virtual
table。(并总是设置,只用量中情况下设置,和构造函数相似)

4 如果有任何直接的nonvirtual base classes拥有destructor,他会以声明顺序相反的顺序调用。

5 如果有任何virtual base
class拥有destructor,而当前讨论的这个class是最尾端的class,那么他们会以其原来的构造顺序的相反顺序被调用。

构造、解构、拷贝语意学,布布扣,bubuko.com

时间: 2024-10-12 18:26:30

构造、解构、拷贝语意学的相关文章

C++对象模型——构造,解构,拷贝语意学(第五章)

第5章 构造,解构,拷贝语意学 (Semantics of Construction, Destruction, and Copy) 考虑下面这个abstract base class 声明: class Abstract_base { public: virtual ~Abstract_base() = 0; virtual void interface() const = 0; virtual const char * mumble() const { return _mumble; } p

深度探索C++对象模型 第五章 构造、析构、拷贝语意学

1. const 成员函数需要吗? 尽量不要,如果存在继承,则无法预支子类是否有可能改变data member 2. pure virtual constructor 可以实现类的隐藏吗(包含data member)?   这样子类无法调用base 的构造函数对数据初始化,所以可以用protected来实现构造函数,可以实现子类调用: 3. 如果class中存在virtual function,则编译器会再构造函数中对vptr进行初始化(在base构造函数调用之后,而代码实现之前) 4.拷贝构造

解构控制反转(IoC)和依赖注入(DI)

1.控制反转 控制反转(Inversion of Control,IoC),简言之就是代码的控制器交由系统控制,而不是在代码内部,通过IoC,消除组件或者模块间的直接依赖,使得软件系统的开发更具柔性和扩展性.控制反转的典型应用体现在框架系统的设计上,是框架系统的基本特征,不管是.NET Framework抑或是Java Framework都是建立在控制反转的思想基础之上. 控制反转很多时候被看做是依赖倒置原则的一个同义词,其概念产生的背景大概来源于框架系统的设计,例如.NET Framework

scala 模式匹配详解 3 模式匹配的核心功能是解构

http://www.artima.com/scalazine/articles/pattern_matching.html这篇文章是odersky谈scala中的模式匹配的一段对话,我做了部分片段翻译(不是连贯的): 模式可以嵌套,就像表达式嵌套,你可以定义深层的模式,通常一个模式看起来就像一个表达式.它基本上就是同一类事情.它看起来像一个复杂的对象树构造表达式,只是漏掉了new关键字.事实上在scala当你构造一个对象,你不需要new关键字然后你可以在一些地方用变量做站位符替代对象树上实际的

ES6 对象解构赋值(浅拷贝 VS 深拷贝)

对象的扩展运算符(...)用于取出参数对象的所有可遍历属性,拷贝到当前对象之中. 拷贝对象 let aa = { age: 18, name: 'aaa' } let bb = {...aa}; console.log(bb); // {age: 18, name: "aaa"} 合并对象 扩展运算符(...)可以用于合并两个对象 let aa = { age: 18, name: 'aaa' } let bb = { sex: '男' } let cc = {...aa, ...bb

es6学习 -- 解构赋值

ES6 允许按照一定模式,从数组和对象中提取值,对变量进行赋值,这被称为解构(Destructuring). 以前,为变量赋值,只能直接指定值. let a = 1; let b = 2; let c = 3; ES6 允许写成下面这样. let [a, b, c] = [1, 2, 3]; 上面代码表示,可以从数组中提取值,按照对应位置,对变量赋值. 本质上,这种写法属于"模式匹配",只要等号两边的模式相同,左边的变量就会被赋予对应的值.下面是一些使用嵌套数组进行解构的例子. 我认为

参数和函数形参 到 解构和不完全解构

<span style="border-left:red;">函数的参数和形参</span> 声明一个有三个形参的函数 where function where(a,b,c){ ... } 而我们调用的时候执行 where(1,2,3,4,5){....} 此时参数 1.2.3会分别赋值给 a.b.c,参数4.5则不会赋值给任何形参.但是我们已让可以通过 隐式参数 arguments 去得到他们. <span style="border-left

ECMAscript6新特性之解构赋值

在以前,我们要对变量赋值,只能直接指定值.比如:var a = 1;var b = 2;但是发现这种写法写起来有点麻烦,一点都不简洁,而在ECMAScript6中引入了一种新的概念,那就是"解构",这种赋值语句极为简洁,比传统的属性访问方法更为清晰.那什么是解构呢?按照一定的模式,允许从数组或者对象中获取到值,并且对其变量进行赋值.称为"解构". 看到上图了吧,解构是不是很简洁.其实解构不单用于数组.对象,只要内部具有iterator接口,就都可以使用解构来给变量赋

解构赋值

1.数组解构 let [a,b,c,d] = ['aa','bb',77,88] 嵌套数组解构 let [a,b,[c,d],e] = ['aa','bb',[33,44],55] 空缺变量 let [a,b,,e] = ['aa','bb',[33,44],55] 多余变量 let [a,b,,e,f] = ['aa','bb',[33,44],55] 默认值 let [a,b,,e,f='hello'] = ['aa','bb',[33,44],55] 2.对象解构 let obj = ne