美文网首页
明明白白——new运算符

明明白白——new运算符

作者: 陈星空 | 来源:发表于2020-03-20 16:32 被阅读0次

分别是new expression、operator new和placement new,但对于这三个“new”,却不甚了了,这些天从《深度探索C++对象模型》读到new和delete,特意结合《C++ Primer》写下这篇笔记,以作总结。三个虽然都是“妞”(new),但每个妞都不相同各有各的特点,各有各的风味,本文重点在于总结比较这三个“妞”,但期间也不忘提一提推倒这三个“妞”的哥们——delete。

new expression 和 operator new

一个看起来很简单的new expression运算,其实暗含一些步骤,像这样的一次简单运用:int *p=new int (5)实际上包含着两个步骤:

  1. 调用一个合适的operator new实体分配足够的未类型化的内存。
  2. 调用合适的构造函数初始化这块内存,当然int没有构造函数,但是会进行赋值操作: *p=5

由此可见:new expression和operator new完全不是一回事,但关系不浅——operator new 为new expression分配内存。

摘录一下 《C++ primer》关于对比new expression 和 operator new的一小段话:

标准库函数 operator new和 operator delete 的命名容易让人误解。与其他operator 函数(如 operator=)不同,这些函数没有重载new或delete expression,实际上,我们不能重定义new或delete expression的行为。

这段话有两个要点:

  1. operator new和operator delete不是new expression和delete expression的重载,它们完全是另外的一个独立的东西,具有不同的语意,这与operator +是对+ expression的重载不同。
  2. new expression和delete expression是不能被重载的,可以看出它们与普通的expression不同。

operator new其实也是可以直接利用的,譬如当我们只想分配内存,而不愿意进行初始化的时候,我们就可以直接用operator new 来进行。用法如下:
T* newelements = static_cast<T*>(operator new ( sizeof(T) );

标准库重载有两个版本的operator new,分别为单个对象和数组对象服务,单个对象版本的提供给分配单个对象new expression调用,数组版的提供给分配数组的 new expression 调用:

void *operator new(size_t);       // allocate an object
void *operator new[](size_t);     // allocate an array

我们可以分别重载这两个版本,来定义我们自己的分配单个对象或对象数组的内存方式。当我们自己在重载operator new时,不一定要完全按照上面两个版本的原型重载,唯一的两个要求是:返回一个void*类型和第一个参数的类型必须为size_t

还要注意的是,在类中重载的operator new和operator delete是隐式静态的,因为前者运行于对象构造之前,后者运行与对象析构之后,所以他们不能也不应该拥有一个this指针来存取数据。另外,new expression 默认调用的是单参数的operator new——上面声明的那种,而其它不同形式的重载,则只能显式调用了。

delete expression与new expression相对应,而operator delete则与operator new对应。依上所述,则不难推断出关于delete expression和operator delete之间的关系以及一些特性,此略。

当使用new expression来动态分配数组的时候,Lippman在《深度探索C++对象模型》中指出:当分配的类型有一个默认构造函数的时候,new expression将调用一个所谓的vec_new()函数来分配内存,而不是operator new内存。但我在VC ++ 2010 上测试的结果却是,不论有没有构造函数,new expression都是调用operator new来分配内存,并在此之后,调用默认构造函数逐个初始化它们,而不调用所谓的vec_new(),也许cfront确实离我们有点遥远。

两个 delete 后的问题

最近在网上看到两个关于指针 delete 后的问题。第一种情况:

int* p = new int;
delete p;
delete p;// p为什么能delete两次,而程序运行的时候还不报错。

第二种情况:

int* p = new int ;
delete p;
*p = 5;     //delete后对*p进行再赋值居然也可以(他的平台上运行并没有引发什么错误)?

在回答这两个问题之前,我们先想想delete p; 这一语句意味着什么?p指向一个地址,以该地址为起始地址保存有一个int变量(虽然该变量并没有进行初始化),delete p之后p所指向的地址空间被释放,也就是说这个int变量的生命结束,但是p仍旧是一个合法的指针,它仍旧指向原来的地址,而且该地址仍旧代表着一个合法的程序空间。与delete之前唯一的不同是,你已经丧失了那快程序空间的所有权。这带来一个什么样的问题?你租了一间储物室(int* p = new int;),后来退租了(delete p;),但你却保存了出入该储物室的钥匙(指针p)没有归还。拥有这片钥匙,你或许什么都不做,这自然没有问题。但是,你或许出于好心,又跑过去告诉房东,“Hi!这储物室已经退租了(第一种情况)”。哦噢,会发生什么?我们假设此时这个房子已经有了新的租客。愚笨的房东直接相信了你的话,认为这个储物室空着,把它又租给新的人。于是一间只能给一个人用的储物室,却租给了两个人,再之后各种难以预料的情况就会发生。
又或许,你很无耻,你虽然退租,但却想用你的钥匙依旧享有储物室的使用权(第二种情况),结果呢,你存在这间储物室的东西可能会被现在的租客丢掉,而你也可能把他的东西丢掉,腾出空间来放你的。

回到上面的程序上来,毫无疑问的是上面的程序在语法上来讲是合乎规范的,但是暗藏着很大的逻辑错误,不论你对一块已经释放的内存再度delete,还是再度给它赋值,都暗含着很大的危险,因为当你delete后,就代表着将这块内存归还。而这块被归还的内存很可能已经被再度分配出去,此时不论是你再度delete还是重新赋值,都将破坏其它代码的数据,同时你存储在其中的数据也很容易被覆盖。至于报不报错,崩不崩溃,这取决于有一个怎么样的“房东”,聪明且负责的“房东”会阻止你上述的行为——终止你的程序,懒惰的房东,则听之任之。

上述情况下的指针p被称为野指针——指向了一块“垃圾内存”,或者说指向了一块不应该读写的内存。避免野指针的好方法是,当一个指针变为野指针的时候,马上赋值为NULL,其缘由在于,你可以很容易的判断一个指针是否为NULL,却难以抉择其是否为野指针。而且,delete一个空指针,不会做任何操作,因此总是安全的。

不用一个基类指针指向派生类数组?

《深度探索C++对象模型》中指出,不要用一个基类指针指向派生类的数组。因为在他的cfront中的vec_delete是根据被删除指针的类型来调用析构函数——也就是说虚函数机制在这儿不起作用了。照这样的思路来说,对一个派生类的数组依次调用其基类的析构函数,显然大多时候不能正确析构——派生类一般大于其基类。但是我感兴趣的一点是,这么多年过去了,这样一个不太合理的设计是否有所改进呢?说它不太合理是,以C++编程者的思路,在这样一种情况下,它应该支持多态,而且在这种情况下支持多态并不需要太复杂的机制和代价。我在vc++2008和vc++ 2010下的结果是:是的,有与cfront不同,它支持多态。

我的测试代码如下:

class point{
public:
 virtual ~point(){
 std::cout<<"point::~point()"<<std::endl;
 }
private:
 int  a;
};
class point3d:public point{
public:
 virtual ~point3d()
 {
 std::cout<<"point3d::~point3d()"<<std::endl;
 }
private:
 int b;
};
int main()
{
 point *p=new point3d[2];
 delete[] p;
 system("pause");
} ;

输出的结果,也令人满意:

VC输出的结果
确实调用了派生类的析构函数,而非基类的析构函数。

即使如此,是否能安心的使用一个基类指针指向派生类数组?我不太安心!——对于基类的析构函数是否为虚函数没有把握。所以最好还是不要把一个基类的指针指向派生类数组。非得这么做?那么我认为delete的时候将之类类型转换为派生类就差不多了,可以这样:
delete[] static_cast<point3d*>(p);
似乎不必要像Lippman说的这样:

for ( int ix = 0; ix < elem_count; ++ix ) 
{ 
 Point3d *p = &((Point3d*)ptr)[ ix ]; 
 delete p; 
}

placement operator new

placement operator new用来在指定地址上构造对象,要注意的是,它并不分配内存,仅仅是 对指定地址调用构造函数。其调用方式如下:
point *pt=new(p) point3d;
观其名字可知,它是operator new的一个重载版本。它的实现方式异常简单,传回一个指针即 可:

void* operator new(site_t,void *p)
{
    return p;
}

不必要惊讶于它的简单,《深度探索C++对象模型》中Lippman告诉我们,它有另一半重要的工作是被扩充而来。我在想,扩充一个类中定义的placement operator new还好说,但是要如何 扩充一个库中提供的placement operator new呢?毕竟它要放之四海而皆准,我原以为这其中 有什么高超的技巧。后来我则坚信根本就没有什么扩充,placement operator new 也并不强 大。

我先明确调用了 placement operator new :
point *pt=(point*)operator new(sizeof(point), p) ;
如我所料,输出结果显示(我在point的默认构造函数和placement operator new中间各输 出一句不同的话),此时 point的默认构造函数并不会被调用。然后我通过new expression 的方式来间接调用placement operator new:
point *pt=new(p) point();
这个时候 point 的默认的构造函数被调用了。可见 placement operator new并没有什么奇特 的地方,它与一般的operator new不同处在于,它不会申请内存。它也不会在指定的地址调用 构造函数,而调用构造函数的的全部原因在于new expression总是先调用一个匹配参数的 operator new然后再调用指定类型的匹配参数的构造函数,而说到底 placement operator new 也是一个 operator new。

通过一个placement operator new构建的一个对象,如果你使用delete来撤销对象,那么其内 存也被回收,如果想保存内存而析构对象,好的办法是显示调用其析构函数。

看一份代码:

1
2
3
4
5
6
7
8
9
struct Base { int j; virtual void f(); };
struct Derived : Base { void f(); };
void fooBar() {
Base b;
b.f(); // Base::f() invoked
b.~Base();
new ( &b ) Derived; // 1
b.f(); // which f() invoked?
}
上述两个类的大小相同,因此将Derived对象放在 Base对象中是安全的,但是在最后一句代码 中 b.f()调用的是哪一个类的f()。答案是Base::f() 的。虽然此时b中存储的实际上是一个 Derived对象,但是,通过一个对象来调用虚函数,将被静态决议出来,虚函数机制不会被启用。

参考:Lippman 的两本书《深度探索C++对象模型》和《C++ Primer》。

相关文章

  • 明明白白——new运算符

    分别是new expression、operator new和placement new,但对于这三个“new”,...

  • 第四章 对象和数组

    Object类型 创建方法有两种,字面量和new运算符 1. 使用new运算符 let obj = new Obj...

  • 原型和原型链

    一、new 和 构造函数 1.1 new 运算符 new 运算符用来创建一个新的对象,其后面需紧跟一个函数,该函数...

  • new.target

    new.target属性允许检测函数或构造方法是否是通过new运算符被调用的。通过new运算符被初始化的函数或构造...

  • instanceof 运算符 和 new运算符

    1. new运算符 new运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象的实例。 更多new知识请...

  • js中new操作符做了什么并实现自己的new操作符

    我们通过new运算符的使用,来探寻new操作符在执行的过程中究竟做了哪些操作,并且根据操作实现自己的new运算符。...

  • new 和 delete

    new和delete运算符是用于动态分配和撤销内存的运算符。搭配使用。(堆) 1)new int; //开辟一个...

  • 02-01-工厂模式和new运算符

    所有详细示例代码都在这里. 选项卡思路: 工厂模式: new 运算符new 运算符: 1.执行函数; 2.自动...

  • 浅谈 JavaScript new 执行过程及function原

    通过new运算符执行结果与直接执行函数结果不一样。new返回了一个对象 new运算符会根据方法返回值的不同,执行方...

  • js面试题--new的原理

    JS中的new操作符 和其他高级语言一样,JS中也有new运算符,我们知道new运算符是用来实例化一个类,从而在内...

网友评论

      本文标题:明明白白——new运算符

      本文链接:https://www.haomeiwen.com/subject/gkwjyhtx.html