一、nullptr
在引入 nullptr 之前,C 和 C++ 使用的都是 NULL,但对于它的定义是不同的:
1 2 3 4 5 6 7
| #ifndef NULL #ifdef __cplusplus #define NULL 0 #else #define NULL ((void *)0) #endif #endif
|
根本原因和C++的重载函数有关。C++ 通过搜索匹配参数的机制,试图找到最佳匹配的函数,而如果继续支持 void* 的隐式类型转换,则会带来语义二义性的问题。
nullptr 是 C++11 用来表示空指针新引入的常量值,在 C++ 中如果表示空指针语义时建议使用 nullptr 而不要使用 NULL,因为 NULL 本质上是个 int 型的 0,其实不是个指针。
1 2 3 4 5 6 7 8 9 10 11 12 13
| void func(void *ptr) { cout << "func ptr" << endl; }
void func(int i) { cout << "func i" << endl; }
int main() { func(NULL); func(nullptr); return 0; }
|
二、final
在 C++11 中,引入了 final 关键字,用于防止类被继承或防止虚函数被重写。它主要有两个用途:修饰类和修饰虚函数。
2.1 修饰类
当 final 关键字用于类时,表示该类不能被其他类继承。这可以保护基类的设计,防止继承带来的复杂性和潜在错误。例如:
1 2 3 4 5 6 7 8 9 10
| class Base final { public: void display() const { std::cout << "Base display" << std::endl; } };
|
在这个例子中,Base 类被标记为 final,因此不能被继承。如果尝试继承 Base 类(如注释掉的 Derived 类),编译器将报错。
2.2 修饰虚函数
当 final 关键字用于虚函数时,表示该虚函数在派生类中不能被重写。这对于一些基类中实现了关键逻辑的函数非常有用,可以确保这些函数在派生类中不会被意外地修改。例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class Base { public: virtual void display() const { std::cout << "Base display" << std::endl; } };
class Derived : public Base { public: void display() const final { std::cout << "Derived display" << std::endl; } };
|
在这个例子中,Derived 类中的 display 函数被标记为 final,因此 FurtherDerived 类不能重写它。如果尝试重写(如注释掉的 FurtherDerived 类),编译器将报错。
三、override
override 关键字用于确保派生类中的函数正确地重写了基类中的虚函数。
当在派生类中声明虚函数时使用 override,编译器会检查该函数是否确实重写了基类中的同名虚函数。如果派生类的函数没有正确地重写基类中的虚函数,或者基类中根本没有这样的虚函数,编译器将报错,从而防止潜在的错误。
1 2 3 4 5 6 7 8 9 10 11
| class Base { public: virtual void someFunction() const = 0; };
class Derived : public Base { public: void someFunction() const override { } };
|
在这个例子中,Derived类通过 override 关键字明确表示 someFunction 函数是对基类 Base 中的纯虚函数的重写。如果 Derived 类中的 someFunction 函数签名与 Base 类中的不一致,编译器将无法通过编译。
使用override时,必须遵守以下规则:
- 函数必须是虚函数
- 函数签名(包括返回类型、参数类型和数量、const属性等)必须与基类中的虚函数完全一致
- 不能重写基类中的非虚函数。
四、=default
C++11 引入了 =default 关键字,用于显式声明类的特殊成员函数(如构造函数、析构函数、赋值操作符等)由编译器生成默认实现。
如果类中有了自定义的构造函数,编译器就不会隐式生成默认构造函数,如下代码:
1 2 3 4 5 6 7 8 9
| struct A { int a; A(int i) { a = i; } };
int main() { A a; return 0; }
|
上面代码编译出错,因为没有匹配的构造函数,因为编译器没有生成默认构造函数,而通过 default,程序员只需在函数声明后加上 =default;,就可将该函数声明为 defaulted 函数,编译器将为显式声明的 defaulted 函数自动生成函数体,如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| struct A { A() = default; A(int i) { a = i; } void display() const { cout << "Data: " << a << endl; } private: int a; };
int main() { A a; A b(10); a.display(); b.display(); return 0; }
|
五、=delete
C++11引入了 =delete 语法,允许显式地禁用类的某些函数(如拷贝构造函数和赋值运算符),以防止对象被意外复制。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| struct A { A() = default; A(const A&) = delete; A& operator=(const A&) = delete; int a; A(int i) { a = i; } };
int main() { A a1; A a2 = a1; A a3; a3 = a1; }
|
=delele 在 C++11 中很常用,std::unique_ptr 就是通过 =delete 修饰来禁止对象的拷贝的。
六、explicit
explicit 关键字是用于控制类构造函数的隐式类型转换的。当构造函数只有一个参数时,可以通过explicit 关键字来指定该构造函数是显式的,这样就可以防止编译器自动使用该构造函数进行隐式类型转换。
当一个构造函数被声明为显式的,它就不能被用于隐式转换和拷贝初始化。这意味着,你不能将一个单一的值赋给一个类类型的对象,除非你直接调用构造函数或者使用强制类型转换。
不用 explicit:
1 2 3 4 5 6 7 8 9 10
| struct A { A(int value) { cout << "value" << endl; } };
int main() { A a = 1; return 0; }
|
使用 explicit:
1 2 3 4 5 6 7 8 9 10 11
| struct A { explicit A(int value) { cout << "value" << endl; } };
int main() { A a = 1; A aa(2); return 0; }
|
使用 explicit 关键字可以避免由于隐式类型转换导致的错误和不明确的代码。在某些情况下,隐式类型转换可能会导致意外的行为,特别是当它发生在不期望的地方时。通过将构造函数声明为显式的,程序员可以确保只有在明确请求时才会进行类型转换。
七、noexcept
noexcept是 C++11 引入的关键字,用于指定函数是否会抛出异常。它有两种用途:作为说明符(声明函数不会抛出异常)和作为运算符(检查表达式是否会抛出异常)。
如果一个声明为noexcept的函数抛出了异常:
- 异常不会被传播到函数外部
- 调用
std::terminate()终止程序
- 不保证会执行栈展开(不会调用局部对象的析构函数)
1 2 3
| void risky() noexcept { throw std::runtime_error("危险操作"); }
|
编译器可以对noexcept函数进行以下优化:
- 不生成异常表(exception table)
- 不保存用于栈展开的信息
- 不需要在异常传播路径上进行析构操作的准备
7.1 noexcept作为说明符
基本语法:
1 2
| 返回类型 函数名(参数列表) noexcept; 返回类型 函数名(参数列表) noexcept(常量表达式);
|
例:
1 2 3 4 5 6 7 8
| void func() noexcept;
void func() noexcept(true);
template <typename T> void process(T t) noexcept(noexcept(t.copy()));
|
7.2 noexcept作为运算符
基本语法:
例:
1 2 3 4 5 6 7 8 9 10 11
| void f() noexcept {} void g() {}
constexpr bool f_noexcept = noexcept(f()); constexpr bool g_noexcept = noexcept(g());
template <typename T> void process(T t) { static_assert(noexcept(t.process()), "T::process必须是noexcept的"); }
|
7.3 noexcept与移动语义
移动语义见 C++11新特性(一)
移动构造函数和移动赋值运算符应该声明为noexcept:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| class MyString { public: MyString(MyString&& other) noexcept : data_(other.data_), size_(other.size_) { other.data_ = nullptr; other.size_ = 0; } MyString& operator=(MyString&& other) noexcept { if (this != &other) { delete[] data_; data_ = other.data_; size_ = other.size_; other.data_ = nullptr; other.size_ = 0; } return *this; } private: char* data_; size_t size_; };
|
标准库容器(如std::vector)在扩容时,会根据元素类型的移动构造函数是否为noexcept做出不同决策:
- 如果移动构造是
noexcept:使用移动操作
- 如果不是
noexcept:使用复制操作(如果扩容过程中抛出异常,可以回滚)
1 2 3 4
| std::vector<MyString> vec;
vec.push_back(MyString("test"));
|
7.4 C++17中的自动noexcept推导
C++17自动为以下函数添加noexcept说明符:
- 隐式生成的特殊成员函数(默认构造、析构、复制、移动)
- 当且仅当操作中调用的所有函数都是
noexcept时
1 2 3 4 5 6 7
| struct Widget { std::string s; Widget(Widget&& w) = default; };
|
7.5 使用场景
- 移动操作:移动构造函数和移动赋值运算符
- 交换函数:
swap函数通常不会失败
- 内存释放函数:析构函数,
delete[]操作等
- 简单的getter/setter:不包含复杂逻辑的访问器
- 默认操作:默认构造、赋值等操作
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| class Resource { public: ~Resource();
Resource(Resource&& other) noexcept; Resource& operator=(Resource&& other) noexcept; int getValue() const noexcept { return value_; } friend void swap(Resource& a, Resource& b) noexcept; private: int value_; std::unique_ptr<int[]> data_; };
|
不适合使用noexcept的场景:
- 可能分配内存的操作(除非有专门的错误处理策略)
- 文件I/O操作
- 网络通信
- 复杂算法执行
实现高效异常安全的代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| class DatabaseConnection { public: DatabaseConnection(const std::string& connectionString); ~DatabaseConnection(); QueryResult executeQuery(const std::string& query); void close() noexcept; bool isConnected() const noexcept; };
|