第五章是「错误」,探讨了错误类型、错误来源、规避或解决方法(调试)。
错误的分类
- 编译时错误
- 链接时错误
- 运行时错误
- 逻辑错误
错误的来源
找到错误的源头,然后去消除或解决错误;而不是从报错现场,逐层往前排查。
参数检查的位置
如果是由调用者检查,存在的问题:
- 同一个函数被调用多次时,每一处调用者都需要做检查
- 调用者需要知晓被调用函数的实现细节,才能做检查
- 一旦函数的实现改变,所有的调用者的参数检查,都需要修改,维护成本不可控制
- 如果现有的参数检查需要做调整,每一处调用是否都要调整?维护成本不可控制
举例:
在调用处做参数检查(低效、几乎不可维护的修改方式):
正确的做法:函数实现内部做检查:
基于 Exception 的处理方式:
class Bad_area{};
int area(int length, int width)
{
if (length <= 0 || width <= 0)
throw Bad_area();
return length * width;
}
void test_area()
{
int x = -1;
int y = 2;
int z = area(x, y);
std::cout << z << std::endl;
}
在 macOS 上:
在 Linux-x64 上也有类似输出。
但在 MSVC 上,不那么直观,报错是 -1073740791 (0xc0000409).
捕获 Exception
抛出Exception并不够用,还需要捕获。一旦捕获,程序就不会crash,而是继续执行。
劳累,是出现错误的一个原因
逻辑错误 - 浮点数初始值
调试是乏味、费时的
尽量在设计和编码阶段,做仔细点,从而减少出错的情况:
实用调试技术
- 使用基于 Exception 的错误处理
- 一些 clean code 的风格
- 注释要简单有效;能用代码表达清楚的不应该写注释
- 名字要有意义
- 一致的代码层次结构
- 代码应该分成许多小函数,每个函数表达一个逻辑功能
- 避免使用复杂的程序语句
- 在可能的情况下,使用标准库而不是自己的代码
混乱的代码容易隐藏错误
代码混乱,带来的风险是:一旦需要debug,debug的耗时会很多。代码如果工整,可以改善。
编写前置条件,可以提升程序质量
执行了参数的检查;即使是注释形式。
适当编写后置条件
个人认为这些内容,其实可以放在单元测试里。
总结
这一章作者的确给出了很多面向实际C++工程的指导,包括错误处理(推荐用Exception方式)、调试方法, 也掺杂了 clean code 方面的很多细则建议。
个人看下来最实用的是基于 exception 的报错处理:示例代码:
#include <stdexcept>
void error(const std::string& s)
{
throw runtime_error(s);
}
void test_try_catch_error()
{
try {
int x = -1;
int y = 2;
int z = area(x, y);
std::cout << z << std::endl;
}
catch (std::runtime_error& e)
{
std::cerr << "runtime error: " << e.what() << "\n";
return;
}
catch (std::exception& e)
{
std::cerr << "exception: " << e.what() << '\n';
return;
}
catch (...) { // 捕获所有其他类型的异常
std::cerr << "An unknown exception occurred\n";
return;
}
}
调试方法,没有明确的答案,需要结合问题来给出;但是可以明确的是,调试程序的关键在于:我是否知道程序是否运行正确呢?
附:思考题和答案
-
举出4种主要错误类型并给出它们的简洁定义
编译时错误:由编译器发现的错误。
链接时错误:由链接器发现的错误。
运行时错误:程序运行时发现的错误。
逻辑错误: 由程序员发现的会导致不正确结果的错误。 -
在学生练习程序中,什么类型的错误我们可以忽略?
硬件故障; 系统软件故障; 发现一个错误后,程序终止的情况。 -
每一个完整的程序应该能提供什么保证?
- 对于所有的合法输入,应输出正确结果
- 对于所有的非法输入,应输出错误信息。
- 举出3种可以减少程序错误,开发出符合要求的软件的方法
- 精心组织软件结构以减少错误
- 通过调试和测试,消除大部分程序错误
- 确定余下的错误是不重要的
-
为什么我们会讨厌调试?
因为在编程中,调试是最乏味、最费时间的工作。 -
什么是语法错误? 给出5个例子
不符合C++语言的语法规范的错误。
int s1 = area(7; // 缺少)
int s2 = area(7) // 缺少;
Int s3 = area(7); // Int 不是类型
int s4 = area('7); // 未结束的字符 (缺少')
- 什么是类型错误? 给出5个例子
不匹配的类型,如:函数返回值类型和接收函数返回值的变量的类型不匹配, 变量的类型和传入到函数参数时和参数类型不匹配。
int x0 = arena(7); // 未声明的函数
int x1 = area(7); // 参数数量不正确
int x2 = area("seven", 2); // 第一个参数类型错误
- 什么是链接器错误? 给出3个例子
函数或全局变量被用到,但是没有定义,或定义了多次,就是链接错误。包括了声明和定义的函数的类型、参数列表不匹配的情况。
int area(int length, int width);
int main()
{
int x = area(2, 3);
}
如果上述 area() 函数没有在 TU 中定义,会导致链接报错。
- 什么是逻辑错误? 给出3个例子
程序能运行,但是结果不正确。
- 可能程序作者所理解的程序逻辑是错误的
- 作者的逻辑正确,但是编写出的程序不是预期的
- 列出4种本章中讨论的可能导致程序错误的因素
- 对问题的理解有误
- 变量初始值错误
- 在疲劳情况下写代码
-
你如何能知道一个结果是合理的? 回答这类问题, 你会用到什么技术?
用到「估计」的技术。 -
对比一下由函数调用者来处理运行时错误和被调函数来处理运行时错误的异同
- 调用者处理错误: 每次调用都需要处理; 需要知晓被调用函数的实现细节; 如果被调用函数有修改,则每一处调用的错误处理都需要修改
- 被调用者处理错误: 只需要在一处处理错误
-
为什么说使用异常比返回一个“错误值”要好?
异常提供了一条可以把各种最好的错误处理方法组合在一起的途径, 可以简化错误处理。
处理错误和检测错误是分离的,对于使用了许多库的大程序来说很有帮助。 -
你应该如何测试一个输入操作是否成功?
很奇怪的问题。打印一下不就可以了吗?调试一下也可以。 -
描述一下抛出和捕获异常的过程。
在被调用的函数内部,根据一些条件判断, 决定 throw 一个 exception。
在调用者(直接或间接)的代码中,try catch 的方式捕获异常。 -
有一个名为 v 的向量, 为什么
v[v.size()]
会导致值范围错误? 这一调用会导致什么结果?
因为operator[]
只能范围[0, v.size()-1]
的索引。
访问v[v.size()]
会导致 out_of_range 的 exception (?? 在 macOS 上使用 AppleClang 15.0.0, 并不会报 exception)
void test_out_of_range()
{
std::vector<int> v;
int n ;
while (cin >> n) v.push_back(n);
for (int i = 0; i <= v.size(); i++)
{
std::cout << "v[" << i << "] == " << v[i] << std::endl;
}
}
1
2
3
4
v[0] == 1
v[1] == 2
v[2] == 3
v[3] == 4
v[4] == -22986736
- 描述一下前置条件和后置条件的定义; 并举个例子(不能用本章的 area() 函数),最好是一个带有循环的计算过程。
函数对于它的参数的要求,称为前置条件。
比如 sqrt() 函数,前置条件是参数大于等于0.
void test_sqrt()
{
double q = -2.4;
double r = sqrt(q);
std::cout << r << std::endl;
}
运行输出 nan
,而不是抛出异常。。
-
什么时候你不会测试前置条件?
函数没有输入的情况下,不用测试前置条件。 -
什么时候你不会测试后置条件?
个人认为测试后置条件,应该放在单元测试中,而不是被调用的函数内。 -
调试程序时的单步执行是指什么?
继续执行下一步。 -
在调试程序时,注释会有什么帮助?
没有什么直接关系。 -
测试与调试有什么不同?
测试,作者认为是大规模的use case的集合。个人认为这个说法比较不正确。
个人认为,测试就是「使用一组或多组输入数据,运行程序,并检查和预期结果是否相同」; 调试,则是实用单一的 use case 作为输入,单步或断点方式运行代码、查看变量取值、函数调用栈等。
附:习题答案
- 下面的程序是获得摄氏温度值并将其转化为绝对温度。但这些代码有很多错误,找到这些错误,指出并修改它们。
double ctok(double c) // converts Celsius to Kelvin
{
int k = c + 273.15;
return int
}
int main()
{
double c = 0; // declare input variable
cin >> d; // retireve temperature to input variable
double k = ctok("c"); // convert temperature
Cout << k << endl; // print out temperature
}
答案:
double ctok(double c)
{
return c + 273.15;
}
int main()
{
double c = 0;
cin >> c;
double k = ctok(c);
std::cout << k << std::endl;
return 0;
}
- 绝对零度是能够达到的最低温度,即 -273.15摄氏度或0K。即使上面的程序是正确的,当输入一个低于这个值的温度时,程序也应该输出错误结束。检查一下,当输入一个低于 -273.15摄氏度的数值时,主程序是否产生错误。
答案:
#include <iostream>
#include <stdexcept> // For using std::runtime_error
double ctok(double c) // converts Celsius to Kelvin
{
if (c < -273.15) {
throw std::runtime_error("Error: Temperature below absolute zero!");
}
double k = c + 273.15;
return k;
}
int main()
{
try {
double c = 0; // declare input variable
std::cin >> c; // retrieve temperature to input variable
double k = ctok(c); // convert temperature
std::cout << k << endl; // print out temperature
}
catch (runtime_error& e) {
std::cerr << e.what() << endl; // print error message
return 1; // return non-zero value to indicate error
}
return 0; // indicate success
}
- 重做上一个练习,但这次把错误处理放在 ctok() 中。
答案:
要将错误处理放在 ctok 函数中, 可以在函数内部处理异常并返回一个特殊值来表示错误,例如返回负数(因为开尔文温度不可能是负数)。然后在 main 函数中检查返回值并输出相应的错误信息。 代码:
#include <iostream>
double ctok(double c)
{
if (c < -273.15) {
std::cerr << "Error: Temperature below absolute zero!\n";
return -1;
}
return c + 273.15;
}
int main()
{
double c = 0;
std::cin >> c;
double k = ctok(c);
if (k == -1)
return 1;
std::cout << k << std::endl;
return 0;
}
- 给这个程序增加一些功能,使他也可以把绝对温度转化为摄氏温度。
#include <iostream>
#include <string>
double convert_temperature(double value, const std::string& type)
{
if (type == "c")
{
// k = c + 273.15
if (value < -273.15) {
throw std::runtime_error("Error: Temperature below absolute zero!");
}
return value + 273.15;
}
else if (type == "k")
{
if (value < 0) {
throw std::runtime_error("Error: Temperature below absolute zero!");
}
// c = k - 273.15
return value - 273.15;
}
else {
throw std::runtime_error("Error: invalid type. It should be one of: c, k");
}
}
int main()
{
try {
double c = -340;
double k = convert_temperature(c, "c");
printf("%f C is %f K\n", c, k);
double c2 = convert_temperature(k, "k");
printf("%f K is %f C\n", k, c2);
}
catch (std::runtime_error& e)
{
std::cout << e.what() << std::endl;
}
return 0;
}
- 编写一个程序,他可以实现绝对温度转化为华氏温度和华氏温度转换为绝对温度
f = 9/5 * c + 32
#include <iostream>
#include <stdexcept>
#include <string>
double convert_temperature(double value, const std::string& type)
{
// K to F
if (type == "k") {
if (value < 0) {
throw std::runtime_error("Error: Temperature below absolute zero!");
}
double c = value - 273.15;
double f = 9.0 / 5 * c + 32;
return f;
}
else if (type == "f")
{
// F to K
double c = (value - 32) * 5 / 9.0;
double k = c + 273.15;
return k;
}
else
{
const std::string msg = "Error: unsupported type: " + type + ", it should be one of: k, f";
throw std::runtime_error(msg);
}
}
int main()
{
try {
double k = 2;
double f = convert_temperature(k, "k");
printf("%f K is %f F\n", k, f);
double k2 = convert_temperature(f, "f");
printf("%f F is %f K\n", f, k2);
}
catch (std::runtime_error& e)
{
std::cout << e.what() << std::endl;
}
return 0;
}
- 一元二次方程的形式如下
ax2 + bx + c = 0
解这个方程,用到二次公式:
x
=
−
b
+
s
q
r
t
(
b
2
−
4
a
c
)
2
a
x = \frac{-b +_ sqrt(b^2-4ac)}{2a}
x=2a−b+sqrt(b2−4ac)
这里面有一个问题:如果 b^2-4ac 小于零的话,它将出错。 编写一个可以解一元二次方程的程序。 建立一个可以计算二次方程根的函数, 给定 a, b, c, 如果 b^2-4ac 小于0就抛出一个异常。 让程序的主函数调用这个函数, 如果有错误由主函数捕获异常。 当程序发现方程没有实根的时候,输出相应的信息。你如何确定程序的结果是合理的? 你能检验结果的正确性吗?
#include <stdexcept>
#include <iostream>
struct QuadraticSolution
{
float x1;
float x2;
};
QuadraticSolution solve_quadratic_function(float a, float b, float c)
{
float delta = b * b - 4 * a * c;
if (delta < 0)
{
throw std::runtime_error("Quadratic function failed to solve: b^2 - 4ac < 0");
}
float x1 = (-b + sqrt(delta)) / (2*a);
float x2 = (-b - sqrt(delta)) / (2*a);
QuadraticSolution s { x1, x2 };
return s;
}
int main()
{
float a = 1;
float b = 2;
float c = 1;
try
{
QuadraticSolution s = solve_quadratic_function(a, b, c);
std::cout << "x1 = " << s.x1 << ", x2 = " << s.x2 << std::endl;
}
catch(std::runtime_error& e)
{
std::cerr << e.what() << std::endl;
}
return 0;
}
- 编写一个程序,读取一个整数序列,并计算前N个整数之和。它首先询问N的值,然后读取N个值并存入一个 vector 中, 再计算前 N 个值之和。 例如:
请收入你希望求和的值的数量:
3
请输入一些整数(按'|'结束输入):
12 23 13 24 15|
前3个数(12 23 13)之和是48.
#include <iostream>
int main()
{
int n;
std::cin >> n;
float sum = 0;
float num;
while (std::cin >> num)
{
if (n >