1. C++ 98
2. C++ 03
3. C++ 11
3.1 nullptr
3.2 auto
3.3 decltype
3.4 初始化列表
3.5 范围for循环
3.6 右值引用
3.7 字符串字面量
3.8 noexcept
3.9 constexpr
3.10 template特性
3.11 Lambda表达式
3.12 函数声明语法
3.13 强类型枚举
3.14 新增基础类型
3.15 多线程Thread
3.16 智能指针
3.17 元组std::tuple
3.18 新增容器
3.19 构造函数
4. C++ 14
5. C++ 17
6. C++ 20
6.1 模块 (Modules)
6.2 协程 (Coroutines)
6.3 范围 (Ranges)
6.4 概念与约束 (Constraints and concepts)
C++是一门以C为基础发展而来的一门面向对象的高级程序设计语言,从1983年由Bjarne Stroustrup教授在贝尔实验室创立开始至今,已有30多个年头。C++从最初的C with class,经历了从C++98、C++ 03、C++ 11、C++ 14、C++17再到C++ 20多次标准化改造,功能得到了极大的丰富,已经演变为一门集面向过程、面向对象、函数式、泛型和元编程等多种编程范式的复杂编程语言。
年份 |
C++ 标准名称 |
非正式名称 |
---|---|---|
1998 |
ISO/IEC 14882:1998 |
C++98 |
2003 |
ISO/IEC 14882:2003 |
C++03 |
2011 |
ISO/IEC 14882:2011 |
C++11 |
2014 |
ISO/IEC 14882:2014 |
C++14 |
2017 |
ISO/IEC 14882:2017 |
C++17 |
2020 |
ISO/IEC 14882:2020 |
C++20 |
1954年,John Backus发明了世界上第一种计算机高级语言Fortran,为之后出现的高级编程语言奠定了基础。
1970年,AT&T的Bell实验室的 Ken Thompson,以BCPL语言为基础,设计出简单且接近硬件的B语言(取BCPL的首字母),并且他用B语言写了第一个Unix操作系统。
1972年,Bell实验室的Dennis Ritchie和Ken Thompson共同发明了C语言,并使用C重写Unix。
1979年,Bjame Stroustrup到了Bell实验室,开始从事将C改良为带类的C(C with Classes)的工作,1983年该语言被正式命名为C++,主要意图是表明C++是C的增强版。
1985年发布了第一个C++版本。第一个版本的C++,因其面向对象的思想使得编程变得简单,并且又保持了C语言的运行效率,在推出的一段时间内,得到了快速的发展,占据了编程语言界的半壁江山。
从1985年到1998年,C++从最初的C with Classes新增了很多其他的特性,比如异常处理、模板、标准模板库(STL)、运行时异常处理(RTTI)与名字空间(Namespace)等。
1998年,C++标准委员会统筹C++的所有特性,发布了第一个C++国际标准C++98。
从1998年到2003年,是C++标准从C++98到C++03的迭代期,期间C++扩增了很多额外的特性,比如以Boost MPL(Boost Metaprogramming Library)与Loki等为代表的模板元编程库的出现,让开发者更加便捷的使用C++在编译期的执行能力,即通过代码编译获得计算结果,学术性的称为模板元编程。到了2003年,C++标准委员会总结最新技术并发布了C++03标准。C++03 是给 C++98 打的补丁,所以现在的人提到 C++98, C++03 往往指的是同一个。
从2003年到2011年,也就是从C++03到C++11,期间C++引入了对象移动、右值引用、lamba表达式(函数式编程)、编译时类型识别(auto)、别名模板以及很多新型关键词(如nullptr、decltype、constexpr)等现代编程语言常具备的能力,让C++与时俱进,开发效率得到了很大的提升。这些新的特性随着C++11标准的发布而被正式确立下来。C++ 11版本也被称为现代C++,而C++ 98/03版本也被称为传统C++。
实际开发中,避免产生“野指针”最有效的方法,就是在定义指针的同时完成初始化操作,即便该指针的指向尚未明确,也要将其初始化为空指针。所谓“野指针”,又称“悬挂指针”,指的是没有明确指向的指针。野指针往往指向的是那些不可用的内存区域,这就意味着像操作普通指针那样使用野指针(例如 &p),极可能导致程序发生异常。C++98/03 标准中,将一个指针初始化为空指针的方式有 2 种:
int *p = 0;
int *p = NULL; //推荐使用
可以看到,我们可以将指针明确指向 0(0x0000 0000)这个内存空间。一方面,明确指针的指向可以避免其成为野指针;另一方面,大多数操作系统都不允许用户对地址为 0 的内存空间执行写操作,若用户在程序中尝试修改其内容,则程序运行会直接报错。相比第一种方式,我们更习惯将指针初始化为 NULL。值得一提的是,NULL 并不是 C++ 的关键字,它是 C++ 为我们事先定义好的一个宏,并且它的值往往就是字面量 0(#define NULL 0)。 C++ 中将 NULL 定义为字面常量 0,虽然能满足大部分场景的需要,但个别情况下,它会导致程序的运行和我们的预期不符
由于 C++ 98 标准使用期间,NULL 已经得到了广泛的应用,出于兼容性的考虑,C++11 标准并没有对 NULL 的宏定义做任何修改。为了修正 C++ 存在的这一 BUG,C++ 标准委员会最终决定另其炉灶,在 C++11 标准中引入一个新关键字,即 nullptr。nullptr 是 nullptr_t 类型的右值常量,专用于初始化空类型指针。nullptr_t 是 C++11 新增加的数据类型,可称为“指针空值类型”。也就是说,nullpter 仅是该类型的一个实例对象(已经定义好,可以直接使用),如果需要我们完全定义出多个同 nullptr 完全一样的实例对象。nullptr 可以被隐式转换成任意的指针类型。举个例子:
int * a1 = nullptr;
char * a2 = nullptr;
double * a3 = nullptr;
在之前的 C++ 版本中,auto 关键字用来指明变量的存储类型,它和 static 关键字是相对的。auto 表示变量是自动存储的,这也是编译器的默认规则,所以写不写都一样,一般我们也不写,这使得 auto 关键字的存在变得非常鸡肋。C++11 赋予 auto 关键字新的含义,使用它来做自动类型推导。也就是说,使用了 auto 关键字以后,编译器会在编译期间自动推导出变量的类型,这样我们就不用手动指明变量的数据类型了。auto 关键字基本的使用语法如下:
auto name = value;
int x = 0;
auto *p1 = &x; //p1 为 int *,auto 推导为 int
auto p2 = &x; //p2 为 int*,auto 推导为 int*
auto &r1 = x; //r1 为 int&,auto 推导为 int
auto r2 = r1; //r2 为 int,auto 推导为 int
name 是变量的名字,value 是变量的初始值。注意:auto 仅仅是一个占位符,在编译器期间它会被真正的类型所替代。或者说,C++ 中的变量必须是有明确类型的,只是这个类型是由编译器自己推导出来的。
auto 的限制:
char url[] = "http://c.biancheng.net/";
auto str[] = url; //arr 为数组,所以不能使用 auto
decltype 是 C++ 11新增的一个关键字,它和 auto 的功能一样,都用来在编译时期进行自动类型推导。既然已经有了 auto 关键字,为什么还需要 decltype 关键字呢?因为 auto 并不适用于所有的自动类型推导场景,在某些特殊情况下 auto 用起来非常不方便,甚至压根无法使用,所以 decltype 关键字也被引入到 C++11 中。auto 和 decltype 关键字都可以自动推导出变量的类型,但它们的用法是有区别的:
auto varname = value;
decltype(exp) varname = value;
其中,varname 表示变量名,value 表示赋给变量的值,exp 表示一个表达式。auto 根据=
右边的初始值 value 推导出变量的类型,而 decltype 根据 exp 表达式推导出变量的类型,跟=
右边的 value 没有关系。另外,auto 要求变量必须初始化,而 decltype 不要求。这很容易理解,auto 是根据变量的初始值来推导出变量类型的,如果不初始化,变量的类型也就无法推导了。decltype 可以写成下面的形式:
decltype(exp) varname;
原则上讲,exp 就是一个普通的表达式,它可以是任意复杂的形式,但是我们必须要保证 exp 的结果是有类型的,不能是 void;例如,当 exp 调用一个返回值类型为 void 的函数时,exp 的结果也是 void 类型,此时就会导致编译错误。C++ decltype 用法举例:
int a = 0;
decltype(a) b = 1; //b 被推导成了 int
decltype(10.8) x = 5.5; //x 被推导成了 double
decltype(x + 100) y; //y 被推导成了 double
(1)一致性初始化
在 C++ 98/03 中的对象初始化方法有很多种,包括小括号,大括号和赋值操作符,这些不同的初始化方法,都有各自的适用范围和作用。最关键的是,这些种类繁多的初始化方法,没有一种可以通用所有情况。为了统一初始化方式,并且让初始化行为具有确定的效果,C++11 引入了“一致性初始化”的概念,意思是对任何初始化动作,你可以使用相同的语法,也就是使用大括号。
int values[]{1, 2, 3};
std::vector v {2, 3, 5, 7, 11, 13, 17};
std::vector cities {"bejing", "shanghai", "guangzhou", "shenzhen"};
(2)初始列
初值列会强迫造成所谓的value initialization,意思是即使某个局部变量属于某个基础类型,也会被初始化为0或者nullptr(如果它是个指针的话):
int i; // i是随机值
int j{}; // j初始化为0
int* p; // p是未定义值
int* q{}; // q初始化为nullptr
C++ 11标准之前(C++ 98/03 标准),如果要用 for 循环语句遍历一个数组或者容器,只能套用如下结构:
for(表达式 1; 表达式 2; 表达式 3){
//循环体
}
C++ 11 标准中,除了可以沿用前面介绍的用法外,还为 for 循环添加了一种全新的语法格式,如下所示:
for (declaration : expression){
//循环体
}
其中,两个参数各自的含义如下:
(1)左值和右值
在 C++ 或者 C 语言中,一个表达式(可以是字面量、变量、对象、函数的返回值等)根据其使用场景不同,分为左值表达式和右值表达式。确切的说 C++ 中左值和右值的概念是从 C 语言继承过来的。左值的英文简写为“lvalue”,右值的英文简写为“rvalue”。很多人认为它们分别是"left value"、"right value" 的缩写,其实不然。lvalue 是“loactor value”的缩写,可意为存储在内存中、有明确存储地址(可寻址)的数据,而 rvalue 译为 "read value",指的是那些可以提供数据值的数据(不一定可以寻址,例如存储于寄存器中的数据)。
通常情况下,判断某个表达式是左值还是右值,最常用的有以下 2 种方法:
(2)右值引用
C++98/03 标准中就有引用,使用 "&" 表示。但此种引用方式有一个缺陷,即正常情况下只能操作 C++ 中的左值,无法对右值添加引用。举个例子:
int num = 10;
int &b = num; //正确
int &c = 10; //错误
如上所示,编译器允许我们为 num 左值建立一个引用,但不可以为 10 这个右值建立引用。因此,C++98/03 标准中的引用又称为左值引用。为此,C++11 标准新引入了另一种引用方式,称为右值引用,用 "&&" 表示。和声明左值引用一样,右值引用也必须立即进行初始化操作,且只能使用右值进行初始化,比如:
int num = 10;
int && a = num; // error,右值引用不能初始化为左值
int && a = 10;
(3)移动构造函数
在 C++ 11 标准之前(C++ 98/03 标准中),如果想用其它对象初始化一个同类的新对象,只能借助类中的复制(拷贝)构造函数。拷贝构造函数的实现原理很简单,就是为新对象复制一份和其它对象一模一样的数据。
#include
using namespace std;
class demo{
public:
demo():num(new int(0))
{
cout<<"construct!"<
可以看到,程序中定义了一个可返回 demo 对象的 get_demo() 函数,用于在 main() 主函数中初始化 a 对象,其整个初始化的流程包含以下几个阶段:
1)执行 get_demo() 函数内部的 demo() 语句,即调用 demo 类的默认构造函数生成一个匿名对象;
2)执行 return demo() 语句,会调用拷贝构造函数复制一份之前生成的匿名对象,并将其作为 get_demo() 函数的返回值(函数体执行完毕之前,匿名对象会被析构销毁);
3)执行 a = get_demo() 语句,再调用一次拷贝构造函数,将之前拷贝得到的临时对象复制给 a(此行代码执行完毕,get_demo() 函数返回的对象会被析构);
4)程序执行结束前,会自行调用 demo 类的析构函数销毁 a。
完整的输出结果如下:
construct! <-- 执行 demo()
copy construct! <-- 执行 return demo()
class destruct! <-- 销毁 demo() 产生的匿名对象
copy construct! <-- 执行 a = get_demo()
class destruct! <-- 销毁 get_demo() 返回的临时对象
class destruct! <-- 销毁 a
如上所示,利用拷贝构造函数实现对 a 对象的初始化,底层实际上进行了 2 次拷贝(而且是深拷贝)操作。当然,对于仅申请少量堆空间的临时对象来说,深拷贝的执行效率依旧可以接受,但如果临时对象中的指针成员申请了大量的堆空间,那么 2 次深拷贝操作势必会影响 a 对象初始化的执行效率。
所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。以前面程序中的 demo 类为例,该类的成员都包含一个整形的指针成员,其默认指向的是容纳一个整形变量的堆空间。当使用 get_demo() 函数返回的临时对象初始化 a 时,我们只需要将临时对象的 num 指针直接浅拷贝给 a.num,然后修改该临时对象中 num 指针的指向(通常另其指向 NULL),这样就完成了 a.num 的初始化。
#include
using namespace std;
class demo{
public:
demo():num(new int(0))
{
cout<<"construct!"<
可以看到,在之前 demo 类的基础上,我们又手动为其添加了一个构造函数。和其它构造函数不同,此构造函数使用右值引用形式的参数,又称为移动构造函数。并且在此构造函数中,num 指针变量采用的是浅拷贝的复制方式,同时在函数内部重置了 d.num,有效避免了“同一块对空间被释放多次”情况的发生。 命令执行此程序,输出结果为:
construct!
move construct!
class destruct!
move construct!
class destruct!
class destruct!
通过执行结果我们不难得知,当为 demo 类添加移动构造函数之后,使用临时对象初始化 a 对象过程中产生的 2 次拷贝操作,都转由移动构造函数完成。我们知道,非 const 右值引用只能操作右值,程序执行结果中产生的临时对象(例如函数返回值、lambda 表达式等)既无名称也无法获取其存储地址,所以属于右值。当类中同时包含拷贝构造函数和移动构造函数时,如果使用临时对象初始化当前类的对象,编译器会优先调用移动构造函数来完成此操作。只有当类中没有合适的移动构造函数时,编译器才会退而求其次,调用拷贝构造函数。
(4)move语义
C++11 标准中借助右值引用可以为指定类添加移动构造函数,这样当使用该类的右值对象(可以理解为临时对象)初始化同类对象时,编译器会优先选择移动构造函数。注意,移动构造函数的调用时机是:用同类的右值对象初始化新对象。那么,用当前类的左值对象(有名称,能获取其存储地址的实例对象)初始化同类对象时,是否就无法调用移动构造函数了呢?当然不是,C++11 标准中已经给出了解决方案,即调用 move() 函数。move 本意为 "移动",但该函数并不能移动任何数据,它的功能很简单,就是将某个左值强制转化为右值。move() 函数的用法也很简单,其语法格式如下:
move( arg )
(5)完美转发
完美转发指的是函数模板可以将自己的参数“完美”地转发给内部调用的其它函数。所谓完美,即不仅能准确地转发参数的值,还能保证被转发参数的左、右值属性不变。举个例子:
template
void function(T t) {
otherdef(t);
}
如上所示,function() 函数模板中调用了 otherdef() 函数。在此基础上,完美转发指的是:如果 function() 函数接收到的参数 t 为左值,那么该函数传递给 otherdef() 的参数 t 也是左值;反之如果 function() 函数接收到的参数 t 为右值,那么传递给 otherdef() 函数的参数 t 也必须为右值。显然,function() 函数模板并没有实现完美转发。一方面,参数 t 为非引用类型,这意味着在调用 function() 函数时,实参将值传递给形参的过程就需要额外进行一次拷贝操作;另一方面,无论调用 function() 函数模板时传递给参数 t 的是左值还是右值,对于函数内部的参数 t 来说,它有自己的名称,也可以获取它的存储地址,因此它永远都是左值,也就是说,传递给 otherdef() 函数的参数 t 永远都是左值。总之,无论从那个角度看,function() 函数的定义都不“完美”。
C++11 标准中规定,通常情况下右值引用形式的参数只能接收右值,不能接收左值。但对于函数模板中使用右值引用语法定义的参数来说,它不再遵守这一规定,既可以接收右值,也可以接收左值(此时的右值引用又被称为“万能引用”)。
#include
using namespace std;
//重载被调用函数,查看完美转发的效果
void otherdef(int & t)
{
cout << "lvalue\n";
}
void otherdef(const int & t)
{
cout << "rvalue\n";
}
//实现完美转发的函数模板
template
void function(T&& t)
{
otherdef(forward(t));
}
int main()
{
function(5);
int x = 1;
function(x);
return 0;
}
程序执行结果为:
rvalue
lvalue
想象一下如下场景,我们要打印如下内容:
this is "test"
我们不得不用如下的代码,对" 进行转义:
std::string normal_str = "this is \"test\"";
C++11引入了字符串字面量的概念。对于前面的例子,我们就可以通过如下的方式实现我们的目的:
std::string normal_str = R"(this is "test")";
C++11支持用户自定义字面量,这里就不再赘述,感兴趣的自行百度。
C++11新标准引入的noexcept运算符,可以用于指定某个函数不抛出异常。预先知道函数不会抛出异常有助于简化调用该函数的代码,而且编译器确认函数不会抛出异常,它就能执行某些特殊的优化操作。C++ 98/03版本中常用throw()表示,在C++ 11中已经被noexcept代替。
(1)noexcept异常说明
C++ 98/03版本:
void func(int x) throw(); //不抛出异常
C++ 11版本:
void func(int x) noexcept; //不抛出异常
void func1(int x); //抛出异常
对于程序违反了异常说明,编译器在编译阶段不会检查报错,但是在程序执行过程中,程序会调用terminate以确保遵守不在运行时抛出异常的承诺。
void func() noexcept
{
throw exception();
}
(2)noexcept运算符
noexcept运算符是一个一元运算符,它的返回值是一个bool类型的右值常量表达式,用于表示给定的表达式是否会抛出异常。
noexcept(f()); //如果f()不抛出异常则结果为true,否则为false
noexcept(e); //当e调用的所有函数都做了步抛出说明且e本身不含有throw语句时,表达式为true,否则返回false
常量表达式,指的就是由多个(≥1)常量组成的表达式。换句话说,如果表达式中的成员都是常量,那么该表达式就是一个常量表达式。这也意味着,常量表达式一旦确定,其值将无法修改。我们知道,C++ 程序的执行过程大致要经历编译、链接、运行这 3 个阶段。值得一提的是,常量表达式和非常量表达式的计算时机不同,非常量表达式只能在程序运行阶段计算出结果;而常量表达式的计算往往发生在程序的编译阶段,这可以极大提高程序的执行效率,因为表达式只需要在编译阶段计算一次,节省了每次程序运行时都需要计算一次的时间。对于用 C++ 编写的程序,性能往往是永恒的追求。那么在实际开发中,如何才能判定一个表达式是否为常量表达式,进而获得在编译阶段即可执行的“特权”呢?除了人为判定外,C++11 标准还提供有 constexpr 关键字。
constexpr 关键字的功能是使指定的常量表达式获得在程序编译阶段计算出结果的能力,而不必等到程序运行阶段。C++ 11 标准中,constexpr 可用于修饰普通变量、函数(包括模板函数)以及类的构造函数。 C++11 标准中,定义变量时可以用 constexpr 修饰,从而使该变量获得在编译阶段即可计算出结果的能力。
#include
using namespace std;
int main()
{
constexpr int num = 1 + 2 + 3;
int url[num] = {1,2,3,4,5,6};
couts<< url[1] << endl;
return 0;
}
类模板:通用的类描述(使用泛型来定义类),进行实例化时,其中的泛型再用具体的类型替换。
函数模板:通用的函数描述(使用泛型来定义函数),进行实例化时,其中的泛型再用具体的类型替换。
(1)C++98标准中两者的区别
函数模板和类模板在C++98标准中一起被引入,两者区别主要在于:在类模板声明时,标准允许其有默认模板参数。而函数模板却不支持。默认模板参数的作用如同函数的默认形参。不过在C++11中,这一限制已经被解除了,如下例所示:
void DefParm(int m = 3) {} // c++98编译通过,c++11编译通过
template
class DefClass {}; // c++98编译通过,c++11编译通过
template
void DefTempParm() {}; // c++98编译失败,c++11编译通过
可以看到,DefTempParm函数模板拥有一个默认模板参数(类型int)。使用仅支持C++98的编译器编译,DefTempParm的编译会失败,而支持C++11的编译器则无问题。
(2)C++11标准中两者的区别
尽管C++11支持了函数模板的默认模板参数,不过在语法上,两者还是存在区别:类模板在为多个默认模板参数声明指定默认值时,必须遵照“从右往左”的规则进行指定。而这个规则对函数模板来说并不是必须的。示例如下:
template
class DefClass1 {};
template
class DefClass2 {}; // ERROR: 无法通过编译:因为模板参数的默认值没有遵循“由右往左”的规则
template
class DefClass3 {};
template
class DefClass4 {}; // ERROR: 无法通过编译:因为模板参数的默认值没有遵循“由右往左”的规则
template
void DefFunc1(T1 a, T2 b) {}; // OK 函数模板不用遵循“由右往左”的规则
template
void DefFunc2(T a) {}; // OK 函数模板不用遵循“由右往左”的规则
可以看到,不按照从右往左定义默认类模板参数的模板类DefClass2和DefClass4都无法通过编译。而对于函数模板来说,默认模板参数的位置则比较随意。DefFunc1和DefFunc2都为第一个模板参数定义了默认参数,而第二个模板参数的默认值并没有定义,C++11编译器却认为没有问题。
lambda 源自希腊字母表中第 11 位的 λ,在计算机科学领域,它则是被用来表示一种匿名函数。所谓匿名函数,简单地理解就是没有名称的函数,又常被称为 lambda 函数或者 lambda 表达式。
(1)匿名函数定义
定义一个 lambda 匿名函数很简单,可以套用如下的语法格式:
[外部变量访问方式说明符] (参数) mutable noexcept/throw() -> 返回值类型
{
函数体;
};
所谓外部变量,指的是和当前 lambda 表达式位于同一作用域内的所有局部变量。
(参数):和普通函数的定义一样,lambda 匿名函数也可以接收外部传递的多个参数。和普通函数不同的是,如果不需要传递参数,可以连同 () 小括号一起省略;
mutable:此关键字可以省略,如果使用则之前的 () 小括号将不能省略(参数个数可以为 0)。默认情况下,对于以值传递方式引入的外部变量,不允许在 lambda 表达式内部修改它们的值(可以理解为这部分变量都是 const 常量)。而如果想修改它们,就必须使用 mutable 关键字。
noexcept/throw():可以省略,如果使用,在之前的 () 小括号将不能省略(参数个数可以为 0)。默认情况下,lambda 函数的函数体中可以抛出任何类型的异常。而标注 noexcept 关键字,则表示函数体内不会抛出任何异常;使用 throw() 可以指定 lambda 函数内部可以抛出的异常类型。值得一提的是,如果 lambda 函数标有 noexcept 而函数体内抛出了异常,又或者使用 throw() 限定了异常类型而函数体内抛出了非指定类型的异常,这些异常无法使用 try-catch 捕获,会导致程序执行失败。
-> 返回值类型:指明 lambda 匿名函数的返回值类型。值得一提的是,如果 lambda 函数体内只有一个 return 语句,或者该函数返回 void,则编译器可以自行推断出返回值类型,此情况下可以直接省略-> 返回值类型
。
函数体:和普通函数一样,lambda 匿名函数包含的内部代码都放置在函数体中。该函数体内除了可以使用指定传递进来的参数之外,还可以使用指定的外部变量以及全局范围内的所有全局变量。需要注意的是,外部变量会受到以值传递还是以引用传递方式引入的影响,而全局变量则不会。换句话说,在 lambda 表达式内可以使用任意一个全局变量,必要时还可以直接修改它们的值。
(2)匿名函数中的[外部变量
对于 lambda 匿名函数的使用,比较让人感到困惑的就是 [外部变量] 的使用。其实很简单,无非下表所示的这几种编写格式。
外部变量格式 | 功能 |
---|---|
[] | 空方括号表示当前 lambda 匿名函数中不导入任何外部变量。 |
[=] | 只有一个 = 等号,表示以值传递的方式导入所有外部变量; |
[&] | 只有一个 & 符号,表示以引用传递的方式导入所有外部变量; |
[val1,val2,...] | 表示以值传递的方式导入 val1、val2 等指定的外部变量,同时多个变量之间没有先后次序; |
[&val1,&val2,...] | 表示以引用传递的方式导入 val1、val2等指定的外部变量,多个变量之间没有前后次序; |
[val,&val2,...] | 以上 2 种方式还可以混合使用,变量之间没有前后次序。 |
[=,&val1,...] | 表示除 val1 以引用传递的方式导入外,其它外部变量都以值传递的方式导入。 |
[this] | 表示以值传递的方式导入当前的 this 指针。 |
注意:单个外部变量不允许以相同的传递方式导入多次。例如 [=,val1] 中,val1 先后被以值传递的方式导入了 2 次,这是非法的。
(3)使用实例
#include
#include
using namespace std;
int main()
{
int num[4] = {4, 2, 3, 1};
//对 a 数组中的元素进行排序
sort(num, num + 4, [=](int x, int y) -> bool{ return x < y; } );
for(int n : num)
{
cout << n << " ";
}
return 0;
}
程序执行结果为:1 2 3 4。调用 sort() 函数实现了对 num 数组中元素的升序排序,其中就用到了 lambda 匿名函数。而如果使用普通函数,需以如下代码实现:
#include
#include
using namespace std;
//自定义的升序排序规则
bool sort_up(int x, int y)
{
return x < y;
}
int main()
{
int num[4] = {4, 2, 3, 1};
//对 a 数组中的元素进行排序
sort(num, num+4, sort_up);
for (int n : num)
{
cout << n << " ";
}
return 0;
}
此程序中 sort_up() 函数的功能和上一个程序中的 lambda 匿名函数完全相同。显然在类似的场景中,使用 lambda 匿名函数更有优势。除此之外,虽然 lambda 匿名函数没有函数名称,但我们仍可以为其手动设置一个名称,比如:
#include
using namespace std;
int main()
{
//display 即为 lambda 匿名函数的函数名
auto display = [](int a,int b) -> void{cout << a << " " << b;};
//调用 lambda 函数
display(10,20);
return 0;
}
程序执行结果为:10 20。可以看到,程序中使用 auto 关键字为 lambda 匿名函数设定了一个函数名,由此我们即可在作用域内调用该函数。
在C++11中,callable object 包括传统C函数,C++成员函数,函数对象(实现了()运算符的类的实例),lambda表达式(特殊函数对象)共4种。程序设计,特别是程序库设计时,经常需要涉及到回调,如果针对每种不同的callable object单独进行声明类型,代码将会非常散乱,也不灵活。如下示例:
// 传统C函数
int c_function(int a, int b)
{
return a + b;
}
// 函数对象
class Functor
{
public:
int operator()(int a, int b)
{
return a + b;
}
};
int main(int argc, char** argv)
{
int(*f)(int, int); // 声明函数类型,赋值只能是函数指针
f = c_function;
cout << f(3, 4) << endl;
Functor ff = Functor(); // 声明函数对象类型,赋值只能是函数对象
cout << ff(3, 4) << endl;
}
幸运的是,C++标准库的头文件里定义了std::function<>模板,此模板可以容纳所有类型的callable object.示例代码如下:
#include
#include
using namespace std;
// 传统C函数
int c_function(int a, int b)
{
return a + b;
}
// 函数对象
class Functor
{
public:
int operator()(int a, int b)
{
return a + b;
}
};
int main(int argc, char** argv)
{
// 万能可调用对象类型
std::function callableObject;
// 可以赋值为传统C函数指针
callableObject = c_function;
cout << callableObject(3, 4) << endl;
// 可以赋值为函数对象
Functor functor;
callableObject = functor;
cout << callableObject(3, 4) << endl;
// 可以赋值为lambda表达式(特殊函数对象)
callableObject = [](int a, int b){
return a + b;
};
cout << callableObject(3, 4) << endl;
}
(1)传统枚举类型的缺陷
枚举类型是C/C++中用户自定义的构造类型,它是由用户定义的若干枚举常量的集合。枚举值对应整型数值,默认从0开始。比如定义一个描述性别的枚举类型。
enum Gender{Male,Female};
其中枚举值Male被编译器默认赋值为0,Female赋值为1。传统枚举类型在设计上会存在以下几个问题。
enum Fruits{Apple, Tomato, Orange};
enum Vegetables{Cucumber, Tomato, Pepper}; //编译报Tomato重定义错误
其中水果和蔬菜两个枚举类型中包含同名的Tomato枚举常量会导致编译错误。因为enum则是非强作用域类型,枚举常量可以直接访问,这种访问方式与C++中具名的namespace、class/struct以及union必须通过"名字::成员名"的访问方式大相径庭。
(2)强类型枚举
非强作用域类型,允许隐式转换为整型,枚举常量占用存储空间以及符号性的不确定,都是枚举类缺点。针对这些缺点,C++11引入了一种新的枚举类型——强类型枚举(strong-typed enum)。强类型枚举使用enum class语法来声明:
enum class Enumeration{VAL1, VAL2, VAL3 = 100, VAL4};
强类型枚举具有如下几个优点:
enum class Type:char{Low,Middle,High};
注意:
enum class {General, Light, Medium, Heavy} weapon;
int main()
{
weapon = Medium; //编译出错
bool b = weapon == weapon::Medium; //编译出错
return 0;
}
(1)C++ 03中的基本算术类型包括9种,列举如下:
(2)C++ 11中的基本算术类型包括12种,C++11的基本类型完全包含上述9种类型,除此之外还包括:
C++11标准中的char16_t和char32_t用来处理Unicode字符,char16_t可以作为UTF-16的一个处理单元,char32_t可以作为UTF-32编码的一个处理单元。
C++11新标准中引入五个头文件支持多线程编程,他们分别是:
(1)
(2)
(3)
(4)
(5)
所谓智能指针,可以从字面上理解为“智能”的指针。具体来讲,智能指针和普通指针的用法是相似的,不同之处在于,智能指针可以在适当时机自动释放分配的内存。也就是说,使用智能指针可以很好地避免“忘记释放内存而导致内存泄漏”问题出现。由此可见,C++ 也逐渐开始支持垃圾回收机制了,尽管目前支持程度还有限。C++98/03 标准中,支持使用 auto_ptr 智能指针来实现堆内存的自动回收;C++11 新标准在废弃 auto_ptr 的同时,增添了 unique_ptr、shared_ptr 以及 weak_ptr 这 3 个智能指针来实现堆内存的自动回收。
(1)shared_ptr
和 unique_ptr、weak_ptr 不同之处在于,多个 shared_ptr 智能指针可以共同使用同一块堆内存。并且,由于该类型智能指针在实现上采用的是引用计数机制,即便有一个 shared_ptr 指针放弃了堆内存的“使用权”(引用计数减 1),也不会影响其他指向同一堆内存的 shared_ptr 指针(只有引用计数为 0 时,堆内存才会被自动释放)。shared_ptr
std::shared_ptr p1; //不传入任何实参
std::shared_ptr p2(nullptr); //传入空指针 nullptr
std::shared_ptr p3(new int(10)); // 在构建 shared_ptr 智能指针,也可以明确其指向。
std::shared_ptr p3 = std::make_shared(10); // C++11 标准中还提供了 std::make_shared 模板函数
// 调用拷贝构造函数
std::shared_ptr p4(p3);
std::shared_ptr p4 = p3;
// 调用移动构造函数
std::shared_ptr p5(std::move(p4));
std::shared_ptr p5 = std::move(p4);
同一普通指针不能同时为多个 shared_ptr 对象赋值,否则会导致程序发生异常。例如:
int* ptr = new int;
std::shared_ptr p1(ptr);
std::shared_ptr p2(ptr); // 错误
(2)unique_ptr
作为智能指针的一种,unique_ptr 指针自然也具备“在适当时机自动释放堆内存空间”的能力。和 shared_ptr 指针最大的不同之处在于,unique_ptr 指针指向的堆内存无法同其它 unique_ptr 共享,也就是说,每个 unique_ptr 指针都独自拥有对其所指堆内存空间的所有权。这也就意味着,每个 unique_ptr 指针指向的堆内存空间的引用计数,都只能为 1,一旦该 unique_ptr 指针放弃对所指堆内存空间的所有权,则该空间会被立即释放回收。
std::unique_ptr p1(); // 创建出空的 unique_ptr 指针:
std::unique_ptr p2(nullptr); // 创建出空的 unique_ptr 指针:
std::unique_ptr p3(new int); // 创建出了一个 p3 智能指针,其指向的是可容纳 1 个整数的堆存储空间。
// 基于 unique_ptr 类型指针不共享各自拥有的堆内存,因此 C++11 标准中的 unique_ptr 模板类没有提供拷贝构造函数,只提供了移动构造函数
std::unique_ptr p4(new int);
std::unique_ptr p5(p4);// 错误,堆内存不共享
std::unique_ptr p5(std::move(p4)); // 正确,调用移动构造函数
(3)weak_ptr
需要注意的是,C++11标准虽然将 weak_ptr 定位为智能指针的一种,但该类型指针通常不单独使用(没有实际用处),只能和 shared_ptr 类型指针搭配使用。甚至于,我们可以将 weak_ptr 类型指针视为 shared_ptr 指针的一种辅助工具,借助 weak_ptr 类型指针, 我们可以获取 shared_ptr 指针的一些状态信息,比如有多少指向相同的 shared_ptr 指针、shared_ptr 指针指向的堆内存是否已经被释放等等。
需要注意的是,当 weak_ptr 类型指针的指向和某一 shared_ptr 指针相同时,weak_ptr 指针并不会使所指堆内存的引用计数加 1;同样,当 weak_ptr 指针被释放时,之前所指堆内存的引用计数也不会因此而减 1。也就是说,weak_ptr 类型指针并不会影响所指堆内存空间的引用计数。除此之外,weak_ptr
std::weak_ptr wp1; // 可以创建一个空 weak_ptr 指针
std::weak_ptr wp2 (wp1); // 凭借已有的 weak_ptr 指针,可以创建一个新的 weak_ptr 指针
// 利用已有的 shared_ptr 指针为其初始化
std::shared_ptr sp (new int);
std::weak_ptr wp3 (sp);
std::tuple是类似pair的模板。每个pair的成员类型都不相同,但每个pair都恰好有两个成员。std::tuple:成员类型不同,有任意数量的成员。使用方式如下:
(1)tuple的创建
std::tuple first;
std::string str_second_1("_1");
std::string str_second_2("_2");
// 指定了元素类型为引用 和 std::string, 下面两种方式都是可以的,只不过第二个参数不同而已
std::tuple second_1(str_second_1, std::string("_2"));
std::tuple second_2(str_second_1, str_second_2);
int i_fourth_1 = 4;
int i_fourth_2 = 44;
// 下面的两种方式都可以
std::tuple forth_1 = std::make_tuple(i_fourth_1, i_fourth_2);
auto forth_2 = std::make_tuple(i_fourth_1, i_fourth_2);
(2)tuple的遍历
#include // std::cout
#include // std::tuple, std::tuple_size
int main ()
{
std::tuple mytuple (10, 'a', 3.14);
// tuple的大小
std::cout << "mytuple has ";
std::cout << std::tuple_size::value;
std::cout << " elements." << '\n';
// 获取tuple的元素类型
std::tuple_element<0, decltype(mytuple)>::type ages; // ages就为int类型
// 获取元素
std::cout << "the elements is: ";
std::cout << std::get<0>(mytuple) << " ";
std::cout << std::get<1>(mytuple) << " ";
std::cout << std::get<2>(mytuple) << " ";
std::cout << '\n';
return 0;
}
//输出结果:
mytuple has 3 elements.
the elements is: 10 a 3.14
(1)std::array
array就是数组,为什么会出现这样一个容器呢,不是有vector和传统数组吗?那你有没有某些时候抱怨过vector速度太慢。array 保存在栈内存中,相比堆内存中的vector,我们就能够灵活的访问元素,获得更高的性能;同时真是由于其堆内存存储的特性,有些时候我们还需要自己负责释放这些资源。array就是介于传统数组和vector两者之间的容器,封装了一些函数,比传统数组方便,但是又没必要使用vector;array 会在编译时创建一个固定大小的数组array 不能够被隐式的转换成指针,定义时需要指定类型和大小。支持快速随机访问。不能添加或删除元素。
(2)std::forward_list
forward_list 是一个列表容器,使用方法和 list 基本类似。但forward_list 使用单向链表进行实现,提供了 O(1) 复杂度的元素插入,不支持快速随机访问,也是标准库容器中唯一一个不提供 size() 方法的容器。当不需要双向迭代时,具有比 list 更高的空间利用率。
(3)std::unordered_map/std::unordered_multimap、std::unordered_set/std::unordered_multiset
加了个unordered前缀,也是把Hash正式带入了STL中,内部没有红黑树,无法自动排序,只是用Hash建立了映射,其他用法相同,当题目只需要映射而不要排序时候,用这个会快很多。
(1)继承构造函数
子类为完成基类初始化,在C++11之前,需要在初始化列表调用基类的构造函数,从而完成构造函数的传递。如果基类拥有多个构造函数,那么子类也需要实现多个与基类构造函数对应的构造函数。
class Base {
public:
Base(int v): _value(v), _c(‘0’){}
Base(char c): _value(0), _c(c){}
private:
int _value;
char _c;
};
class Derived: public Base {
public:
// 初始化基类需要透传参数至基类的各个构造函数,非常麻烦
Derived(int v) :Base(v) {}
Derived(char c) :Base(c) {}
// 假设派生类只是添加了一个普通的函数
void display() {
// dosomething
}
};
书写多个派生类构造函数只为传递参数完成基类初始化,这种方式无疑给开发人员带来麻烦,降低了编码效率。从 C++11 开始,推出了继承构造函数(Inheriting Constructor),使用 using 来声明继承基类的构造函数,我们可以这样书写。
class Base {
public:
Base(int v) :_value(v), _c('0'){}
Base(char c): _value(0), _c(c){}
private:
int _value;
char _c;
};
class Derived :public Base {
public:
// 使用继承构造函数
using Base::Base;
// 假设派生类只是添加了一个普通的函数
void display() {
//dosomething
}
};
注意事项:
(2)委托构造函数
在实际的开发中,为了满足不同用户的不同需求,我们的一个类可能会有很多构造函数的重载版本,特别的,这些重载版本的工作内容有的比较复杂,有的比较简单,并且他们之间会有一些交叉重复的工作,即一些代码、或者数据的初始化在每个构造函数中都会去写一遍,这样显得代码特别的臃肿丑陋,委托构造函数应运而生。使用委托构造函数实例:允许在一个构造函数的初始化列表中调用另外一个构造函数,委托另外一个构造函数进行一些初始化工作。
class foo {
public:
foo(int data) :ma(data) {}
foo(int data, int a) {
ma = data + a;
cout << "foo(int,int)" << endl;
}
foo(int data, int a, int b) :foo(data, a) {
ma = data + a;
ma += b;
cout << "foo(int,int,int)" << endl;
}
foo(int data, int a, int b, int c) :foo(data, a, b) {
ma = data + a;
ma += b;
ma += c;
cout << "foo(int,int,int,int)" << endl;
}
foo(int data, int a, int b, int c, int d) :foo(data, a, b, c) {
ma = data + a;
ma += b;
ma += c;
ma += d;
cout << "foo(int,int,int,int,int)" << endl;
}
void show() {
cout << "ma = " << ma << endl;
cout << "---------------------------" << endl;
}
int ma;
};
class foo {
public:
foo(int data):ma(data){}
foo(int data, int a) {
ma = data + a;
cout << "foo(int,int)" << endl;
}
foo(int data, int a, int b) :foo(data, a) {
ma += b;
cout << "foo(int,int,int)" << endl;
}
foo(int data, int a, int b, int c) :foo(data, a, b) {
ma += c;
cout << "foo(int,int,int,int)" << endl;
}
foo(int data, int a, int b, int c, int d) :foo(data, a, b, c) {
ma += d;
cout << "foo(int,int,int,int,int)" << endl;
}
void show() {
cout << "ma = " << ma << endl;
cout << "---------------------------" << endl;
}
int ma;
};
整个过程是一个递归的过程,我们可以通过打印知道整个过程的执行顺序,需要注意的是:
1)切不可构成环装的调用过程,比如将foo(int data, int a, int b) :foo(data, a) 改为foo(int data, int a, int b) foo(data, a, b, 1)类似这样的语句,否则程序会抛出异常‘;
2)一旦初始化列表中进行了委托构造,即调用了其他构造函数,我们就不能再在初始化列表中初始化成员变量了;
C++14引入了二进制文字常量、将类型推导从Lambda函数扩展到所有函数、变量模板以及数字分位符等。C++14 是对 C++11的重要补充和优化,是C++发展历程中的一个小型版本,虽然新增的内容较少,但是仍然为用户“带来了极大的方便”,为实现使C++“对新手更为友好”这一目标作出努力。
到了2017年,C++迎来了C++17标准。此次对C++的改进和扩增,让C++变得更加容易接受和便于使用了。C++17引入了许多新的特性,比如类模板参数推导、UTF-8文字常量、fold表达式、新类型以及新的库函数等。
C++20 的 Big Four(四大新特性:概念、范围、协程和模块)以及核心语言(包括一些新的运算符和指示符)。
module
的引入至少有如下几个优点:
C++分离编译带来的一个问题就是编译会非常慢,因为C++ 的编译器在处理一个源代码文件的时候,首先要做的就是用相应的头文件的内容替换 #include 预编译指令。这就存在一个问题,对每一个源代码文件编译器都要重复一遍内容替换,这会占用大量的处理器时间。而引入module以后就不存在这个问题了,只需要import一下就可以在所有的源代码文件中使用,没有头文件的替换动作,使得编译时间可以大大减小。示例如下:
// helloworld.cpp
export module helloworld; // module declaration
import ; // import declaration
export void hello() // export declaration
{
std::cout << "Hello world!\n";
}
// main.cpp
import helloworld; // import declaration
int main()
{
hello();
}
协程,就是能够暂停执行然后在接下来的某个时间点恢复执行的函数,C++中的协程是无栈的(stack less)。使用协程可以方便的编写异步代码(和编写同步代码类似)。主要涉及三个关键字:
参考libo源码阅读的博客,可以更好的理解协程。
range提供给我们一种函数式处理容器元素的方法,可以使得我们的代码简洁不少。以往此类代码的一般方法是在算法库函数上指定一个迭代器范围,搭配Lambda使用,但是在遇到transform这样的需求时我们不得不手动拷贝一个容器,在其之上做一些其他改动,range的引入使得我们可以非常简答的随意组合(用逻辑运算符就可以),并直接在容器上操作,其实可以看做一个简化版的MapReduce。
void cpp_11()
{
std::vector v{1, 2, 3, 4, 5};
std::vector even;
std::copy_if(v.begin(), v.end(), std::back_inserter(even),
[](int i) { return i % 2 == 0; });
std::vector results;
std::transform(even.begin(), even.end(),
std::back_inserter(results),
[](int i) { return i * 2; });
for (int n : results) std::cout << n << ' ';
putchar('\n');
}
void cpp_20()
{
std::vector v{1, 2, 3, 4, 5};
for(int i : v | ranges::views::filter([](int i) { return i % 2 == 0; })
| ranges::views::transform([](int i) { return i * 2; })){
cout << i << " ";
}
putchar('\n');
}
概念和约束最大的作用就是可以在模板参数类型出现问题的时候不会一次报出几千行错误。我们可以使用concept来限制模板参数的类型。如果某处实例化的类型与concept相悖的话就会报错。当然含有concept的模板声明更像是一个特殊的实例化。下面的代码片段展示了一个简单概念 Integral 的定义和使用方式:
template
concept bool Integral
{
returnstd::is_integral::value;
}
Integral auto gcd(Integral auto a, Integral auto b)
{
if (b == 0)
{
return a;
}
else
{
return gcd(b, a % b);
}
}
Integral 这个概念需要 std::is_integral
template
concept bool Integral
{
returnstd::is_integral::value;
}
template
requires Integral
T gcd(T a, T b)
{
if (b == 0)
{
return a;
}
else
{
return gcd(b, a % b);
}
}
参考:http://c.biancheng.net/cplus/11/