目录
- C++11介绍
- 统一的列表初始化
- 对内置类型
- initializer_list
- 声明
- auto
- decltype
- nullptr
- 范围for
- 容器新增接口
- emplace
- 容器的新方法
C++的前身是“C with Classes”, 最早于 1979年由 祖师爷Bjarne Stroustrup(本贾尼·斯特劳斯特鲁普) 在贝尔实验室开始设计,旨在将 Simula(一种具有重大历史意义的编程语言)的 面向对象特性引入C语言。于1983年正式命名为C++。
作为如今热门的主流编程语言,C++有其制定标准与特性的委员会。
标准委员会成立:1990年,C++标准委员会成立,开始C++的标准化进程。
C++98标准: 1998年,发布了第一个C++正式标准,即C++98。这个标准使得C++更加稳定,并被广泛采用。同时,C++的标准库也得到了扩展,引入了诸如STL(Standard Template Library) 等功能。
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++98,C++11的原因,期间的多个版本要么是难产,要么只是对之前大版本的打补丁。直到C++11的到来。
C++11历经九九八十一难,终于艰难问世…,要知道隔壁 Java 可是每两年乃至每六个月更新一次新标准。于是,在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++11主要更新:
相比于C++98/03,C++11则带来了数量可观的变化,其中包含了约140个新特性,以及对C++03标准中约600个缺陷的修正,这使得C++11更像是从C++98/03中孕育出的一种新语言。相比较而言,C++11能更好地用于系统开发和库开发、语法更加泛华和简单化、更加稳定和安全,不仅功能更强大,而且能提升程序员的开发效率,公司实际项目开发中也用得比较多,所以我们要作为一个重点去学习。C++11增加的语法特性非常篇幅非常多,我们这里没办法一 一讲解,所以本文主要讲解实际中比较实用的语法。
C++11
官网:https://en.cppreference.com/w/cpp/11
后续标准与修订:
- C++03标准:2003年,发布了C++03标准,对C++98仅有很少的修订。
- C++11标准:2011年,发布了C++11标准,这是C++标准的一次重大升级,增加了大量新特性,如自动类型推导、智能指针、Lambda表达式、并发编程等,使得C++变得更加现代化和易用。
- C++14标准:2014年,发布了C++14标准,这是对C++11的一些小修订和改进。
- C++17标准:2017年,发布了C++17标准,引入了一些新特性,例如文件系统库、并行STL、变量模板、constexpr if等。
- C++20标准:2020年,发布了C++20标准,增加了概念(Concepts)、协程(Coroutines)、范围概述(Ranges)、模块(Modules)等重磅特性,进一步增强了C++的灵活性和强大性。
虽说现在已经更新到20了,但是有些版本只是小改动,像11,17,20这样的版本才更新了不少实用的新功能。但是实际上由于多方因素,最新的版本也不会大范围被快速使用,存在一定的滞后性,况且C++上手也有一定难度,前面的就够吃一壶的了,学习成本比较高。
再者就是编译器对新版本是否支持
主流的编译器有:GCC
、Clang
、MSVC
,其中 GCC
就是在 Linux
中使用的编译器,基本上 GCC 4.6 及后续版本就能对 C++11 进行很好的支持,而 MSVC
是微软 VS 系列的编译器,从 VS 2015 及后续版本对C++11 语法支持较好
统一的列表初始化
在C++98中,标准允许使用花括号{}对数组或者结构体元素进行统一的列表初始值设定。比如
C++11
中对 { } 进行了全面升级,使其不仅能初始化数组,还能初始化内置类型,用户定义的自定义类型以及 STL
中的容器的初始化
对内置类型
- 注意C++11之后初始化变量采用{}时,可以不加=符号; 但是可读性不是很好,不推荐。
有了C++11后的{},map的插入操作就会简洁很多。
实际上,{}的升级其实并不是什么新鲜事,其本质就是调用了构造函数。
如下面的日期类:
类对象在实例化时会自动调用构造函数初始化
Date d1(2024, 10, 12);
而d2这种写法实际上进行了隐式类型转化,采用了构造加赋值;先构造一个无名对象,再赋值
Date d2 = { 2024, 10, 12 };
如果要禁止隐式类型转化,在构造函数前面加上关键字explicit
,这样就能禁止隐式类型转化了
此时的d3为C++11{}升级后的写法,发现不受隐式类型转化的影响
虽说使用{}初始化时,可以不用=,那d2,d3不应该是一样的吗?需要注意的是上述的d1,d2,d3中只有d3是C++11之后才支持的写法,d1,d2是C++11之前就支持的语法,且语言是有向前性的,所以此处d2的写法并不是11之后的写法
Date d3{ 2024, 10, 12 };//升级后的{},不需要进行隐式类型转化
所以{}的升级支持内置,自定义类型的初始化实际上就是通过构造函数实现的。
initializer_list
上面介绍了{}对内置类型,用户定义的自定义类型的升级;而C++11过后{}对 STL
中的容器的初始化也进行了升级,使得容器的初始化更加得心应手。
介绍使用前先了解initializer_list
,initializer_list是一个类模板,提供以下成员函数:
- 提供了begin和end函数,用于支持迭代器遍历。
- 以及size函数支持获取容器中的元素个数
如:下面就是一个initializer_list
auto il1 = { 1,2,3,4,5,6 };
使用typeid
查看il1
的类型
typeid(il1).name()
可以看到编译器会自动识别{}里面的元素类型,此时类模板initializer_list
的类型已经推导出来为int
了
具体细节请参考——initializer_list参考文档
使用场景
initializer_list
一般是作为构造函数的参数,C++11对STL中的不少容器就增加,initializer_list作为参数的构造函数,这样初始化容器对象就更方便了。也可以作为operator=的参数,这样就可以用大括号赋值。
list<string> l1 = { "huawei","apple","xiaomi","oppo" };//构造函数l1 = { "vivo","redmi" };//赋值
注意:
需要区分initializer_list与构造函数的迭代器区间构造:
//构造函数的迭代器区间构造int arr[] = { 1,2,3,4,5,6,7,8,9 };vector<int>v2(arr, arr+sizeof(arr) / sizeof(int));
- initializer_list是在{}中添加任意多个同类型元素,大小是任意的。
- 迭代器区间构造需要指明出区间范围,也就是大小是固定的。
C++11之后,各大容器也都新增了该构造函数接口
要实现该接口也简单,借助std里的initializer_list,再利用范围for将元素添加到容器中即可。如:在list
中添加该接口。
list(initializer_list<T> il){list_init();for (const auto& e : il){push_back(e);}}
声明
c++11提供了多种简化声明的方式,尤其是在使用模板时。
auto
在C++98中auto是一个存储类型的说明符,表明变量是局部自动存储类型,但是局部域中定义局部的变量默认就是自动存储类型,所以auto就没什么价值了。
C++11中废弃auto原来的用法,将其用于实现自动类型推断。这样要求必须进行显示初始化,让编译器将定义对象的类型设置为初始化值的类型
使用
int TestAuto()
{return 10;
}int main()
{int a = 10;auto b = a;auto c = 'a';auto d = TestAuto();cout << typeid(b).name() << endl;cout << typeid(c).name() << endl;cout << typeid(d).name() << endl;//auto e; 无法通过编译,使用auto定义变量时必须对其进行初始化return 0;
}
- typeid是一个运算符,它用于在运行时确定一个对象的类型信息
注意事项:
- 使用auto定义变量时必须对其进行初始化, 在编译阶段编译器需要根据初始化表达式来推导auto的实际类型因此auto并非是一种“类型”的声明,而是一个类型声明时的“占位符”,编译器在编译期会将auto替换为变量实际的类型。
- 当在同一行声明多个变量时,这些变量必须是相同的类型,否则编译器将会报错,因为编译器实际只对第一个类型进行推导,然后用推导出来的类型定义其他变量。
auto a = 1, b = 2;auto c = 3, d = 4.0; // 该行代码会编译失败,因为c和d的初始化表达式类型不同
- auto不能作为函数的参数
//错误示例:参数无法推导
void TestAuto(auto a)
{}
- auto不能直接用来声明数组
//错误示例
void TestAuto()
{int a[] = { 1,2,3 };auto b[] = { 4,5,6 };
}
auto在使用模板时会提供很大的便捷性,如使用map的迭代器
例如获取map的普通迭代器,如果要自己一五一十的写类型,就需要像it2一样,但是有了auto就可以让编译器自己推导了。
int main()
{std::map<string, string> m;auto it1 = m.begin();std::map<string, string>::iterator it2 = m.begin();cout << typeid(it1).name() << endl;cout << typeid(it2).name() << endl;return 0;
}
通过typeid查看迭代器类型,可以看到it1
与it2
类型是一致的
decltype
关键字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的类型是doubledecltype(&x) p; // p的类型是int*cout << typeid(ret).name() << endl;cout << typeid(p).name() << endl;F(1, 'a');//ascii可以转为intreturn 0;
}
decltype的使用需要指定推导的类型比auto麻烦一点。根据场景选择合适的就好。
nullptr
由于C++中NULL被定义成字面量0,这样就可能回带来一些问题,因为0既能指针常量,又能表示整形常量。所以出于清晰和安全的角度考虑,C++11中新增了nullptr,用于表示空指针。
#ifndef NULL
#ifdef __cplusplus
#define NULL 0
#else
#define NULL ((void *)0)
#endif
#endif
所以C++中使用空指针就用nullptr。
范围for
使用:
该语法就是使用auto自动推导所要访问的数据,紧接着编译器会自己去使用该容器所提供的迭代器(访问方式)去访问数据。
- 数组天生就支持随机访问。
int main()
{int arr[] = { 1,2,3,4,5,6,7 };for (auto e : arr){cout << e << " ";}cout << endl;vector<int> v= { 1,2,3,4,5,6,7 };for (auto e : v){cout << e << " ";}cout << endl;set<int> s = { 1,2,3,4,5,6,7 };for (auto e : s){cout << e << " ";}cout << endl;return 0;
}
语法糖范围for用起来感觉很爽,其底层实际上也是编译器调用迭代器实现的。下面就是模拟string实现时,查看汇编发现范围for其实就是对迭代器的一层封装。
容器新增接口
C++11对容器新增了一些接口
emplace
可以看到常用的容器中基本都增加了emplace系列的接口,其用法我们稍后介绍。
vector
list
map
容器的新方法
下列为C++11后容器新增的一些方法。
这一系列的接口需要涉及新的知识点——右值引用和移动语义,都说这些接口能提高效率,那么是怎么做到的呢?让我们下回见真章。
关于C++11后容器的一些变化可以自行查阅——STL的相关调整,凡是有C++11
这样标志的说明就是C++11才更新的新特性。