C++动态内存分配(new/new[]和delete/delete[])详解

C++动态内存分配(new/new[]和delete/delete[])详解

为了解决这个普通的编程问题,在运行时能创建和销毁对象是基本的要求。当然,C已提供了动态内存分配函数malloc( )和free( ),以及malloc( )的变种(realloc:改变分配内存的大小,calloc:指针指向内存前初始化),这些函数在运行时从堆中(也称自由内存)分配存储单元,但是运用这些库函数需要计算需要开辟内存的大小,容易出现错误。

那么通常我们在C语言中我们开辟内存的方式如下:

(void*)malloc(sizeof(void)); 

然而,在C+ +中这些函数不能很好地运行。构造函数不允许通过向对象传递内存地址来初始化它。如果那么做了,我们可能

  • 忘记了。则对象初始化在C + +中难以保证。
  • 期望某事发生,但结果是在给对象初始化之前意外地对对象作了某种改变。
  • 把错误规模的对象传递给了它。

当然,即使我们把每件事都做得很正确,修改我们的程序的人也容易犯同样的错误。不正确的初始化是编程出错的主要原因,所以在堆上创建对象时,确保构造函数调用是特别重要的。

C+ +是如何保证正确的初始化和清理并允许我们在堆上动态创建对象的呢?

答案是使动态对象创建成为语言的核心。malloc( )和free( )是库函数,因此不在编译器控制范围之内。如果我们有一个能完成动态内存分配及初始化工作的运算符和另一个能完成清理及释放内存工作的运算符,编译器就可以保证所有对象的构造函数和析构函数都会被调用。

若使用原始的动态内存开辟方式就会显得很繁琐,具体代码如下:

#include<cstdlib>
#include<cstring>
#include<iostream>
using namespace std;
class Obj
{
 int i,j,k;
 enum {sz=100};
 char buf[sz];
public:
  void initialize()
  {
    cout<<"initialize"<<endl;
    i=k=j=0;
    memset(buf,0,sz);
  }
  void destroy() const
  {
   cout<<"destroying Obj"<<endl;
  }
};
int main()
{
  Obj* obj=(Obj*)malloc(sizeof(Obj));
   if(obj!=0)
  obj->initialize();
  obj->destroy();
  free(obj);
 return 0;
}

在上面这行代码中,我们可以看到使用malloc( )为对象分配内存:obj* Obj = (obj*)malloc(sizeof(obj)) ;
这里用户必须决定对象的长度(这也是程序出错原因之一)。因为它是一块内存而不是一个对象,所以malloc( )返回一个void*.C++不允许将一个void* 赋予任何指针,所以必须映射。因为malloc( )可能找不到可分配的内存(在这种情况下它返回 0),所以必须检查返回的指针以确信内存分配成功。

但最坏的是:Obj->initialize( ) ;用户在使用对象之前必须记住对它初始化。注意构造函数没有被使用,因为构造函数不能被显式地调用—而是当对象创建时由编译器调用。这里的问题是现在用户可能在使用对象时忘记执行初始化,因此这也是引入程序缺陷的主要来源。许多程序设计者发现 C的动态内存分配函数太复杂,令人混淆。所以, C程序设计者常常在静态内存区域使用虚拟内存机制分配很大的变量数组以避免使用动态内存分配。因为C++能让一般的程序员安全使用库函数而不费力,所以应当避免使用 C的动态内存方法。C++中的解决方案是把创建一个对象所需的所有动作都结合在一个称为new的运算符里。当用new(new的表达式)创建一个对象时,它就在堆里为对象分配内存并为这块内存调用构造函数。

因此,如果我们写出下面的表达式foo *fp = new foo(1,2) ; 在运行时等价于调用malloc(sizeof(foo)),并使用(1,2)作为参数表来为

foo调用构造函数,返回值作为this指针的结果地址。在该指针被赋给 fp之前,它是不定的、未初始化的对象— 在这之前我们甚至不能触及它。它自动地被赋予正确的 foo类型,所以不必进行映射。缺省的new还检查以确信在传递地址给构造函数之前内存分配是成功的,所以我们不必显式地确定调用是否成功。在本章后面,我们将会发现,如果没有可供分配的内存会发生什么事情。我们可以为类使用任何可用的构造函数而写一个 ne w表达式。如果构造函数没有参数,可以写没有构造函数参数表的new表达式:

foo *fp = new foo ;我们已经注意到了,在堆里创建对象的过程变得简单了—只是一个简单的表达式 ,它带有内置的长度计算、类型转换和安全检查。这样在堆里创建一个对象和在栈里创建一个对象一样容易。

new表达式的反面是delete表达式。delete表达式首先调用析构函数,然后释放内存(经常是调用free( ))。正如new表达式返回一个指向对象的指针一样,delete表达式需要一个对象的地址。delete fp ;上面的表达式清除了早先创建的动态分配的对象foo。delete只用于删除由new创建的对象。如果用malloc( )(或calloc( )或realloc( ))创建一个对象,然后用delete删除它,这个行为是未定义的。因为大多数缺省的new和delete实现机制都使

用了malloc( )和free( ),所以我们很可能会没有调用析构函数就释放了内存。如果正在删除的对象指针是 0,将不发生任何事情。为此,建议在删除指针后立即把指针赋值为0以免对它删除两次。对一个对象删除两次一定不是一件好事,这会引起问题。

当创建一个new表达式时有两件事发生。首先,使用运算符new分配内存,然后调用构造函数。在delete表达式里,调用析构函数,然后使用运算符 delete释放内存。我们永远无法控制构造函数和析构函数的调用(否则我们可能意外地搅乱它们),但可以改变内存分配函数运算  符new和delete。被new和delete使用的内存分配系统是为通用目的而设计的。但在特殊的情形下,它不能满足我们的需要。改变分配系统的原因是考虑效率:我们也许要创建和销毁一个特定的类的非常多的对象以至于这个运算变成了速度的瓶颈。 C++允许重载new和delete来实现我们自己的存储分配方案,所以可以像这样处理问题。

另外一个问题是堆碎片:分配不同大小的内存可能造成在堆上产生很多碎片,以至于很快用完内存。也就是内存可能还有,但由于是碎片,找不到足够大的内存满足我们的需要。通过为特定类创建我们自己的内存分配器,可以确保这种情况不会发生。 
在嵌入和实时系统里,程序可能必须在有限的资源情况下运行很长时间。这样的系统也可能要求分配内存花费相同的时间且不允许出现堆内存耗尽或出现很多碎片的情况。由客户定制的内存分配器是一种解决办法,否则程序设计者在这种情况下要避免使用new和delete,从而失去了C + +很有价值的优点。

当重载运算符new和delete时,记住只改变原有的内存分配方法是很重要的。编译器将用new代替缺省的版本去分配内存,然后为那个内存调用构造函数。所以,虽然编译器遇到new 时会分配内存并调用构造函数,但当我们重载new时,可以改变的只是内存分配部分。(delete 也有相似的限制。)

当重载运算符new时,也可以替换它用完内存时的行为,所以必须在运算符new里决定做什么:返回0、写一个调用new - handler的循环、再试着分配或用一个 bad_alloc异常处理重载new和delete与重载任何其他运算符一样。但可以选择重载全局内存分配函数,或为特定的类使用特定的分配函数

当全局版本的new和delete不能满足整个系统时,对其重载是很极端的方法。如果重载全局版本,那么缺省版本就完全不能被访问—甚至在这个重载定义里也不能调用它们。

重载的ne w 必须有一个size_t 参数。这个参数由编译器产生并传递给我们,它是要分配内存的对象的长度。必须返回一个指向等于这个长度(或大于这个长度,如果我们有这样做的原因)的对象的指针,或如果没有找到存储单元(在这种情况下,构造函数不被调用),返回一个0。然而如果找不到存储单元,不能仅仅返回0,还应该调用new-handler或进行异常处理,通知这里存在问题。

运算符new的返回值是一个void *,而不是指向任何特定类型的指针。它所做的是分配内存,而不是完成一个对象的建立—直到构造函数调用了才完成对象的创建,这是由编译器所确保的动作,不在我们的控制范围内。

运算符delete接受一个指向由运算符new分配的内存的void *。它是一个void *因为它是在调用析构函数后得到的指针。析构函数从存储单元里移去对象。运算符 delete的返回类型是void。

下面提供了一个如何重载全局new和delete的简单的例子:

#include <stdlib.h> 

void * operator new(size_t sz)
{
  printf("operator new:%d bytes\n",sz);
  void* m=malloc(sz);
  if(!m) puts("out of memory");
  return 0;
}
void operator delete(void* m)
{
puts("operator delete");
free(m);
}
class s
{
  int i[100];
public:
  s(){puts("s::s()");}
  ~s(){puts("s::~s()");}
};
int main()
{
 puts("creating & destorying an int ");
 int* p=new int(47);
 delete p;
 puts("creating & destorying an s");
 s* S=new s;
 delete S;
 puts("creating & destorying an s[3]");
 s* SA=new s[3];
 delete [] SA;
}

这里可以看到重载new和delete的一般形式。为了实现内存分配器,使用了标准 C库函数 malloc( )和free( )(可能缺省的new和delete也使用这些函数)。它们还打印出了它们正在做什么的信息。注意,这里使用 printf( )和puts( )而不是i o s t r e a m s。当创建了一个i o s t r e a m对象时(像全局的c i n、c o u t和c e r r),它们调用new去分配内存。用printf( )不会进入死锁状态,因为它不调用new来初始化本身。

在main( )里,创建内部数据类型的对象以证明在这种情况下重载的new和delete也被调用。然后创建一个类型s的单个对象,接着创建一个数组。对于数组,我们可以看到需要额外的内存用于存放数组对象数量的信息。在所有情况里,都是使用全局重载版本的new和delete。

为一个类重载new和delete时,不必明说是 static,我们仍是在创建 static成员函数。它的语法也和重载任何其他运算符一样。当编译器看到使用new创建类对象时,它选择成员版本运算符new而不是全局版本的new。但全局版本的new和delete为所有其他类型对象使用(除非它们有自己的new和delete)。

如果为一个类重载了运算符new和delete,那么无论何时创建这个类的一个对象都将调用这些运算符。但如果为这些对象创建一个数组时,将调用全局运算符new( )立即为这个数组分配足够的内存。全局运算符 delete( )被调用来释放这块内存。可以通过为那个类重载数组版本的运算符new [ ]和delete [ ]来控制对象数组的内存分配。这里提供了一个显示两个不同版本被调用的例子: 这里,全局版本的new和delete被调用,除了加入了跟踪信息以外,它们和未重载版本new 和delete的效果是一样的。当然,我们可以在重载的new和delete里使用想要的内存分配方案。

可以看到除了加了一个括号外,数组版本的new和delete与单个对象版本是一样的。在这两种情况下,要传递分配的对象内存大小。传递给数组版本的内存大小是整个数组的大小。应该记住重载运算符new唯一需要做的是返回指向一个足够大的内存的指针。虽然我们可以初始化那块内存,但通常这是构造函数的工作,构造函数将被编译器自动调用。

感谢阅读,希望能帮助到大家,谢谢大家对本站的支持!

(0)

相关推荐

  • 浅析c++中new和delete的用法

    new和delete运算符用于动态分配和撤销内存的运算符 new用法: 1.开辟单变量地址空间1)new int;  //开辟一个存放数组的存储空间,返回一个指向该存储空间的地址.int *a = new int 即为将一个int类型的地址赋值给整型指针a. 2)int *a = new int(5) 作用同上,但是同时将整数赋值为5 2. 开辟数组空间一维: int *a = new int[100];开辟一个大小为100的整型数组空间二维: int **a = new int[5][6]三维

  • C++之CNoTrackObject类和new delete操作符的重载实例

    本文实例讲述了C++中CNoTrackObject类和new delete操作符的重载,分享给大家供大家参考.具体如下: 头信息: 复制代码 代码如下: class CNoTrackObject{  public: //在此出过错,没有加public 默认为类的私有变量,MyThreadData继承这个类后也无法访问成员变量      void* operator new(size_t nSize);      void operator delete(void*);      virtual

  • C++ new、delete(new[]、delete[])操作符重载需要注意的问题

    new.delete(new[].delete[])操作符的重载需要注意: 1.重载的 new.delete(或者 new[].delete[])操作符必须是类的静态成员函数(为什么必须是静态成员函数,这很好理解,因为 new 操作符被调用的时候,对象还未构建)或者是全局函数,函数的原型如下: 复制代码 代码如下: void* operator new(size_t size) throw(std::bad_alloc); // 这里的 size 为分配的内存的总大小 void* operato

  • 详解C++中new运算符和delete运算符的使用

    C++ 支持使用 new 和 delete 运算符动态分配和释放对象.这些运算符为来自称为"自由存储"的池中的对象分配内存. new 运算符调用特殊函数 operator new,delete 运算符调用特殊函数 operator delete. 在 Visual C++ .NET 2002 中,标准 C++ 库中的 new 功能将支持 C++ 标准中指定的行为,如果内存分配失败,则会引发 std::bad_alloc 异常. 如果内存分配失败,C 运行库的 new 函数也将引发 st

  • C++中new与delete、malloc与free应用分析

    一般来说,在C/C++的面试时,对于new/delete和malloc/free这两对的使用和区别经常被考查到,如果这种基础的问题都答不上来,估计很难过面试了.本文即是对new/delete和malloc/free这两对的使用和区别较为简单的分析一下,供大家参考. 一.new和delete new和delete是C++的运算符,用于动态分配内存和释放内存. 1.new表达式 标准库定义了operator new函数的几个重载版本,没有使用noexcept说明的版本在内存分配失败时可能会抛出bad

  • C++表达式new与delete知识详解

    在C++中,new表达式用于动态创建对象,即在堆(自由存储区)空间上为对象分配内存,而程序员也要小心的使用这些申请来的内存空间,当不再使用时应该调用delete表达式来释放该存储空间并且将指针置零. 本文学习了如何动态创建对象,动态创建的对象与一般对象的区别,动态创建的对象的初始化以及释放动态分配的内存等知识点. C++中分配的内存大致有三类:静态存储区,栈内存和堆内存 其中,静态存储区是在程序编译阶段就已经分配好的,用于全局变量,static变量等:堆栈是比较常用的对象存储方式. new和de

  • C++基础入门教程(五):new和delete

    对于以前没有接触过C++,然后初次接触Cocos2d-x的朋友来说,可能对于内存管理方面会比较生疏. 也经常会因为内存问题导致各种小Bug,我也曾经写过一篇retain和release倒底怎么玩?,用来驾驭Cocos2d-x的对象引用和释放也算是足够了. 但,难道大家就不想知道retain和release背后的秘密吗?(小若:不想.)   没错,今天木头来带大家走进科学,走进世界,一起来探讨C++的new和delete.(小若:没兴趣.)   好,既然大家都等不及了,那就开始吧~ 1.动态分配内

  • C++中new和delete的使用方法详解

    C++中new和delete的使用方法详解 new和delete运算符用于动态分配和撤销内存的运算符 new用法:           1.     开辟单变量地址空间 1)new int;  //开辟一个存放数组的存储空间,返回一个指向该存储空间的地址.int *a = new int 即为将一个int类型的地址赋值给整型指针a. 2)int *a = new int(5) 作用同上,但是同时将整数赋值为5           2.     开辟数组空间 一维: int *a = new in

  • c++中new和delete操作符用法

    "new"是C++的一个关键字,同时也是操作符.当我们使用关键字new在堆上动态创建一个对象时,它实际上做了三件事:获得一块内存空间.调用构造函数.返回正确的指针.当然,如果我们创建的是简单类型的变量,第二步就会被省略. new用法: 1. 开辟单变量地址空间 1)new int; 开辟一个存放数组的存储空间,返回一个指向该存储空间的地址.int *a = new int 即为将一个int类型的地址赋值给整型指针a. 2)int *a = new int(5) 作用同上,但是同时将整数

  • C++ new/delete相关知识点详细解析

    每个程序在执行时都占用一块可用的内存空间,用于存放动态分配的对象,此内存空间称为程序的自由存储区(free store)或堆(heap).C语言用一堆标准库函数malloc和free在自由存储区中分配存储空间,而C++则用new和delete表达式实现相同的功能. 一.new和delete创建和释放动态数组:数组类型的变量有三个重要的限制:数组长度固定,在编译时必须知道其长度,数组只在定义它的语句内存在.动态数组:长度固定,编译时不必知道其长度,通常是运行时确定:一直存在,直到程序显示释放它.

随机推荐