深入理解C++11核心编程(三)---通用为本,专用为末
C++11的新特性具有广泛的可用性,可以与其他已有的,或者新增的语言特性结合起来进行自由的组合,或者提升已有特性的通用性。
继承构造函数
C++中的自定义类型--类,具有可派生性,派生类可以自动获得基类的成员变量和接口(虚函数和纯虚函数,这里指的是public派生)。不过基类的非虚函数则无法再被派生类使用了。这条规则对于类中最为特别的构造函数也不例外,如果派生类要使用基类的构造函数,通常需要在构造函数中显式声明。
struct A{A(int i){}};
struct B:A{B(int i):A(i)};
B派生于A,B又在构造函数中调用A的构造函数,从而完成构造函数的"传递"。在B中有成员的时候:
struct A{A(int i){}};
struct B:A{
B(int i):A(i),d(i){}
int d;
};
派生于结构体A的结构体B拥有一个成员变量d,那么在B的构造函数B(int i)中,我们可以在初始化其基类A的同时初始化成员d。
有的时候,我们的基类可能拥有数量众多的不同版本的构造函数,而派生类却只有一些成员函数时,那么对于派生类而言,其构造就等同于构造基类。
在派生类中我们写的构造函数完完全全就是为了构造基类。那么为了遵从于语法规则,我们还需要写很多的"透传"的构造函数。
struct A{
A(int i){}
A(double d,int i){}
A(float f,int i,const char*c){}
//...
};
struct B:A{
B(int i): A(i){}
B(double d, int i): A(d,i){}
B(float f,int i,const char*c): A(f,i,c){}
//...
virtual void ExtraInterface(){}
};
我们的基类A有很多的构造函数的版本,而继承于A的派生类B实际上只是添加了一个接口ExtraInterface.那么如果我们在构造B的时候想要拥有A这样多的构造方法的话,就必须一一"透传"各个接口。
事实上,在C++中已经有了一个好用的规则,就是如果派生类要使用基类的成员函数的话,可以通过using声明(using-declaration)来完成。
#include<iostream>
using namespace std;
struct Base{
void f(double i){
cout<<"Base:"<<i<<endl;
};
};
struct Derived:Base{
using Base:: f;
void f(int i){
cout<<"Derived:"<<i<<endl;
}
};
int main(){
Base b;
b.f(4.5); //Base:4.5
Derived d;
d.f(4.5); //Base:4.5
}
我们的基类Base和派生类Derived声明了同名的函数f,不过在派生类中的版本跟基类有所不同。派生类中的f函数接受int类型为参数,而基类中接受double类型的参数。我们这里使用了using 声明,声明派生类Derived也使用基类版本的函数f。这样一来,派生类中实际就拥有了两个f函数的版本。
C++11中,这个想法被扩展到了构造函数上。子类可以通过使用using声明来声明继承基类的构造函数。
struct A{
A(int i){}
A(double d,int i){}
A(float f,int i,const char*c){}
//...
};
struct B:A{
using A:: A;//继承构造函数
//...
virtual void ExtraInterface(){}
};
我们通过using A:: A的声明,把基类中的构造函数悉数继承到派生类B中。C++11标准继承构造函数被设计为跟派生类中的各种类默认函数(默认构造、析构、拷贝构造等)一样,是隐式声明的。意味着如果一个继承构造函数不被相关代码使用,编译器不会为其产生真正的函数代码。
不过,继承构造函数只会初始化基类中成员变量,对于派生类中的成员变量,则无能为力。只能通过初始化一个默认值的方式来解决。如果无法满足需求的话,只能自己来实现一个构造函数,以达到基类和成员变量都能够初始化的目的。
基类构造函数的参数会有默认值。对于继承构造函数来讲,参数的默认值是不会被继承的。事实上,默认值会导致基类产生多个构造函数的版本,这些函数版本都会被派生类继承。
struct A{
A(int a=3,double=2.4){}
}
struct B:A{
using A:: A;
};
我们的基类的构造函数A(int a=3,double=2.4)有一个接受两个参数的构造函数,且两个参数均有默认值。那么A到底有多少个可能的构造函数版本呢?
事实上,B可能从A中继承来的候选继承构造函数有如下一些:
A(int=3,double=2.4); 这是使用两个参数的情况。
A(int=3); 这是减掉一个参数的情况。
A(const A&); 这是默认的复制构造函数。
A(); 这是不使用参数的情况。
相应地,B中的构造函数将会包括以下一些:
B(int,double); 这是一个继承构造函数。
B(int);这是减少掉一个参数的继承构造函数。
B(const B&); 这是复制构造函数,这不是继承来的。
B(); 这是不包含参数默认构造函数。
可以看见,参数默认值会导致多个构造函数版本的产生,因此程序员在使用有参数默认值的构造函数的基类的时候,必须小心。
继承构造函数“冲突”的情况。通常发生在派生类拥有多个基类的时候。多个基类中的部分构造函数可能导致派生类中的继承构造函数的函数名、参数(有的时候,我们也称其为函数签名)都相同,那么继承类中的冲突的继承构造函数将导致不合法的派生类代码。
struct A{A(int){}};
struct B{B(int){}};
struct C:A,B{
using A:: A;
using B:: B;
};
A和B 的构造函数会导致C中重复定义相同类型的继承构造函数。可以通过显式定义继承类的冲突的构造函数,阻止隐式生成相应的继承构造函数来解决冲突。比如:
struct C:A,B{
using A:: A;
using B:: B;
C(int){} //其中的构造函数C(int)就很好地解决了继承构造函数的冲突问题。(为什么能够解决继承构造函数从图的问题。)
};
如果,基类的构造函数被声明为私有成员函数,或者派生类是从基类中虚继承的,那么就不能够在派生类中声明继承构造函数。此外,如果一旦使用了继承构造函数,编译器就不会再为派生类生成默认构造函数了,程序员必须注意继承构造函数没有包含一个无参数的版本。
#include<iostream>
using namespace std;
struct A{
A(int){
}
};
struct B:A{
using A:: A;
};
int main(){
B b;//B没有默认构造函数
B b(3);//构造函数。
}
委派构造函数
与继承构造函数类似,委派构造函数也是C++11中对C++的构造函数的一项改进,其目的也是为了减少程序员书写构造函数的时间。通过委派其他构造函数,多构造函数的类编写将更加容易。
一个代码冗余的例子:
#include<iostream>
using namespace std;
class Info{
public:
Info() :type(1), name('a'){ //一次初始化,可以初始化很多变量。
InitRest();
}
Info(int i):type(i), name('a'){
InitRest();
}
Info(char e):type(1), name('e'){
InitRest();
}
private:
void InitRest(){/*其他初始化*/
}
int type;
char name;
};
int main(){
return 0;
}
在代码中,我们声明了一个Info的自定义类型。该类型拥有2个成员变量以及3个构造函数。这里的3个构造函数都声明了初始化列表来初始化成员type和name,并且都调用了相同的InitRest。可以看到,除了初始化列表有的不同,而其他的部分,3个构造函数基本上是相似的,因此其代码存在着很多重复。
改进方法1:非静态变量的初始化
#include<iostream>
using namespace std;
class Info{
public:
Info(){
InitRest();
}
Info(int i): type(i){
InitRest();
}
private:
void InitRest(){
}
int type{1};
char name{'a'};
};
虽然构造函数简单了不少,但是每个构造函数还是需要调用InitRest函数进行初始化。能不能在一些构造函数中连InitRest都不用调用呢?
我们将一个构造函数设定为"基派版本",比如本例中的Info()版本的构造函数,而其他构造函数可以通过委派"基准版本"来进行初始化。
Info(){InitRest();}
Info(int i){this->Info(); type=i;}
Info(char e){this->Info(); name=e;}
我们通过this指针调用我们的"基准版本"的构造函数。但是一般的编译器都会阻止this->Info()的编译。原则上,编译器不允许在构造函数中调用构造函数,即使参数看起来并不相同。
还有一种是用placement new 来强制在本对象地址(this指针所指地址)上调用类的构造函数。这样,就可以绕过编译器的检查,从而在2个构造函数中调用我们的"基准版本"。但是在已经初始化一部分的对象上再次调用构造函数,却是危险的做法。
在C++11中,我们可以委派构造函数来达到期望的效果。C++11中的委派构造函数是在构造函数的初始化列表位置进行构造的、委派的。
#include<iostream>
using namespace std;
class Info{
public:
Info(){
InitRest();
}
Info(int i): Info(){
type=i;
}
Info(char e): Info(){
name=e;
}
private:
void InitRest(){
}
int type{1};
char name{'a'};
};
在 Info(int) 和 Info(char) 的初始化列表的位置,调用了"基准版本"的构造函数 Info() 。 这里我们为了区分被调用者和调用者,称在初始化列表中调用"基准版本"的构造函数为委派构造函数,而被调用的"基本版本"则为目标构造函数。在C++11中,所谓委派构造,就是指委派函数将构造的任务委派给了目标构造函数来完成这样一种类构造的方式。
委派构造函数只能在函数体中为 type、name 等成员赋初值。 这是由于委派构造函数不能有初始化列表造成的。在C++中,构造函数不能同时"委派"和使用初始化列表,所以如果委派构造函数要给变量赋初值,初始化代码必须放在函数体中。比如:
struct Rule1{
int i;
Rule1(int a):i(a){}
Rule1():Rule1(40),i(1){}//无法通过编译
Rule1的委派构造函数Rule1() 的写法就是非法的。我们不能在初始化列表中既初始化成员,为委托其他构造函数完成构造。
(初始化列表的初始化方式总是优于构造函数完成的(实际上在编译完成时就已经决定了))
稍微改造一下目标构造函数,使得委派构造函数依然可以在初始化列表中初始化所有成员。
class Info{
public:
Info():Info(1,'a'){}
Info(int i): Info(i,'a'){}
Info(char e): Info(1,e){}
private:
Info(int i,char e): type(i), name(e){/*其他初始化*/}
int type;
char name;
//...
};
我们定义了一个私有的目标构造函数Info(int,char), 这个构造函数接受两个参数,并将参数在初始化列表中初始化。由于这个目标构造函数的存在,我们可以不再需要InitRest函数了,而是将其代码都放入Info(int,char)中。这样,其他委派构造函数就可以委派该目标构造函数来完成构造。
在使用委派构造函数的时候,我们建议程序员抽象出最为"通用"的行为做目标构造函数。这样做一来代码清晰,二来行为也更加正确。由于在C++11中,目标构造函数的执行总是先于委派构造函数而造成的。因此避免目标构造函数和委派构造函数体中初始化同样的成员通常是必要的,
在构造函数比较多的时候,我们可能会拥有不止一个委派构造函数,而一些目标构造函数很可能也是委派构造函数,这样一来,我们就可以在委派构造函数中形成链状的委派构造关系。
class Info{
public:
Info(): Info(1){} //委派构造函数
Info(int i): Info(i,'a'){} //即是目标构造函数,也是委派构造函数
Info(char e): Info(1,e){}
private:
Info(int i,char e): type(i), name(e){/*其他初始化*/}//目标构造函数
int type;
char name;
};
链状委托构造,这里我们使Info() 委托Info(int)进行构造,而Info(int)又委托Info(int,char)进行构造。在委托构造的链状关系中,就是不能形成委托环。比如:
struct Rule2{
int i,c;
Rule2():Rule2(2){}
Rule2(int i):Rule2('c'){}
Rule2(char c):Rule2(2){}
};
Rule2定义中,Rule2()、Rule2(int)和Rule2(char)都依赖于别的构造函数,形成环委托构造关系。这样的代码通常会导致编译错误。委托构造的一个很实际的应用就是使用构造模板函数产生目标构造函数。
#include<list>
#include<vector>
#include<deque>
using namespace std;
class TDConstructed{
template<class T>TDConstructed(T first, T last):l(first,last){} //尽可能还是多理解这个地方
list<int> l;
public:
TDConstructed(vector<short> &v):TDConstructed(v.begin(),v.end()){}
TDConstructed(deque<int> &d):TDConstructed(d.begin(),d.end()){}
};
我们定义了一个构造函数模板。通过两个委派构造函数的委托,构造函数模板会被实例化。T会分别被推导为 vector<short>::iterator 和 deque<int>::iterator 两种类型。这样一来, 我们的TDConstructed类就可以很容易地接受多种容器对其进行初始化。
(委托构造使得构造函数的泛型编程成为了一种可能)
在异常处理方面,如果在委派构造函数中使用try的话,那么从目标构造函数中产生的异常,都可以在委派构造函数中被捕捉到。我们看下面的例子:
#include <iostream>
using namespace std;
class DCExcept{
public:
DCExcept(double d)
try: DCExcept(1,d){
cout<<"Run the body."<<endl;
//其他初始化
}
catch(...){
cout<<"caught exception."<<endl;
}
private:
DCExcept(int i,double d){
cout<<"going to throw!"<endl;
throw 0; //抛出异常。
}
int type;
double data;
};
int main(){
DCExcept a(1.2);
}
我们在目标构造函数DCException(int,double)跑出了一个异常,并在委派构造函数DCExcept(int)中进行了捕捉。而委派构造函数的函数体部分的代码并没有被执行。这样的设计是合理的,因为如果函数体依赖于目标构造函数构造的结果,那么当目标构造函数构造发生异常的情况下,还是不要执行委派构造函数函数体中的代码为好。
右值引用:移动语义和完美转发
指针成员与拷贝构造
对C++程序员来说,编写C++程序有一条必须注意的规则,就是在类中包含了一个指针成员的话,那么就要特别小心拷贝构造函数的编写,因为一不小心,就会出现内存泄露。
#include <iostream>
using namespace std;
class HasPtrMem{
public:
HasPtrMem(): d(new int(0)){}
~HasPtrMem() {
delete d;
}
int *d; //指针成员d
};
int main(){
HasPtrMem a;
HasPtrMem b(a);
cout<<*a.d<<endl;//0
cout<<*b.d<<endl;//0
}
我们定义了一个HasPtrMem的类。这个类包含一个指针成员,该成员在构造时接受一个new操作分配堆内存返回的指针,而在析构的时候则会被delete操作用于释放之前分配的堆内存。在main函数中,我们声明了HsaPtrMem类型的变量a,又使用a初始化了变量b。按照C++语法,这会调用HasPtrMem的拷贝构造函数。(这里的拷贝构造函数由编译器隐式生成,其作用是执行类似于memcpy的按位拷贝。这样的构造方式有一个问题,就是a.d和b.d都指向同一块堆内存。因此在main作用域结束的时候,a和b的析构函数纷纷被调用,当其中之一完成析构之后(比如b),那么a.d就成了一个"悬挂指针",因为其不再指向有效的内存了。那么在该悬挂指针上释放内存就会造成严重的错误。
这样的拷贝方式,在C++中也常被称为"浅拷贝"。而在为声明构造函数的情况下,C++也会为类生成一个浅拷贝的构造函数。通常最佳的解决方案是用户自定义拷贝构造函数来实现"深拷贝":
#include <iostream>
using namespace std;
class HasPtrMem{
public:
HasPtrMem(): d(new int(0)){
cout<<"Construct:"<<endl;
}
HasPtrMem(HasPtrMem&h): d(new int(*h.d)){
cout<<"Copy construct:"<<endl;
} //拷贝构造函数,从堆中分配内存,并用*h.d初始化
~HasPtrMem() {
delete d;
}
int *d; //指针成员d
};
int main(){
HasPtrMem a;
HasPtrMem b(a);
cout<<*a.d<<endl;//0
cout<<*b.d<<endl;//0
}
(问题:浅拷贝和深拷贝 的差别)
我们为HasPtrMem添加了一个拷贝构造函数。拷贝构造函数从堆中分配内存,将该分配来的内存的指针交还给d, 又使用*(h.d)对 *d进行了初始化。通过这样的方法,就避免了悬挂指针的困扰。
拷贝构造函数中为指针成员分配新的内存再进行内容拷贝的做法在C++编程中几乎被视为不可违背的。不过在一些时候,我们确实不需要这样的拷贝语义。
#include <iostream>
using namespace std;
class HasPtrMem{
public:
HasPtrMem(): d(new int(0)){
cout<<"Construct:" << ++n_cstr<<endl;
}
HasPtrMem(const HasPtrMem&h): d(new int(*h.d)){
cout<<"Copy construct:"<< ++n_cptr<<endl;
} //拷贝构造函数,从堆中分配内存,并用*h.d初始化
~HasPtrMem() {
cout<<"Destruct:"<<++n_dstr<<endl;
}
int *d;
static int n_cstr;
static int n_dstr;
static int n_cptr;
};
int HasPtrMem::n_cstr=0;
int HasPtrMem::n_dstr=0;
int HasPtrMem::n_cptr=0;
HasPtrMem GetTemp(){
return HasPtrMem();
}
int main(){
HasPtrMem a=GetTemp();
}
(回顾:静态变量和非静态变量)
数据成员可以分静态变量、非静态变量两种.
静态成员:静态类中的成员加入static修饰符,即是静态成员.可以直接使用类名+静态成员名访问此静态成员,因为静态成员存在于内存,非静态成员需要实例化才会分配内存,所以静态成员不能访问非静态的成员..因为静态成员存在于内存,所以非静态成员可以直接访问类中静态的成员.
非成静态员:所有没有加Static的成员都是非静态成员,当类被实例化之后,可以通过实例化的类名进行访问..非静态成员的生存期决定于该类的生存期..而静态成员则不存在生存期的概念,因为静态成员始终驻留在内容中..
我们声明了一个返回一个HasPtrMem变量的函数。为了记录构造函数、拷贝构造函数,以及析构函数调用的次数,我们用了一些静态变量。在main函数中,我们简单地声明了一个HasPtrMem的变量a,要求它使用GetTemp的返回值进行初始化。
//正常情况下的输出:
Construct:1
Copy construct:1 //这个是临时对象的构造
Destruct:1 //这个应该是临时对象的析构
Copy construct:2
Destruct:2
Destruct:3
但是在C++11或者非C++里面的结果
只是一个浅拷贝
这里的构造函数被调用了一次,是GetTemp函数中HasPtrMem()表达式显示地调用了构造函数而打印出来的。而拷贝构造函数则被调用了两回。一次是从GetTemp函数中HasPtrMem()生成的变量上拷贝构造出来一个临时值,以用做GetTemp的返回值,而另一次则是由临时值构造出main中变量a调用的。对应的,析构函数也就调用了3次。
ttt.jpg-132.9kB
最头疼的就是拷贝构造函数的调用。在上面的代码上,类HasPtrMem只有一个Int类型的指针。如果HasPtrMem的指针指向非常大的堆内存数据的话,那么拷贝构造函数就会非常昂贵。可以想象,一旦这样,a的初始化表达式的执行速度非常慢。临时变量的产生和销毁以及拷贝的发生对于程序员来说基本上是透明的,不会影响程序的正常值,因而即使该问题导致程序的性能不如预期,也不易被程序员察觉(事实上,编译器常常对函数返回值有专门的优化)
然后,按照C++的语义,临时对象将在语句结束后被析构,会释放它所包含的堆内存资源。而a在拷贝构造的时候,又会被分配堆内存。这样意义不大,所以,考虑在临时对象构造a的时候不分配内存,即不使用拷贝构造。
剩下的就是移动构造:
tt1.jpg-313.6kB上半部分从临时变量中拷贝构造变量a的做法,即在拷贝时分配新的堆内存,并从临时对象的堆内存中拷贝内容至a.d。而构造完成后,临时对象将析构,因此,其拥有的堆内存资源会被析构函数释放。
下半部分,在构造函数时使得a.d指向临时对象的堆内存资源。同时我们保证临时对象不释放所指向的堆内存,那么,在构造完成后,临时对象被析构,a就从中"偷"到了临时对象所拥有的堆内存资源。
在 C++11 中,这样的"偷走"临时变量中资源的构造函数,就被称为"移动构造函数"。
#include <iostream>
using namespace std;
class HasPtrMem{
public:
HasPtrMem(): d(new int(3)){
cout<<"Construct:" << ++n_cstr<<endl;
}
HasPtrMem(const HasPtrMem&h): d(new int(*h.d)){
cout<<"Copy construct:"<< ++n_cptr<<endl;
} //拷贝构造函数,从堆中分配内存,并用*h.d初始化
HasPtrMem(HasPtrMem &&h):d(h.d){
h.d=nullptr;//将临时值得指针成员置空。
cout<<"Move construct:"<<++n_mvtr<<endl;
}
~HasPtrMem() {
delete d;
cout<<"Destruct:"<<++n_dstr<<endl;
}
int *d;
static int n_cstr;
static int n_dstr;
static int n_cptr;
static int n_mvtr;
};
int HasPtrMem::n_cstr=0;
int HasPtrMem::n_dstr=0;
int HasPtrMem::n_cptr=0;
int HasPtrMem::n_mvtr=0;
HasPtrMem GetTemp(){
HasPtrMem h;
cout<<"Resource from"<<__func__<<":"<<hex<<h.d<<endl;
return h;
}
int main(){
//HasPtrMem b;
HasPtrMem a=GetTemp();
cout<<"Resource from"<<__func__<<":"<<hex<<a.d<<endl;
}
这里其实,就多了一个构造函数HasPtrMem(HasPtrMem&&), 这个就是我们所谓的移动构造函数。与拷贝构造函数不同的是,移动构造函数接受一个所谓的"右值引用"的参数,关于右值,读者可以暂时理解为临时变量的引用。移动构造函数使用了参数h的成员d初始化了本对象的成员d(而不是像拷贝构造函数一样需要分配内存,然后将内存一次拷贝到新分配的内存中),随后h的成员d置为指针空值nullptr。完成了移动构造函数的全过程。
所谓的偷堆内存,就是指将本对象d指向h.d所指的内存这一条语句,相应的,我们还将h的成员d置为指针空值。
//理论上的结果:
Construct:1
Resource from GetTemp:0x603010
Move construct:1
Destruct:1
Move construct:2
Destruct:2
Resource from main:0x603010
Destruct:3
//实际上的结果:似乎只要涉及到需要临时变量的生成的时候,都会有问题。
Construct:1
Resource from GetTemp:0x603010
Resource from main:0x603010
Destruct:1
如果堆内存不是一个int长度的数据,而是以MBty为单位的堆空间,那么这样的移动带来的性能提升是非常惊人的。
如果传了引用或者指针到函数里面作为参数,效果虽然不差。但是从使用的方便性上来看效果却不好,如果函数返回临时值的话,可以在单条语句里面完成很多计算,比如可以很自然地写出如下语句:
Caculate(GetTemp(), SomeOther(Maybe(),Useful(Values,2)));
但如果通过传引用或者指针的方法而不返回值的话,通常就需要很多语句来完成上面的工作。
string*a; vector b;//事先声明一些变量用于传递返回值
...
Useful(Values,2,a);//最后一个参数是指针,用于返回结果
SomeOther(Maybe(),a,b);//最后一个参数是引用,用于返回结果
Caculate(GetTemp(), b);
当声明这些传递返回值的变量为全局的,函数再将这些引用和指针作为返回值返回给调用者,我们也需要Caculate调用之前声明好所有的引用和指针。函数返回临时变量的好处就是不需要声明变量,也不需要知道生命期。程序员只需要按照最自然的方式,使用最简单语句就可以完成大量的工作。
然后,移动语义何时会被触发。之前我们只是提到了临时对象,一旦我们用到的是个临时变量,那么移动构造语义就可以得以执行。**那么,在C++中如何判断产生了临时对象?如何将其用于移动构造函数?是否只有临时变量可以用于移动构造?.....
在C++98/03的语言和库中,以及存在了一些移动语义相关的概念:
A. 智能指针的拷贝(auto_ptr "copy")
B. 链表拼接(list::splice)
c. 容器内的置换(swap on containers)
这些操作都包含了从一个对象到另一个对象的资源转移的过程,唯一欠缺的是统一的语法和语义的支持,来使我们可以使用通用的代码移动任意的对象。如果能够任意地使用对象的移动,而不是拷贝,那么标准库中的很多地方的性能都会大大提高。
左值、右值与右值引用
在C语言中,我们常常会提起左值(lvalue)、右值(rvalue),编译器报出的错误信息里面有时也会包含左值、右值的说法。不过,左值、右值通常不是通过一个严谨的定义而为人所知的,大多数时候左右值的定义与其判别方法是一体的。一个最典型的判别方法就是,在赋值表达式中,出现在等号左边的就是"左值",而在等号右边的,则称为"右值"。不过C++中,有一个被广泛认同的说法,那就是可以取值的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值。更为细致地,在C++11中,右值是由两个概念构成得,一个是将亡值,另一个则是纯右值。
纯右值就是C++98标准中右值的概念,讲的是用于辨别临时变量和一些不跟对象关联的值。比如非引用返回的函数返回的临时变量值,就是一个纯右值。一些运算表达式,比如1+3产生的临时变量值,也是纯右值。而不跟对象关联的字面量值,比如:2、'c'、true,也是纯右值。此外,类型转换函数的返回值、lambda表达式等,也都是右值。
而将亡值则是C++11新增的跟右值引用相关的表达式,这样表达式通常是将要被移动的对象。比如返回右值引用T&&的函数返回值、std::move的返回值,或者转换为T&&的类型转换函数的返回值。而剩下的,可以标识函数、对象的值都属于左值。在C++11的程序中,所有的值比属于左值、将亡值、纯右值三者之一。
在C++11中,右值引用就是对一个右值进行引用的类型。事实上,由于右值通常不具有名字,我们也只能通过引用的方式找到它的存在。通常情况下,我们只能是从右值表达式获得其引用。比如:
T&&a = ReturnRvalue();
假设ReturnRvalue返回一个右值,我们就声明了一个名为a的右值引用,其值等于ReturnRvalue函数返回的临时变量的值。
右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。(也就是说需要找一个寄主)
(问题:我可不可以理解为移动构造函数比拷贝构造函数更适合右值。所以对于对应的右值,移动构造函数更容易被匹配到。)
通常情况下,右值引用是不能够绑定到任何的左值的。
int c
int &&d=c
相对地,在C++98标准中就已经出现的左值引用是否可以绑定到右值(由右值进行初始化)?
T&e = ReturnRvalue();
const T&f = ReturnRvalue();
这里一共有11个特性,先只学到这里,因为我需要先对C++先过一遍,然后,再回来看右值和强制转换的部分。