第2章 C++程序的组成及开发过程
本章包括
◆ C++程序的开发过程
◆ 编译器和编译过程
◆ C++程序的调试方法
◆ C++程序的组成部分
◆ C++程序的开发环境
◆ 开发多文件程序的过程
C++程序由数据(常量、变量,包括对象)和函数组成。本书将分别对各个组成部分进行介绍。但是,如何将各个部分组织成一个完整的程序,才是本书的重点。本章从一个简单的程序开始,讲述一个完整程序的组成以及C++程序的开发过程。
2.1 一般开发过程
C和C++程序的开发一般要经历4个过程:编辑、编译、链接、运行。
◆ 编辑:将程序的设计思想转换成代码。
◆ 编译:将代码转换成机器码。
◆ 链接:将程序编译后的各部分机器码和C++库函数等链接成一个可执行程序。
◆ 运行:在计算机上执行上述可执行程序。
如果源代码中没有任何错误,那么开发可以一次完成。在实际开发中,任何程序多多少少总会存在一些问题。这些问题包括语法、逻辑方面的错误,也包括效率方面的损失。如果是语法方面的错误,则在程序编译过程中就会出错,编译器会给出错误提示。开发者可以根据这个错误提示修改源代码,然后重新编译。
说明
链接的过程中也可能出错,其原因可能是程序中引用的某个函数或者某个变量不存在,或者程序中使用的静态链接库或动态链接库不存在。关于链接错误的讨论超出了本书的范畴,因此在这里不做更多的说明。
如果是逻辑方面的错误(比如程序的输出不是预期的结果),或者运行效率很低,则没有明显的错误提示。开发者要么根据自己的经验去纠正错误,要么利用调试器对程序进行调试,找到原因所在。因此,在大多数情况下,开发程序还包括一个调试的过程。
所谓调试,就是在程序运行过程中,监视相关源代码的执行情况。一般来讲,程序在编译之后会变成机器码,而机器码对于人来讲是非常难以阅读的。根据机器码找到运行过程中的错误非常困难,所以应当利用一个调试器,在程序运行过程中,监视可读性好的源代码的执行情况。
一旦错误被发现,就必须修正。这包括重新编辑源代码、重新编译、重新链接、重新运行程序以及重新调试。很多时候,这个过程会反复多次,直到最终程序运行无误,如图2-1所示。
图2-1 C++程序的一般开发过程
2.2 从简单程序开始
学习编程最好从简单的程序开始,虽然简单程序不能包含所有的开发技巧,但仍然可以从中学习到程序的基本组成和一般开发过程。这其中包括书写源代码、编译和链接。完成链接后,一个可执行程序就生成了,用户就可以在操作系统中运行它了(双击文件,或者在命令行窗口中输入命令)。下面就以一个简单的程序为例,来讲解开发程序的各个步骤。
2.2.1 书写源代码
C++程序是用类似自然语言的方式写成的文本,这个文本就是所谓的源代码。显然,要书写C++程序必须要有一个文本编辑器。读者可以根据自己的喜好选择编辑器,如Windows的记事本、DOS的Edit命令、EMACS和Vim等。当然如果读者已经熟悉了某种集成开发环境,如微软的Visual Studio系列,或者开源的Dev-C++,那就更加方便了。
当心
无论用什么编辑器书写源代码,最终保存的文件必须是简单的纯文本文件。而且文本中除了符合C++语法的部分外,不能有任何其他的成分,否则会导致编译错误,以致不能生成可执行文件。
源代码文件简称为源文件,其扩展名一般是.cpp。但是C++标准并没有对扩展名做出规定,不同的编译器可能会有不同的要求,甚至任何扩展名的文件都可以当做C++源文件。本书中如没有特别说明,则源文件的扩展名都是.cpp。
编写求两个整数和的程序,如示例代码2.1所示。
示例代码2.1
#include <cstdlib> // 包含头文件cstdlib #include <iostream> // 包含头文件iostream using namespace std; // 使用名称空间std /* add:求两个数之和的函数 输入:两个整数 返回:上述两个整数的和 */ int add( int x, int y ){ // 求两个整数之和的函数 int z = x + y; // 计算参数x与y的和,并保存到变量z中 return z; // 返回z的值 } int main(int argc, char *argv[]) // 主函数 { int a = add( 1, 2 ); // 调用求和函数求(1+2),并将结果保存到变量a中 cout<<"1 + 2 = "<<a<<endl; // 输出计算结果 system("PAUSE"); // 调用system函数,等待用户输入 return EXIT_SUCCESS; // 主函数返回 }
2.2.2 编译成目标文件
源代码书写完成,不代表一个程序开发结束,还需要对源代码进行编译和链接。编译的目的是将源代码转换成计算机可以辨认的二进制指令和数据,并保存到一个二进制文件中,即目标文件。目标文件通常以.obj作为扩展名。编译成的目标文件仍然不是可执行程序,还需要链接器的处理。
2.2.3 链接成可执行程序
链接的作用是将目标文件与一个或多个库处理成一个可执行程序。库是可链接文件的集合,一般编译器都会提供一些库,包括C++标准库,还有一些库可以购买得到,程序员也可以自己开发。综上所述,创建一个可执行程序的步骤如下:
step 1 书写源代码,保存到源代码文件中,扩展名是.cpp。
step 2 将源代码文件编译成目标文件,扩展名是.obj。
step 3 将目标文件和各种必需的库链接成可执行程序。
2.2.4 运行程序
链接完成后,可执行文件就生成了,如何运行就是操作系统的事情了。但此时程序员的任务并没有结束,还需要检验程序是否运行正常,并且符合设计的要求。通常来讲,很少有程序能够一次运行成功,总会有一些意想不到的错误出现。此时就需要程序员返回去检查源代码,重新编译、链接、试运行,而这个过程也经常会反复多次才能结束。
编译示例代码2.1,然后进行链接,完成后生成可执行程序。在操作系统中用命令行或者其他方法运行这个可执行程序,其运行结果如图2-2所示。
图2-2 程序的运行结果
2.3 C++程序的组成
上一节中讲的是一个非常简单的程序,但其中已经包含了完整程序的各个组成部分,下面将一一进行简单介绍。
1. 预处理指令
程序前两行行首的字符“#”是一个预处理标志。程序在编译之前,先运行预处理器。预处理器遍历整个源代码,找到由“#”开始的行,并执行其后的预处理指令。include是一条预处理指令,其含义是将后面的文件复制到当前源代码文件中。include指令后面的文件,在C和C++中习惯上称为头文件。文件名周围的尖括号“<>”表明这个文件是一个工程或标准头文件。预处理器首先从预定义的目录中开始查找,预定义的目录可通过环境变量和预处理器的命令行选项指定。如果文件名被双引号("")包围,则从当前目录开始查找。
提示
C++标准的头文件不带有扩展名。当然,C++同C一样,仍然可以指定头文件的扩展名为.h。有些编译器也允许使用别的扩展名。
2. 注释
在两个include指令之后,都有一行由双斜杠“//”引领的文本,这行文本称为“注释”。注释的内容不参与编译,因此可以用任何语言书写。注释从双斜杠开始,到本行结束。还有一种注释是由两组字符“/*”和“*/”包围的文本,这种注释同C语言中的注释一样,可以跨越多行。
3. 使用名称空间
程序的第4行(其上面为一空行)表明在当前文件中使用名称空间std。所谓名称空间就是名称的搜索范围,如第17行的cout,在示例代码中没有定义,编译器会到std名称空间中继续搜索。
4. 一般函数
在示例代码中,定义了add函数。程序中的函数同数学中的函数含义差不多,输入一些参数,函数就会返回一个值。不过在程序中,函数更加灵活,既可以不接受任何参数,也可以不返回任何值。而且在程序中定义函数的主要目的是实现某个功能,而不仅仅是返回一个值,这将在以后的章节中讨论。
add函数由4部分组成:返回类型(最左面的int)、函数名(add)、参数列表(int x, int y)和函数体。返回类型指的是函数返回值的类型。函数名和参数列表唯一标识了程序中的一个函数。函数体由一对花括号“{”和“}”包围起来,其中是实现函数功能的语句,即首先计算两个参数的和,然后返回这个和。return是函数返回指令,其后跟的值就是函数的返回值。
5. 语句
在C++程序中,最基本的组成单元是语句。一个语句由一个分号结束。在代码中,每个由分号结束的句子都是一条语句。如程序中的using语句,变量定义和初始化语句,函数返回语句,函数调用、定义变量并用函数调用的结果初始化变量的语句等。
6. 变量
示例程序中的“z”是一个变量。所谓变量就是由一个名称标识的一段内存,这段内存用来保存数据,并且数据的值是可变的。不同的变量,其内存大小也不一样,从一个字节到多个字节不等,取决于变量的类型。示例中,“z”的类型是int,即整型,代表类似-1,0,123这样的整数。
7. 主函数
每个程序只有一个主函数,即main函数。main函数是整个程序的入口函数,所有的C和C++程序都是从main函数开始运行的。同一般函数一样,main函数也由4部分组成:返回类型、函数名、参数列表和函数体。标准C++中对前3部分都有明确的规定,正如示例程序中第14行一样。值得注意的是,有的编译器要求main函数返回void型,这个已经不符合C++标准了。
8. 调用函数
在主函数的程序代码中,调用了前面定义的函数add,求1+2,并将结果保存到变量a中。调用函数时只要在函数名后面加上参数即可。参数用小括号“(”和“)”包围起来,紧接在函数名之后即可。“int a =”的作用是声明一个变量(保存数据的实体),并将函数调用的返回值赋给这个变量。int是变量的类型,这里应同函数的返回类型保持一致。
9. 使用cout对象
在示例代码中,使用cout对象输出了一条信息到命令行窗口中。所谓对象,简而言之,就是自定义数据类型的变量。而cout对象是C++标准库中提供的一个对象,用于输出数据。与之相对应,还有一个用于输入的对象cin。这些都将在后面的章节中详细介绍。为了使用这两个对象,必须包含头文件iostream。
紧接在cout对象后面的是输出重定向运算符“<<”,其后的所有内容都将输出到命令行窗口中,如“1 + 2 = ”。cout对象可以连续使用多个“<<”运算符,从而一次性输出多组数据,如后面的变量a和最后的endl,endl表示换行,即在输出变量a后另起一行。
10. 使用宏
在示例代码中,主函数的返回值“EXIT_SUCCESS”实际上是个宏。这个宏是在头文件cstdlib中定义的:
#define EXIT_SUCCESS 0
这是一个预处理指令,其含义是:在书写源代码时,可以用“EXIT_SUCCESS”代替“0”。当预处理器处理源文件时,会将文件中的所有“EXIT_SUCCESS”都替换成“0”。这样当开始编译时,第19行的实际代码是“return 0;”。
2.4 注释
注释是这样一种文本,其本身不参与编译,而只是帮助别人理解程序。如果程序比较简单,那么理解起来就很简单。如果程序比较复杂,别人在看程序时就会觉得难以理解。即便是程序员自己,在过了一段时间后,也有可能忘记当初为什么要写这些。
2.4.1 注释的类型
C++的注释有两种类型:双斜杠型(从“//”开始的一行)和斜杠星型(包含在“/*”和“*/”之间)。双斜杠型注释是C++引入的新的注释类型。这种注释从“//”开始,直到本行结束,双斜杠型注释不能跨行。斜杠星型注释则从“/*”开始,直到“*/”结束,可以跨越多行,这种注释是从C语言继承来的。
一般来讲,程序中使用双斜杠型注释就足够了。只有当注释的内容比较多时(10行以上),才有必要使用斜杠星型的注释。
斜杠星型的注释不能嵌套,例如下述的注释:
/*这种注释不能嵌套, /*注释在这里结束 */ */
注释在第3行就会结束。因为由“/*”开始的注释,在遇到的第一个“*/”处结束。第4行的“*/”由于没有对应的“/*”,所以会被当做错误的语法,将导致编译错误。
2.4.2 使用注释的注意事项
用户在程序中使用注释的时候,需要注意下面的内容。
◆ 简单的代码不必注释:如果代码足够简单,那么代码就可以说明问题,不必再加上多余的注释。
◆ 注释要说明设计的原因,而不是描述代码:阅读代码的人更加关心的是程序为什么要这么设计,而不是代码的功能。
◆ 修改代码时,记得修改注释:很多人在修改代码时,往往会忘记修改注释,这样注释就成了过时的,甚至是错误的注释,当别人阅读这样的代码时,很容易被误导。
2.5 标准IO对象
C++标准库提供了两个对象,分别用于数据的输入(Input)和输出(Output)。输出对象cout在前面已经介绍过了,现在来看一下输入对象cin。同cout一样,要使用cin对象,必须先包含头文件iostream:
#include <iostream>
使用cin对象的语法如下:
cin>>var;
其含义是从键盘输入一个数据,并保存到变量var中。
说明
C++的标准IO对象cin和cout同C语言的scanf函数和printf函数功能类似,但其功能更加强大。在使用C++中的IO对象时不需要指定数据的类型,而使用C语言的IO函数必须指定。
下面给出一个程序提示用户输入整数,并将其保存到一个变量中,然后再输出,如示例代码2.2所示。
示例代码2.2
#include <cstdlib> // 包含头文件cstdlib #include <iostream> // 包含头文件iostream using namespace std; // 使用名称空间std int main(int argc, char *argv[]) // 主函数 { int var = 0; // 保存输入数据的变量 cout<<"请输入一个整数:"; // 提示用户输入 cin>>var; // 将用户输入的数据保存到变量var中 cout<<"刚才输入的数是:"<<var<<endl; // 输出用户刚才输入的数据 cout<<endl; // 输出一个空行 system("PAUSE"); // 调用函数system,等待用户反应 return EXIT_SUCCESS; // 主函数返回 }
编译并链接上述源代码,运行生成的可执行程序,其结果如图2-3所示。
图2-3 输入和输出数据结果
在上述源代码中,保存输入数据的变量是int型的,即整型的。如果用户输入的数据不是整型的,而是别的类型的,如abc,则不会改变变量的值。
2.6 使用名称空间
名称空间(namespace)是包含各种名称(变量、函数、类等的标识符)的域。使用名称空间的主要目的是避免名称冲突。随着应用程序规模越来越大,各种变量、函数、类等的标识符也越来越多,添加一个新的标识符很容易与已经存在的标识符冲突。如果使用名称空间,则某个标识符就被限制在特定的名称空间中,从而减少了发生冲突的可能性。
如果要使用名称空间中的变量、函数、类等,需要指明其所在的名称空间。指明名称空间的方法有两种:一种是在标识符前面加上名称空间名和域运算符“::”,例如std::cin(std是标准C++名称空间);另外一种是在使用标识符之前,使用using namespace加上名称空间名,这样以后用该名称空间中的标识符时就不用再指明了。
标准C++有一个std名称空间,所有C++标准库中的标识符都在这个名称空间中。例如标准IO对象cin和cout就定义在这个名称空间中:
namespace std{ ⋯⋯ ostream cin; ostream cout; ⋯⋯ }
前面介绍的简单程序中就使用了标准名称空间std:
using namespace std;
如果不加入上述语句,在使用cout对象时,必须这样书写:
std::cout<<⋯⋯
其中“std::”的含义是在名称空间std中查找名称cout。如果程序比较短小,像这样使用cout对象也没有什么不可以的。但是一旦程序比较长,而且多次用到cout对象,如果每次都要在前面加上“std::”,显然比较烦琐。
2.7 编译器和编译过程
C++编译器是一种系统软件,用来将源代码转换成机器码。现在比较著名的编译器有Microsoft的CL、Borland的BCC、Intel的ICC,以及开源的、跨平台的GCC。虽然C++标准是唯一的,但各个编译器的实现未必一致。尽管如此,各种编译器的组成以及编译过程还是基本一致的。
说明
因为不同种类计算机的硬件平台未必相同,所以一种编译器一般是针对一种或者几种平台设计的。为某种平台编译的程序,一般不能在另外一种平台上运行,例如针对32位计算机编译的程序,一般不能在64位计算机上运行。
一般来讲,C++编译器由预处理器、词法分析器、语法分析器、语义分析器、代码优化器以及机器码生成器组成,其组成结构如图2-4所示。
图2-4 编译器的组成结构
预处理器的作用是根据源代码中的预处理指令,对程序文本进行处理。词法分析器、语法分析器和语义分析器合称编译器前端。其中词法分析器的作用是从左至右逐个字符地对源程序进行扫描,用其中由字符组成的单词产生一个个的单词符号,把作为字符串的源程序改造成为单词符号串的中间程序。例如对于“aVar = bVar + cVar;”语句,词法分析的目标就是要找出aVar,bVar,cVar,以及“=”和“+”这些符号,并结合符号表对其进行管理,上面语句进行词法分析之后的符号表如图2-5所示。
图2-5 词法分析之后的符号表
语法分析器以上述中间程序作为输入,分析其中的单词符号串是否符合语法规则。语法分析器首先判断各种单词符号所属的类型,如表达式、赋值、循环等。然后判断这些符号是否符合语法规则,再按照语法规则将每条语句构造成语法树或其他结构。上面例句的语法树如图2-6所示。
图2-6 语法树
语义分析器根据语义规则对语法树中的语法单元进行静态语义检查,如类型检查和转换等,其目的在于保证语法正确的结构在语义上也是合法的。例如,对于上图的语法树,右侧的子树表现出来的语法结构是两个操作数相加,语义分析器的任务就是判断这两个操作数能否相加,需不需要进行类型转换等。
说明
语法分析和语义分析通常是交互进行的。每完成一步语法分析,就进行语义分析。如果需要进行类型转换等操作,就在上述语法树中插入一些操作,构造新的语法树,然后再进行语义分析。在这个过程中,如果有不符合语法、语义的代码,编译器就会报错。
代码优化器和机器码生成器合称编译器后端。优化是编译器的一个重要组成部分。由于编译器将源程序翻译成中间代码的工作是机械的、按固定模式进行的,因此,生成的中间代码往往在时间和空间上有很大浪费。当需要生成高效目标代码时,就必须进行优化。目标代码的优化主要包括以下三个方面:
◆ 生成较短的目标代码。
◆ 充分利用计算机中的寄存器,减少目标代码访问存储单元的次数。
◆ 充分利用计算机指令系统的特点,以提高目标代码的质量。
目标代码生成器把语法分析后或优化后的中间代码变换成目标代码。目标代码有以下三种形式:
◆ 可以立即执行的机器语言代码。
◆ 待装配的机器语言模块,由链接程序将其和其他模块链接起来,转换成能执行的机器语言代码。
◆ 汇编语言代码,必须经过汇编程序汇编后,才能成为可执行的机器语言代码。
2.8 选择集成开发环境
在程序开发过程的各个阶段中要用不同的工具,例如编写源代码时要用文本编辑器、编译时要用编译器、链接时要用链接器。早期的程序员在开发时经常在上述几种工具之间来回切换,非常烦琐,现在开发程序则不必这么麻烦,利用集成开发环境即可完成所有工作,极大地提高了编程的效率。
集成开发环境(Integrated Developing Environment,简称IDE)是一种应用程序。该类程序提供图形化的用户界面工具,并在其中集成文本编辑器、编译器、链接器以及调试器,方便开发者查找程序中的错误。通过集成开发环境,开发者可以进行代码编写、软件分析、编译链接以及调试等工作,不必在各种工具之间进行切换,使用起来非常方便。在C++程序开发中,有很多IDE可以使用,例如微软的Visual Studio系列、Borland的C++ Builder以及开源的Dev-C++等。
2.9 Dev-C++简介
Dev-C++是免费的开源集成开发环境。在本书成稿时,Dev-C++的最新版本是4.9.9.2,本书将以该版本为例讲解Dev-C++的安装和使用。相比其他IDE软件,Dev-C++的一个突出优点就是免费,这对于初学编程的人来讲非常实用。另外,Dev-C++使用GCC编译器,比较符合C++标准,有利于初学者掌握正确的概念。
Dev-C++包括多页面窗口、工程编辑器以及调试器等,提供高亮度语法显示,方便开发者书写和阅读程序。Dev-C++有完善的调试功能,方便查找程序中的错误。利用Dev-C++来学习C或者C++是个不错的选择。
2.9.1 安装
下面将详细讲解Dev-C++的安装过程。
step 1 双击Dev-C++的安装程序,选择安装语言,如图2-7所示。
图2-7 选择安装语言
step 2 选择要安装的组件,如图2-8所示。如果选择了“Associate C and C++ files to Dev-C++”组件,可以将Dev-C++与C/C++程序源文件关联起来,包括*.h,*.c和*.cpp文件。安装完成后,通过双击上述类型文件的图标,就可以打开Dev-C++编辑文件。
图2-8 选择要安装的组件
说明
这里省略了一些安装步骤,只显示关键部分。另外,Dev-C++的各个版本在安装时具体过程也可能略有不同,以实际过程为准。
step 3 在安装向导对话框中选择程序的安装路径,然后单击“下一步”按钮,开始安装,如图2-9所示。
图2-9 选择安装路径
step 4 安装完成后,在“环境选项”对话框中可以设置工作环境,如选择界面的语言,如图2-10所示。
图2-10 选择界面语言
2.9.2 建立工程
在大多数集成开发环境里开发程序的第一步就是要建立工程,Dev-C++也是如此。所谓工程就是所有软件相关文件的集合。一个工程还可以保存开发者的一些设置,如编译选项、调试信息等。下面详细讲解建立工程的步骤。
提示
一个工程可以用来维护多个源文件之间的关系,并可以编译成一个可执行程序或其他类型的软件,如动态链接库等。
step 1 创建工程。选择“文件”→“新建”→“工程”命令,创建新的工程,如图2-11所示。
图2-11 建立工程
step 2 选择工程类型。在“新工程”对话框中,选择工程类型,如图2-12所示。
图2-12 选择工程类型
step 3 单击对话框中的“确定”按钮,在弹出的对话框中选择工程的保存路径,如图2-13所示。
图2-13 选择保存工程的位置
step 4 工程建立之后,Dev-C++会自动为工程加入源文件main.cpp,并且该源文件中已经写好主函数,不过该文件还没有保存。开发者单击工具栏中的“保存”按钮,或开始编译,Dev-C++会弹出一个保存对话框,如图2-14所示。
图2-14 保存文件
2.9.3 编译和运行
在工程中加入文件之后就可以开始编程了。如果要进行多文件编程,可以继续向工程中添加文件,可以通过单击新建源代码窗口工具栏中的按钮实现,如图2-15所示。
图2-15 加入新文件
加入的新文件也需要保存,其过程同保存工程默认的文件一样。添加新文件并书写代码之后,就可以开始编译了。其过程很简单,只需选择相应的菜单命令或者单击工具栏中的按钮即可,如图2-16所示。
图2-16 编译、运行以及调试按钮
工具栏中的“编译并运行”按钮将“编译”和“运行”两个动作合为一体了,开发者也可以通过直接按快捷键“F9”来激活这个动作。如图2-16所示例程的运行结果如图2-17所示。
图2-17 运行结果
2.10 程序的调试
如果代码能够通过编译和链接,并且生成了可执行程序,那么它就可以在计算机上运行了。但是程序的运行并不一定是一帆风顺的,很有可能存在逻辑错误或者效率方面的问题。此时就应当通过调试来查找导致错误的原因。
提示
调试的英文为Debug。Bug是虫子、害虫的意思,这里是指计算机程序中的错误。Debug是消灭害虫的意思,而在计算机中就是消灭错误的意思。当然,消灭错误首先要找到错误的原因,然后才能进行修正。如果修正后还有错误,则继续调试。
2.10.1 调试的基本过程
在程序的使用过程中,一旦有错误报告,开发人员就要对程序进行调试,并最终消灭错误。一个完整的调试过程一般要经历4个步骤。
step 1 错误(Bug)重现。
step 2 查找原因。
step 3 修正错误。
step 4 验证。
如果最后一步验证不成功,则从第二步重新开始,直至最后消灭错误。调试过程如图2-18所示。
图2-18 调试过程
错误重现,指的是根据用户或者测试人员的描述,设定条件,重复导致错误的步骤,使得错误出现在被调试的系统中,以方便开发人员进行调试。查找原因,即综合利用各种调试工具,使用各种调试手段,寻找导致错误的根源。通常测试人员报告的是软件错误所表现出的外在症状,如界面或执行结果中所表现出的异常,或者与软件需求和功能规约不符的地方。而这些外在症状总是由一个或多个内在因素所导致的,要么是由于代码的缺陷造成的,要么是某些设置造成的。
说明
查找原因是调试过程中最关键的步骤,也是最耗时的步骤。因此,平常所说的调试,如不加以说明,一般指的就是查找原因的过程。
修正错误,即针对上述步骤查找到的错误原因,修改代码或者设置。代码修改之后,对于编译型语言或者混合型语言,还要对代码进行编译,并进行必要的配置。验证,即按照用户或者测试人员的描述,重复导致错误的步骤,检验一下修正之后错误是否还在。如果通过验证,则开发人员应当报告问题已解决,并说明解决方案。如果没有通过验证,则应当继续查找原因,修正错误,并进行验证。这个过程有时要重复进行,直至问题解决。
2.10.2 调试手段
这里所说的调试手段指的是查找原因的各种方法,包括设置断点、运行到断点、单步执行、查看变量等。通过IDE工具中的调试器可以在程序运行过程中,在源代码中要考察的位置设置断点,并通过调试命令直接运行到该处,然后通过单步执行和查看变量的方式监视每条语句的执行情况。
图2-19 Dev-C++的“调试”菜单
下面将通过Dev-C++这个IDE软件讲解上述各种调试方法。在Dev-C++的菜单栏中有一个“调试”菜单,其中集成了各种调试命令,如图2-19所示。同时,也可以利用窗口下面的调试工具栏进行操作。
各种调试命令的功能如表2-1所示。
表2-1 各种调试命令的功能
在程序以调试模式开始运行之前,首先应当设置断点。断点的功能是停止继续执行其所在处的语句以及后面的语句,但并不是结束程序,而是等待调试者的进一步操作。调试者可以通过“下一步”命令逐条运行语句,或者查看程序的当前状态。
提示
在开始调试之前,在哪里设置断点是调试人员首先应当考虑的问题。通常,根据用户或测试人员对错误的描述,可以估计出大概的出错地方,调试的断点就应当设置在相应的地方。
当程序暂停执行之后,就可以通过“添加查看”、“查看变量”等命令检查程序的当前状态。如果检查的结果与期望的值不同,那么在此之前运行过的语句很可能就是导致错误的原因。通过进一步分析,或者设置新的断点重新调试,不断尝试,最终可以找到原因所在。
2.10.3 调试实例
下面的程序是一个有问题的程序。其原本的目的是求两个数之和,并输出结果值。但从实际运行的情况来看,其结果值存在误差。本节就以此程序为例,讲解程序的调试过程。
#include <cstdlib> #include <iostream> using namespace std; // 使用名称空间std int Add(int a, int b) // 求和函数 { return a - b; } int main(int argc, char *argv[]) // 主函数 { int x = Add( 1, 2 ); // 调用函数Add,求1和2的和 cout<< x <<endl; // 输出结果值 system("PAUSE"); // 暂停程序的执行 return EXIT_SUCCESS; }
下面详细讲解调试的步骤。
step 1 既然是求和的结果不对,那么一定是求和函数Add的问题。所以,应当在调用Add函数的地方设置断点,如图2-20所示。
图2-20 设置断点
step 2 断点设置好之后,就应当以调试的方式运行程序。程序从主函数开始执行,直至运行到第一个断点处停止执行,如图2-21所示。
图2-21 执行到断点处
step 3 断点由红色变成蓝色,表示程序暂停在该断点处。该处的语句是调用函数Add,通过“单步进入”命令进入到该函数内部,以监视函数Add的运行情况,如图2-22所示。
图2-22 单步进入函数
当心
此时如果使用“下一步”命令,则会执行第13行语句,而不是进入函数内部。
step 4 程序进入到函数Add内部之后,暂停在函数的第一条语句处。此时可以通过“添加查看”命令来查看程序的运行状态,如图2-23所示。
图2-23 查看参数的值
step 5 有经验的调试人员可以很容易地察觉程序的错误,即本来应当是“return a + b; ”的语句错写成了“return a - b; ”。因此,只要将其中的减号改成加号即可。不过,从调试的角度来看,还可以通过查看表达式来找到错误的原因。在第7行,函数Add返回表达式“a-b”。为了查看该表达式的结果,可以通过“添加查看”命令,来查看该表达式的值,如图2-24所示。
图2-24 查看表达式的值
step 6通过执行几次“下一步”命令,一直运行到第14行。此时可以查看变量x的值。在查看值的窗口中,可以通过右键快捷菜单选择“修改数据”命令,手工修改变量的值,如图2-25所示。
图2-25 修改变量的值
step 1 将上述x变量的值修改为3,然后通过“跳过”命令运行完当前的主函数,其输出结果如图2-26所示。
图2-26 输出结果
至此,调试结束。开发人员应当根据上述查看结果,修改程序,并重新运行程序进行验证。
2.11 综合实例
通常在一个实际的工程中都会有多个源文件,本节就来练习建立一个多文件的程序。本实例建立一个具有两个源文件的程序,并在主程序中调用另外一个源文件中的函数,该函数计算两个参数之和,并返回结果值。建立C++工程如图2-27所示。
图2-27 多文件工程
程序如示例代码2.3所示。
示例代码2.3
////////////////////////////////////////////////////////////////////////// // main.cpp #include <cstdlib> #include <iostream> using namespace std; // 使用名称空间 int add(int x, int y); // 函数声明 int main(int argc, char *argv[]) // 主函数 { cout<<"——多文件程序——"<<endl; cout<<"3 + 4 = "; // 输出提示信息 cout<< add( 3,4 ) << endl; // 输出函数调用结果,并换行 system("PAUSE"); // 等待用户反应 return EXIT_SUCCESS; // 主函数返回 } ////////////////////////////////////////////////////////////////////////// // Method.cpp /* add:求两个数之和的函数 输入:两个整数 返回:上述两个整数的和 */ int add(int x, int y) // 求两个整数之和的函数 { int z = x + y; // 计算参数x与y的和,并保存到变量z中 return z; // 返回z的值 }
读者可建立一个控制台工程以及相应的源文件,并将上述代码复制到文件中,然后编译并运行,结果如图2-28所示。
图2-28 多文件程序的运行结果
2.12 小结
本章主要介绍了C++程序的必要组成部分,以及开发一个程序的主要步骤,并简单介绍了一下开发C++程序的集成开发环境。另外,本章最后按照实际的开发过程,列举了一个具有多个源文件的程序实例。在后面的章节中,还将进一步详细介绍C++程序的各个组成部分。