C++对象模型——new 和 delete 运算符(第六章)

6.2    new 和 delete 运算符

    运算符 new 的使用,看起来似乎是个单一运算,像这样:
int *pi = new int(5);
    但事实上它是由以下两个步骤完成:
    1.通过适当的 new 运算符函数实体,配置所需的内存:
// 调用函数库中的new运算符
int *pi = __new(sizeof(int));
    2.给配置得来的对象设立初值:
*pi = 5;
    更进一步地,初始化操作应该在内存配置成功(经由 new 运算符)后才执行:
// new运算符的两个分离步骤
// given: int *pi = new int(5);
// 重写声明
int *pi;
if (pi = __new(sizeof(int)))
    *pi = 5;
    delete 运算符的情况类似,当程序员写下:
delete pi;
    时,如果pi的值是0,C++语言会要求 delete 运算符不要有操作.因此,编译器必须为此调用构造一层保护膜:
if (pi != 0)
    __delete(pi);
    请注意pi并不会因此被自动清除为0,因此像这样的后继行为:
// 没有良好的定义,但是合法
if (pi && *pi == 5)
    ...
    虽然没有良好的定义,但是可能(也可能不)被评估为真.这是因为对于pi所指向的内存的变更或再使用,可能(也可能不)会发生.
     pi所指对象的生命会因 delete 而结束,所以后继任何对pi的参考操作就不再保证有良好的行为,并因此被视为是一种不好的程序风格.然而,把pi继续当做一个指针来用,仍然是可以的(虽然其使用受到限制),例如:
// pi仍然指向合法空间
// 甚至即使储存于其中的object已经不再合法
if (pi == sentine1)
    ...
    在这里, 使用指针pi和使用pi所指的对象,其差别在于哪一个的声明已经结束了.虽然 该地址上的对象不再合法,但地址本身却仍然代表一个合法的程序空间.因此pi能够继续被使用,但只能在受限制的情况下,很像一个 void * 指针的情况.
    以constructor来配置一个 class object,情况类似,例如:
Point3d *origin = new Point3d;
    被转换为:
Point3d *origin;
if (origin = __new(sizeof(Point3d)))
    origin = Point3d::Point3d(origin);
    如果exception handling的情况下,destructor应该被放在一个try区段中.exception handler会调用 delete 运算符,然后再一次丢出该exception.
    一般的lirary对于 new 运算符的实现操作都是很直接了当,但有两个精巧之处值得斟酌:
extern void *operator new(size_t size) {
    if (size == 0)
        size = 1;
    void *last_alloc;
    while (!(last_alloc = malloc(size))) {
        if (_new_handler)
            (*_new_handler)();
        else
            return 0;
    }
    return last_alloc;
}
    虽然这样写是合法的:
new T[0];
    但是 语言要求每一次对 new 的调用都必须传回一个独一无二的指针.解决该问题的传统方法是 传回一个指针,指向一个默认为1 byte的内存区块(这就是为什么程序代码中的size被设为1的原因).这个实现技术的另一个有趣之处是,它允许使用者提供一个属于自己的_new_handler()函数.这正是为什么每一次循环都调用_new_handler()的缘故.
    new 运算符实际上总是以标准C malloc()完成,虽然并没有规定一定这么做不可.相同的情况,delete 运算符也总是以标准的C free()完成:
extern void operator delete(void *ptr) {
    if (ptr)
        free((char *)ptr);
}

针对数组的 new 语意

    当这么写:
int *p_array = new int[5];
    时,vec_new()不会真正被调用,因为它的主要功能是把default constructor施行于 class objects所组成的数组的每一个元素上.倒是 new 运算符函数会被调用:
int *p_array = (int *)__new(5 * sizeof(int));
    相同的情况,如果写:
// struct simple_aggr {float f1, f2; };
simple_aggr *p_aggr = new simple_aggr[5];
    vec_new()也不会被调用.为什么呢?因为 simple_aggr并没有定义一个constructor或destructor,所以配置数组以及清除p_aggr数组的操作,只是单纯地获得内存和释放内存而已.这些操作由 new 和 delete 运算符来完成就绰绰有余了.
    然而如果 class 定义有一个default constructor,某些版本的vec_new()就会被调用,配置并构造 class objects所组成的数组,例如这个算式:
Point3d *p_array = new Point3d[10];
    通常会被编译为:
Point3d *p_array;
p_array = vec_new(0, sizeof(Point3d), 10, &Point3d::Point3d, &Point3d::~Point3d);
    在个别的数组元素构造过程中,如果发生exception,destructor就会被传递给vec_new().只有已经构造妥当的元素才需要destructor的施行,因为它们的内存已经被配置出来了,vec_new()有责任在exception发生的时候把那些内存释放掉.
    在 delete 时不需要指定数组元素的数目,如下所示:
delete []p_array;
    寻找数组维度给 delete 运算符的效率带来极大的影响,所以才导致这样的妥协: 只有在中括号出现时,编译器才寻找数组的维度,否则它便假设只有单独一个object要被删除.如果程序员没有提供必须的中括号,像这样:
delete p_array;
    那么就 只有第一个元素会被解构,其他元素仍然存在.
    应该如何记录元素数目?一个明显的方法就是为vec_new()所传回的每一个内存区块配置一个额外的word,然后 把元素数目包藏在那个word中.通常这种 被包藏的数值称为所谓的cookie.然而 Sun编译器却维护一个"联合数组",放置指针以及大小.它也把destructor的地址维护于此数组中.
    cookie策略有一个普遍引起忧虑的话题,那就是如果一个坏指针被交给delete_vec(),取出来的cookie自然是不合法的.一个不合法的元素数目和一个坏的起始地址,会导致destructor以非预期的次数被施行于一段非预期的区域,然而在"联合数组"的政策下,坏指针的可能结果就只是取出错误的元素数目而已.
    在原始编译器中,有两个主要函数用来储存和取出所谓的cookie:
// array_key是新数组的地址
// mustn't either be 0 or already entered elem_count is the count; it may be 0
typedef void *pv;
extern int__insert_new_array(PV array_key, int elem_count);
// 从表格中取出(并去除)array_key
// 若不是传回elem_count,就是传回-1
extern int __remove_old_array(PV array_key);
    下面是cfront中的vec_new()原始内容经过修润后的一份呈现,并附加批注:
PV __vec_new(PV ptr_array, int elem_count, int size, PV construct) {
    // 如果ptr_array是0,从heap中配置数组
    // 如果ptr_array不是0,表示程序员写的是:
    // T array[count]
    // 或
    // new (ptr_array) T[10];
    int alloc = 0;
    int array_sz = elem_count * size;
    if (alloc = ptr_array == 0)
        // 全局运算符new
        ptr_array = PV(new char[array_sz]);
    // 在exception handling下,将丢出exception bad_alloc
    if (ptr_array == )
        return 0;
    // 把数组元素数目放到cache中
    int status = __insert_new_array(ptr_array, elem_count);
    if (status == -1) {
        // 在exception handling下将丢出exception,将丢出exception bad_alloc
        if (alloc)
            delete ptr_array;
        return 0;
    }
    if (construct) {
        register char *elem = (char *)ptr_array;
        register char *lim = elem + array_sz;
        // PF是一个typedef,代表一个函数指针
        register PF fp = PF(constructor);
        while (elem < lim) {
            // 通过fp调用constructor作用于"this"元素上(由elem指出)
            (*fp)((void *)elem);
            // 前进到下一个元素
            elem += size;
        }
    }
    return PV(ptr_array);
}
    vec_delete()的操作差不多,但其行为并不总是C++程序员所预期或需求的.例如,已知下面两个 class 声明:
class Point {
public:
    Point();
    virtual ~Point();
};
class Point3d : public Point {
public:
    Point3d();
    virtual ~Point3d();
};
    如果配置一个数组,内带10个Point3d objects,会预期Point和Point3d的constructor被调用各10次,每次作用于数组中的一个元素:
Point *ptr = new Point3d[10];
    当 delete "由ptr所指向的10个Point3d元素"时,会发生什么事情呢?(*** 这个问题确实没有想过***)很明显,需要虚拟机制的帮助,以获得预期的Point destructor和Point3d destructor各10次的互换(每一个作用于数组中的一个元素):
// 这并不是所需要的
// 只有Point::~Point被调用...
delete [] ptr;
    施行于数组上的destructor,是根据交给vec_delete()函数的"被删除的指针类型的destructor"--本例中正是Point destructor.这很明显并非所希望的.此外,每一个元素的大小也一并被传递过去.这就是vec_delete()如何迭代走过每一个数组元素的方式. 本例中被传递过去的是Point class object的大小而不是Point3d class object的大小.整个运行过程非常不幸地失败了,不只是因为执行起错误的destructor,而且自从第一个元素之后,该destructor即被施行于不正确的内存区块中(因为元素的大小不正确).
    程序员应该怎样做才好?最好就是避免以一个base class 指针指向一个derived class objects所组成的数组--如果derived class object比其base大的话.如果一定要这样写程序,解决之道在于程序员层面,而非语言层面:
for (int ix = 0; ix < elem_count; ++ix) {
    Point3d *p = &((Point3d*)ptr)[ix];
    delete p;
}
    基本上, 程序员必须迭代走过整个数组,把 delete 运算符施行于每一个元素上.以此方式,调用操作将是 virtual,因此,Point3d和Point的destructor都会施行于数组中的每一个objects上.

Placement Operator new 的语意

    有一个预先定义好的重载(overloaded)new 运算符,称为 placement operator new.它需要第二个参数,类型为 void *,调用方式如下:
Point2w *ptw = new(arena)Point2w;
    其中 arena指向内存中一个区块,用以放置新产生出来的Point2w object.这个预先定义好的placement operator new 只要将"获得的值(如arena)"所指向的地址传回即可:
void *operator new(size_t, void *p) {
    return p;
}
    Placement new operator所 扩充的另一半便是将Point2w constructor自动实施于arena所指的地址上:
Point2w *ptw = (Point2w *)arena;
if (ptw != 0)
    ptw->Point2w::Point2w();
    这正是使placement operator new 如此强大的原因.这一份代码决定objects被放置在哪里:编译系统保证object的constructor会施行于其上.
    然而却有一个轻微的不良行为,下面是这个问题的程序片段:
// 让arena成为全局性定义
void fooBar() {
    Point2w *p2w = new(arena)Point2w;
    // ...
    p2w = new(arena)Point2w;
}
    如果placement operator在原已存在的一个object上构造新的object.而该现有的object有一个destructor,这个destructor并不会被调用.调用该destructor的方法之一是将那个指针 delete 掉.不过 在此例中如果像下面这样做,绝对是个错误:
// 以下并不是实施destructor的正确方法
delete p2w;
p2w = new(arena)Point2w;
    是的, delete 运算符会发生作用,但它也会释放由p2w所指的内存,这是错误的,因为下一个指令就要用到p2w.因此, 应该明确地调用destructor并保留储存空间,以便再使用:
// 以下是实施destructor的正确方法
p2w->~Point2w;
p2w = new(arena)Point2w;
    ( Standard C++以一个placement operator delete 矫正了这个错误,它会对object实施destructor,但不释放内存,所以就不必再直接调用destructor了).
    剩下的唯一问题是一个设计问题:在例子中对placement operator的第一次调用,会将新object构造于原已存在的object上?还是会构造于新地址上?如果这样写:
Point2w *p2w = new(arena)Point2w;
    如何知道arena所指的这块区域是否需要先解构?这个问题在语言层面上并没有解答,一个合理的习俗是令执行 new 的这一端也要负责执行destructor的责任.

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值