执行期语义学(二)
new 和 delete 运算符
运算符new
看起来是一个单一运算符,比如:
int *pi = new int(5);
但是事实上它是由两个步骤完成的
-
通过适当的
new
运算符函数实例,配置所需要的内存。// 调用函数库中的new运算符 int *pi = __new( sizeof( int ) );
-
将配置得来的对象设立初值:
*pi = 5;
更进一步地说,初始化操作是在内存配置成功后才执行:
int *pi;
if ( pi = __new( sizeof ( int ) ))
*pi = 5
delete
运算符的情况类似
delete pi;
当写下上面这句代码时,如果pi
的值是0,C++语言会要求delete
运算符不要有操作。因此编译器必须为此调用一层保护膜:
if ( pi != 0 )
__delete( pi );
我们需要注意的是,在我们将以指针指向的对象delete
之后,我们需要显式将这个指针置为空指针,避免产生野指针,比如:
auto p = new Class();
delete p;
// 这里p指向的对象,可以称为一个匿名对象吧,
// 它已经被释放了,但是p依然指向那块原先
// 匿名对象存在的地址,所以,如果我们对这时候
// 使用这个指针就会产生未定义行为
// 所以,我们需要这么做
p = nullptr;
我们在看看一个class object 吧
Point3d *origin = new Point3d;
// 被转换为
Point3d *origin;
if (origin = __new( sizeof( Point3d ) ) )
origin = Point3d::Point3d(origin);
// 如果加上异常捕获
if ( origin = __new( sizeof( Point3d ) ) ) {
try {
origin = Point3d::Point3d( origin );
}
catch( ... ) {
// 调用 delete library function
// 以释放因 new 而分配的内存
__delete( origin );
// 将原来的 execption 上传
throw;
}
}
而 destructor 的应用也极为类似
delete origin;
// 最终会变为
if ( origin != 0 ) {
Point3d::~Point3d( origin );
__delete( origin );
}
如果在考虑异常捕获的情况下,我们需要将destructor
放在一个try
区段中。异常处理会调用delete
运算符,然后再一次抛出异常。
一般的library
对于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_handler
应该是一个构造函数或者对分配的内存进行初始化的函数之类的。C++语言要求每一次对new
的调用都必须传回一个独一无二的指针。解决这个问题的传统方法是传回一个指针,指向一个默认为一个字节的内存区块(这就是为什么程序代码中size
被设置为1的原因)。
delete
的实现也一般是通过标准的C free()来完成
extern void operator delete( void *ptr)
{
if (ptr)
free( (char*)ptr );
}
还有一个点需要我们注意:**一般来说C++的内存管理会将内存分为五个区:常量区,静态(全局)区,栈区,堆区,自由存储区。**一般而言调用malloc()
和free()
是对堆区进行操作,而调用new
和delete
是对自由存储区进行操作,可以这么理解,自由存储区是C++从堆中抽象出来的一个新的区。
针对数组的 new 语意
我们看看下面的两个初始化
int *p_array = new int[5];
// stryct simple_aggr { float f1, f2; };
simple_aggr *p_aggr = new simple_aggr[5];
在上面这两个情况下,vec_new()
并不会被真正调用,因为一个int
和一个struct
但是并没有提供显式的构造函数,所以,对于编译器来说只需要提供一个new
和delete
就足够了,为其分配足够大小的空间。但是如果我们定义了一个default constructor,某些版本的vec_new()
就会被调用,向下面这样。
Point3d *p_array = new Point3d[10];
// 通常会被编译为
Point3d *p_array;
p_array = vec_new(0, sizeof( Point3d ), 10,
&Point3d::Point3d,
&Point3d::~Point3d);
如果在考虑异常处理的情况下,当一个异常出现时我们需要将前面申请的内存都释放掉。
在C++2.0版本之前,将数组的真正大小提供给程序员的delete
运算符,是程序员的责任。这就意味着当我们写下
int array_size = 10;
Point3d *p_array = new Point3d[array_size]);
delete [array_size] p_array;
// 当前版本
delete[] p_array;
Placement Operator new 的语意
有一个预先定义好的重载的(overloaded)new 运算符,称为 placement operator new。它需要第二个参数,类型为void*
。调用方式如下:
Point2w *p2w = new (arena) Point2w;
// 其中arena指向一个内存中的区块,这个区块用来存放新产生出来的Point2w p2w object
void* operator new(size_t, void* p)
{
return p;
}
上面所展示的只是操作发生的一半而已,而操作的另一半编译器会帮我们拓展。
Point2w *ptw = ( Point2w* )arena;
if (ptw != 0 )
ptw->Point2s::Point2w();
上面这段代码决定了objects被放置在哪里;编译系统保证object的constructor会实施于其上。就是编译器可以保证这个对象被正确的初始化。
现在我们考虑另一种情况:如果这个arena
是一个全局性定义,
void test()
{
Point2w *p2w = new (arena) Point2w;
p2w = new (arena) Point2w;
}
上面这种情况下,会同时对同一块内存进行两次构造,那么编译器如何保证这两个操作的正确执行呢?
一种方式是先调用delete
再来new
Point2w *p2w = new (arena) Point2w;
delete p2w;
p2w = new (arena) Point2w;
但是这样又显得有点多余了,因为下一个操作也是针对同一块内存,我们又要再分配一次,new
和malloc
是一种耗时操作,一般能减少就减少。所以我们一般这样做。
Point2w *p2w = new (arena) Point2w;
p2w->~Point2w();
p2w = new (arena) Point2w;
现在问题又来了,我们怎么直到这个arena
是否已经被初始化了呢?这个问题书上并没有给出答案,这里说一下我的观点:我觉得编译器应该会在new的时候做一个标记,在下次new的时候检查这个标记判断是否需要调用析构函数,比如
is_init = false;
// 现在已经分配好内存了
// 这是第一次调用
Point2w *ptw = (Point2w*)arean;
if (is_init) ptw->~Point2w(); // is_init -> false
ptw->Point2w();
is_init = true;
// 第二次调用
Point2w *ptw = (Point2w*)arean;
if (is_init) ptw->~Point2w();
ptw->Point2w();
is_init = true;
ok,现在我们再来看看另外一个问题:**arena
所表现的真正指针类型。**C++标准说它必须指向相同类型的class,要不就是一块”新鲜的内存“,并且需要足够容纳该类型的object。
也就是说我们可以这样,我写过的一个内存池就是用这种方法来分配定长的内存块来避免内存碎片化,虽然可能会有点内存的浪费。
char *arena = new char[ sizeof(Point2w) ];
Point2w *arena = new Point2w;
一般而言,placement new operator
并不支持多态。
作者在最后提出了一个问题
struct Base { int j; virtual void f(); }
struct Derived : Base { void f(); }
void fooBar() {
Base b;
b.f(); // Base::f()
b.~Base();
new (&b) Derived;
b.f(); // 这个f()是属于那个的
}
答案是Dervied::f()
,哈哈,作者说这是引入new重载之后的已给最隐晦的问题,大家重载的时候需要注意一下。