C++11新特性
什么是C++11
C++11标准为C++编程语言的第三个官方标准,正式名叫ISO/IEC 14882:2011 - Information technology – Programming languages – C++。在正式标准发布前,原名C++0x。它将取代C++标准第二版ISO/IEC 14882:2003 - Programming languages – C++成为C++语言新标准。
C++11是对目前C++语言的扩展和修正, C++11不仅包含核心语言的新机能,而且扩展了C++的标准程序库(STL) ,并入了大部分的C++ Technical Report 1(TR1) 程序库(数学的特殊函数除外)。
C++11包括大量的新特性:包括lambda表达式,类型推导关键字auto、 decltype,和模板的大量改进。
类型推导
auto
auto的自动类型推导,用于从初始化表达式中推断出变量的数据类型。从这个意义上讲,auto并非一种“类型”声明,而是一个类型声明时的“占位符”,编译器在编译时期会将auto替换为变量实际的类型。
注意:
- 定义变量时必须初始化;
- 不支持函数形参(C++11);
- 不能作为自定义类型的成员变量;
- 不能作为模板实例化时的参数;
- 不能出现在顶级数组类型。
1 |
|
decltype
decltype
可以从一个变量或表达式中得到其类型。
追踪返回类型
返回类型后置:在函数名和参数列表后面指定返回类型。
1 |
|
易用性的改进
初始化
- 类内成员初始化
- 初始化列表
- 使用列表初始化可以防止类型收窄
基于范围的for循环
使用基于范围的for循环,其for循环迭代的范围必须是可确定的。
1 |
|
静态断言
C/C++提供了调试工具assert
,这是一个宏,用于在运行阶段对断言进行检查,如果条件为真,执行程序,否则调用abort()
。
C++ 11新增了关键字static_assert
,可用于在编译阶段对断言进行测试。
静态断言的好处:
更早的报告错误,我们知道构建是早于运行的,更早的错误报告意味着开发成本的降低
减少运行时开销,静态断言是编译期检测的,减少了运行时开销
noexcept
C++11 使用noexcept
替代throw()
代表此函数不能抛出异常,如果抛出,就会异常。
nullptr
nullptr
是为了解决原来C++中NULL
的二义性问题而引进的一种新的类型,因为NULL
实际上代表的是0。
强类型枚举
C++ 11引入了一种新的枚举类型,即“枚举类”,又称“强类型枚举”。声明请类型枚举非常简单,只需要在enum后加上使用class或struct。如:
1 |
|
“传统”的C++枚举类型有一些缺点:它会在一个代码区间中抛出枚举类型成员(如果在相同的代码域中的两个枚举类型具有相同名字的枚举成员,这会导致命名冲突),它们会被隐式转换为整型,并且不可以指定枚举的底层数据类型。
1 |
|
在C++11中,强类型枚举解决了这些问题:
1 |
|
常量表达式
常量表达式主要是允许一些计算发生在编译时,即发生在代码编译而不是运行的时候。
这是很大的优化:假如有些事情可以在编译时做,它将只做一次,而不是每次程序运行时都计算。
constexpr
函数的限制:
函数中只能有一个return语句(有极少特例)
函数必须返回值(不能是void函数)
在使用前必须已有定义(不能先声明)
return返回语句表达式中不能使用非常量表达式的函数、全局数据,且必须是一个常量表达式
常量表达式的构造函数有以下限制:
- 函数体必须为空
- 初始化列表只能由常量表达式来赋值
用户定义字面量
用户自定义字面值,或者叫“自定义后缀”更直观些,主要作用是简化代码的读写。
1 |
|
根据 C++ 11 标准,只有下面参数列表才是合法的,最后四个对于字符串相当有用,第二个参数会自动推断为字符串的长度。
1 |
|
原生字符串字面值
原生字符串字面值(raw string literal)使用户书写的字符串“所见即所得”。C++11中原生字符串的声明相当简单,只需在字符串前加入前缀,即字母R,并在引号中使用括号左右标识,就可以声明该字符串字面量为原生字符串了。
1 |
|
类的改进
继承构造
C++ 11允许派生类继承基类的构造函数(默认构造函数、复制构造函数、移动构造函数除外)。
1 |
|
注意:
继承的构造函数只能初始化基类中的成员变量,不能初始化派生类的成员变量
如果基类的构造函数被声明为私有,或者派生类是从基类中虚继承,那么不能继承构造函数
一旦使用继承构造函数,编译器不会再为派生类生成默认构造函数
委托构造
和继承构造函数类似,委托构造函数也是C++11中对C++的构造函数的一项改进,其目的也是为了减少程序员书写构造函数的时间。
如果一个类包含多个构造函数,C++ 11允许在一个构造函数中的定义中使用另一个构造函数,但这必须通过初始化列表进行操作,如下:
1 |
|
继承控制
C++11之前,一直没有继承控制关键字,禁用一个类的进一步衍生比较麻烦。
C++ 11添加了两个继承控制关键字:final
和override
。
final阻止类的进一步派生和虚函数的进一步重写
override确保在派生类中声明的函数跟基类的虚函数有相同的签名
类默认函数的控制
=default
- 编译器将为显式声明的
=default
函数自动生成函数体 - 仅适用于类的特殊成员函数,且该特殊成员函数没有默认参数
- 函数既可以在类体里(inline)定义,也可以在类体外(out-of-line)定义
- 编译器将为显式声明的
=delete
- 在函数声明后加上
=delete
,就可将该函数禁用 - 可用于禁用类的某些转换构造函数,从而避免不期望的类型转换
- 禁用某些用户自定义的类的 new 操作符,从而避免在自由存储区创建类的对象
- 在函数声明后加上
模板的改进
右尖括号的改进
在C++98/03的泛型编程中,模板实例化有一个很繁琐的地方,就是连续两个右尖括号>>
会被编译解释成右移操作符,而不是模板参数表的形式,需要一个空格进行分割,以避免发生编译时的错误。
在实例化模板时会出现连续两个右尖括号,同样static_cast
、dynamic_cast
、reinterpret_cast
、const_cast
表达式转换时也会遇到相同的情况。C++98标准是让程序员在>>
之间填上一个空格,在C++11中,这种限制被取消了。在C++11标准中,要求编译器对模板的右尖括号做单独处理,使编译器能够正确判断出>>
是一个右移操作符还是模板参数表的结束标记。
别名
1 |
|
函数模板的默认模板参数
C++11之前,类模板是支持默认的模板参数,却不支持函数模板的默认模板参数。类模板的默认模板参数必须从右往左定义,数模板的默认模板参数则没这个限定。
可变参数模板
在C++11之前,类模板和函数模板只能含有固定数量的模板参数。C++11增强了模板功能,允许模板定义中包含0到任意个模板参数,这就是可变参数模板。
可变参数模板和普通模板的语义是一样的,只是写法上稍有区别,声明可变参数模板时需要在typename
或class
后面带上省略号...
:
1 |
|
省略号...
的作用有两个:
- 声明一个参数包,这个参数包中可以包含0到任意个模板参数
- 在模板定义的右边,可以将参数包展开成一个一个独立的参数
应用:
- 函数的递归调用
- 类的递归继承
右值引用
左值引用、右值引用
左值引用是对一个左值进行引用的类型,右值引用则是对一个右值进行引用的类型。
左值引用和右值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。
左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。
1 |
|
const 类型 &
为 “万能”的引用类型,它可以接受非常量左值、常量左值、右值对其进行初始化。
右值引用,使用&&
表示:
1 |
|
通常情况下,右值引用是不能够绑定到任何的左值的。
移动语义
右值引用是用来支持转移语义的。转移语义可以将资源 ( 堆,系统对象等 ) 从一个对象转移到另一个对象,这样能够减少不必要的临时对象的创建、拷贝以及销毁,能够大幅度提高 C++ 应用程序的性能。临时对象的维护 ( 创建和销毁 ) 对性能有严重影响。
转移语义是和拷贝语义相对的,可以类比文件的剪切与拷贝,当我们将文件从一个目录拷贝到另一个目录时,速度比剪切慢很多。
通过转移语义,临时对象中的资源能够转移其它的对象里。
转移构造函数
1 |
|
和拷贝构造函数类似,有几点需要注意:
- 参数(右值)的符号必须是右值引用符号,即
&&
。 - 参数(右值)不可以是常量,因为我们需要修改右值。
- 参数(右值)的资源链接和标记必须修改,否则,右值的析构函数就会释放资源,转移到新对象的资源也就无效了。
有了右值引用和转移语义,我们在设计和实现类时,对于需要动态申请大量资源的类,应该设计转移构造函数和转移赋值函数,以提高应用程序的效率。转移赋值函数
1 |
|
标准库函数 std::move()
如果已知一个命名对象不再被使用而想对它调用转移构造函数和转移赋值函数,也就是把一个左值引用当做右值引用来使用。标准库提供了函数 std::move()
,这个函数以非常简单的方式将左值引用转换为右值引用。
完美转发 std::forward()
完美转发适用于这样的场景:需要将一组参数原封不动的传递给另一个函数。
“原封不动”不仅仅是参数的值不变,在 C++ 中,除了参数值之外,还有一下两组属性:左值/右值和 const/non-const。完美转发就是在参数传递过程中,所有这些属性和参数值都不能改变,同时,而不产生额外的开销,就好像转发者不存在一样。在泛型函数中,这样的需求非常普遍。
C++11是通过引入一条所谓“引用折叠”(reference collapsing)的新语言规则,并结合新的模板推导规则来完成完美转发。
C++11中的引用折叠规则:
TR的类型定义 | 声明v的类型 | v的实际类型 |
---|---|---|
T & | TR | T & |
T & | TR & | T & |
T & | TR && | T & |
T && | TR | T && |
T && | TR & | T & |
T && | TR && | T && |
一旦定义中出现了左值引用,引用折叠总是优先将其折叠为左值引用。
智能指针
C++11中有unique_ptr
、shared_ptr
与weak_ptr
等智能指针(smart pointer),定义在<memory>
中。可以对动态资源进行管理,保证任何情况下,已构造的对象最终会销毁,即它的析构函数最终会被调用。
unique_ptr
unique_ptr
持有对对象的独有权,同一时刻只能有一个unique_ptr
指向给定对象(通过禁止拷贝语义、只有移动语义来实现)。
unique_ptr
指针本身的生命周期:从unique_ptr
指针创建时开始,直到离开作用域。
离开作用域时,若其指向对象,则将其所指对象销毁(默认使用delete
操作符,用户可指定其他操作)。
1 |
|
shared_ptr
shared_ptr
允许多个该智能指针共享第“拥有”同一堆分配对象的内存,这通过引用计数(reference counting)实现,会记录有多少个shared_ptr
共同指向一个对象,一旦最后一个这样的指针被销毁,也就是一旦某个对象的引用计数变为0,这个对象会被自动删除。
1 |
|
通常情况下shared_ptr
可以正常运转,但是在循环引用的场景下,shared_ptr
无法正确释放内存。循环引用,顾名思义,A
指向B
,B
指向A
,在表示双向关系时,是很可能出现这种情况的,例如:
1 |
|
输出:
Son
Father
son: 2
father: 2
可以看到,程序分别执行了Son和Father的构造函数,但是没有执行析构函数,出现了内存泄漏。
weak_ptr
weak_ptr
是为配合shared_ptr
而引入的一种智能指针来协助shared_ptr
工作,它可以从一个shared_ptr
或另一个weak_ptr
对象构造,它的构造和析构不会引起引用计数的增加或减少。没有重载 *
和->
但可以使用lock
获得一个可用的shared_ptr
对象
weak_ptr
的使用更为复杂一点,它可以指向shared_ptr
指针指向的对象内存,却并不拥有该内存,而使用weak_ptr
成员lock
,则可返回其指向内存的一个share_ptr
对象,且在所指对象内存已经无效时,返回指针空值nullptr
。
1 |
|
解决循环引用的问题:
1 |
|
闭包实现
闭包有很多种定义,一种说法是,闭包是带有上下文的函数。即有状态的函数。
那什么叫 “带上状态” 呢? 意思是这个闭包有属于自己的变量,这些个变量的值是创建闭包的时候设置的,并在调用闭包的时候,可以访问这些变量。
函数是代码,状态是一组变量,将代码和一组变量捆绑 (bind) ,就形成了闭包。
闭包的状态捆绑,必须发生在运行时。
仿函数:重载 operator()
仿函数实现闭包:
1 |
|
std::bind绑定器
std::function
在C++中,可调用实体主要包括:函数、函数指针、函数引用、可以隐式转换为函数指定的对象,或者实现了opetator()的对象。
C++11中,新增加了一个std::function
类模板,它是对C++中现有的可调用实体的一种类型安全的包裹。通过指定它的模板参数,它可以用统一的方式处理函数、函数对象、函数指针,并允许保存和延迟执行它们。
1 |
|
std::function
对象最大的用处就是在实现函数回调,使用者需要注意,它不能被用来检查相等或者不相等,但是可以与NULL
或者nullptr
进行比较。
std::bind
std::bind
是这样一种机制,它可以预先把指定可调用实体的某些参数绑定到已有的变量,产生一个新的可调用实体,这种机制在回调函数的使用过程中也颇为有用。
C++98中,有两个函数bind1st
和bind2nd
,它们分别可以用来绑定functor
的第一个和第二个参数,它们都是只可以绑定一个参数,各种限制,使得bind1st
和bind2nd
的可用性大大降低。
在C++11中,提供了std::bind
,它绑定的参数的个数不受限制,绑定的具体哪些参数也不受限制,由用户指定。
lambda表达式
C++11中的lambda表达式用于定义并创建匿名的函数对象,以简化编程工作。
lambda表达式的基本构成:
函数对象参数
[]
,标识一个lambda的开始,这部分必须存在,不能省略。函数对象参数是传递给编译器自动生成的函数对象类的构造函数的。函数对象参数只能使用那些到定义lambda为止时lambda所在作用范围内可见的局部变量(包括lambda所在类的this)。函数对象参数有以下形式:- 空。没有使用任何函数对象参数。
- =。函数体内可以使用lambda所在作用范围内所有可见的局部变量(包括lambda所在类的this),并且是值传递方式(相当于编译器自动为我们按值传递了所有局部变量)。
- &。函数体内可以使用lambda所在作用范围内所有可见的局部变量(包括lambda所在类的this),并且是引用传递方式(相当于编译器自动为我们按引用传递了所有局部变量)。
- this。函数体内可以使用lambda所在类中的成员变量。
- a。将a按值进行传递。按值进行传递时,函数体内不能修改传递进来的a的拷贝,因为默认情况下函数是const的。要修改传递进来的a的拷贝,可以添加mutable修饰符。
- &a。将a按引用进行传递。
- a, &b。将a按值进行传递,b按引用进行传递。
- =,&a, &b。除a和b按引用进行传递外,其他参数都按值进行传递。
- &, a, b。除a和b按值进行传递外,其他参数都按引用进行传递。
操作符重载函数参数
标识重载的()
操作符的参数,没有参数时,这部分可以省略。参数可以通过按值(如:(a,b))和按引用(如:(&a,&b))两种方式进行传递。可修改标示符
mutable
声明,这部分可以省略。按值传递函数对象参数时,加上mutable
修饰符后,可以修改按值传递进来的拷贝(注意是能修改拷贝,而不是值本身)。错误抛出标示符
exception
声明,这部分也可以省略。函数返回值
->返回值类型
,标识函数返回值的类型,当返回值为void,或者函数体中只有一处return的地方(此时编译器可以自动推断出返回值类型)时,这部分可以省略。是函数体
{}
,标识函数的实现,这部分不能省略,但函数体可以为空。
除去在语法层面上的不同,lambda和仿函数有着相同的内涵——都可以捕获一些变量作为初始化状态,并接受参数进行运行。
而事实上,仿函数是编译器实现lambda的一种方式,通过编译器都是把lambda表达式转化为一个仿函数对象。因此,在C++11中,lambda可以视为仿函数的一种等价形式。
lambda表达式的类型在C++11中被称为“闭包类型”,每一个lambda表达式则会产生一个**临时对象(右值)**。因此,严格地将,lambda函数并非函数指针。
不过C++11标准却允许lambda表达式向函数指针的转换,但提前是lambda函数没有捕获任何变量,且函数指针所示的函数原型,必须跟lambda函数函数有着相同的调用方式。
lambda表达式的价值在于,就地封装短小的功能闭包,可以及其方便地表达出我们希望执行的具体操作,并让上下文结合更加紧密。
线程
在C++11之前,C/C++一直是一种顺序的编程语言。顺序是指所有指令都是串行执行的,即在相同的时刻,有且仅有单个CPU的程序计数器执行代码的代码段,并运行代码段中的指令。而C/C++代码也总是对应地拥有一份操作系统赋予进程的包括堆、栈、可执行的(代码)及不可执行的(数据)在内的各种内存区域。
而在C++11中,一个相当大的变化就是引入了多线程的支持。这使得C/C++语言在进行线程编程时,不比依赖第三方库。