Placement new 是 C++ 中的一个特殊功能,允许开发者在已经分配的内存位置上直接构造对象。这种技术特别适用于内存管理敏感的应用,如嵌入式系统、游戏开发、实时系统等,其中控制内存分配和避免额外的内存分配开销非常关键。

例如在日志模块中,通常需要使用单例模式来实现一个全局唯一的对象用来写入日志。而通过 Placement new 作为单例模式申请内存确保了被使用的内存不被释放掉,最终实现了生命周期和程序的整个生命周期等同。

new 的两个操作

当我们在 C++ 中使用普通的 new 操作符时,它实际上执行了两个操作:

  1. 分配内存:new 操作符首先在堆上分配足够的内存来存储指定类型的对象。这是通过调用内存分配函数 operator new 来完成的,该函数接受一个参数,即要分配的字节数。

  2. 在分配的内存中构造对象:一旦内存被分配,new 操作符会在这块内存上构造对象。这是通过调用对象的构造函数来完成的。

以下是一个简单的示例,说明了这两个步骤:

class MyClass {
public:
    MyClass() {
        // Constructor code
    }
};

int main() {
    MyClass* myObject = new MyClass(); // 使用 new 操作符分配内存并构造对象
    // ...
    delete myObject; // 使用 delete 操作符释放内存并析构对象
    return 0;
}

在上述代码中,new MyClass() 首先调用 operator new 函数来在堆上分配足够的内存来存储一个 MyClass 对象。然后,它在这块内存上调用 MyClass 的构造函数来构造一个新的 MyClass 对象。最后,它返回一个指向新构造的对象的指针,我们可以使用这个指针来访问对象。

当我们不再需要这个对象时,我们可以使用 delete 操作符来释放对象占用的内存并调用其析构函数。这是通过首先调用对象的析构函数,然后调用内存释放函数 operator delete 来完成的。

Placement new

在 C++ 中,placement new 是一种特殊的 new 操作符,它允许我们在已经分配的内存上构造对象。这样,我们就可以更加灵活地控制对象的内存管理,例如在特定位置创建对象,或者在已经分配的内存中重新构造对象。

以下是一个简单的示例,说明了如何使用 placement new:

#include <new> // 必须包含这个头文件来使用 placement new

class MyClass {
public:
    MyClass() {
        // Constructor code
    }
};

int main() {
    char buffer[sizeof(MyClass)]; // 分配足够的内存来存储 MyClass 对象
    MyClass* myObject = new (buffer) MyClass(); // 在 buffer 上构造 MyClass 对象

    // 使用 myObject...

    myObject->~MyClass(); // 显式调用析构函数来析构对象,因为我们不能使用 delete

    return 0;
}

在上述代码中,我们首先在栈上分配了一个足够大的缓冲区来存储一个 MyClass 对象。然后,我们使用 placement new (new (buffer) MyClass()) 在这个缓冲区上构造了一个 MyClass 对象。注意,我们需要传递一个指向已分配内存的指针给 placement new。

当我们不再需要这个对象时,我们不能使用 delete 操作符来释放内存和析构对象,因为内存并不是由 new 操作符分配的。相反,我们需要显式地调用对象的析构函数 (myObject->~MyClass()) 来析构对象。然而,我们不需要手动释放内存,因为它是在栈上分配的,当它离开作用域时,编译器会自动释放它。

区别

综上普通的 new 说在堆上申请空间并构造对象,而 placement new 可以自己确定在堆上还是在栈上申请空间。

在 C++ 中,当我们使用普通的 new 操作符时,它会在堆上分配内存,并返回一个指向新分配内存的指针。我们通常不知道这个指针的具体值,也就是说,我们不知道对象实际存储在内存的哪个位置。

class MyClass {
public:
    MyClass() {
        // Constructor code
    }
};

int main() {
    MyClass* myObject = new MyClass(); // 使用 new 操作符分配内存并构造对象
    // 我们不知道 myObject 指向的具体内存地址
    // ...
    delete myObject; // 使用 delete 操作符释放内存并析构对象
    return 0;
}

然而,当我们使用 placement new 时,我们可以指定对象应该被构造在哪个位置。这是通过传递一个指向已分配内存的指针给 placement new 来实现的。

#include <new> // 必须包含这个头文件来使用 placement new

class MyClass {
public:
    MyClass() {
        // Constructor code
    }
};

int main() {
    char buffer[sizeof(MyClass)]; // 分配足够的内存来存储 MyClass 对象
    MyClass* myObject = new (buffer) MyClass(); // 在 buffer 上构造 MyClass 对象
    // 我们知道 myObject 指向的具体内存地址,就是 buffer 的地址
    // ...
    myObject->~MyClass(); // 显式调用析构函数来析构对象,因为我们不能使用 delete
    return 0;
}

在上述代码中,我们知道 myObject 指向的具体内存地址,因为它就是我们分配的 buffer 的地址。

当我们使用普通的 new 操作符分配内存时,我们可以使用 delete 操作符来释放内存并调用对象的析构函数。然而,对于使用 placement new 的情况,我们不能使用 delete 来释放内存,因为内存并不是由 new 操作符分配的。相反,我们需要显式地调用对象的析构函数来析构对象。然而,我们不需要手动释放内存,因为它是在栈上分配的,当它离开作用域时,编译器会自动释放它。

内存释放

在 C++ 中,placement new 的语法是 new (address) Type(initializer),其中 address 是一个指向已分配内存的指针,Type 是要构造的对象的类型,initializer 是传递给 Type 构造函数的参数。

以下是一个简单的示例,说明了如何使用 placement new:

#include <new> // 必须包含这个头文件来使用 placement new

class MyClass {
public:
    int value;
    MyClass(int v) : value(v) {
        // Constructor code
    }
};

int main() {
    char buffer[sizeof(MyClass)]; // 分配足够的内存来存储 MyClass 对象
    MyClass* myObject = new (buffer) MyClass(42); // 在 buffer 上构造 MyClass 对象,传递 42 给构造函数

    // 使用 myObject...

    myObject->~MyClass(); // 显式调用析构函数来析构对象,因为我们不能使用 delete

    return 0;
}

在上述代码中,我们首先在栈上分配了一个足够大的缓冲区来存储一个 MyClass 对象。然后,我们使用 placement new (new (buffer) MyClass(42)) 在这个缓冲区上构造了一个 MyClass 对象,并传递了 42MyClass 的构造函数。

当我们不再需要这个对象时,我们不能使用 delete 操作符来释放内存和析构对象,因为内存并不是由 new 操作符分配的。相反,我们需要显式地调用对象的析构函数 (myObject->~MyClass()) 来析构对象。然而,此处是不需要手动释放内存,因为它是在栈上分配的,当它离开作用域时,编译器会自动释放它。

placement new 使用场景

在 C++ 中,placement new 的使用场景通常是在需要对内存管理进行精细控制的情况下。以下是一些可能会优先使用 placement new 的情况:

  1. 内存池:如果你正在实现一个内存池,那么你可能需要在特定的内存位置上构造对象。在这种情况下,你可以使用 placement new 在内存池中的特定位置上构造对象。
#include <new>

class MyClass {
public:
    MyClass() {
        // Constructor code
    }
};

char memoryPool[1000]; // 内存池

int main() {
    MyClass* myObject = new (memoryPool) MyClass(); // 在内存池上构造对象

    // 使用 myObject...

    myObject->~MyClass(); // 显式调用析构函数来析构对象

    return 0;
}
  1. 对象重用:如果你有一个对象,你想在相同的内存位置上构造一个新的对象,那么你可以使用 placement new。这可以避免内存分配和释放的开销,提高性能。
#include <new>

class MyClass {
public:
    MyClass() {
        // Constructor code
    }
};

int main() {
    MyClass* myObject = new MyClass(); // 使用 new 构造对象

    // 使用 myObject...

    myObject->~MyClass(); // 显式调用析构函数来析构对象

    myObject = new (myObject) MyClass(); // 在相同的内存位置上构造新的对象

    // 使用新的 myObject...

    delete myObject; // 使用 delete 来释放内存并析构对象

    return 0;
}

在上述两个例子中,我们都使用了 placement new 来在特定的内存位置上构造对象。这可以提供更大的灵活性,允许我们更精细地控制内存管理。

placement new 的优点

在 C++ 中,placement new 操作符相比于普通的 new 操作符有以下优点:

  1. 预知内存地址:使用 placement new,我们可以在已经分配的内存上构造对象。这意味着我们可以预先知道对象的内存地址。

  2. 内存管理优化:在构建内存池、垃圾收集器或者在性能和异常安全性至关重要的情况下,placement new 非常有用。因为它允许我们在特定的内存位置上构造对象,这可以避免内存分配和释放的开销,提高性能。

  3. 减少分配失败的风险:由于内存已经被分配,所以使用 placement new 没有分配失败的风险。此外,由于不需要进行内存分配,所以在预分配的缓冲区上构造对象的时间通常会更少。

  4. 资源有限的环境:在资源有限的环境中,例如嵌入式系统,预先分配内存并使用 placement new 可以更好地控制内存使用,避免动态内存分配可能带来的问题。