1、引用
C++中的引用主要用作函数的形参,接近于const指针,必须在创建时初始化。
以Person类为例,如下:
Person p; //调用P的构造函数,创建对象P
Person &p2 = p; //引用变量P2指向P
Person p3 = p2; //P2是引用,创建一个p3的对象,会调用Person的拷贝构造函数,p3和p不是一个对象。
Person &P4 = get(p); //形参是引用,所以参数传入时不会再生成一个临时对象。
//返回一个引用,并将P4指向该引用。
Person P5 = get(p); //形参是引用,所以参数传入时不会再生成一个临时对象。
//返回一个引用,由于要创建一个P5的对象,所以会调用拷贝构造函数生成一个对象。
Person& get(Person &p)
{
return p;
}
实际上,现在的C++标准对于形参为const引用的C++函数,如果实参不匹配,那么编译器会生成临时变量,其行为也类似按值传递,为确保原始数据不被修改,将使用临时变量来存储值。所以尽可能地使用const。
上述提到的实参与引用参数不匹配的情况主要有两种:
1、实参类型正确,但是不是左值。
2、实参类型不正确,但是可以转换为正确的类型。
那什么是左值呢?左值参数即是可以被引用的数据对象,程序可以获取其地址,例如,变量、数组、对象、解引用的指针等都是左值。非左值包括字面常量(用引号括起来的字符串不算)和多项式,以及函数返回的临时对象(引用除外)。const变量也是左值,是一种特殊的左值,属于不可修改的左值。
以上的引用我们又称之为左值引用,C++11中新增了另一种引用,即右值引用。这种引用可以指向右值,使用&&声明。
double &&r3 = 5 + 9;//r3关联到13
将右值关联到右值引用将导致该右值被存储到特定的位置,且可以获取该位置地址。即我们虽然不能将取地址符用于14,但是我们可以用在r3上。这样我们就可以使用右值引用来访问该数据。
引入右值引用的主要目的之一是实现移动语义。
2、构造函数、拷贝构造函数、赋值运算符
在讲移动语义之前,先说一下构造函数、拷贝构造函数、赋值构造函数。C++为我们提供了4个特殊的成员函数,除了上面3个以外还包括析构函数,这里就不多说了。
Person();
Person(const Person & p);
Person& operator=(const Person &p);
函数原型如上所示,下面讲一下拷贝构造函数和赋值运算符的区别,以及何时会调用拷贝构造函数,何时会调用赋值运算符。
两者最根本区别在于:拷贝构造函数是用来创建对象,赋值运算符是用来将一个对象的值复制给另外一个对象。调用的是拷贝构造函数还是赋值运算符,主要是看是否有新的对象实例产生。如果产生了新的对象实例,那调用的就是拷贝构造函数;如果没有,那就是对已有的对象赋值,调用的是赋值运算符。
以下同样也Person为例
Person p = Person(); //构造函数
Person p1 = p; //p1对象不存在,p对象存在,调用拷贝构造函数
p1 = p; //p1和p对象都存在,调用赋值运算符
Person p2(p); //p2对象不存在,p对象存在,调用拷贝构造函数
Test::get(); //函数内部p对象创建,调用构造函数
//函数返回时临时对象不存在,p对象存在,调用拷贝构造函数
//释放内部p对象;接着释放临时对象
Person p3 = Test::get();//函数内部p对象创建,调用构造函数
//函数返回时临时对象不存在,p对象存在,调用拷贝构造函数
//释放内部p对象;
Test::get(p3); //什么都不调用
Person P4 = Test::get(p3);//调用一次拷贝构造函数,生成对象P4
class Test
{
public:
static Person& get(Person &p)
{
return p;
}
static Person get()
{
Person p;
return p;
}
};
3、移动语义、移动构造函数、移动赋值运算符
首先我们分析下为什么需要移动语义。
先看一下C++11之前的复制过程
Person p(get());
static Person get()
{
Person p2;
return p2;
}
首先会创建函数内对象p2,然后调用拷贝构造函数创建一个临时对象,接着再调用拷贝构造函数创建对象p,然后再将临时对象删除。这时就做了冗余的工作,如果直接将p与临时对象关联起来,那岂不就少做了很多工作吗?这类似于计算机中移动文件的情形:实际文件还留在原来的地方,而只是修改了记录,这种方法我们称为移动语义。移动语义实质上没有移动数据,而只是修改了记录。
怎么样才能使用移动语义呢?
必须让编译器知道什么时候需要使用什么时候不需要使用。
首先我们定义一个移动构造函数,这时就可以使用右值引用了,它使用右值引用作为参数,该引用关联到右值实参,负责调整记录,将所有权转移给新对象的过程中,移动构造函数可能会修改其实参,这意味着右值引用参数不应该是const。移动构造函数如下所示
Person(Person &&p)
使用的时候,需要传入右值。如下所示
Person P(P1+P2);
移动语义只在消除额外的工作,机智的编译器可能自动消除额外的赋值工作,但通过使用右值引用,程序员可以指出何时使用移动语义。
除了构造函数外,赋值运算符也可以使用移动语义。原型如下
Person& operator=( Person && p);
移动构造函数和移动赋值运算符都是用右值,如果想要使用左值,该如何办呢?可以使用std::move函数,该函数将左值转换成右值。对于大多数程序员来说,右值引用的好处并非让他们能够编写使用右值引用的代码,而是能够利用右值引用实现移动语义的库代码。例如,STL类现在都有拷贝构造函数,移动构造函数,复制赋值运算符,移动赋值运算符。
通常情况下编译器将提供6个特殊的成员函数,但是,如果您提供了析构函数、拷贝构造函数和复制赋值运算符,那么编译器将不提供移动构造函数和移动赋值运算符,相反,如果提供了移动构造函数和移动赋值运算符,那么编译器将不会提供拷贝构造函数和复制运算符。