system(\"PAUSE\"); // 调用函数system,等待用户反应 return EXIT_SUCCESS; // 主函数返回 }编译并链接上述源代码,运行生成的可执行程序,其结果如图2.3所示。
图2.3 输入和输出数据结果
在上述源代码中,保存输入数据的变量是int型的,即整型的。如果用户输入的数据不是整型的,而是别的类型的,如abc,则不会改变变量的值。
2.6 使用名称空间
名称空间(namespace)是包含各种名称(变量、函数、类等的标识符)的域。使用名称空间
14
第 章 C++程序的组成及开发过程
的主要目的是避免名称冲突。随着应用程序规模越来越大,各种变量、函数、类等的标识符也越来越多,添加一个新的标识符很容易与已经存在的标识符冲突。如果使用名称空间,则某个标识符就被限制在特定的名称空间中,从而减少了发生冲突的可能性。
如果要使用名称空间中的变量、函数、类等,需要指明其所在的名称空间。指明名称空间的方法有两种:一种是在标识符前面加上名称空间名和域运算符“::”,例如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所示。
15
C++
图2.4 编译器的组成结构
预处理器的作用是根据源代码中的预处理指令,对程序文本进行处理。词法分析器、语法分析器和语义分析器合称编译器前端。其中词法分析器的作用是从左至右逐个字符地对源程序进行扫描,用其中由字符组成的单词产生一个个的单词符号,把作为字符串的源程序改造成为单词符号串的中间程序。例如对于“aVar = bVar + cVar;”语句,词法分析的目标就是要找出aVar,bVar,cVar,以及“=”和“+”这些符号,并结合符号表对其进行管理,上面语句进行词法分析之后的符号表如图2.5所示。
图2.5 词法分析之后的符号表
语法分析器以上述中间程序作为输入,分析其中的单词符号串是否符合语法规则。语法分析器首先判断各种单词符号所属的类型,如表达式、赋值、循环等。然后判断这些符号是否符合语法规则,再按照语法规则将每条语句构造成语法树或其他结构。上面例句的语法树如图2.6所示。
图2.6 语法树
语义分析器根据语义规则对语法树中的语法单元进行静态语义检查,如类型检查和转换等,其目的在于保证语法正确的结构在语义上也是合法的。例如,对于上图的语法树,右侧的子树表现出
16
第 章 C++程序的组成及开发过程
来的语法结构是两个操作数相加,语义分析器的任务就是判断这两个操作数能否相加,需不需要进行类型转换等。
语法分析和语义分析通常是交互进行的。每完成一步语法分析,就进行语义分析。如果需要进行类型转换等操作,就在上述语法树中插入一些操作,构造新的语法树,然后再进行语义分析。在这个过程中,如果有不符合语法、语义的代码,编译器就会报错。
代码优化器和机器码生成器合称编译器后端。优化是编译器的一个重要组成部分。由于编译器将源程序翻译成中间代码的工作是机械的、按固定模式进行的,因此,生成的中间代码往往在时间和空间上有很大浪费。当需要生成高效目标代码时,就必须进行优化。目标代码的优化主要包括以下三个方面:
生成较短的目标代码。
充分利用计算机中的寄存器,减少目标代码访问存储单元的次数。 充分利用计算机指令系统的特点,以提高目标代码的质量。
目标代码生成器把语法分析后或优化后的中间代码变换成目标代码。目标代码有以下三种形式:
可以立即执行的机器语言代码。
待装配的机器语言模块,由链接程序将其和其他模块链接起来,转换成能执行的机器语言
代码。
汇编语言代码,必须经过汇编程序汇编后,才能成为可执行的机器语言代码。
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++标准,有利于初学者掌握正确的概念。
17
C++
Dev-C++包括多页面窗口、工程编辑器以及调试器等,提供高亮度语法显示,方便开发者书写和阅读程序。Dev-C++有完善的调试功能,方便查找程序中的错误。利用Dev-C++来学习C或者C++是个不错的选择。
2.9.1 安装
下面将详细讲解Dev-C++的安装过程。
双击Dev-C++的安装程序,选择安装语言,如图2.7所示。
选择要安装的组件,如图2.8所示。如果选择了“Associate C and C++ files to Dev-C++”组件,可以将Dev-C++与C/C++程序源文件关联起来,包括*.h,*.c和*.cpp文件。安装完成后,通过双击上述类型文件的图标,就可以打开Dev-C++编辑文件。
图2.7 选择安装语言 图2.8 选择要安装的组件
这里省略了一些安装步骤,只显示关键部分。另外,Dev-C++的各个版本在安装时具体过程也可能略有不同,以实际过程为准。
在安装向导对话框中选择程序的安装路径,然后单击“下一步”按钮,开始安装,如图2.9所示。
安装完成后,在“环境选项”对话框中可以设置工作环境,如选择界面的语言,如图2.10所示。
图2.9 选择安装路径 图2.10 选择界面语言
18
第 章 C++程序的组成及开发过程
2.9.2 建立工程
在大多数集成开发环境里开发程序的第一步就是要建立工程,Dev-C++也是如此。所谓工程就是所有软件相关文件的集合。一个工程还可以保存开发者的一些设置,如编译选项、调试信息等。下面详细讲解建立工程的步骤。
一个工程可以用来维护多个源文件之间的关系,并可以编译成一个可执行程序或其他类型的软件,如动态链接库等。
创建工程。选择“文件”→“新建”→“工程”命令,创建新的工程,如图2.11所示。
选择工程类型。在“新工程”对话框中,选择工程类型,如图2.12所示。
图2.11 建立工程 图2.12 选择工程类型
单击对话框中的“确定”按钮,在弹出的对话框中选择工程的保存路径,如图2.13所示。
工程建立之后,Dev-C++会自动为工程加入源文件main.cpp,并且该源文件中已经写好主函数,不过该文件还没有保存。开发者单击工具栏中的“保存”按钮,或开始编译,Dev-C++会弹出一个保存对话框,如图2.14所示。
图2.13 选择保存工程的位置 图2.14 保存文件
2.9.3 编译和运行
在工程中加入文件之后就可以开始编程了。如果要进行多文件编程,可以继续向工程中添加文件,可以通过单击新建源代码窗口工具栏中的按钮实现,如图2.15所示。
19
C++
加入的新文件也需要保存,其过程同保存工程默认的文件一样。添加新文件并书写代码之后,就可以开始编译了。其过程很简单,只需选择相应的菜单命令或者单击工具栏中的按钮即可,如图2.16所示。
图2.15 加入新文件 图2.16 编译、运行以及调试按钮
工具栏中的“编译并运行”按钮将“编译”和“运行”两个动作合为一体了,开发者也可以通过直接按快捷键“F9”来激活这个动作。如图2.16所示例程的运行结果如图2.17所示。
图2.17 运行结果
2.10 程序的调试
如果代码能够通过编译和链接,并且生成了可执行程序,那么它就可以在计算机上运行了。但是程序的运行并不一定是一帆风顺的,很有可能存在逻辑错误或者效率方面的问题。此时就应当通过调试来查找导致错误的原因。
调试的英文为Debug。Bug是虫子、害虫的意思,这里是指计算机程序中的错误。Debug是消灭害虫的意思,而在计算机中就是消灭错误的意思。当然,消灭错误首先要找到错误的原因,然后才能进行修正。如果修正后还有错误,则继续调试。
2.10.1 调试的基本过程
在程序的使用过程中,一旦有错误报告,开发人员就要对程序进行调试,并最终消灭错误。一个完整的调试过程一般要经历4个步骤。
错误(Bug)重现。
查找原因。 修正错误。 验证。
如果最后一步验证不成功,则从第二步重新开始,直至最后消灭错误。调试过程如图2.18所示。
20
第 章 C++程序的组成及开发过程
图2.18 调试过程
错误重现,指的是根据用户或者测试人员的描述,设定条件,重复导致错误的步骤,使得错误出现在被调试的系统中,以方便开发人员进行调试。查找原因,即综合利用各种调试工具,使用各种调试手段,寻找导致错误的根源。通常测试人员报告的是软件错误所表现出的外在症状,如界面或执行结果中所表现出的异常,或者与软件需求和功能规约不符的地方。而这些外在症状总是由一个或多个内在因素所导致的,要么是由于代码的缺陷造成的,要么是某些设置造成的。
查找原因是调试过程中最关键的步骤,也是最耗时的步骤。因此,平常所说的调试,如不加以说明,一般指的就是查找原因的过程。
修正错误,即针对上述步骤查找到的错误原因,修改代码或者设置。代码修改之后,对于编译型语言或者混合型语言,还要对代码进行编译,并进行必要的配置。验证,即按照用户或者测试人员的描述,重复导致错误的步骤,检验一下修正之后错误是否还在。如果通过验证,则开发人员应当报告问题已解决,并说明解决方案。如果没有通过验证,则应当继续查找原因,修正错误,并进行验证。这个过程有时要重复进行,直至问题解决。
2.10.2 调试手段
这里所说的调试手段指的是查找原因的各种方法,包括设置断点、运行到断点、单步执行、查看变量等。通过IDE工具中的调试器可以在程序运行过程中,在源代码中要考察的位置设置断点,并通过调试命令直接运行到该处,然后通过单步执行和查看变量的方式监视每条语句的执行情况。
下面将通过Dev-C++这个IDE软件讲解上述各种调试方法。在Dev-C++的菜单栏中有一个“调试”菜单,其中集成了各种调试命令,如图2.19所示。同时,也可以利用窗口下面的调试工具栏进行操作。
图2.19 Dev-C++的“调试”菜单
21
C++
各种调试命令的功能如表2.1所示。
表2.1 各种调试命令的功能
调试命令 调试 停止执行 参数 切换断点 下一步 单步进入 跳过 运行到光标 添加查看 查看变量 查看CPU窗口
快捷方式 F8
Ctrl+Alt+F2 Ctrl+F5 F7 Shift+F7 Ctrl+F7 Shift+F4 F4 Ctrl+W
功能
以调试方式启动程序,运行到第一个断点处或需要用户输入的地方 停止调试
传递给程序主函数的参数 在当前光标所在行设置或取消断点 仅执行下一条语句
进入当前函数,并停止在函数的第一条语句之前
跳出当前函数,并执行后面的语句,直至下一个断点或程序结束 运行程序,直至当前光标所在位置的语句
查看表达式的值,该表达式不一定是程序中的代码 查看当前所选变量的值
查看CPU中各个寄存器的值以及当前程序的汇编代码
在程序以调试模式开始运行之前,首先应当设置断点。断点的功能是停止继续执行其所在处的语句以及后面的语句,但并不是结束程序,而是等待调试者的进一步操作。调试者可以通过“下一步”命令逐条运行语句,或者查看程序的当前状态。
在开始调试之前,在哪里设置断点是调试人员首先应当考虑的问题。通常,根据用户或测试人员对错误的描述,可以估计出大概的出错地方,调试的断点就应当设置在相应的地方。
当程序暂停执行之后,就可以通过“添加查看”、“查看变量”等命令检查程序的当前状态。如果检查的结果与期望的值不同,那么在此之前运行过的语句很可能就是导致错误的原因。通过进一步分析,或者设置新的断点重新调试,不断尝试,最终可以找到原因所在。
2.10.3 调试实例
下面的程序是一个有问题的程序。其原本的目的是求两个数之和,并输出结果值。但从实际运行的情况来看,其结果值存在误差。本节就以此程序为例,讲解程序的调试过程。
#include #include using namespace std; // 使用名称空间stdint Add(int a, int b) // 求和函数 {
return a - b; }
int main(int argc, char *argv[]) // 主函数
22
{ int x = Add( 1, 2 ); cout<< x <既然是求和的结果不对,那么一定是求和函数Add的问题。所以,应当在调用Add函数的地方设置断点,如图2.20所示。图2.20 设置断点
断点设置好之后,就应当以调试的方式运行程序。程序从主函数开始执行,直至运行到第一个断点处停止执行,如图2.21所示。
23
C++
图2.21 执行到断点处
断点由红色变成蓝色,表示程序暂停在该断点处。该处的语句是调用函数Add,通过“单步进入”命令进入到该函数内部,以监视函数Add的运行情况,如图2.22所示。
图2.22 单步进入函数
此时如果使用“下一步”命令,则会执行第13行语句,而不是进入函数内部。 程序进入到函数Add内部之后,暂停在函数的第一条语句处。此时可以通过“添加查看”命令来查看程序的运行状态,如图2.23所示。
图2.23 查看参数的值
有经验的调试人员可以很容易地察觉程序的错误,即本来应当是“return a + b;”的语句错写成了“return a – b;”。因此,只要将其中的减号改成加号即可。不过,从调试的角度来看,还可以通过查看表达式来找到错误的原因。在第7行,函数Add返回表达式“a
24
第 章 C++程序的组成及开发过程
- b”。为了查看该表达式的结果,可以通过“添加查看”命令,来查看该表达式的值,如图2.24所示。
图2.24 查看表达式的值
通过执行几次“下一步”命令,一直运行到第14行。此时可以查看变量x的值。在查看值的窗口中,可以通过右键快捷菜单选择“修改数据”命令,手工修改变量的值,如图2.25所示。
图2.25 修改变量的值
将上述x变量的值修改为3,然后通过“跳过”命令运行完当前的主函数,其输出结果如图2.26所示。
25
C++
图2.26 输出结果
至此,调试结束。开发人员应当根据上述查看结果,修改程序,并重新运行程序进行验证。
2.11 综合实例
通常在一个实际的工程中都会有多个源文件,本节就来练习建立一个多文件的程序。本实例建立一个具有两个源文件的程序,并在主程序中调用另外一个源文件中的函数,该函数计算两个参数之和,并返回结果值。建立C++工程如图2.27所示。
图2.27 多文件工程
程序如示例代码2.3所示。
示例代码2.3
////////////////////////////////////////////////////////////////////////// // main.cpp
#include #include using namespace std; // 使用名称空间
int add(int x, int y); // 函数声明
int main(int argc, char *argv[]) // 主函数 {
cout<<\"——多文件程序——\"<system(\"PAUSE\"); // 等待用户反应 return EXIT_SUCCESS; // 主函数返回 }////////////////////////////////////////////////////////////////////////// // Method.cpp
/* add:求两个数之和的函数 输入:两个整数
返回:上述两个整数的和 */
int add(int x, int y) // 求两个整数之和的函数 { int z = x + y; // 计算参数x与y的和,并保存到变量z中 return z; // 返回z的值
26
} 第 章 C++程序的组成及开发过程读者可建立一个控制台工程以及相应的源文件,并将上述代码复制到文件中,然后编译并运行,结果如图2.28所示。
图2.28 多文件程序的运行结果
2.12 小结
本章主要介绍了C++程序的必要组成部分,以及开发一个程序的主要步骤,并简单介绍了一下开发C++程序的集成开发环境。另外,本章最后按照实际的开发过程,列举了一个具有多个源文件的程序实例。在后面的章节中,还将进一步详细介绍C++程序的各个组成部分。
◆ ◆ ◆
27