跳转至

现代嵌入式C++教程——std::unique_ptr:零开销的独占所有权

笔者突然想起来,好像我都没给这个系列写std::unique_ptr的意思。

想象一下:有一个对象你只想给它一个主人——没有共享、没有争抢、没有复杂的引用计数。你要的是简洁、确定、尽可能"无成本"的管理方式。欢迎进入 std::unique_ptr 的世界:C++ 标准库给嵌入式开发者准备的一把轻量级、明确且高效的所有权钥匙。


为什么在嵌入式也要爱 unique_ptr

嵌入式常常流行"手工 new/free",或者根本不鼓励堆分配。事实是,合理使用堆(或定制分配策略)能让代码更清晰、模块更松耦合。相比裸指针,unique_ptr 的好处一言以蔽之:

  • 明确的所有权语义:谁持有,谁负责销毁。
  • 零或极低的运行时开销:在典型实现下,sizeof(unique_ptr<T>) == sizeof(T*)
  • 无拷贝、可移动:防止误拷贝引发双重释放(double free)。
  • 与 RAII 完美契合:资源在析构里自动释放,异常安全(在允许异常的系统里尤其好)。

好了,废话不多说,看点代码。


最基本的用法(又简单又安全)

#include <memory>

struct Sensor { void shutdown(); ~Sensor() { shutdown(); } };

void f() {
    auto p = std::make_unique<Sensor>(); // 推荐:安全、异常友好
    // 使用 p->...
} // 离开作用域时自动 delete
查看完整可编译示例
// std::unique_ptr 基本用法示例
// 演示 unique_ptr 的基本使用和零开销特性

#include <memory>
#include <cstdio>
#include <cstring>

// 简单的 Sensor 类
struct Sensor {
    int id;
    bool active;

    Sensor(int i) : id(i), active(true) {
        printf("Sensor %d constructed\n", id);
    }

    void shutdown() {
        if (active) {
            printf("Sensor %d shutting down\n", id);
            active = false;
        }
    }

    ~Sensor() {
        shutdown();
    }

    void read() const {
        printf("Sensor %d reading\n", id);
    }
};

// 基本用法
void basic_example() {
    printf("=== Basic Usage Example ===\n");

    auto p = std::make_unique<Sensor>(42);
    p->read();
    (*p).read();

    // 离开作用域时自动 delete
}

// sizeof 验证零开销
void size_example() {
    printf("\n=== Size Verification Example ===\n");

    printf("sizeof(int*):            %zu bytes\n", sizeof(int*));
    printf("sizeof(std::unique_ptr<int>): %zu bytes\n", sizeof(std::unique_ptr<int>));
    printf("sizeof(std::unique_ptr<Sensor>): %zu bytes\n", sizeof(std::unique_ptr<Sensor>));

    // 通常 unique_ptr 的大小等于裸指针
    static_assert(sizeof(std::unique_ptr<int>) == sizeof(int*),
                  "unique_ptr should be same size as raw pointer");
}

// 移动语义示例
void move_example() {
    printf("\n=== Move Semantics Example ===\n");

    auto p1 = std::make_unique<Sensor>(1);
    printf("p1 created\n");

    // unique_ptr 不可拷贝,但可以移动
    auto p2 = std::move(p1);
    printf("p1 moved to p2\n");

    if (p1) {
        printf("p1 is valid\n");  // 不会执行
    } else {
        printf("p1 is null (after move)\n");  // 会执行
    }

    if (p2) {
        printf("p2 is valid\n");  // 会执行
        p2->read();
    }
}

// reset 和 release 示例
void reset_release_example() {
    printf("\n=== Reset and Release Example ===\n");

    auto p = std::make_unique<Sensor>(99);
    printf("Sensor 99 created\n");

    // reset() 销毁旧资源并接管新资源
    p.reset(new Sensor(100));
    printf("After reset: Sensor 100\n");

    // release() 返回原始指针并将 unique_ptr 置空
    Sensor* raw = p.release();
    printf("Released raw pointer to Sensor %d\n", raw->id);

    // 现在需要手动释放
    delete raw;
    printf("Manually deleted Sensor 100\n");
}

int main() {
    basic_example();
    size_example();
    move_example();
    reset_release_example();

    printf("\n=== All Examples Complete ===\n");

    return 0;
}

std::make_unique 是首选:一行代码既申请内存又构造对象,避免了 new 与构造之间的临界窗口(异常安全)。对于嵌入式项目,把 make_unique 和自定义分配器结合使用可以做到既安全又可控(后面示范)。


真正的"零开销"是什么意思?

unique_ptr 的"零开销"不是玄学,而是几条可检验的事实:

  • 标准 unique_ptr<T, std::default_delete<T>> 在多数实现中只包含一个指针字段,因此大小等于裸指针。可以用下面的静态断言验证(在可编译的环境里):
static_assert(sizeof(std::unique_ptr<int>) == sizeof(int*), "通常应相等");
  • 为什么能这样?因为默认删除器 std::default_delete<T> 是空类型,且编译器会利用空基类优化(EBO)把它"挤掉"。也就是说,unique_ptr 实际上只需要存储那根指针。

但注意:当你使用有状态的删除器(例如捕获了闭包的 lambda)时,删除器本身包含状态,unique_ptr 的大小可能会增加——这就是"零开销"条件:无状态删除器


自定义删除器:强大但要小心

嵌入式里管理的资源不只有 new/delete,还可能是 malloc、文件描述符、裸 C 接口返回的句柄,或是自定义分配器分配的内存。unique_ptr 支持自定义删除器:

// 使用 malloc / free
std::unique_ptr<char, void(*)(void*)> buf(
    static_cast<char*>(std::malloc(128)),
    [](void* p){ std::free(p); }
);

// 或者:用函数指针(注意:函数指针会占空间)
void free_fn(void* p) { std::free(p); }
std::unique_ptr<char, void(*)(void*)> buf2(
    static_cast<char*>(std::malloc(128)),
    free_fn
);
查看完整可编译示例
// std::unique_ptr 自定义删除器示例
// 演示如何使用自定义删除器管理非堆资源

#include <memory>
#include <cstdio>
#include <cstdlib>
#include <cstring>

// ========== malloc/free 删除器 ==========
struct FreeDeleter {
    void operator()(void* p) noexcept {
        printf("[FreeDeleter] Freeing memory at %p\n", p);
        std::free(p);
    }
};

using MallocPtr = std::unique_ptr<char, FreeDeleter>;

void malloc_example() {
    printf("=== malloc/free Example ===\n");

    MallocPtr buf(static_cast<char*>(std::malloc(128)));
    if (buf) {
        std::strcpy(buf.get(), "Hello from malloc!");
        printf("Buffer content: %s\n", buf.get());
    }
    // 离开作用域时自动调用 free()
}

// ========== 函数指针删除器 ==========
void my_free_fn(void* p) {
    printf("[my_free_fn] Freeing memory\n");
    std::free(p);
}

void func_ptr_deleter_example() {
    printf("\n=== Function Pointer Deleter Example ===\n");

    // 注意:函数指针作为删除器会增加 unique_ptr 的大小
    using FuncPtrDeleter = std::unique_ptr<char, void(*)(void*)>;

    FuncPtrDeleter buf(static_cast<char*>(std::malloc(64)), my_free_fn);
    printf("Buffer allocated with function pointer deleter\n");

    printf("sizeof(unique_ptr<char, FreeDeleter>): %zu\n",
           sizeof(std::unique_ptr<char, FreeDeleter>));
    printf("sizeof(unique_ptr<char, void(*)(void*)>): %zu\n",
           sizeof(FuncPtrDeleter));
}

// ========== 文件句柄删除器 ==========
struct FileDeleter {
    void operator()(FILE* f) const noexcept {
        if (f) {
            printf("[FileDeleter] Closing file\n");
            std::fclose(f);
        }
    }
};

using FilePtr = std::unique_ptr<FILE, FileDeleter>;

void file_example() {
    printf("\n=== File Handle Example ===\n");

    // 打开临时文件
    FilePtr fp(std::fopen("/tmp/test.txt", "w"));
    if (fp) {
        std::fprintf(fp.get(), "Hello, embedded world!\n");
        printf("Written to file\n");
    }
    // 离开作用域时自动 fclose
}

// ========== 自定义内存池删除器 ==========
struct MemoryPool {
    static constexpr size_t POOL_SIZE = 1024;
    char buffer[POOL_SIZE];
    size_t offset = 0;

    void* allocate(size_t size) {
        if (offset + size > POOL_SIZE) {
            printf("[Pool] Out of memory!\n");
            return nullptr;
        }
        void* ptr = buffer + offset;
        offset += size;
        printf("[Pool] Allocated %zu bytes at %p\n", size, ptr);
        return ptr;
    }

    void deallocate(void* p) {
        printf("[Pool] Deallocated %p (simple pool, no actual free)\n", p);
        // 简单池实现,实际上不释放
    }
};

struct PoolDeleter {
    MemoryPool* pool;
    void operator()(void* p) noexcept {
        if (pool && p) {
            pool->deallocate(p);
        }
    }
};

template<typename T>
using PoolPtr = std::unique_ptr<T, PoolDeleter>;

void pool_example() {
    printf("\n=== Memory Pool Example ===\n");

    MemoryPool pool;

    // 使用 placement new 在池中分配
    void* mem = pool.allocate(sizeof(int));
    int* ptr = new(mem) int(42);

    PoolPtr<int> pooled(ptr, PoolDeleter{&pool});
    printf("Value from pool: %d\n", *pooled);
}

// ========== 数组 unique_ptr ==========
void array_example() {
    printf("\n=== Array unique_ptr Example ===\n");

    auto arr = std::make_unique<int[]>(10);
    for (int i = 0; i < 10; ++i) {
        arr[i] = i * i;
    }

    printf("Array contents: ");
    for (int i = 0; i < 10; ++i) {
        printf("%d ", arr[i]);
    }
    printf("\n");

    // 离开作用域时自动调用 delete[]
}

int main() {
    malloc_example();
    func_ptr_deleter_example();
    file_example();
    pool_example();
    array_example();

    printf("\n=== All Examples Complete ===\n");

    return 0;
}

关键提醒:

  • 捕获外部变量的 lambda 会变成有状态的删除器,从而可能 增加 unique_ptr 的大小。如果尺寸敏感(比如放入许多小对象的数组或表中),请避免捕获,改用函数指针或无状态函数对象。
  • 如果 deleter 是函数指针,unique_ptr 内部需要存储那指针(所以比裸指针多一倍空间),但函数指针适合共享同一删除逻辑的场景。

管理数组、C 接口与自定义分配器

使用 unique_ptr<T[]> 来管理数组:它会在析构时调用 delete[] 而不是 delete

auto arr = std::make_unique<int[]>(64); // 分配 64 个 int,析构时调用 delete[]
arr[0] = 42;

嵌入式常见场景:使用专用堆或分配器(例如来自 RTOS 或定制内存池)。unique_ptr 无法直接接受分配器对象,但你可以把删除器写成调用分配器释放的函数或函数对象:

struct Pool { void* alloc(size_t); void free(void*); };
extern Pool g_pool;

auto p = std::unique_ptr<MyType, void(*)(MyType*)>(
    static_cast<MyType*>(g_pool.alloc(sizeof(MyType))),
    [](MyType* t){ t->~MyType(); g_pool.free(t); }
);

如果池的释放函数不需要对象完整类型(例如仅内存回收),你可以将析构和回收分开,注意析构调用时类型需完整。


与多态一起用:必须注意析构器

如果你用 unique_ptr<Base> 指向 Derived,确保 Base 有虚析构函数,否则 delete 会是未定义行为:

struct Base { virtual ~Base() = default; };
struct Derived : Base { /* ... */ };

std::unique_ptr<Base> p = std::make_unique<Derived>();

这是面向对象设计的基本规则,不是 unique_ptr 的特例。

查看完整可编译示例
// std::unique_ptr 与多态示例
// 演示如何使用 unique_ptr 管理派生类对象

#include <memory>
#include <cstdio>

// 基类必须有虚析构函数
struct Base {
    virtual ~Base() = default;
    virtual void speak() const = 0;
};

struct Dog : Base {
    void speak() const override {
        printf("Woof!\n");
    }
    ~Dog() {
        printf("Dog destroyed\n");
    }
};

struct Cat : Base {
    void speak() const override {
        printf("Meow!\n");
    }
    ~Cat() {
        printf("Cat destroyed\n");
    }
};

struct Robot : Base {
    void speak() const override {
        printf("Beep boop!\n");
    }
    ~Robot() {
        printf("Robot destroyed\n");
    }
};

// 工厂函数返回 unique_ptr<Base>
std::unique_ptr<Base> create_animal(const char* type) {
    if (strcmp(type, "dog") == 0) {
        return std::make_unique<Dog>();
    } else if (strcmp(type, "cat") == 0) {
        return std::make_unique<Cat>();
    } else {
        return std::make_unique<Robot>();
    }
}

// 使用 unique_ptr<Base> 存储派生类
void polymorphism_example() {
    printf("=== Polymorphism Example ===\n");

    std::unique_ptr<Base> pet1 = std::make_unique<Dog>();
    std::unique_ptr<Base> pet2 = std::make_unique<Cat>();

    printf("Pet 1 says: ");
    pet1->speak();

    printf("Pet 2 says: ");
    pet2->speak();

    // 移动语义
    std::unique_ptr<Base> moved = std::move(pet1);
    if (!pet1) {
        printf("pet1 is now null (moved)\n");
    }

    printf("Moved pet says: ");
    moved->speak();
}

// 容器存储 unique_ptr
#include <vector>

void container_example() {
    printf("\n=== Container Example ===\n");

    std::vector<std::unique_ptr<Base>> zoo;

    zoo.push_back(std::make_unique<Dog>());
    zoo.push_back(std::make_unique<Cat>());
    zoo.push_back(std::make_unique<Robot>());
    zoo.push_back(create_animal("dog"));

    printf("Zoo sounds:\n");
    for (const auto& animal : zoo) {
        printf("  ");
        animal->speak();
    }

    // 容器销毁时,所有 unique_ptr 自动释放
    printf("Zoo being destroyed...\n");
}

// PIMPL 模式示例 (简化版)
class Widget {
public:
    Widget();
    ~Widget();
    void do_work();

private:
    struct Impl;
    std::unique_ptr<Impl> pImpl;
};

struct Widget::Impl {
    void do_work() {
        printf("Widget::Impl doing work\n");
    }
};

Widget::Widget() : pImpl(std::make_unique<Impl>()) {}
Widget::~Widget() = default;  // 即使 Impl 是不完整类型也没关系
void Widget::do_work() { pImpl->do_work(); }

void pimpl_example() {
    printf("\n=== PIMPL Example ===\n");
    Widget w;
    w.do_work();
}

int main() {
    polymorphism_example();
    container_example();
    pimpl_example();

    printf("\n=== All Examples Complete ===\n");

    return 0;
}

转移所有权、释放与重置

unique_ptr 不可拷贝,但可以移动,这是它防止双重释放的核心:

auto p1 = std::make_unique<int>(7);
auto p2 = std::move(p1); // p1 变成空,p2 拥有对象

有几个实用小函数:

  • p.release():返回原始指针并将 unique_ptr 置空(不会调用删除器)。谨慎使用:你拿回裸指针就要自己负责释放。
  • p.reset(new T(...)):销毁旧资源并接管新资源。
  • p.get():返回内部裸指针(不转移所有权)。

在嵌入式里,如果你必须与 C API 交互,release() 很常见,但记得把释放责任写清楚,避免内存泄漏。

查看完整可编译示例
// std::unique_ptr 基本用法示例
// 演示 unique_ptr 的基本使用和零开销特性

#include <memory>
#include <cstdio>
#include <cstring>

// 简单的 Sensor 类
struct Sensor {
    int id;
    bool active;

    Sensor(int i) : id(i), active(true) {
        printf("Sensor %d constructed\n", id);
    }

    void shutdown() {
        if (active) {
            printf("Sensor %d shutting down\n", id);
            active = false;
        }
    }

    ~Sensor() {
        shutdown();
    }

    void read() const {
        printf("Sensor %d reading\n", id);
    }
};

// 基本用法
void basic_example() {
    printf("=== Basic Usage Example ===\n");

    auto p = std::make_unique<Sensor>(42);
    p->read();
    (*p).read();

    // 离开作用域时自动 delete
}

// sizeof 验证零开销
void size_example() {
    printf("\n=== Size Verification Example ===\n");

    printf("sizeof(int*):            %zu bytes\n", sizeof(int*));
    printf("sizeof(std::unique_ptr<int>): %zu bytes\n", sizeof(std::unique_ptr<int>));
    printf("sizeof(std::unique_ptr<Sensor>): %zu bytes\n", sizeof(std::unique_ptr<Sensor>));

    // 通常 unique_ptr 的大小等于裸指针
    static_assert(sizeof(std::unique_ptr<int>) == sizeof(int*),
                  "unique_ptr should be same size as raw pointer");
}

// 移动语义示例
void move_example() {
    printf("\n=== Move Semantics Example ===\n");

    auto p1 = std::make_unique<Sensor>(1);
    printf("p1 created\n");

    // unique_ptr 不可拷贝,但可以移动
    auto p2 = std::move(p1);
    printf("p1 moved to p2\n");

    if (p1) {
        printf("p1 is valid\n");  // 不会执行
    } else {
        printf("p1 is null (after move)\n");  // 会执行
    }

    if (p2) {
        printf("p2 is valid\n");  // 会执行
        p2->read();
    }
}

// reset 和 release 示例
void reset_release_example() {
    printf("\n=== Reset and Release Example ===\n");

    auto p = std::make_unique<Sensor>(99);
    printf("Sensor 99 created\n");

    // reset() 销毁旧资源并接管新资源
    p.reset(new Sensor(100));
    printf("After reset: Sensor 100\n");

    // release() 返回原始指针并将 unique_ptr 置空
    Sensor* raw = p.release();
    printf("Released raw pointer to Sensor %d\n", raw->id);

    // 现在需要手动释放
    delete raw;
    printf("Manually deleted Sensor 100\n");
}

int main() {
    basic_example();
    size_example();
    move_example();
    reset_release_example();

    printf("\n=== All Examples Complete ===\n");

    return 0;
}

不要在中断/ISR里做堆操作

这是工程常识:不要在 ISR 中进行 new/delete 或会阻塞的操作。即便 unique_ptr 很轻量,但如果它持有堆分配的对象,分配/释放仍是堆操作。所以在 ISR 场景下,建议:

  • 使用预分配对象池 + unique_ptr 的自定义删除器返回到池;
  • 或者仅在任务/线程上下文使用 unique_ptr,ISR 只使用指针或信号量。

与标准容器配合(移动异常安全)

unique_ptr 的移动构造/赋值通常标记为 noexcept,这对容器(如 std::vector)很重要:在扩容时,容器更倾向于移动元素而非拷贝(拷贝不可行),且保证异常安全行为。换句话说,unique_ptr 与容器搭配,既安全又高效。


前向声明与 PIMPL(妙用)

unique_ptr 支持不完整类型的持有者,这非常适合 PIMPL(编译单元隐藏实现):

头文件 foo.h

struct Impl;
class Foo {
    std::unique_ptr<Impl> pImpl;
public:
    Foo();
    ~Foo(); // 在实现文件中定义,Impl 完整
};

源文件 foo.cpp~Foo() 可以看到 Impl 的完整定义并正确 delete。这个技巧能大幅减少编译依赖,是嵌入式大工程里常用的手段。


小结

std::unique_ptr 是 C++ 给我们的一位朴素而可靠的朋友:它把"谁负责释放"这件事写清楚了,并在绝大多数情况下做到零或极低的额外开销。对嵌入式开发者来说,unique_ptr 能把杂乱的资源释放逻辑封装得干净、可维护,同时保持性能。如果你还在用裸 new/delete,不妨试着用 unique_ptr 把那些责任交给 RAII——你会发现代码更稳、更容易审计,也更像成年人的工程。