【C++11(一)】列表初始化和右值引用
【C++11(一)】列表初始化和右值引用
1.C++11
在 2003 年 C++ 标准委员会曾经提交了一份技术勘误表,使得 C++03 这个名字已经取代了 C++98成为 了C++11 之前的最新 C++ 标准名称,不过由于 C++03(TC1) 主要是对 C++98 标准中的漏洞进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为 C++98/03 标准。
相比于 C++98/03,C++11 则带来了数量可观的变化,其中包含了约 140 个新特性,以及对 C++03 标准中约 600 个缺陷的修正,这使得 C++11 更像是从 C++98/03 中孕育出的一种新语言。
2.列表初始化
用列表初始化和使用初始化列表是两个完全不一样的概念
(列表初始化是C++11支持的一种定义变量的方式,初始化列表是类成员变量定义的地方)
(1){}初始化

C++98中,定义变量都需要使用=,但在C++11中,定义变量可以不用=,只用{}大括号就能定义变量(大括号里写变量的初始值)。这个特性在new数组时经常使用
如果初始化的数组存储的元素不是内置类型而是自定义类型,元素和元素之间的初始化要用逗号间隔,并且{}用大括号表示数组中的一个元素。实际上,C++11的列表初始化只推荐在new数组时使用,在其他地方使用会降低代码的可读性
(2)initializer_list
C++11中,一串被用大括号{}引用的数据叫做initializer_list,是C++11新加入的数据结构

STL的容器可以支持用列表初始化了,是因为它的内部的构造函数和operator=函数重载了参数是initializer_list的版本,所以当外界使用列表初始化时,内部会识别为initializer_list类型就会去调用特定的构造
用initializer_list可以为容器赋值,随便打开一个容器的文档,在C++11标准中都能看到形参为initializer_list的构造函数
用initializer_list为容器赋值的操作也是经常被使用的,这样就不用循环调用插入接口
并且initializer_list的内容不可修改,它指向的内容在常量区
所以,在初始化map对象时,可以不用使用make_pair函数或者构造pair对象,因为map实现了initializer_list的构造函数,数据被识别成 initializer_list 类型后再调用相应的构造函数进行初始化。
{“string”, “字符串”} 和 {“left”, “左”} 会被识别成一个 pair 类型,而这两个使用的 {} 括起来就被识别成 initializer_list 类型,从而去初始化对象
int main()
{
vector<int> v1 = { 1,2,3,4,5 };
list<int> l1 = { 10, 20, 30 };
Date d1 = { 2024, 1, 9 };
return 0;
}
其中,v1 和 l1 的初始化方式是一样的,v1 和 l1 的 {} 内的数据会被识别成 initializer_list 类型。
但是 d1 是多参数构造类型转换,是构造+拷贝构造经过优化之后直接构造({ 2024, 1, 9 }; 会被识别成一个 Date 对象,构造完成之后再去拷贝构造 d1,但是这个过程会被编译器进行优化),之中情况只产生于 {} 内的参数个数和 Date 中的构造函数的参数个数一样的时候。当他们的参数个数不匹配的时候,{} 内也会被识别成 initializer_list 类型,但会由于参数个数不匹配而报错。
当使用大括号对容器进行赋值时 v = {10, 20, 30}; ,这个时候调的是赋值重载,而不是 initializer_list 的构造.
3.右值引用和移动语义
(1)左值引用与右值引用
左值:可以被取地址,可以对它赋值,可以出现赋值符号的左边,const修饰符后的左值,不能给它赋值,但是可以取它的地址
右值:不能被取地址,可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边
左值引用:就是给左值的引用,给左值取别名。
右值引用:就是对右值的引用,给右值取别名。
左值引用总结:
左值引用只能引用左值,不能引用右值。
但是 const 左值引用既可引用左值,也可引用右值。
右值引用总结:
右值引用只能右值,不能引用左值。
但是右值引用可以引用 move 以后的左值。
move函数改变了左值的语义,使左值变为右值
右值指的是不能被取地址的值,但引用了一个右值后,右值会被存储到可写数据区中,所以可以修改引用,也能取引用的地址
(2)右值引用使用场景和意义
前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么 C++11 还要提出右值引用呢?
我们用模拟实现实现的string举例:
namespace Raja
{
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;
}
const char* c_str() const
{
return _str;
}
static string to_string(int x)
{
Raja::string ret;
while (x)
{
int val = x % 10;
x /= 10;
ret += ('0' + val);
}
reverse(ret.begin(), ret.end());
return ret;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
}
左值引用的使用场景:
- 引用传参和引用返回都能提高效率
void func1(Raja::string s)
{}
void func2(const Raja::string& s)
{}
int main()
{
Raja::string s1("hello world");
// func2 的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值
func1(s1);
func2(s1);
// string operator+=(char ch) 传值返回存在深拷贝
// string& operator+=(char ch) 传左值引用没有拷贝提高了效率
s1 += '!';
return 0;
}
左值引用的短板:当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。如果是自定义类型的话,会造成深拷贝的代价。这时候右值引用的价值就体现出来了,可以使用右值引用和移动语义解决上述问题。
右值引用和移动语义:移动构造和移动赋值
内置类型的右值为纯右值
称自定义类型的右值为将亡值(为什么是将亡值?自定义类型在作为函数返回值或者隐式类型转换时,会产生一个临时对象,这个临时对象具有常属性,即不能修改,不能取地址,是一个右值,临时变量的作用是:作为中间值,将自己的数据赋值给其他变量,赋值完成后临时变量销毁,即临时变量的生命周期很短)
用一个将亡值构造或赋值一个对象,如果涉及到资源管理,需要用到深拷贝吗?既然将亡值马上会被释放,并且将亡值中的数据是我们想要的,为什么不直接把将亡值的数据掠夺?将自己的资源与将亡值交换,将亡值还会释放你的资源(当你没有资源时,需要把指针置空,否则程序会崩溃)
我们在 Raja::string 中增加移动语义的构造和赋值:
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "移动构造" << endl;
swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
int main()
{
Raja::string ret;
ret = Raja::string::to_string(10);
return 0;
}

没有显示拷贝构造的原因:新版编译器执行了返回值优化( RVO)或着说命名返回值优化(NRVO)。这些优化技术允许编译器消除不必要的拷贝或移动操作,从而提高性能。换句话说,我们在调用值方式返回局部对象函数时,编译器不会创建临时对象,而是在调用者的上下文中直接构造这个对象,也就不用调用拷贝/移动构造函数了。(对象具有复杂的行为或当编译器无法确定优化是安全的时候,编译器可能不会执行 RVO/NRVO。)
无编译器优化结果:
Raja::to_string 函数中会先用返回的 ret 生成构造生成一个临时对象,但是我们可以看到,编译器把 ret 识别成了右值,即将亡值,调用了移动构造。然后在把这个临时对象做为 Raja::to_string 函数调用的返回值赋值给接收的 ret,这里调用的移动赋值。
虽然说我们的左值引用,也可以达到这样的移动构造,但是有一个问题,并不是所有的对象,资源都是可以被转移走的。移动构造之所以这么叫,就是因为移走了别人的资源。这部分资源之所以会被移走,就是因为它有右值属性。而它之所以有右值属性,要么就是这个变量是个将亡值,资源不转移就浪费了;要么就是被程序员亲自move了,程序员许可把这个对象的资源转移走。
就是这样的一个逻辑闭环,右值引用以一个既安全,又高效的方式,完成了局部变量的资源拷贝问题。而这个过程,也叫做右值引用的移动语义
右值引用的出现,C++11后,类的默认成员函数从6个变成了8个。新增两个成员函数:移动构造,移动赋值重载
STL库内部的所有容器,也都更新了移动构造和移动赋值重载
(3)右值引用引用左值及更深入的使用场景分析
按照语法,右值引用只能引用右值,但右值引用一定不能引用左值吗?不一定
有些场景下,可能真的需要用右值去引用左值实现移动语义。当需要用右值引用引用一个左值时,可以通过 move 函数将左值转化为右值。
int main()
{
Raja::string s1("hello, world!\n");
Raja::string s2(s1);
Raja::string s3 = move(s1);
return 0;
}
将上述代码中的 s1 进行 move 操作,然后 move 返回一个 s1 的右值,再去构造 s3,此时会出现的问题是什么呢?
如上图,当我们构造完 s3 之后,由于我们将 s1 转换为了右值,所以这里调用的是移动构造,将 s3 和 s1 的资源互换,此时 s1 就变成了空串,所以我们不能随便将一个左值进行 move 操作,否则可能会产生意想不到的结果。
实质上,右值被右值引用引用以后的属性是左值
必须只能是左值,因为右值是不能直接修改,但是右值被右值引用以后,需要被修改
(4)万能引用(引用折叠)
一个模板参数T,T&&不是类型为T的右值引用,语法规定T&&为万能引用,万能引用能接收左值和右值。
但在接收右值后,t会被折叠成左值

明明我们的rri是一个右值引用,却调用了左值引用的函数重载,为什么?
右值引用后,右值引用指向的对象是右值属性,但是引用本身是左值属性
完美转发
聪明的人就会想到,调用之前move一下不就好了,比如这样:
fuc1(move(rri));

func1是一个万能引用(引用折叠)的函数模板,经过了折叠引用这一步,T&&这个参数类型是不确定的
请问如何调用func2参数的最开始的引用类型?
如果T&&是右值的话,传参后t会变成左值,那么我们可以对其进行move操作
如果T&&是左值的话,传参后t还是左值,我们无需对其进行操作
这个地方就不能粗暴的进行move了,不然会把原本就是左值的参数,给move成右值
为了解决这个情况,C++提供了一个函数模板forward,称为完美转发,其可以识别到参数的左右值类型,从而将其转化为原来的值,有了完美转发,万能引用就变得名副其实了。
template <class T>
void fuc1(T&& t)
{
func2(forward<T>(t));
}

更多推荐



所有评论(0)