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唯一需要做的是返回指向一个足够大的内存的指针。虽然我们可以初始化那块内存,但通常这是构造函数的工作,构造函数将被编译器自动调用。 

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