目录
7.3 unordered_map和unordered_set
一、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的双向链表结构,其结构为单链表
相比于双向链表,因为少了一个指针,更加节省内存,适用于需要频繁进行前向遍历和插入、删除操作的场景。
由于是单向链表,其迭代器也只支持从前往后遍历
这里不作更多介绍,有兴趣可以移步:
7.3 unordered_map和unordered_set
C++11中新增的容器unordered_map和unordered_set,其底层为哈希表
关于二者,在前面哈希表的文章中详细介绍过
【C++】哈希表-CSDN博客https://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)https://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表达式的处理方式,完全就是按照函数对象的方式处理的
未完待续...