C++11部分新特性讲解

一、统一的列表初始化

C++98的处理方式

在原本的C++98中,已经有了对数组或者结构体的{}初始化方式,他们的使用格式见下方代码:

#include <iostream>
using namespace std;

struct test {
	int _x;
	int _y;
};
int main() {

	int arry[] = { 0,1,2 };
	int arry[6] = { 1,2,3 };
	test t = { 1,2 };
	return 0;
}

其中,对结构体的初始化,是将1和2分别赋值给结构体中的成员,_x和_y。
注意:如果是对class类的对象进行初始化,要求初始化的成员的访问权限为公有。

C++11的处理方式

到了C++11,{}初始化的范围扩大到了所有的内置类型和用户的自定义类型

#include <iostream>
using namespace std;

struct test {
	int _x;
	int _y;
};

int main() {


	//内置类型
	int x1 = 1;
	int x2{ 1 };

	//数组
	int arry[] = { 1,2,3 };
	int arry[5]{};

	//自定义类型
	test t{ 5,6 };

	return 0;
}

注意:{}进行初始化加不加=都可以,例如上面自定义类型test的初始化还可以写成test t={5,6};

在创建自定义类型对象的时候,也可以通过{}调用有参构造函数,对对象进行初始化

#include <iostream>
using namespace std;

struct Date {
	Date(int year,int month,int day)
		:_year(year)
		,_month(month)
		,_day(day)
	{
		cout << "Date(int year,int month,int day) " << endl;
	}

	int _year;
	int _month;
	int _day;
};

int main() {

	Date date{ 2024,7,27 };
	return 0;
}

运行程序会发现屏幕上打印了"Date(int year,int month,int day) ",说明使用{}进行构造调用了有参构造

二、auto和decltype关键字

auto和decltype用法

auto关键字的作用使用来定义一个变量,编译器能够自动确定变量的类型,当然前提是在定义的时候就进行了初始化,编译器根据初始化所使用量的类型来确定定义变量的类型。

decltype关键字则是将变量的类型声明为制定表达式的类型

#include <iostream>
using namespace std;
int main() {
	auto x = 1;
	cout << typeid(x).name() << endl;

	decltype(1.1 * 5.6) ret;
	cout << typeid(ret).name() << endl;
}

最后的结果就是,x的类型显示为为其进行初始化的常量1的类型int,ret的类型显示为表达式(1.1*5.6)运算后的类型double。

使用auto,判断{}的类型

有了auto自动获取类型,再配合typeid()函数,我们就可以知道{}的类型,进而了解一下{}进行初始化的底层原理

#include <iostream>
using namespace std;
int main() {
	auto il={ 1,2,3 };
	cout << typeid(il).name() << endl;
}

运行代码,结果如下图所示

由此我们可知,{}的类型是一个名叫initializer_list的模板类,使用它进行初始化,其实就是使用它进行构造

三、nullptr

由于c++中的NULL被定义成字面量0,这样就带来了一些问题,因为0既能表示指针常量,又能表示指针常量,所以在C++11中新增了nullptr用来表示空指针。

四、右值引用和移动构造以及移动赋值

什么是右值引用

相信左值引用大家已经很熟悉了,在了解右值引用前,先要知道什么是右值,判断左右值有一个最权威的可以看作定义的方法就是,可以获取它的地址的就是左值,反之则是右值。除此之外左值可以出现在=的左边,而右值不可以。但是要知道左值定义的时候如果加了const也是不可以赋值,但是可以对他进行区地址。有了左右值,对左值的引用就是左值引用,对右值的引用就是右值引用。左右值及其引用的常见例子见下

#include <iostream>
using namespace std;

//double fmin(double x,double y) {
//	return x < y ? x : y;
//}
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;


	double x = 1.1, y = 2.2;

	//以下几个都是常见的右值
	10;
	x + y;
	fmin(x,y);

	//以下几个都是对右值的右值引用
	int&& rr1 = 10;
	double&& rr2 = x + y;
	double&& rrs=fmin(x, y);

}

注意:1.const左值引用既可以引用左值也可以引用右值
           2.右值引用可以引用move后的左值

右值引用的使用场景和意义

在研究右值引用的作用之前,先想一下左值引用的作用,也就是在函数传参和函数传返回值的时候使用来减少拷贝。那么再思考左值引用有没有彻底解决这两个问题?答案是没有,在函数传参部分的问题是解决了,但是传返回值的时候,当返回值如果是函数中的局部变量这种情况没有解决。所以右值引用的价值之一,就是补齐了最后这个模块——传值返回的拷贝问题。

首先先来看不使用引用,函数内返回局部变量的情况,我们以自己模拟实现的string类进行实验,调用to_string函数,内部就是生成了一个局部的string对象,并返回。

由图可知,编译器优化之前,进行了两次拷贝,第一次是将局部string对象,传入寄存器,方便使用,第二次拷贝是函数外,使用寄存器中的string对象,对新创建的string对象ret进行拷贝构造。但是编译器会对两次拷贝构造进行优化,直接使用函数内的局部string对象,对ret进行拷贝构造。

看完之前的做法之后,我们再来看有了右值引用后的做法,首先,我们在string类内,实现一个移动构造函数,什么是移动构造函数呢?移动构造函数就是将构造的参数设置为右值引用,当通过右值或者将亡值(将要被释放的局部变量),进行string的拷贝构造的时候,就会调用该拷贝构造函数,该函数的内部,将局部变量和string的资源直接进行交换,比如类成员是一个指针,指向一段区域,则直接将双方指针指向区域进行交换,新创建的string对象就会掌管将要释放的对象的所管理的区域,进而免去了耗时的拷贝操作。

//移动构造
string(string&& s){

    cout<<"string(string&& s)"<<endl;
    
    swap(s);
}

有了以上铺垫,我们来考虑,有了右值引用和移动构造以后,函数内返回局部变量的情况

由上图我们可以看出,编译器优化之前,第一步仍然是将局部对象拷贝构造给寄存器,第二步,由于寄存器里的值为将亡值,所以不再调用普通的构造函数,而是根据重载的特性调用移动构造。经过编译器优化之后,就只需要一次移动构造就完成了。大大减少了拷贝成本。

除了上面说的一种情景外,还有好多移动拷贝应用的例子。比如我们去看C++官网中,list的push_back接口可以发现,C++11中新增了传入参数为右值引用的版本

这一改动,其实就是底层调用了传入参数的移动构造,减少了参数的拷贝。因为要想讲解清楚涉及到要自己模拟实现list,以及模拟实现一个使用拷贝构造的参数,还涉及到万能引用,以及forward保持参数原属性。这些部分讲起来比较复杂,如果有需要可以在评论区留言,再另外单独写一篇文章进行讲解。

移动构造和移动赋值的默认生成

移动构造和移动赋值是C++11新增的,除了自己手动实现外,如果没有自己实现移动构造,并且没有实现析构函数、拷贝构造、拷贝赋值重载中任意的一个。编译器就会自动生成一个默认的移动构造函数,默认生成的移动构造函数,对内置类型成员会按成员进行逐字节拷贝,自定义类型的成员,如果实现了移动构造则调用其移动构造,没有实现则调用其拷贝构造。

此外,就算不满足上述条件也可以通过使用default关键字,强制编译器生成默认函数,以及使用delete关键字,禁止生成默认函数

Person(Person&& p)=default;

Person(Person& p)=delete;

五、lambda表达式

lambda表达式有什么用?

在回答这个问题之前,我们先来看一个C++98,对自定义类型进行排序的例子。

#include <iostream>
#include<vector>
#include<algorithm>
using namespace std;

struct Goods {
	string _names;//名字
	double _price;//价格
	int _evaluate;//评价
	Goods(const char* str, double price, int evaluate)
		:_names(str)
		, _price(price)
		, _evaluate(evaluate) 
	{}
};
struct ComparePriceLess
{
	bool operator()(const Goods&gl,const Goods&gr) {
		return gl._price < gr._price;
	}
};
struct ComparePriceGreater
{
	bool operator()(const Goods& gl, const Goods& gr) {
		return gl._price > gr._price;
	}
};

int main() {
	vector<Goods> v = { {"苹果",2.1,5},{"香蕉",3,4},{"橙子",2.2,3},{"菠萝",1.5,4} };
	sort(v.begin(), v.end(), ComparePriceLess());//升序
	sort(v.begin(), v.end(), ComparePriceGreater());//降序
	return 0;
}

代码中对自定义的Goods类,使用algorithm库中的sort函数,对v进行排序,排序需要传入用于比较的仿函数,但是这种写法太复杂了,每次为了实现一个algorithm算法,都要重新实现去写一个类,如果每次比较的逻辑不一样,还要去实现多个类,特别是相同类的命名,因此在C++11中出现了lamdba表达式。

lambda表达式的语法

lambda表达式的书写格式为:[capture-list](parameters)mutable -> return-type {statement}
[capture-list]:捕捉列表,总是放在开始的位置,编译器就是通过他来判断接下来的代码是否是lambda函数,它能够捕捉上下文中的变量,以供lambda使用。
(parameters):参数列表。和普通函数的参数列表一样。如果不需要参数,可以连同()一起省略
mutable:lambda函数默认具有const属性。加了mutable可以取消其常量性。注意使用该修饰符时,参数列表不可省略(即使参数为空)。
-> return-type:返回值类型。没有返回值类型时可以省略。返回值类型明确时也可以省略,由编译器自动推倒。
{statement}:函数体。可以使用参数和捕捉到的变量

lambda表达式使用的例子

#include <iostream>
using namespace std;

int main() {

	int a = 1, b = 2;
	auto swap1 = [a, b]()mutable{
		int temp=a;
		a = b;
		b = temp;
		};
	swap1();

	cout << "a=" << a << " b=" << b << endl;
	return 0;
}

上述代码就是用lambda表示试,实现的一个交换a,b值的函数,通过捕捉列表将a,b的值进行传入。添加mutable关键字,取消常量性,使得a,b可以修改。可是运行函数函数的结果是a=1 b=2。未完成交换,原因是上述捕捉方式是传值捕捉,要想完成交换需要使用引用捕捉。下面对代码进行修改。

#include <iostream>
using namespace std;

int main() {

	int a = 1, b = 2;
	auto swap1 = [&a, &b](){
		int temp=a;
		a = b;
		b = temp;
		};
	swap1();

	cout << "a=" << a << " b=" << b << endl;
	return 0;
}

我们在a,b之前加上&,就是引用捕捉,捕捉了a,b本身,实现了a,b的交换。除此之外,可以捕捉=,表示普通捕捉lambda实现上方的所有变量,可以捕捉&,表示引用捕捉lambda实现上方的所有变量。甚至还可以混合捕捉,下面再看几个例子。

#include <iostream>
using namespace std;

int main() {

	int x=0, y=1;
	auto func1 = [x, &y] {};
	auto func2 = [=, &y] {
		//cout<<m<<endl;//m捕捉不到
		cout << x << endl;
		};
	int m;
	return 0;
}

上述两个lambda表达式,都是混合捕捉。其中func1表示,对x进行普通捕捉,对y进行引用捕捉。
func2表示,对所有变量普通捕捉,对y引用捕捉。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值