《深度探索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; }

protected:
    char* _mumble;
};

你看出什么问题了没有?虽然这个class被设计为一个抽象的 base class (其中有pure virtual function,使得 Abstract_base不可能拥有实体),但它仍然需要一个明确的构造函数以初始化其data member _mumble。如果没有这个初始化操作,其derived class的局部性对象_mumble将无法决定初值。例如:

class Concrete_derived : public Abstract_base {
public:
    Concrete_derived();
    // ...
};

void foo() {
    // Abstract_base::_mumble未被初始化
    Concrete_derived trouble;
    // ...
}

测试

《深度探索c++对象模型》第五章笔记_第1张图片

 一般而言,class的 data member应该被初始化,并且只在 constructor中或是在class的其它 member functions中指定初值。其它任何操作都将破坏封装性质,使class的维护和修改更加困难。

纯虚拟函数的存在( Presence of a Pure Virtual Function )

C++新手常常很惊讶地发现,一个人竟然可以定义和调用( invoke ) 一个pure virtual function;不过它只能被静态地调用( invoked statically),不能经由虚拟机制调用。例如,你可以合法地写下这段码:

// 定义纯虚函数
// 但是只可能被静态调用

inline void
Abstract_base::interface() const {
    std::cout << "Abstract_base::interface() const has been recalled" << std::endl;
}

inline void
Concrete_derived::interface() const {
    // 调用纯虚函数
    Abstract_base::interface();
    std::cout << "Concrete_derived::interface() const has been recalled" << std::endl;
    
}

这里把前面的代码一整合,子类公有继承父类,测试:

《深度探索c++对象模型》第五章笔记_第2张图片

报错,子类中没有声明:virtual void interface() const = 0。给子类中加上这句就没问题了。

一个比较好的替代方案就是,不要把 virtual destructor声明为pure.

虚拟规格的存在( Presence of a Virtual Specification)

一般而言,把所有的成员函数都声明为virtual function,然后再靠编译器的优化操作把非必要的virtual invocation去除,并不是好的设计观念。

虚拟规格中const的存在

不用const

重新考虑class 的声明

由前面的讨论可知,重新定义 Abstract_base 如下,才是比较适当的一种设计:

class Abstract_base {
public:
    virtual ~Abstract_base();                    // 不再是pure
    virtual void interface() = 0;                // 不再是const
    const char*
        mumble() const { return _mumble; }       // 不再是virtual

protected:
    Abstract_base(char* pc = 0);                // 新增一个有参构造
    char* _mumble;
};

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

考虑这个代码片段:

(1)Point global;
(2)
(3)Point foobar()
(4){
(5)    Point local;
(6)   Point* heap = new Point;
(7)    *heap = local;
(8)    // ...
(9)    delete heap;
(10)    return local;
(11)}

一个object 的生命,是该object的一个执行期属性。local object的生命从L5的定义开始,到L10 为止。 global object的生命和整个程序的生命相同.heap object的生命从它被new运算符配置出来开始,到它被delete运算符摧毁为止。

Point的声明:

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

观念上,编译器会为Point声明一个trivial default constructor、一个trivial destructor、一个trivial copyconstructor,以及一个trivial copy assignment operator。但实际上,编译器会分析这个声明,并为它贴上 Plain Old Data卷标。

POD指的是这样一些数据类型:基本数据类型、指针、union、数组、构造函数是 trivial 的 struct 或者 class。
POD用来表明C++中与C相兼容的数据类型,可以按照C的方式来处理(运算、拷贝等)。非POD数据类型与C不兼容,只能按照C++特有的方式进行使用。

然而,事实上那些trivial members要不是没被定义,就是没被调用,程序的行为一如它在C中的表现一样.

只有一个小小的例外。在C 之中,global被视为一个“临时性的定义”,因为它没有明确的初始化操作。一个“临时性的定义”可以在程序中发生多次.那些实例会被链接器折叠起来,只留下单独一个实体,被放在程序 data segment中一个“特别保留给未初始化之global objects使用”的空间。由于历史的缘故,这块空间被称为BSS,这是 Block Started by Symbol的缩写,是 IBM 704assembler 的一个pseudo-op.

global在C++中被视为完全定义(它会阻止第二个或更多个定义).C和C++的一个差异就在于,BSS data segment在C++中相对地不重要。C++的所有全局对象都被当作“初始化过的数据”来对待。

foobar()函数中的 L5,有一个Point object local ,同样也是既没有被构造也没有被解构。当然啦,Point object local 如果没有先经过初始化,可能会成为一个潜在的程序臭虫(bug),万一第一次使用它就需要其初值的话(如L7)。至于heap object在L6的初始化操作:

new会被转换成

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

delete会被转换成:

_delete(heap);

抽象数据类型(Abstract Data Type)

以下是 Point的第二次声明,在 public接口之下多了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;
};

如果要对class中的所有成员都设定常量初值,那么给予一个explicit initialization list会比较高效(比起意义相同的constructor 的 inline expansion而言)。甚至在 local scope中也是如此。

void test()
{
    Point local1 = {1.0, 1.0, 1.0};
    Point local2;
    local2._x = 1.0;          // 注意成员变量是private,外部访问不到,这里有误
    local2._y = 1.0;
    local2._z = 1.0;
}

local1的初始化操作会比 local2的高效.这是因为当函数的 activation record被放进程序堆栈时,上述initialization list 中的常量就可以被放进local1内存中了.

Explicit initialization list带来三项缺点:

  1. 只有当class members都是 public时,此法才奏效。
  2. 只能指定常量,因为它们在编译时期就可以被评估求值( evaluated)
  3. 由于编译器并没有自动施行之,所以初始化行为的失败可能性会比较高一些。

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

总结一句话:自己把需要的都写着

为继承做准备

我们的第三个Point声明,将为“继承性质”以及某些操作的动态决议( dynamic resolution)做准备。当前我们限制对z成员进行存取操作:

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

    virtual float getZ();
private:
    float _x, _y;
};

有了虚函数,有参构造函数会产生膨胀(初始化vtbl)

合成的copy constructor和copy assignment operator也会膨胀。

5.2继承体系下的对象构造

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

  1. 记录在member initialization list中的 data members初始化操作会被放进constructor的函数本身,并以members的声明顺序为顺序。
  2. 如果有一个member并没有出现在member initialization list之中,但它有一个default constructor,那么该default constructor必须被调用。
  3. 在那之前﹒如果class object有virtual table pointer(s),它(们)必须被设定初值,指向适当的virtual table(s).
  4. 在那之前,所有上一层的base class constructors必须被调用,以base class的声明顺序为顺序(与member initialization list中的顺序没关联):《深度探索c++对象模型》第五章笔记_第3张图片
  5. 在那之前,所有virtual base class constructors必须被调用,从左到右,从最深到最浅:《深度探索c++对象模型》第五章笔记_第4张图片

 

 

你可能感兴趣的:(C&C++记录学习,笔记,深度探索c++对象模型,c++,keep,studying)