C++ new和delete 堆和栈

一、new和delete基本用法


程序开发中内存的动态分配与管理永远是一个让C++开发者头痛的问题,在C中,一般是通过malloc和free来进行内存分配和回收的,在C++中,new和delete已经完全包含malloc和free的功能,并且更强大、方便、安全。

new一般用法:

 new 类型 (初值)

用new分配数组空间时不能指定初值。

delete一般用法:

    delete [] 指针变量

[]部分是可选的,当释放数组所占内存时必须加[]。当你对一个指针使用 delete,delete 知道是否有数组大小信息的唯一方法就是由你来告诉它。如果你在你使用的 delete 中加入了方括号,delete 就假设那个指针指向的是一个数组。否则,就假设指向一个单一的对象。

    int *i = new int;             //没有初始值
    int *j = new int(100);         //初始值为100
    int *iArr = new int[3];        //分配具有3个元素的数组
    delete i;                   //释放单个变量所占用的内存
    delete j;
    delete []iArr;               //释放数组所占用的内存

一般我们常说的内存泄漏是指堆内存的泄漏。堆内存是指程序从堆中分配的,大小任意的(内存块的大小可以在程序运行期决定),使用完后必须显示释放的内存。应用程序一般使用malloc,realloc,new等函数从堆中分配到一块内存,使用完后,程序必须负责相应的调用free或delete释放该内存块,否则,这块内存就不能被再次使用,我们就说这块内存泄漏了。

二、new/delete和malloc/free的区别


对于非内部数据类型的对象而言,光用maloc/free无法满足动态对象的要求。对象在创建的同时要自动执行构造函数,对象在消亡之前要自动执行析构函数。
由于malloc/free是库函数而不是运算符,不在编译器控制权限之内,不能够把执行构造函数和析构函数的任务强加于malloc/free。
C++语言需要一个能完成动态内存分配和初始化工作的运算符new,以及一个能完成清理与释放内存工作的运算符delete。new出来的指针是直接带类型信息的。而malloc返回的都是void指针。

例1:

#include <iostream>
#include <malloc.h>
using namespace std;
class myclass
{
public:
    myclass()
    {
        i = 1;
    }
    void myfoo()
    {
        cout << "i = " << i << endl;
    }
private:
    int i;
};
int main()
{
    myclass *p = new myclass;
    myclass *q = (myclass *)malloc(sizeof(myclass));
    p->myfoo();
    q->myfoo();
    delete p;
    free(q);
    return 0;
}

程序执行结果为:
    i = 1
    i = 0

从上例可看出,new调用了类myclass的构造函数,而malloc只是分配了空间,并没有调用构造函数,因此会出现调用q->myfoo()函数时,输出的结果具有随机性。如果用free释放“new创建的动态对象”,那么该对象因无法执行析构函数而可能导致程序出错。如果用delete释放“malloc申请的动态内存”,理论上讲程序不会出错,但是该程序的可读性很差。所以new/delete必须配对使用,malloc/free也一样。

例2:

#include <iostream>
using namespace std;
class myclass
{
public:
    ~myclass()
    {
        cout << "Goodbye" << endl;
    }
};
int main()
{
    myclass *p = new myclass;
    free(p);
    return 0;
}

上例中,~myclass()为类的析构函数,对象离开作用域或被delete的时候会调用。指针p指向了一个堆上创建的myclass对象,若用free来释放内存,则不会调用析构函数,所以上面的程序没有输出。如将free(p)改为:

delete p

程序执行时将会调用到myclass类的析构函数,输出结果为:
        Goodbye

三、new和多维数组


当使用new运算符定义一个多维数组变量或数组对象时,它产生一个指向数组第一个元素的指针,返回的类型保持了除最左边维数外的所有维数。例如:

  int *p1 = new int[10];

返回的是一个指向int的指针int*。

 

 int (*p2)[10] = new int[3][10]; 

new了一个二维数组,去掉最左边那一维[3],剩下int[10],所以返回的是一个指向int[10]这种一维数组的指针int (*)[10]。

 

  int (*p3)[3][10] = new int[5][3][10]; 

new了一个三维数组, 去掉最左边那一维[5], 还有int[3][10],所以返回的是一个指向二维数组int[2][10]这种类型的指针int (*)[3][10]。

 

四、内存分配时的出错处理


我们都知道,使用 malloc/calloc 等分配内存的函数时,一定要检查其返回值是否为“空指针”,即检查分配内存的操作是否成功,这是良好的编程习惯,也是编写可靠程序所必需的。但是,如果你简单地把这一招应用到 new 上,那可就不一定正确了。

例如:

int* p = new int[SIZE];
if ( p = = 0 ) // 检查 p 是否空指针
    return -1;

其实,这里的 if( p == 0 )完全是没意义的。在C++里,如果 new 分配内存失败,默认是抛出异常的。所以,如果分配成功,p == 0 就绝对不会成立;而如果分配失败了,也不会执行 if ( p == 0 ),因为分配失败时,new 就会抛出异常跳过后面的代码。如果你想检查 new 是否成功,应该捕捉异常:

try
{
    int* p = new int[SIZE];
}
catch ( const bad_alloc& e )
{
    return -1;
}

事实上,C++中并非只有抛出异常的new,也有不抛异常的new,即通常所说的“nothrow new”。可以这样使用它:

        T* p = new (nothrow) T(MAX_SIZE);

其中,nothrow是头文件<new>中定义的一个类型为std::nothrow_t的常量,我们可以直接使用它。这时,如果内存分配失败,p的值将为空(0),且不会有异常抛出,跟C的malloc很像了。

四、内存分配的“栈”和“堆”


栈,就是那些由编译器在需要的时候分配,在不需要的时候自动清除的变量的存储区。里面的变量通常是局部变量、函数参数等。
堆,就是那些由new分配的内存块,他们的释放编译器不去管,由我们的应用程序去控制,一般一个new就要对应一个delete。如果程序员没有释放掉,那么在程序结束后,操作系统会自动回收。

下面通过汇编代码来了解下栈和堆内存的分配:

int main()
{   
        int* p = new int;
        return 0;
}

其中,“int* p = new int;
”对应汇编代码为:

0041358E  push        4   //分配一个int型数据大小内存(4个字节),相当于call operator new前,参数入栈
00413590  call        operator new (4111D6h)
00413595  add         esp,4  //call operator new后,恢复栈结构
00413598  mov         dword ptr [ebp-0D4h],eax  //eax值给call operator   new返回的结果生成一个临时变量
0041359E  mov         eax,dword ptr [ebp-0D4h]  //临时变量的值赋给寄存器eax
004135A4  mov         dword ptr [p],eax   //寄存器eax值赋给栈上指针p

 

 上面这句代码就涉及了内存分配的堆和栈,new分配了一块堆内存,指针p分配的是一块栈内存,这句代码的意思就是:在栈内存中存放了一个指向一块堆内存的指针p。在程序会先确定在堆中分配内存的大小,然后调用operator new分配内存,调用结束后返回值存入eax中,再将内存的首地址放入栈中(为p赋值)。

 

堆和栈主要的区别有以下几点:
(1)管理方式和碎片问题
    对于栈来讲,是由编译器自动管理,无需我们手工控制;对于堆来说,释放工作由程序员控制,容易产生内存碎片。
(2)分配效率
    栈是系统提供的数据结构,计算机会在底层对栈提供支持,分配专门的寄存器存放栈的地址,压栈出栈都有专门的指令执行,这就决定了栈的效率比较高。堆则是C/C++提供的,它的机制相对复杂,显然堆的效率比栈要低得多。

下面通过汇编代码分析下栈和堆内存存取的效率。

void main()
{
    char a = 1;
    char c[] = "1234567890";
    char* p = (char *)malloc(10);
    strcpy(p, c);
    a = c[1];
    a = p[1];
    return;
}

程序中对堆和栈存取的汇编代码为:

a = c[1];
004135E7  mov         al,byte ptr [ebp-1Fh]
004135EA  mov         byte ptr [ebp-9],al
a = p[1];
004135ED  mov         eax,dword ptr [ebp-2Ch]
004135F0  mov         cl,byte ptr [eax+1]
004135F3  mov         byte ptr [ebp-9],cl

可以看出,在栈上存取时直接就把字符串中的元素读到寄存器al中,在堆上存取时则要先把指针值读到eax中,在根据eax读取字符,显然慢了。

 

(3)增长方向不同
    栈内存由一个栈指针esp来开辟和回收,栈内存是从高地址向低地址增长的,增长时,栈指针向低地址方向移动,指针的地址值也就相应的减小;回收时,栈指针向高地址方向移动,地址值也就增加。所以栈内存的开辟和回收都只是指针的加减。
    对于堆来讲,增长方向是向上的,也就是向着内存高地址方向移动;回收时,指针向低地址方向移动,地址值也就减小。
(4)空间大小不同
    一般来讲在32位系统下,堆内存可以达到4G的空间,从这个角度来看堆内存几乎是没有什么限制的。但是对于栈来讲,一般都是有一定的空间大小的。无论是堆还是栈,都要防止越界现象的发生,因为越界的结果要么是程序崩溃,要么是摧毁程序的堆、栈结构,产生以想不到的结果。






 

posted @ 2019-04-13 17:13  王陸  阅读(3204)  评论(0编辑  收藏  举报