C++11新特性(二)

一、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); // 输出func ptr
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;
}
};

// 以下代码会导致编译错误
// class Derived : public Base {
// };

在这个例子中,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;
}
};

// 以下代码会导致编译错误
// class FurtherDerived : public Derived {
// public:
// void display() const override {
// std::cout << "FurtherDerived 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(); // output: 0
b.display(); // output: 10

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) { // 没有explicit关键字
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; // error,不可以隐式转换
A aa(2); // ok
return 0;
}

使用 explicit 关键字可以避免由于隐式类型转换导致的错误和不明确的代码。在某些情况下,隐式类型转换可能会导致意外的行为,特别是当它发生在不期望的地方时。通过将构造函数声明为显式的,程序员可以确保只有在明确请求时才会进行类型转换。

七、noexcept

noexcept是 C++11 引入的关键字,用于指定函数是否会抛出异常。它有两种用途:作为说明符(声明函数不会抛出异常)和作为运算符(检查表达式是否会抛出异常)。

如果一个声明为noexcept的函数抛出了异常:

  1. 异常不会被传播到函数外部
  2. 调用std::terminate()终止程序
  3. 不保证会执行栈展开(不会调用局部对象的析构函数)
1
2
3
void risky() noexcept {
throw std::runtime_error("危险操作"); // 程序会立即终止,不会传播异常
}

编译器可以对noexcept函数进行以下优化:

  1. 不生成异常表(exception table)
  2. 不保存用于栈展开的信息
  3. 不需要在异常传播路径上进行析构操作的准备

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())); // 当且仅当t.copy()不抛出异常时,此函数也不抛出异常

7.2 noexcept作为运算符

基本语法:

1
noexcept(表达式)  // 如果表达式保证不抛出异常,则返回true

例:

1
2
3
4
5
6
7
8
9
10
11
void f() noexcept {}
void g() {}

constexpr bool f_noexcept = noexcept(f()); // true
constexpr bool g_noexcept = noexcept(g()); // false

template <typename T>
void process(T t) {
// 编译时检查t.process()是否声明为noexcept
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:
// 移动构造函数声明为noexcept
MyString(MyString&& other) noexcept
: data_(other.data_), size_(other.size_) {
other.data_ = nullptr;
other.size_ = 0;
}

// 移动赋值运算符声明为noexcept
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;
// 当vector扩容时,如果MyString::MyString(MyString&&)是noexcept,
// 将使用移动构造函数;否则使用复制构造函数
vec.push_back(MyString("test"));

7.4 C++17中的自动noexcept推导

C++17自动为以下函数添加noexcept说明符:

  1. 隐式生成的特殊成员函数(默认构造、析构、复制、移动)
  2. 当且仅当操作中调用的所有函数都是noexcept
1
2
3
4
5
6
7
struct Widget {
std::string s;

// C++17中,如果std::string的移动构造是noexcept,
// 则Widget的移动构造也自动成为noexcept
Widget(Widget&& w) = default; // 自动推导noexcept
};

7.5 使用场景

  1. 移动操作:移动构造函数和移动赋值运算符
  2. 交换函数swap函数通常不会失败
  3. 内存释放函数:析构函数,delete[]操作等
  4. 简单的getter/setter:不包含复杂逻辑的访问器
  5. 默认操作:默认构造、赋值等操作
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Resource {
public:
// 析构函数默认已是noexcept
~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的场景:

  1. 可能分配内存的操作(除非有专门的错误处理策略)
  2. 文件I/O操作
  3. 网络通信
  4. 复杂算法执行

实现高效异常安全的代码:

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;
};

C++11新特性(二)
http://example.com/2026/05/11/C++11新特性(二)/
作者
Yu xin
发布于
2026年5月11日
许可协议