当前位置: 首页 > news >正文

《C++程序设计原理与实践》笔记 第5章 错误

本章将讨论程序的正确性、错误和错误处理。

5.1 引言

在编写程序时,错误是不可避免的。而最后的程序必须是没有错误的,至少不存在不可接受的错误。

错误的分类有很多种,例如:

  • 编译时错误(compile-time errors):由编译器发现的错误,例如语法错误、类型错误
  • 链接时错误(link-time errors):链接器将对象文件链接为可执行程序时发现的错误
  • 运行时错误(run-time errors):程序运行中发现的错误,可进一步分为
    • 由计算机(硬件或操作系统)检测出的错误
    • 由库(例如标准库)检测出的错误
    • 由用户代码检测出的错误
  • 逻辑错误(logic errors):程序员在寻找错误结果的原因时发现的错误

除非特别说明,我们会假定你的程序:

  • 对于所有合法输入应输入正确结果
  • 对于所有非法输入应输出错误信息
  • 不需要关心硬件故障
  • 不需要关心系统软件故障
  • 发现错误后允许程序终止

有以下三种方法来编写可接受的软件:

  • 精心组织软件结构以减少错误
  • 通过调试和测试消除大部分错误
  • 确定剩下的错误是不重要的

上述任何一种方法都不能保证完全消除错误,我们必须同时使用上述三种方法。

5.2 错误的来源

  • 不够明确(poor specification):如果不具体明确程序应该做什么,就不可能充分检查所有“死角”,并确认所有可能的情况都被正确处理(即对于任意输入都能给出正确结果或者充分的错误信息)。
  • 不完备的程序(incomplete programs):在开发过程中,显然会有一些没有考虑到的情况,这是不可避免的。我们必须要达到的目标是知道何时能够处理所有情况。
  • 意外的参数(unexpected arguments):如果给函数传递了一个不能处理的参数,就会遇到问题,例如sqrt(-1.2)。5.5.3节将讨论这类问题。
  • 意外的输入(unexpected input):程序通常都会读取数据(来自键盘、文件、GUI、网络连接等)。程序会对输入做很多假设,例如用户会输入一个数字,如果用户输入的不是数字会怎样呢?5.6.3和10.6节将讨论这类问题。
  • 意外的状态(unexpected state):大部分程序都会保留很多数据(“状态”)以供系统的不同部分使用,例如4.6.3节读取温度程序中的vector。如果这些数据是不完整的或者错误的,程序的各个部分仍然应该正常运转。26.3.5节将讨论这类问题。
  • 逻辑错误(logical errors):即程序没有按照期望的方式运行,我们必须查找并修正这些问题。6.6和6.9节将给出这类问题的例子。

5.3 编译时错误

在编写程序时,编译器是检查错误的第一道防线。编译器发现的大部分错误都是低级错误。例如,考虑下面这个简单函数的一些调用:

int area(int length, int width);  // calculate area of a rectangle

5.3.1 语法错误

如果按照以下方式调用area()

int s1 = area(7, 4;    // error: ) missing
int s2 = area(7, 4)    // error: ; missing
Int s3 = area(7, 4);   // error: Int is not a type
int s4 = area('7, 4);  // error: non-terminated character (' missing)

上面每一行都有一个语法错误。即使是一个小错误,编译器也会报告很多繁杂信息,甚至会指向程序中的其他行。因此,如果你在编译器所指向的行中没有发现错误的话,还应该检查一下前几行是否有错误。

对于同样的代码,不同编译器可能会给出不同的错误信息。例如对于s3的声明,Visual Studio使用的MSVC编译器报错如下(与书中给出的错误信息比较接近,也比较难以理解):

MSVC编译器错误信息

g++编译器报错如下(相比MSVC的错误信息更加“智能”,编译器甚至猜到是将int错误拼写为Int):

g++编译器错误信息

实际上,(MSVC编译器给出的)这些令人费解的信息可以解释为“在s3前有一个语法错误,需要检查一下Int或s3的类型”,这样理解就不难发现问题了。

注:如果使用IDE写代码,在编译之前IDE就能发现一些错误。例如Visual Studio的提示为

Visual Studio编辑器提示

CLion的提示为

CLion编辑器提示

5.3.2 类型错误

一旦排除了语法错误,编译器就会开始检查类型错误:即声明的变量、函数等的类型,以及赋值给变量、传递给函数参数的值或表达式的类型之间不匹配。例如:

int x0 = arena(7);         // error: undeclared function
int x1 = area(7);          // error: wrong number of arguments
int x2 = area("seven",2);  // error: 1st argument has a wrong type
  • 对于arena(7),我们将area错写为arena,因此编译器会认为是调用函数arena。如果没有名为arena的函数,将会得到未定义函数的错误信息;如果确实有叫做arena的函数,并且该函数接受7作为参数,这将是一个更坏的情况:程序将会正确编译,但不会按照预期的方式运行(这是一个逻辑错误)。
  • 对于area(7),编译器将检测到参数个数错误。在C++中,函数调用必须提供正确的参数个数、类型和顺序。
  • 对于area("seven",2),第一个参数声明为int类型,但提供了string,编译器不会识别出"seven"表示的是数字7。

5.3.3 警告

你可能会希望编译器报告的一些错误并不是真正的错误,但是当你有了一定的编程经验后,你会希望编译器能够拒绝更多代码,而不是更少。考虑下面的例子:

int x4 = area(10,7);      // OK: but what is a rectangle with a width of minus 7?
int x5 = area(10.7,9.3);   // OK: but calls area(10,9)
char x6 = area(100,9999);  // OK: but truncates the result
  • 对于x4,我们没有从编译器得到错误信息。对于编译器来说,area(10,–7)是正确的:area()需要两个整数,也提供了两个整数。没有人规定这些参数必须是正数。
  • 对于x5,好的编译器应该给出警告信息:double型参数10.7和9.3将被截断为int型参数10和9。C++允许从doubleint的隐式转换,因此编译器不会拒绝该函数调用。
  • 对于x6int类型的返回值999900被赋给一个char变量,x6最有可能的结果是截断后的值-36。同样,好的编译器应该给出警告信息。

5.4 链接时错误

一个程序一般由几个独立编译的部分组成(例如一个.cpp源文件或.h头文件),称为翻译单元(translation unit)。程序中的每个函数在所有被使用的翻译单元中的声明类型必须严格一致,我们使用头文件来保证这一点,详见8.3节。并且,每个函数只能定义一次。如果违反任意一条规则,链接器将报错。例如:

// main.cpp
int area(int length, int width);
int main() {
    int x = area(2, 3);
}

如果直接将该源文件编译并链接为可执行文件,链接器将报错找不到area()的定义(编译器不会报错,因为编译器只关心函数调用与声明是否一致,不需要函数定义)。

参考:GCC编译器的使用方法

area()的定义与调用必须具有严格相同的类型(包括返回值类型和参数类型):

int area(int x, int y) { /* ... */ }             // “our” area()
double area(double x, double y) { /* ... */ }    // not “our” area()
int area(int x, int y, char unit) { /* ... */ }  // not “our” area()

函数的链接规则同样适用于程序的其他实体,例如变量和类型:同一名字的实体只能有一个定义,但可以有多个声明,并且所有声明的类型必须相同

5.5 运行时错误

5.5.1 调用者处理错误

5.5.2 被调用者处理错误

5.5.3 报告错误

5.6 异常

5.6.1 参数错误

5.6.2 范围错误

5.6.3 输入错误

5.6.4 截断错误

5.7 逻辑错误

5.8 估计

5.9 调试

5.9.1 实用调试建议

5.10 前置条件和后置条件

5.10.1 后置条件

5.11 测试

相关文章:

  • 静息态fMRI方法在脑动力学表征上的比较
  • LabVIEW自动整理程序框图
  • 拨测API接口+监控方案
  • 第一性原理详解
  • 信息化转型?数字化转型?企业到底该如何选择
  • ES6的Class继承--super关键字
  • Linux 简单命令 - find -grep命令深入学习
  • OP-TEE driver(五):libteec库中的接口在驱动中的实现
  • F-003 FPGA基础配置
  • java-php-python-ssm百分百教育集团教务管理系统设计计算机毕业设计
  • Apple M1 Macos 安装虚拟机软件UTM
  • 167.两数之和II-输入有序数组 || 双指针
  • aspnetcore6.0源代码编译调试
  • 【力扣刷题】Day04——链表专题
  • 云计算以及云计算安全相关的中文概述
  • Android组件 - 收藏集 - 掘金
  • Effective Java 笔记(一)
  • ES学习笔记(12)--Symbol
  • Intervention/image 图片处理扩展包的安装和使用
  • Java IO学习笔记一
  • Java读取Properties文件的六种方法
  • JAVA多线程机制解析-volatilesynchronized
  • jquery ajax学习笔记
  • 入口文件开始,分析Vue源码实现
  • 使用 QuickBI 搭建酷炫可视化分析
  • 通过几道题目学习二叉搜索树
  • 《天龙八部3D》Unity技术方案揭秘
  • raise 与 raise ... from 的区别
  • ​sqlite3 --- SQLite 数据库 DB-API 2.0 接口模块​
  • #Lua:Lua调用C++生成的DLL库
  • #图像处理
  • (补)B+树一些思想
  • (层次遍历)104. 二叉树的最大深度
  • (多级缓存)缓存同步
  • (附源码)springboot 智能停车场系统 毕业设计065415
  • (附源码)springboot“微印象”在线打印预约系统 毕业设计 061642
  • (附源码)springboot工单管理系统 毕业设计 964158
  • (一)SpringBoot3---尚硅谷总结
  • (原創) 如何使用ISO C++讀寫BMP圖檔? (C/C++) (Image Processing)
  • ***利用Ms05002溢出找“肉鸡
  • .NET Conf 2023 回顾 – 庆祝社区、创新和 .NET 8 的发布
  • .net on S60 ---- Net60 1.1发布 支持VS2008以及新的特性
  • .net 使用$.ajax实现从前台调用后台方法(包含静态方法和非静态方法调用)
  • .NET面试题解析(11)-SQL语言基础及数据库基本原理
  • .xml 下拉列表_RecyclerView嵌套recyclerview实现二级下拉列表,包含自定义IOS对话框...
  • /bin/rm: 参数列表过长"的解决办法
  • [ C++ ] STL_stack(栈)queue(队列)使用及其重要接口模拟实现
  • [16/N]论得趣
  • [2018/11/18] Java数据结构(2) 简单排序 冒泡排序 选择排序 插入排序
  • [20180312]进程管理其中的SQL Server进程占用内存远远大于SQL server内部统计出来的内存...
  • [AIGC] Kong:一个强大的 API 网关和服务平台
  • [Android] Android ActivityManager
  • [Angular] 笔记 16:模板驱动表单 - 选择框与选项
  • [AutoSar]BSW_Memory_Stack_004 创建一个简单NV block并调试
  • [Big Data - Kafka] kafka学习笔记:知识点整理