同学们好,这周我们来讲一讲c++11的这个新特性。 c++11是c++标准委员会在2011年发布的一个 很新的这个c++标准 那距离上一个c++的正式标准c++98的发布已经过了好多年,那实际上c++11的很多特性呢 在前好几年的各种c++编译器里面就获得了支持。 那完全支持c++标准的编译器 现在也有,就是这个gcc的4.8。我们看c++11都带来了那些好处啊。 啊第一个是它用了一个统一的初始化方法 可以用来初始化数组以及各种容器 比方说我们对一个整形数组arr进行初始化 只需要后面跟一个花括号然后把这个元素的值写进去就行啦 对于一个vector的变量iv可以用同样的方式初始化,所以这个初始化的方式是统一的嘛 甚至对于一个mp类型的这种容器也可以用花括号初始化,而且可以用花括号嵌套花括号的办法 那我们这条语句里面当然我们看到的就是这个mp容器里面有两个元素,一个元素呢 被初始化成a,也就是说这个元素它这个first的陈源边缘是1,second陈源边缘是a 那第二个元素first陈源边缘是2,second陈源边缘是b 然后我们再看这个对于这个string也可以用相同的方式进行初始化 最让人高兴的就是我们 对于new这种动态分配的数组啊现在也有了统一的初始化方法 额花括号,那现在我们在这里看到的这个new 这个数组呢,它的前三个元素被初始化成123,后面的元素就初始化成0 这个是解决了我们长久以来的一个很大的困惑啊 好了那我们下面再看这个额 另外一个例子,这里我们定义了一个类A,它有构造函数,用来初始化里面的i和j 然后呢我们甚至可以在 一个函数的return语句里面直接写一个初始化列表m ,n 它这样就能返回一个临时的A的对象,因为这块返回值说的是A对象所以你写这个 花括号m n辨析也能判断它是一个A对象 这个写法我觉得很别扭,也不推荐大家去疯狂的使用它,反正看的挺难受的 但是这个东西是挺不错的,就是A*pa=new A 这个初始化方法跟前面统一了,当然你这里写的是圆括号也是没有问题的 那c++11还从Java里面学来了一个好处,就是这个成员变量可以有一个默认的初始值 就像这个我们定义class B的时候它的成员变量m,我们可以直接在这个地方给它赋一个默认的初始值 那带来的好处就是当我们在main里面定义一个这个B的对象的时候我们也不需要 用构造函数去初始化里面的成员变量,在这种情况下这个B.m 它就自动有这个默认的初始值就是1234,所以你输出来就是1234 这个非常方便 我们再说一下这个auto关键字,在c++11 里面这个auto关键字它的含义跟我们原来 c语言和c++语言里面定义自动变量的那个auto是完全两回事了 那auto关键字呢它也是用于定义变量,但是它定义变量的时候这个变量不需要指定类型 编译器可以自动对这个变量进行类型的判别 当然有一个前提就是你定义这个变量的时候必须对这个变量进行初始化 比方说,我写auto i=100,那明显编译器可以推断这个i就是整形的对吧,所以i就是整形的 那下面呢你在这个auto p=new A 假设A是一个类啊,那当然辨析器也能判断出来,以为new A这个表达式的返回值 就是A*类型的对吧,所以这个时候p呢当然也就是A*类型的啊,这个很容易 那你来个auto k=34343LL那这个常数它是一个 带LL后缀的那它就是一个残整形,那当然k就是long long类型的吧 这些看上去还没有带来什么好处对吧,但实际上我们看这个地方 我们定义这个比较复杂的这个map 那我们要写一个这种map上面的迭代器就要把前面这一大串 都写一遍,或者你再来个title dede field什么之类的就稍微有点麻烦 但是有了auto以后呢,我们直接在这里就可以写 auto i=mp.begin,i=!mp.end而用这个方法来便利这个这个mp 在这里呢,编译器自动根据这个mp.begin就能判断出i的类型是什么 那i的类型是什么呢,当然就是这个map.string.int.greter上面这个的 迭代器,这一大长串的这种类型我们用一个auto迭代器对吧 给它替换掉了,这个时候我们就会感觉非常的方便 这个auto关键字它还有更大的 这个用处,不一定仅仅是用来让你少敲几个字符 比如说我们看这个例子,这有个class A,然后呢我们定义为一个加号,那在这个加号里面呢 我们把一个整型变量n和这个class A的对象给它加起来,然后我们返回 A对象,注意我们这个时候加号的返回值是class A的对象 然后我们写了一个模板,写一个模板叫add 做加法的,它能够把两个不同类型的变量给它加起来 然后我们返回这两个变量相加的这个值, 那这两个变量相加的值到底是什么类型的呢 不确定,如果按照我们以前学的模板的写法,我们这个位置呢 你要么就写T1,要么就写T2,要么就写woid对吧,你总得有一个什么类型或者是woid 但是有了auto关键字以后呢我们这个位置可以写auto,实际上这个位置 被从模板实地化出来以后返回值它甚至可以不是T1也不是T2,可以是一个别的类型都是有可能的 那也就是说编译器对这个add进行实地化的时候会自动去判别这个 add函数它的返回值应该是什么,它怎么自动判别呢,就通过下面这条语句 下面这个表达式 这里我们牵涉到一个新出现的东西叫作decltype 这个下一页片子还会讲,这个decltype是用来做类型判别的 那么在c++11里面你可以把函数的返回值写在函数的后面 就是通过一个剑号然后后面写一个比如说int什么之类的来指明这个函数的返回值 是某种类型的,那我们在这里写剑号 decltype(x+y)这个告诉编译器什么信息呢 它告诉编译器说,这个add它的返回值是 decltype(x+y)这种类型的,这个decltype(x+y)这个 关键字,它会根据(x+y)这个表达式的类型 去返回一个,代表一种类型吧,就是 decltype(x+y)这个式子就代表(x+y)这个表达式的类型 那我们在这边return x+y 跟这个 x+y是一致的,所以说编译器 以后从这个add模板实地化出来add函数的时候就会根据这 x+y的表达式 的类型去推断出这个函数返回值的类型 我们举具体例子来看,比如说这里有一个auto d=add(100.1.50) 那么这个add被实地化出来 以后这个 x+y 不就是100+1.5对吧 那么100+1.5是什么类型的呢 整形和复点数相加它的返回值当然就是浮点数类型对吧 这个1.5你可以看成是floot,也可能是double 这个差不多啦,我们不宜深究啦,假设它是double吧 那总而言之由于100+1.5的返回值是double的话 那么编译器就能自动判断我们从这个实地化出来的那个函数 它的返回值就是auto这个地方就用double去替换了,那当然 你来个auto d=什么东西那d呢就是double 但1.5也有可能被处理成floot,就这块 double也可能实际上是floot,这个可能不同编译器处理的方法不一样啊 好了那么下面我们再看这个,这个add,我们auto k =add,那就是说k的 类型就跟add函数的返回值的类型是一样的 那这个时候编译器就会从add模板实地化出来另外一个add函数 那另外那个add函数它的返回值是什么类型的呢,当然就是由x+y这个 这个表达式的类型所确定的,因为我们在这里说了这个函数的返回值是x+y这个表达式的类型 那x+y是什么呢,就等于是100加上A 对吧,100加上A这个东西怎么解释啊,我们前面也承载了一个加号 它能进行整数跟class A对象的相加,然后它的返回值是一个class A的对象 也就是说100+A它的返回值就是A 这种类型对吧,那自然,这块x+y的类型是A,那 这个add实地化出来以后它的返回值这个auto肯定 这个位置就相当是一个A啦,所以我们说这个k这个对象它的值它的类型也就是A啦 这是auto关键字相当的好用 那现在我们再解释一下这个 decltype这个关键字,它的作用就是返回表达式的类型 比方说我们前面定义了这个什么i啊double啊stract还有这个A*类型的变量A 那我们下面就可以写decltype(a) x1,这个时候我们是定义的x1这个 这个变量哦,但是X1它的类型是什么呢?它是由前面这个表达式所决定的, 由于,由于这个,由于这个a,这个东西它的类型是 是什么,是A*,对吧,所以说第一是要把type算出来,这个东西就是A*,所以X1就是A*。 然后我们再看这个type的,呃,decltype ( i), 这个i是什么东西呢?i前面是个int,对吧 所以说X2就是int了。然后这个再看,呃,这个X3 那X3类型就取决于这个a->x 类型,a->x 是什么? 是a里面的那个成员变量x,是double类型的,所以说X3就是double类型, 但是第一是要type有个很奇怪的地方就是当你把这个a->x 用括号括起来算作一个整体的时候, 这个时候呢,第一次要算出来的type就是double的引用了, 而不是double,所以这个时候 这个X4是一个 double类型的引用,啊,我们用t来进行初始化, 这里面有一点特别,那具体这个第一是要type的规则 还有点复杂,我这里就略过不讲了,同学们要是有兴趣,自己去查一查啊,告诉大家这一点稍微奇怪的地方而已。 啊,那接下来我们再说这个iii一个特别好的东西,叫做这个智能指针shared_ptr, 这个shared_ptr能解决什么问题呢?我们知道我们,呃,利用 出来的new空间,我们一定要记得delete,对吧,要不然就形成这个内存碎片, 那我们一旦new成本空间,我们就想着要在程序的各条执行路径上去, 都delete它,否则就会有问题。 当你在,你可以说我在对象运行期间,new的成本空间,我在这个类的 虚构函数里面去delete,这是一种解决办法,但它不能解决所有的问题,因为有时候你的new操作是 发生在某个全局函数里面的,对吧。 总而言之,呃,动态分配成本空间会造成很重的这个精神负担, 我有时候白天用了iii,用了new,我都半夜会爬起来看看我是不是什么地方没有delete,哪条执行上面没有delete, 所以这是一个问题。那有个shared_ptr,那么晚上就可以睡个踏实觉了, 那shared_ptr如何解决这个问题呢,呃,首先我们要使用这个shared_ptr 我们就得include头文件memory,然后这个shared_ptr呢它是一个类模板, 它是一个类模板, 那么它当然就有构造函数,我们通过shared_ptr的构造函数,还有一些其他的比如说 赋值运算符吧,就可以让shared_ptr对象托管,注意托管new运算符返回的指针, 啊,就是说它托管了一个你动态分布的存储空间, 啊,托管以后有什么好处呢? 我们接下来看啊,首先呢,它怎么托管呢?就是我们定义一个 shared_ptr的对象,然后在构造函数里面就可以去new一个什么T类型的对象出来。 那这个你要托管的是T类型的那种对象的指针,那我们在在把 这个shared_ptr实际上画出来的时候,这块就写一个T,那当然T可以是int,char各类型都可以, 那我们一旦做了这个操作以后啊, 刚才new出来的这一片存储空间就交给了这个ptr,这个对象托管了, 然后从此以后,这个ptr呢就可以向T类型的指针一样来使用了, 比如说你可以写*ptr,你就代表刚才new出来的这个对象。 呃,然后呢 不但这个ptr可以当做指针一样使用,而且从此以后你就不去 不必再去需要操心释放内存的事情了,你根本就不用管什么delete, 啊,当程序运行结束的时候,或者说这个这个ptr对象消亡的时候,自动就会把这个new 出来的存储空间,刚才new出来的存储空间给delete掉,你根本就不用关心这个delete的事情, 以上就轻松了很多。 呃,那么在iii里面啊,多个shared_ptr对象可以同时托管一个指针, 那这个指针当然是指向,指向new出来的一个什么东西呢,啊,一定的是指向new出来的一个什么东西。 呃,在多个shared_ptr对象同时托管 一个指针的情况下,这个系统会维护一个托管计数,啊,它就会算, 这个指针当前有多少个shared_ptr对象在托管它。 呃,当这个指针啊,变成没有shared_ptr对象在托管它的时候, 那么系统就会自动把这个指针给它delete掉,它会delete这个 指针的,所以当然这个指针就得是new出来的,否则delete就出错了。啊, 那在shared_ptr它解决了我们很大的问题,但是它也有一点不完美的地方, 就是这shared_ptr,它不能托管指向动态分配的数组的这个指针, 啊,这个美中不足啊。 你要让它托管指向动态分配的数组的指针,你的程序就会出错,啊,这个还是有待改进啊。 那我们接下来看看shared_ptr如何使用。 首先我们include头文件memory, 然后我们在这里有一个structA,它这个,有这个虚构函数, 输出什么它的成员变量n, destructor 啊,它有构造函数,用来初始化这个成员变量n的。 那好了,那我们在iii里面呢,我们可以new一个A对象, 然后把new出来这个A对象就交给了sp1去托管,准确的说是把 指向A对象的这个指针吧交给sp1托管,然后我们检测sp1托管了A2啊, 然后我们呢,可以,又用了另外一个shared_ptr的这个对象 sp2,我们注意把它实例化出来的时候,这块是A对吧,因为你当初这边是new的是一个A的对象, 好,现在,现在这个sp2是用赋值构造函数初始化的,而且sp1是 参数,因此这个时候效果就是sp2和sp1就共同托管这个A2了, 那接下来我们把这个sp1所指向的这个n 和sp2所指向的这个n都给它输出,那结果就当然是1,啊,2, 我们注意这个shared_ptr的对象它可以像普通的指针那样来使用,否则不就不方便了对吧, 所以我们看,这个时候sp1所以它是一个对象,但是肯定在shared_ptr里面重载了这个键号, 所以说我们用起来就像指针一样就是sp1这个东西指向的n,那不就是前面那个A2里面那个n,就是2对吧。 所以我们对于shared_ptr完全就可以像指针一样来使用,特别方便。 好,接下来我们就让我们定义了一个新的shared_ptr的对象,sp3 呃,暂时还没有对它进行什么,初始化啊,然后呢,我们,呃, 下面这条语句是取得一个shared_ptr对象sp1所托管的那个指针, 也就是你知道了一个,一个shared_ptr 托管了某个东西,那你怎么把那个托管的指针 拿出来呢?就用,就用shared_ptr 的 get 成员函数,啊,那这时候p就指向了前面那个A2了, 所以我们输出p->n, 它也是2。 那前面不是定义了sp3对吧,在这里我们让sp等于sp1, 啊,这个赋值语句它指的sp3也托管了这个A2, 那这个时候A2就一共有三个shared_ptr对象在托管它,就是A1,啊,就是 sp1,sp2,sp3,所以我们输出*sp3.n,它跟sp->n是一样的, 我们只是说明一下sp3可以这么用,前面是可以加*的,这时候当然输出的结果也就是,啊, 也就是2,在这,啊,好那么一个shared_ptr对象可以放弃它所托管的那个 那个指针,嗯,好吧,那我现在这个sp1.reset reset这个函数如果没给参数的话,那么就意味着放弃它托管的这个东西, 也就是说这时候sp1就放弃托管A2, 啊,sp1也就放弃托管A2了。 呃,那这个时候A2还有sp1, 2和sp3在托管它,对吧,然后呢,呃, s,呃,对于shared_ptr 你可以用这个, 感叹号去判断它是否有托管一个指针啊, 就是如果这个sp1没有托管任何东西,那么!sp1它的返回值就会是TRUE, 那我们就这时候不妨把sp1称为一个空指针嘛,因为它没有指向任何东西,没有托管任何东西,也就是说没有托管任何东西。 那我们就刚才sp1执行了Reset,那它就放弃了托管,它就没有托管任何东西了, 所以下面这个非s,呃,null !sp1它的返回值就是true的, 那我们就输出一个 Sp1 is null 在这,sp1是null。呃,接下呢我们又,呃, new出来一个A3对象,它的地址呢用q来指着, 然后我让sp1reset q ,这时候sp1就去托管这个q了, 那我们在输出这个sp1的n是多少呢?那当然就是3,对吧,因为这个时候q前面是指向这个A3的。 呃,接下来我们让new又新生成一个对象sp4, sp4呢,它是以sp1作为赋值 构造函数的参数的,这个时候实际上就是sp4 和sp1共同托管这个A3。 然后我们再来一个Sp5, 呃,这个sp5呢,这个sp5呢,如果你想让这个sp5也共同去托管这个q, 你这么写的话是不合适的,会导致程序初始化,为什么会iii下页片子 再强调一下啊。那,那 sp5在这里就没有用了。好,下面我们再来sp1.Reset,这个时候sp1就放弃托管这个 A3了,呃,然后我们又输出一个 before end main, 这个时候注意,A3有 A3原来是sp1 sp4都在托管的,对吧,现在我们执行了 sp1.reset这个时候sp1就没有托管A(3)了,但是还有A4在 托管A(3)啊,然后我们就输出一个before end main,在这,然后我们执行了sp4.reset 这个时候啊,这是sp4啊,这个时候是sp4它放弃托管这个A(3)了 也就是这个A(3),前面new出来这个A(3)啊,已经没有需要的ptr对象再托管它了 在这种情况下,A(3)就会自动被delete掉 就是你new出来的东西你交给需要的ptr对象托管啦 后来这些对象又放弃了托管,导致你new出来的东西已经没有人托管它啦,那这个new出来的东西就会被自动delete掉 表现在程序的输出,我们就看到在这个end main这条语句之前就输出了一个3 destructor 为什么会有3 destructor啊,就是前面new出来这个A(3), 它被delete掉了,引发了析构函数的调用 然后输出end main,然后程序就结束啦 那程序结束了,那些前面出现的什么sp1,sp2,sp3就全部都消完了对吧,然后这些 前面那个new出来的A2是交给谁托管了呀,不是由sp3,sp2在托管它对吧 那是只要一个需要的ptr对象,它这个 它消完了,那它托管的那个指针就有可能被delete掉,为什么说是有可能而不是一定呢 因为有两个ptr对象可以同时托管一个指针对吧 那其中一个需要的ptr对象消完了它不应该把那个指针delete掉,那两个 托管同一指针的需要的ptr对象同时都消完了,那然后那个 先后都消完了,然后那个指针就没人托管了,在这种情况下 那个指针就会被delete掉 那当然我们这个程序结束的时候所有需要的ptr对象都消完,所以那个前面new出来的A 2也就会delete掉,虽然我们没有显示的去写delete什么 但是这个A2原来是交给sp2对象托管的,现在没人托管它了 那些需要的ptr的对象都消完了,那A2就会被自动delete掉,所以A2的 希望函数也会被调用,输出一个2 destructor 所以这个需要的ptr确实非常好使啊 那这个智能指针使用上有一些需要注意的地方我们来看一下啊 这里有一个class A,它由析构函数输出这个波浪线A 然后我们这里new出来一个class A的对象 然后我们通过这个ptr托管了new出来的这个对象 然后我们又定义了一个ptr2,然后通过ptr.reset(p) 想让ptr2也托管p 那表面上看起来好像这个时候应该能够做到ptr和ptr2共同托管p 但实际上 事实上却不是这个关系,就是这个时候呢你这么做 的话呢,系统并不会去增加ptr中对p的托管技术 所以这个时候在整个系统看来吧,ptr 2它托管了一个指针,ptr1这前面的ptr也托管了一个指针 虽然这两个指针是同一个地址 但是呢,这个需要的ptr系统它不这么认为 它并不知道这里的p跟这里的p是相同的 在这个需要的ptr系统看来 其中有一个ptr指针托管了p,另外一个指针ptr也托管了另一个指针p,虽然这两个p内容是一样的 但是需要的ptr的系统它不知道这一点 那么在这种情况下呢,当这个ptr消完的时候就会delete这个p 所以我们看到,当整个main结束的时候两个 需要的ptr对象都会消完,但ptr对象消完的时候就delete p 就输出了一次波浪线A,调用这个析构函数一次 当这个ptr2也消完的时候呢,它又去delete p,又会调用了析构函数一次 然后这个程序输出这些东西以后就崩溃了,为什么就崩溃了呢 就是因为p被delete了两次,啊我们看到 new出来的一个指针你只能delete一次,你要是去delete它两次,程序就会崩溃 两个波浪线A都是在end这条语句之后出现的,也就是说意味着是在main结束的时候这两个ptr 消完的时候才进行的这个delete 关键就在于这条语句在执行的时候参数就是一个光秃秃的p 这个编译系统也好,还是需要的ptr的实现系统也好,他不会去判断这里的p跟前面某一个需要的ptr对象 所托管的那个p是不是相同的,不做这种判断,所以说 效果就变成了这个ptr和ptr2,他分别托管的是 表面上看应该是不同的指针,然后还会去分别delete这两个指针,最后造成delete两次,程序就出错了 下面我们再看这个空指针nullptr,原来我们在 用空指针的时候直接就是大写的null就行了,现在呢我们有这个 空指针可以使用了,就是nullptr,那它的用法跟大写的null 有相似之处啊,我们现在对指针进行初始化想把它初始化成空指针就有这两种办法,啊你可以写nullptr 包括对于一种shared ptr的对象我们也可以让 它等于nullptr,那它就没有初始化啊没有托管任何东西 然后像这个一个是这种null,另外一个是nullptr 它两个也可以判断是否相等,只要p1和p2是相同类型的指针就可以判断是否相等 那判断是否相等算出来的结果是相等,也就是这个程序会输出equal 1,在这啊 那如果你要判断p3是不是等于null ptr,当然也是相等的,前面就是这么初始化的对吧,所以就会输出 这个equal 2,那你要判断p3是否等于p2 这个编译是会出错的,因为p3它的类型是一个需要ptr 对象,跟p2类型根本就不匹配,当然编译就会出错,编译出错啊 如果我们判断p3是不是null,这个时候呢也会输出这个 相同,虽然这里p3是个需要的ptr的对象 由于它被初始化成nullptr,然后我们这边是一种空指针,肯定是个等等号是经过某种 形式的重载的,总之会认为p3和这个null是相等的,所以会输出equal 4 然后这个nullptr它可以被自动转换成一个bool类型的变量,然后转换的结果就是false 这样转换过去b就是false,但是nullptr不能被自动转化成一个 整形,写这样一条语句编译的时候就会出错的,这就是nullptr空指针 那这个nullptr你也不能说它有特别大的作用啊,就是从形式上看起来 会好一点,因为原来那个null吧它就 不能很充分的体现指针的这种特点吧 好下面还有一个好东西,就是这个基于范围的for循环,它能够使得我们写for循环的时候 更轻松一些,我们看这个例子 这里面有一个class A,它有一个成员变量n在构造函数里面初始化了 然后我们这边有一个数组,ary,它里面有12345 现在我们就用新式的基于范围的for循环来便利这个 数组,我们怎么写呢,for int&e ary然后e乘等于10 那我们先看这块到底是什么意思,这块的意思就是说,对于ary里面的每一个 元素依次进行,然后这个元素呢就变成是e 而且 这里写的&就说明这里e是那个元素的引用 那也就意味着当你修改e的时候,那个ary数组里面的元素也就被修改了 所以我们在这里在这个for循环里面执行了e乘等于10,由于这个e是引用 实际上这个时候就会对于这个数组里面的每一个元素一次都把它乘等于10 也就是说这个循环执行完以后吧,这个ary这个数组他就变成什么呢,他就变成了10,20,30,40,50了 好下面我们再变一次这个ary数组,只不过这个时候我们使用的这个 东西已经不是引用了 就是int,因为我们知道ary里面是int嘛 所以这块肯定写int,然后你这块可以写引用,如果你想改ary的值的话 如果你不想改ary的值你这里直接就不要写引用了 那现在我们在这个循环里面就把每一个ary数组的元素都给它输出了 ary数组元素就是e嘛,就是我们看到输出的结果就是10,20,30,40,50 好,再接着往下看,我们这里定义的一个vector st,这个st里面放着的内容就是把ary的给拷过来了对吧 那然后我们依然可以用这种基于范围的for循环去便利这个st 然后我甚至懒得指定这个 这个st里面的变量是什么类型,我就可以直接写auto and it 那这个it就代表这个st里面的变量 由于你这边写了&,所以说这it实际上就是代表st里面呢 每一个元素的引用,因此在这里我们让it.n乘等于10,实际上就会修改了st这个 vector里面的元素 那这个循环执行的结果就是,使得st里面的元素都被乘以10了 也就是st里面的元素就变成了什么100,200,300,400,500了 接下来我们就可以用另外一个循环来 输出st里面的内容,就这么写,这么写意味着对st里面每一个 元素,我们把这个元素称为it 那这个元素是A类型的,对吧 那我们前面这边写这个auto,是因为编译器它可以自动去判断,我们可以不写A 这里我们说对于st里面每一个元素it,就是A类型的,我们把这个it的n给它输出,那当然输出的结果就是 这个循环输出就是100,200, 300, 400, 500. 啊, 这个基于范围的for循环还是很不错的。 嗯,那下面还有一个特别重要的这个概念呢叫做这个 右值引用还有这个move语义,啊。右值引用。 嗯,那什么叫右值呢?一般来说你可以就是认为就是说如果一个 表达式能出现在等号的,赋值号的左边那它就是个左值。 它不能出现在赋值号的左边呢它就是个右值。你也可以说,如果一个地址不能,一个表达式不能取其地址, 那这个表达式就是一个右值。如果能取其地址,就是左值,啊。一般来说可以这么讲,哦。 嗯,那我们来看,如果有个class A,我们 并列一个class A 的引用。然后,让它等于这个临时的变量, 临时的class A的变量。这条语句编译是出错的。因为这个 A()它是一个临时的变量,它没有名字。 实际上你也不可以取它的地址,嗯,你不能取它的地址,啊。 所以这个临时变量,这个A()它是一个右值。 嗯,那我们,呃 前面学引用的时候,说的就是引用啦!没有说他是左值引用还是 右值引用。那在C++11里面引用了右值引用了这个概念以后, 那我们就的知道我们前面学到的那些引用,全部都是左值引用。所谓左值引用 就是说,它只能引用左值不能引用右值。那我们在这里定义的A&r,这个r 这个r是我们前面所学过的那种引用,对吧?那它就是一种左值引用。而我们这边这个A() 是一个右值,它是临时变量无名变量是个右值。那么你在这里让一个左值引用去引用一个右值, 这个时候编译就会出错。那在C++11里面,就有右值引用这种东西。 那顾名思义右值引用就可以用来引用右值,对吧? 那么在这里我们就定义了右值引用,右值引用个左值引用的区别 就是它有两个&号,啊。它不是引用了引用,啊,它是右值引用。 那现在这个r就是右值引用,那让我们来看等于A(), 诶,这就是可以的了,右值引用它可引用右值。 这么一看又好像很无聊,啊,拿这个右值引用到底是干嘛使的呢? 实际上右值引用它的主要目的是提高程序的运行效率, 嗯,它能够减少需要进行深拷贝的对象进行深拷贝的次数。 嗯,那右值引用它的概念还挺复杂的。 如果大家要进行详细的了解,可以看我列出来的这连个参考,啊。 嗯,那右值引用, 嗯,它的作用是提高程序的效率。什么叫减少需要进行深拷贝的对象及深拷贝的次数呢? 什么叫深拷贝啊?就是,就是如果你写了一个类,这个类里面有一个指针成员变量, 然后这个指针成员变量呢可能会指向动态分配的一个存储空间。 那么,在这种情况下当你这个类的赋值构造函数,啊,在对对象进行赋值的时候, 就不是进行浅拷贝。就是简简单单把两个字变得一模一样,这就叫浅拷贝。 这种情况下我们一般不会用浅拷贝,而会用深拷贝。也就是说,你要把A对象赋值到B,那你一般来说就需要把 A对象里面的那个指针指向了存储空间里面的东西也赋值到 B里面的那个指针所指向的存贮空间里面去,这就叫做深拷贝,那深拷贝很显然是很需要花费时间的,对吧? 那又有的时候我们写了一个类,里面有指针成员变量, 可能在这个类的赋值构造函数,或者说赋值号被调用的过程中 需要进行深拷贝,这个效率就比较低。然后,有的深拷贝呢其实是不需要的。 那我们如何去去掉这种不需要的深拷贝,就是右值引用所做的事情。那实际上现在有一些编译器,它 它就就能够在某些情况下去自动的去优化,减少你做深拷贝的次数。 那C++11这个提供了右值引用和这边的move语句, 就让程序员自己可以有意识的进行减少这个深拷贝的操作。 那下面呢咱们来看看一个右值引用, 以提高程序效率的具体例子。啊,我们还是通过我们的老朋友就是自己写的这个String类 来进行这个说明,啊。那我们自己写的String类嘛里面就有一个 嗯, str这样一个指针成员变量,啊。它指向动态分类的存储空间。 存储空间。我们就用这个动态的存储空间用来放字符串。 所以我们在这个String的无参构造函数里面,我们就已经 动态分配存储空间就只有一个字符,这个字符就是0. 那么也就是说这个时候,这个str就指向一个空的字符串。 嗯,就是对于这种String的对象来说, 嗯,它既然代表一个字符串,那它里面起码也得是一个空串。 你让这个str在某种场合变成NULL 我认为是不太合理的这个设计。就让它,无论如何str都会指向一个字符串,啊。如果你 没有去显示的初始化它,它就只是个空串,这样子比较合理的。 如果str它总是指向一片存储空,那么,我们在定义了str 的时候都不用去判断它是不是NULL。反正就是会 省事儿。那我们再看这边,嗯,另外一个构造函数就是用一个 嗯,Const char*去初始化它,那当然我们就要把这个 str指向new出来的一片存储空间,它要足够大才能放的下 嗯,这个s的长度再加上1,啊,用来放那个iii的。然後,我们用strcopy把s的内容给拷过来就行了。 那有的同学在写这个题的时候可能 可能会去判断这个s是不是等等NULL,如果是等等NULL那就什么都不做,啊。 嗯,这个个人认为这是没有什么必要的,如果S等于NULL你就直接让它出错好了,算了,你不用管它。 嗯,说S等于NULL的时候,我还得处理一下他部署说,不,不。不要做这些事情。 你可以想象strcopy这个C语言的阔函数,如果你这块给的是NULL, 或者你这块给的是NULL它也一定出错的。strcopy在执行的过程中也不会去判断这两个参数到底是不是NULL。 所以该出错的地方就让它出错,啊,有的,有的责任是调用我们这个 函数或者是用我们这个类的程序员呢,我们就不必去为他去考虑问题。 我怕们就简单一点,根本就不用怕它s是不是NULL。 嗯,还有一个赋值构造函数在这儿,赋值构造函数我们输出一个copy constructor called。 然後我们这里当然也就新分配一个存储空间,要足够大,对吧? 嗯,然后呢我们就把s 里面的str的内容给拷贝到这个str里面去,啊。这就完成了这个 赋值就是深拷贝。 我们在这里同样,我们也不用去判断s.str是不是NULL,为什么? 因为s.str肯定不会是NULL。因为我们在String的所有的构造函数里面都使得 s.str不是NULL。所以,s.str不会是NULL。 嗯, 接下来再看有一个赋值号我们必须需要进行重载的。对不对?它可以 用于两个,嗯,String对象之间的赋值,两个String对象 之间互相赋值的话,我们知道是需要进行深拷贝的,啊。 所以我们得重载这个赋值号。先从一个copy operator=called,然后 就是要判断end赋值号两边的东西是同一个对象,那就不合适了。所以我们要判断 str是不是等于s.str。只有在不等的情况下,我们才需要做事情。 在这里我们就把原来str给它的delete掉,在这里我们也不用去判断 这个东西是不是等等NULL。为什么呢?因为我们的构造函数确保了这个东西不会是NULL。 所以我们直接定义它就行了。嗯,然後我们就新分配一个存储空间,啊,它足够大。 然後,把这个s.str的内容拷过来。嗯,这里s.str也不会是NULL。 所以这些带来很大的方便。然後最后return *this,它的返回就是引用。 这都是传统的写法啦!那现在有了右值引用以后呢, 嗯,我们可以先写一个 看上去很像赋值构造函数啊,但实际上不叫赋值构造函数,它叫移动构造函数。 因为就是说,它的参数是一个右值引用。啊,两个&的就是右值引用。 所以前面这里所提到的进行的是深拷贝,它是比较花时间的。现在我们想要像一个什么办法使得 在有些情况下呢我们不需要去进行这种深拷贝。而是进行所谓的移动的操作,啊。这就是所谓的移动语句。 所以我们写了一个移动的,这个,构造函数。 参数是一个右值引用,然後在这里面呢我直接就 把str指向了s.str指向的地方。就是这里是s, 嗯,它指向某一个存储空间。嗯,我这个 这是单填对象,我直接让它的str就指向了s指向的地方,对吧? 嗯,这里并没有进行什么拷贝的操作,而是直接呢我们 对初始化的这个对象它的str 就指向了s.str指向的地方,然後输出一个move constructor called。 然後我们把s.str,把它要改变了。 让它指向一片新的存储空间,这存储空间里面放着一个空串。 这就是我们的移动,嗯,构造函数所做的事情,它并没有进行 深拷贝,对吧?这个深拷贝是很花时间的。 然後同理,我们还需要写一个所谓移动的赋值号。 这个移动的赋值号它接受的参数也是右值引用。 然後在这种情况下我们先输出一个move operator called。然後我们做的事情跟那个赋值有点像。 但是呢,由于这是一个赋值号,所以我们要看到赋值号两边的东西。 千万可别是同一个对象的,对吧,在不是同一个对象的情况下,我们就把 当前对象原有的存储空间给它缩回,delete掉对吧!然后我直接按当前的对象str就指向了这个 s.str指向对象,然后我把s.str对象让它指向新分配的存储空间,里面放一个空串, 这个就是移动的赋值号。 然后接下来就是一个虚构函数,啊,这个虚构函数呢就直接delete str就行了,你也不用判断这个东西是什么, 好,呃, 那我们现在写出了这个iii,它加,加入了,所谓的移动赋值 号和移动构造函数,那我们看看这么写法的时序类 用起来的时候,它在什么情况下能够避免不必要的 生拷贝呢?我们看,接着往下看,在这里我们写了一个模版, 这个模版叫做moveswap就是 交换,它的作用是交换,a b两个实参的这个内容, 呃,但是它不会去进行生拷贝,而是进行所谓的,移动语义,所以我们称之为moveswap。 在这里面做了什么事情呢?先来一个临时变量temp,它是T类型的, 然后我们呃,对这个temp进行初始化的时候,写的是move A, 那同学们知道如果我们这里不写moveA而是直接写A的话,那这个temp是用什么构造函数来初始化呢? 当然就用赋值构造函数初始化对吧。 那如果你的赋值构造函数里面进行了生拷贝操作的话,那么这条语句就会引发生拷贝,那就会比较耗时。 但是我们这里写的并不是A而是move A,这个move是个什么东西呢? move在str里面定义好的一个函数模板, 这个move的作用是把一个左值引用 或者把一个左值变成一个右值, 啊,就是说,这个A是一个左值, 啊,或者叫左值引用,左值引用也是左值吧,呃,那我们, 经过一个move的操作,这个move表达式它的返回值就变成一个右值了, 那它同样是a,但它是一个右值, 那由于这个表达式的返回值是一个右值, 那我们前面所看到的普通构造函数,它的参数是 左值引用,所以在这一块就不会调用赋值构造函数了,那它会调用哪一个构造函数呢? 呃,就会调用我们前面所看到的这个 移动构造函数,因为我们这个移动构造函数,它的参数是一个右值引用,右值引用能够跟右值匹配。 那这个move a 是个右值,那我们应该调用移动构造函数,因为移动构造函数,它的参数是一个右值的 这个引用,跟这个右值啊,是匹配的。 那么也就是说,这个temp是用这个 移动构造函数初始化的,那在temp执行期间,我们考察一下发生了什么事情 就在这,并没有进行生拷贝的操作,对吧,但是它呢, 确实是把参数的内容, 它是让这个s就指向了,这个,它是让当前对象的s对象就指向了参数 s.str所指向的地方,然后它修改了参数的str指向的地方。啊 那也就是说经过这条执行的语句以后呢, 这个temp里面的这个,temp里面的这个str就会指向了原来 a.str所指向的那个地方,是吧。而且呢, a.str就被改成指向一个新的地方了,这就是, 这条初始化语句的效果,啊,它跟调用赋值构造函数相比呢, 它的效率更高,它 没有进行生拷贝,但是,它改变了a的内容, 那改变了a的内容,a的内容被改变了会不会变成一个问题呢?那就取决于 这个a会是一个什么东西了,对吧,如果a是一个临时变量, 它本来以后就再也不会用,是吧,那你把它的内容改变掉,当然就不会有什么问题对吧,那如果a它不是一个临时变量, 那这个时候就会出问题了,啊,就不合适了。好那么我们接下来看a等于move b, 啊那这个时候move b 这个东西返回值,它又是一个右值, 所以在这里,a等于move b,它不会调用我们前面的那个 传统的operator等于,而调用在这里的 move assignment。因为这里的参数是一个右值引用。 是一个move b 这个表达式是一个右值,所以它跟前面那个参数为,呃,这个 左值引用的这个operator等于是不匹配的,它跟参数为右值引用的operator等于是匹配的。 所以这个a等于move b会调用这个move assignment, 那么move assignment所做的事情跟刚才的赋值构造函数是很像的,对吧, 它直接让a的str指向了b的str指向的地方, 然后又让b呢,它里面的str指向了一个新的地方,啊,那新的地方里面放着一个空算, 这里面当然就没有进行这个生拷贝,提高了效率, 但是它改变了b的内容, 那改天b的内容在什么情况下不会造成麻烦呢?就是说b它本来就是一个临时对象, 它以后再也不会用到了,它很快就要,就要被销亡了,那你改了它,反正以后也不会用到它,那就无所谓了,对吧。 所以b应该是这个临时对象,这样的东西才不会有问题。 那b等于move temp就类似了,对吧,temp是一个左值经过move temp,它就 变成一个右值了,所以在这里也会执行这个move assignment, 那总而言之,现在在这里被调用的就是两个move assignment 和一个move contructor 在这个过程中一次生拷贝都没有进行, 那如果按照我们传统的写法你把这个,这个move都去掉直接就写abtemp的话,那么就会 怎么样呢?就会调用一次赋值构造函数,呃,调用两次传统的那个赋值号, 这样就会进行三次生拷贝的这个操作,当然效率就比这中moveswap要 低了,对吧。但是你能, 一定要注意你能使用这种move swap有个前提,这个前提就是这个a b的值,你不在乎它会被修改, 修改掉了不会有麻烦,那如果ab本身就是个临时变量,那你修改它们,当然就不会有麻烦了。 好了,我们就看这个move swap在什么情况下能起作用啊。 好,我们再看这个main里面,main里面我们再回顾一下右值引用的这个概念啊,比如说我们来了一个, 在这边定义了一个临时的对象,那它就是一个右值,那如果你洗写string & r 等于这个右值,这就会是错误的,因为这是一个左值引用,左值引用而这边是一个右值,这就是错的。 那我们来一个这个string s, 然后我们来s等于string OK啊这条语句,就是没问题的。 呃,而且这个时候呢,到底是哪一个啊operator等于被调用的呢, 由于我们这个string OK 是可以是一个临时变量, 它没有名字,它是一个右值,所以此处实际上是会调用那个 前面看到的,move operator等于,因此我们看到, 啊,因此我们看到啊这个程序在*之前就输出了一行move operator= called... 啊在这里,是调用的那个参数为右值的那个,那个赋值重载运算符。 好,接下来我们输出*,然后我们让一个右值引用, 把它等于一个这个临时的这个变量, 然后还可以输出这个临时变量, 输出r.str,我们发现输出的内容就是b,啊就这个名词变量它的引用还在起作用,还在引用这个临时变量, 然后我们让这个string s1, s2啊,呃,它这个分别是 等于hello和world,然后我们调用了moveswap(s1,s2) 那我们调用moveswap(s1,s2)的话呢, 呃,我们在输出, s2的内容,就发现已经是变成hello了,然后在moveswap执行的过程中 我们发现啊调用了一次move contructor, 就是move构造函数调用了两次这个 move的这个这个,调用了两次move的这个赋值运算符对吧。 然后s1,和s2内容被交换了啊,然后输出一个hello, 那,那我们写这样的代码的时候,实际上就是, 我这个程序员在有意识地避免生拷贝,是不是这样的, 那就是说这个s在moveswap执行的过程中 执行到这条语句的时候 a就已经被改了,也就是说这个s1就已经被改了。 但是我不在乎,为什么?反正我这个s1就是要跟s2内容交换的, 你把s1临时改成一个共算无所谓,等会它还能够变回来。对吧, 因此在这种情况下,我们调用move swap 虽然在执行 moveswap 过程中啊,ab都被临时地意外修改成了一个空算, 但这并不影响我们的程序运行的结果,反正s1 s2 就是那个ab本来就都是要内容被修改的。你在这个中间的过程中把它改成一个 别的什么空算,不影响最后的结果是不无关系的,带来的好处就是我们避免掉了在s1 s2的交换过程中 那种三次生拷贝,这当然就提高了程序的执行效率,对吧。所以这个我们这么做是没有问题的。 那我们就看到这个所谓的move语义 就是,就是通过这个operator等于 右值引用的operator等于和这种 move constructor它们体现出来的, 它和右值引用一块使呢, 就能够做到确实避免生拷贝的这种效果。才能够有效地提高程序的执行效率。