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++重载决议的一个特点:参数在通用参数和更加符合自身的参数之间,优先选择后者
 
 
 
 
在C++11中,对类型进行了更加细化的分类。  我们把上例的4个类型进行重新划分
                                        const                            non-const
lvalue                  two                                  one
 
rvalue                 three()                            four()
 
 
 
所以: one 是 非常量-左值, two是 常量-左值  ,three是 常量-右值, four是 非常量-右值;
 对应:              T &                             const T &              const T &&                    T &&
 双&&符号便是 右值引用
     
 引言:
 对于开销特别大的 复制,         例如vector<string> readFile(const string &filename)函数  ,此函数返回值为一个vector<string>对象的副本
 
    首先该函数内部返回  vector<string> ----->    return temp(为返回值的副本) -------->vec 再拷贝给 我们用于接收返回值的变量vector vec;一共进行了两次拷贝构造 和两次析构。
           想想我们如果把圣经的每个单词都存入vector, 然后在返回到main函数的vec, 那么用知乎上的比喻就好比 
    把大象从冰箱A 移动到冰箱B  
    ① 先用克隆技术 在冰箱B 克隆 一个大象的副本。
    ② 把冰箱A 的大象销毁
    ③从而实现了把大象从冰箱A 移动到冰箱B的假象 
 
通过上述的例子, 我们很好的理解的这种无谓的开销在程序的每一处都存在着,极大的影响着我们的效率, 好比读入圣经需要100M内存, 上述进行两次拷贝就消耗了300M内存,这种无谓的开销成本巨大。
 
好在C++11增加了一个新特性来解决这个问题。
 
下面让我们看一下代码。
 
1.这段代码,体现了复制零时对象带来的巨大开销。
 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 }

 

 再看这个 
 
2.隐式转换 同样带来无谓的复制开销
 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给 返回的零时变量 更加节约。

 在主函数中, coll 又使用 拷贝方式拷贝了 返回的临时变量,  不过通常聪明的编译器会在编译阶段 省去这种麻烦,  直接把返回值返回给 coll从而避免开销。 这里只讨论 非编译器优化的情况。
 
 
 
 5.  当然std::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 已经为空。

posted @ 2014-10-09 16:33  tilly_chang  阅读(425)  评论(0编辑  收藏  举报