现代嵌入式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>>在多数实现中只包含一个指针字段,因此大小等于裸指针。可以用下面的静态断言验证(在可编译的环境里):
- 为什么能这样?因为默认删除器
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。
嵌入式常见场景:使用专用堆或分配器(例如来自 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 不可拷贝,但可以移动,这是它防止双重释放的核心:
有几个实用小函数:
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:
源文件 foo.cpp 中 ~Foo() 可以看到 Impl 的完整定义并正确 delete。这个技巧能大幅减少编译依赖,是嵌入式大工程里常用的手段。
小结¶
std::unique_ptr 是 C++ 给我们的一位朴素而可靠的朋友:它把"谁负责释放"这件事写清楚了,并在绝大多数情况下做到零或极低的额外开销。对嵌入式开发者来说,unique_ptr 能把杂乱的资源释放逻辑封装得干净、可维护,同时保持性能。如果你还在用裸 new/delete,不妨试着用 unique_ptr 把那些责任交给 RAII——你会发现代码更稳、更容易审计,也更像成年人的工程。