C++日新月异的未来代码:C++11(上)

C++11 能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个重点去学习

1.统一的列表初始化

1.1 普通{ }初始化

struct Point
{
	int _x;
	int _y;
};
int main()
{
	int array1[] = { 1, 2, 3, 4, 5 };
	int array2[5] = { 0 };
	Point p = { 1, 2 };
	return 0;
}

C++98 中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定

struct Point
{
	int _x;
	int _y;
};
int main()
{
	int x1 = 1;
	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;
}

C++11 扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自定义的类型,使用初始化列表时,可添加等号 =,也可不添加

简单来说,C++11 一切皆可用 {} 初始化,并且可以不写 =,建议日常定义,不要去掉 =,但是我们要能看懂

🔥值得注意的是:Point p1 = { 1, 1 } ,是一种多参数隐式转化,无论哪一种初始化方式都是要调用对应的构造函数的

1.2 initializer_list

initializer_listC++11 引入的一个轻量级容器,用于支持统一的初始化语法和参数列表初始化。它允许函数或类接收任意数量的同类型参数,并提供简洁的初始化方式

vector<int> v1 = { 1,2,3 }; // 调用 vector 的 initializer_list 构造函数

stl 中的容器基本都是支持 initializer_list 初始化的,这初始化的方式和上面的隐式类型不同,具体方式如下:

在这里插入图片描述

首先要知道 initializer_list 主要有这三个成员

  1. 创建一个临时数组(存储在栈上),包含元素 {1, 2, 3}
  2. 生成一个 initializer_list<int> 对象,该对象引用临时数组(内部保存数组的起始地址和长度)
  3. initializer_list 引用临时数组(不拥有其内存),vector 会将临时数组的内容复制元素到自己的内存空间(堆上),与临时数组无关
  4. 临时数组被销毁

🔥值得注意的是: vector 是复制引用数组的元素,而不是直接接收,是因为该临时数组销毁之后,如果 vector 还接收着的话会造成悬空引用

因此 initializer_list 的存在还是很有必要的:

C++11 之前,若想让函数接收一个类似 {1, 2, 3} 的初始化列表作为参数,需要通过数组或容器(如 vector)传递,不够直观。initializer_list 允许函数直接以初始化列表为参数,使代码更符合直觉

2.声明

2.1 auto、nullptr

C++ 增加的特性在前面的文章进行过详细讲解,不过多叙述

传送门:C++命运石之门代码抉择:C++入门(下)

2.2 decltype

// decltype的一些使用使用场景
template<class T1, class T2>
void F(T1 t1, T2 t2)
{
	decltype(t1 * t2) ret;
	cout << typeid(ret).name() << endl;
}
int main()
{
	const int x = 1;
	double y = 2.2;
	decltype(x * y) ret; // ret的类型是double
	decltype(&x) p;      // p的类型是int*
	cout << typeid(ret).name() << endl;
	cout << typeid(p).name() << endl;
	F(1, 'a');
	return 0;
}

typeid().name() 是一种读取类型的方式,但是无法使用,decltypeauto 类似,可以自动推导类型,auto 只能推导,decltype 可以推导并使用

3.左值右值

3.1 概念

传统的 C++ 语法中就有引用的语法,而 C++11 中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名

🚩什么是左值?什么是左值引用?

左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址,一般可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时 const 修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名

int main()
{
	// 以下的p、b、c、*p都是左值,字符串也算一种左值
	int* p = new int(0);
	int b = 1;
	const int c = 2;
	// 以下几个是对上面左值的左值引用
	int*& rp = p;
	int& rb = b;
	const int& rc = c;
	int& pvalue = *p;
	return 0;
}

简单来说就是能够取地址的就是左值,左值大多数能赋值(除 const 左值),为左值取别名的变量就是 左值引用

🚩什么是右值?什么是右值引用?

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名

int main()
{
	double x = 1.1, y = 2.2;
	// 以下几个都是常见的右值
	10;
	x + y;
	fmin(x, y);
	// 以下几个都是对右值的右值引用
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rr3 = fmin(x, y);
	// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
	10 = 1;
	x + y = 1;
	fmin(x, y) = 1;
	return 0;
}

简单来说,不能取地址的,就是右值,右值不能做左操作数,为右值取别名的变量就是 右值引用

🔥值得注意的是: 右值引用之后,是可以对引用变量取地址修改的,不想修改的话加 const 即可

3.2 左值引用与右值引用比较

🚩左值引用

int main()
{
	// 左值引用只能引用左值,不能引用右值。
	int a = 10;
	int& ra1 = a;   // ra为a的别名
	//int& ra2 = 10;   // 编译失败,因为10是右值
	// const左值引用既可引用左值,也可引用右值。
	const int& ra3 = 10;
	const int& ra4 = a;
	return 0;
}
  1. 左值引用只能引用左值,不能引用右值
  2. 但是 const 左值引用既可引用左值,也可引用右值

因为两者的生命周期不同,会造成悬空。假设允许 int& ra2 = 1010 是右值,无固定内存地址,生命周期短暂。若 ra2 绑定到 10,当表达式结束后,10 被销毁,ra2 将悬空。而 const int& 通过延长右值生命周期,创建一个临时对象存储右值,并将引用绑定到该临时对象,避免了此问题

🚩右值引用

int main()
{
	// 右值引用只能右值,不能引用左值。
	int&& r1 = 10;

	// error C2440: “初始化”: 无法从“int”转换为“int &&”
	// message : 无法将左值绑定到右值引用
	int a = 10;
	int&& r2 = a;
	// 右值引用可以引用move以后的左值
	return 0;
}
  1. 右值引用只能右值,不能引用左值
  2. 但是右值引用可以 move 以后的左值

右值引用是个临时存储的对象,如果引用左值的话,原本左值中正在使用的值会因为右值引用的销毁而受影响,但是可以使用 move 转成右值,下面会对 move 进行解析

3.3 左值引用与右值引用的应用

string& func()
{
	static string a;
	return a;
}

int main()
{
	string ret = func();
	return 0;
}

之前我们学习过引用返回,是一种左值引用,将变量的地址返回回去,但是这种方式的局限性巨大,大部分情况下返回的是一个局部变量,那么就不能用左值引用返回了,这里只能使用传值返回,传值返回会导致至少 1 次拷贝构造(如果是一些旧一点的编译器可能是两次拷贝构造)

那么右值引用和移动语义解决上述问题:

首先介绍一个概念

  • 普通类型的右值: 纯右值
  • 自定义类型的右值: 将亡值

在这里插入图片描述

早期编译器还没有优化的时候返回的 str 先传递给临时变量,这个过程是拷贝构造,是一次深拷贝,由于此时的临时变量是右值,所以可以使用移动构造,下面将详细介绍移动构造的实现:

// 拷贝构造
		string(const string& s)
			:_str(nullptr)
		{
			cout << "string(const string& s) -- 深拷贝" << endl;
			string tmp(s._str);
			swap(tmp);
		}
//移动构造
		string(string&& s)
			:_str(nullptr)
		{
			cout << "string(string&& s) -- 移动拷贝" << endl;
			//......
		}

如图为移动构造的大概框架

  1. 参数传递: 移动构造函数接收一个右值引用参数 string&& s

  2. 资源转移: 举个例子,省略号处可能执行 data = s.datasize = s.size 的操作,看起来好像是转移了资源,其实不是的,只是把原来对象的指针和属性转移给了新对象,并没有拷贝原来对象的主要数据,简单来说就是资源所有权的转移

  3. 原对象资源清理: 将原对象的data指针置为 nullptrsize 置为 0,这样当原对象在析构时(即出了局部作用域时),由于资源所有权被转移给新对象,所以这部分资源不会给释放

通过这样的方式,移动构造函数实现了资源的高效转移,避免了像拷贝构造函数那样对资源进行复制,提高了对象创建和资源管理的效率


通过不断的编译器优化,如今已经可以只使用一次移动构造就能解决了

在这里插入图片描述

两个步骤直接合二为一,编译器通过特殊处理,将本来是左值的 str 返回值,识别成将亡值,直接使用移动构造转移资源

不仅仅有移动构造,还有移动赋值:

在这里插入图片描述

移动构造: func 函数中创建了 string 对象 str ,当 return str 时,会调用移动构造函数。因为 str 是函数内局部对象,返回时将其资源以移动方式构造一个临时对象用于返回值。这避免了对字符串内容的深拷贝,直接转移资源所有权,提升效率

移动赋值:main 函数中,先创建了 string 对象 ret2 ,然后 ret2 = func(); ,这里func() 返回一个临时对象,此操作会调用 string 类的移动赋值运算符。它将 func 函数返回的临时对象的资源(如内部存储字符串的指针等相关资源)转移给 ret2 ,而不是进行字符串内容的复制,同样是为了提高效率

🔥值得注意的是: 移动构造函数是用于创建一个新对象,并从另一个对象(通常是临时对象)那里获取资源所有权;而移动赋值是用于将一个已存在对象的资源所有权转移给另一个已存在的对象。简单来说,移动构造函数是在对象创建时起作用,而移动赋值是在对象已经存在之后进行资源转移时起作用


int main()
{
	string s1("hello world");
	string s2(s1);
	string s3(move(s1));
	return 0;
}

了解了右值引用相关的移动构造,那么 move 就好解释多了,move 简单理解就是把资源完全复制到一个右值新对象,对原来的对象并没有修改

s2 这里 s1 是左值,调用的是拷贝构造,这里我们把 s1 move处理以后,会被当成右值,调用移动构造,但是这里要注意,一般是不要这样用的,因为我们会发现 s1 的资源被转移给了 s3s1 被置空了

3.4 完美转发

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);
}
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&& 的推导规则:

  • 当传入左值(如变量 a)时,T 被推导为左值引用类型(如 int&),此时 T&& 折叠为 int&(即左值引用),也叫引用折叠

  • 当传入右值(如 10move(a))时,T 被推导为非引用类型(如 int),此时 T&& 成为 int&&(即右值引用)

在这里插入图片描述

但运行之后发现全都是左值,并没有按照传入实参的类型调用

关键点: 变量名本身永远是左值,无论这个变量是左值引用还是右值引用类型

为什么 t 是左值?

T&& 无论被推导成什么,都是根据 t 这个实参的传入进行调整的,我们知道右值是不可以被修改的,所以这里理应是个左值,而且 C++ 标准明确规定:变量名(无论其类型是左值引用还是右值引用)都是左值表达式

因此,Fun(t) 中的 t 始终被视为左值,调用 Fun(int&)Fun(const int&)

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; }
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{
	Fun(forward<T>(t));
}
int main()
{
	PerfectForward(10);           // 右值
	int a;
	PerfectForward(a);            // 左值
	PerfectForward(std::move(a)); // 右值
	const int b = 8;
	PerfectForward(b);      // const 左值
![请添加图片描述](https://i-blog.csdnimg.cn/direct/5331a7028bb842c8987b625c5cc4c15e.png)
	PerfectForward(std::move(b)); // const 右值
	return 0;
}

因此保持参数的原始值类别(左值或右值)就要用到 forward

  • 如果原始参数是左值,T 会被推导为左值引用(如 int&),forward<int&>(t) 返回左值引用
  • 如果原始参数是右值,T 会被推导为非引用类型(如 int),forward<int>(t) 返回右值引用(int&&

这样,t 的原始值类别(左值 / 右值)就被正确传递给 Fun 函数


希望读者们多多三连支持

小编会继续更新

你们的鼓励就是我前进的动力!

请添加图片描述

评论 63
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值