作者:@小萌新
专栏:@C++进阶
作者简介:大二学生 希望能和大家一起进步!
本篇博客简介:介绍C++11的一些背景知识 本篇博客主要是讲解一些关键字
C++11
- 前言
- C++11诞生简介
- 列表初始化
- {}初始化
- 关键字
- auto
- decltype
- nullptr
- 范围for
- STL的更新
- 新容器
- 新方法
- 新函数
前言
经过了漫长了C++语法学习还有高阶数据结构学习我们也差不多来到了C++学习的收尾部分C++11
这部分主要是一些零碎的知识点 中间还可能涉及到一些linux相关知识 涉及到linux部分如果我们在之前的博客中还没有学习 我会特意标注出来 并且在讲解前置知识的博客写完之后继续这部分的学习
C++11的语法大部分是很鸡肋的 我们这里只会学习其中比较有用的部分 如果有同学想要学习所有的C++11新语法 新特性可以参考C++11官网 C++11官网
C++11诞生简介
-
在2003年C++标准委员会曾经提交了一份技术勘误表(简称TC1),使得C++03这个名字已经取代了
C++98称为C++11之前的最新C++标准名称。不过由于C++03(TC1)主要是对C++98标准中的漏洞
进行修复,语言的核心部分则没有改动,因此人们习惯性的把两个标准合并称为C++98/03标准。 -
从C++0x到C++11,C++标准10年磨一剑,第二个真正意义上的标准珊珊来迟。相比于
C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中
约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。 -
C++11能更好地用于系统开发和库开发、语法更加泛化和简单化、更加稳定和安全,不仅功能更
强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个
重点去学习。
这里还有一个关于C++11的小故事
1998年是C++标准委员会成立的第一年,本来计划以后每5年视实际需要更新一次标准,C++国际
标准委员会在研究C++ 03的下一个版本的时候,一开始计划是2007年发布,所以最初这个标准叫
C++ 07。但是到06年的时候,官方觉得2007年肯定完不成C++ 07,而且官方觉得2008年可能也
完不成。最后干脆叫C++ 0x。x的意思是不知道到底能在07还是08还是09年完成。结果2010年的
时候也没完成,最后在2011年终于完成了C++标准。所以最终定名为C++11。
列表初始化
{}初始化
我们在前面的C语言中我们一般习惯使用这样子的代码来初始化数组
int arr[] = { 1,2,3,4,5 };
int arr2[4] = { 0 };
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。
int arr[] = { 1,2,3,4,5 };
point p{ 1,2};
C++11扩大了用大括号括起的列表(初始化列表)的使用范围,使其可用于所有的内置类型和用户自
定义的类型,使用初始化列表时,可添加等号(=),也可不添加。
// 对于内置类型进行初始化
int a = { 3 };
int a{ 3 };
// 对于数组进行初始化
int arr[5] = { 1,2,3,4,5 };
int arr2[5]{ 1,2,3,4,5 };
// 对于结构体元素进行初始化
point p = { 1,3 };
point p{ 1,3 };
// new表达式
int* p1 = new int[4] {0};
// int* p1 = new int[4] = {0};
// new表达式初始化的时候不可以加等号
这里特别要注意的一点是 使用new表达式初始化不能使用等于号了
创建自定义对象的时候也可以使用列表初始化
class Date
{
public:
Date(int year,int month , int day)
:_year(year)
,_month(month)
,_day(day)
{
;
}
private:
int _year;
int _month;
int _day;
};
void test4()
{
Date d1 = { 2023,1,23 };
Date d2{ 2023,1,23 };
}
initializer_list容器
C++11中新增了initializer_list容器 C++11中能够使用{}初始化就是因为这个容器的功劳
它提供了以下的成员函数
- begin和end迭代器 用于支持迭代器遍历
- size函数支持获取容器中的元素个数
它的本质就是一个用大括号括起来的列表
我们可以使用这样子的代码来确认它的类型
auto l = { 1,2,3,4,5 };
cout << typeid(l).name() << endl;
代码运行的结果是下面这样子
initializer_list的使用场景
initializer_list容器并没有提供增删查改相关接口函数 这是因为这个容器提供出来了并不是让你储存数据 而是为了能够让其他容器进行列表初始化的
代码格式如下
vector<int> v = { 1,2,3,4,5 };
set<int> s = { 1,2,3,4,5 };
map<int, int> m = { make_pair(1,2),make_pair(3,4) };
这些容器之所以能够支持这样子的列表初始化 实际上是因为容器的成员函数当中增加了一个以initializer_list为参数的构造函数
那么底层原理实际上就是 先通过initializer_list构造出来一个列表 然后将这个列表中的所有元素拷贝到我们要构造的容器当中
initializer_list接口函数模拟实现
假设我们在一个简化版的vector容器中模拟这个构造函数的话 那么它的代码应该是这样子的
vector(initializer_list<T> il)
{
_start = new T[il.size()];
_finish = _start;
_endofstorage = _start + il.size();
for (auto x : il)
{
push_back(e);
}
}
同样的 如果我们要实现等于号操作支持列表初始化也可以进行一个函数重载
vector<T>& operator=(initializer_list<T> il)
{
vector<T> tmp(il);
std::swap(_start, tmp._start);
std::swap(_finish, tmp._finish);
std::swap(_endofstorage, tmp._endofstorage);
return *this;
}
这里总结下
- 构造函数中遍历initializer_list时 可以使用范围for遍历 也是使用迭代器遍历 二者本质上没有差别 因为范围for遍历的底层就是使用迭代器实现的
- 假如我们使用迭代器遍历 由于使用了模板参数 所以我们在声明类型的时候要加上typename 告知编译器这是一个类型
- 如果不增加一个以initializer_list为参数的赋值运算符重载函数 括号初始化也可以运行
关键字
auto
在C++11中 auto关键字的作用是自动推导类型
使用示例如下
map<int, string> m;
map<int, string>::iterator it = m.begin();
auto it = m.begin();
我们可以发现 使用auto关键字之后代码变得简洁了很多
在某些情况下 使用auto自动推导类型是十分有用的 比如说下面的情况
short a = 32670;
short b = 32670;
short c = a + b;
cout << c << endl;
上面的代码输出的结果是这样子的 会造成数据溢出
但是如果我们使用auto推导的话情况就会不一样了
short a = 32670;
short b = 32670;
auto c = a + b;
cout << c << endl;
decltype
关键字decltype可以将变量类型声明为表达式指定的类型
const int x = 1;
double y = 2.2;
decltype(x * y) ret;
decltype (&x) p;
cout << typeid(ret).name() << endl;
cout << typeid(p).name() << endl;
F(1, 'a');
F(1, 2.2);
显示结果如下
decltype除了能够推演表达式的类型 它还能够推演返回值的类型
void* GetMemory(size_t size)
{
return malloc(size);
}
void test11()
{
cout << typeid(decltype(GetMemory)).name() << endl;
cout << typeid(decltype(GetMemory(0))).name() << endl;
}
- 如果我们不带参数 则推导的是函数的类型
- 如果我们带参数 则推导的是返回值的类型
nullptr
在C语言中NULL 即空指针 被定义成了0 但是这在一些情况下可能会带来一些问题
比如说下面的代码
void f(int arg)
{
cout << "void f(int arg)" << endl;
}
void f(int* arg)
{
cout << "void f(int* arg)" << endl;
}
int main()
{
f(NULL);
return 0;
}
当我们像函数参数中传递一个空指针的时候实际上我们是想要调用第二个函数 可是结果缺不符合我们的预期
于是C++11的标准中引入了nullptr来代表空指针 使得我们写的代码更加的严谨
范围for
这个语法我们从刚刚学习C++就开始使用了 语法格式如下
vector<int> v;
v.push_back(2);
v.push_back(5);
v.push_back(4);
v.push_back(7);
v.push_back(1);
for (auto x : v)
{
cout << x << endl;
}
主体是这四行代码
for (auto x : v)
{
cout << x << endl;
}
- auto关键字的目的是为了自动推导类型 当然如果你知晓类型是什么格式也可以直接声明之来替换auto
- x为变量名 它代表的是容器中的每个数据 你也可以修改它的名字使之更好理解
- 变量名和容器名之间要用分号隔开
- 最后的就是容器名字 输入你想要遍历的容器即可
范围for的底层原理并不复杂 其实就是迭代器遍历 只要一个容器可以被迭代器遍历 那么它就可以被范围for遍历
和普通for循环类似 范围for也可以被continue和break关键字打断
范围for的两个使用条件
- 它的范围要是确定的 在begin和end迭代器之间
- 它的迭代器要支持++和==操作 如果它的迭代器不支持此操作 那么范围for也是不支持的
STL的更新
新容器
array容器
它的本质是一个静态数组
我们创建一个array对象需要提供两个参数 一个是数据的类型 一个是大小 具体示例代码如下
array<int,10> a1;
array<double, 12> a2;
array和普数组的对比
相同点: 它和普通数组一样支持通过下标访问操作符【】来访问数据 支持范围for遍历 并且创建后的数组大小不可改变
不同点:
- 它使用一个类对于array容器进行了封装 对于数组边界的检查更加严格了 如果使用下标访问操作符越界会出现断言检查 如果使用 at 成员函数访问会抛出异常
- 普通数组对于越界的检查只有在 写 操作的时候才会很严格 如果只是进行 读 操作并不会那么严格
因为它的对象是在栈上创建的 而栈的空间一般都很小 所以说我们不宜使用array创建过大的对象
forward_list容器
forward_list容器的本质上是一个单链表
相比于双链表来说单链表的缺点太多了 所以说我们平时不常使用forward_list容器来存储数据
这里简单介绍下forward_list容器的几个特点
- forward_list容器只支持头插头删 因为单链表的尾插尾删 都要先找到尾 时间复杂度是O(N) 而头插头删的时间复杂度则是O(1)
- forward_list容器提供的插入函数叫做insert_after 也就是在指定元素的后面插入一个元素 和别的容器在前面插入不一样 因为如果要做到前插的话还要继续遍历容器找到它的前一个元素 时间复杂度很高
- forward_list容器提供的删除函数叫做erase_after 也就是在指定元素的后面删除一个元素 和别的容器在前面删除不一样 因为如果要做到前删的话还要继续遍历容器找到它的前一个元素 时间复杂度很高
unordered_map和unordered_set容器
关于这两个容器 它们的底层是用哈希表实现的 具体的使用可以参考我的这篇博客
unordered_map和unordered_set的容器使用
关于它们的底层实现可以参考我的这篇博客
unordered_map和unordered_set的容器的模拟实现
新方法
C++容器中提供了一些新方法
- 提供了一个以initializer_list作为参数的构造函数 用于支持列表初始化
- 提供了cbegin和cend方法 用于返回const迭代器
- 提供了emplace系列方法 并在容器原有插入方法的基础上重载了一个右值引用版本的插入函数 用于提高向容器中插入元素的效率
关于第三点 我们在学到右值引用的时候会具体讲解
新函数
to_string
在C++11中提供了一个函数可以将别的类型转化为字符串类型
to_stirng ()
它的底层实现原理是函数重载
string类型转化为其他类型
如果要将string类型转化为其他类型 这里我们只需要调用对应的函数即可
最后的数字代表转化后的类型
i 代表 int
l 代表 long
ll 代表 long long 依次类推