C++学习之路: 左值&右值 的讨论 和 ”move“ 值传递方式
本章我们讨论一下左值和右值, 剔除我们在学习C语言时养成一些错误常识。
先上代码
1 #include <iostream> 2 #include <string> 3 using namespace std; 4 5 6 //在c++98中,变量分为左值和右值,左值指的是可以取地址的变量,右值指的是非左值。二者的根本区别在于能否获取内存地址,能否赋值不是区分的依据。 7 8 //根据这个原则 我们尝试给以下2个变量和2个表达式 取地址 9 string one("one"); // 可取地址 显然这是一个左值 10 const string two("two");//可取地址 显然也是一个左值 11 string three() { return "three"; } //这个表达式返回值是string, 返回值是一个零时变量,不可取地址, 所以是右值 12 const string four() { return "four"; } //同3 13 14 void test(const string &s) 15 { 16 cout << "test(const string &s):" << s << endl; 17 } 18 19 void test(string &s) 20 { 21 cout << "test(string &s):" << s << endl; 22 } 23 24 25 int main(int argc, char const *argv[]) 26 { 27 28 29 test(one); 30 test(two); 31 test(three()); 32 test(four()); 33 34 35 return 0; 36 }
1 //打印结果 2 test(string &s):one 3 test(const string &s):two 4 test(const string &s):three 5 test(const string &s):four
从打印结果来看, 函数重载上, 输入参数为非const 形式时(包含修改语义), 对于右值(常量或者零时变量)含有修改语义的函数, 是不可接受右值的。
课堂总结:
10.在c++98中,变量分为左值和右值,左值指的是可以取地址的变量,右值指的是非左值。二者的根本区别在于能否获取内存地址,能否赋值不是区分的依据。 11.四个变量或者表达式 a)string one("foo"); b)const string two("bar"); c)string three() { return "three"; } d)const string four() { return "four"; } e)前两个为左值,后二者为右值 12.C++98中的引用分为两种“const引用”和“非const引用”,其中const引用,可以引用所有的变量。而后者只能引用非const左值,即one。 13.如果同时提供const引用和非const引用版本的重载,那么one会优先选择更加符合自身的非const引用。其他三个变量只能选择const引用。 14.上面反应了C++重载决议的一个特点:参数在通用参数和更加符合自身的参数之间,优先选择后者 |
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 using namespace std; 5 6 //void readFile(const string &filename, vector<string> &words) 7 vector<string> readFile(const string &filename) 8 { 9 vector<string> ret; 10 ret.push("cesfwfgw"); 11 12 //.... 13 // 14 15 return ret; 16 } 17 18 19 int main(int argc, const char *argv[]) 20 { 21 vector<string> coll = readFile("fef.text"); 22 return 0; 23 }
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 using namespace std; 5 6 void test(string &s) 7 { 8 9 } 10 11 12 int main(int argc, const char *argv[]) 13 { 14 test(string("hello")); 15 return 0; 16 }
C风格“hello” 被隐式转化成零时对象string(hello), 然后当做引用传递给test函数。 这里隐式转化也同样带来了这种问题。
如何避免上述开销呢?? 这里介绍一种 不同于复制的 一种“传递” 叫做 “move” ;
只要右值显式使用 T && 便可实现 move 值传递,来避免拷贝带来的巨大开销。
3.这段代码演示 move 和引用传递, 以及值拷贝传递的区别。
1 #include <iostream> 2 #include <string> 3 using namespace std; 4 5 6 string one("one"); 7 const string two("two"); 8 string three() { return "three"; } 9 const string four() { return "four"; } 10 11 void test(const string &s) 12 { 13 cout << "test(const string &s):" << s << endl; 14 } 15 16 void test(string &s) 17 { 18 cout << "test(string &s):" << s << endl; 19 } 20 21 void test(string &&s) 22 { 23 cout << "test(string &&s):" << s << endl; 24 } 25 26 void test(const string &&s) 27 { 28 cout << "test(const string &&s):" << s << endl; 29 } 30 31 32 int main(int argc, char const *argv[]) 33 { 34 35 36 test(one); 37 test(two); 38 test(three()); //传递右值临时变量 的引用 39 test(four()); //传递常量右值 零时变量的 引用 40 41 42 return 0; 43 }
three 和 four 两个表达式为右值, 在旧标准中 当作参数传递给 函数调用时 都是使用 复制的方式, 因为临时变量无法使用引用(因为这样做没有意义), 为了避免复制开销,
我们把函数重载了 Test(string &&) 的版本 和Test(const string &&) 当给这个函数传递 右值时调用这个版本, 内部实现我们尚不讨论, 当右值临时变量的引用传入后, 把
temp临时变量的值 直接 “move” 给 函数,然后 临时变量失效, 值已经 “传” 给函数, 临时变量失效是符合逻辑的, 因为它 马上就要被释放掉。
上例 three() 返回值 生成一个string 临时变量当作 右值引用 传给Test 函数, 随之把值传递给函数后失效, 避免了复制开销。
同理 four() 也是一样, 只不过是const 版本。
为什么不能把 左值 使用move 来避免开销呢? 因为 move 会使原变量失效, 即真正实现了 把大象 从冰箱A 移动到了 冰箱B, 传递后当然冰箱A中 便没有了 大象。
左值不使用 move方式 来避免失效, 因为通常左值在函数内部还有其作用, 失效后不可用是我们不想看到的结果。
然后临时变量不存在这种考虑, 因为它本来就是无名的 且 无保存价值 的变量, move后失效 随之被释放还能避免对象 析构的开销 何乐而不为呢?
编译时记得加后缀 -std=c++0x 因为这是C++11标准的新特性。
1 test(string &s):one 2 test(const string &s):two 3 test(string &&s):three //右值调用了右值的版本 4 test(const string &&s):four //同样const右值临时变量 也调用正确
打印结果正确, 编译器 聪明的 选择了正确的函数调用, 当参数是右值临时变量时, 选择了 T && 版本的函数 来 避免 无谓的 复制零时变量 这种行为, 节约了开销。
通常 const T && 这种方式 没有太大意义, 所以我们在后面讨论不在进行对其的演示。
4. 下面介绍 std::move 强制转化, 实现返回值 强制使用 "move" 来避免开销
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 using namespace std; 5 6 //void readFile(const string &filename, vector<string> &words) 7 vector<string> readFile(const string &filename) 8 { 9 vector<string> ret; 10 ret.push("cesfwfgw"); 11 12 //.... 13 // 14 15 return std::move(ret); //强制转化为右值引用 16 } 17 18 19 int main(int argc, const char *argv[]) 20 { 21 vector<string> coll = readFile("fef.text"); 22 return 0; 23 }
上述代码中 readfile 函数, return ret时 强制使用了 move方式, 让ret返回时不再使用 值拷贝方式, 因为本来ret 执行完 return 那行以后便要被释放, 直接move给 返回的零时变量 更加节约。
1 #include <iostream> 2 #include <string> 3 #include <vector> 4 using namespace std; 5 6 7 8 int main(int argc, const char *argv[]) 9 { 10 vector<string> coll; 11 coll.push_back("foo"); 12 13 //...... 14 15 vector<string> coll2(std::move(coll)); 16 //coll失效 17 18 return 0; 19 }
coll 失效后我们无法在 屏幕上打印 出“foo” , 因为coll 已经为空。