目录
一. 左值引用和右值引用的概念和性质
1.1 什么是左值引用和右值引用
1.2 左值引用和右值引用的性质
二. 移动构造和移动赋值
2.1 左值引用的缺陷
2.2 临时对象返回减少拷贝的问题(移动构造和移动赋值)
2.3 C++11 STL容器接口的一些变化
三. 完美转发问题
3.1 模板&& -- 万能引用
3.2 完美转发forward
一. 左值引用和右值引用的概念和性质
1.1 什么是左值引用和右值引用
要明确左值引用和右值引用的概念,就要先明确什么是左值和右值。
- 左值:可以取到地址的数据表达式。除了被const修饰的左值,都可以被赋值。
- 右值:无法取地址的数据表达式,主要包括:字面常量、函数返回值等。
注意:左值可以出现在赋值符号的左边和右边,但是右值只能出现在赋值符号的右边。
在C++11中,对于内置类型的右值称为普通右值,对自定义类型的右值称为将亡值。顾名思义,左值引用就是左值的别名,右值引用就是右值的别名。
右值引用的语法为:类型&& 引用变量名称 = 被引右值
代码1.1:左值引用和右值引用
int main()
{
int x = 10;
int y = 20;
//左值引用
int& r1 = x;
//右值引用
int&& rr1 = x + y;
int&& rr2 = 10;
return 0;
}
1.2 左值引用和右值引用的性质
- 右值是无法取地址的,但是给某个右值去别名之后,会在内存中的某个位置存储其值,这样就可以获取地址,也可以修改变量值。
代码1.2:取右值引用的地址
int main()
{
int&& rr = 10;
std::cout << &rr << std::endl;
rr = 20;
std::cout << rr << std::endl; //输出20
return 0;
}
- 一般情况下,左值引用不能作为右值的别名,但是经过const修饰的左值引用,可以作为右值的别名。
- 一般情况下右值引用也不能作为左值的别名,但是经过move处理后的左值,可以用右值引用作为其别名。move函数并没有移动什么内容,move仅是将左值强制转换为右值引用,实现语义的转换。
- move要谨慎使用,因为它会将对象资源转移的权利赋给其他人(见图1.1)。
代码1.3:左右值引用之间的赋值
int main()
{
//int& rr1 = 10; //报错
const int&& rr2 = 10;
int x = 10;
//int&& rr3 = x; //报错
int&& rr4 = std::move(x);
//x还可以被继续赋给左值引用,更改rr4也会更改x
int& rx = x;
rr4 = 100;
std::cout << x << std::endl; //输出100
return 0;
}
二. 移动构造和移动赋值
采用模拟实现的String,来演示移动赋值和移动构造的实现。
namespace zhang
{
class string
{
public:
typedef char* iterator;
iterator begin()
{
return _str;
}
iterator end()
{
return _str + _size;
}
string(const char* str = "")
:_size(strlen(str))
, _capacity(_size)
{
//std::cout << "string(char* str)" << std::endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
// s1.swap(s2)
void swap(string& s)
{
std::swap(_str, s._str);
std::swap(_size, s._size);
std::swap(_capacity, s._capacity);
}
// 拷贝构造
string(const string& s)
:_str(nullptr)
{
std::cout << "string(const string& s) -- 拷贝构造(深拷贝)" << std::endl;
/*string tmp(s._str);
swap(tmp);*/
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
}
// 赋值重载
string& operator=(const string& s)
{
std::cout << "string& operator=(string s) -- 拷贝赋值(深拷贝)" << std::endl;
/*string tmp(s);
swap(tmp);*/
_str = new char[s._capacity + 1];
strcpy(_str, s._str);
_size = s._size;
_capacity = s._capacity;
return *this;
}
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
std::cout << "string(string&& s) -- 移动构造" << std::endl;
swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
std::cout << "string& operator=(string&& s) -- 移动赋值" << std::endl;
swap(s);
return *this;
}
~string()
{
//std::cout << "析构" << std::endl;
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;
}
const char* c_str() const
{
return _str;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
zhang::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
zhang::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += ('0' + x);
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
}
2.1 左值引用的缺陷
左值引用的缺陷主要体现在以局部自定义类型对象作为函数的返回值时,由于出了作用域对象就会被销毁,如果直接返回其引用,就会出现引用所指向的那块空间被销毁的问题。
因此,返回局部对象就不得不采用传值返回。传值返回,则至少调用一次拷贝构造函数,对于一些版本较老的编译器,甚至会调用两次拷贝构造,因为函数返回值要先拷贝构造给一个临时对象,然后再拷贝构造给接收它的对象。
如果是采用一个已经存在的对象,通过赋值来接收函数的返回值,那么就必须要拷贝构造一次、赋值一次,无法进一步优化。
2.2 临时对象返回减少拷贝的问题(移动构造和移动赋值)
在C++98还未出现右值引用时,有以下两种方法可以接收返回值。
- 返回全局变量的左值引用
- 采用输出型参数 -- to_string(int val, string& str)
但是,采用全局变量存在线程安全问题,采用输出型参数则影响函数原本的形态,C++11的右值引用可以解决返回局部对象时的拷贝问题。
为此,定义一个移动构造函数string(string&& s)和一个移动拷贝函数string& operator(string&& s),用于接收自定义类型右值(将亡值)作为参数。
移动构造和移动赋值,是利用生命周期马上要结束的自定义类型对象,将生命即将结束的对象的资源传递给其他对象或将要构造出来的对象,然后带走接收将亡值的对象原本的资源,将其析构释放,这样就完成了资源的转移,从而减少拷贝的消耗。
代码2.1:string的移动构造和移动赋值
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
std::cout << "string(string&& s) -- 移动构造" << std::endl;
swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
std::cout << "string& operator=(string&& s) -- 移动赋值" << std::endl;
swap(s);
return *this;
}
在string类中定义了移动构造和移动赋值之后,编译器在拷贝值返回函数的返回值时,由于返回值出了函数作用域马上会消亡,编译器会自动识别其为右值,然后调用移动构造函数完成返回值的接收,这样就避免了拷贝的消耗。
一般来说,编译的会对返回值接收的过程进行优化,直接用返回的右值对象作为参数,移动拷贝构造新对象,这样只用调用一次移动构造函数。如果用版本较老的编译器,则先通过移动构造创建临时对象,然后在拷贝给接收它的新对象,这样会调用两次移动构造函数。
2.3 C++11 STL容器接口的一些变化
C++11所有的STL容器接口,只要涉及到构造和插入新的值,都会加入右值引用版本(见图2.5),如果传递的参数是右值,就会进行资源转移,以此来减少拷贝的消耗,这在C++98中是不存在的。
三. 完美转发问题
3.1 模板&& -- 万能引用
对于模板函数Func(T&& t),参数t可以被称为万能引用,其可以接收左值,也可以接收右值,T&&由此被称为万能引用。
如果Func(T&& t)函数中的子函数_func(t)要传递参数t,那么默认情况下,经万能引用接收的参数全部被处理成了左值(参考图3.1代码3.1的运行结果)。因此,模板T&&也可以称为引用折叠,即:将两个&&折叠为一个&,右值变左值,左值还是左值。
注意:万能引用仅对模板&&有效,如果是确定类型的&&,则只能接收右值。
代码3.1:万能引用
void _func(int& t) { std::cout << "左值引用" << std::endl; }
void _func(const int& t) { std::cout << "常量左值引用" << std::endl; }
void _func(int&& t) { std::cout << "右值引用" << std::endl; }
void _func(const int&& t) { std::cout << "常量右值引用" << std::endl; }
template<class T>
void Func(T&& t)
{
//T&& -- 万能引用
_func(t);
}
int main()
{
int x = 10;
Func(x); //传左值
Func(std::move(x)); //传右值
const int y = 10;
Func(y); //传右值
Func(std::move(y)); //传常量右值
return 0;
}
3.2 完美转发forward
万能引用T&&将参数变为左值,如果还想要让其保持原有的类型(右值)在作为参数传递给子函数,则要经参数t经函数std::forward做完美转发处理,语法为std::forward<T>(t)。
代码3.2:完美转发
void _func(int& t) { std::cout << "左值引用" << std::endl; }
void _func(const int& t) { std::cout << "常量左值引用" << std::endl; }
void _func(int&& t) { std::cout << "右值引用" << std::endl; }
void _func(const int&& t) { std::cout << "常量右值引用" << std::endl; }
template<class T>
void Func(T&& t)
{
//forward -- 完美转发
_func(std::forward<T>(t));
}
int main()
{
int x = 10;
Func(x); //传左值
Func(std::move(x)); //传右值
const int y = 10;
Func(y); //传右值
Func(std::move(y)); //传常量右值
return 0;
}