1 右值引用
-
函数返回过程:
函数内一个非static的临时变量的生命周期就是在函数调用结束的时候,这样就会出现一个矛盾,函数内如何返回一个出了函数体就会消失的对象,可以推理出来,函数返回对象时会进行一次拷贝,也就是说,上层的函数内拷贝了一份下层函数返回的对象,然后下层函数内的临时对象就会被析构,而上层拷贝出来的新对象继续使用。 -
举个例子:
class A
{
public:
int a;
};
A getTemp()
{
return A();
}
int main()
{
A maina = getTemp();
}
上面的代码的调用步骤:
- 构造步骤:
- 1.getTemp函数调用了A的构造函数构造出了一个临时对象
- 2.构造一个临时对象返回给getTemp函数时拷贝一份作为getTemp()中的临时对象。
- 3.从getTemp()返回的时候把函数内的临时对象又拷贝一份给main()函数中的maina对象(这也是临时对象,只不过有了个名字)
- 析构步骤:
- 1.最开始调用的A的构造函数返回给getTemp()就析构掉了。
- 2.getTemp()的临时对象在出了getTemp()的作用域之后就会被析构掉。
- 3.maina在出了main()函数的作用域以后也会被析构掉。
所以可以看出上面的代码一共调用构造函数1次,拷贝了两次A析构了三次。
如果对象很大的话,不断进行临时对象的构造跟析构,对于程序来说开销很大,而且临时变量的产生和销毁对于程序员说是没有感觉的,因为并不影响程序的正确性。只会偷偷影响你程序的性能。(真坏)
C++11对此进行了优化,加一个移动构造函数,把上面代码的getTemp()生成的临时对象,直接给予到上层的main()函数,而不是让main()函数再去拷贝一份。然后在main()函数中接受到的这个临时对象,给他个名字maina。
这样就减少了临时对象的不断进行拷贝构造,然后再被析构掉。
C++11的移动构造函数参数是一个右值引用,先了解一下什么是左值右值。
- 区分左右值可以看表达式是否可以用&符号取地址,可以取地址的为左值,不可以取地址的为右值。
int i = 0;// i是左值, 0是右值
&i; // 正确
&0; // 错误
右值引用可以理解为对临时对象的引用,临时对象是程序员无法直观在代码上看到的,无法直观感受到临时对象的构造跟析构,给临时对象加一个引用,就相当于给临时对象的内存加了个名字。
例如:A && a = getTemp();就是把getTemp() 里面的临时对象加了个名字a;
引用的区别:
左值引用只能绑定左值
右值引用只能绑定右值
但是常量左值引用是一个万能的引用,可以绑定非常量左值,常量左值,常量右值,只是绑定以后不能修改,只能读取int a = 0; int &b = 1; // 不对 int &&c = a; // 不对 const int d = 1; const int& e = a; //可以 const int& f = 1; // 可以 const int& g = d; // 可以
2.移动构造
C++类中有几种构造相关函数:默认构造函数、普通构造函数、拷贝构造函数、移动构造函数
拷贝赋值函数、移动赋值函数(重载运算符"=")
class A
{
public:
// 默认构造函数
A()
{
str_ = new char[1];
*str_ = '\0';
};
//普通构造函数
A(const char* cstr)
{
if (cstr)
{
str_ = new char[strlen(cstr) + 1];
strcpy(str_, cstr);
}
else
{
str_ = new char[1];
*str_ = '\0';
}
};
//拷贝构造函数
A(const A& a)
{
str_ = new char[strlen(a.str_) + 1];
strcpy(str_, a.str_);
}
//拷贝赋值函数
A& operator = (const A& a)
{
if (this == &a)
return *this;
delete[] str_;
str_ = new char[strlen(a.str_) + 1];
strcpy(str_, a.str_);
return *this;
}
//移动构造函数
A(A&& a)noexcept
{
str_ = a.str_;
a.str_ = nullptr;
}
// 移动赋值函数
A& operator = (A&& a) noexcept
{
if (this == &a)
return *this;
delete[] str_;
str_ = a.str_;
this->str_ = a.str_;
return *this;
}
~A()
{
delete[] str_;
}
char* get_str() const { return str_; }
private:
char* str_;
};
- 移动构造函数与拷贝构造函数的区别:
拷贝构造的参数是const A&,是常量左值引用,而移动构造的参数是A&& 是右值引用,临时对象是个右值的时候,优先进入移动构造函数而不是拷贝构造函数。
就比如下面代码
vector<A> vc;
for (int i = 0; i < 100; i++)
{
vc.push_back(A("aaa"));
}
"aaa"是个右值,调用构造函数构造出来一个临时对象以后,调用移动构造函数把构造出来的临时值直接移动到vector里面,而不是再拷贝一份到vector里面。
如果按照下面这样写:
A s("aaa");
for (int i = 0; i < 100; i++)
{
vc.push_back(s);
}
s是一个左值,就会调用拷贝构造函数,拷贝出来一份到vector里面,再把临时值析构掉。
注意:移动构造函数需要把移动之前原来的指针置空,要不然析构的时候就被析构掉移动过的数据了。。
那么如果希望使用左值的时候也可以调用移动构造函数,减少拷贝,需要怎么做呢?
C++11提供的移动语义std::move就可以解决上述问题
3.移动语义 std::move
std::move的唯一功能就是将一个左值强制转换成一个右值
那么我们这样写,就可以调用的是移动构造函数,而不是拷贝构造函数了。
A s("aaa");
for (int i = 0; i < 100; i++)
{
vc.push_back(std::move(s));
}
- 注意:我们用移动语义调用一个移动构造函数抢夺其他的对象资源,那么被抢夺的对象会发生什么事情
A s("aaa");
A a(std::move(s));
std::cout << s.get_str() << std::endl;
上面的代码s调用普通构造函数构造自己,然后a用move语义把s这个左值强制变成右值去调用移动构造函数,那么s自己已经失效了,再去调用s获取数据的指针,肯定会发生错误的,因为原有指针在构造函数中已经被置空了。
4.区分浅拷贝、深拷贝、移动语义
浅拷贝:只拷贝指针
注意:可能会造成重复析构一个资源
深拷贝:拷贝内存,新的指针指向新的内存
移动语义:抢夺内存资源为己用
注意:原来的指针没作用了
5.完美转发 std::forward
- 定义: 在函数模板中,完全依照模板的参数类型(保留参数的左右值属性),将参数传递给函数模板中调用的另外一个函数。
template<typename T>
void func(T & val)
{
print(val);
}
比如上述的函数模板,传入1,按照func(1);调用 1是右值,但是在进入func内部以后1变成了个左值val,就会变成调用print(val);而val是个左值。
如果我想在调用print函数的时候,是调用的print(1)而不是print(val)那就可以使用std::forward函数进行完美转发。
就可以这样写:
template<typename T>
// 如果写T&,就不支持传入右值,而写T&&,既能支持左值,又能支持右值
void func(T&& val)
{
print(std::forward<T>(val));
}