目录
[三、传统 new/delete 写法(下篇问题源头)](#三、传统 new/delete 写法(下篇问题源头))
[3.1 表面上没问题("工程幻觉")](#3.1 表面上没问题(“工程幻觉”))
[3.2 但工程上隐含 4 个风险点](#3.2 但工程上隐含 4 个风险点)
[❌ 风险 1:必须人工维护 new ↔ delete 对称性](#❌ 风险 1:必须人工维护 new ↔ delete 对称性)
[❌ 风险 2:多分支/异常路径不可控](#❌ 风险 2:多分支/异常路径不可控)
[❌ 风险 3:默认拷贝 = 双重 delete(经典炸点)](#❌ 风险 3:默认拷贝 = 双重 delete(经典炸点))
[❌ 风险 4:插件卸载/多线程回调极易 UAF](#❌ 风险 4:插件卸载/多线程回调极易 UAF)
[四、unique_ptr 写法(中篇思想的工程落地)](#四、unique_ptr 写法(中篇思想的工程落地))
[五、逐点对比:unique_ptr 相比 new 到底改变了什么?](#五、逐点对比:unique_ptr 相比 new 到底改变了什么?)
[对比 1:释放责任从"人"转移给"语言机制"](#对比 1:释放责任从“人”转移给“语言机制”)
[对比 2:异常/return 安全是天然的](#对比 2:异常/return 安全是天然的)
[对比 3:拷贝风险被编译期封死](#对比 3:拷贝风险被编译期封死)
[对比 4:所有权规则"自解释"](#对比 4:所有权规则“自解释”)
[六、最常见误区:ControlCore* ctrl_core_; 和 std::unique_ptr ctrl_core_; 到底是不是在创建对象?](#六、最常见误区:ControlCore* ctrl_core_; 和 std::unique_ptr ctrl_core_; 到底是不是在创建对象?)
[6.1 ControlCore* ctrl_core_; 的含义](#6.1 ControlCore* ctrl_core_; 的含义)
[6.2 std::unique_ptr ctrl_core_; 的含义](#6.2 std::unique_ptr ctrl_core_; 的含义)
一、前言
承接系列前文:
-
上篇:裸指针为什么危险(泄漏/异常/多分支/悬空指针)
-
中篇:RAII 的思想:释放必须绑定生命周期
-
下篇:车辆运动控制工程实战:unique_ptr / shared_ptr / weak_ptr 在 ROS 中如何落地
这一篇作为末篇 ,只做两件事:
1)用最小代码把 new vs unique_ptr 的工程差异 讲到"不可反驳"
2)把最常见误区讲清:ControlCore* ctrl_core_; / unique_ptr<...> ctrl_core_; 并不是创建对象
二、结论先行
现代 C++ 不是"不用 new",
而是"不让你再靠 new/delete 去表达所有权与生命周期"。
三、传统 new/delete 写法(下篇问题源头)
cpp
class VehicleController {
public:
VehicleController() {
ctrl_core_ = new ControlCore();
}
~VehicleController() {
delete ctrl_core_;
}
private:
struct ControlCore {
void step(double ref, double cur) {
(void)ref;
(void)cur;
}
};
ControlCore* ctrl_core_;
};
3.1 表面上没问题("工程幻觉")
构造函数
new析构函数
delete看起来"对称"
3.2 但工程上隐含 4 个风险点
❌ 风险 1:必须人工维护 new ↔ delete 对称性
这是一条人为约定,不是语言保证。后续改代码很容易漏。
❌ 风险 2:多分支/异常路径不可控
cpp
VehicleController() {
ctrl_core_ = new ControlCore();
if (init_failed) return; // delete 走不到
}
❌ 风险 3:默认拷贝 = 双重 delete(经典炸点)
cpp
VehicleController a;
VehicleController b = a; // 默认拷贝构造
// 两个 ctrl_core_ 指向同一对象 -> 析构 delete 两次 -> 未定义行为
❌ 风险 4:插件卸载/多线程回调极易 UAF
控制器析构了,回调还在用这个裸指针,就会 Use-After-Free。
四、unique_ptr 写法(中篇思想的工程落地)
cpp
class VehicleController {
public:
VehicleController() {
ctrl_core_ = std::make_unique<ControlCore>();
}
private:
struct ControlCore {
void step(double ref, double cur) {
(void)ref;
(void)cur;
}
};
std::unique_ptr<ControlCore> ctrl_core_;
};
| 名称 | 属于哪一层 | 标准叫法 | 是不是对象 | 具体含义 |
|---|---|---|---|---|
ControlCore |
类型层 | 类型 / 结构体类型 | ❌ 否 | 定义了一种**"控制核心"的蓝图**,描述它长什么样 |
VehicleController |
类型层 | 类类型 | ❌ 否 | 定义了一种**"车辆控制器"的蓝图** |
VehicleController() |
成员函数 | 构造函数 | ❌ 否 | 控制器对象"出生时"执行的初始化逻辑 |
ctrl_core_ |
对象成员 | 成员变量 | ❌(本身不是 ControlCore 对象) | 用来持有/管理 某个 ControlCore 对象 |
std::unique_ptr<ControlCore> |
类型层 | 智能指针类型 | ❌ 否 | 表达"对 ControlCore 的唯一所有权"的类型 |
std::make_unique<ControlCore>() |
表达式 | 对象创建语句 | ✅ 是 | 在堆上创建一个 ControlCore 对象 |
ControlCore 对象 |
运行时实体 | 对象实例 | ✅ 是 | 真正参与控制计算的那个"实体" |
五、逐点对比:unique_ptr 相比 new 到底改变了什么?
对比 1:释放责任从"人"转移给"语言机制"
| 写法 | 谁负责释放 |
|---|---|
new/delete |
人(靠记忆、靠规范) |
unique_ptr |
C++ 生命周期规则(成员析构自动释放) |
对比 2:异常/return 安全是天然的
构造中途 throw/return,不会泄漏;裸指针要靠人补齐每条路径。
对比 3:拷贝风险被编译期封死
cpp
VehicleController a;
VehicleController b = a; // ❌ 编译期报错(unique_ptr 不可拷贝)
对比 4:所有权规则"自解释"
cpp
std::unique_ptr<ControlCore> ctrl_core_;
看到就知道:唯一拥有、不可共享、生命周期绑定。
六、最常见误区:ControlCore* ctrl_core_; 和 std::unique_ptr<ControlCore> ctrl_core_; 到底是不是在创建对象?
很多人会误以为下面两行是在"创建 ControlCore 对象":
cpp
ControlCore* ctrl_core_;
std::unique_ptr<ControlCore> ctrl_core_;
但它们都不是创建 ControlCore 对象,它们做的事情是:
在
VehicleController这个类里声明一个成员变量
裸指针:只是"地址槽位"
unique_ptr:是"带唯一所有权语义的管理器槽位"
用来保存(或管理)某个
ControlCore对象。真正"创建对象"的动作,发生在
new/make_unique那一行,而不是这两行。
6.1 ControlCore* ctrl_core_; 的含义
cpp
ControlCore* ctrl_core_;
1)含义:
声明一个"裸指针成员变量"
这个变量里能放一个地址(
ControlCore*)通过这个地址可以访问某个
ControlCore对象
注意:它不负责创建对象,也不负责释放对象。
也就是说,这行只是:
"我准备留一个地方,未来可以存放一个
ControlCore的地址。"
对象一般是后面才创建并赋值的,例如:
cpp
ctrl_core_ = new ControlCore(); // ✅ 这行才创建对象(堆上)
如果你创建了对象,最终还得手动释放:
cpp
delete ctrl_core_;
ctrl_core_ = nullptr;
2)总结:
ControlCore* ctrl_core_;= "我有个地址槽位,但谁拥有对象、谁负责释放完全没写在类型里。"
6.2 std::unique_ptr<ControlCore> ctrl_core_; 的含义
cpp
std::unique_ptr<ControlCore> ctrl_core_;
1)含义:
声明一个"独占型智能指针成员变量"
它内部同样存着一个
ControlCore*地址但它额外表达并强制一条规则:
如果它指向了一个 ControlCore 对象,那么它就是该对象的唯一所有者(owner),并负责在析构时自动释放。
注意:这行本身也不创建 ControlCore 对象,只是声明一个"管理器变量"。
对象仍然需要你在构造函数里创建,例如:
cpp
ctrl_core_ = std::make_unique<ControlCore>(); // ✅ 这行才创建对象(堆上)
不同的是:你不需要写 delete,因为当 VehicleController 析构时:
ctrl_core_成员析构
unique_ptr析构会自动delete它管理的对象
并且 unique_ptr 有额外的工程保证:
不能拷贝(避免双重 delete)
只能 move(显式转移所有权)
2)总结:
std::unique_ptr<ControlCore> ctrl_core_;= "我有个专属负责人槽位:只要对象归我管,我就负责它的生死。"
七、对照表
| 代码 | 是否创建对象 | 是否表达所有权 | 是否自动释放 |
|---|---|---|---|
ControlCore* ctrl_core_;(声明成员变量) |
❌ | ❌ | ❌ |
std::unique_ptr<ControlCore> ctrl_core_; (声明成员变量) |
❌ | ✅(唯一) | ✅ |
ctrl_core_ = new ControlCore(); |
✅ | ❌(仍不明确) | ❌ |
ctrl_core_ = std::make_unique<ControlCore>(); |
✅ | ✅ | ✅ |
八、总结
new负责"造对象",但它不负责"谁来管对象"。
unique_ptr不只是"自动 delete",它是在类型层面写死:
对象的唯一拥有者是谁,生命周期跟谁绑定,能不能拷贝,什么时候必然释放。所以在车辆运动控制工程里:
如果对象应该与控制器同生共死,用unique_ptr不是习惯,而是工程正确性。