文章目录
- 1、列表初始化
- 2、声明
- decltype
- 3、STL新容器
- 小总结
- 4、右值引用
- 1、概念
- 2、使用场景(包含移动构造)
- 3、完美转发
- 4、移动赋值
- 5、C++98的const引用延长生命周期
1、列表初始化
大括号{}来代替初始化,并且是所有类型。
struct ZZ
{
int _x;
int _y;
};
int main()
{
int x1 = 1;
int x2 = { 2 };
int x3{ 3 };
int arr1[5]{ 0 };
int arr2[]{ 1, 2, 3, 4, 5 };
ZZ z{ 4, 7 };
int x4(4);//调用了int的构造
int* pa = new int[4]{ 0 };
return 0;
}
像类的初始化写法,加=相当于构造函数初始化+拷贝构造然后优化成直接构造。
vector<int> v1 = { 1, 2, 3, 4, 5 };//原写法
vector<int> v1{ 1, 2, 3, 4, 5 };//C++11写法
11支持这样写,实际上这里是把v1当成了initializer_list的类。这样写auto v = { 1, 2, 3, 4, 5 },然后用typeid(v).name()就可以看到它的类型。展开写的话就是initializer_list< int > it1 = { 1, 2 }。在这个系统带的类里也有迭代器,不能修改,它指向的内容在常量区,begin和end返回是头地址和尾地址下一位。在vector写入这样类型的初始化可以这样写
vector(initializer_list<T> il)
{
for (auto& e : il)
{
push_back(e);
}
}
或者用迭代器也可以。
Date d1(2023, 5, 23);
Date d2(2023, 5, 24);
vector<Date> v1 = { d1, d2 };
vector<Date> v2 = { Date d1(2023, 5, 23), Date d2(2023, 5, 24) };
vector<Date> v3 = { {2023, 5, 23 }, { 2023, 5, 24 } };
三个初始化都行,这时候会被当作是initializer_list< Date >。
2、声明
auto就是其中之一,自动推演变量类型。
decltype
const int x = 1;
double y = 2.2;
decltype(x * y) ret;//ret类型就是double
decltype(&x) tmp;//tmp类型就是const int*
vector<decltype(x * y)> v;
它可以推导表达式类型,用这个类型去实例化模板参数或者定义对象。
还有C++中nullptr的引入。
范围for循环已经用过,智能指针之后写
3、STL新容器
array,forward_list,两个新map和set,但是前两个很鸡肋。array是固定大小的静态数组,用vector更舒服;另外一个是在需要单链表头插的时候很适合,其他都不算好,相比list每个结点可以节省一个指针的空间,头插头删时用它可以。
小总结
增加支持initializer_list的构造函数,使用更方便,有一定价值
增加cbegin和cend系列迭代器接口,也有点鸡肋
移动构造和移动赋值,提高了拷贝效率(之后写),重要
支持右值引用相关插入接口函数,提高了拷贝效率,重要
4、右值引用
1、概念
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址+可以对它赋
值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左
值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
int* p = new int(0);
int b = 1;
const int c = 2;
p,b,c,*p都是左值。
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能
取地址。右值引用就是对右值的引用,给右值取别名。
int* p = new int(0);
int b = 1;
const int c = 2;
int& ret1 = b;
const int& ret2 = b + c;
int&& ret3 = b + c;
int&& ret4 = b;//错误
右值引用符号是&&。上面四个就是左/右值引用分别给左/右值取别名,左值引用如果不加const就给右值取别名那就是权限放大,所以必须加const才能通过;右值引用不能给左值取别名,所以ret4那里出错。
但是这样写就可以int&& ret4 = move(b);
- 左值引用只能引用左值,不能引用右值。
- 但是const左值引用既可引用左值,也可引用右值。
- 右值引用只能右值,不能引用左值。
- 但是右值引用可以move以后的左值。
2、使用场景(包含移动构造)
void func(int& a)
{
cout << a << endl;
}
void func(int&& a)
{
cout << a << endl;
}
int main()
{
int a = 0;
int b = 1;
func(b);
func(a + b);
return 0;
}
虽然可以变成const int& a,来传入左右值,但是进入函数内部还是无法区分左右值,所以就可以这样写成函数重载。之所以要区分左右值,是因为内置类型不需要分,但是自定义类型需要考虑。我们可以用自己造的string类。
string s1("hello");
string ret1 = s1;
string ret2 = (s1 + '!');
如果这两个都是深拷贝,代价有点高。所以类里就得做一个函数重载
string(string&& s)
:_str(nullptr)
{
swap(s);
}
这个接收右值的就是移动构造。深拷贝相当于两个空间,那么为了减少代价,就不另外开辟空间,让新变量指向这个空间,替换掉旧变量。move的作用则是把s1指向的内容给拿走,s1连同之前的空间废掉,交给其他变量。move是一个函数调用,返回的是右值。所以右值引用的移动构造效率比较高,它是不动空间,动指向空间的东西。
左值引用直接减少了拷贝,用的是引用传参和传引用返回。但是有些场景不能用引用返回,比如函数内的局部对象,否则就会有一些强行的拷贝。
C++中传值返回拷贝比较多,有些场景是不可避免的右值返回。编译器在这里做了很多复杂的操作。这里就不展开写了。
11之后所有STL容器都增加了移动构造,不仅是构造,插入接口也都增加了右值引用。很多原先对于右值只能深拷贝,现在就改成移动构造来提高效率。
匿名对象被认为是右值,直接传的字符串会被认为是右值。如果没有右值引用,两个就都是深拷贝。
左/右值引用都减少拷贝。左值引用是直接,右值引用是间接,先识别是不是右值,是的话就不再深拷贝,转为移动拷贝,移动拷贝效率高。移动拷贝就是直接移动资源。
右值不能取地址,不能修改,右值取别名后会右值会被存储到特定位置。int&& r1 = 10,10不能被取地址,但是r1可以取地址,可以修改数值,所以r1是左值,如果前面加上const,那就不能修改,右值引用的const就只是const作用,但是左值引用+const,也就是const int&这样的,就可以左右值都能引用。
对于那些具有常性的变量(比如临时变量)是不能被修改的,比如string& s3 = s1 + s2,这里就会报错,但是如果去掉引用就可以了,但是去掉引用后s1 + s2可以修改?是不是有点不太正常?这是因为传到移动构造后,传进来的是右值,传出去的时候就变成左值了。所以右值引用引用后属性是左值,这样才能实现资源转移。
3、完美转发
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<typename T>
void PerfectForward(T&& t)//引用折叠、万能引用,左右值都能传
{
Fun(t);//Fun(forward<T>(t));
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
借助模板的推导,如果传左值,就会自动推导成T&,右值就是&&。这个代码就传给PerfectForward,然后Fun再传递一层。
要右值引用的话,是把参数类型换成右值引用就可以了吗?在类里面保证这点的话,比较麻烦,需要改很多,比如往一个函数里传右值,但是这个函数还需要把这个值传给别的函数,还得换成左值,所以要保证整个链路都是右值才行。各个函数都增加右值引用的函数,函数括号里的参数,传到函数内部用来赋值等操作都会引起右值转为左值。但是即使全改成右值也不行,需要有别的措施来保证右值。
为什么会成为左值?其实是因为这个右值转左值转早了,Fun(t)那里传的就已经全是左值了,在一个个函数传参数时很有可能会不在合适的地方转成左值,用Fun(forward< T >(t))可以保持右值,这也就是完美转发。
左值引用已经能解决大部分问题,不过它没解决的问题就是局部对象返回问题和插入接口、对象拷贝问题。比如自定义类型的局部变量/对象的返回,这一直是一个不好解决的问题。浅拷贝的类,像日期类,那么就用拷贝构造,如果是深拷贝,那就是移动构造,移动构造可以转移右值,因为它没有拷贝,只有移动资源,效率更高。
类里还是要有右值引用的版本,编译器会找最合适的函数来运行代码。
4、移动赋值
函数返回值时,会进行两次深拷贝,其实应当是一次拷贝构造,一次拷贝赋值,相对应的就有移动构造和移动赋值。返回局部变量时,它会优化成移动构造+移动赋值,就不是两个拷贝了。
右值引用借助移动语义来改善自定义类型的深拷贝问题。
string& operator=(string&& str)//移动赋值
string& operator=(const string& str)/拷贝赋值
移动赋值是如何实现的?比如s = t,t是一个右值,马上要结束生命周期了,那么移动赋值会把t的内容和s的内容直接换过来,这样s里面就是t的内容了,t里面就是s的内容了,t带着s的内容析构,那么移动赋值就完成了。
5、C++98的const引用延长生命周期
const string& ret1 = string("hello world");
没有右值引用的时候,就用这种方法来完成临时变量的生命周期延长,但这种方法有缺陷,但是当出了这个类,析构后还是不见了,想用类外的函数来使用类内的东西就不行了。
如果类外的函数不是引用类型的,它返回的值会是一个临时变量,如果用引用类型的变量来接收肯定就不行,但加了一个const就可以了,并且这个临时变量也确实延长周期了。
本篇gitee
结束。