提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
前言
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、文明交流,请勿谩骂。