今天我们主要围绕右值进行炸开展开讲解!
目录
右值引用以及移动语义:
什么是左值引用与右值引用?
无论左值引用还是右值引用,都是给对象取别名。
左值与左值引用:
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋值(const左值对象除外)。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
右值与右值引用:
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
注意 :右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇,这个了解一下实际中右值引用的使用场景并不在于此,这个特性也不重要。
左值引用与右值引用比较:
左值引用总结:
- 左值引用只能引用左值,不能引用右值。
- 但是const左值引用既可引用左值,也可引用右值。
cpp
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra = a;
//int& ra2 = 10; // 编译失败,因为10是右值
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
右值引用总结:
- 右值引用只能右值,不能引用左值。
- 但是右值引用可以move以后的左值。
cpp
int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: "初始化": 无法从"int"转换为"int &&"
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
return 0;
}
右值引用使用场景和意义:
那么左值引用既可以引用左值,加上const也可以引用右值,为什么要搞出来一个右值引用呢?
先说结论:是为了进行区分左值还是右值!编译器时钟都会自动匹配最适合的。
那么这么区分的意义是什么呢?
那我们需要先普及一个小知识:
右值通常分为
- 纯右值
- 将亡值
纯右值通常就是那些字面常量,
而将亡值通常是针对自定义类型的,为什么叫将亡值呢?
自定义类型右值对象一般是匿名对象、返回值、临时对象等,这些对象立马就会被释放
注意:内置类型也有将亡值哦
下图是个简要描述右值引用大概得用途,但是并没有详细说明怎么做到的,随后会通过代码一步一步分析:
我们现在有个string类(不完整)
cpp
namespace cyc
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
//cout << "string(char* str)" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
::swap(_str, s._str);
::swap(_size, s._size);
::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
~string()
{
delete[] _str;
_str = nullptr;
}
char& operator[](size_t pos)
{
assert(pos < _size);
return _str[pos];
}
void reserve(size_t n)
{
if (n > _capacity)
{
char* tmp = new char[n + 1];
strcpy(tmp, _str);
delete[] _str;
_str = tmp;
_capacity = n;
}
}
void push_back(char ch)
{
if (_size >= _capacity)
{
size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;
reserve(newcapacity);
}
_str[_size] = ch;
++_size;
_str[_size] = '\0';
}
//string operator+=(char ch)
string& operator+=(char ch)
{
push_back(ch);
return *this;
}
string operator+(char ch)
{
string s1(_str);
s1 += ch;
return s1;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
}
因为我们可以使用右值引用来区分左值与右值,便可以对右值进行一些小心思!
比如拷贝构造与赋值运算符重载
我们先来看移动拷贝
cpp
string(string&& s)
:_str(nullptr)
{
cout << "string(string&& s) -- 移动拷贝" << endl;
swap(s);
}
没错,就这短短的一段代码就完成了资源的转移!
cpp
int main()
{
cyc::string s1("hello world");
cyc::string ret1 = s1; // 左值
cyc::string ret2 = s1 + '!';// 右值
return 0;
}
当没有写移动构造时:
当写了时:
这里就构成了移动构造。
我们简单来看一下他是怎样优化的,优化前后是怎样的
现在来看一看移动赋值:
cpp
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
当没有实现移动构造与移动赋值时:
为什么会出现3次深拷贝呢?
因为我们的赋值本质上是复用拷贝构造(现代写法)。
再来看实现了移动构造与移动赋值
同样,我们一样分析一下编译器是如何进行优化的
于是此时我们可以进行一下总结:
左值引用与右值引用都是给对象取别名。
左值引用是直接减少拷贝,但有一些问题没有解决,比如传局部变量返回,对象拷贝问题。
而右值引用是间接减少拷贝,因为我们右值引用的存在,我们可以进行区分并对右值进行一些别的操作,比如移动构造,移动赋值(本质是资源的转移)。
STL中的验证(VS下)
我们的STL中也都存在了移动拷贝,移动赋值...
c++官网可查哦
可以清楚的看到将s1(原本为左值)move后进行拷贝构造,直接将资源进行了转移!
同样,移动赋值也是如此。
但是不仅仅只有如上,对于STL插入接口也有右值引用版本!
一个关于链表插入的形象示意图,可以看到我们push时直接将资源转移,而不是进行拷贝构造
完美转发:
好抽象的名字哈哈
观察如下代码与结果,为什么和我们当前的理解不一致?
明明存在的右值哪里去了,为什么右值属性丢失了呢?
回忆一下我们之前说过引用右值后,可以修改引用,这是不是太违背常理了呢?
其实不然,这样是有很大的深意de!
如果还是引用后还是右值,那么说明此时这个对象是不可修改的,那我们如何能转移这个对象的资源呢?
那么我们此时就有一个困扰了,有些情景下会要传多次右值,比如我们list的push_back(复用insert进行实现),此时就需要完美转发保存他的右值属性,否则虽然可以调用右值引用的push_back,但却无法调用右值引用的insert,最终造成还是左值引用的问题。
此时就需要完美转发来帮助我们!
在模板下使用forward<T>(目标对象)
即可
cpp
void PerfectForward(T&& t)
{
Fun(forward<T>(t));
}
关于const引用延长生命周期:
这个是C++11之前必须要有的东西,但有了右值引用后就不在这么必要了。
具体为什么请看讲解~
我们知道匿名对象的生命周期只有一行。
但是const引用后很明显的看到延长了Ref的生命周期。
那么这么做的目的到底是什么呢?
答案在于传参!
我们一般会穿匿名对象给类成员函数,而我们也会进行调用一些其中的成员,如果生命周期只有一行,那还怎么玩呢?
所以const& 延长生命周期是很有必要的。
那么我们直接使用const& 传值返回岂不是一个很妙的想法?哈哈
答案是不可以的,因为出了作用于局部变量就销毁了,即使我们可以访问,但也很有可能访问到的不是我们期望的值。
那我们只使用const& 接收呢?
答案是可以的,原因在于我们引用的是临时对象,而延长了生命周期。
新的类功能:
默认成员函数:
默认成员函数
原来C++类中,有6个默认成员函数:
- 构造函数
- 析构函数
- 拷贝构造函数
- 拷贝赋值重载
- 取地址重载
- const 取地址重载
最后重要的是前4个,后两个用处不大。默认成员函数就是我们不写编译器会生成一个默认的。
C++11 新增了两个:移动构造函数和移动赋值运算符重载。
针对移动构造函数和移动赋值运算符重载有一些需要注意的点如下:
如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,如果实现了就调用移动构造,没有实现就调用拷贝构造。
如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造完全类似)如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
cpp
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
private:
cyc::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
Person s4;
s4 = std::move(s2);
return 0;
}
default:
强制生成默认函数的关键字default:
C++11可以让你更好的控制要使用的默认函数。假设你要使用某个默认的函数,但是因为一些原因这个函数没有默认生成。比如:我们提供了拷贝构造,就不会生成移动构造了,那么我们可以使用default关键字显示指定移动构造生成。
cpp
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p)
:_name(p._name)
, _age(p._age)
{}
Person(Person&& p) = default;
private:
cyc::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
delete:
禁止生成默认函数的关键字delete:
如果能想要限制某些默认函数的生成,在C++98中,是该函数设置成private,并且只声明补丁已,这样只要其他人想要调用就会报错。在C++11中更简单,只需在该函数声明加上=delete即可,该语法指示编译器不生成对应函数的默认版本,称=delete修饰的函数为删除函数。
cpp
class Person
{
public:
Person(const char* name = "", int age = 0)
:_name(name)
, _age(age)
{}
Person(const Person& p) = delete;
private:
cyc::string _name;
int _age;
};
int main()
{
Person s1;
Person s2 = s1;
Person s3 = std::move(s1);
return 0;
}
可变模板参数:
持续新~~~~