您的位置:首页 > 编程语言 > C语言/C++

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

2015-08-16 23:13 525 查看

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的责任.
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: