Kodewick
第 21 课,共 30 课目录

智能指针

第 21 课,共 30 课 · 第 5 部分 · 内存和 lambda · 约 15 分钟

上一课的 new 和 delete,难的是记着 delete:早了不行,晚了不行,忘了也不行。智能指针替你记:它是一个装着指针的对象,自己死的时候(析构函数运行时),顺手把指着的东西 delete 掉。智能指针在 <memory> 里。

main.cpp
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:

main.cpp
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 是一个普通的局部变量,它在哪里死,对象就在哪里被删。用一对花括号围出一段,看得最清楚:

main.cpp
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}

不能复制

名字里的 unique 是「唯一」:一个对象只归一个 unique_ptr 管。要是能复制,两个 unique_ptr 都会在死的时候 delete 它,就成了上一课的「delete 两次」。所以它干脆不让复制:

main.cpp
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 交给另一个。交出去以后,原来那个就空了:

main.cpp
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 一个个死,对象一个个删:

main.cpp
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()),最后一个主人死了,才删对象。

main.cpp
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 它,那是智能指针的事。

练一练

写好代码,点检查。提示随便看,实在想不出来再看答案。做完点单步走到最后,看看堆上有没有剩下的块。

练习 1

交给 make_unique

程序用 new 建了一张票,可是忘了 delete。不要写 delete,把它改成 std::make_unique,让票自己作废。程序要显示下面三行。

要显示
出票:7 号
看演出
作废:7 号
exercise1.cpp
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}
输出
点「运行」或按 Ctrl+Enter
练习 2

修好它:复制不了的包裹

ship 要接管包裹,可这段代码编译不过。改一处,让程序显示下面三行。

要显示
在路上
已送达:小林
下一件
exercise2.cpp
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}
输出
点「运行」或按 Ctrl+Enter
练习 3

数一数主人

doc 是一份共享的文档。再建两个 shared_ptr 复制它(alice 和 bob),然后显示 doc.use_count()。程序要显示 3。

要显示
3
exercise3.cpp
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}
输出
点「运行」或按 Ctrl+Enter

这一课学会了

  • 智能指针在 <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;只借来用就传引用。