参考文章https://www.cprogramming.com/c++11/rvalue-references-and-move-semantics-in-c++11.html
问题引出
一个例子
problem.cc
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45
| #include <iostream> #include <string.h> class String { public: String(const char *s) { std::cout << "initlize\n"; size_ = strlen(s) + 1; data_ = new char[size_]; strcpy(data_, s); }
~String() { std::cout << "call deconstructor\n"; delete []data_; }
String(const String &s) { std::cout << "call copy constructor\n"; size_ = s.size_; data_ = new char[size_]; memcpy(data_, s.data_, size_); }
friend std::ostream& operator<<(std::ostream& out, const String &s) { if (s.size_ == 0 || s.data_ == NULL) { return out; } out << s.data_; return out; } public: int size_; char *data_; };
String test() { String str("hello"); return str; } int main(void) { String s = test(); std::cout << "in main function\n"; return 0; }
|
让我们编译运行一下:
需要注意使用编译选项 -fno-elide-constructors关闭RVO(Return Value
Optimization),否则现在的编译器会给你自动优化
1
| g++ problem.cc -o a.out -fno-elide-constructors
|
运行的结果如下:
1 2 3 4 5 6 7
| 第一行 initlize 第二行 call copy constructor 第三行 call deconstructor 第四行 call copy constructor 第五行 call deconstructor 第六行 in main function 第七行 call deconstructo
|
我们可以看到该程序调用了两次拷贝构造函数和三次析构函数。那么这个程序的流程是怎么样的呢?下面作者将对每一行输出做出解释。
输出解释
第一行
在调用test
函数之后,String str("hello")
会调用构造函数构造对象str
,对应输出的结果initlize
。
第二行
当test
函数返回str的时候,它返回的是一个临时对象,这个临时对象是通过对str
拷贝而构造的,而不是直接返回的str
本身。因为str
只是test
函数中的局部变量,它的作用域只在test
函数间,不可能跳到函数外再传给主函数中的s
,因此test
函数返回的只是一个临时变量。
第三行
上面已经阐明,test
函数返回的只是一个临时变量。所以当test
函数退出的时候,局部变量str
将会被析构。对应第三行的输出。
第四行
在main
函数中,test
返回的临时变量会通过拷贝构造函数赋值给s
,因此对应第四行中的拷贝构造函数。
第五行
当临时变量复制给s
之后,这个临时变量也就被析构了。因此对第五行的输出。
第六行
本行的输出在临时变量析构之后发生,在s
析构之前输出。
第七行
main
函数中的局部变量s
析构。
问题说明
我们可以看到,test
函数本来应该返回的str
,但其实返回的是一个临时变量,这就导致了不必要的拷贝构造。
那么我们有什么办法来解决这个问题呢?将test
函数的返回值变成引用类型String &
行不行呢?
这显然是不行的,因为str
只是一个临时变量,当test
函数结束的时候就会被释放,如果引用一个将被释放的值可能会出错。
因此对于临时变量这一问题,C++
引入了右值(rvalue)和std::move
这两个概念。
右值(rvalue)和std::move
什么是右值呢,它与左值相对于。左值也就是我们的变量之类的,它可以放在等号的左边。而右值可以理解为临时变量,C++
使用&&作为右值引用的符号。而std::move可以将左值变为右值。
算了,不想写了,就这样吧。把代码贴上。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| #include <iostream> #include <string.h> #include <string> class String { public: String(const char *s) { std::cout << "initlize\n"; size_ = strlen(s) + 1; data_ = new char[size_]; strcpy(data_, s); }
~String() { std::cout << "deallocate string:" << "\n"; delete []data_; }
String(const String &s) { size_ = s.size_; data_ = new char[size_]; memcpy(data_, s.data_, size_); std::cout << "allocate and copy string\n"; }
String(String &&s) { size_ = s.size_; data_ = s.data_; s.size_ = 0; s.data_ = nullptr; }
friend std::ostream& operator<<(std::ostream& out, const String &s) { if (s.size_ == 0 || s.data_ == NULL) { return out; } out << s.data_; return out; } public: int size_; char *data_; };
String test() { String str("test"); return str; }
int main(void) { String s("hello"); String b = std::move(s); std::cout << b << ", " << s << "\n";
std::string ss = "hle"; std::string bb(std::move(ss)); std::cout << bb << std::endl; String a("hello"); return 0; }
|