C++中new和delete之后发生了什么

众所周知,如果我们使用new向系统申请了内存,我们应该使用指针指向这一块内存,俾能我们使用结束后,通过delete该指针释放此内存资源。

如果理解只达到这种程度,在内存管理稍微复杂一点时便一定会束手无策。总有一些事情比其他事情更基本一点,现在我来谈谈当我们new和delete之后到底发生了什么。

C++中的五种内存

在C++中内存分为五个区:堆、栈、自由存储区、全局/静态存储区和常量存储区。

  1. 堆区:用户使用new获得的内存在这里。用户需要自行管理其声明周期,也就是说一个new要对应一个delete,如果因为某些原因(之后我会说明一些可能的原因)内存没有被释放,那么在程序结束后,会由操作系统自行回收,这显然不是我们想看到的。
  2. 栈区:存储局部变量、函数参数等,比方说你在某个函数里定义了一个int变量a,这个a就存放在栈区。这块内存的生命周期由系统管理,不需要我们去操心。
  3. 自由存储区:用malloc分配的内存放置在这里。这块内存和堆很相似,不过是使用free来释放内存的。
  4. 全局/静态存储区:存放全局变量和静态变量。
  5. 常量存储区:存放常量,不允许更改。

new和delete

回到我们的主题。先看一段代码:

    int *p = new int;
    cout << *p << endl;//输出-842150451
    cout << &p << endl;//输出004FFC14
    *p = 1;
    cout << *p << endl;//输出1
    cout << &p << endl;//输出004FFC14
    delete p;
    cout << *p << endl;//输出-572662307
    cout << &p << endl;//输出004FFC14

首先声明了一个整形指针指向我们新开辟的内存,但没有将其显示初始化也没有为其赋值。
输出*p显示为-842150451。嗯,看起来这是编译器为我们默认初始化的值。
输出&p显示为004FFC14,现在我们知道了我们开辟的内存在哪里。

接下来我们将p指向的值定义为1。
输出*p显示为1,很好,这正是我们所期望的。
输出&p显示为004FFC14,内存地址没有变化。

接着我们delete p,之后发生了什么呢?
输出*p显示-572662307。对p调用delete后我们仍然能取到一个值!
输出&p显示004FFC14。哇!还是原来的地址。

从此我们可以看出,delete指针并非将该指针弃置不用,而是将其指向的内存中的数据清除,但是指针仍然指向原来的内存!

那么如果我们想按照delete的英文本意,把这个指针从世界上彻底销毁,需要怎么做呢?

    p = nullptr;
    cout << *p << endl;//程序到此停止执行
    cout << &p << endl;

将p的值赋为nullptr,现在这个指针才被销毁了。注意这里取一个空指针的地址和值的行为,其结果将是未定义的。

神奇的定值

我发现申请相同类型的内存时,编译器都会分配给其一个定制,对于int该值为前面提到的-842150451。同样delete掉指针后,也会有一个定值为-572662307。我分析了一下其原码和补码,没发现有什么特殊的,如果你知道这些数字的意义请留言告诉我,谢谢。

关于动态数组

如果要动态分配一个数组,要在类型名后跟一对方括号,在其中指明要分配的对象的数目,其类型必须是整形但不必是常量。其返回值为指向数组第一个对象的指针。

int* p = new int[get_size()];

注意这里分配的内存世界上并不是一个数组类型(也不存在这样的类型),因此不能对动态数组调用begin或end,也不能使用范围
for语句来处理其中的元素。
为了释放动态数组,我们要使用一种特殊形式的delete——在指针前面加上一个空方括号。

delete [] p;

如果这里我们忘记了方括号,其结果将是未定义的。

再深入一点

class A {
    ;
};
A* pA = new A;
delete pA;

这里发生了什么呢?实际上,这段程序里面隐含调用了一些我们没有看到的东西,那就是:

    static void* operator new(size_t sz);
    static void operator delete(void* p);

值得注意的是,这两个函数都是static的,所以如果我们重载了这2个函数(我们要么不重载,要重载就要2个一起行动),也应该声明为static的,如果我们没有声明,系统也会为我们自动加上。另外,这是两个内存分配原语,要么成功,要么没有分配任何内存。
回到主题,new A;实际上做了2件事:调用opeator new,在自由存储区分配一个sizeof(A)大小的内存空间;然后调用构造函数A(),在这块内存空间上类砖砌瓦,建造起我们的对象。同样对于delete,则做了相反的两件事:调用析构函数~A(),销毁对象,调用operator delete,释放内存。

使用new_handler处理异常

当operator new无法满足某一内存分配需求是,它会抛出异常。某些旧式编译器会在此时返回一个null指针,但是现在我们可以使用new_handler定制异常处理行为。
new_handler是个typedef,定义一个指针指向函数,该函数没有参数也不返回任何东西。
我们使用set_new_handler函数,其参数是个指针,指向operator new无法分配足够内存是该被调用的函数,其返回值也是个指针,指向set_new_handler被调用前正在执行(但马上就要被替换)的那个new_handler函数。
更详尽的内容推荐阅读《Effective C++》一书的条款49。

使用智能指针

如果可以,我们应该使用STL提供的shared_ptr和unique_ptr替换原始指针,这样我们可以不用自行管理内存的生命周期,获得类似JAVA和C#的自动内存回收体验。

    shared_ptr<int> p = make_shared<int>(1);
    shared_ptr<int> q(new int(2));

注意:

  1. 不要使用相同的内置指针初始化(或reset)多个智能指针。
  2. 不delete get()返回的指针
  3. 不适用get()初始化或reset另一个智能指针
  4. 如果使用了get()返回的指针,记住当最后一个对应的智能指针销毁后,你的指针就变为无效了。
  5. 不过你使用智能指针管理的资源不是new分配的内存,记住传递给它一个删除器。

allocator类

该类帮助我们将内存分配和对象构造分离开来,它分配的内存是原始的、未构造的。

allocator<string> alloc;//可以分配string的allocator对象
auto const p = alloc.allocate(n);//分配n个未初始化的string

malloc/free

从C程序员转换过来的C++程序员总是有个困惑:new/delete到底究竟和C语言里面的malloc/free比起来有什么优势?或者是一样的?

  1. malloc/free只是对内存进行分配和释放;new/delete还负责完成了创建和销毁对象的任务。
  2. new的安全性要高一些,因为他返回的就是一个所创建的对象的指针,对于malloc来说返回的则是void*,还要进行强制类型转换,显然这是一个危险的漏洞。
  3. 我们可以对new/delete重载,使内存分配按照我们的意愿进行,这样更具有灵活性,malloc则不行。

不过,new/delete也并不是十分完美,大概最大的缺点就是效率低(针对的是缺省的分配器),原因不只是因为在自由存储区上分配(和栈上对比),而且new只是对于堆分配器(malloc/realloc/free)的一个浅层包装,没有针对小型的内存分配做优化。另外缺省分配器具有通用性,它管理的是一块内存池,这样的管理往往需要消耗一些额外空间。我们可以针对new/delete进行重写以追求更高的效率,对于这方面更深入的探讨可以参考《Effective C++》第八章。

猜你喜欢

转载自blog.csdn.net/dreamiond/article/details/75201473