new
是C++里非常重要的一个关键词,用于申请内存、初始化对象。俗话说“有借有还再借不难”,通过new向操作系统“借”到的内存用完后必然要“还”回去,所以对应地还有一个delete
操作符与new
共同管理内存,delete
的作用是析构对象、释放内存。
说到内存管理,有些同学会想到C标准库函数malloc()
和free()
。C++是C语言的延续,那么C++一定可以丝滑地使用这两个标准库函数管理内存,那为什么还要提供关键词new
和delete
呢?
我们申请到内存后一般都会先做初始化,关键词new
相当于用一条语句做两件事——申请内存并初始化。考虑类Test
struct Test {
...
};
使用new
生成一个Test
的对象
Test* test = new Test(...);
相当于malloc()
申请内存后再进行初始化
// void initTest(Test* test);
void* testBuff = malloc(sizeof(Test));
initTest((Test*)testBuff);
同理,delete
也是一条语句做了两件事——先析构对象释放相关资源,然后再释放内存。
malloc()
和free()
是函数,只用于管理内存。
new
和delete
是操作符,new
和delete
用于管理内存,还有其他职责——new
初始化对象,delete
析构对象(清理资源)。
函数和操作符的使用方式存在很大差异。另外,编译后函数调用对应一条函数调用指令,操作符对应一条或多条其他汇编指令。
plain new是new
的常见用法,用于生成对象。会申请内存并调用构造函数
Test* test = new Test(...);
operator new用于申请内存,作用与malloc()
一样,实际上是函数调用。只会申请内存,不会调用构造函数
void* buff = operator new(sizeof(Test));
placement new用于调用构造函数初始化指定内存。只会调用构造函数,不会申请内存
// void* testBuff;
Test* test = new (testBuff)Test(...);
实际上只要观察Test* test = new Test(...);
对应的汇编结果就能发现plain new用法包含了两步——申请内存、调用构造函数
...
mov edi, 4
call operator new(unsigned long)
mov rbx, rax
mov rdi, rbx
call Test::Test() [complete object constructor]
mov QWORD PTR [rbp-24], rbx
...
第3行指令表示调用函数operator new(unsigned long)
申请内存;第6行指令表示调用Test
的构造函数初始化内存。所以
Test* test = new Test();
相当于
void* buff = operator new(sizeof(Test));
Test* test = new ((Test*)buff)Test;
重载new操作符实际上是重写函数operator new(...)
,对应的是申请内存的步骤,所以重载new操作符后只会影响plain new的内存分配,不影响调用构造函数。
重载new操作符的形式有两种——全局重载、局部重载。
全局重载就是直接重写全局函数operator new(...)
void* operator new(size_t size) {
...
}
从前面Test* test = new Test(...);
对应的汇编结果就能看出operator new()
是一个全局函数(汇编后函数符号没有任何改变,也没有加任何作用阈),全局重写new操作符后,所有类型的new操作都会调用自定义的operator new()
申请内存。
局部重载就是在类中重写静态函数operator new(...)
(即便不以static
修饰,编译器也会自动转为静态函数)
struct Test {
void* operator new(size_t size) throw() {
...
}
};
局部重载new操作符后,Test* test = new Test(...);
对应的汇编代码为
...
mov edi, 4
call Test::operator new(unsigned long)
mov rbx, rax
mov rdi, rbx
call Test::Test() [complete object constructor]
mov QWORD PTR [rbp-24], rbx
...
可以看到第3行的函数调用变为Test::operator new(unsigned long)
,增加了作用域Test
,意味着自定义的operator new()
只会对Test
有效。
我们知道Test* test = new Test(...);
实际上是两步——申请内存、调用构造函数,现在有一个问题,如果内存申请失败返回空指针,构造函数初始化成员函数时岂不是会出现访问空指针的问题?之前在项目上遇到过类似的问题,重载new操作符后构造函数报了空指针访问异常,代码如下
class Test {
private:
int value;
public:
Test() {
printf("[Test] Constructor\n");
value = 1;
}
void* operator new(size_t size) {
printf("[Test] operator new\n");
return NULL; // 内存申请失败,返回空指针
}
};
客户代码
int main() {
printf("-------- start --------\n");
Test* point = new Test();
printf("[main] point is %p\n", point);
printf("-------- done --------\n");
return 0;
}
运行结果出现内存访问错误,错误的位置发生在Test的构造函数中
-------- start --------
[Test] operator new
[Test] Constructor
Segmentation fault: 11
new操作符申请内存失败后,在构造函数中出现了访问空指针的错误。这个问题看起来非常棘手,new操作符会先申请内存然后调用构造函数,这个流程是编译器决定的不能修改,那么要解决这个问题似乎只有两个思路——在使用new之前手动判断内存是否会申请成功;在构造函数中判断this指针是否为空。
用这两种方式解决都存在隐患——因为需要改动很多代码,而人总是会出错的。事实上,一个throw()
声明(或者宏_NOEXCEPT
)就可以解决这个问题。
关键字thow
的是用来声明异常的,说明函数会抛出哪些类型的异常,显而易见throw()
说明函数不会抛出任何类型的异常,Test类只要做以下改动即可
class Test {
...
void* operator new(size_t size) throw() { // 只比原来的代码多了 throw()
...
}
...
};
运行结果
-------- start --------
[Test] operator new
[main] point is 0x0
-------- done --------
new操作符申请内存失败后未调用构造函数,不会出现访问空指针的问题。
为了一探究竟,我们比较一下加了thow()
声明和没有throw()
声明的汇编结果(右侧是无throw()
的,左侧是有throw()
的)
总的来说,加了throw()
声明后会多两条汇编指令,用于在operator new
的返回结果是空指针时不调用Test的构造函数。
最后,分享一个在线汇编神器: https://godbolt.org/