C++对象模型:构造、析构、拷贝语意学

目录

1、有关纯虚函数

2、无继承情况下的函数构造

2.1struct 构造

2.2抽象数据类型

2.3有虚函数的情况

3.继承体系下的对象构造

3.1概述

3.2虚拟继承的情况

3.3vptr的初始化

4.对象复制语意学

5.析构函数语意学


1、有关纯虚函数

抽象基类的数据成员初始化

如果一个类被声明为抽象基类(其中有 pure virtual function),则抽象基类不能实例化,但它仍需要一个显示的构造函数以初始化其成员变量。如果没有这个初始化操作,其 derived class 的局部对象 _mumble 将无法决定初值。

class ABC
{
public:
    virtual void interface() = 0;                          //考虑尽量不用const

    const char* mumble() const { return _mumble; }        //不会被派生类改写的不要用virtual

    virtual ~ABC() { };                              //不要将析构函数声明为pure virtual
protected:
    char* _mumble;   
    ABC(char* mumble_value = 0);
}

//初始化其成员变量
ABC::ABC(char* mumble_value) : _mumble( mumble_value ) { };

这样,可以由 derived class 的构造函数去初始化其 ABC 部分的成员变量。

关于虚析构函数,不要将 virtual destructor 声明为 pure。因为每一个 derived class destructor 会被编译器加以扩张,以静态的方式调用其每一个 virtual base class 以及上一层 base class 的 destructor,只要缺乏任何一个基类析构函数的定义就会导致链接失败。而此时如果此时是纯虚函数,则不可能有析构函数来调用。

2、无继承情况下的函数构造

2.1struct 构造

global 变量存放于全局静态区,局部变量存放于栈,用 new 方法生成的对象存放于堆。

Point global;
Point foobar() {
    Point local;
    Point *heap = new Point;
    *heap = local;
    delete heap;
    return local;
}

C++中的Plain Ol' Data 声明,类似于结构体,只有成员无相关操作。以C++来编译这段代码的话,编译器会为Point声明:trivial默认构造函数,trivial析构函数,trivial拷贝构造函数,trival拷贝赋值运算符。 

1.第一行代码Point global;会在程序起始时调用Point的constructor,然后在系统产生的exit()之前调用destructor。 
在C++中,全局对象都是被以“初始化过的数据”来对待,因此置于data segment。 
2.第四行代码 Point *heap = new Point;声明一个堆上的对象,其中new运算符会被转化为:Point *heap = __new(sizeof(Point));
此时并没有default constructor施行于*heap object(无自身定义的default constructor,系统提供的构造函数会使初值不确定)。 
3.第五行代码 heap = local;由于local没有初始化,因此会产生编译警告”local is used before being initaalized”。 
接着delete heap;会被转化为__delete(heap);这样会触发heap的trival destructor。 
4.最后函数已传值的方式将local当作返回值传回,这样会触发trival copy constructor,不过由于该对象是个POD类型,所以return操作只是一个简单的bitwise copy。

2.2抽象数据类型

提供private数据实现封装,但是没有virtual function:

class Point {
public:
    Point( float x = 0.0, float y = 0.0, float z = 0.0 ) 
        : _x(x), _y(y), _z(z) { }
private:
    float _x, _y, _z;
};

对于global实例: Point global;
现在有了默认构造函数作用在其身上,由于global定义在全局范畴,其初始化操作会延迟到程序启动(startup)时才开始。(统一构造一个_main()函数,该函数内调用所有global对象的默认构造函数), 具体见第六章。 

考虑Point *heap = new Point;与之前不同在于,Point 有自己的 nontrival 默认构造函数,所以会显式调用这个默认构造函数如下:

Point *heap = __new(sizeof(Point));
if(heap != 0)
    heap->Point:Point():

观念上,我们的 Point class 有一个相关的默认拷贝构造、拷贝运算符和析构函数会生成,然而他们都是不重要的(trivial),而且编译器实际上根本没有生成他们。

2.3有虚函数的情况

class Point {
public:
    Point(float x = 0.0, float y = 0.0) :
        _x(x), _y(y) { }
    virtual float z();
private:
    float _x, _y;
};

构造时,不仅会为每个对象多引入一个4 bytes 的 vptr 外,虚函数的导入也会引发编译器对类的膨胀作用:

Point::Point(Point *this, float x, float y) : 
    _x(x), _y(y) {
    this->__vptr_Point = __vbtl_Point;//设定虚表
    this->_x = x;  //扩展初值列表
    this->_y = y;
    return this;//返回this
}

人为合成一个copy constructor和一个copy assignment operator,其操作是 nontrival,但是 implicit destructor 仍然是trival。如果以一个子类对象初始化父类对象,且是以位运算为基础,那么vptr的设定有可能是非法的。

//拷贝构造函数的内部合成
inline Point*
Point::Point(Point *this, const Point& rhs) {
    this->__vptr_Point = __vbtl_Point;
    //将rhs坐标的连续位拷贝到this对象
    return this;
}

同样在foobar()中, *heap = local 的操作很有可能触发拷贝赋值运算符函数的生成,及其调用操作的一个 inline expansion :以 this 取代 heap 。以 rhs 取代 local 。

3.继承体系下的对象构造

3.1概述

当我们定义一个object如:T object  实际上会发生什么呢?如果T有一个constructor(不管是trival还是nontrival),它都会被调用,构造函数可能含有大量的隐藏代码。根据 class T 的继承体系,编译器会扩充每一个 constructor 。

1.如果有 virtual base class,虚基类的构造函数必须被调用,由浅到深,从左往右:

- 如果class位于成员初始化列表,有任何显示指定的参数都应该传递过去;若没有位于初始化列表,而class含有一个默认构造函数,也应该调用。

- class中的每一个virtual base class subobject的偏移量必须在执行期可存取。

- 如果class是最底层的class,其constructors可能被调用。 

2.如果有base class,基类的构造函数必须被调用; 

- 如果class位于成员初值列,有任何显示指定的参数都应该传递过去。 
- 若没有位于初值列,而class含有一个默认构造(拷贝)函数,也应该调用。 
- 如果class是多重继承下的第二或者后继的base class,那么this指针应该有所调整。 

3. 如果有虚函数,必须设定vptr指向适当的虚表; 

4. 如果一个member没有出现在成员初值列表中,但是该成员有一个默认构造函数,那么这个默认构造函数必须被调用; 

5. 成员初值列表中的member初始化操作放在constructor的函数体内,且顺序和声明顺序一致。

继承情况下的对象构造顺序为:虚基类 -> 基类 -> vptr与虚表 -> 类成员初始化 -> 自定义的代码。

再次以 Point 为例,增加拷贝构造、拷贝赋值运算符、虚析构函数,Line class 在 Point 基础上扩充。

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; }
private:
    float _x, _y;
};

class Lines {
    Point _begin, _end;
public:
    Lines(float = 0.0, float = 0.0, float = 0.0, float = 0.0);
    Lines(const Point&, const Point&);
    draw();
};

来看 Line 构造函数的定义与内部转化:

Lines::Lines(const Point& begin, const Point& end) :
    _begin(begin), _end(end){} //定义

//下面是内部转化,将初值列中的成员构造放入函数体,调用这些成员的构造函数
Lines* Lines::Lines(Lines *this, const Point& begin, const Point& end) 
{
    this->_begin.Point::Point(begin);
    this->_end.Point::Point(end);
    return this;
};

如果使用了Lines b = a; 这个时候调用合成的拷贝构造函数,合成的拷贝构造在内部可能如下:

inline Lines&
Lines::Lines(const Lines& rhs) {
    if(*this = rsh) return *this;   
    //证同测试,或者可以采用copy and swap,具体见effective C++
    //还要注意深拷贝和浅拷贝
    this->_begin.Point::Point(rhs._begin);
    this->_end.Point::Point(rhs._end);
    return *this;
}

3.2虚拟继承的情况

考虑下面这个虚拟继承(继承自Point)

class Point3d : public virtual Point {
public:
    Point3d(float x = 0.0, float y = 0.0, float z = 0.0)
        : Point(x, y), _z(z) { }
    Point3d(const Point3d& rhs)
        : Point(rhs), _z(rhs._z) { }
    ~Point3d();
    Point3d& operator=(const Point3d&);
    virtual float z() { return _z; }
private:
    float _z;
}

虚拟继承时,由于virtual base class的共享性的原因,传统的 constructor 扩张并没有用。例如以下继承关系:

实际Point3d的constructor的扩充如下:

//c++伪码
Point3d*
Point3d::Point3d(Point3d* this, bool __most_derived,
                float x = 0.0, float y = 0.0, float z = 0.0) {
    if(__most_derived != false) this->Point::Point();
    //虚拟继承下两个虚表的设定
    this->__vptr_Point3d = __vtbl_Point3d;
    this->__vptr_Point3d__Point = __vtbl_Point3d__Point;
    this->z = rhs.z;
    return this;
}

_most derived 在自己(最底层派生类) 构造时为 true ,在派生路径上的任一父类调用时设为 false

在更加深层次的继承情况下,例如Vextex3d,调用Point3d和Vertex的constructor时,总会将该参数设置为false,于是就压制了两个constructors对Point constructor的调用操作。 例如:

//c++伪码
Vextex3d*
Vextex3d::Vextex3d(Vextex3d* this, bool __most_derived,
                float x , float y , float z ) 
{
    if(__most_derived != false) 
        this->Point::Point();         //只由Vertex3d构造一次Point

    //设定__most_derived为false
    //调用上一层 base classes
    this->Point3d::Point3d(false, x, y, z);
    this->Vertex::Vertex(false, x, y);

    //设定vptrs
    return this;
}

从而保证了子类中只有一个虚拟继承的 subobject 。

3.3vptr的初始化

vptr的初始化操作:在base class constructor调用操作之后,但是在程序员供应的代码或”成员初值列中所列出的成员初始化操作”之前。 

不能放在任何操作之前:基类中有 virtual function 调用,基类构造完成时,派生类构造才能覆盖重写的 virtual function。

不能在所有初始化过程之后:当一个 subobj constructor 调用一个 virtual function 时,希望调用的是自己类对应的 virtual function 而不是派生类的 virtual function 。

之前的PVertex constructor可能被扩张成:

PVertex*
PVertex::Pvertex(PVertex*this, bool __most_derived,
            float x, float y, float z) 
{
    //有条件调用virtual base class constructor
    if(__most_derived != false) this->Point::Point();
    //无条件调用上一层base class constructor
    this->Vertex3d::Vertex3d(x, y, z);

    //设定vptr
    this->__vptr_PVertex = __vtbl_PVertex;
    this->__vptr_Point3d__PVertex = __vtbl_Point3d__PVertex;

    //执行程序员的代码
    if(spyOn)
        cerr << "Within Pvertex::PVertex()"
             << "size: "
             //虚拟机制调用
             << (*this->__vptr_PVertex[3].faddr)(this)
             << endl;
    return this;
}

在 class 的 constructor 的 成员初始化列表中调用该 class 的一个 virtual function 安全吗?

实际而言,vptr 保证能够在成员初始化之前由编译器设定好,总是安全的。但在语意上这可能是不安全的,因为 virtual 函数本身可能会依赖未被设定初值的成员变量。

4.对象复制语意学

在不涉及虚拟继承只有一个子对象的情况下,编译器合成的派生类的赋值运算符函数会调用所有即时基类的 operator = 函数:

inline Point&
Point::operator=(const Point& p) {
    _x = p._x;
    _y = p._y;
    return *this;
}

//Point3d虚拟继承自Point
class Point3d : virtual public Point {
public:
    Point3d(float x = 0.0, float y = 0.0, float z = 0.0);
private:
    float _z;
};

//如果没有为Point3d设定一个copy assignment operator,编译器就会为其合成一个
inline Point3d&
Point3d::operator=(point3d* this, const Point3d& p) {
    this->Point::operator=(p);  //base class operator=
    _z = p._z;  //memberwise copy
    return *this;
}

同样考虑之前的继承体系,类Vertex虚拟继承自Point,并且从Point3d和Vertex派生出Vertex3d。则编译器生成的 copy operator 如下:

inline Vertex&
Vertex operator=(const Vertex& v) {
    this->Point::operator=(v);
    _next = v._next;
    return *this;
}
inline Vertex3d&
Vertex operator=(const Vertex3d& v) {
    this->Point::operator=(v);
    this->Point3d::operator=(v);    //在 Point3d 的复制运算符函数抑制 Point 的复制运算符函数
    this->Vertex::operator=(v);     //在 Vertex 的复制运算符函数抑制 Point 的复制运算符函数
    return *this;
}

编译器如何在Point3d和Vertex的copy assignment operator抑制 Point 的 copy assignment operator 呢?constructor 中的解决办法是添加一个额外的参数 _most_derived。

编译器生成默认 copy assignment operator 的解决办法是:为copy assignment operator提供 分化函数( split functions )以支持这个 class 称为 most-derived class 或者成为中间的base class。( 即分情况特殊处理 ) 

也可以将虚基类的copy assignment operator 调用放在最后,虽然也会造成重复拷贝,但是语意正确。
建议:尽可能不要允许一个virtual base class的拷贝操作,更进一步,不要在任何virtual base class中声明数据。

5.析构函数语意学

析构函数也是根据编译器的需要才会合成出来,两种情况: 
1. class中内含的某个object拥有析构函数; 
2. 继承自某个base class,该base class含有析构函数

例如,在 Point 类中,默认情况下并没有被编译器合成出一个 destructor ,甚至它拥有一个 virtual function 。如果把两个 Point 对象 _begin 和 _end 组合成一个 Line 类,也不会合成出一个 destructor。当我们从 Point 派生出 Point3d(即使是虚拟派生),如果没有声明一个 destructor ,编译器就没有必要合成一个 destructor 。使用 new 与 delete 管理的对象在非必要情况下也不会生成 destructor 。

与构造函数相比,即使拥有虚函数或者虚拟继承,不满足上述两个条件,编译器是不会合成析构函数的。

如果说 Vertex 析构函数牵扯从链表中删除一个节点(或者其他语意),那么这时候定义 destructor 就是必要的。而且其派生的类Vertex3d 如果我们不定义一个 explicit destructor ,编译器也会合成一个,其唯一任务是调用 Vertex destructor。若自己定义了析构函数,则编译器会扩展它使它调用 Vertex destructor。

在继承体系中,由我们定义的destructor的扩展方式和constructor类似,只是顺序相反,顺序如下: 
1. destructor的函数体首先执行。 
2. 如果class拥有member class object,且该class含有destructor,那么它们会以声明顺序相反的顺序依次被调用。 
3. 如果object内含一个vptr,重新被设定指向适当的base class的virtual table(即对象在析构的过程中,依次蜕变为其基类)。 
4. 如果有任何上层的nonvirtual base classes拥有destructor,那么它们会以声明顺序相反的顺序依次被调用。 
5. 如果有任何virtual base classes拥有destructor,那么它们会以原来构造顺序相反的顺序依次被调用。

 

主要参考:
作者:幸福的起点_ 
来源:CSDN 
原文:https://blog.csdn.net/qq_25467397/article/details/80451635 
版权声明:本文为博主原创文章,转载请附上博文链接!

猜你喜欢

转载自blog.csdn.net/SimonxxSun/article/details/84942871