C/C++编程:new表达式

1059 篇文章 290 订阅

作用

  • 在堆上申请并且初始化一个对象
  • 这些对象的生存期不受它们创建时所在的作用域限制。也就是说,有一个new就需要一个delete,否则会发生内存泄露

使用场合

  • 如果你想在堆上分配一个对象,应该用new操作符。它即分配对象又为对象调用构造函数。
  • 如果你仅仅想分配内存,就应调用operator new函数:它不会调用构造函数。
  • 如果你想定制自己的在堆对象被建立时的内存分配过程,你应该重载operator new数,然后使用new操作符,new操作符会调用你定义的operator new。
  • 如果你想在一块已经获得的指针的内存中建立一个对象,应该用placement new

如何申请的内存:分配函数&&释放函数

new 表达式通过调用 分配函数适当的分配存储

  • 若 类型 是非数组类型,则函数名是 operator new。
  • 若 类型 是数组类型,则函数名是 operator new[]。

解析:在调用分配函数时,new表达式将请求的字节数作为std::size_t类型的第一参数传递给它,该参数对于非数组T精确地为sizeof(T)。解释如下:

void *operator new(std::size_t);
void *operator new[](std::size_t);

void operator delete(void *);
void operator delete[](void *);

std::size_t是一个typedef,对应于合适的整形。比如:

int *pi = new int;

将会被转换为:

int *pi = new(sizeof(int));

而:

int *pa = new int[40];

将会被转换为:

int *pa = new(40 * sizeof(int));

当然,使用new的语句也可能会包含初始值,因此,使用new运算符时,可能不仅仅是调用new()函数。

同样:

delete pi;

将被转化为:

delete (pi);

注意:数组的分配中可能带有一个未指明的开销(overhead)

而且每次调用new的这个开销可能不同,除非选择的分配函数是标准非分配形式。new 表达式所返回的指针等于分配函数所返回的指针加上该值。

new T;      // 调用 operator new(sizeof(T))
            // (C++17) 或 operator new(sizeof(T), std::align_val_t(alignof(T))))
new T[5];   // 调用 operator new[](sizeof(T)*5 + overhead)
            // (C++17) 或 operator new(sizeof(T)*5+overhead, std::align_val_t(alignof(T))))
new(2,f) T; // 调用 operator new(sizeof(T), 2, f)
            // (C++17) 或 operator new(sizeof(T), std::align_val_t(alignof(T)), 2, f)

许多实现使用数组开销的存储数组中的对象数量,它为delete[]表达式所用,以进行正确数量的析构函数调用。

另外,若 new 被用于分配 char、unsigned char 或 std::byte (C++17 起)的数组,则它可能从分配函数请求额外内存,以此保证所有不大于请求数组大小的类型的对象,当将其放入所分配的数组中时能够正确对齐。

内存不足了,会发生什么?

new-handler与set_new_handler

当operator new无法满足某一内存分配需求时,它会抛出异常。在抛出异常之前,它会先调用一个客户指定的错误处理函数,一个所谓的new-handler。为了指定这个“用以处理内存不足”的函数,客户必须调用set_new_handler,这是声明于<new>的标准程序库函数:

namespace std{
	typedef void (*new_handler)();
	new_handler set_new_handler(new_handler p) throw();
};

如上,new_handler是个typedef,定义出一个指针指向函数,该函数没有返回值也不返回任何东西,set_new_handler是“获得一个new_handler并返回一个new_handler”的函数。set_new_handler声明式尾端的throw()是一份异常明细,表示该函数不抛出任何异常。

你可以这样使用set_new_handler:

#include <iostream>

void outOfMem(){
	std::cerr << "unable to satisfy request for memory\n";
	std::abort();
}

int main(){
	std::set_new_handler(outOfMem);
	int *p = new int[10000000000L];
}

当operator new无法满足内存申请时,它会不断调用new-handler函数,直到找到足够内存。
在这里插入图片描述

总结

  • set_new_handler允许客户指定一个函数,在内存分配无法获得满足时被调用

实现自己的new-handler

一个设计良好的new-handler函数可以有如下选择:

  • 让更多内存可被使用。这就造成operator new内的下一次内存分配动作可能成功。实现此策略的一个做法是,程序一开始就分配一大块内存,而后当new-handler第一次被调用,将它们还给程序使用
  • 安装另一个new-handler。如果目前这个new-handler无法取得更多可用内存,或许它知道另外哪个new-handler有此能力。这时,目前这个new-handler就可以安装另外那个new-handler以替换自己(只要掉用set_new_handler)。下次当operator new调用new-handler,调用的是最新安装的那个(另一个功能是让new-handler修改自己的行为,于是当它下次被调用,就会做些不同的事。为达到这个目的,做法之一是令new-handler修改“会影响new-handler”行为的static、namespace数据或者global数据。)
  • 卸除new-handler。也就是将null指针传给set_new_handler。一旦没有安装任何new-handler,operator new会在内存分配不成功时抛出异常。
  • 抛出bad_alloc(或者派生自bad_alloc)的异常。这样的异常不会被operator new捕捉,因此会被传播到内存索求处
  • 不返回,通常调用abort或者new

如果你希望class有自己的专属new-handler:令每一个class提供自己的set_new_handler和operator new即可。其中set_new_handler使客户得以指定class专属的new-handler,operator new则确保在分配class对象内存的过程中以class专属的new-handler替换global new-handler

看个例子:

// 资源管理类
class NewHandlerHolder{
public:
    explicit NewHandlerHolder(std::new_handler nh) :handler(nh){};
    ~NewHandlerHolder() {std::new_handler (handler);}
private:
    std::new_handler  handler;
    NewHandlerHolder(const NewHandlerHolder &);
    NewHandlerHolder& operator=(const NewHandlerHolder);
};
class Widget{
public:
    static std::new_handler  set_new_handler(std::new_handler p) throw();
    static void * operator new(std::size_t size) throw(std::bad_alloc);
private:
    static std::new_handler currentHandler;
};

std::new_handler Widget::currentHandler = 0;
std::new_handler Widget::set_new_handler(std::new_handler p) throw() {
    std::new_handler  oldHandler = currentHandler;
    currentHandler = p;
    return oldHandler;
}

void * Widget::operator new(std::size_t size) throw(std::bad_alloc){
    NewHandlerHolder h(std::set_new_handler(currentHandler));
    return ::operator new(size);
}

注:static成员必须在class定义式外被定义(除非它们是const而且是整型)

怎么使用:

void outOfMem();
Widget::set_new_handler(outOfMem);

Widget* pm1 = new Widget;  //如果内存分配失败,将调用outOfMem

Widget::set_new_handler(0);
Widget* pm1 = new Widget;    //如果内存分配失败, 将抛出异常

更好的方法是使用模板(可以被任何又需要的类使用):

// 资源管理类
class NewHandlerHolder{
public:
    explicit NewHandlerHolder(std::new_handler nh) :handler(nh){};
    ~NewHandlerHolder() {std::new_handler (handler);}
private:
    std::new_handler  handler;
    NewHandlerHolder(const NewHandlerHolder &);
    NewHandlerHolder& operator=(const NewHandlerHolder);
};

template<typename T>
class NewHandlerSupport{  //基类,用以支持class专属的set_new_handler;
public:
    static std::new_handler set_new_handler(std::new_handler p) throw();
    static void * operator new(std::size_t size) throw(std::bad_alloc);
    // ...   其他的operator new版本
private:
    static std::new_handler  currentHandler;
};

template<typename T>
std::new_handler  NewHandlerSupport<T>::set_new_handler(std::new_handler p) throw() {
    std::new_handler  oldHandler = currentHandler;
    currentHandler = p;
    return oldHandler;
}

template<typename T>
void * NewHandlerSupport<T>::operator new(std::size_t size) throw(std::bad_alloc) {
    NewHandlerHolder h(std::set_new_handler(currentHandler));
    return ::operator new(size);
}

//讲每一个currentHandler 初始化为null
template<typename T>
std::new_handler  NewHandlerSupport<T>::currentHandler = 0;

使用方法:继承自NewHandlerSupport即可,比如:

class Widget : public NewHandlerSupport<Widget>{
    
};

实例

针对POD类型

实例

//-------------int --------------------
int *pin = new int{};
int *pi = new int(6);
delete pin;
delete pi;

//-------------double --------------------

double  *pd = new double(99.99);
double  *pdn = new double{99.99};
delete pd;
delete pdn;

// ---------------struct---------
struct where{
    double  x;
    double  y;
    double  z;
};
where *one = new where{2.5, 5.3, 9.3};

语义

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的情况类似,对于:

delete pi;

时,如果pi的值是0,C++语言会要求delete运算符不要有操作。因此编译器必须为此调用构造一层保护膜:

if(pi != 0){
	__delete(pi);
}

请注意,pi并不会因此自动清除为0。

pi所指对象的生命会因为delete而结束。所以后继任何对pi的操作都不是良构的。但是我们还是可以把pi继续当作一个指针来用(虽然其使用受到限制),比如:

// ok : pi仍然指向合法空间,即使存储于其中的object已经不再合法
if(pi == sentinel)

在这里,使用指针pi和使用pi所指的对象,其差别在于哪一个的生命已经结束了。虽然该地址上的对象不再合法,但地址本身却仍然代表一个合法的程序空间,因此pi能够继续被使用,但是只能在受到限制的情况下,很像一个void*指针的情况

针对非POD类型

实例

以constructor来配置一个class object,情况类似。

Point3d *origin = new Point3d;
delete origin;

语义

new (编译器提供的,非重载的)

Point3d *origin = new Point3d;

被转换为:

Point3d *origin;
if(origin = __new(sizeof(Point3d))){
	origin = Point3d::Point3d (origin);
}

如果出现exception handling,那么准换结果可能会更复杂一点:

if(origin = __new(sizeof(Point3d))){
	try{
		origin = Point3d::Point3d(origin);
	}catch(...){
		// 调用delete library function以释放因new而配置的内存
		__delete(origin );
		// 将原来的exception上传
		throw; 
	}
}

在这里,如果以new运算符配置object,而其构造函数丢出一个exception,配置得来的内存就会被释放掉。然后exception再被丢出去(上传)

delete

delete origin;

会变成:

if(origin != 0){
	Point3d::~Point3d(origin);
	__delete(origin);
}

如果在exception handling的情况下,destructor应该被放在一个try区段中。exception handler会调用delete运算符,然后再一次丢出该exception。

针对数组的new语义(动态数组)

我们通常称使用new T[]形式分配的内存为动态数组

之所以叫动态数组,一是它的内存是动态分配的,还有就是它可以像数组一样使用。

实例

new一个动态数组(这是唯一直接创建大小在运行时定义的数组的方法)

POD类型
int *p = new int[9]{1, 2, 3, 4, 5, 6, 7, 8, 9};
 delete [] p;

int *ar = new int[4]{1, 2, 3, 4};


double* p = new double[]{1,2,3}; // 创建 double[3] 类型的数组

多维数组

  • 第一维可以是整数类型、枚举类型、拥有单个到整数或枚举类型的非 explicit 转换函数的类类型 (C++14 前)、任何能转换成 std::size_t 的表达式
  • 第一维之外的所有维都必须指定为正的整数常量表达式(C++14 前)类型为 std::size_t 的经转换的常量表达式 (C++14 起)
  • 第一维为零是可接受的,分配函数也会被调用。
  • 注意:std::vector 提供了与一维的动态数组类似的功能。
  • 常量表达式:定义能在编译时求值的表达式
int n = 42;
double a[n][5]; // 错误
auto p1 = new double[n][5];   // OK
auto p2 = new double[5][n];   // 错误:仅第一维可为非常量
auto p3 = new (double[n][5]); // 错误:语法 (1) 不能用于动态数组

### 特性

使用初始化列表初始化

可以像一般的数组一样,使用初始化列表对动态数组进行初始化,代码如下:

int *p = new int[9]{1, 2, 3, 4, 5, 6, 7, 8, 9};
  for(int i = 0; i < 9; i++){
      cout << p[i] << "\t";
  }
  delete [] p;

在这里插入图片描述

和数组的区别

动态数组和数组还是有一些区别,例如编译器无法从数组名获知数组的大小信息。下面的代码就不能正确动作:

int *p = new int[9]{1, 2, 3, 4, 5, 6, 7, 8, 9};
    for(int i = 0; i < sizeof(p)/ sizeof(p[0]); i++){
        cout << p[i] << "\t";
    }
    delete [] p;

同样地,C++11中引入的范围for也被不支持。下面的代码直接会发生编译错误:
在这里插入图片描述
但是这件事有一个例外,动态申请的对象数组在释放内存时,如果需要调用析构函数,编译器会在动态数组的前面保存动态数组的大小信息,比如下面的例子:

 struct demo{
        demo(){}
        void test(){
            cout << "test()\n";
        }
        ~demo(){
            cout << " ~demo\n";
        }
    };

在动态分配内存之后,可以像下面的代码一样取得数组的大小:

  demo *pd = new demo[10];
    int count = *((int *)pd - 1);
    for(int i = 0; i < count; i++){
        pd[i].test();
    }
    delete[] pd;

有一点值得注意的是,这个方法有可能依赖于编译器的实现,所以在作者不能保证在所有的编译器中都能正常动作。

原理

new

准备

vec_new():主要功能是把default constructor 施行的class objects所组成的数组的每一个元素身上。

情况一:调用普通的new和delete

普通类型

当遇到:

int *p_array = new int[5];

时,会调用new运算符:

int *p_array = (int *)__new(5 * sizeof(int));

未定义析构函数和构造函数的类

struct test {
	float f1, f2
};
test *p_aggr= new test[5];
delete[] p_aggr

因为test没有定义析构函数或者构造函数,这里的new只是单纯的获得内存和释放内存而已。这些操作由new和delete运算符来完成就绰绰有余了。并不会调用vec_new()

情况二:调用vec_new()
无继承

如果类定义有一个默认构造函数,某些版本的vec_new()就会被调用,配置并构造类对象所组成的数组。比如:

class Point3d{
	public:
		Point();
		~Point();
};

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发生的时候把那些内存释放掉。

有继承
class Point{
	public:
		Point();
		virtual ~Point();
};

class Point3d : public Point{
public:
	Point3d();
	virtual ~Point3d();
};

//这不是一个好主意
Point *ptr = new Point3d[10];

// 错误写法:只有Point::~Point()被调用,会发生内存谢了
delete [] ptr;
//正确写法
for(int ix = 0; ix < elem_count; ++ix){
	Point3d *p = &((Point3d*)ptr)[ix];
	delete p;
}

错误写法的原因:被传递过去的是Point class object的大小而不是Point3d class object的大小。所以,有两个错误:执行错误的析构函数,而且自从第一个元素之后,该析构函数被运行在不正确的内存区块中(因为元素的大小不对)

正确写法:最好是避免以一个基类指向一个派生类对象所组成的数组。如上实例

delete[]

delete[]需要析构掉先前申请的所有空间,很明显,需要直到之前申请了都是元素。

那那应该如何记录元素的数目?一个明显的方法是为vec_new()所传回的每一个内存区块配置一个额外的word,然后把元素数组包藏在那个word中。通常这种被包藏的数值称为cookie

问题: delete[] 与delete的区别

对于:

delete[] p_array;

delete p_array;

寻找数组维度会给delete运算符的效率带来很大的影响,所以:只有在[]出现时,编译期才寻找数组的维度,否则它就假设只有单独一个对象要被删除。也就是说,对于delete p_array,只有第一个元素才会被析构,其他的元素仍然存在。

其他:待整理

::(可选) new (布置参数)(可选) (类型) 初始化器(可选)	(1)	
::(可选) new (布置参数)(可选)  类型  初始化器(可选)	(2)	

1、类型不能包括()

new int(*[10])(); // 错误:分析成 (new int) (*[10]) ()
new (int (*[10])()); // OK:分配 10 个函数指针的数组

2、无括号的 类型 是贪心的:它将包含可以是声明符一部分的所有记号:

new int + 1; // OK:分析成 (new int) + 1,增加 new int 所返回的指针
new int * 1; // 错误:分析成 (new int*) (1)

3、


auto p = new auto('c');          // 创建单个 char 类型的对象。p 是一个 char*
 
auto q = new std::integral auto(1);         // OK: q 是一个 int*
auto q = new std::floating_point auto(true) // 错误:不满足类型约束
 
auto r = new std::pair(1, true); // OK: r 是一个 std::pair<int, bool>*
auto r = new std::vector;        // 错误:无法推导元素类型

重载operator new

一般library对new运算符的实现操作都很直截了当,但注意两点(此处不考虑exception handling):

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运算符实际上是用malloc完成的,同样的,delete用free()完成:
extern void operator delete(void *ptr){
	if(ptr)
		free((char *) ptr);
}

面试题

什么时候只能用new不能用malloc

如果要用到C++特性的话,请用new不要用malloc

class stream {
public:
    virtual int sys_read(void *buf, size_t size) = 0;
    int read_to_buffer( void *buf, size_t size){
        return sys_read(buf, size);
    }
};

class stream_file : public stream{
public:
    int sys_read(void *buf, size_t size){
        printf("-----------------");
    }
};

int main() {
    stream_file * streamFile;
   // streamFile = (stream_file *) calloc(1, sizeof(stream_file));// Process finished with exit code 139 (interrupted by signal 11: SIGSEGV)
    streamFile = new stream_file;
   streamFile->read_to_buffer(NULL,  0);

calloc分配时没有分配虚表,所以会段错误

什么时候你会想要重载operator new与operator delete

问题:为什么会想要替换编译器提供的operator new或者operator delete呢?

  • 用来检测运用上的错误。如果将“new所得内存”delete掉却不幸失败,会导致内存泄漏。如果在“new所得内存”上多次delete程序会崩溃。此外各种各样的编程错误可能导致数据"overruns"(写入点在分配区尾端之后)或者"underruns"(写入点在分配区起始点之前)。如果我们自行定义一个operator new,就可以超额分配内存,以额外空间(位于客户所得区块之前或之后)放置特定的byte patterns(即签名)。operator delete就可以检查上诉签名是否原封不动,如果改变了就表示分配区在某个生命时间段发生了orerrun或者underrun,这时候operator delete可以log这个事实和这个惹是生非的指针
  • 为了强化效能。编译器所带的operator new和operator delete主要用于一般目的,它们不但可以被长时间执行的程序接受,也可以被执行时间小于一秒的程序接受。它们必须处理一系列需求,包括大块内存、小块内存、大小混合型内存。它们必须接纳各种分配形态,范围从程序存活期间的少量区块动态分配,到大数量短命对象的持续分配和归还。它们必须考虑碎片问题,这最终会导致程序无法满足大区块要求… 现实存在这么多问题,因此编译器提供的operator new和operator delete只是一般化的,不会针对任何人有最佳表现。对某些应用程序而言,将编译器自带的new和delete替换为定制版本,可以效率更高,空间更少。
    • 为了增加分配和归还的速度
    • 为了降低缺省内存管理器带来的额外开销
  • 为了收集使用上的数据。对heap运用错误进行调试、收集heap使用信息等

看个例子。下面是个初阶段的global operator new,促进并协助检查“overruns”或者“underruns”

// 无法通过编译
static const int signature = 0xDEADBEFE;
typedef unsigned char Byte;

// 错误1:所有operator new都应该内含一个循环,反复调用每个new-handler函数
// 错误2:对齐
void * operator new(std::size_t size) throw(std::bad_alloc){
    using namespace std;
    size_t realSize = size + 2 * sizeof(int); // 增加大小,使得其能够塞入两个signature 

    void *pMem = malloc(realSize);
    if(!pMem){
        throw bad_alloc();
    }

	// 将signature 写入内存的最前和最后区域
    *(static_cast<int *>(pMem)) = signature; 
    *(reinterpret_cast<int *>(static_cast<Byte *>pMem)
       + realSize - sizeof(int)) = signature;

	// 返回值指针,指向第一个signature 之后的内存位置
    return static_cast<Byte *>(pMem) + sizeof(int);
}

有关对齐:C++要求所有的operator new返回的指针都有适当的对齐(取决于数据类型)。malloc就是在这样的要求下工作的,所以令operator new返回一个得自malloc的指针是安全的。然而上面的operator new返回的是一个得自malloc且偏移一个int大小的指针。没人能够保证它的安全!

构造

new表达式所创建的对象按照下列规则初始化:

  • 对于非数组的类型,在所得内存区域中构造单个对象
    • 如果没有初始化器,则对象被默认初始化
    • 如果初始化器是带括号的实参列表,则对象被直接初始化
    • 如果初始化器是花括号包围的实参列表,则对象被列表初始化(C++11起)
  • 如果类型是数组类型,则初始化一个数组的对象
    • 如果没有初始化器,则对象被默认初始化
    • 如果初始化器是一对空括号,则每个元素被值初始化
    • 若 初始化器 是花括号包围的实参列表,则数组被聚合初始化

如果初始化因抛出异常而终止(例如来自构造函数),则如果new表达式中已经分配了任何存储,则它将调用适当的解分配函数(也就是delete)

  • 对于非数组类型,调用operator delete
  • 对于数组类型,调用operator delete[]

若 new 表达式使用 ::new 语法,则在全局作用域查找解分配函数,否则若 T 是类类型,则在 T 的作用域查找。

  • 若失败的是常规(非布局)分配函数,则遵循 delete 表达式中所述的规则查找解分配函数。
  • 对于失败的布局new,与之匹配的解分配函数中除第一个外的所有形参的类型,必须与布置 new 的各形参类型相同。(???)对解分配函数的调用中,将先前从分配函数取得的值作为第一实参,将对齐作为可选的对齐参数, (C++17 起)并将
    布置参数(若存在)作为额外的布置参数。若找不到解分配函数,则不解分配内存。

new表达式创建的对象(也就是拥有动态存储期的对象),持续到将new表达式所返回的指针用于匹配的 delete 表达式之时。如果指针的原值丢失,则对象变为不可达而且无法解分配,也就是发生内存泄漏

  • 对指针赋值时可能发生:
int* p = new int(7); // 动态分配的 int 带值 7
p = nullptr; // 内存泄漏
  • 或指针离开作用域:
void f()
{
   int* p = new int(7);
} // 内存泄漏
  • 或因为异常
void f()
{
 int* p = new int(7);
 g();      // 可能抛出异常
 delete p; // 若无异常则 ok
} // 若 g() 抛出异常则内存泄漏

为简化动态分配的对象管理,通常将new表达式的结果存储在智能指针中,这些指针保证在上述情形中执行delete表达式

布局new

作用:

通常,new负责再堆中找到一个能够满足要求的内存卡。new运算符还有另一种变体,被称为定位new,它让您能够指定要使用的位置。可以使用这种设置其内存管理规程、处理需要通过特定地址进行访问的硬件或者再特定位置创建对象。

#include<new>
struct chaff{
	char dross[20];
	int slog;
};

char buffer1[50];
char buffer2[500];

int main(){
	chaff *p1, *p2;
	int *p3, *p4;

	p1 = new chaff; //堆上分配空间给struct
	p3 = new int[20]; // 堆上分配空间给array

	p2 = new(buffer1)chaff; //在buffer1上分配空间给struct
	p4 = new(buffer2)int[20]; //在buffer2上分配空间给array
}
class Widget{
public:
	Widget(int widgetSize);
};

Widget * constructWidgetInBuffer(void *buffer, int widgetSize){
	return new(buffer)Widget(widgetSize);
}

对于new(buffer)Widget(widgetSize),它调用的就是布局new:

void *operator_new(size_t, void * location){  //size_t没有名字,为了防止编译器发出警告说它没有被使用。
	return location;
}

使用场景

传统的new操作符只能在动态内存中得到空间,如果我们想要重一个静态内存中得到一个空间,该怎么做呢?

就用布局new操作符

#include <iostream>
#include <new>
using namespace std;
int main()
{
	char buffer[10];
	char *p = new (buffer) char[sizeof(buffer)];
	p[9] = 'a';
	cout<<buffer[9];
    return 0;
}

p指针指向了buffer的内存空间,可以直接对buffer进行写值,而且p等于buffer数组的首地址。

注意:这里不能对p进行释放空间。

布局new操作符,能够让你指定要使用的位置。

什么叫做布局new(也叫做定位new)

若提供了 布置参数,则将它们作为额外实参传递给分配函数。这些分配函数被称作“布局 new”,这来源于标准分配函数 void* operator new(std::size_t, void*),它直接返回未更改的第二实参。它被用于在已分配的存储中构造对象:

// 在任何块作用域内……
{
    alignas(T) unsigned char buf[sizeof(T)];
    // 静态分配拥有自动存储期的存储,对任何对象类型 `T` 足够大。
    T* tptr = new(buf) T; // 构造一个 `T` 对象,将它直接置于
                          // 你预分配的位于内存地址 `buf` 的存储。
    tptr->~T();           // 如果程序依赖对象的析构函数的副作用,你必须**手动**调用它。
}   

注意:分配器 (Allocator) 类的各成员函数封装了此功能。

  • 若不抛出的分配函数(例如 new(std::nothrow) T 所选择的)因为分配失败返回空指针,则 new 表达式立即返回,而不会试图初始化对象或调用解分配函数。(也就是说不能对返回值释放空间)
  • 若将空指针作为实参传给不分配的布置 new 表达式,这会使得被选择的标准不分配布置分配函数返回空指针,则行为未定义。

new操作符和new操作的区别

对于:

string *ps = new string("memory mangement");

这里的new是new operator(new操作符)。这个操作符就像sizeof一样是语言内置的,你不能改变它的含义,它的功能总是一样的。它要完成的功能分为两部分:

  • 第一部分是分配足够的内存以便容纳所需类型的对象
  • 第二部分是它调用构造函数初始化内存中的对象。

new操作符总是做这两件事情,你不能以任何方式改变它的行为。

你所能改变的是如何为对象分配内存。new操作符调用一个函数来完成必需的内存分配。你能够重写或者重载这个函数来改变它的行为。new操作符为分配内存所调用函数的名字是operator new(new操作)。

函数operator new通常这样声明:

void * operator new(size_t size);

返回值类型是void*,因为这个函数返回一个未经处理(raw)的指针,未初始化的内存(如果你喜欢,也可以写一种operator new函数,在返回一个指针之前能够初始化内存以存储一些数据,但是一般不这么做)。你能增加额外的参数重载operator new,但是第一个参数类型必须是size_t。

你一般不会直接调用operator new,但是一旦这么做,你可以像调用其他函数一样调用它:

void *rawMemory = operator new(sizeof(string));

操作符operator new将返回一个指针,指向一块足够容纳一个string类型对象的内存。

就像malloc一样,operator new的职责只是分配内存。它对构造函数一无所知。operator new所了解的是内存分配。

注意

  • 如果在new表达式中使用[],必须在相应的delete表达式也使用[];
  • 如果没有在new表达式中使用[],一定不要在delete表达式中使用[]
  • 0
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值