第六章: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(),但是最好不要这么做。

posted @ 2021-06-30 12:35  放不下的小女孩  阅读(139)  评论(0)    收藏  举报