第六章:new和delete
一、普通new运算符和delete运算符
1.new运算符和delete运算符实际上都由由两个步骤组成:
new运算符:
①分配所需的内存:通过调用适当库的new运算符函数来分配内存(实际上所有new运算符都是由malloc完成,自己重载new时也应该用malloc分配内存,delete都由free完成)
②在分配的内存上建立对象或初始化(内置类型)
int *pi=new int(5); //分解为以下两个步骤 int *pi=_new(sizeof(int)); //分配内存 *pi=5; //设置初始值
delete运算符:
①调用对象的析构函数
②调用标准库delete运算符释放空间
2.delete运算符需要保证删除的内存有效
delete pi; //转换为: if(!pi) _delete(pi);
3.delete只是清除内存中的对象,但是指针本身依然有效,依然指向一个合法的地址,类似于void*指针。
4.当需要以构造函数来配置对象时并且引入异常处理机制时,new和delete复杂一些
Point3d *origin=new Point3d; //转换为 Point3d *origin; //C++伪码 if(origin=_new(sizeof(Point3d))){ try{ origin=Point3d::Point3d(origin); } catch(...){ _delete(origin); //删除已经分配的内存; throw; //抛出原来的错误 delete origin; //转换为 if(origin!=0){ try{ Point3d::~Point3d(origin);} catch{...} { _delete(origin); throw; }}
5.一般的library中的new操作符会在传入需要地址大小的参数size为空时,自动申请一个大小为1的地址空间并返回(为了保证能返回一个有效地址)并且还能让使用者提供一个自己的处理函数new_handle().
注:书上没讲具体操作,只是写了伪码,C++primer上说如果程序员要实现内存分配和构造对象分离的操作,那么内存分配通过new运算符实现(可以自己重载new运算符通过molloc函数),对象构建则通过定位new实现)。
二、针对数组的new和delete
1.如果创建一个元素不含construct的数组,则不会调用vec_new(),因为只需要单纯的分配和释放内存。
2.如果创建的数组其元素包含构造函数和析构函数则会调用vec_new()和vec_delete().
int *p_array=new int[5]; //转换如下 int *p_array=(int*)_new(5*sizeof(int)); Point3d *p_array=new Point3d[10]; //通常被编译为 Point3d *p_array; p_array=vec_new(0,sizeof(Point3d),10,&Point3d::Point3d,&Point3d::~Point3d);
3.删除一个数组的元素现在的做法是直接调用 delete[] p_array 不用在【】中加入数组大小(以前需要加入,现在加入数组大小被视为不良的做法)。
因为现在的编译器①在new申请的内存区块中配置一个额外的word用来存放包含元素个数的包 或者 ②维护一个联合数组放置指针及大小。
情况1当一个坏指针被交给delete_vec,会导致一个不正确的元素个数和起始地址(因为包改变了原本内存大小,所以一个错误的指针也没法指向正确的开头)。
情况2当传入一个坏指针时,最多造成元素个数的错误。
4.不要将一个基类指针指向派生类数组,否则就需要遍历数组自行进行析构函数的调用。
Point *ptr=new Point3d[10]; //调用自动的删除函数 delete [] ptr; //此时传入vec_delete()函数的参数是Point的析构函数,其大小和Point3d不同,所以删除地址时,只有第一个元素的Point子对象被正确的清除。 //需要程序员手动清除 for(int ix=0;ix<elem_count;++ix) { Point3d* p=&((Point3d*)ptr)[ix]; delete p; }
三、位new
1.位new需要一个地址作为参数,而他本身也返回这个地址。位new的实现只需要返回这个获得的地址即可,剩下的会由编译器进行扩张。
Point2w *ptw=new(arenea) Point2w; //下面是实现 void* operator new(size_t,void* p) { return p; } //下面是编译器的扩张伪码 Point2w *ptw=(Point2w*) arena; if(!ptw) ptw->Point2w::Point2w();
2.当我们在一个已有对象的内存空间上施行定位new之前,我们需要调用析构函数释放原地址上的对象而保留内存供定位new使用。
p2w->~Point2w; p2w=new (arena) Point2w;
2.C++标准要求定位new的指针必须指向相同类型的类或者是一块“新鲜”的足够容纳该类型对象的内存。注意这意味着定位new并不支持多态不能在指向基类指针的内存上定位new一个派生类对象,这可能会造成内存泄漏。并且还会导致下面这个问题。
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; //1 b.f(); //哪一个f()被调用 }
Base和Derived类大小相同所以不会内存泄漏 (Derived没有声明新的虚函数,所以使用基类的虚函数表指针)。但是会产生b.f()到底调用哪个函数的问题。实际上大部分编译器调用的是基类的f(),但是最好不要这么做。

浙公网安备 33010602011771号