C++11新特性(二)
一、nullptr
在引入 nullptr 之前,C 和 C++ 使用的都是 NULL,但对于它的定义是不同的:
1 | |
根本原因和 C++ 的重载函数有关。C++ 通过搜索匹配参数的机制,试图找到最佳匹配的函数,而如果继续支持 void* 的隐式类型转换,则会带来语义二义性的问题。
C++ 不允许直接将 void * 隐式转换到其他类型。但如果编译器尝试把 NULL 定义为 ((void*)0),那么在下面这句代码中:
1 | |
没有了 void * 隐式转换的 C++ 只好将 NULL 定义为 0。而这依然会产生新的问题,将 NULL 定义成 0 将导致 C++ 中重载特性发生混乱。考虑下面这两个 foo 函数:
1 | |
那么 foo(NULL); 这个语句将会去调用 foo(int),从而导致代码违反直觉。
nullptr 是 C++11 用来表示空指针新引入的常量值,专门用来区分空指针和 0。而 nullptr 的类型为 nullptr_t,能够隐式的转换为任何指针或成员指针的类型,也能和他们进行相等或者不等的比较。
在 C++ 中如果表示空指针语义时建议使用 nullptr 而不要使用 NULL,因为 NULL 本质上是个 int 型的 0,其实不是个指针。
1 | |
二、final
在 C++11 中,引入了 final 关键字,用于防止类被继承或防止虚函数被重写。它主要有两个用途:修饰类和修饰虚函数。
2.1 修饰类
当 final 关键字用于类时,表示该类不能被其他类继承。这可以保护基类的设计,防止继承带来的复杂性和潜在错误。例如:
1 | |
在这个例子中,Base 类被标记为 final,因此不能被继承。如果尝试继承 Base 类(如注释掉的 Derived 类),编译器将报错。
2.2 修饰虚函数
当 final 关键字用于虚函数时,表示该虚函数在派生类中不能被重写。这对于一些基类中实现了关键逻辑的函数非常有用,可以确保这些函数在派生类中不会被意外地修改。例如:
1 | |
在这个例子中,Derived 类中的 display 函数被标记为 final,因此 FurtherDerived 类不能重写它。如果尝试重写(如注释掉的 FurtherDerived 类),编译器将报错。
三、override
override 关键字用于确保派生类中的函数正确地重写了基类中的虚函数。
当在派生类中声明虚函数时使用 override,编译器会检查该函数是否确实重写了基类中的同名虚函数。如果派生类的函数没有正确地重写基类中的虚函数,或者基类中根本没有这样的虚函数,编译器将报错,从而防止潜在的错误。
1 | |
在这个例子中,Derived类通过 override 关键字明确表示 someFunction 函数是对基类 Base 中的纯虚函数的重写。如果 Derived 类中的 someFunction 函数签名与 Base 类中的不一致,编译器将无法通过编译。
使用override时,必须遵守以下规则:
- 函数必须是虚函数
- 函数签名(包括返回类型、参数类型和数量、const属性等)必须与基类中的虚函数完全一致
- 不能重写基类中的非虚函数。
四、=default
C++11 引入了 =default 关键字,用于显式声明类的特殊成员函数(如构造函数、析构函数、赋值操作符等)由编译器生成默认实现。
如果类中有了自定义的构造函数,编译器就不会隐式生成默认构造函数,如下代码:
1 | |
上面代码编译出错,因为没有匹配的构造函数,因为编译器没有生成默认构造函数,而通过 default,程序员只需在函数声明后加上 =default;,就可将该函数声明为 defaulted 函数,编译器将为显式声明的 defaulted 函数自动生成函数体,如下:
1 | |
五、=delete
C++11引入了 =delete 语法,允许显式地禁用类的某些函数(如拷贝构造函数和赋值运算符),以防止对象被意外复制。
1 | |
=delele 在 C++11 中很常用,std::unique_ptr 就是通过 =delete 修饰来禁止对象的拷贝的。
六、explicit
explicit 关键字是用于控制类构造函数的隐式类型转换的。当构造函数只有一个参数时,可以通过explicit 关键字来指定该构造函数是显式的,这样就可以防止编译器自动使用该构造函数进行隐式类型转换。
当一个构造函数被声明为显式的,它就不能被用于隐式转换和拷贝初始化。这意味着,你不能将一个单一的值赋给一个类类型的对象,除非你直接调用构造函数或者使用强制类型转换。
不用 explicit:
1 | |
使用 explicit:
1 | |
使用 explicit 关键字可以避免由于隐式类型转换导致的错误和不明确的代码。在某些情况下,隐式类型转换可能会导致意外的行为,特别是当它发生在不期望的地方时。通过将构造函数声明为显式的,程序员可以确保只有在明确请求时才会进行类型转换。
七、noexcept
noexcept是 C++11 引入的关键字,用于指定函数是否会抛出异常。它有两种用途:作为说明符(声明函数不会抛出异常)和作为运算符(检查表达式是否会抛出异常)。
如果一个声明为noexcept的函数抛出了异常:
- 异常不会被传播到函数外部
- 调用
std::terminate()终止程序 - 不保证会执行栈展开(不会调用局部对象的析构函数)
1 | |
编译器可以对noexcept函数进行以下优化:
- 不生成异常表(exception table)
- 不保存用于栈展开的信息
- 不需要在异常传播路径上进行析构操作的准备
7.1 noexcept作为说明符
基本语法:
1 | |
例:
1 | |
7.2 noexcept作为运算符
基本语法:
1 | |
例:
1 | |
7.3 noexcept与移动语义
移动语义见 C++11新特性(一)
移动构造函数和移动赋值运算符应该声明为noexcept:
1 | |
标准库容器(如std::vector)在扩容时,会根据元素类型的移动构造函数是否为noexcept做出不同决策:
- 如果移动构造是
noexcept:使用移动操作 - 如果不是
noexcept:使用复制操作(如果扩容过程中抛出异常,可以回滚)
1 | |
7.4 C++17中的自动noexcept推导
C++17自动为以下函数添加noexcept说明符:
- 隐式生成的特殊成员函数(默认构造、析构、复制、移动)
- 当且仅当操作中调用的所有函数都是
noexcept时
1 | |
7.5 使用场景
- 移动操作:移动构造函数和移动赋值运算符
- 交换函数:
swap函数通常不会失败 - 内存释放函数:析构函数,
delete[]操作等 - 简单的getter/setter:不包含复杂逻辑的访问器
- 默认操作:默认构造、赋值等操作
1 | |
不适合使用noexcept的场景:
- 可能分配内存的操作(除非有专门的错误处理策略)
- 文件I/O操作
- 网络通信
- 复杂算法执行
实现高效异常安全的代码:
1 | |