C++学习记录——이십오 C++11(1)


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);

  1. 左值引用只能引用左值,不能引用右值。
  2. 但是const左值引用既可引用左值,也可引用右值。
  3. 右值引用只能右值,不能引用左值。
  4. 但是右值引用可以move以后的左值。
  5. 将亡值是一种右值,是生命周期即将结束的值。

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

结束。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值