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的责任.
相关文章推荐
- C/C++编译链接过程详解
- 归并排序的C++实现
- c语言中对字符串进行处理的函数
- C语言中二级指针的用处
- VS2010 C++编译报错LINK : fatal error LNK1123: 转换到 COFF 期间失败: 文件无效或损坏
- 冈萨雷斯数字图像处理中科院&电子科大halcon/C++/Opencv视频教程下载
- 函数指针C++和回调函数
- [LeetCode] Gas Station
- C++标准库介绍
- 快速傅里叶算法 C语言实现
- C++命名空间
- c++primer之try语句块和异常处理
- C++ 语法
- C#调用C++ Dll
- c++判断当前系统及编译器
- C++1001
- c 语言 逆波兰计算器 C语言程序设计中的例子
- c++继承与派生
- C++Template 读书笔记 第五章
- C/C++ -- 代码技巧及优化