5.1 无继承情况下的对象构造

Q1:POD对象的构造

• 以下讨论围绕下述例子展开:

 Eg:

            struct point
            {
                float x, y, z;
            };


            point global;
            int main()
            {
                point local;
                point * heap = new point;
                *heap = global;
                delete heap;
                return 0;
            }

• 观念上,编译器会为 point 声明一个 trivial 默认构造函数,一个trivial 析构函数,一个 trival 复制构造函数,以及一个 trival 赋值操作符函数

• 实际上,这些 trivial member 要不就是没定义,要不就是没有调用,程序行为与 C 中表现的一样

• 与 C 中的区别为:

在 C 中,全局变量 global 被视为 “临时性的定义(因为没有显示的初始化操作)”,将被放在 BSS 段中

在 C++ 中,全局变量 global 被视为完全定义的,由于类构造行为的隐式应用,因此,C++不支持“临时性定义”

• C与C++一个区别:BSS段在 C++ 中相对不重要

*注:也即是说,在 C++ 中所有的全局对象都被以“初始过的数据”来对待

• 例子中各个变量的性质:

    • global : 初始化过的对象
    • local : 既没有被构造也没有被析构
    • heap = new point :传回的类对象: 没有被构造
    • *heap = local : 观念上,触发了 trivial 赋值操作符函数做拷贝操作;事实上,赋值操作执行纯粹的位搬移操作
    • delete heap : 观念上,触发了 trivial 析构函数;事实上,未定义或调用

• 该对象会分析类 point 的声明,并为其贴上 POD 的标签,之后的构造操作如上述,执行简单的位搬移操作



Q2:ADT(抽象数据类型)

• 以下围绕以下例子讨论:

Eg:

        class Point
        {
        public:
            Point(int i = 0, int j = 0, int k = 0) :x(i), y(j), z(k){}

        private:
            int x, y, z;
        };

该类中仅定义了默认构造函数,并未定义复制构造函数,赋值操作符函数与析构函数,这些函数时 trivial的,事实上,编译器根本没有定义这些函数

• 这种情况下,其余三种成员函数将不被定义,其对应操作采用“逐比特行为”

• 考虑类成员的显示初始化式

• 显示初始化式的使用要求:

1. 没有定义构造函数

2. 全体数据成员均为 public

3. 只能指定常量



• 显示初始化式的使用例子:

Point p = {1,2,3};   //假设数据成员 x,y,z 均是 public

• 显示初始化式与构造函数的效率问题:

Eg:

        void test()
        {
            Point local1 = {1,2,3};     //假设数据成员 x,y,z 均是 public;
            Point local2;       //该语句将调用默认构造函数对 local2 进行初始化

            //对local2 调用默认构造函数,相当于如下的 inline expansion
            local2.x = 0
            local2.y = 0
            local2.z = 0
        }

local1 的初始化:当函数的 activation record 放进程序堆栈时,上述显示初始化列表中的常量就可以被放入 local1 的内存中
local2 的初始化:对默认构造函数进行了内联展开,对初始化列表中的每一个成员逐一进行赋值操作

*说明:显示初始化式的效率比内联构造函数的高,但其存在很多使用要求与条件。

• 在编译器层面有一个优化机制:编译器会抽取构造函数中逐成员的常量指定操作中的常量值,并对相应成员直接进行显示初始化式中的初始化方式,即直接将常量值放入对应类成员的内存中,而不是将构造函数扩展为一系列的赋值指令。

Eg:

        {
            Point local;    //将调用默认构造函数对其进行初始化,其中初始化式中有逐成员指定常量值的操作 x(0), y(0), z(0)
        }

编译器优化调整,将不再扩展构造函数,改为:

        {
            Point local = {0,0,0};
        }

• 例子中各个变量的属性:

    point global;
    point test()
    {
        point local;
        point * heap = new point;
        *heap = global;
        delete heap;
        return local;
    }

• global:执行 point::point(0,0,0); 调用默认构造函数
• local:也要执行默认构造函数进行初始化
• heap:被附加一个对默认函数的有条件的调用操作。如下:

            point * heap = new point;
            if(heap != 0)
                heap->point::point(0,0,0);

• *heap = global:仍然是简单的位拷贝操作
• delete heap:不会导致析构函数的调用,因为没有显示的提供一个析构函数实例,并不会合成
• return local:简单的位拷贝操作




Q3:为继承做准备,引入虚函数的情况

• 围绕以下例子进行讨论:

    class Point
    {
    public:
        Point(int i = 0, int j = 0, int k = 0) :x(i), y(j), z(k){}

        virtual float func();

    private:
        int x, y, z;
    };

在数据成员均是用数值来存储的情况下,默认构造函数,复制构造函数,赋值操作符函数,析构函数均使用默认语义(逐比特操作)是合理的

• 加入虚成员函数后,每一个类对象要多负担一个 vptr,同时会引起对某些未定义成员函数的合成,以及对某些已定义成员函数的膨胀:

1. 默认构造函数的膨胀

自定义的默认构造函数在完成了基类构造函数的调用之后,被附加了对 vptr 进行初始化的代码,然后才是程序员提供的代码

Eg:

            point::point(point * this, int i = 0, int j = 0, int z = 0) : x(i),y(j),z(k)
                {
                    //紧跟在基类的构造函数被调用后执行以下步骤:
                    this->_vptr_point = __vtbl_point;

                    //用户定义的代码:
                    this->x = i;
                    this->y = j;
                    this->z = k;
                    return this;
                }


2. 编译器需要合成复制构造函数与赋值操作符函数(因为此处类中有虚函数,为之前讨论的需要合成的情况)

不能对有虚函数的类采用逐比特的复制构造函数,假设调用复制构造函数的类的参数是其派生类对象,则此时派生类对象中的 vptr 指向派生类的虚函数表,此时直接逐比特复制将会导致基类的 vptr 指向了派生类的虚函数表,是一个错误的 vptr 初始化,因此,编译器需要合成一个复制构造函数。

Eg:

point* point::point(point * this, const point & rhs)
{
    //直接指定 point 的虚函数表的地址为 vptr 的值,而不是使用 rhs 的 vptr 的值来初始化
    this->_vptr_point = __vtbl_point;    

    //将 rhs 对象中的数据成员的值逐比特拷贝到 this 对象

    return this;  
}

• C++标准要求编译器尽量延迟 nontrival member 的实际合成操作,直到真正遇到起使用场合

• 例子中各个变量的属性:

    point global;
    point test()
    {
        point local;
        point * heap = new point;
        *heap = global;
        delete heap;
        return local;
    }

• global, local, heap 的初始化操作均调用默认构造函数进行初始化,delete heap 操作也是逐比特操作(不合成析构函数)

• *heap = global:该操作将触发复制构造函数的合成

• return local:由于复制构造函数的出现,将会初始 test() 函数发生变化,激发 NRV 优化

若存在大量传值返回操作,则应该定义复制构造函数,因为这会触发 NRV 优化,而触发了NRV 优化后,将不再需要调用复制构造函数

你可能感兴趣的:(合成默认构造函数,合成复制构造函数,合成析构函数,合成复制操作符函数,虚函数指针的初始化)