复制和移动
int 赋给另一个变量是复制一份,对象也一样:Playlist b = a; 以后,b 是一个新的歌单,里面的歌也是复制过去的。改 b 不影响 a:
1#include <iostream>2#include <string>3#include <vector>4 5class Playlist {6public:7 void add(const std::string& song) {8 songs_.push_back(song);9 }10 11 int count() const {12 return songs_.size();13 }14 15private:16 std::vector<std::string> songs_;17};18 19int main() {20 Playlist a;21 a.add("第一首");22 a.add("第二首");23 Playlist b = a;24 b.add("第三首");25 std::cout << a.count() << " " << b.count() << '\n';26 return 0;27}
2 3
点单步,看画面上 a 和 b 是两个对象,各有自己的一排歌。这一课讲复制是怎么发生的、怎么不让复制,以及比复制更省事的移动。
拷贝构造函数
用一个已有的对象做出一个新对象时,调用的是拷贝构造函数。上面没写,编译器替你生成了一个:一个成员一个成员地复制。自己写一个,让它显示一行,就能看见复制在什么时候发生:
1#include <iostream>2#include <string>3 4class Playlist {5public:6 Playlist(const std::string& name) : name_(name) {}7 8 Playlist(const Playlist& other) : name_(other.name_) {9 std::cout << "复制了 " << name_ << '\n';10 }11 12private:13 std::string name_;14};15 16void by_value(Playlist p) {17}18 19void by_ref(const Playlist& p) {20}21 22int main() {23 Playlist a{"跑步"};24 Playlist b = a;25 Playlist c{a};26 by_value(a);27 by_ref(a);28 return 0;29}
复制了 跑步 复制了 跑步 复制了 跑步
拷贝构造函数的参数是 const Playlist&:从 other 那里抄,不改它。Playlist b = a;、Playlist c{a}; 都是复制;按值传参也是复制(第 8 课说的「函数拿到的是一份复制」就是它做的)。by_ref 拿的是引用,没有复制。
check 调用了 3 次,一共显示几个「复制」?
1#include <iostream>2 3class Ticket {4public:5 Ticket(int number) : number_(number) {}6 7 Ticket(const Ticket& other) : number_(other.number_) {8 std::cout << "复制 ";9 }10 11private:12 int number_;13};14 15void check(Ticket t) {16}17 18int main() {19 Ticket t{7};20 for (int i = 0; i < 3; i++) {21 check(t);22 }23 std::cout << '\n';24 return 0;25}
check(Ticket t) 按值传参,每调用一次就复制一次。参数写成 const Ticket& t 就一个都没有了。
拷贝赋值
b = a; 和 Playlist b = a; 看起来很像,其实不一样:前者的 b 早就有了,是把 a 的内容抄过来盖掉旧的,调用的是拷贝赋值运算符 operator=:
1#include <iostream>2#include <string>3 4class Playlist {5public:6 Playlist(const std::string& name) : name_(name) {}7 8 Playlist(const Playlist& other) : name_(other.name_) {9 std::cout << "拷贝构造:" << name_ << '\n';10 }11 12 Playlist& operator=(const Playlist& other) {13 name_ = other.name_;14 std::cout << "拷贝赋值:" << name_ << '\n';15 return *this;16 }17 18private:19 std::string name_;20};21 22int main() {23 Playlist a{"跑步"};24 Playlist b{"睡前"};25 Playlist c = a;26 b = a;27 return 0;28}
拷贝构造:跑步 拷贝赋值:跑步
看等号左边是不是正在声明的新变量:Playlist c = a; 声明了新的 c,是拷贝构造;b = a; 的 b 已经存在,是拷贝赋值。和上一课的 += 一样,operator= 返回 *this 的引用。
= delete:不许复制
有些东西复制了就乱了。一个银行账户复制出一份,两份都有 100 块钱,到底算谁的?在拷贝构造函数和拷贝赋值后面写 = delete,就把它们删掉了,谁复制都是编译错误:
1#include <iostream>2#include <string>3 4class Account {5public:6 Account(const std::string& owner, int balance) : owner_(owner), balance_(balance) {}7 Account(const Account&) = delete;8 Account& operator=(const Account&) = delete;9 10 int balance() const {11 return balance_;12 }13 14private:15 std::string owner_;16 int balance_;17};18 19int main() {20 Account mine{"小林", 100};21 Account copy = mine;22 std::cout << copy.balance() << '\n';23 return 0;24}
main.cpp: In function 'int main()':
main.cpp:21:20: error: use of deleted function 'Account::Account(const Account&)'
21 | Account copy = mine;
| ^~~~use of deleted function 'Account::Account(const Account&)':用了被删掉的函数。不能复制的对象,传给函数时就用引用(const Account& 或 Account&)。
移动:搬走,不复制
复制一个装着几千首歌的歌单,要把每首歌都抄一遍。可如果原来那个反正不要了,直接把里面的东西搬过去就行,快得多。std::move(在 <utility> 里)就是说「这个可以搬走」:
1#include <iostream>2#include <string>3#include <utility>4#include <vector>5 6int main() {7 std::string draft = "今天的日记";8 std::vector<std::string> diary;9 diary.push_back(draft);10 diary.push_back(std::move(draft));11 std::cout << diary.size() << '\n';12 std::cout << "[" << draft << "]\n";13 return 0;14}
2 []
第一次 push_back(draft) 是复制,draft 还在;第二次 push_back(std::move(draft)),文字被搬进了 vector,draft 变空了。点单步,看 draft 在哪一步变空。
注意: 搬走以后的对象还能用来赋新值,也能正常死掉,但它里面是什么,标准只说「有效但不确定」(vector 的移动构造保证变空;string 在 g++ 上是空的)。所以搬走以后,别再读它,要用就先给它一个新值。
移动构造函数
自己的类也能写移动构造函数。它的参数是 Playlist&&,两个 & 表示「一个可以被搬走的东西」。做法是把每个成员也 std::move 过来:
1#include <iostream>2#include <string>3#include <utility>4#include <vector>5 6class Playlist {7public:8 Playlist(const std::string& name) : name_(name) {}9 10 Playlist(Playlist&& other) : name_(std::move(other.name_)), songs_(std::move(other.songs_)) {11 std::cout << "搬走了 " << name_ << '\n';12 }13 14 void add(const std::string& song) {15 songs_.push_back(song);16 }17 18 int count() const {19 return songs_.size();20 }21 22private:23 std::string name_;24 std::vector<std::string> songs_;25};26 27int main() {28 Playlist old_list{"跑步"};29 old_list.add("第一首");30 old_list.add("第二首");31 Playlist new_list = std::move(old_list);32 std::cout << new_list.count() << " " << old_list.count() << '\n';33 return 0;34}
搬走了 跑步 2 0
歌全都到了 new_list,old_list 剩 0 首。点单步,看 songs_ 那排格子从一个对象整个跑到另一个对象里,一首歌都没有复制。这里读 old_list.count() 只是为了看它空了:vector 搬走以后保证是空的。平时照上面的注意,别去读搬走的对象。
std::move 自己什么都不搬,它只是给对象贴上「可以搬」的标签;真正动手的是移动构造函数。要是你写了拷贝构造函数、却没写移动构造函数,贴了标签也只会复制。(两个都没写的话,编译器会自己生成一个移动构造函数。)
三五法则
析构函数、拷贝构造、拷贝赋值、移动构造、移动赋值,这五个是一伙的:自己写了其中一个,多半另外四个也要想一想。这叫「三五法则」(早先只有前三个,叫三法则)。
好消息是,大多数类一个都不用写。成员是 std::string、std::vector、int 这些时,编译器生成的那五个已经会正确地复制、搬走、收拾,这有时被叫做「零法则」。这一课自己写它们,只是为了让你看见它们什么时候被调用。
| 写法 | 调用的是 |
|---|---|
Playlist b = a; / Playlist b{a}; | 拷贝构造 |
b = a;(b 已经有了) | 拷贝赋值 |
Playlist b = std::move(a); | 移动构造 |
b = std::move(a);(b 已经有了) | 移动赋值 |
按值传参 f(a) | 拷贝构造 |
练一练
写好代码,点检查。提示随便看,实在想不出来再看答案。
修好它:账户不能复制
✓Account 不许复制,所以程序编译不过。不要改 Account,只改 report,让程序显示 小林:100。
小林:100
1#include <iostream>2#include <string>3 4class Account {5public:6 Account(const std::string& owner, int balance) : owner_(owner), balance_(balance) {}7 Account(const Account&) = delete;8 Account& operator=(const Account&) = delete;9 10 void show() const {11 std::cout << owner_ << ":" << balance_ << '\n';12 }13 14private:15 std::string owner_;16 int balance_;17};18 19void report(Account acc) {20 acc.show();21}22 23int main() {24 Account mine{"小林", 100};25 report(mine);26 return 0;27}
复制出来的是副本
✓给 Document 写一个拷贝构造函数:标题后面加上「(副本)」,正文照抄。程序要显示下面两行。
周末计划:去爬山 周末计划(副本):去爬山
1#include <iostream>2#include <string>3 4class Document {5public:6 Document(const std::string& title, const std::string& text) : title_(title), text_(text) {}7 8 void show() const {9 std::cout << title_ << ":" << text_ << '\n';10 }11 12private:13 std::string title_;14 std::string text_;15};16 17int main() {18 Document plan{"周末计划", "去爬山"};19 Document copy = plan;20 plan.show();21 copy.show();22 return 0;23}
搬走,别复制
✓Inbox 只有拷贝构造函数,所以 std::move 也只能复制。给它加一个移动构造函数,显示「搬走了」,把 messages_ 搬过去。程序要显示下面两行。(最后读 old_box 只是为了看它空了:搬走的 vector 保证是空的。)
搬走了 新:3 封,旧:0 封
1#include <iostream>2#include <string>3#include <utility>4#include <vector>5 6class Inbox {7public:8 Inbox(const std::vector<std::string>& messages) : messages_(messages) {}9 10 Inbox(const Inbox& other) : messages_(other.messages_) {11 std::cout << "复制了\n";12 }13 14 int count() const {15 return messages_.size();16 }17 18private:19 std::vector<std::string> messages_;20};21 22int main() {23 Inbox old_box{{"你好", "开会", "账单"}};24 Inbox new_box = std::move(old_box);25 std::cout << "新:" << new_box.count() << " 封,旧:" << old_box.count() << " 封\n";26 return 0;27}
这一课学会了
- 对象赋给新变量、按值传参,都会复制出一个独立的新对象,调用拷贝构造函数
T(const T& other)。 - 已经存在的对象
b = a;调用拷贝赋值T& operator=(const T& other),最后返回*this。 - 在拷贝构造和拷贝赋值后面写
= delete就不许复制(use of deleted function);这样的对象用引用传。 std::move(x)表示x可以搬走;移动构造函数T(T&& other)把成员搬过来,不复制。- 搬走以后的对象只能赋新值或者让它死,别再读它的值。
- 五个特殊成员函数(析构、拷贝构造、拷贝赋值、移动构造、移动赋值)写了一个就想想其余的;成员都是
string、vector这类时,一个都不用写。