C++ new和delete的使用

提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档


前言

new和delete是C++里非常重要的两个关键字,意味着从“自由存储(堆)”分配指定大小的内存和释放掉这些内存。这些用法哪怕初学者也会,但是今天要讲的不是这个。今天要讲的是使用中容易忽视的细节和可能引发的错误


一、new和delete介绍

首先,new和delete总是成对出现,顺序也不能错。一定是先new再delete。其次,new和delete是针对单个对象,还有new[]和delete[]针对数组。最后,我们先从最简单的使用开始,慢慢带入。

二、简单使用

1.new和delete

这段代码演示针对内置对象的使用。

代码如下(示例):

#include <iostream>
using namespace std;

void test_1(){
	auto* p = new int;
	delete p;
}

p是指向一块new出来的“自由存储”,delete负责回收掉这块存储。这应该是最简单的用法了,需要注意的是p是局部变量,出了作用域p就被回收了(p在栈里),如果你没有在这里delete p,那么这块存储就一直在,一直不能被回收,直到你的程序结束被系统回收。

当然这还不是p面临的所有问题,还有其他问题后面再说。

2.自定义对象

new和delete还可以操作自定义对象。

代码如下(示例):

#include <iostream>

using namespace std;

struct t{
    t()= default;//没有特殊操作
    explicit t(int){}//阻止隐式转换
};

void test_2() {
    auto* p = new t;//默认构造
    auto* q = new t(1);//带参构造
    delete p;
    delete q;
}

用法和内置类型最大的差别就是在构造函数上,构造函数分为有参和无参,new不同的构造函数会调用不同的构造函数,除此之外没有大差别,delete不区分有参和无参,会调用同一个“析构函数”,如果你在自定义对象里面又申请了自由存储,切记在析构里回收掉。

3.new[]和delete[]

和new与delete的组合差不多,请看代码:

#include <iostream>

using namespace std;

void test_3(){
    auto*p = new int[10];
    delete[] p;
}

唯一的差别就是new[]针对的是数组,delete后面必须要加上[]。

4.主存耗尽

上面说了test_1()里面还有一个bug:这个bug在平常使用中不会出现,只有特殊情况才会触发。这个错误我详细描述下:new出来的空间在RAM上,甚至可能还带上一些SWAP空间,这些空间是有限的,当空间不足的时候会抛出一个std::bad_alloc错误,这个异常你要把它抓住,要不然会导致程序异常终止。具体复现代码请看:

#include <iostream>

using namespace std;

void test_4(){
    for(;;)
        auto p = new int[8192];
}

我建议你复现之前保存下重要工作!我是在windows11上操作的,由于这个系统自带bug,我差点把它玩崩溃掉。
不出意外的话意外发生了:

terminate called after throwing an instance of 'std::bad_alloc'
  what():  std::bad_alloc

当然系统还给了我另一个提示:
在这里插入图片描述
最后画面突然黑了两下,系统短暂卡死!当然,我今天不是来给windows11找bug的,测完这个之后操作系统感觉不太对劲,我重启了它。原因是主存耗尽,引发其它程序也不能正常运行。

这里有一个问题需要特别强调下:因为是模拟主存耗尽,而且是我的程序吃掉了绝大多数的主存,所以最后我的程序同时被操作系统检测到了“问题”,虽然在windows11上没有把我“杀死”,实测在Ubuntu上这么操作是会被操作系统直接杀死的。原因是:为了操作系统的稳定运行,会保留一部分主存给操作系统用,一旦出现主存耗尽的情况,操作系统会自主决定杀死一些“占用大”的程序来保证“自身”的运行。

所以,还有一种情况:本来主存就不足了,而我的程序同时也不是占用最大的那个,操作系统就可能决定不杀死我的程序,转而杀死其它占用大的程序。但是,不代表我们就安全了,我们还面临一个问题,那就是std::bad_alloc。这个问题怎么解决?

很多人可能没想过这个问题,而事实是绝大多数场景下你都不会面临这个问题。但是,凡事总有万一,如果你不处理这个问题,你的程序就提前终结,这肯定不是你想要的结果。

庆幸的是,C++标准给了我们解决方法,请看下面。

5.try&catch

没错,它闪亮登场了。只要是异常就归它管,这里的std::bad_alloc异常是派生自std::exception,我们只要抓住它就可以了。

请看示例:

#include <iostream>

using namespace std;

void test_5() {
    try {
        auto *p = new int;
        //...
        delete p;
    } catch (bad_alloc &e) {
        //...
    }
}

看起来很完美,唯一的缺点就是每次new都要try&catch,增加了繁琐性。有没有一个稍微简单的方法?请看下面:

6.nothrow

new和delete可以选择nothrow版本,具体类似于下下面的样子:
在这里插入图片描述
意思就是,如果主存耗尽,或者由于其他原因不能正常操作,不抛出异常。

示例代码:

#include <iostream>

using namespace std;

void test_6() {
    auto *p = new(nothrow) int;
    if(p){
		//...
	}
    delete p;
}

这里new只要加上nothrow参数指名不抛出异常,但也不代表一定申请成功,所以还需要if判断。写法没有try&catch那么臃肿,比较推荐这个方法。

原则上,我们无法知道哪一次new会导致这种问题,所以如果你不想有意外惊喜,又或者确实有这个需要的话,就做一些处理吧。

7.看下源代码

C++标准库的源代码对new&delete和new[]&delete[]分别做了重载。

// Macro for noexcept, to support in mixed 03/0x mode.
#ifndef _GLIBCXX_NOEXCEPT
# if __cplusplus >= 201103L
#  define _GLIBCXX_NOEXCEPT noexcept
#  define _GLIBCXX_NOEXCEPT_IF(...) noexcept(__VA_ARGS__)
#  define _GLIBCXX_USE_NOEXCEPT noexcept
#  define _GLIBCXX_THROW(_EXC)
# else
#  define _GLIBCXX_NOEXCEPT
#  define _GLIBCXX_NOEXCEPT_IF(...)
#  define _GLIBCXX_USE_NOEXCEPT throw()
#  define _GLIBCXX_THROW(_EXC) throw(_EXC)
# endif
#endif

//@{
/** These are replaceable signatures:
 *  - normal single new and delete (no arguments, throw @c bad_alloc on error)
 *  - normal array new and delete (same)
 *  - @c nothrow single new and delete (take a @c nothrow argument, return
 *    @c NULL on error)
 *  - @c nothrow array new and delete (same)
 *
 *  Placement new and delete signatures (take a memory address argument,
 *  does nothing) may not be replaced by a user's program.
*/
_GLIBCXX_NODISCARD void* operator new(std::size_t) _GLIBCXX_THROW (std::bad_alloc)
  __attribute__((__externally_visible__));
_GLIBCXX_NODISCARD void* operator new[](std::size_t) _GLIBCXX_THROW (std::bad_alloc)
  __attribute__((__externally_visible__));
void operator delete(void*) _GLIBCXX_USE_NOEXCEPT
  __attribute__((__externally_visible__));
void operator delete[](void*) _GLIBCXX_USE_NOEXCEPT
  __attribute__((__externally_visible__));
#if __cpp_sized_deallocation
void operator delete(void*, std::size_t) _GLIBCXX_USE_NOEXCEPT
  __attribute__((__externally_visible__));
void operator delete[](void*, std::size_t) _GLIBCXX_USE_NOEXCEPT
  __attribute__((__externally_visible__));
#endif
_GLIBCXX_NODISCARD void* operator new(std::size_t, const std::nothrow_t&) _GLIBCXX_USE_NOEXCEPT
  __attribute__((__externally_visible__, __alloc_size__ (1), __malloc__));
_GLIBCXX_NODISCARD void* operator new[](std::size_t, const std::nothrow_t&) _GLIBCXX_USE_NOEXCEPT
  __attribute__((__externally_visible__, __alloc_size__ (1), __malloc__));
void operator delete(void*, const std::nothrow_t&) _GLIBCXX_USE_NOEXCEPT
  __attribute__((__externally_visible__));
void operator delete[](void*, const std::nothrow_t&) _GLIBCXX_USE_NOEXCEPT
  __attribute__((__externally_visible__));

从源代码里可以看出:默认的delete和delete[]都是noexcept的,默认的new是可以throw的,当我们指定nothrow的时候它就调用noexcept那个版本了。这个特性是C++11(201103L)以后的版本支持的,切记!关于C++的版本代号请查询官方文档,这里不再赘述。

这里有个小插曲:函数声明为noexcept的特性由C++标准提供强保证,简而言之就是C++标准保证声明为new(nothrow)的函数一定不会抛出异常,可以放心大胆地使用。有意思的是,我们自己也可以把一个函数声明为noexcept的,编译器会对其进行优化,当然你也要保证这个函数一定不会抛出异常,假如抛出了会怎么办?你可以自己试一试。


总结
1、总体没什么难度,多注意下就不会出错。
2、有疑问,或者有不对的地方请在此留言,我可以在邮箱收到提醒邮件。
3、文明交流,请勿谩骂。

  • 5
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值