导读:本期聚焦于白鲨创作的《C++ placement new怎么用?如何指定内存地址构造对象?》,敬请观看详情。placement new 是 C++ 里能在已分配内存上调用构造函数的主要手段,常被用在内存池、共享内存以及嵌入式场景中,用来把对象精确放置到某个指定地址。它的基本形式是 new (address) Type(args),和普通 new 的区别在于它不申请内存,只负责调用构造函数并返回目标地址。使用 placement new 时必须手动管理对象生命周期:构造完成后要显式调用析构函数,再释放底层内存,否则容易造成资源泄漏。另一个关键点是内存对齐,如果目标缓冲区没有满足类型的对齐要求,程序可能崩溃或产生未定义行为。对带参构造函数、数组形式以及异常安全也需要正确处理。本文会从基础语法、手动析构、对齐策略、内存池实践以及常见陷阱几个方面展开,并给出可以直接编译运行的代码示例。

placement new 是 C++ 提供的一种特殊重载形式的 operator new,它允许开发者在已经分配好的原始内存上构造对象。换句话说,普通 new 会先申请内存再调用构造函数,而 placement new 跳过内存申请这一步,只负责在指定地址上调用构造函数。这一点在实现内存池、共享内存对象、嵌入式系统以及实时应用中非常有用,因为在这些场景下内存通常已经由开发者准备好,只需要把对象精确地放置进去。接下来我们结合代码仔细拆解它的用法。

C++ placement new怎么用?如何指定内存地址构造对象?

一、placement new 的基本语法和与普通 new 的区别

使用 placement new 之前需要包含头文件 <new>。它的语法形式是 new (address) Type(args),其中 address 是一个指向原始内存的指针,Type 是要构造的类型,args 是传给构造函数的参数。执行完这条语句后,Type 的构造函数会在 address 指向的内存上运行,整个表达式返回一个 Type* 指针,通常这个指针的值就等于 address 转换后的地址。例如下面的代码在栈上分配了一块字符缓冲区,并在它上面构造了一个 Widget 对象:

#include <new>
#include <iostream>

class Widget {
public:
    Widget(int v) : value_(v) {
        std::cout << "构造 Widget,值 = " << value_ << '\n';
    }
    ~Widget() {
        std::cout << "析构 Widget,值 = " << value_ << '\n';
    }
    void show() const {
        std::cout << "当前值 = " << value_ << '\n';
    }
private:
    int value_;
};

int main() {
    alignas(Widget) char buffer[sizeof(Widget)];
    Widget* w = new (buffer) Widget(42);
    w->show();
    w->~Widget();
    return 0;
}

这里 buffer 是一块已经存在的内存,placement new 在它上面执行了 Widget 的构造函数。可以看到,整个过程中没有调用系统级的内存分配函数,对象直接出现在我们预先准备好的地址上。这对于需要精确控制内存位置的应用来说非常关键。和普通 new 相比,placement new 不分配内存、不释放内存,也不跟踪对象的生命周期,因此开发者必须自己保证内存足够大、对齐正确,并且在使用完毕后手动析构。

如果构造函数带多个参数,可以直接把参数写在类型后面,就像普通对象构造一样。例如 new (buffer) Widget(10, "test") 这种形式完全合法。需要注意的是,placement new 不会检查目标内存是否已经存储了其他对象。如果同一块内存上已经构造过一个对象,而又没有先调用析构函数,那么直接再 placement new 一次会造成旧对象的资源泄漏,甚至产生未定义行为。

二、手动调用析构函数与生命周期管理

由于 placement new 只负责构造,C++ 并没有一个对应的 placement delete 来自动调用析构函数,因此对象的生命周期必须由开发者手动终结。做法是显式调用析构函数,语法为 w->~Widget();。这一调用会执行 Widget 的析构函数体,释放对象内部持有的资源,但不会释放对象占用的底层内存。底层内存仍然存在,可以被重新用于构造其他对象,也可以在之后由开发者统一释放。

还有一点必须注意:不能对 placement new 构造的对象使用 delete w;。因为 delete 会先调用析构函数,然后把指针交给对应的 operator delete 去释放内存。而这块内存并不是通过普通 new 从堆上申请的,它可能位于栈上、内存池中或者共享内存中,直接 delete 会导致未定义行为,通常表现为程序崩溃或堆损坏。下面是一个使用动态内存作为底层存储的完整示例:

#include <cstdlib>
#include <new>
#include <iostream>

class Resource {
public:
    Resource(int id) : id_(id) {
        std::cout << "Resource " << id_ << " 构造\n";
    }
    ~Resource() {
        std::cout << "Resource " << id_ << " 析构\n";
    }
    void run() const {
        std::cout << "Resource " << id_ << " 运行中\n";
    }
private:
    int id_;
};

int main() {
    void* mem = std::aligned_alloc(alignof(Resource), sizeof(Resource));
    if (!mem) return 1;

    Resource* r = new (mem) Resource(7);
    r->run();

    r->~Resource();   // 显式析构对象
    std::free(mem);   // 再释放底层内存
    return 0;
}

释放顺序非常重要:一定是先调用析构函数,再释放底层内存。如果先释放内存,对象还认为自己存活在那一块地址上,后续再析构就会访问已经归还给系统的内存,造成严重的未定义行为。如果对象内部有堆内存、文件句柄、锁或线程资源,析构函数会负责正确清理这些资源,因此遗忘析构就等于泄漏了这些资源。

重复析构也是需要避免的。对同一个地址调用两次析构函数,逻辑上等于销毁一个已经销毁过的对象,这是未定义行为。在实际工程中,通常会封装一个 RAII 辅助类,在作用域结束时自动调用析构并归还内存,减少手动管理出错的可能。

三、内存对齐与缓冲区准备

placement new 对目标地址的对齐要求与普通 new 完全一致。也就是说,传给 placement new 的地址必须满足 alignof(Type) 的对齐要求。比如某个结构体的对齐值是 8,那么存储它的地址必须能被 8 整除。如果缓冲区没有正确对齐,程序可能不会在当下立即崩溃,但后续访问成员变量时可能触发硬件异常,或者出现数据错乱。不同平台对未对齐访问的容忍度不一样,x86 通常会慢一些,而 ARM、MIPS 等平台往往直接抛异常。

最稳妥的栈上缓冲区写法是使用 alignas 指定对齐方式。下面是一个包含 double 成员的结构体示例:

#include <cstddef>
#include <cstdint>
#include <iostream>
#include <new>

struct Packed {
    char c;
    double d;
};

int main() {
    std::cout << "sizeof(Packed) = " << sizeof(Packed) << '\n';
    std::cout << "alignof(Packed) = " << alignof(Packed) << '\n';

    alignas(Packed) unsigned char storage[sizeof(Packed)];
    bool aligned = (reinterpret_cast<std::uintptr_t>(storage) % alignof(Packed) == 0);
    std::cout << "storage 是否对齐: " << aligned << '\n';

    Packed* p = new (storage) Packed{'A', 3.14};
    p->~Packed();
    return 0;
}

如果底层内存是动态分配的,使用 malloc 时要特别注意。C 标准规定 malloc 返回的内存对齐到 max_align_t,但某些类型的对齐要求可能超过这个默认值。此时应该使用 C++17 提供的带对齐参数的 operator new,或者 C11 的 aligned_alloc。例如前面示例中使用的 std::aligned_alloc(alignof(Resource), sizeof(Resource)) 就能保证返回的地址满足 Resource 的对齐要求。

当需要构造对象数组时,除了对齐,还必须正确计算总内存大小。不要简单使用 new char[n * sizeof(T)] 然后直接 placement new 数组,因为 char 数组默认对齐只有 1,很可能不满足 T 的对齐要求。更安全的做法是直接分配 alignas(T) 的缓冲区,或者使用标准库提供的 std::allocator<T> 来获取对齐内存。

四、在内存池中使用 placement new 构造对象

内存池的核心思想是一次性从系统申请一大块内存,后续对象构造都在这块内存上进行,从而避免频繁调用 malloc/new 带来的碎片和锁开销。placement new 正好承担构造职责。下面给出一个简化版的定长内存池实现,它从预分配的内存中取出空闲块,并用 placement new 在块上构造对象:

#include <new>
#include <vector>
#include <utility>
#include <cstddef>
#include <iostream>

template <typename T>
class FixedPool {
public:
    explicit FixedPool(std::size_t count) {
        element_size_ = sizeof(T);
        // 使用带对齐参数的 operator new 一次性分配 count 个对象的内存
        storage_ = static_cast<char*>(::operator new(count * element_size_,
                                       std::align_val_t(alignof(T))));
        free_blocks_.reserve(count);
        for (std::size_t i = 0; i < count; ++i) {
            free_blocks_.push_back(storage_ + i * element_size_);
        }
    }

    template <typename... Args>
    T* create(Args&&... args) {
        if (free_blocks_.empty()) return nullptr;
        void* addr = free_blocks_.back();
        free_blocks_.pop_back();
        // 在已回收的地址上构造对象
        return new (addr) T(std::forward<Args>(args)...);
    }

    void destroy(T* p) {
        if (!p) return;
        p->~T();
        free_blocks_.push_back(reinterpret_cast<char*>(p));
    }

    ~FixedPool() {
        // 注意:析构时不能假设所有对象都已经归还
        ::operator delete(storage_, std::align_val_t(alignof(T)));
    }

private:
    char* storage_ = nullptr;
    std::size_t element_size_ = 0;
    std::vector<char*> free_blocks_;
};

struct Task {
    int id;
    explicit Task(int i) : id(i) {
        std::cout << "Task " << id << " 构造\n";
    }
    ~Task() {
        std::cout << "Task " << id << " 析构\n";
    }
};

int main() {
    FixedPool<Task> pool(4);
    Task* t1 = pool.create(1);
    Task* t2 = pool.create(2);

    pool.destroy(t1);
    pool.destroy(t2);
    return 0;
}

在这个池子中,create 负责从空闲列表取出一个地址,并在该地址上调用 T 的构造函数。由于内存已经存在,整个过程没有系统分配调用。销毁对象时 destroy 先调用析构函数,再把地址放回空闲列表,这样这块内存就可以被下一个对象复用。这种模式在游戏开发、高频交易系统和嵌入式实时环境里非常常见。

需要强调,内存池只管理内存,不管理对象生命周期,生命周期由使用方负责。如果希望更安全,可以用智能指针配合自定义删除器。例如把 Task* 放进 std::unique_ptr,删除器只调用 pool.destroy 而不释放内存,这样即使异常抛出,对象也能被正确析构并归还到池中。这种组合兼顾了性能和安全性。

五、常见错误与异常安全注意事项

使用 placement new 最容易犯的错误可以归纳为几类。第一是忘记调用析构函数,导致对象内部资源泄漏。第二是对栈上或内存池中的对象直接调用 delete,这会破坏内存管理结构。第三是重复析构同一个地址,等于对一个已经销毁的对象再次执行析构逻辑。第四是没有检查内存对齐,尤其在动态分配场景下默认 malloc 可能不满足超对齐类型的要求。

异常安全也是一个容易忽略的点。假设构造函数在 placement new 执行过程中抛出异常,那么 C++ 运行时会负责析构已经构造成功的成员变量和基类子对象,但不会调用完整对象的析构函数。同时,因为内存本来就是外部提供的,placement new 不会释放这块内存。所以如果构造失败,我们不应该在 catch 块中对目标地址调用析构函数,因为对象根本没有构造完成。下面是一个错误示范:

#include <new>
#include <cstdlib>
#include <iostream>

struct MayThrow {
    int value = 0;
    MayThrow() = default;
    MayThrow(int v) {
        if (v < 0) throw std::runtime_error("negative");
        value = v;
    }
    ~MayThrow() {
        std::cout << "MayThrow 析构\n";
    }
};

int main() {
    void* mem = std::aligned_alloc(alignof(MayThrow), sizeof(MayThrow));
    try {
        MayThrow* p = new (mem) MayThrow(-1);
        p->~MayThrow();
    } catch (...) {
        // 构造函数失败,对象并没有完整构造,不要在这里调用 p->~MayThrow()
        std::cout << "构造函数抛出了异常\n";
    }
    std::free(mem);
    return 0;
}

数组形式的 placement new 也有额外陷阱。new (addr) T[n] 通常会在 addr 前面记录数组元素个数,因此它实际需要的内存可能大于 n * sizeof(T),而具体大小因编译器而异。销毁时应该对每个元素逐个调用析构函数,而不是直接对数组指针调用 delete[]。实际工程中,为了避免这些编译器相关的细节,更推荐使用循环逐个构造单个对象,而不是使用数组形式的 placement new。

总体来说,placement new 是一把非常锋利的工具。它把内存分配和对象构造这两件事彻底分开,让开发者可以完全掌控对象的物理位置和生命周期。但权力越大,责任也越大。只要严格遵循“先析构、后释放”“确保对齐”“谨慎处理异常”这几条原则,就能在内存池、共享内存、缓冲区复用等场景中发挥出普通 new 无法替代的作用。

C++ placement new指定内存地址对象构造修改时间:2026-10-04 01:28:43

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/1004/65331.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。