智能指针
上一课的 new 和 delete,难的是记着 delete:早了不行,晚了不行,忘了也不行。智能指针替你记:它是一个装着指针的对象,自己死的时候(析构函数运行时),顺手把指着的东西 delete 掉。智能指针在 <memory> 里。
1#include <iostream>2#include <memory>3#include <string>4 5class Order {6public:7 Order(const std::string& item) : item_(item) {8 std::cout << "创建订单:" << item_ << '\n';9 }10 ~Order() {11 std::cout << "删除订单:" << item_ << '\n';12 }13 void show() const {14 std::cout << "订单:" << item_ << '\n';15 }16 17private:18 std::string item_;19};20 21int main() {22 std::cout << "开始\n";23 std::unique_ptr<Order> order = std::make_unique<Order>("咖啡");24 order->show();25 std::cout << "结束\n";26 return 0;27}
开始 创建订单:咖啡 订单:咖啡 结束 删除订单:咖啡
没有一个 delete,「删除订单」却显示了,而且在「结束」后面:main 结束时,order 这个变量死了,它把订单也删了。点单步走到最后:堆上的块在最后一步变灰,没有泄漏。
unique_ptr
std::unique_ptr<Order> 读作「独占一个 Order 的智能指针」。std::make_unique<Order>("咖啡") 在堆上建一个 Order,括号里的东西交给构造函数,再把它装进 unique_ptr。
用起来和普通指针一样:*p、p->成员,也能和 nullptr 比。类型名很长,常用 auto:
1#include <iostream>2#include <memory>3 4int main() {5 auto stock = std::make_unique<int>(40);6 *stock -= 15;7 std::cout << "库存:" << *stock << '\n';8 if (stock != nullptr) {9 std::cout << "有东西\n";10 }11 return 0;12}
库存:25 有东西
小技巧: 也可以写 std::unique_ptr<int> stock(new int(40));,C++11 里只能这样写。C++14 起有 std::make_unique,代码里就看不到 new 了,这门课都用它。
什么时候删
unique_ptr 是一个普通的局部变量,它在哪里死,对象就在哪里被删。用一对花括号围出一段,看得最清楚:
1#include <iostream>2#include <memory>3#include <string>4 5class Order {6public:7 Order(const std::string& item) : item_(item) {8 std::cout << "创建订单:" << item_ << '\n';9 }10 ~Order() {11 std::cout << "删除订单:" << item_ << '\n';12 }13 14private:15 std::string item_;16};17 18int main() {19 auto cake = std::make_unique<Order>("蛋糕");20 {21 auto bread = std::make_unique<Order>("面包");22 std::cout << "花括号里\n";23 }24 std::cout << "花括号外\n";25 cake.reset();26 std::cout << "结束\n";27 return 0;28}
创建订单:蛋糕 创建订单:面包 花括号里 删除订单:面包 花括号外 删除订单:蛋糕 结束
点单步:走出第 23 行的 },bread 死了,「面包」跟着删掉;cake.reset() 不用等到最后,现在就删(写 cake = nullptr; 也一样)。之后 cake 是空的。
这段程序显示什么?
1#include <iostream>2#include <memory>3 4class Box {5public:6 Box(char name) : name_(name) {7 std::cout << "+" << name_;8 }9 ~Box() {10 std::cout << "-" << name_;11 }12 13private:14 char name_;15};16 17int main() {18 for (int i = 0; i < 2; i++) {19 auto b = std::make_unique<Box>('a' + i);20 }21 std::cout << '\n';22 return 0;23}
b 是循环体里的变量,每一圈结束它就死了,Box 也跟着删掉。所以是建一个、删一个,再建一个、删一个。上一课用 new 写的同样的循环,是建了不删。
不能复制
名字里的 unique 是「唯一」:一个对象只归一个 unique_ptr 管。要是能复制,两个 unique_ptr 都会在死的时候 delete 它,就成了上一课的「delete 两次」。所以它干脆不让复制:
1#include <iostream>2#include <memory>3 4int main() {5 auto points = std::make_unique<int>(30);6 std::unique_ptr<int> copy = points;7 std::cout << *copy << '\n';8 return 0;9}
main.cpp: In function 'int main()':
main.cpp:6:33: error: use of deleted function 'std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = int; _Dp = std::default_delete<int>]'
6 | std::unique_ptr<int> copy = points;
| ^~~~~~报错很长,先看第一个 error::use of deleted function,用了一个被删掉的函数。这个函数就是 unique_ptr 的拷贝构造函数,它写了 = delete(第 18 课讲过)。
std::move 转交
不能复制,但可以转交:用 std::move(第 18 课)把对象从一个 unique_ptr 交给另一个。交出去以后,原来那个就空了:
1#include <iostream>2#include <memory>3#include <string>4 5class Order {6public:7 Order(const std::string& item) : item_(item) {8 std::cout << "创建订单:" << item_ << '\n';9 }10 ~Order() {11 std::cout << "删除订单:" << item_ << '\n';12 }13 void show() const {14 std::cout << "订单:" << item_ << '\n';15 }16 17private:18 std::string item_;19};20 21void finish(std::unique_ptr<Order> order) {22 order->show();23 std::cout << "做好了\n";24}25 26int main() {27 auto order = std::make_unique<Order>("奶茶");28 auto counter = std::move(order);29 if (order == nullptr) {30 std::cout << "order 已经空了\n";31 }32 finish(std::move(counter));33 std::cout << "回到 main\n";34 return 0;35}
创建订单:奶茶 order 已经空了 订单:奶茶 做好了 删除订单:奶茶 回到 main
点单步,跟着箭头走:第 28 行,箭头从 order 挪到 counter,order 空了;第 32 行又交给了 finish 的参数。finish 一结束,参数死了,订单就删了,所以「删除订单」在「回到 main」前面。
参数写成 std::unique_ptr<Order>(传值)的函数,意思是「这个对象交给我,归我管」。调用的地方必须写 std::move,一眼就看得出东西交出去了。
从函数交出来
上一课的 make_order 返回一个普通指针,拿到的人得记得 delete。返回 unique_ptr 就不用了。还可以把它们放进 vector,vector 死的时候,里面的 unique_ptr 一个个死,对象一个个删:
1#include <iostream>2#include <memory>3#include <string>4#include <vector>5 6class Order {7public:8 Order(const std::string& item) : item_(item) {9 std::cout << "创建订单:" << item_ << '\n';10 }11 ~Order() {12 std::cout << "删除订单:" << item_ << '\n';13 }14 15private:16 std::string item_;17};18 19std::unique_ptr<Order> make_order(const std::string& item) {20 auto order = std::make_unique<Order>(item);21 return order;22}23 24int main() {25 std::vector<std::unique_ptr<Order>> orders;26 orders.push_back(make_order("咖啡"));27 orders.push_back(make_order("蛋糕"));28 std::cout << "一共 " << orders.size() << " 个订单\n";29 return 0;30}
创建订单:咖啡 创建订单:蛋糕 一共 2 个订单 删除订单:咖啡 删除订单:蛋糕
return order; 不用写 std::move:order 是局部变量,函数结束时反正要死,编译器自己会把它搬出去。push_back(make_order(...)) 也一样,交进去的是一个临时的值。要把一个有名字的 unique_ptr 放进去,就得写 orders.push_back(std::move(p));。
shared_ptr:大家一起管
有时候一个对象真的有好几个主人,说不清谁最后用完。这时用 std::shared_ptr:它可以复制,每个复制都算一个主人;它记着现在有几个主人(use_count()),最后一个主人死了,才删对象。
1#include <iostream>2#include <memory>3#include <string>4 5class Song {6public:7 Song(const std::string& title) : title_(title) {8 std::cout << "载入:" << title_ << '\n';9 }10 ~Song() {11 std::cout << "卸载:" << title_ << '\n';12 }13 14private:15 std::string title_;16};17 18int main() {19 auto song = std::make_shared<Song>("晴天");20 std::cout << song.use_count() << '\n';21 {22 auto in_playlist = song;23 auto in_favorites = song;24 std::cout << song.use_count() << '\n';25 }26 std::cout << song.use_count() << '\n';27 song.reset();28 std::cout << "结束\n";29 return 0;30}
载入:晴天 1 3 1 卸载:晴天 结束
点单步:第 22、23 行各复制一次,三根箭头指着同一个块,主人数从 1 变成 3。出了花括号,两个复制死了,回到 1。song.reset() 走掉最后一个主人,「卸载」才显示。
用哪个
| 情况 | 用 |
|---|---|
| 堆上的一个对象,一个主人 | std::unique_ptr + std::make_unique(先想它) |
| 真的有好几个主人 | std::shared_ptr + std::make_shared |
| 一排东西 | std::vector |
| 只是借来看看、不管死活 | 引用,或普通指针 |
最后一行是说:函数只想用一下对象、不打算接管它,参数就写 const Order&(调用时写 *order),不用传智能指针。真要普通指针,order.get() 给你里面那个地址,但不要 delete 它,那是智能指针的事。
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。做完点单步走到最后,看看堆上有没有剩下的块。
交给 make_unique
✓程序用 new 建了一张票,可是忘了 delete。不要写 delete,把它改成 std::make_unique,让票自己作废。程序要显示下面三行。
出票:7 号 看演出 作废:7 号
1#include <iostream>2#include <memory>3 4class Ticket {5public:6 Ticket(int seat) : seat_(seat) {7 std::cout << "出票:" << seat_ << " 号\n";8 }9 ~Ticket() {10 std::cout << "作废:" << seat_ << " 号\n";11 }12 13private:14 int seat_;15};16 17int main() {18 Ticket* t = new Ticket(7);19 std::cout << "看演出\n";20 return 0;21}
修好它:复制不了的包裹
✓ship 要接管包裹,可这段代码编译不过。改一处,让程序显示下面三行。
在路上 已送达:小林 下一件
1#include <iostream>2#include <memory>3#include <string>4 5class Parcel {6public:7 Parcel(const std::string& to) : to_(to) {}8 ~Parcel() {9 std::cout << "已送达:" << to_ << '\n';10 }11 12private:13 std::string to_;14};15 16void ship(std::unique_ptr<Parcel> parcel) {17 std::cout << "在路上\n";18}19 20int main() {21 auto parcel = std::make_unique<Parcel>("小林");22 ship(parcel);23 std::cout << "下一件\n";24 return 0;25}
数一数主人
✓doc 是一份共享的文档。再建两个 shared_ptr 复制它(alice 和 bob),然后显示 doc.use_count()。程序要显示 3。
3
1#include <iostream>2#include <memory>3#include <string>4 5int main() {6 auto doc = std::make_shared<std::string>("周报");7 std::cout << doc.use_count() << '\n';8 return 0;9}
这一课学会了
- 智能指针在
<memory>里,自己死的时候把指着的对象delete掉,不用自己写delete。 auto p = std::make_unique<Order>(...);用法和普通指针一样:*p、p->show()、和nullptr比。unique_ptr在哪里死,对象就在哪里删;reset()或= nullptr可以提前删。unique_ptr不能复制(use of deleted function),用std::move转交,交出去的那个变空。- 函数可以返回
unique_ptr,也可以放进vector;参数写std::unique_ptr<T>表示「交给我管」。 shared_ptr可以复制,use_count()是主人数,最后一个主人死了才删对象。- 先想
unique_ptr;真有好几个主人才用shared_ptr;只借来用就传引用。