C++11特性详解

C++11是C++程序设计语言标准的一个新的版本,在2011年由ISO批准并发布。C++11新标准从而代替了原来的C++98和C++03.。C++11标准是对C++的一次巨大的改进和扩充。在核心语法,STL标准模板等方面增加众多新功能,新亮点。例如新增auto,deltype,nullptr等关键字,增加范围for循环,新增lambda表达式等。下面将对C++增加的众多新特性进行总结。
新类型
long long类型unsigned long long类型char16_t和char32_t类型nullptr类型
统一的初始化
C++11扩大了用大括号括起的列表(初始化列表)的适用范围,使其可用于所有内置类型和用户定义的类型(即类对象)。使用初始化列表时,可添加等号(=),也可不添加:
|
|
- 使用初始化列表相对与赋值列表,可以避免不必要的临时对象的产生,提高效率。
- 初始化列表语法可防止缩窄,即禁止将数值赋给无法存储它的数值变量。常规初始化允许程序员执行可能没有意义的操作,初始化列表语法禁止
窄的类型转换,编译器将检查变量的大小来进行隐式转换。
列表初始化构造函数 std::initallizer_list, 可以接受任意数量相同类型的参数,并且可以防止对参数进行不必要的拷贝。只需要实现一个接受 std::initializer_list 参数的构造函数即可:
|
|
声明
auto 关键字
编译器根据初始化表达式推断变量的类型。
auto关键字只能用于变量声明,不能用于函数声明。auto关键字不能与const或volatile一起使用,但可以与const或volatile限定符一起使用,以推断变量的类型。
|
|
常见应用场景
|
|
使用auto关键字 简化了冗长的类型声明,程序员不需要再写冗长的类型声明,编译器会根据初始化表达式自动推导出变量的类型。
dectype 关键字
decltype关键字用于获取变量的类型,decltype关键字返回表达式类型,而不计算表达式的值。decltype关键字可以用于任何表达式,包括函数调用、类型转换等。
|
|
使用 decltype 关键字在定义模板时非常有用,因为只有等到模板被实例化时才能确定类型
|
|
如果不使用 decltype 关键字,那么 add 函数的返回类型将无法确定,因为 T 和 U 的类型只有在函数被调用时才能确定。
根据使用的表达式,指定的类型可以为引用和cosnt
|
|
(())用于引用类型,表示引用类型,而不是引用的值。
返回类型后置
C++11新增了一种函数声明语法:在函数名和参数列表后面(而不是前面)指定返回类型:
|
|
可以使用decltype来指定模板函数的返回值类型
|
|
为什么后置?
在编译器遇到函数
add的参数列表前,T和U还不在作用域内,因此必须在参数列表后使用decltype。这种新语法使得能够这样做。
模板别名
对于冗长或复杂的标识符,如果能够创建其别名将很方便。以前,C++为此提供了typedef:
|
|
C++11引入了using关键字,它不仅能够完成typedef的工作,还可以完成其他任务:
|
|
差别在于,新语法也可用于模板部分具体化,但typedef不能
|
|
nullptr
空指针是不会指向有效数据的指针。以前,C++在源代码中使用0表示这种指针,但内部表示可能不同。这带来了一些问题,因为这使得0即可表示指针常量,又可表示整型常量。C++11新增了关键字nullptr,用于表示空指针;它是指针类型,不能转换为整型类型。为向后兼容,C++11仍允许使用0来表示空指针,因此表达式nullptr == 0为true,但使用nullptr而不是0提供了更高的类型安全。例如,可将0传递给接受int参数的函数,但如果您试图将nullptr传递给这样的函数,编译器将此视为错误。因此,出于清晰和安全考虑,尽量使用nullptr。
智能指针
智能指针是
C++中的一种特性,它提供了一种更安全、更方便的方式来管理动态分配的内存。智能指针的主要目的是自动释放不再使用的内存,以避免内存泄漏。
智能指针的原理源RAII 机制,RAII(Resource Acquisition Is Initialization)是通过利用对象出了作用域的自动销毁的机理,使得资源也具有了生命周期,有了自动销毁(自动回收)的功能。RAII要求,资源的有效期与持有资源的对象的生命期严格绑定,即由对象的构造函数完成资源的分配(获取),同时由析构函数完成资源的释放。在这种要求下,只要对象能正确地析构,就不会出现资源泄露问题。
所谓智能指针(smart pointer),就是智能/自动化的管理指针所指向的动态资源的释放。它其实是一个对象,当中存储指向动态分配(堆)对象的指针。
智能指针主要有以下几种:
std::shared_ptr:共享指针,内部有一个引用计数器,当智能指针通过复制操作传递给另一个对象,引用计数会加一(所有复制的shared_ptr发生浅拷贝,共享引用计数和对象指针),多个智能指针可以共享同一块内存,当最后一个智能指针被销毁时,内存才会被释放。std::unique_ptr:独占指针,只能有一个智能指针拥有这块内存,当智能指针被销毁时,内存才会被释放。std::weak_ptr:弱指针,它指向一个std::shared_ptr管理的对象,但不增加引用计数。弱指针可以用来避免循环引用问题。
智能指针的使用非常简单,只需要在声明指针时使用std::make_shared或std::unique_ptr等函数即可。例如:
|
|
智能指针的销毁是自动的,当智能指针超出作用域时,会自动调用析构函数释放内存。因此,不需要手动释放内存,避免了内存泄漏的问题。
异常规范方面的修改
与auto_ptr一样,C++编程社区的集体经验表明,异常规范的效果没有预期的好。因此,C++11摒弃的异常规范。然而,标准委员会认为,指出函数不会引发异常有一定的价值,他们为此添加了关键字noexcept:
noexcept 关键字用于指示函数不会抛出任何异常。例如:
|
|
如果函数声明为noexcept,但实际在函数体内抛出了异常,编译器将生成一个uncaught_exception异常,该异常将终止程序。
nonexcept 用于希望函数出现异常时,程序能够立即终止,而不是进入异常处理机制。
作用域内枚举
传统的C++枚举提供了一种创建名称常量的方式,但其类型检查相当低级。另外,枚举名的作用域为枚举定义所属的作用域,这意味着如果在同一个作用域内定义两个枚举,它们的枚举成员不能同名。最后,枚举可能不是可完全移植的,因为不同的实现可能选择不同的底层类型。为解决这些问题,C++11新增了一种枚举。这种枚举使用class或struct定义:
|
|
这种枚举被称为作用域内枚举,因为枚举成员的作用域为枚举类型的作用域。因此,上述两种枚举都是有效的,但new1::RED和new2::RED是不同的枚举成员。
对类的修改
为简化和扩展类设计,
C++11做了多项改进。这包括允许构造函数被继承和彼此调用、更佳的方法访问控制方式以及移动构造函数和移动赋值运算符。
显式转换运算符
C++很早就支持对象自动转换。但随着编程经验的积累,程序员逐渐认识到,自动类型转换可能导致意外转换的问题。为解决这种问题,C++引入了关键字explicit,以禁止单参数构造函数导致的自动转换
例如:
|
|
上述代码中,MyString类有一个接受int参数的构造函数。如果使用explicit关键字修饰该构造函数,则不能使用隐式转换。例如,下面的代码将无法编译:
|
|
类内成员初始化
C++98允许在类内初始化静态成员,但不允许在类内初始化非静态成员。C++11改变了这种状况,允许在类内初始化非静态成员。例如:
|
|
内成员函数可使用等号或大括号版本的初始化,但不能使用圆括号版本的初始化。
通过使用类内初始化,可避免在构造函数中编写重复的代码,从而降低了程序员的工作量、厌倦情绪和出错的机会。
如果构造函数在成员初始化列表中提供了相应的值,这些默认值将被覆盖,因此第三个构造函数覆盖了类内成员初始化。
模板和STL方面的修改
为改善模板和标准模板库的可用性,
C++11做了多个改进;有些是库本身,有些与易用性相关
基于范围的for循环
C++11新增了一种基于范围的for循环,可以简化遍历容器的工作。例如:
|
|
上述代码中,for循环将遍历vec中的每个元素,并将当前元素赋给n。n的类型由vec中的元素类型推断得出。
实现自定义类基于范围的for循环,需要在类内定义迭代器对象,迭代器重载*运算符,->运算符,++运算符和==运算符,自定义类,需要重载begin()和end()函数。并返回迭代器对象例如:
|
|
新的STL容器
C++11新增了unordered_map和unordered_set容器,分别对应std::map和std::set的无序版本,以及forward_list。forword_list是一种单向链表,只能沿一个方向遍历;与双向链接的list容器相比,它更简单,在占用存储空间方面更经济
C++11还新增了模板array,要实例化这种模板,可指定元素类型和固定的元素数
新的STL方法
C++11新增了STL方法cbegin()和cend()。与begin()和end()一样,这些新方法也返回一个迭代器,指向容器的第一个元素和最后一个元素的后面,因此可用于指定包含全部元素的区间。另外,这些新方法将元素视为const。与此类似,crbegin()和crend()是rbegin()和rend()的const版本。
除传统的复制构造函数和常规赋值运算符外,STL容器现在还有移动构造函数和移动赋值运算符
- 移动构造函数:
Vector(Vector&& other) - 移动赋值运算符:
Vector& operator=(Vector&& other)
移动构造函数和移动赋值运算符都接受一个右值引用参数(&&),并接管该参数的资源,从而避免复制。
valarray升级
valarray是C++标准库中的一个类模板,用于存储和操作数值数组,支持常见的数组操作和数学运算。
valarray的特点
- 高效:
valarray被设计为在数值计算中具有高效的性能,尤其是在处理大规模数据时。 - 简洁的语法:提供了简洁的语法来进行元素级的操作,例如加法、减法、乘法、除法等。
- 专用操作:
valarray提供了一些专门的操作,如切片(slice)、掩码(mask)和移位
|
|
模板valarray独立于STL开发的,其最初的设计导致无法将基于范围的STL算法用于valarray对象。C++11添加了两个函数begin()和end(),它们都接受valarray作为参数,并返回迭代器,这些迭代器分别指向valarray对象的第一个元素和最后一个元素后面。这让您能够将基于范围的STL算法用于valarray。
摒弃export
C++98新增了关键字export,旨在提供一种途径,让程序员能够将模板定义放在接口文件和实现文件中,其中前者包含原型和模板声明,而后者包含模板函数和方法的定义。实践证明这不现实,因此C++11终止了这种用法,但仍保留了关键字export,供以后使用。
尖括号
为避免与运算符»混淆,C++要求在声明嵌套模板时使用空格将尖括号分开,C++11不再这样要求
|
|
右值引用
传统的C++引用(现在称为左值引用)使得标识符关联到左值。左值是一个表示数据的表达式(如变量名或解除引用的指针),程序可获取其地址。最初,左值可出现在赋值语句的左边,但修饰符const的出现使得可以声明这样的标识符,即不能给它赋值,但可获取其地址
C++11新增了右值引用,这是使用&&表示的。右值引用可关联到右值,即可出现在赋值表达式右边,但不能对其应用地址运算符的值。右值包括字面常量(C-风格字符串除外,它表示地址)、诸如x + y等表达式以及返回值的函数(条件是该函数返回的不是引用)。右值引用的主要用途是启用所谓的移动语义,这可减少不必要的复制,从而提高程序的效率。
移动语义和右值引用
|
|
则v2是v1的副本,副本的创建过程称为复制。C++库通过使用vector的复制构造函数来执行复制。
移动构造函数和移动赋值运算符都接受一个右值引用参数(&&),并接管该参数的资源,从而避免复制。
当用一个右值(临时对象)来初始化另一个对象时,编译器会尝试调用移动构造函数。例如:
|
|
s1 + "bye bye" 返回了一个临时的右值对象,编译器会调用移动构造函数给s2复制,这避免的临时对象的拷贝。
|
|
当函数返回一个对象时,编译器会尝试调用移动构造函数。如果函数返回一个局部对象的引用,则编译器会调用移动赋值运算符。
你明确地使用 std::move 来转换一个左值为右值时,也会调用移动构造函
|
|
std::move函数将v1转换为右值引用,从而允许vector的移动构造函数使用v1的资源,而无需复制它们。移动构造函数接管v1的资源,将v1重置为一个空容器,然后v2包含v1之前的数据。
若满足以下条件,编译器会自动生成移动构造函数,否则不会自动生成,此时若没有定义移动构造函数,会调用拷贝构造函数。
- 类中没有用户定义的拷贝构造函数、拷贝赋值运算符、移动赋值运算符和析构函数。
- 类中的所有非静态数据成员、基类和所有非静态数据成员的类类型都有可移动构造函数。
默认移动构造函数会完成以下工作,
|
|
如果你想编写代码查看编译器如何调用移动构造函数如何,请添加编译器选项
-fno-elide-constructors关闭返回值优化RVO
新的类功能
特殊的成员函数
在原有4个特殊成员函数(默认构造函数、复制构造函数、复制赋值运算符和析构函数)的基础上,C++11新增了两个:移动构造函数和移动赋值运算符。这些成员函数是编译器在各种情况下自动提供的。
默认的移动构造函数和移动赋值运算符的工作方式与复制版本类似:执行逐成员初始化并复制内置类型。如果成员是类对象,将使用相应类的构造函数和赋值运算符,就像参数为右值一样。如果定义了移动构造函数和移动赋值运算符,这将调用它们;否则将调用复制构造函数和复制赋值运算符。
默认的方法和禁用的方法
C++11让您能够更好地控制要使用的方法。假定您要使用某个默认的函数,而这个函数由于某种原因不会自动创建。例如,您提供了移动构造函数,因此编译器不会自动创建默认的构造函数、复制构造函数和复制赋值构造函数。在这些情况下,您可使用关键字default显式地声明这些方法的默认版本
另一方面,关键字delete可用于禁止编译器使用特定方法。例如,要禁止复制对象,可禁用复制构造函数和复制赋值运算符
关键字
default只能用于6个特殊成员函数,但delete可用于任何成员函数。delete的一种可能用法是禁止特定的转换。
委托构造函数
如果给类提供了多个构造函数,您可能重复编写相同的代码。也就是说,有些构造函数可能需要包含其他构造函数中已有的代码。为让编码工作更简单、更可靠,C++11允许您在一个构造函数的定义中使用另一个构造函数。这被称为委托,因为构造函数暂时将创建对象的工作委托给另一个构造函数。委托使用成员初始化列表语法的变种
|
|
继承构造函数
C++11新增了继承构造函数,这让派生类能够继承基类的构造函数。继承构造函数使用using关键字,然后指定要继承的构造函数。例如,如果基类MyClass有如下构造函数:
|
|
派生类MyDerivedClass可以这样继承这些构造函数:
|
|
现在,MyDerivedClass可以使用MyClass的构造函数来创建对象。
管理虚方法:override和final
虚方法对实现多态类层次结构很重要,让基类引用或指针能够根据指向的对象类型调用相应的方法,但虚方法也带来了一些编程陷阱。例如,假设基类声明了一个虚方法,而您决定在派生类中提供不同的版本,这将覆盖旧版本
override:如果派生类方法使用override修饰符,但该方法没有覆盖基类中的任何方法,编译器将生成错误消息。这有助于捕获由于拼错方法名而导致的错误。final:final修饰符用于停止方法覆盖。如果基类方法被声明为final,则任何尝试覆盖该方法的行为都将导致编译器错误。
此外,如果类被声明为final,则任何尝试继承该类的行为都将导致编译器错误。
Lambda函数
Lambda函数是C++11新增的一项功能,它让您能够编写类似函数的对象,这些对象的行为类似于函数指针,但它们是类型安全的,且使用起来更方便。Lambda函数让您能够编写内联代码块,然后将其作为参数传递给算法或其他函数。
|
|
Lambda函数的定义由[]开始,然后是一对括号,括号中是参数列表,然后是->,最后是函数体。
Lambda函数的参数列表可以是空的,也可以包含多个参数。函数体可以是单个语句,也可以是多个语句。如果函数体包含多个语句,则这些语句必须用花括号括起来。
Lambda函数的返回类型可以是自动推断的,也可以是显式指定的。如果函数体包含单个语句,则返回类型将自动推断为该语句的结果类型。否则,返回类型必须显式指定。
闭包函数
Lambda函数可以捕获其所在作用域中的变量,这些变量称为闭包变量。闭包变量可以是值或引用。如果闭包变量是值,则Lambda函数将创建该变量的副本,并在函数体中使用该副本。如果闭包变量是引用,则Lambda函数将使用该变量的当前值。
|
|
捕获所有变量
[&]:捕获所有变量,按引用捕获, 在函数体内的修改会影响外部变量
|
|
[=]:捕获所有变量,按值捕获,在函数体内的变量是一个副本,对函数体内 外部变量的修改不会影响外部变量
|
|
[&, x]:捕获所有变量,除了x,按引用捕获[=, &x]:捕获所有变量,除了x,按值捕获
包装器
C++提供了多个包装器(wrapper,也叫适配器[adapter])。这些对象用于给其他编程接口提供更一致或更合适的接口。
stack:vector进行包装,使其行为类似于stack。queue: 对deque进行包装,使其行为类似于queue。priority_queue: 优先队列,对vector进行包装。
包装器function
std::function 是 C++ 标准库中一个通用的函数包装器,可以用来存储、传递和调用任何可调用对象,包括普通函数、lambda 表达式、函数对象(functors)、成员函数指针等。std::function 提供了统一的接口来处理不同类型的可调用对象,从而简化了函数的管理和使用。
std::function 的主要特点
- 多态性:可以存储不同类型的可调用对象,并通过同一个接口调用它们。
- 类型安全:std::function 会检查存储的可调用对象是否符合指定的函数签名。
- 复制和赋值:std::function 对象可以复制和赋值,存储的可调用对象也会随之复制。
|
|
可变模板参数
C++11新增了可变模板参数,让您能够定义接受可变数量参数的模板。可变模板参数使用...表示,可以在模板参数列表中指定。例如,以下代码定义了一个可变模板参数的模板:
|
|
这个模板接受任意数量的参数,并将它们传递给doSomething方法。
您可以使用sizeof...运算符来获取可变模板参数的数量。例如,以下代码将打印可变模板参数的数量:
|
|
并发编程
C++11引入了多个用于并发编程的特性,包括std::thread、std::mutex、std::condition_variable等。这些特性使得在C++中编写多线程程序变得更加容易和安全。
std::thread类用于创建和管理线程。您可以使用std::thread类的构造函数来创建一个新的线程,并使用join或detach方法来等待线程结束或将其分离。例如,以下代码创建了一个新的线程,并在其中执行一个函数:
|
|
std::mutex类用于保护共享数据,防止多个线程同时访问。您可以使用std::mutex类的lock和unlock方法来锁定和解锁互斥锁。例如,以下代码使用互斥锁来保护一个共享变量:
|
|
std::condition_variable类用于在线程之间进行同步。您可以使用std::condition_variable类的wait和notify_one或notify_all方法来等待条件变量被通知或通知其他线程。例如,以下代码使用条件变量来同步两个线程:
|
|