十三、异常、类型转换和 lambda
- 异常
- 异常处理的基本语法
- 示例
- 异常类
- 异常规范
- `noexcept`
- 标准异常
- 非异常
- 返回值
- 错误码
- 输出参数
- 全局状态(如errno)
- 结论
- `std::opti onal`
- 基本用法
- 包含头文件
- 声明和初始化
- 访问值
- 修改值
- 赋值和比较
- 注意事项
- 总结
- 类型转换
- 隐式类型转换(Automatic Type Conversion)
- 静态类型转换(static_cast)
- 动态类型转换(dynamic_cast)
- 常量类型转换(const_cast)
- C风格的类型转换
- **总结**
- `lambda`
- 基本语法
- 示例
- 简单的lambda表达式
- 捕获列表示例
- 修改捕获的按值传递的变量
异常
C++ 中的异常处理是一种在程序执行期间处理错误或异常情况的机制。它允许程序在遇到错误时,不是立即崩溃或退出,而是以一种优雅的方式处理错误,可能是记录错误信息、释放资源或尝试恢复。
异常处理的基本语法
C++ 异常处理主要涉及三个关键字:try
、catch
、throw
。
try
:标识一个代码块,在这个代码块中的代码将被监控,以便检测是否有异常被抛出。catch
:跟在try
块后面,用来捕获并处理异常。可以有多个catch
块来捕获不同类型的异常。throw
:用于抛出一个异常,可以是基础数据类型、自定义类型或派生自std::exception
的类型。
示例
#include <iostream>
using namespace std;
int main() {
try {
// 抛出一个整数作为异常
throw 20;
} catch (int e) {
// 捕获并处理整数类型的异常
cout << "An exception occurred. Exception Nr. " << e << '\n';
}
return 0;
}
异常类
C++ 标准库提供了一系列的标准异常类,它们都派生自 std::exception
类。这些异常类包括:
std::logic_error
:用于报告程序逻辑错误,如无效参数或无效操作。std::runtime_error
:用于报告运行时错误,如超出范围的数组访问。
异常规范
在 C++98 中,函数可以使用异常规范来声明它们可能抛出的异常类型。然而,从 C++11 开始,异常规范(除了 noexcept
)被认为是弃用的,并在 C++17 中被彻底移除。
noexcept
noexcept
关键字用于指定一个函数不抛出异常。如果一个标记为 noexcept
的函数尝试抛出异常,程序将调用 std::terminate()
,导致程序非正常退出。
标准异常
C++ 标准库提供了一套丰富的异常处理机制,允许开发者通过抛出(throw)和捕获(catch)异常来处理程序中的错误情况。这些异常可以是标准库定义的,也可以是用户自定义的。标准库定义了一系列标准异常类,这些类都继承自std::exception
类,位于<stdexcept>
和<exception>
等头文件中。
-
std::exception
:这是所有标准异常的基类。它定义了一个名为what
的虚成员函数,该函数返回一个表示异常的字符串描述。 -
std::bad_alloc
:当new
操作符无法分配足够的内存时抛出。它继承自std::exception
。 -
std::bad_cast
:在执行动态类型转换(如dynamic_cast
)失败时抛出。它继承自std::bad_exception
(注意:在某些实现中,它直接继承自std::exception
)。 -
std::bad_exception
(注意:这实际上不是一个常用的异常,因为某些实现中它不存在或未被广泛使用):这个异常类的用途并不明确,且在某些C++标准库实现中可能不存在。它可能是作为基类设计的,但实际上很少被直接使用。 -
std::bad_typeid
:当在typeid
操作中使用了nullptr
或指向非多态类型对象的指针时抛出。它继承自std::exception
。 -
std::bad_weak_ptr
:当尝试访问std::weak_ptr
管理的对象,但该对象已被销毁时抛出。它继承自std::exception
。 -
std::domain_error
:当数学函数接收到一个无效参数时抛出(如,sqrt
接收到一个负数)。它继承自std::logic_error
。 -
std::invalid_argument
:当函数接收到一个无效参数时抛出。它继承自std::logic_error
。 -
std::length_error
:当尝试创建一个超出其最大可能长度的对象时抛出(如,std::vector
尝试扩展其大小超过max_size()
)。它继承自std::length_error
。 -
std::logic_error
:这是表示程序逻辑错误的异常的基类。std::domain_error
、std::invalid_argument
、std::length_error
、std::out_of_range
都是它的子类。 -
std::out_of_range
:当尝试访问某个序列(如std::vector
或std::string
)的超出其当前范围的元素时抛出。它继承自std::logic_error
。 -
std::overflow_error
:当算术运算的结果超出了可表示的范围时抛出(如,整数溢出)。它继承自std::runtime_error
。 -
std::range_error
:当函数接收到一个无效范围时抛出(尽管在标准库中没有直接继承自std::range_error
的常用异常,但它被用作一个基类)。 -
std::runtime_error
:这是表示运行时错误的异常的基类。std::overflow_error
、std::underflow_error
等都是它的子类。 -
std::underflow_error
:当算术运算的结果小于可表示的最小值时抛出(如,整数下溢)。它继承自std::runtime_error
。 -
std::overflow_error
:当算术运算的结果大于可表示的最大值时抛出(如,整数上溢)。它继承自std::runtime_error
。
使用这些标准异常可以让你的代码更加健壮和易于维护,因为它们为常见的错误情况提供了清晰的错误表示和统一的错误处理机制。
非异常
在C++中,错误处理是一个重要的方面,它涉及到如何优雅地处理程序中可能发生的错误情况。C++提供了几种机制来处理错误,其中非异常处理是其中一种方式。非异常处理通常依赖于返回值、错误码(error codes)、输出参数(out parameters)以及全局状态(如errno)等。
返回值
最常见的非异常错误处理方式是使用函数的返回值。函数可以返回一个特定的值来表示成功或不同类型的错误。例如,许多标准库函数返回整数值,其中0通常表示成功,而非0值表示不同类型的错误。
int readFile(const char* filename) {
// 尝试打开文件
if (/* 文件打开失败 */) {
return -1; // 表示错误
}
// 处理文件...
return 0; // 表示成功
}
错误码
错误码通常是一个枚举或整数,用于表示程序中发生的具体错误类型。函数可以通过输出参数返回错误码,以便调用者可以检查并采取相应的行动。
enum class ErrorCode {
Success,
FileNotFound,
PermissionDenied,
// 其他错误码...
};
ErrorCode readFile(const char* filename, std::string& content) {
// 尝试打开文件
if (/* 文件打开失败 */) {
return ErrorCode::FileNotFound;
}
// 读取文件内容到content...
return ErrorCode::Success;
}
输出参数
输出参数是函数参数的一种,用于从函数返回额外的信息。虽然它们不直接用于错误处理,但经常与错误码一起使用,以提供有关错误或操作结果的更多细节。
全局状态(如errno)
在C(和兼容C的C++代码)中,errno
是一个全局变量,用于报告函数调用的错误状态。当某些库函数(如I/O函数)失败时,它们会设置errno
以指示具体的错误类型。然而,由于errno
是全局的,它可能在多线程程序中引起问题,并且不是类型安全的。因此,在C++中,更推荐使用上述其他机制。
结论
非异常错误处理在C++中仍然很有用,尤其是在需要兼容C代码或避免异常开销的场景中。然而,随着C++的发展,异常处理已成为一种更受推荐的方式,因为它提供了一种结构化和类型安全的错误处理机制。在设计新的C++系统时,应优先考虑使用异常处理,但在需要时也可以使用非异常错误处理机制。
std::opti onal
std::optional
是 C++17 标准库中引入的一个非常有用的特性,它提供了一种可能包含或不包含值的包装类型。std::optional
可以包含其模板参数所指定的类型的值,或者不包含任何值(表示为“无状态”或“空”状态)。这使得函数能够返回一个值或者表示没有值返回的情况,而不需要使用特殊的错误码、指针、特殊的返回值(如使用 -1
表示错误或 nullptr
表示空),或者抛出异常。
基本用法
包含头文件
要使用 std::optional
,你需要包含头文件 <optional>
:
#include <optional>
声明和初始化
你可以声明一个 std::optional
类型的变量,并给它赋值:
std::optional<int> maybeInt; // 默认构造,初始化为不包含值的状态
std::optional<int> maybeInt2 = 42; // 直接初始化,包含值 42
std::optional<int> maybeInt3{42}; // 列表初始化,同样包含值 42
访问值
你可以使用 *
操作符来访问 std::optional
中包含的值,但首先你需要检查它是否确实包含了一个值,这可以通过 has_value()
成员函数来完成:
if (maybeInt2.has_value()) {
int value = *maybeInt2; // 安全地访问值
}
或者使用 value()
成员函数,如果 std::optional
不包含值,则 value()
会抛出一个 std::bad_optional_access
异常:
try {
int value = maybeInt2.value(); // 直接访问值,如果不存在则抛出异常
} catch (const std::bad_optional_access& e) {
// 处理异常
}
修改值
如果你想要修改 std::optional
中的值(如果它存在的话),你可以使用 emplace()
或 value()
(如果你确定它包含值):
if (maybeInt2.has_value()) {
maybeInt2.value() = 100; // 修改已存在的值
}
// 或者使用 emplace() 来重新构造值
maybeInt2.emplace(200); // 不管之前是否有值,都会用 200 重新构造
赋值和比较
std::optional
支持赋值操作,包括从另一个 std::optional
赋值,以及从内部类型的值赋值(这将导致 std::optional
变为包含该值的状态):
std::optional<int> anotherInt = maybeInt2; // 从另一个 optional 赋值
std::optional<int> yetAnotherInt = 300; // 直接从 int 赋值
if (anotherInt == yetAnotherInt) {
// 比较两个 optional
}
注意事项
- 使用
std::optional
时,要特别注意空状态的检查,以避免解引用空std::optional
导致的未定义行为。 std::optional
的引入旨在提供一种更优雅、更类型安全的方式来处理可选值,尤其是在函数返回类型中。- 虽然
std::optional
在 C++17 中引入,但许多现代编译器和库都提供了对它的支持,甚至在 C++17 正式发布之前。然而,如果你使用的是较旧的编译器或库,可能需要寻找替代方案或更新你的工具链。
总结
异常处理是 C++ 中一个重要的特性,它提供了一种结构化的方法来处理错误和异常情况。通过合理使用 try
、catch
和 throw
,以及利用标准异常类,可以使代码更加健壮和易于维护。
类型转换
在C++中,类型转换是一种将变量从一种类型转换为另一种类型的过程。C++提供了几种不同的类型转换方式,包括隐式类型转换(自动类型转换)、静态类型转换(static_cast)、动态类型转换(dynamic_cast)、常量类型转换(const_cast)以及C风格的类型转换(如 (type)value
或 type(value)
)。每种转换方式都有其特定的用途和限制。
隐式类型转换(Automatic Type Conversion)
隐式类型转换是编译器自动进行的类型转换,通常发生在赋值操作、算术运算或函数调用时。例如,将一个整数赋值给浮点数变量时,整数会被隐式转换为浮点数。
int a = 5;
double b = a; // 隐式转换,a 从 int 转换为 double
静态类型转换(static_cast)
static_cast
用于基本数据类型之间的转换,以及有明确定义转换关系的类之间的转换(如派生类到基类的转换,但注意基类指针或引用不能直接转换为派生类指针或引用,除非使用了dynamic_cast
)。
double d = 3.14;
int i = static_cast<int>(d); // 将 double 转换为 int
动态类型转换(dynamic_cast)
dynamic_cast
主要用于安全地将基类指针或引用转换为派生类指针或引用。如果转换失败,转换结果将是一个空指针(对于指针)或抛出异常(对于引用)。它主要用于处理类的继承层次结构中的向下转换(即基类到派生类)。
class Base {};
class Derived : public Base {};
Base* basePtr = new Derived();
Derived* derivedPtr = dynamic_cast<Derived*>(basePtr); // 安全地将 Base* 转换为 Derived*
常量类型转换(const_cast)
const_cast
用于修改类型的const属性。它可以将常量指针(或引用)转换为非常量指针(或引用),反之亦然。
const int* cpi = &someValue;
int* pi = const_cast<int*>(cpi); // 移除 const 限定符
C风格的类型转换
C风格的类型转换(如 (type)value
或 type(value)
)是一种较为通用的类型转换方式,但不建议在C++中使用,因为它不够明确,可能会隐藏潜在的错误。它基本上可以被视为上述类型转换的简化形式,但缺乏类型检查的安全性。
double d = 3.14;
int i = (int)d; // C风格的类型转换
总结
在C++中,了解不同类型的转换及其适用场景是非常重要的。推荐使用static_cast
、dynamic_cast
和const_cast
等C++风格的类型转换,因为它们提供了更好的类型检查和安全性。C风格的类型转换应该尽量避免使用。
lambda
C++中的lambda表达式是一种定义匿名函数对象的方式。它们提供了一种简洁、灵活的机制来编写可以在需要函数对象的地方使用的代码块。Lambda表达式在C++11及以后的版本中引入,极大地增强了C++的表达能力。
基本语法
Lambda表达式的基本语法如下:
[capture](parameters) mutable -> return_type {
// 函数体
}
- capture:捕获列表,指定lambda表达式体内可以访问的外部变量。捕获列表可以为空,也可以包含变量的列表,这些变量被按值或按引用捕获。
- parameters:参数列表,与普通函数的参数列表类似,但也可以为空。
- mutable:一个可选的说明符,用于指定lambda表达式体内的代码可以修改被捕获的按值传递的变量的值。
- return_type:返回类型,如果lambda表达式体中的代码块有返回语句,则需要指定返回类型(除了lambda表达式体只包含一个返回语句且编译器可以自动推导返回类型的情况)。
- 函数体:包含lambda表达式要执行的代码。
示例
简单的lambda表达式
#include <iostream>
#include <algorithm>
#include <vector>
int main() {
std::vector<int> vec = {1, 2, 3, 4, 5};
// 使用lambda表达式打印每个元素
std::for_each(vec.begin(), vec.end(), [](int i) {
std::cout << i << " ";
});
return 0;
}
捕获列表示例
#include <iostream>
#include <vector>
#include <algorithm>
int main() {
int x = 10;
std::vector<int> vec = {1, 2, 3, 4, 5};
// 使用lambda表达式,捕获外部变量x
std::for_each(vec.begin(), vec.end(), [&x](int i) {
std::cout << x + i << " "; // 注意这里修改了x的值(虽然在这个例子中并没有)
});
return 0;
}
注意,在上面的例子中,捕获列表使用了[&x]
,表示按引用捕获变量x
。如果你不希望lambda表达式体内部修改x
的值,可以使用[x]
(按值捕获)。
修改捕获的按值传递的变量
#include <iostream>
int main() {
int x = 10;
// 使用mutable关键字允许修改捕获的按值传递的变量
auto f = [x](int y) mutable {
x = y; // 如果没有mutable,这里会编译错误
std::cout << "x = " << x << std::endl;
};
f(20); // 输出: x = 20
return 0;
}
在这个例子中,mutable
关键字允许我们在lambda表达式体内修改捕获的按值传递的变量x
。
Lambda表达式是C++中一个非常强大的特性,它们使得代码更加简洁、灵活,并且易于阅读和维护。