一,new与delete的细节
假如有下面的代码:
string *sp = new string("a value"); string *arr = new string[10];
整个new的语句可以分为三步完成,
首先,编译器使用名为operator new(operator new[])的标准库函数,该函数的作用是分配一块足够大的、原始的空间。第二步,编译器运行相应的构造函数构造对象,第三步,编译器将得到的对象首地址返回。
当在程序中使用 type *p = new type(..)时,编译器按照两条原则寻找对应的operator:
如果Type是用户自定义类型,优先在类以及父类的成员函数中寻找operator new函数,如果没有,则在全局函数中寻找用户的自定义版本,再没有则使用标准库的版本
如通用户定义了和标准库一样的函数,不会报重定义
标准库定义了八个可供用户重载在版本,其中nothrow_t定义在头文件new中,用于表示不抛出异常
void *operator new (size_t); void *operator new[] (size_t); void operator delete (void *) noexcept; void operator delete[] (void *) noexcept; void *operator new (size_t, std::nothrow_t) noexcept; void *operator new[] (size_t, std::nothrow_t) noexcept; void operator delete(void*, std::nothrow_t) noexcept; void operator delete[](void *, std::nothrow_t) noexcept;
用户可以自定义上面的几个函数(注意是自定义,不是重载,参数类型、个数只要满足要求即可,即使与标准库定义的一样也不会报错),这些要求包括:
1, 不能定义这样的new :void *operator new(size_t. void *), 他只能被标准库使用
2,new必须返回void*, 第一个参数必须是size_t,且不能有默认参数
3,delete必须返回void, 第一个参数必须是void *
4,当我们在类中定义operator delete时,若将第二个参数置为size_t, 那该形参的初始值就会变成第一个形参所指物的大小,如果这个类有一个虚析构函数,那么size_t的大小和operator delete的版本将有具体的动态类型决定,虽然我也不太懂,暂且先记下来- -!
二、定位new(placement new)
上面提到的operator new()函数会分配空间,虽然我们可以显式调用对象的析构函数,但是却没有办法调用对象的构造函数,此时需要用到一个叫做定位new的函数,该函数只接受一个void *参数,之后会在该指针所指的位置,调用构造函数,构造出一个对象。具体的各种形式如下:
new (place_addr) type;
new (place_addr) type (initilizers);
new(place_addr) type [size];
new(place_addr) type[size] (initilizers);
值得注意的是,定位new并不规定这个构造对象的空间是什么空间,可以是标准库的operator new出来的,可以是用户自己的operator new出来的,可以是malloc出来的,甚至可以是栈中的自动变量,如数组- -!,随便是啥都可以。
三、总结
虽然这些东西平时不常用,但是在需要自己精确控制对象的空间分配以及初始化行为时,仅仅从析构函数与构造函数是不够的,这些技术在STL中的内存分配器中就被广泛使用。