C++幕后故事(七)--一个对象的生与死

这节里面咱们会学习到如下四点:c++

1.对象的生成时机
2.对象构造过程和POD类型
3.对象的复制语意
4.析构语意
设计模式

1.对象生成的时机

根据对象的控制力度不一样,对象的生成时机也是不同的。数组

咱们能够把它分为两类:安全

image

1.new操做符用户手动控制时机,随时new,随时生成。bash

2.编译器控制下也是有细微的差异,请看下面的表格。less

全局对象/全局静态对象 构造先于main函数的,在main以前还有不少的准备工做
局部静态对象 第一次调用的时候生成,第二次时不会在构造
局部对象 每次调用的时候都会生成

编译器为VS2013 x86,下面是代码验证:函数

/*
    测试:对象的构造、析构、拷贝语意
*/
namespace object_ctor_dtor_copy_semantic
{
class Cat
{
public:
    explicit Cat(const string &name) : mName(name) { cout << mName << endl; }
    ~Cat() { cout << "~" << mName << endl; }

private:
    string mName;
};

// 全局对象
Cat g_Cat("global cat");
// 全局静态对象
Cat g_s_Cat("global static cat");

void test_obj_ctor()
{
    // 局部变量
    Cat local_cat("local cat");

    // 局部静态对象
    static Cat local_s_cat("local static cat");
}

};

int main(int argc, char *argv[])
{
    cout << "------------start main------------" << endl;
    object_ctor_dtor_copy_semantic::test_obj_ctor();
    cout << "------------end main------------" << endl;
    return 0;
}
// 打印的结果
// global cat
// global static cat
// ------------start main------------
// local cat
// local static cat
// ~local cat
// ------------end main------------
// ~local static cat
// ~global static cat
// ~global cat
复制代码

关于局部对象这里有个小技巧跟你们分享下:类的实例只有在真正须要的时候再初始化布局

void test_local_useless(bool find) {
    Dog dog;
    
    // 返回操做,而这里初始化的dog对象没有任何的做用,无缘无故的增长dog的构造函数调用下降效率
    if (find) { return; }
    // 应该将对象的初始化延迟到真正须要的时候在初始化
    // 对dog的一系列操做
    // ...
}
复制代码

2.对象构造

2.1 构造函数作了什么?

咱们已经知道对象在何时生成,可是对象在生成过程除了咱们本身写的构造函数里面的动做,编译器在幕后也帮咱们作了不少的工做,这节咱们就要搞清楚编译器作了什么。 这一节既然要分析,咱们就来分析最复杂的模型,虚继承+虚函数模型。由于最难的搞懂了,那简单的还不是毛毛雨。学习

image

看以下代码:测试

class Point {
public:
    Point() : mX(1), mY(2) { cout << "point" << endl; }
    virtual ~Point() { cout << "~point" << endl; }
protected:
    int mX;
    int mY;
};

class Point3D : public virtual Point
{
public:    
    Point3D() : mZ(3) { cout << "point3d" << endl; }
    virtual ~Point3D() { cout << "~point3d" << endl; }
    virtual void VirFun1() { cout << "~VirFun1" << endl; }
    
protected:
    int mZ;
};

class Vertex : public virtual Point
{
public:
    Vertex() : mAngle(4) { cout << "vertex" << endl; }
    virtual ~Vertex() { cout << "~vertex" << endl; }
    virtual void VirFun2() { cout << "~VirFun2" << endl; }
    
protected:
    int mAngle;

};

class Vertex3D : public Point3D, public Vertex
{
public:
    Vertex3D() { cout << "vertex3D" << endl; }
    virtual ~Vertex3D() { cout << "~vertex3D" << endl; }
    virtual void VirFun3() { cout << "~VirFun3" << endl; }
};

class PVertex : public Vertex3D
{
public:
    PVertex() : mCount(5) { cout << "PVertex3D" << endl; }
    virtual ~PVertex() { cout << "~PVertex3D" << endl; }
    virtual void VirFun4() { cout << "~VirFun4" << endl; }

    void setvalue(int value) { mY = value; }

protected:
    int mCount;
};

void test_virtual_inherit_ctor() {
    PVertex pvertex;
    // pvertex.PVertex::~PVertex();
    // pvertex.setvalue(10);
    // 露出海面的表象
    // point
    // point3d
    // vertex
    // vertex3D
    // PVertex3D
    // ~PVertex3D
    // ~vertex3D
    // ~vertex
    // ~point3d
    // ~point
}

int main() {
   test_virtual_inherit_ctor();
   return 0;
}
复制代码

调用函数,打印的结果如上面代码中注释的那样,其实那只是冰山一角。咱们先看看PVertex布局是啥样的。老规矩将上面的代码保存为main.cpp。

1.借助VS2013开发人员命令提示,进入到main.cpp所在目录。

2.运行命令cl /d1 reportSingleClassLayoutPVertex main.cpp

3.拿出重要的部分咱们看看的

class PVertex   size(40):
        +---
        | +--- (base class Vertex3D)
        | | +--- (base class Point3D)
 0      | | | {vfptr}
 4      | | | {vbptr}
 8      | | | mZ
        | | +---
        | | +--- (base class Vertex)
12      | | | {vfptr}
16      | | | {vbptr}
20      | | | mAngle
        | | +---
        | +---
24      | mCount
        +---
        +--- (virtual base Point)
28      | {vfptr}
32      | mX
36      | mY
        +---

PVertex::$vftable@Point3D@:
        | &PVertex_meta
        |  0
 0      | &Point3D::VirFun1
 1      | &Vertex3D::VirFun3
 2      | &PVertex::VirFun4

PVertex::$vftable@Vertex@:
        | -12
 0      | &Vertex::VirFun2

PVertex::$vbtable@Point3D@:
 0      | -4
 1      | 24 (PVertexd(Point3D+4)Point)

PVertex::$vbtable@Vertex@:
 0      | -4
 1      | 12 (PVertexd(Vertex+4)Point)

PVertex::$vftable@Point@:
        | -28
 0      | &PVertex::{dtor}
复制代码

从导出的结构中看出,这个内存模型真是至关复杂,看着都有点头晕目眩。当对象之间的关系复杂以后,甚至连对象的大小都有膨胀的感受。

咱们关系整理下:

image

从上面能够看出,PVertex虚函数(除了虚析构函数)是追加在Point3D vfptr表中,而析构函数则是放在Point vfptr表中。

咱们把内存模型搞清楚了,剩下的简单多了,咱们下图所示:

image

看了半天发现,其实仍是很复杂,复杂到一页word装不下。整个的调用流程,感受都是在不断的设置虚表,设置虚基类表,不断的重复。而咱们写的代码只是其中的一小部分。 好,咱们再简化下这张图。(红色的线表示调用过程,蓝色线表示回溯过程)

image

这样看就简洁多了,整个调用的流程也是一目了然。

问题1:可是是否是以为有点奇怪,PVertex怎么直接调用Point构造函数,不是应该下面这样图?

image

可是这样的调用流程会形成将Point构造两次,大大的下降效率。因此编译器会决定由谁构造Point。关于virtual base class constructor如何被调用有着明确的定义:只有当一个完成的class object被定义出来(PVertex)时,它才会被调用;若是object只是某个完整object的subobject(Point3D),它就不会被调用(摘自《深刻探索C++对象模型》)。

举个例子:

1.咱们定义了一个Vertex3D对象,这时Point3D就是Vertex3D是个subobject对象,因此此时Point3D就不会调用Point构造函数。

2.定义了一个Point3D,它就是个完整的object,因此会直接调用Point构造函数。 问题2:在构造函数调用链中,咱们发现整个过程都是在不断的设置虚表地址和虚基类表地址。为何要来回不断的设置呢,在最开始的时候一次性搞定不就好了。

举个例子:

在不一样的对象域中不停的修改虚表和虚基类表地址作法我称之为入乡随俗

咱们在构造PVertex时,PVertex先去构造Point。此时Point对象已经构造完毕是个完整的对象,可是PVertex仍是残缺对象。若是这个时候咱们Point虚表地址仍是PVertex的虚表地址。此时咱们在Point构造函数间接调用到PVertex虚函数,而此时PVertex还未彻底构造完毕(好比一些成员变量还未初始化),这时调用PVertex虚函数就存在安全风险。说的简单点,在父类构造函数中就要把虚表地址设置为父类本身的而不是子类的。虚基类表也是一样的道理。同时会联想到在对象析构的时候也是相似的。

若是感兴趣的同窗能够再看下汇编代码,其实这里的汇编代码的思路就是很是的清晰,就是如何把内存给填满的。我把代码就放在最后面了。附录1.1汇编代码填充内存结构。

2.2 POD类型

所谓的POD全称是Plain Old Data。基本数据类型、指针、union、数组、构造函数是 trivial 的 struct 或者 class。其实C的struct极其的类似。

看下面代码:

class Dog {
public:
    int mSize;
    int mAge;
};

void test_pod_type() {
    // 1.没有加上括号,注意这里的成员值都是随机值
    Dog *dog = new Dog;

    // 2.加上括号,注意这里的成员值都为0
    Dog *dog1 = new Dog();
}
复制代码

可是结果却大不相同。加上括号初始化,会将对象中的成员变量作初始化。可是没有加上括号的对象中成员变量倒是个随机值。

可是若是Dog有构造函数,可是里面什么都不作。上面的两行初始化的结果倒是同样的,对象中成员变量的值都是随机值。

针对上面的代码,作个表格更直观点。

无构造函数 存在构造函数(未初始化) 存在构造函数(初始化)
不带()初始化 随机值 随机值 初始化为0
带()初始化 初始化为0 随机值 初始化为0

因此最佳的实践方式:给类加上构造函数同时给类中的成员变量赋初值,在构造对象的时候采用正规的作法加上括号

3.对象的复制语意

一说到复制语意,我就想到了build设计模式,固然这二者没有强相关性,硬要说关联那就是它们都是和对象的构造有关。

对象的复制语意分为两种,一种就是拷贝构造,还有一种就是赋值构造****(operator=)。可是有的同窗,这两种方式不能很好的区分。其实很简单,拷贝构造是从无到有的过程,赋值构造从新赋值过程,用已经存在的对象去从新赋值另一个已经存在的对象。(这里不说起std::move构造)。

在复制过程当中,编译器也会为咱们提供默认的复制构造语意,咱们把编译器提供的叫作浅拷贝(bitwise copy)。在拷贝的时候,每一个对象都拥有本身独立的一份资源而不是共享资源,这种方式叫作深拷贝(memberwise copy)。

为何会有两种方式?

编译器提供两种方式,是由于两种方式各有优缺点。浅拷贝效率略高于深拷贝,可是存在资源释放问题。深拷贝是把资源也会对应的拷贝一份,这样就会形成效率的降低。当类中不含有任何的资源,那么编译器提供的浅拷贝就已经胜任任务。

最后若是咱们不想要复制语意,能够将拷贝构造函数或者operator=设置为private属性。还可使用c++11 delete语法禁止复制语意。

4.析构语意

对象的析构能够当作对象构造的逆向过程。对象的析构函数是个很是重要的函数,由于在对象消失的那一刻对释放资源,作一些清理的工做。

咱们接着第二节对象构造里面的代码,画下析构的流程。

image

这里我就画了简易的示意图,其实它里面设置虚表和虚基类表地址的套路和它的构造流程是十分的类似,我就再也不重复了。

5.总结

这一节提到的拷贝构造,赋值构造,析构函数被称为C++的big three,这三个函数十分的重要必定要时刻当心。看一我的写的类文件,首先就要看从这三个函数开始,写了也不能表明水平很高,可是不写水平确定不高。

image

附录1:

1.汇编代码填充内存结构

; 调用PVertex的构造函数
00983181  lea         ecx,[pvertex]  
00983184  call        object_ctor_dtor_copy_semantic::PVertex::PVertex (09712CBh)  

; 设置Point3D域的虚基类表
009767D2  mov         eax,dword ptr [this]  
009767D5  mov         dword ptr [eax+4],98D7D4h  
; 设置Vertex域的虚基类表
009767DC  mov         eax,dword ptr [this]  
009767DF  mov         dword ptr [eax+10h],98D7E4h  
    ; 调整this指针,指向Point域
    009767E9  add         ecx,1Ch  
    ; 调用Point够着函数
    009767EC  call        object_ctor_dtor_copy_semantic::Point::Point (097193Dh) 

        ; 设置point虚表地址
        00976D33  mov         eax,dword ptr [this]  
        00976D36  mov         dword ptr [eax],98D68Ch  
        ; 初始化成员变量的值
        00976D3C  mov         eax,dword ptr [this]  
        00976D3F  mov         dword ptr [eax+4],1  
        00976D46  mov         eax,dword ptr [this]  
        00976D49  mov         dword ptr [eax+8],2 
        ; 再将this指针调回为pvertex的首地址
        00976809  mov         ecx,dword ptr [this]  
        ; 调用Vertex3D
        0097680C  call        object_ctor_dtor_copy_semantic::Vertex3D::Vertex3D (09713B1h) 
            00976F29  mov         ecx,dword ptr [this]  
            00976F2C  call        object_ctor_dtor_copy_semantic::Point3D::Point3D (0971311h) 
                ; 设置Point3D虚表
                00976C5D  mov         eax,dword ptr [this]  
                00976C60  mov         dword ptr [eax],98D6B8h
                ; 根据虚基类表找到偏移值
                00976C66  mov         eax,dword ptr [this]  
                00976C69  mov         ecx,dword ptr [eax+4]  
                00976C6C  mov         edx,dword ptr [ecx+4]  
                00976C6F  mov         eax,dword ptr [this]  
                ; 设置析构函数的虚表地址
                00976C72  mov         dword ptr [eax+edx+4],98D6C0h  
                ; 根据上面找到的偏移值,初始化成员变量
                00976C7A  mov         eax,dword ptr [this]  
                00976C7D  mov         dword ptr [eax+8],3  
            ; 调整this指针,指向Vertex的首地址
            00976F3A  mov         ecx,dword ptr [this]  
            00976F3D  add         ecx,0Ch  
            00976F40  call        object_ctor_dtor_copy_semantic::Vertex::Vertex (0971429h) 
                ; 设置Vertex虚表
                0097707D  mov         eax,dword ptr [this]  
                00977080  mov         dword ptr [eax],98D6F8h  
                ; 根据虚基类表找到偏移值
                00977086  mov         eax,dword ptr [this]  
                00977089  mov         ecx,dword ptr [eax+4]  
                0097708C  mov         edx,dword ptr [ecx+4]  
                0097708F  mov         eax,dword ptr [this]  
                ; 设置析构函数的虚表地址
                00977092  mov         dword ptr [eax+edx+4],98D704h  
                ; 根据上面找到的偏移值,初始化成员变量
                0097709A  mov         eax,dword ptr [this]  
                0097709D  mov         dword ptr [eax+8],4  
        ; 设置Vertex3D虚表地址
        00976F49  mov         eax,dword ptr [this]  
        00976F4C  mov         dword ptr [eax],98D73Ch  
        00976F52  mov         eax,dword ptr [this]  
        ; 设置Vertex3D虚基类表地址
        00976F55  mov         dword ptr [eax+0Ch],98D748h  
        00976F5C  mov         eax,dword ptr [this]  
        00976F5F  mov         ecx,dword ptr [eax+4]  
        00976F62  mov         edx,dword ptr [ecx+4]  
        00976F65  mov         eax,dword ptr [this]  
        ; 设置Vertex3D析构函数的虚表地址
        00976F68  mov         dword ptr [eax+edx+4],98D754h 

; 设置PVertex继承Point3D的虚表地址
00976818  mov         eax,dword ptr [this]  
0097681B  mov         dword ptr [eax],98D7A0h  
; 设置PVertex继承的Vertex的虚表地址
00976821  mov         eax,dword ptr [this]  
00976824  mov         dword ptr [eax+0Ch],98D7B0h  
; 设置PVertex继承的Point的虚表地址
0097682B  mov         eax,dword ptr [this]  
0097682E  mov         ecx,dword ptr [eax+4]  
00976831  mov         edx,dword ptr [ecx+4]  
00976834  mov         eax,dword ptr [this]  
00976837  mov         dword ptr [eax+edx+4],98D7C4h  
; 成员变量的初始化
0097683F  mov         eax,dword ptr [this]  
00976842  mov         dword ptr [eax+18h],5   
复制代码
相关文章
相关标签/搜索