Cpp学习28 异常;输入与输出 (完)

铁名_IronName Lv5

Exception 的必要性

异常处理提供了一种机制,可以将错误或其他异常情况的处理与代码的常规控制流程解耦。这使得我们可以更灵活地根据具体情况选择最合适的错误处理时间和方式,从而减轻(如果不是全部)返回码带来的混乱。

基本异常处理

C++ 中的异常处理是通过三个关键字实现的,它们协同工作: throw 、 try 和 catch 。

在 C++ 中, throw 语句用于表示发生了异常或错误情况(类似于抛出罚旗)。表示发生了异常通常也称为引发异常。

1
2
3
4
5
throw -1; // throw a literal integer value
throw ENUM_INVALID_INDEX; // throw an enum value
throw "Can not take square root of negative number"; // throw a literal C-style (const char*) string
throw dX; // throw a double variable that was previously defined
throw MyException("Fatal Error"); // Throw an object of class MyException

在 C++ 中,我们使用 try 关键字来定义一个语句块(称为 try 代码块 )。try 代码块充当观察者的角色,它会检查 try 代码块内任何语句抛出的任何异常。

处理异常是 catch 代码块的职责。catch 关键字用于定义一个代码块(称为 catch 代码块 ),该代码块处理特定数据类型的异常。

以下是一个用于捕获整数异常的 catch 代码块示例:

1
2
3
4
5
catch (int x)
{
// Handle an exception of type int here
std::cerr << "We caught an int exception with value" << x << '\n';
}

当抛出异常(使用 throw 语句 )时,程序会找到最近的包含该异常的 try 代码块(如有必要,会向上遍历调用栈查找包含该异常的 try 代码块——我们将在下一课详细讨论这一点),以查看该 try 代码块中是否有 catch 处理程序可以处理该类型的异常。如果可以,程序会跳转到 catch 代码块的顶部,此时该异常被认为已处理完毕。

如果在程序结束前仍找不到合适的 catch 处理程序,程序将抛出运行时异常错误。

程序在将异常与 catch 代码块匹配时,不会执行隐式转换或类型提升!例如,char 异常不会与 int 异常匹配,int 异常也不会与 float 异常匹配。但是,程序会执行从派生类到其父类的强制类型转换。

异常、函数和堆栈展开

try 代码块不仅可以捕获 try 代码块内部语句的异常,还可以捕获 try 代码块内部调用的函数的异常。

如果一个函数不想处理异常,它完全可以不处理。异常会沿着栈向上传播,直到找到其他函数来处理为止!这使我们能够决定在调用栈的哪个位置最适合处理可能发生的任何错误。

未捕获的异常和兜底处理程序

当找不到函数的异常处理程序时,会调用 std::terminate() 并终止应用程序。在这种情况下,调用栈可能被展开,也可能不会!如果调用栈没有被展开,局部变量将不会被销毁,因此任何预期在销毁这些变量时进行的清理工作都不会发生!
(挺危险的。)

幸运的是,C++ 也提供了一种机制来捕获所有类型的异常。这被称为“捕获所有异常处理程序”(catch-all handler) 。捕获所有异常处理程序的工作方式与普通的 catch 块类似,不同之处在于它不使用特定的类型来捕获异常,而是使用省略号运算符 (…) 作为捕获类型。因此,捕获所有异常处理程序有时也被称为“省略号捕获处理程序”。

1
2
3
4
5
6
7
8
9
10
11
12
try
{
throw 5; // throw an int exception
}
catch (double x)
{
std::cout << "We caught an exception of type double: " << x << '\n';
}
catch (...) // catch-all handler
{
std::cout << "We caught an exception of an undetermined type\n";
}

异常、类和继承

1
2
3
4
5
6
7
8
9
10
11
12
13
int main()
{
try
{
A a{0};
}
catch (int)
{
std::cerr << "Oops\n";
}

return 0;
}

在上述程序中,当类 A 抛出异常时,A 的所有成员都会被销毁。成员 m_member 的析构函数会被调用,从而有机会清理它分配的所有资源。

异常类

 异常类就是一个专门设计用来抛出异常的普通类。

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
26
27
28
29
30
31
32
33
class ArrayException
{
private:
std::string m_error;

public:
ArrayException(std::string_view error)
: m_error{ error }
{
}

const std::string& getError() const { return m_error; }
};

class IntArray
{
private:
int m_data[3]{}; // assume array is length 3 for simplicity

public:
IntArray() {}

int getLength() const { return 3; }

int& operator[](const int index)
{
if (index < 0 || index >= getLength())
throw ArrayException{ "Invalid index" };

return m_data[index];
}

};

派生异常类的处理程序应该列在基类的处理程序之前。(不然会派生类异常会触发基类异常的catch)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
class Derived: public Base
{
public:
Derived() {}
};

int main()
{
try
{
throw Derived();
}
catch (const Derived& derived)
{
std::cerr << "caught Derived";
}
catch (const Base& base)
{
std::cerr << "caught Base";
}

return 0;
}

所有这些异常类都派生自一个名为 std::exception类(定义在头文件<exception>中)。std::exception 是一个小型接口类,旨在作为 C++ 标准库抛出的任何异常的基类。

std::exception 有一个名为 what() 的虚成员函数,它返回一个 C 风格的异常描述字符串。大多数派生类都会重写 what() 函数来更改异常消息。

从 std::exception 或 std::runtime_error 派生您自己的类

重新抛出异常

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
int main()
{
try
{
try
{
throw Derived{};
}
catch (Base& b)
{
std::cout << "Caught Base b, which is actually a ";
b.print();
std::cout << '\n';
throw; // note: We're now rethrowing the object here
}
}
catch (Base& b)
{
std::cout << "Caught Base b, which is actually a ";
b.print();
std::cout << '\n';
}

return 0;
}

这个看似没有抛出任何特定异常的 throw 关键字,实际上会重新抛出刚刚捕获到的同一个异常。不会创建任何副本,这意味着我们不必担心会因副本复制或切片而导致的性能下降。

函数 try 块

函数 try 代码块旨在允许您在整个函数体周围建立异常处理程序,而不是在代码块周围建立异常处理程序。当需要构造函数处理成员初始化列表中抛出的异常时,请使用函数 try 块。

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
26
27
28
class B : public A
{
public:
B(int x) try : A{x} // note addition of try keyword here
{
}
catch (...) // note this is at same level of indentation as the function itself
{
// Exceptions from member initializer list or
// from constructor body are caught here

std::cerr << "Exception caught\n";

throw; // rethrow the existing exception
}
};

int main()
{
try
{
B b{0};
}
catch (int)
{
std::cout << "Oops\n";
}
}

Exception 的风险和弊端

与构造函数不同,构造函数中抛出异常可以有效地表明对象创建不成功,但在析构函数中绝对 应该抛出异常。
如果在堆栈展开期间析构函数抛出异常,程序将停止运行。

当以下所有条件都成立时,异常处理最为有效:

  • 正在处理的错误很可能不会经常发生。
  • 错误非常严重,否则程序无法继续执行。
  • 错误发生的地方无法处理该错误。
  • 目前还没有更好的方法将错误代码返回给调用者。

异常规范和 noexcept

异常规范是一种语言机制,最初设计用于在函数规范中记录函数可能抛出的异常类型。

在 C++ 中,所有函数都被分为_非抛出异常函数_和_潜在抛出异常_函数。 非抛出异常函数承诺不会抛出调用者可见的异常。 潜在抛出异常函数则可能抛出调用者可见的异常。
要将函数定义为不抛出异常的函数,我们可以使用 noexcept 说明符 。

1
void doSomething() noexcept; // this function is specified as non-throwing

请注意, noexcept 实际上并不会阻止函数抛出异常或调用其他可能抛出异常的函数。只要 noexcept 函数内部捕获并处理了这些异常,并且这些异常不会导致函数退出,那么这样做是允许的。(只是不允许往上传咯)

noexcept 运算符也可以在表达式内部使用。它接受一个表达式作为参数,并返回 true 或 false ,以判断编译器是否认为该表达式会抛出异常。
noexcept 运算符可用于根据代码是否可能抛出异常来有条件地执行代码。这是为了满足某些异常安全保证

异常安全保证

异常安全保证是一种契约性准则,它规定了当发生异常时函数或类的行为方式。异常安全保证分为四个级别:

  • 无法保证——如果抛出异常,无法保证会发生什么(例如,类可能处于无法使用的状态)。
  • 基本保证——如果抛出异常,不会发生内存泄漏,对象仍然可用,但程序可能会处于修改后的状态。
  • 强保证——如果抛出异常,不会发生内存泄漏,程序状态也不会改变。这意味着函数要么完全成功,要么即使失败也不会产生任何副作用。如果失败发生在任何修改之前,这很容易做到;但也可以通过回滚所有更改,使程序恢复到失败前的状态来实现。
  • 无异常/无失败保证——该函数要么始终成功(无失败),要么始终失败且不抛出任何对调用者可见的异常(无异常)。如果未向调用者公开异常,则异常可能会在内部抛出。noexcept 符对应于这种级别的异常安全保证。
最佳实践

始终使移动构造函数、移动赋值和交换函数 noexcept 。

尽量 noexcept 使用复制构造函数和复制赋值运算符。

在其他函数中使用 noexcept 可以表示不失败或不抛出异常的保证。

std::move_if_noexcept

我们可以使用标准库函数 std::move_if_noexcept() 来判断应该执行移动操作还是复制操作。std::move_if_noexcept 与 std::move 对应,用法相同。
如果编译器能够判断传递给 std::move_if_noexcept 参数对象在进行移动构造时不会抛出异常(或者该对象仅支持移动且没有复制构造函数),则 std::move_if_noexcept 行为与 std::move() 完全相同(并返回转换为右值的对象)。否则,std::move_if_noexcept 将返回指向该对象的普通左值引用。


输入和输出 (I/O) 流

在 C++中,最基本的 I/O 操作就是用流来实现的。抽象地说, 就是一个可以按顺序访问的字节序列。随着时间的推移,流可以产生或消耗数量几乎无限的数据。

通常我们会处理两种不同类型的数据流。 输入流用于保存来自数据生成器(例如键盘、文件或网络)的输入。例如,用户可能在程序当前未预期任何输入时按下键盘上的某个键。程序不会忽略用户的按键操作,而是将数据放入输入流中,并在那里等待程序准备好接收它。
相反, 输出流用于保存特定数据使用者(例如显示器、文件或打印机)的输出。当向输出设备写入数据时,该设备可能尚未准备好接收数据——例如,程序向打印机的输出流写入数据时,打印机可能仍在预热。数据将保留在输出流中,直到打印机开始读取它。

还有另一组称为字符串流的类,它们允许你使用熟悉的插入 (<<) 和提取 (>>) 运算符来操作字符串。

1
2
3
4
5
6
7
8
std::stringstream os {};
os << "12345 67.89"; // insert a string of numbers into the stream
int nValue {};
double dValue {};

os >> nValue >> dValue;

std::cout << nValue << ' ' << dValue << '\n';

流状态和输入验证

goodbit, badbit, eofbit, failbit

输入验证是指检查用户输入是否符合预设条件的过程。输入验证通常可以分为两种类型:字符串验证和数值验证。
为了帮助我们,C++ 提供了一些有用的函数,可以用来判断特定字符是数字还是字母,位于 <cctype> 头文件中。

(略,a lot of codes)

基本文件 I/O

在以下示例中,我们将使用 ofstream 类来实现文件输出。这非常简单:

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
26
#include <fstream>
#include <iostream>

int main()
{
// ofstream is used for writing files
// We'll make a file called Sample.txt
std::ofstream outf{ "Sample.txt" };

// If we couldn't open the output file stream for writing
if (!outf)
{
// Print an error and exit
std::cerr << "Uh oh, Sample.txt could not be opened for writing!\n";
return 1;
}

// We'll write two lines into this file
outf << "This is line 1\n";
outf << "This is line 2\n";

return 0;

// When outf goes out of scope, the ofstream
// destructor will close the file
}

现在,我们将把上一个例子中写入的文件从磁盘读出来。注意,如果到达文件末尾(EOF),ifstream 函数会返回 0。我们将利用这一点来确定要读取多少内容。
记住,提取运算符会遇到空格而中断。为了读取整行,我们需要使用 getline() 函数。

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
26
27
28
#include <fstream>
#include <iostream>
#include <string>

int main()
{
// ifstream is used for reading files
// We'll read from a file called Sample.txt
std::ifstream inf{ "Sample.txt" };

// If we couldn't open the input file stream for reading
if (!inf)
{
// Print an error and exit
std::cerr << "Uh oh, Sample.txt could not be opened for reading!\n";
return 1;
}

// While there's still stuff left to read
std::string strInput{};
while (std::getline(inf, strInput))
std::cout << strInput << '\n';

return 0;

// When inf goes out of scope, the ifstream
// destructor will close the file
}

缓冲输出

C++ 中的输出可以缓冲。这意味着输出到文件流的任何内容可能不会立即写入磁盘。相反,多个输出操作可能会被批量处理。这样做主要是为了提高性能。当缓冲区的内容被写入磁盘时,这称为刷新缓冲区。刷新缓冲区的一种方法是关闭文件——缓冲区的内容将被刷新到磁盘,然后文件将被关闭。
在调用 exit() 之前显式关闭所有打开的文件始终是一个好习惯。

文件模式

使用 open() 显式打开文件

1
2
3
4
5
6
7
8
9
std::ofstream outf{ "Sample.txt" };
outf << "This is line 1\n";
outf << "This is line 2\n";
outf.close(); // explicitly close the file

// Oops, we forgot something
outf.open("Sample.txt", std::ios::app);
outf << "This is line 3\n";
outf.close();

随机文件 I/O

可以进行随机文件访问——即跳转到文件中的不同位置读取其内容。当文件包含大量记录,而您只想检索特定记录时,这种方法非常有用。您无需读取所有记录直到找到所需记录,而是可以直接跳转到想要检索的记录。

结束了?
不,是新的阶段。我们Unreal Engine 见!

  • 标题: Cpp学习28 异常;输入与输出 (完)
  • 作者: 铁名_IronName
  • 创建于 : 2026-08-15 10:57:32
  • 更新于 : 2026-08-15 12:46:21
  • 链接: https://blog.ironname.top/2026/Cpp/Cpp学习28/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论