第6章 执行期语意学

第6章 执行期语意学

6.1 对象的构造和析构

constructor和destructor的安排

{
    if (cache)
        // 检查cache; 如果温和就传回1
        return 1;
    Point point;
    // constructor在这里行动
    switch(int(point.x())) {
        case -1:
        // numble;
        // destructor在这里行动
        return;
        case 0:
        // numble;
        // destructor在这里行动
        return; 
        case 1:
        // numble;
        // destructor在这里行动
        default:
        // numble;
        // destructor在这里行动
        return;
    }
}

另外也很有可能在这个区段的结束符号(右大括号)之前被生出来, 即使程序分析的结构发现绝不会进行到那里, 一般而言会把object尽可能放置在使用它的那个程序区段附近, 这么做可以节省非必要的对象产生操作或摧毁操作

全局对象

Matrix identity;

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

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

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

我的理解是data segment(包括object)中的值全为0, 只是在main函数执行时, 设定了值(object 执行constructor操作)

当cfront还是唯一的C++编译器, 而且跨平台移植性比效率的考虑更重要的时候,有一个可移植但成本颇高的静态初始化(以及内存释放)方法, 称为munch策略:

  1. 为每一个需要静态初始化的文件产生一个_sti()函数, 内含必要的constructor调用操作或是inline expansions. 例如起前面所说的identify对象会在matrix.c中产生出下面的_sti()函数(可能是static initialization的缩写):
__sti__matrix_c__identity() {
    // C++伪码
    identify.Matrix::Matrix; // 这就是static initialization
}
  1. 在每一个需要静态的内存释放操作的文件中, 产生一个__std()函数(可能是static deallocation的缩写), 内含必要的destructor调用操作, 或是其inline expansions
  2. 提供一组runtime library "munch"函数: 一个_main()函数(用以调用可执行文件中的所有__sti()函数), 以及一个exit()函数(以类似方式调用所有的_std()函数)

cfront 2.0版之前并不支持nonclass object的静态初始化操作; 也就是说C语言的限制仍然残留着. 所以下面的每一个初始化操作都被标记为不合法:

extern int i;

// 全部都要求静态初始化(static initialization)
// 在2.0版之前的C和C++中, 这些都是不合法的
int j = i;
int *pi = new int(i);
double sal = compute_sal(get_employee(i));

使用被静态初始化的objects, 有下列缺点:

(1) 如果exception handling被支持, 那些objects将不能够被放置于try区段之内. 这对于被静态调用的constructors可能是特别无法接受的, 因为任何的throw操作将必然触发exception handling library默认的terminate()函数

(2) 为了控制"需要跨越模块做静态初始化"的objects的相依顺序, 而扯出来的复杂度

作者建议根本就不要用那些需要静态初始化的global objects(虽然这项建议几乎普遍不为C程序员所接收)

局部静态对象

const Matrix& identity() {
    static Matrix mat_identity;
    // ...
    return mat_identity;
}
  • mat_identity的constructor必须只能实施一次, 虽然上述函数可能被调用多次
  • mat_identify的destructor必须只能实施一次, 虽然上述函数可能会被调用多次

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

实际上identify()被调用时才把mat_identity构造起来是一种更好的做法, 现在的C+标准已经强制要求这一点

cfront的做法: 首先导入一个临时性对象以保护mat_identity的初始化操作. 第一次处理identify()时, 这个临时对象被评估为false, 于是constructor会被调用, 然后临时对象被改为true. 这样就解决了构造的问题. 而在相反的那一端, destructor也需要有条件地施行于mat_identity身上, 但只有mat_identity已经被构造起来才算数, 可以通过那个临时对象是否为true来判断mat_identity是否已经构造

对象数组

Point knots[10];    // 没有明显初值

如果Point没有定义一个constructor也没有定义一个destructor, 那么上面代码所执行的工作不会比"内建(build-in)类型所组成的数组"更多(即不会调用下面所要讲到的vec_new()), 也就是说我们只要配置足够内存以存在10个连续的Point元素即可

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

void* vec_new(
    void    *array,         // 数组起始地址
    size_t  elem_size;      // 每一个class object的大小
    int     elem_count;     // 数组中的元素个数
    void    (*constructor)(void *),
    void    (*destructor)(void *, char)
)
  • constructor是class的default constructor的函数指针
  • destructor是class的default destructor的函数指针
  • array持有的若不是具名数组(本例中为knots)的地址, 就是0. 如果是0, 那么数组将经由应用程序的new运算符, 被动态配置于heap中
  • 在vec_new()中, constructor施行于elem_cout个元素之上

下面是编译器可能对10个Point元素所做的vec_new()调用操作:

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

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

void* vec_delete(
    void    *array,
    size_t  elem_size,
    int     elem_count,
    void    (*destructor)(void *, char)
)

如果程序员提供一个或多个明显初值给一个由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];

// C++伪码

// 显示地初始化前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);

Default Constructors和数组

如果想要在程序中取出一个constructor的地址, 是不可以的. 当然, 这是在编译器支持vec_new()时该做的事情. 然而, 经由一个指针来启动constructor, 将无法(不被允许)存取default argument values

举个例子, 在cfront2.0之前, 声明一个由class objects所组成的数组, 意味着这个class必须没有声明constructs或一个default constructor(没有参数那种)----> 有还是没有一个default constructor(没有参数那种)???. 一个constructor不可以取一个或一个以上的默认参数值. 这违反直觉的, 会导致以下的大错

class complex {
    complex(double = 0.0, double = 0.0);
};

在当时的语言规则下, 此复数函数库的使用者没办法声明一个由complex class objects组成的数组.

我的理解是在2.0版本之前, 这样带有默认参数的构造函数无法区分无参构造函数

然而在2.0版, 修改了语言本身, 为支持句子:complex::complex(double = 0.0, double = 0.0), 当程序员写出complex c_array[10]时, 而编译器最终需要调用vec_new(&c_array, sizeof(complex), 10, &complex::complex, 0);, 默认的参数如何能够对vec_new()而言有用?

cfront所采用的方法是产生一个内部的stub construct, 没有参数. 在其函数内调用由程序员提供的constructor, 并将default参数值显式地指定过去(由于construct的地址已经被取得, 所以它不能够成为一个inline):

// 内部产生的stub constructor
// 用以支持数组的构造
complex::complex() {
    complex(0.0, 0.0);
}

编译器自己又一次违反了一个明显的语言规则: class如今支持了两个没有带参数的constructs. 当然, 只有class objects数组真正被产生出来时, stub实例才会被产生以及被使用

猜你喜欢

转载自www.cnblogs.com/hesper/p/10629644.html