C++对象模型——对象的构造和解构(第六章)

6.1    对象的构造和解构 (Object Construction and Destruction)

一般而言,constructor和destructor的插入如预期所示:

{
    Point point;
    // point.Point::Point()    一般而言会被插入在这里
    ...
    // point.Point:;~Point() 一般而言会被插入在这里
}

如果一个区段(以{}括起来的区域)或函数中有一个以上的离开点,情况会稍微混乱一点.Destructor必须被放在每一个离开点(当时object还存活)之前,例如:

{
    Point point;
    // constructor 在这里行动
    switch (int(point.x())) {
    case -1:
        // mumble
        // destructor 在这里行动
        return;
    case 1:
        // mumble
        // destructor 在这里行动
        return;
    default:
        // mumble
        // destructor 在这里行动
        return;
    }
    // destructor在这里行动
}

在这个例子中,point的destructor必须在 switch 指令四个出口的 return 操作前被生成出来,另外也很可能在这个区段的结束符号(右大括号)之前被生成出来——即使程序的分析的结果发现绝对不会进行到那里.

同样的道理,goto指令也可能需要许多个destructor调用操作.例如下面的程序片段:

{
    if (cache2)
        // 检查cache;如果吻合就传回1
        return 1;
    Point xx;
    // xx的constructor 在这里行动
    while (cvs.iter(xx))
        if (xx == value)
            goto found;
    // xx的destructor 在这里行动
    return 0;

found:
    // cache item
    // xx的destructor 在这里行动
    return 1;
}

    Destructor调用操作必须放在最后两个 return 指令之前,但是却不必被放在最初的 return 之前,因为那时object尚未被定义出来.

一般而言会把object尽可能放置在使用它的那个程序区段附近,这样做可以节省不必要的对象产生操作和销毁操作.以本例而言,如果在检查cache之前就定义了Point object,那就不够理想,这个道理似乎非常明显,但许多Pascal或C程序员使用C++的时候,仍然习惯把所有的objects放在函数或某个区段的起始处.

全局对象 (Global Objects)

如果有以下程序片段:

Matrix identity;
main() {
    // identity 必须在此处被初始化
    Matrix m1 = identity;
    ...
    return 0;
}

C++保证,一定会在main()函数中第一次用到identity之前,把identity构造出来,而在main()函数结束之前把identity销毁.像identity这样的所谓的 global object 如果有constructor或destructor的话,它需要静态的初始化操作和内存释放操作.

C++程序中所有的global objects都被放置在程序的data segment中.如果明确指定给它一个值,object将以该值为初值.否则object所配置到的内存内容为0.因此在下面这段码中:

int v1 = 1024;
int v2;

v1和v2都被配置于程序的data segment,v1值为1024,v2值为0(这和C略有不同,C并不自动设定初值).在C语言中一个global object只能够被一个常量表达式(可在编译时期求值的那种)设定初值.当然,constructor并不是常量表达式.虽然 class object在编译时期可以被放置于data segment中并且内容为0,但constructor一直要到程序激活(startup)时才会实施.必须对一个"放置于program data segment中的object的初始化表达式"做评估,这正是为什么一个object需要静态初始化的原因.

当cfront还是唯一的C++编译器,而且跨平台移植性比效率的考虑更重要的时候,有一个可移植但成本颇高的静态初始化(以及内存释放)方法.cfront的束缚是,它的解决方案必须在每一个UNIX平台上都有效.因此不论是相关的linker或object-file format,都不能预先做任何假设.由于有这样的限制,下面的策略就浮现出来:

1.为每一个需要静态初始化的档案产生一个_sti()函数,内带必要的constructor调用操作或 inline expansions.例如前面所说的identity对象会在matrix.c中产生出下面的_sti()函数(sti可能是 static initialization的缩写):

__sti__matrix_C__identity() {
    identity.Matrix::Matrix();        // 这就是 static initialization
}

其中matrix_c是文件名编码,_identity表示文件中所定义的第一个 static object.在__sti之后附加上这两个名称,可以为可执行文件提供一个独一无二的识别符号.

2.类似情况,在每一个需要静态的内存释放操作(static deallocation)的文件组宏,产生出一个__std()函数(std可能是 static deallocation的缩写),内带必要的destructor调用操作,或是其 inline expansions.在例子中会有一个__std()函数被产生出来,针对identity对象调用Matrix destructor.

3.提供一组runtime library "munch"函数:一个_main()函数(用以调用可执行文件中的所有的__sti()函数),以及一个exit()函数(以类似方式调用所有的__std()函数).

如下图所示(直接摘录书中p244原图):

cfront在程序中插入一个_main()函数调用操作,main()函数的第一个指令.这里的exit()和C library的exit()不同,为了链接前者,在cfront的CC命令中必须先指定C++ standard library.

最后一个需要解决的问题是,如何收集一个程序中各个object files的__sti()函数和__std()函数.它必须是可移植的.在当时,cfront(也代表C++)若要成功地流行于UNIX各平台,必须依靠它.

解决方法是使用nm命令.nm会倾印出object file的符号表格项目(symbol table entries).一个可执行文件系统由.o文件产生出来,nm将施行于可执行文件身上,其输出被导入munch程序中.munch程序会搜寻以__sti或__std开头的名称,然而把函数名称加到一个sti()函数和std()函数的跳转表格(jump table)中,接下来它把这个表格写到一个小的program text文件中,将这个内含表格的文件加以编译,然后整个可执行文件被重新链接,_main()和exit()于是在各个表格上访问一遍,轮流调用每一个项目(代表一个函数地址).

局部静态对象 (Local Static Objects)

假设有以下程序片段:

const Matrix &identity() {
    static Matrix mat_identity;
    // ...
    return mat_identity;
}

Local static class object保证了什么样的语意?

mat_identity的constructor必须只能施行一次,虽然上述函数可能会被调用多次.

mat_identity的destructor必须只能施行一次,虽然上述函数可能会被调用多次.

编译器的策略之一就是,无条件地在程序起始(startup)时构造出对象,然而这会导致所有的 local static class objects都在程序起始时被初始化,即使它们所在的那个函数从不曾被调用过.因此,只在identity()被调用时才把mat_identity构造出来,是比较好的做法.

首先导入一个临时性对象以保护mat_identity的初始化操作.第一次处理identity()时,这个临时对象被评估为 false,于是constructor会被调用,然后临时对象被改为 true.这样就解决了构造的问题,而在相反的那一端,destructor也需要有条件地施行与mat_identity身上,但只有在mat_identity已经被构造起来时才算数.要判断mat_identity是否被构造出来,很简单,如果那个临时对象为 true,就表示构造好了.困难的是,由于cfront产生C码,mat_identity对函数而言仍然是local,因此没办法在静态的内存释放函数(static
deallocation function)中存取它.解决的方式是:取出local object的地址(由于object是 static,其地址在downstream component中将转换到程序内用来放置global object的data segment中),下面是cfront的输出:

// 被产生出来的临时对象,作为戒护之用
static struct Matrix *__0__F3 = 0;
// C++的reference在C中以pointer来代替
// identity()的名称会被mangled
struct Matrix *identity_Fv() {
    static struct Matrix __lmat_identity;
    // 如果临时性的保护对象已经被设立,就什么也不做,否则
    // a.调用constructor:__ct__6MatrixFv
    // b.设定保护对象,使它指向目标对象
    __0__F3 ? 0 (__ct__6MatrixFv(&__lmat_identity), (__0__F3 = (&__lmat__identity)));
}

最后,destructor必须在"与 text program file有关联的静态内存释放函数(static deallocation function)"中被有条件地调用.

对象数组 (Array of Objects)

假设有下列的数组定义:

Point knots[10];

需要完成什么呢?如果Point既没有定义一个constructor也没有定义一个destructor,那么工作不会比建立一个"内建(build-in)类型所组成的数组"更多,也就是说,只需配置足够的内存以储存10个连续的Point元素.

然而Point的确定义了一个 default destructor,所以这个destructor必须轮流施行于每一个元素上.一般而言这是经由一个或多个runtime library函数达成.在cfront中,使用一个被命名为vec_new()的函数,产生出以 class objects 构造而成的数组.新近的编译器则提供两个函数一个用来处理"没有virtual base class"的 class,另一个用来处理"内带virtual base class"的 class.后一个函数通常被称为vec_vnect.函数类型通常如下:

void *vec_new() {
    void *array,        // 数组起始地址
    size_t elem_size,    // 每一个class object的大小
    int elem_count;        // 数组中的元素数目
    void (*constructor)(void *),
    void (*destruction)(void *, char)
}

    其中constructor和destructor参数是这个 class 的default construct和default destructor的函数指针.

参数array带有的若不是具名数组(本例为knots)的地址,就是0.如果是0,那么数组将经由应用程序的 new 运算符,被动态配置于heap中.

参数elem_size表示数组中的元素大小(书上翻译为元素数目可能有误).在vec_new中,constructor施行于elem_count个元素上.对于支持exception handling的编译器而言,destructor的提供是必要的.下面是编译器可能针对10个Point元素所做的vec_new()调用操作:

Point knots[10];
vec_new(&knots, sizeof(Point), 10, &Point::Point, 0);

如果Point也定义了一个destructor,当knots的生命结束时,该destructor也必须施行于那10个Point元素上.这是一个经由一个类似的vec_delete()的runtime library函数完成的,其函数类型如下:

void *vec_delete{
    void *array,        // 数组起始地址
    size_t elem_size,    // 每一个class object的大小
    int elem_count,        // 数组中的元素数目
    void (*destructor)(void *, char)
}

有些编译器会另外增加一些参数,用以传递其他数值,以便能有条件地导引vec_delete()的逻辑,在vec_delete()中,destructor被施行于elem_count个元素上.

如果程序员提供一个或多个明显初值给一个由 class objects组成的数组,像下面这样,会如何?

Point knots[10] = {
    Point,
    Point(1.0, 1.0, 0.5),
    -1.0
};

对于那些明显获得初值的元素,vec_new不再有必要.对于那些尚未被初始化的元素,vec_new()的施行方式就像面对"由class elements组成的数组,而该数组没有explicit initialization list"一样,因此上一个定义很可能被转换为:

Point knots[10];
// 明确地初始化前3个元素
Point::Point(&knots[0]);
Point::Point(&knots[1], 1.0, 1.0, 0.5);
Point::Point(&knots[2], -1.0, 0.0, 0.0);
// 以vec_new初始化后7个元素
vec_new(&knots+3, sizeof(Point), 7, &Point::Point, 0);

版权声明:本文为博主原创文章,未经博主允许不得转载。

时间: 2024-10-12 14:24:35

C++对象模型——对象的构造和解构(第六章)的相关文章

对象的构造顺序(十六)

在 C++ 中的类可以定义多个对象,那么对象构造的顺序是怎样的呢?对于局部对象:当程序执行流到达对象的定义语句时进行构造.我们以代码为例进行分析 #include <stdio.h> class Test { private:     int mi; public:     Test(int i)     {         mi = i;                  printf("Test(int i): %d\n", mi);     }          Te

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

第19课 - 对象的构造(下)

第19课 - 对象的构造(下) 1. 特殊的构造函数 (1)无参构造函数 当类中没有定义构造函数时,编译器默认提供一个无参构造函数,并且其函数体为空. (2)拷贝构造函数 当类中没有定义拷贝构造函数时,编译器默认提供一个拷贝构造函数,简单的进行成员变量的值复制. 1 #include <stdio.h> 2 3 class Test 4 { 5 private: 6 int i; 7 int j; 8 public: 9 int getI() 10 { 11 return i; 12 } 13

第17课 - 对象的构造

第17课 - 对象的构造(上) 0. 问题 对象中成员变量的初始值是什么? 下面的类定义中成员变量 i 和 j 的初始值是什么?  对象定义在 全局空间.栈上.堆上,具有不同的属性. 1 #include <stdio.h> 2 3 class Test 4 { 5 private: 6 int i; 7 int j; 8 public: 9 int getI() { return i; } // 类成员函数,直接访问 10 int getJ() { return j; } 11 }; 12

第21课 对象的构造顺序

1. 对象的构造顺序 (1)对于局部对象:当程序执行流到达对象的定义语句时进行构造 [实例分析]局部对象的构造顺序 (2)对于堆对象 ①当程序执行流到达new语句时创建对象 ②使用new创建对象将自动触发构造函数的调用 [编程实验]堆对象的构造顺序 (2)对于全局对象 ①对象的构造顺序是不确定的 ②不同的编译器使用不同的规则确定构造顺序 [实例分析]全局对象的构造顺序 2.小结 (1)局部对象的构造顺序依赖于程序的执行流 (2)堆对象的构造顺序依赖于new的使用顺序 (3)全局对象的构造顺序是不

【OC学习-28】自定义对象的归档和解归档:例子说明以及简单总结

对对象进行归档,和对对象进行拷贝差不多路数,也就是需要协议,之前的时对NSString和NSNumber这些对象为什么没有归档协议?因为它们自带了. 所以如果我们要对自定义的对象(而不是Foundation里默认的对象)进行归档的话就需要引入归档协议<NSCoding>. 举例:声明一个User类,然后创建一个对象user1,把这个user1进行归档,然后把归档后的文件解归档到user2中,看看是否是原先的那些数据. (1)User.h #import <Foundation/Found

C和C++的面向对象专题(7)——单例模式解决静态成员对象和全局对象的构造顺序难题

本专栏文章列表 一.何为面向对象 二.C语言也能实现面向对象 三.C++中的不优雅特性 四.解决封装,避免接口 五.合理使用模板,避免代码冗余 六.C++也能反射 七.单例模式解决静态成员对象和全局对象的构造顺序难题 八.更为高级的预处理器PHP 七.单例模式解决静态成员对象和全局对象的构造顺序难题 上回书说道,我们的程序有一个隐藏的漏洞,如果ClassRegister这个类所在的.o文件,如果在所有.o文件中是第一个被链接的的,那么就不会出问题. 这么说太抽象了,让我们画个图表 ClassRe

android NDK 实用学习(三)- java端类对象的构造及使用

1,读此文章前我假设你已经读过: android NDK 实用学习-获取java端类及其类变量 android NDK 实用学习-java端对象成员赋值和获取对象成员值 2,java端类对象的构造: ①首先获取类; ②获取类的无参构造函数: jmethodID initID = env->GetMethodID(jcSetDataMgr, "<init>", "()V"); ③构造类实例: jobject jresult = env->New

对象的构造(十四)

我们在 C 语言中,每个变量都有其初始值.那么问题来了,对象中成员变量的初始值是多少呢?从设计的角度来看,对象只是变量,因此:在栈上创建对象时,成员变量初始为随机值:在堆上创建对象时,成员变量初始为随机值:在静态存储区创建对象时,成员变量初识为 0 值. 下来我们以代码为例进行验证,代码如下 #include <stdio.h> class Test { private:     int i;     int j; public:     int getI() { return i; }