【C++】C++11新特性(1)

目录

一、C++11简介

二、更泛用的列表初始化

三、新增类型:initializer_list

四、简化声明的方式:auto和decltype

4.1 auto

4.2 decltype

五、范围for循环

六、指针空值nullptr

七、STL新容器

7.1 array

7.2 forward_list

7.3 unordered_map和unordered_set

八、lambda表达式

8.1 语法

8.2 捕获列表

8.3 lambda底层和细节


一、C++11简介

C++11标准由国际标准化组织(ISO)和国际电工委员会(IEC)旗下的C++标准委员会于2011年8月12日公布,并于2011年9月出版,为C++编程语言的第三个官方标准。

相比于C++98/03,C++11带来了约140个新特性和对C++03中约600个缺陷的修正,能够更好的用于系统开发和库开发,功能更强大

二、更泛用的列表初始化

在C++98中,我们可以用大括号{}在创建数组或结构体元素的同时进行初始化,例如:

struct Coordinate
{
	int _x;
	int _y;
};

int main()
{
	int arr1[] = { 1,2,3,4,5 };
	int arr2[5] = { 0 };
	Coordinate c = { 1,1 };
	return 0;
}

在C++11中扩大了这类用大括号括住的列表的使用范围,我们在使用其进行初始化时可以不必带等号,且可以用于所有的内置类型、自定义类型和new表达式

class Coordinate
{
public:
	Coordinate(int x, int y)
		:_x(x)
		,_y(y)
	{}
private:
	int _x;
	int _y;
};

int main()
{
	int arr1[]{ 1,2,3,4,5 }; //现在等号可加可不加
	int arr2[5]{ 0 };
	int* p = new int[5]{ 0 }; //现在列表初始化也可以适用于new表达式中
    vector<int> v{ 1,2,3,4,5 }; //可以像这样初始化vector等容器
	Coordinate c1 = { 1,1 };
	Coordinate c2{ 1,1 }; //通过列表初始化方式调用自定义类型的构造函数,也可不加等号
	return 0;
}

上面的代码无法在C++11之前的标准下运行,否则会报错:

还可以这样来初始化自定义类型:

class Date
{
public:
	Date(int year, int month, int day)
		:_year(year)
		,_month(month)
		,_day(day)
	{}
private:
	int _year;
	int _month;
	int _day;
};

int main()
{
	Date* p = new Date[3]{ {2024,7,29},{2024,7,30},{2024,7,31} };
	return 0;
}


三、新增类型:initializer_list

initializer_list是C++11中提供的新类型,定义在<initializer_list>头文件中

template<class T> 
class initializer_list;

initializer_list常常作为其他类型的构造函数的参数使用,C++11中不少容器也增加了以initializer_list为参数的构造函数和拷贝构造,如list、vector、map、set等:

initializer_list也支持迭代器,其迭代器就是原生指针

不过需要注意,list和vector用这种方式初始化是因为支持initializer_list为参数的构造函数,而自定义类型用这种方式初始化是通过列表初始化调用构造函数


四、简化声明的方式:auto和decltype

4.1 auto

auto在很久之前我们已经提过了,这里再简述一下

C++11之后,auto被赋予了新的功能,即自动类型推断。也就是说我们可以不显式定义一个变量的类型,而根据等式右侧的值来进行类型推导,如:

需要注意的是,使用auto定义变量时必须对变量进行初始化,因为在编译阶段编译器需要根据初始化表达式来推导auto的实际类型。

因此,auto并非是一种类型的声明,而是一个占位符,编译器在编译时会将auto替换为变量实际的类型 

一些使用auto的注意事项:

  • 如果auto后加上*就限定了赋值的对象必须是指针

  • 我们使用auto时,也可以在同一行定义多个变量,前提是这些变量必须是相同的类型

  • 用auto声明引用类型时要加上&
#include <iostream>
using namespace std;
 
int main()
{
	int a = 10;
	auto& b = a;
	return 0;
}
  • auto不能在函数的参数中使用

  • auto不能用于声明数组

4.2 decltype

decltype可以将一个变量或表达式的类型提取出来,用于声明另一个变量的类型,例如:

有人要问了:typeid也可以取出类型,为什么不能用它来声明类型呢?

因为这种方式取出的是字符串形式的类型,无法用于声明变量


五、范围for循环

现代C++倾向于让各种繁杂的操作变得简洁,因此诞生了许多语法糖,范围for算是其中的典型。

在C++98/03中,不同的容器和数组遍历的方式有很多,不够统一,也不够简洁。

而C++11出现了基于范围的for循环,可以更简洁的去遍历容器和数组,也更方便我们使用了。

以前我们遍历数组的方式如下:

int main()
{
	int array[] = { 1,2,3,4,5 };
	for (int i = 0; i < sizeof(array) / sizeof(array[0]); i++)
	{
		cout << array[i] << " ";
	}
	cout << endl;
	return 0;
}

对于一个有范围的集合而言,由程序员来声明循环的范围未免太多余,还容易出错。接下来我们来使用范围for遍历数组:

for循环的括号中由冒号":"分为两部分,左边是范围内用于迭代的变量,右边表示被迭代的范围

这里也用到了前面的auto关键字,如果我们想对范围内的元素进行修改,还可以用到引用&

和普通循环一样,范围for中也可以使用continue和break。


六、指针空值nullptr

在过去,我们给一个没有指向的指针进行初始化的时候会使用NULL,而NULL实际上是一个宏。

我们在C语言中使用NULL没有问题,但是在C++中就会出现问题,为什么呢?

在C头文件stddef.h中,可以看到如下代码:

#ifndef NULL
#ifdef __cplusplus
#define NULL   0
#else
#define NULL   ((void *)0)
#endif
#endif

可以看到,在C++中NULL被定义为0,这样会造成什么麻烦呢?

可以看到,就算传递的参数为NULL,程序还是会调用int类型的Func,而不是int*的Func,这违背了我们的目的。

因此出现了指针空值nullptr来填补这个bug,使用nullptr时不需要包含头文件,因为它是C++11作为新关键字引入的。为了提高代码的健壮性,我们后续表示指针空值时最好都使用nullptr。


七、STL新容器

7.1 array

C++11新增的array容器,其目的是用来替代C语言风格的定长数组

template < class T, size_t N > 
class array;

该容器使用时需要引入<array>头文件,它提供了一种固定大小的数组容器,与 C 语言中的数组相比,具有更好的类型安全和内存管理特性

array基本语法如下:

#include <array>

std::array<T, N> array_name;

其中:

  • T是数组中元素类型
  • N是数组大小

7.2 forward_list

C++11中新增的容器forward_list,有别于list的双向链表结构,其结构为单链表

相比于双向链表,因为少了一个指针,更加节省内存,适用于需要频繁进行前向遍历和插入、删除操作的场景。

由于是单向链表,其迭代器也只支持从前往后遍历

这里不作更多介绍,有兴趣可以移步:

cplusplus.com/reference/forward_list/forward_list/?kw=forward_listicon-default.png?t=N7T8https://cplusplus.com/reference/forward_list/forward_list/?kw=forward_list

7.3 unordered_map和unordered_set

C++11中新增的容器unordered_map和unordered_set,其底层为哈希表

关于二者,在前面哈希表的文章中详细介绍过

【C++】哈希表-CSDN博客icon-default.png?t=N7T8https://blog.csdn.net/Eristic0618/article/details/140054885?spm=1001.2014.3001.5501


八、lambda表达式

在对一个集合中的元素进行排序时,我们可以使用仿函数来自定义排序规则,例如:

struct Fruit
{
	string _name;
	double _price; //价格
	int _evaluate; //评价
	Fruit(const char* str, double price, int evaluate)
		:_name(str)
		, _price(price)
		, _evaluate(evaluate)
	{}
};

struct ComparePriceLess //价格升序
{
	bool operator()(const Fruit& gl, const Fruit& gr)
	{
		return gl._price < gr._price;
	}
};

struct CompareEvaluateLess //评价升序
{
	bool operator()(const Fruit& gl, const Fruit& gr)
	{
		return gl._evaluate < gr._evaluate;
	}
};

int main()
{
	vector<Fruit> v = { { "苹果", 2.1, 5 }, { "香蕉", 3, 4 }, { "橙子", 2.2, 3 }, { "菠萝", 1.5, 4 } };
	sort(v.begin(), v.end(), ComparePriceLess());
	for (auto e : v)
	{
		cout << e._name << " ";
	}
	cout << endl;
	sort(v.begin(), v.end(), CompareEvaluateLess());
	for (auto e : v)
	{
		cout << e._name << " ";
	}
	cout << endl;
	return 0;
}

但是随着语言的发展,人们开始觉得仿函数用起来还是有点麻烦了,如果每次比较的逻辑不一样,就得重新实现一个类。因此,C++11中新增了lambda表达式

lambda有很多种叫法,有lambda表达式、lambda函数和匿名函数

8.1 语法

lambda表达式的书写格式:

[capture-list] (parameters) mutable throw() ->return-type { statement }

其中:

  • [capture-list]:捕获列表,也称为lambda导入器,位于表达式的开头。其中[]是Lambda引出符,编译器根据该引出符判断接下来的代码是否是Lambda函数。捕获列表可以捕捉上下文变量供lambda函数使用。捕获列表可以为空
  • (parameters):参数列表,类似普通函数的参数列表,如果不需要传递参数则可以连同括号全部省略
  • mutable:默认情况下lambda函数具有常性,加上mutable可以取消其常性。(使用该修饰符时参数列表不可省略,即使列表为空)
  • throw():用于在lambda函数内部抛出异常
  • ->return-type:返回类型,没有返回值时可省略,也可由编译器自行推导
  • { statement }:函数体,在函数体内除了可以使用参数列表中的参数,还可以使用捕获列表捕获到的变量

因为参数列表、可变规则、异常说明和返回类型都是可选择省略的,而捕获列表和函数体可以为空,所以一个最简单的lambda函数如下(这个表达式没有任何意义)

[]{};

例如上面的仿函数,将其修改为lambda表达式后:

//struct ComparePriceLess //价格升序
//{
//	bool operator()(const Fruit& gl, const Fruit& gr)
//	{
//		return gl._price < gr._price;
//	}
//};

//sort(v.begin(), v.end(), ComparePriceLess());

sort(v.begin(), v.end(), [](const Fruit& g1, const Fruit& g2) { 
        return g1._price < g2._price; 
    });

一样的参数列表,一样的函数体,一样的返回值,lambda表达式的用法和仿函数区别并不大

8.2 捕获列表

lambda表达式和普通函数的最大区别就在于其除了可以使用参数外,还可以通过捕获列表访问上下文中的数据。

捕获列表描述了上下文中哪些数据可以被lambda函数使用,其中又分为传值传引用方式

  • [var]:表示通过传值方式捕获变量var
  • [=]:表示通过传值方式捕获所有父作用域中的变量(包括this)
  • [&var]:表示通过传引用方式捕获变量var
  • [&]:表示通过传引用方式捕获所有父作用域中的变量(包括this)
  • [this]:表示通过传值方式捕获当前的this指针

例如:

其中,父作用域指包含lambda函数的语句块。除了单个捕获项,捕获列表还可由多个捕获项组成,例如:

  • [=, &a, &b]:以传引用方式捕获变量a和b,以传值方式捕获其他所有变量
  • [&, a, b]:以传值方式捕获变量a和b,以传引用方式捕获其他所有变量

需要注意,捕获列表中不允许变量重复传递,否则会导致编译错误,例如[=, a];捕获不属于父作用域的变量也会导致错误

8.3 lambda底层和细节

本质上,lambda表达式就是一个函数对象,其底层其实还是仿函数,就像范围for的底层还是迭代器。

所以我们也可以用其创建函数对象,并用仿函数的方式来使用该对象,例如:

lambda函数也有类型

每个lambda表达式都会生成一个仿函数,后面一长串的是其uuid

UUID_百度百科 (baidu.com)icon-default.png?t=N7T8https://baike.baidu.com/item/uuid/5921266通过传值方式捕获的参数,需要加上mutable取消lambda表达式的const属性才可以在表达式中进行修改。但是如果是引用捕获,不需要加也可以修改,可以看作是特殊处理

和普通函数一样,值捕获的参数在表达式内部进行修改不会影响外部的变量,类似于形参和实参的关系;只有引用捕获在修改后会改变原变量

如果在一个类成员函数中实现了一个lambda函数,并直接捕获这个类的成员变量,也会报错:

因为类的成员变量不属于该lambda函数的父作用域

我们可以通过捕获this指针来间接调用类成员变量:

我们写一段功能一样的仿函数和lambda函数,对比二者的汇编:

class Add
{
public:
	int operator()(int x, int y)
	{
		return x + y;
	}
};

int main()
{
	Add a;
	a(1, 2);
	auto f = [](int x, int y) {return x + y; };
	f(1, 2);
	return 0;
}

可以看到,二者的汇编代码也是几乎一致的

实际上在底层编译器对于lambda表达式的处理方式,完全就是按照函数对象的方式处理的

未完待续...

评论 16
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值