Skip to content

Commit

Permalink
[faq] 迁移入对异常抛出的解释
Browse files Browse the repository at this point in the history
  • Loading branch information
FeignClaims committed Mar 22, 2024
1 parent 56ca785 commit 660fdfc
Show file tree
Hide file tree
Showing 5 changed files with 369 additions and 0 deletions.
77 changes: 77 additions & 0 deletions faq/exception_throwing/hardcore.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
************************************************************************************************************************
硬核题: 抛出一个构造函数和析构函数会输出的类
************************************************************************************************************************

大概率不考, 但理论很简单, 建议了解一下.

========================================================================================================================
异常抛出的对象
========================================================================================================================

异常抛出时是要抛出一个对象的, 那么这个对象应该存放在哪?

显然不能存放在栈上, 因为 :doc:`normal` 中已经指出, 在栈回溯的过程中, 这些栈上的自动存储对象都需要被析构.
所以当以 :cpp:`throw object;` 抛出时, 会调用拷贝构造函数创建一个新的对象, 它不存放在栈中.

直到异常处理完成, 这个异常抛出的对象才会被析构.

.. code-block:: cpp
:linenos:
void function() {
try {
Printer printer{Info{.ctor = "i", .copy_ctor = "n", .dtor = "t"}};
throw printer;
} catch (Printer& printer) {
Printer another{Info{.ctor = "8", .dtor = "_"}};
}
}
auto main() -> int {
function();
}
.. admonition:: 点击查看提示
:class: dropdown

6 个字符, 大小固定为 8 位的有符号整数类型.

.. admonition:: 点击查看答案
:class: dropdown, solution

:godbolt:`E1dezb8eP`, 答案: :code:`int8_t`.

========================================================================================================================
异常捕获时
========================================================================================================================

假设抛出了类型 :cpp:`Printer`,

- 如果 :cpp:`catch` 中写的是 :cpp:`catch (Printer printer)`, 则还需要发生拷贝.
- 如果 :cpp:`catch` 中写的是 :cpp:`catch (Printer& printer)`, 则直接引用异常抛出的对象.

.. code-block:: cpp
:linenos:
void function() {
try {
Printer printer{Info{.ctor = "i", .copy_ctor = "n", .dtor = "t"}};
throw printer;
} catch (Printer& printer) {
Printer another{Info{.ctor = "8", .dtor = "_"}};
}
}
auto main() -> int {
function();
}
.. admonition:: 点击查看提示
:class: dropdown

8 个字符, 没能编出什么有意义的词了😭.

.. admonition:: 点击查看答案
:class: dropdown, solution

:godbolt:`7Wz5f4jvY`, 答案: :code:`intn8_tt`.
19 changes: 19 additions & 0 deletions faq/exception_throwing/index.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
.. _exception_throwing:

########################################################################################################################
异常的抛出与捕获
########################################################################################################################

.. warning::

这里所有内容都可以归结为一句话: :doc:`异常抛出仍会恰当地进行析构, 从而避免资源泄露 </faq/basic_concepts/resource>`. 因此实际进行异常处理时, **完全不需要考虑这些**, 而更应该考虑 `异常保证等级 <https://zh.cppreference.com/w/cpp/language/exceptions#.E5.BC.82.E5.B8.B8.E5.AE.89.E5.85.A8>`_ 等内容.

但考试就是爱考这个, 而更为重要、与写好代码更相关的 *异常保证等级* 完全不在应试教学范围内.

.. toctree::
:maxdepth: 1
:caption: 目录

preface.rst
normal.rst
hardcore.rst
197 changes: 197 additions & 0 deletions faq/exception_throwing/normal.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,197 @@
************************************************************************************************************************
正常题: 抛出时栈回溯 (stack unwinding)
************************************************************************************************************************

抛出异常时, 控制流沿着栈 **向上** 进行回溯, 直到找到一个能 **匹配** 到的 :cpp:`try-catch` 块, 如果没能找到, 则调用 :cpp:`std::terminate` 终止程序.

自动存储期对象是分配在栈上的 (:ref:`具体地, 分配在代码块 {} 之内 <question_board:lifetime>`), 因此当抛出异常而发生栈回溯时, 若栈回溯离开了代码块 (函数代码块、:cpp:`try-catch` 块、自行添加的 :cpp:`{}` 等), 则会依次析构 *已构造但尚未销毁的自动存储期对象*, 以它们的构造函数完成的逆序调用析构函数.

.. hint::

如果不理解什么是栈, 请进行 :ref:`断点调试 <question_board:debugger>`, 其中:

- 调用堆栈显示的就是栈的内容, 各行是对应函数调用的记录 (如局部变量等), 称为栈帧. 如果对递归函数进行断点调试就会发现, 每次调用都创建了新的栈帧, 即可以理解为单纯就是调用了新的函数, 所以递归函数的调用与普通函数的调用是完全一致的.
- 局部变量窗口显示的就是栈帧中的局部变量内容, 因此当栈回溯过程离开了该局部变量所在的代码块, 自然会析构掉其中 *已构造但尚未销毁的自动存储期对象*.

========================================================================================================================
题 1
========================================================================================================================

.. code-block:: cpp
:linenos:
void function() {
Printer c1{Info{.ctor = "c", .dtor = "n"}};
try {
throw 1;
} catch (int) {
std::cout << "i";
}
}
auto main() -> int {
function();
}
.. admonition:: 点击查看提示
:class: dropdown

3 个字符, C++ 标准输入流.

.. admonition:: 点击查看答案
:class: dropdown, solution

:godbolt:`ThTaW6qnM`, 答案: :code:`cin`.

========================================================================================================================
题 2
========================================================================================================================

:cpp:`try-catch` 块也是代码块.

.. code-block:: cpp
:linenos:
void function() {
Printer c1{Info{.ctor = "s", .dtor = "n"}};
try {
Printer c2{Info{.ctor = "t", .dtor = "r"}};
throw 1;
} catch (int) {
Printer c3{Info{.ctor = "l", .dtor = "e"}};
}
}
auto main() -> int {
function();
}
.. admonition:: 点击查看提示
:class: dropdown

6 个字符, 获取 C 风格字符串的长度.

.. admonition:: 点击查看答案
:class: dropdown, solution

:godbolt:`vMo9Kv6K6`, 答案: :code:`strlen`.

========================================================================================================================
题 3
========================================================================================================================

.. code-block:: cpp
:linenos:
void function1() {
Printer c1{Info{.ctor = "O", .dtor = "F"}};
throw 1.0;
}
void function2() {
Printer* c1 = new Printer{Info{.ctor = "E", .dtor = "I"}};
function1();
Printer c2{Info{.ctor = "H", .dtor = "L"}};
}
auto main() -> int {
try {
function2();
} catch (double) {
}
}
.. admonition:: 点击查看提示
:class: dropdown

3 个字符, 输入流读取到流结束时产生的标志.

.. admonition:: 点击查看答案
:class: dropdown, solution

:godbolt:`6nvf1bj5j`, 答案: :code:`EOF`.

========================================================================================================================
题 4
========================================================================================================================

:cpp:`catch` 块判断是否匹配时, 与判断函数是否匹配的方式不同, 不正式地说, 它不会进行大部分隐式类型转换.

.. code-block:: cpp
:linenos:
void function1() {
try {
Printer c1{Info{.ctor = "i", .dtor = "n"}};
throw 1;
} catch (double) {
}
}
void function2() {
Printer* c1 = new Printer{Info{.ctor = "c", .dtor = "u"}};
function1();
Printer c2{Info{.ctor = "o", .dtor = "t"}};
}
auto main() -> int {
try {
function2();
} catch (int) {
}
}
.. admonition:: 点击查看提示
:class: dropdown

3 个字符, 标准输入流.

.. admonition:: 点击查看答案
:class: dropdown, solution

:godbolt:`7TEqd7arP`, 答案: :code:`cin`.

========================================================================================================================
题 5
========================================================================================================================

:cpp:`catch` 块判断是否匹配时, 异常对象可以与其 **公用基类** 匹配.

.. code-block:: cpp
:linenos:
class Base {};
class Derived : public Base {};
void function1() {
try {
Printer c1{Info{.ctor = "r", .dtor = "o"}};
throw Derived{};
} catch (Base&) {
}
}
void function2() {
Printer* c1 = new Printer( Info{.ctor = "f", .dtor = "z"} );
function1();
Printer c2(Info{.ctor = "n", .dtor = "t"});
}
auto main() -> int {
try {
function2();
} catch (Derived&) {
}
}
.. admonition:: 点击查看提示
:class: dropdown

5 个字符, 如何获取 vector 的首元素?

.. admonition:: 点击查看答案
:class: dropdown, solution

:godbolt:`7nzo55zPv`, 答案: :code:`front`.
75 changes: 75 additions & 0 deletions faq/exception_throwing/preface.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
************************************************************************************************************************
题目中所使用的类
************************************************************************************************************************

为了提高可读性, 部分地方采用了 C++20 的语法进行初始化, 见下文.

.. code-block:: text
:linenos:
ctor: 非拷贝构造时输出的字符
copy_ctor: 拷贝构造时输出的字符
copy_assign: 拷贝赋值时输出的字符
dtor: 析构时输出的字符
========================================================================================================================
:cpp:`class Printer`
========================================================================================================================

在非拷贝构造, 被用于拷贝构造, 被用于拷贝赋值, 析构时输出对应的字符串.

例如,

.. code-block:: cpp
:caption: 构造析构
auto main() -> int {
// 构造时输出 "0", 被用于拷贝构造时输出 "1", 被用于拷贝赋值时输出 "2", 析构时输出 "3", 其余情况不输出
Printer c1{Info{.ctor = "0", .copy_ctor = "1", .copy_assign = "2", .dtor = "3"}};
// 构造时输出 "4", 析构时输出 "5", 其余情况不输出
Printer c2{Info{.ctor = "4", .dtor = "5"}};
}
// 最终输出
// 0: c1 构造
// 4: c2 构造
// 5: c2 析构
// 3: c1 析构
.. code-block:: cpp
:caption: 拷贝构造
auto main() -> int {
// 构造时输出 "0", 被用于拷贝构造时输出 "1", 被用于拷贝赋值时输出 "2", 析构时输出 "3", 其余情况不输出
Printer c1{Info{.ctor = "0", .copy_ctor = "1", .copy_assign = "2", .dtor = "3"}};
// 拷贝构造输出 "1", 并且之后 c2.info == c1.info
// 即此后构造时输出 "0", 被用于拷贝构造时输出 "1", 被用于拷贝赋值时输出 "2", 析构时输出 "3", 其余情况不输出
Printer c2{c1};
}
// 最终输出
// 0: c1 构造
// 1: Printer c2{c1}
// 3: c2 析构
// 3: c1 析构
.. code-block:: cpp
:caption: 拷贝赋值
auto main() -> int {
// 构造时输出 "0", 被用于拷贝构造时输出 "1", 被用于拷贝赋值时输出 "2", 析构时输出 "3", 其余情况不输出
Printer c1{Info{.ctor = "0", .copy_ctor = "1", .copy_assign = "2", .dtor = "3"}};
// 构造时输出 "4", 被用于拷贝构造时输出 "5", 被用于拷贝赋值时输出 "6", 析构时输出 "7", 其余情况不输出
Printer c2{Info{.ctor = "4", .copy_ctor = "5", .copy_assign = "6", .dtor = "7"}};
// 输出 "2", 并且之后 c2.info == c1.info
// 即此后构造时输出 "0", 被用于拷贝构造时输出 "1", 被用于拷贝赋值时输出 "2", 析构时输出 "3", 其余情况不输出
c2 = c1;
}
// 最终输出
// 0: c1 构造
// 4: c2 构造
// 2: c2 = c1;
// 3: c2 析构
// 3: c1 析构
1 change: 1 addition & 0 deletions faq/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@
escape_character/main
newline_character/main
comma_operator/main
exception_throwing/index

***********************************************************************************************************************
类与类层次
Expand Down

0 comments on commit 660fdfc

Please sign in to comment.