文章目录
- 1. 统一的列表初始化
- 1.1 {}初始化
- 1.2 std::initializer_list
- 2. 声明
- 2.1 auto
- 2.2 decltype
- 3.3 nullptr
- 4 范围for循环
- 5 STL中一些变化
- 6 右值引用和移动语义
- 6.1 左值引用和右值引用
- 6.2 左值引用与右值引用特性
- 6.3 左值引用和右值引用使用场景和意义
- 6.4 右值引用及其一些更深入的使用场景分析
- 6.5 完美转发
1. 统一的列表初始化
1.1 {}初始化
1️⃣ 在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。如:
struct Point
{
int _x;
int _y;
};
int main()
{ //初始化数组 内容确定
int array1[] = { 1, 2, 3, 4, 5 };
//初始化数组 第一个参数为0
int array2[5] = { 0 };
//初始化结构体 _x = 1, _y = 2
Point p = { 1, 2 };
return 0;
}
2️⃣C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和自定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
struct Point
{
int _x;
int _y;
};
int main()
{
//正常初始化 还是有 = 的
int x1 = 1;
//C++新增初始化 不仅可以用{},而且=都可以省
int x2{ 2 };
//数组 自定义类型用{}初始化也可以省略 =
int array1[]{ 1, 2, 3, 4, 5 };
int array2[5]{ 0 };
Point p{ 1, 2 };
// C++11中列表初始化也可以适用于new表达式中
int* pa = new int[4]{ 0 };
return 0;
}
3️⃣创建对象时也可以使用列表初始化方式调用构造函数初始化
class Date
{
public:
Date(int year, int month, int day)
:_year(year)
, _month(month)
, _day(day)
{
cout << "Date(int year, int month, int day)" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2022, 1, 1); //以前的方式
// C++11支持的列表初始化,这里会调用构造函数初始化
// Date类的初始化接受3个参数,列表就可以按顺序发进这三个值
Date d2{ 2022, 1, 2 };
Date d3 = { 2022, 1, 3 };
return 0;
}
1.2 std::initializer_list
在C++11中,诸如 {1,3,42,3 }这样用{ }组合起来的列表是有类型的,这个类型就是 initializer_list。
int main()
{
// 用auto自动识别类型
auto il = { 10, 20, 30 };
// class std::initializer_list<int>
cout << typeid(il).name() << endl;
return 0;
}
📝这个类型常作为C++_STL库中一些容器的构造函数参数,这也是这些容器能够支持列表初始化(就是{})的原因,C++_STL容器的构造函数在C++11中重载了initializer_list的版本,{1,3,42,3 }这样的列表会隐式类型转换成initializer_list,随后作为参数传入容器的构造函数。
int main()
{
//容器支持{}初始化
vector<int> v = { 1,2,3,4 };
list<int> lt = { 1,2 };
// 这里{"sort", "排序"}会先初始化构造一个pair对象
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
// 使用大括号对容器赋值
v = { 10, 20, 30 };
return 0;
}
2. 声明
2.1 auto
C++11提供了多种简化声明的方式,尤其是在使用模板时。
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。
📋C++11中废弃auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型。
int main()
{
int i = 10;
//识别指针类型
auto p = &i;
cout << typeid(p).name() << endl;
map<string, string> dict = { {"sort", "排序"}, {"insert", "插入"} };
//识别模板容器迭代器
auto it = dict.begin();
return 0;
}
2.2 decltype
📋关键字decltype将变量的类型声明为表达式指定的类型
int x;
//推断x的类型,将y声明为该类型
decltype(x) y;
// decltype的一些使用使用场景
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
//先让模板推断类型 再让decltype推断二者的乘积的类型
decltype(t1 * t2) ret;
// 根据传入类型的不同,打印不同的类型的名称
cout << typeid(ret).name() << endl;
}
int main()
{
const int x = 1;
double y = 2.2;
// ret的类型是double
decltype(x * y) ret;
// p的类型是int*
decltype(&x) p;
F(1, 'a');
return 0;
}
3.3 nullptr
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。📋所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
4 范围for循环
如果容器或自定义类型支持迭代器以及迭代器相关的重载,就能支持范围for循环。
示例:
//这里的函数只声明了 没实现 只能通过语法层,内部实现后就能成功使用范围for
class Dateiterator
{
public:
typedef DateNode Node;
Node* _node;
Dateiterator(Node* node = nullptr)
{}
//->重载
int* operator->()
{};
//!=重载
bool operator!=(const Dateiterator& l) {};
//==重载
bool operator==(const Dateiterator& l) {};
//前置++
Dateiterator& operator++(){}
//*重载
int& operator*(){}
//!=重载
bool operator!=(const Dateiterator* node){}
};
class Date
{
public:
typedef DateNode Node;
typedef Dateiterator iterator;
iterator begin()
{
return iterator(_root);
}
iterator end()
{
return iterator(nullptr);
}
private:
Node* _root;
};
int main()
{
Date s;
for (auto e : s)
{
}
}
5 STL中一些变化
新容器:
其中unordered_map和unordered_set是最有用的,这里不做过多介绍。
容器中的一些新方法
C++11更新后,容器中增加的新方法最后用的插入接口函数的右值引用版本,这个版本能提高容器插入的效率。
相应的,构造函数也会有右值引用的版本
稍后会讲解何为右值引用。
6 右值引用和移动语义
6.1 左值引用和右值引用
传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用**。无论左值引用还是右值引用,都是给对象取别名。**
🔎那么,何为左值?何为左值引用?
💡📋左值是一个表示数据的表达式(我们常见的变量名或解引用的指针),一般情况
下,我们可以1️⃣获取它的地址+2️⃣可以对它赋值,3️⃣左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值
,不能给他赋值,但是可以取它的地址。📋左值引用就是给左值的引用,给左值取别名。
// 以下的a、b、c都是左值
// 都可以取地址 在=左边 可以修改(const修饰的不可以)
int a = 1;
const int b = 2;
int* c = new int(0);
//取地址
&a;
//修改 出现在=左边
a = 2;
//左值引用就是对左值的引用
int& _a = a;//_a是a的左值引用
🔎什么是右值?什么是右值引用?
💡📋右值也是一个表示数据的表达式,是我们常说的临时变量,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,1️⃣右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,2️⃣右值不能取地址。右值引用就是对右值的引用,给右值取别名。
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 对右值的右值引用
int&& rr1 = 10;
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
// 右值不能修改,不能取地址
10 = 1;
&10;
右值不能被修改和取地址,但是右值被右值引用后,可以通过右值引用进行取地址和修改。原先的右值是个临时变量,生命周期很短,但是给它起了右值引用后,右值被存储到特定位置,就可以取地址或者修改了。
double x = 1.1, y = 2.2;
int&& rr1 = 10;
const double&& rr2 = x + y;
//右值引用可以修改数据
rr1 = 20;
//当然 const修饰的版本不能修改
rr2 = 5.5; // 报错
//但都可以取地址
&rr1;
&rr2;
6.2 左值引用与右值引用特性
左值引用:
- 左值引用只能引用左值,不能引用右值。
- 但是const左值引用既可引用左值,也可引用右值
int x = 10;
//left是x的别名
int& left = x;
//const 左值引用既可以引用左值 又可以引用右值
const int& left2 = 10;
const int& left2 = x;
右值引用:
- 右值引用只能右值,不能引用左值。
- 但是右值引用可以引用move以后的左值。
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
//move: 将左值以右值的方式返回
int&& r3 = std::move(a);
6.3 左值引用和右值引用使用场景和意义
先模拟实现一个没有移动构造的string,方便对后续的知识点进行解释:(代码内容不重要,只知道是一个没有实现移动构造的string类就行),想详细了解可以看C++_string简单源码剖析:string模拟实现
这里也贴一份:
namespace bit
{
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(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
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;
}
private:
char* _str;
size_t _size;
size_t _capacity; // 不包含最后做标识的\0
};
}
回到正题
📝左值引用
作为函数的参数和返回值是,可以减少拷贝,有效的提高效率
左值引用的短板
:
📝但是当函数返回对象是一个局部变量,出了函数作用域就不存在了,就不能使用左值引用返回,只能传值返回。例如:string function2(string s1)函数中可以看到,这里只能使用传值返回,传值返回会导致至少1次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)
右值引用和移动语义解决上述问题
:
📝在bit::string中增加移动构造,📋以右值引用为参数的拷贝构造一般成为移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。
// 移动构造
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
不仅仅有移动构造,还有移动赋值:
在bit::string类中增加移动赋值函数,再去调用bit::to_string(1234),不过这次是将bit::to_string(1234)返回的右值对象赋值给s对象,这时调用的是移动构造。下面的情况是调用移动赋值。
// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
//像这样的情况,先无参构造,再对其赋值,就会调用移动赋值
int main()
{
bit::string ret1;
ret1 = bit::to_string(1234);
return 0;
}
//输出 string& operator=(string&& s) -- 移动语义
STL中的容器都是增加了移动构造和移动赋值
由于编译器的优化问题,我们很多时候都看不到构造的细节,这里讲讲常见的优化场景:
6.4 右值引用及其一些更深入的使用场景分析
因为STL的大部分容器都有重载右值引用版本,所以在C++_list简单源码剖析:list模拟实现中,我们也可以对模拟实现的list添加右值引用版本,针对添加的是右值引用版本的insert(list),push_back(list)和移动构造(ListNode):
//listNode
ListNode(T&& data)
:_next(nullptr)
, _prev(nullptr)
, _data(data)
{}
//list
void push_back(T&& x)
{
insert(end(), x);
}
iterator insert(iterator pos, T&& x)
{
Node* cur = pos._node;
Node* newnode = new Node(x);
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
📝写个测试案例去测试实际情况
int main()
{
bit::list<bit::string> lt;
lt.push_back(bit::string("11111"));
}
📝但是,结果却出奇的发现,实际lt并没有调用任何的移动构造,这是为什么😕?
📝经过调试我们发现,我们传入右值bit::string(“11111”)后,😧确实是进入了list右值版本的push_back,但是却在下一级的调用中,却没有调用list右值版本的insert,
要解释这个原因,我们就先讲讲右值引用本身的特性,🔎为什么右值不能被修改,但是却通过右值引用可以修改?
💡原因是:右值引用本身是左值,只有右值引用本身的属性是左值,才能转移他的资源。
💡要解决这样的问题很简单,就是在每次向下级传值时,都对传入的值move一下,就可以保证传入的值是一个右值,从而去调用一个右值版本:
//修改后的版本
//listNode
ListNode(T&& data)
:_next(nullptr)
, _prev(nullptr)
, _data(move(data))
{}
//list
void push_back(T&& x)
{
insert(end(), move(x));
}
iterator insert(iterator pos, T&& x)
{
Node* cur = pos._node;
Node* newnode = new Node(move(x));
Node* prev = cur->_prev;
// prev newnode cur
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = cur;
cur->_prev = newnode;
return iterator(newnode);
}
此时就可以成功使用移动构造了。
这里再介绍一次move函数
:
📋当需要用右值引用去引用一个左值时,可以通过move函数将左值转化为右值。C++11中,std::move()函数位于头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。
6.5 完美转发
模板中的&& 万能引用:
📝模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
//万能引用👇
template<class T>
void PerfectForward(T&& t)
{
Fun(t);
}
📝模板的万能引用只是提供了能够同时接收左值引用和右值引用的能力,
int main()
{
int a;
const int b = 8;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
📝输出结果:PerfectForward函数都成功接受了四种参数并成功调用
📝但是,引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值(无论是成为左值引用还是右值引用,引用本身还是左值),我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发。
📋std::forward 完美转发在传参的过程中保留对象原生类型属性:
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
//万能引用👇
template<class T>
void PerfectForward(T&& t)
{
//完美转发
//将t转化为本来的类型
//由此可以传入不同类型的值而调用不同接口的Fun函数
Fun(std::forward<T>(t));
}
int main()
{
int a;
const int b = 8;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
本文就到这里,感谢你看到这里❤️❤️! 我知道一些人看文章喜欢静静看,不评论🤔,但是他会点赞😍,这样的人,帅气低调有内涵😎,美丽大方很优雅😊,明人不说暗话,要你手上的一个点赞😘!