正文

我所理解的动态内存分配2006-11-23 16:34:00

【评论】 【打印】 【字体: 】 本文链接:http://blog.pfan.cn/goal00001111/20673.html

分享到:

本文的内容大部分来自《C++Primer第三版中文版》,是我学习C++的一个笔记,写出来主要是作为初学者的一个参考,希望对大家有所帮助。

       全局对象和局部对象的生命期是严格定义的,程序员不能够以任何方式改变它们的生命期。但是,有时候需要创建一些生命期能被程序员控制的对象,它们的分配和释放可以根据程序运行中的操作来决定。这种对象被称为动态分配的对象(dynamicaily allocated object)。动态分配的对象被分配在内存的堆中(有些资料称为自由空间(free store),其实就是堆)。关于堆的具体含义大家可以参考《数据结构》教程和文章《明晰C++内存分配的五种方法的区别》。

程序员用new表达式创建动态分配的对象,用delete表达式结束此类对象的生命期。动态分配的对象可以是单个对象,也可以是对象的数组,动态分配的数组的长度可以在运行时计算。

       在本文中,我将向大家介绍我所理解的两种形式的new的表达式:一种支持单个对象的动态分配,另一种支持数组的动态分配。并详细分析一种智能指针auto_ptr

 

单个对象的动态分配与释放

       new表达式由关键字new及其后面的类型指示符构成。该类型指示符可以是内置类型或class类型。例如:

                            new int ;

       从堆中分配了一个int型的对象。类似地

                            new Student ;

       分配了一个Student类对象。

       需要注意的是堆中分配的对象没有名字。new表达式没有返回实际分配的对象,而是返回了一个指向该对象的指针,对该对象的全部操作都要通过这个指针间接完成。例如:

                            int *pi = new int ;

       new表达式创建了一个int型的对象,由pi指向它。

       堆的特点是分配的内存是未初始化的。堆的内存包含随机的位模式,它是程序运行前该内存上次被使用留下的结果。测试    if (*pi == 0) 总是会失败,因为由pi指向的对象含有随机的位。因此我们要对用new表达式创建的对象进行初始化。例如

                            int *pi = new int (0) ;

       该语句表示pi指向一个int型的对象,该对象的初始值为0。括号中的表达式被称作初始化式(initializer)。初始化式的值不一定是常量,在该例中,任意的能够被转换成int型结果的表达式都是有效的初始化式。

       类似地,如下语句:

                            Student *ps = new Student(“Tom”) ;

       创建了一个Student类的对象。在类对象的情况下,括号中的值被传递给该类相关的构造函数,它在该对象被成功分配之后才被调用。

       new表达式的操作顺序如下:从堆中分配对象,然后用括号内的值初始化该对象,并返回一个指向该对象的指针。如果分配内存失败(通常是因为没有足够的内存),通常会抛出一个bad_alloc异常。

       动态分配内存的对象与静态分配内存的对象不同,编译器不会自动释放它们所占的内存-----除非整个程序结束。所以当动态分配内存的对象完成它的使命,需要被销毁的时候不能依赖编译器,而要靠程序员自己亲自来操刀。

       当指针pi所指对象的内存被释放时,它的生命期也随之结束。例如:

                            delete  pi;

       释放了pi指向的内存,结束了int型对象的生命期。通过delete表达式,我们可以在任何时候结束对象的生命期(当然是在new表达式之后),把内存还给堆。因为堆是有限的资源,所以我们不再需要已分配的内存时,就应该马上将其返还给堆,否则将造成内存泄漏。

       看过前面的delete表达式,你可能会问,如果pi因为某种原因被设置为NULL,又会怎样呢?是不是应该写成

                     if (pi != NULL)  //这样做有必要吗?

delete pi;

       答案是不。如果指针操作数被设置为NULL,则C++会保证delete表达式不会调用操作符delete;没有必要测试其是否为NULL。实际上在多数实现下,如果增加了指针的显式测试,那么该测试实际上会被执行两次。

       在这里,搞清楚pi的生命期和pi指向的对象的生命期之间的区别是很重要的。指针pi本身是个在全局域中声明的全局对象,它的生命期由编译器控制。结果pi的存储区在程序开始之前就被分配,一直保持到程序结束。而pi指向的对象的生命期是由程序员控制的,它是在程序执行过程中遇到new表达式时才被创建,遇到delete表达式时被销毁并收回存储区。因此,当程序执行delete pi;语句时,pi指向对象的内存被释放,但指针pi本身的内存并没有受到delete表达式的影响。在delete pi;之后,pi被称作空悬指针(俗称野指针),即指向无效内存的指针。空悬指针是错误的根源,它很难被检测到,如果对它进行操作将会产生无法预测的结果。一个比较好的办法是在指针所指的对象被释放后,马上将该指针设置为NULL,这样可以清楚地表明该指针不再指向任何对象。

 

       此外,需要注意的是,delete表达式只能应用在指向内存是用new表达式动态分配的指针上,将delete表达式应用在指向堆以外内存的指针上,会使程序运行期间出现未定义的行为。唯一的例外是,当指针指向NULL时,不管指针指向的对象是如何分配内存的,使用delete都不会引发麻烦。下面的例子给出了安全的和不安全的delete表达式:

void f()

{

       int i;

       char *str = "asdd";

       int *pi = &i;

       short *ps = NULL;

       double *pd = new double(123.3);

      

       delete str;       //危险!str指向的不是动态对象

       delete pi;  //危险!pi指向的对象i是一个局部对象

       delete ps; //安全!ps指向NULL

       delete pd; //安全!pd指向一个动态分配的对象

}

       注意:下面三个常见的程序错误都与动态内存分配有关:

1.  应用delete表达式失败,使内存无法返回堆,这被称做内存泄漏(memory leak)

2.  对同一内存区应用了多次delete表达式。这通常发生在多个指针指向同一个动态分配对象的时候。若多个指针指向同一对象,当通过某一个指针释放该对象时就会发生这种情况。

3.  在对象被释放后读写该对象。这常常会发生,原因是没有及时把该指针设置为NULL

 

这些操纵动态内存分配的错误比较容易出现,而且难于跟踪和修正。为了帮助程序员更好地管理动态分配的内存,C++库提供了auto_ptr类类型的支持。关于这个话题我将在后面做详细地介绍。

 

数组的动态分配与释放

       new表达式也可以在堆中分配数组。在这种情况下,new表达式中的类型指示符后面必须有一对方括号,里面的数值代表数组的长度,而且该数值可以是一个复杂的表达式。New表达式返回指向数组第一个元素的指针。例如:

//分配单个int型的对象,用1024初始化

int *pi = new int (1024) ;

//分配一个含有1024个元素的int型数组,未被初始化

int *pia = new int [1024] ;

//分配一个含有4 * 1024个元素的int型二维数组,未被初始化

int (*pia2)[1024] = new int [4][1024] ;

       pi指向一个int型的单个对象,初始值为1024pia指向数组的第一个元素,该数组有1024个元素。pia2是一个数组指针,它指向一个由具有1024个元素的数组构成的二维数组的第一个元素,即pia2指向一个二维数组,该数组的每个元素都是一个数组-----1024个元素的数组。

       一般地,在堆上分配的数组不能给出初始化值集。我们不可能在前面的new表达式中通过指定初始值来初始化数组的元素。在堆中创建的数组必须在for循环中被初始化,即数组的元素一个接一个地初始化。例如:

                     for (int i=0; i<1024; ++i)

                            pia[i] = 0;

       动态分配数组的主要好处是,它的第一维不必是常量值。我们可以在程序执行期间根据需要给数组分配合适的空间,以避免不必要的浪费。例如:

                     char *str = new char[strlen(errorTxt) + 1];

       注意此处对strlen(errorTxt)返回的值加1是必需的,这样才能容纳C风格字符串的结尾空字符。避免此类错误的一个较好选择是使用C++标准库string代替C风格字符串。

      

       注意,对于用new表达式分配的数组,只有第一维可以用运行时计算的表达式来指定,其他维必须是在编译时刻已知的常量值。例如:

int GetDim();   //返回一个长度

//分配一个二维数组

int (*pia3)[1024] = new int [GetDim()][1024] ;   //OK

//错误:数组的第二维不是常量

int **pia4 = new int [4][GetDim()] ;   //WRONG

 

       用来释放数组的delete表达式形式如下:

                     delete[] pia;

       空的方括号是必需的,它表明指针指向堆中的数组而非单个对象。因为piaint型的指针,所以如果编译器没有看到空方括号对,它就无法判断需要被删除的存储区是否为数组。

       如果不小心忘了该空括号对,会怎么样呢?编译器不会捕捉到这样的错误,并且不保证程序会正确执行。

阅读(3430) | 评论(0)


版权声明:编程爱好者网站为此博客服务提供商,如本文牵涉到版权问题,编程爱好者网站不承担相关责任,如有版权问题请直接与本文作者联系解决。谢谢!

评论

暂无评论
您需要登录后才能评论,请 登录 或者 注册