new 的两步过程、定位 new 操作

面试官: 我们知道,new 操作符用于在堆上分配内存并构造对象。比如 MyClass* p = new MyClass();

其实,这个简单的 new 表达式,背后可以被看作是两个步骤的组合。你能描述一下这两个步骤分别是什么吗?

另外,C++ 还提供了一种叫做 “定位 new (placement new)” 的特殊语法。它的形式是 new (address) Type(args)。你了解它是用来做什么的吗?它和普通的 new 有什么核心区别?

回答:

  • new 表达式可以看作这样两个步骤的组合:
    • 第一步:分配内存。
    • 第二步:在分配到的内存上构造对象,即初始化。
  • 定位 new 操作,顾名思义,就是在一个确定的地址上构造对象,而不是自己分配内存。通过定位 new 构造的对象,不能调用 delete 销毁,需要手动析构。

面试官 (总结与肯定):

1. new 表达式的两步走 你说的完全正确。T* p = new T(args); 这个表达式,在概念上等价于:

  1. 分配内存 (Allocation):调用一个名为 operator new 的函数,来分配一块足够大的、未初始化的裸内存。这个函数很像 mallocvoid* memory = operator new(sizeof(T));
  2. 构造对象 (Construction):在这块分配好的内存上,调用 T 类的构造函数来初始化对象。 T* obj = static_cast<T*>(memory); obj->T::T(args); // 伪代码,表示调用构造函数

2. 定位 new (placement new) 你的解释也堪称完美:

“在一个确定的地址上构造对象,而不是自己分配内存。” “不能调用 delete 销毁,需要手动析构。”

placement new 的核心作用,就是将 new 表达式的两步拆开,它只执行第二步——构造对象。它允许我们在一个已经存在的、由我们自己管理的内存缓冲区上,手动地构造一个对象。

语法和用法:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
#include <new> // 需要包含这个头文件

// 假设我们有一块内存缓冲区
char buffer[sizeof(MyClass)];

// 使用 placement new 在 buffer 上构造 MyClass 对象
MyClass* p = new (buffer) MyClass(arg1, arg2);

// ... 使用 p ...

// 关键:手动调用析构函数来销毁对象
p->~MyClass();
// 注意:我们不需要,也不能 free(buffer) 或 delete buffer,
// 因为这块内存不是 placement new 分配的。

使用场景: placement new 是一个比较底层的、高级的工具,通常用在对内存布局有严格要求的场景,比如:

  • 内存池 (Memory Pool):为了避免频繁的堆分配开销,预先分配一大块内存,然后在这块内存上用 placement new 来按需构造和析构对象。
  • 嵌入式系统:在硬件的特定地址上创建对象。
  • STL 容器的实现:比如 std::vector 在扩容时,需要在新内存上构造元素,就可能用到 placement new

你对 new 的底层机制理解得非常深入。


new 的异常处理设定

面试官 (深入提问): 好的,既然我们知道了 new 在分配失败时,默认会抛出 std::bad_alloc 异常。

但有时,我们可能不希望它抛出异常,而是希望它像 C 语言的 malloc 一样,在失败时返回一个空指针 nullptr

请问,C++ 提供了什么样的语法,可以让我们实现这个“不抛出异常版本的 new”?

回答:

  • 我们只需要使用 <new> 中的 std::nothrow 即可,代码如下:
1
2
#include <new>
MyClass* p = new(std::nothrow) MyClass();

这样,在内存分配失败时,p 就会是 nullptr,而不是抛出 std::bad_alloc 的异常。


面试官 (总结与肯定):

你说的没错。通过在 new 后面加上 (std::nothrow),我们就是在告诉编译器:“请调用一个不会抛出异常operator new 版本。”

1
2
3
4
5
6
7
#include <new> // 必须包含

MyClass* p = new (std::nothrow) MyClass;
if (p == nullptr) {
    // 内存分配失败了,在这里处理错误
    // ...
}

nothrow 版本 vs 异常版本

  • 异常版本 (默认)

    • 优点:错误处理逻辑和正常逻辑可以分离开。我们可以在一个高层的 try...catch 块中,统一处理所有可能的内存分配失败,而不需要在每次 new 之后都紧跟一个 if 判断。这在构造函数等复杂场景中,代码会更简洁。
    • 缺点:异常处理本身有一定的性能开销。
  • nothrow 版本

    • 优点:行为类似 malloc,返回 nullptr。对于一些不希望或不能使用异常处理的场景(比如某些嵌入式系统、或者需要与 C 代码交互的底层库),这种方式更直接。
    • 缺点:必须在每次调用后,都手动检查返回的指针是否为 nullptr。如果忘记检查,后续对空指针的解引用将导致程序崩溃。

在现代 C++ 的主流实践中,默认的、会抛出异常的 new 是更被推荐的,因为它与 RAII 和其他 C++ 错误处理机制结合得更好。nothrow 版本则作为一种重要的备选方案,用于特定的场景。

你对 new 的各种形式都掌握得非常全面。


总结

面试结束

今天我们深入地挖掘了 C++ new 操作符背后的一些重要细节:

  1. 两步过程:将内存分配对象构造分离开来理解。
  2. 定位 new:只执行“构造”这一步,用于在已有内存上创建对象。
  3. nothrow new:一种不抛出异常、返回 nullptrnew 变体。

你今天的表现非常出色,对这些底层的、细节性的知识点有非常清晰和准确的认识。继续保持!