C++程序设计教程(第3版)(通用版)
上QQ阅读APP看书,第一时间看更新

第一部分 C++过程化语言基础

第1章 C++入门

C++是一门优秀的程序设计语言。C++比C更容易被人们所学习和掌握,并且以其独特的语言机制在计算机科学领域中得到广泛的应用。学习本章后,要求了解C++语言的概念,了解C与C++之间的关系,了解C++语言对程序设计方法的支持,了解C++程序开发的过程,了解简单的C++程序结构,学会最简单的C++程序开发。

1.1 从C到C++

C语言是贝尔实验室的Dennis Ritchie在B语言的基础上开发出来的,1972年在一台DEC PDP-11计算机上实现了最初的C语言。C是作为UNIX操作系统的开发语言而广为人们所认识的。实际上,当今许多新的、重要的操作系统都是用C或C++编写的。C语言是与硬件无关的。由于C语言的严谨设计,使得把用C语言编写的程序移植到大多数计算机上成为可能。到20世纪70年代末,C已经演化为“传统的C语言”。Kernighan和Ritchie在1978年出版的The C Programming Language一书中全面地介绍了传统的C语言,这本书已经成为最成功的计算机学术著作之一。

C语言在各种计算机上的快速推广导致了许多C语言版本的出现。这些版本虽然是类似的,但通常是不兼容的。对希望开发出的代码能够在多种平台上运行的程序开发者来说,这是一个大麻烦。显然,人们需要一种标准的C语言版本。为了明确地定义与机器无关的C语言,1989年美国国家标准协会制定了C语言的标准——ANSI C。Kernighan和Ritchie编著的第二版The C Programming Language(1988年版)介绍了ANSI C的全部内容。

至此,C语言以其独有的特点风靡了全世界:

(1)语言简洁、紧凑,使用方便、灵活。C语言只有32个关键字,程序书写形式自由。

(2)丰富的运算符和数据类型。

(3)C语言可以直接访问内存地址,能进行位操作,使其能够胜任开发操作系统的工作。

(4)生成的目标代码质量高,程序运行效率高。

(5)可移植性好。

C语言盛行的同时,也暴露出它的局限性:

(1)C类型检查机制相对较弱,这使得程序中的一些错误不能在编译时发现。

(2)C本身几乎没有支持代码重用的语言机制,因此一个程序员精心设计的程序,很难为其他程序所用。

(3)当程序的规模达到一定的程度时,程序员很难控制程序的复杂性。

为了满足管理程序的复杂性需要,1980年,贝尔实验室的Bjarne Stroustrup开始对C进行改进和扩充。最初的成果称为“带类的C”,1983年正式取名为C++,在经历了3次C++修订后,于1994年制定了ANSI C++标准的草案。以后又经过不断完善,成为目前的C++。C++仍在不断发展中。

C++包含了整个C,C是建立C++的基础。C++包括C的全部特征、属性和优点,同时添加了对面向对象编程(OOP)的完全支持。

1.2 程序与语言

1. 程序

程序是以某种语言为工具编制出来的动作序列,它表达了人的思想。计算机程序是用计算机程序设计语言所要求的规范书写出来的一系列动作,它表达了程序员要求计算机执行的操作。

对于计算机来说,一组机器指令就是程序。当我们说机器代码或者机器指令时,都是指的程序,它是按计算机硬件设计规范的要求编制出来的动作序列。

对于使用计算机的人来说,程序员用某高级语言编写的语句序列也是程序。程序通常以文件的形式保存起来,所以,源文件、源程序和源代码都是程序。

程序是任何有目的的、预想好的动作序列。它构成软件。

计算机要运转起来,需要一整套可运行软件,即计算机程序。

学术界对程序的定义是比较严格的,这里不作详述。

2. 程序语言的发展

最早,程序员使用最原始的计算机指令,即机器语言程序。只有机器语言才能为机器所识别和运行。这些指令由一串二进制的数表示。不久,发明了汇编语言,它可以将机器指令映射为一些能被人读懂的助记符,如ADD,SUB。程序员运行汇编程序将用助记符写成的源程序转换成机器指令,然后再运行机器指令程序,得到所要的结果。那时,编写程序的都是计算机专业人员,编写程序的语言都是低级的或较低级的。

以后,随着硬件的发展,Fortran、BASIC、Pascal、C等几十种甚至几百种高级语言应运而生,中间经历了严酷的优胜劣汰过程,最后剩下的是一些比较优秀的高级语言。C++首当其冲。

多年来,计算机程序的主要目标是力求编写出短小的代码以使运行速度更快。因为硬件成本和上机运行费很高。当计算机变得更小、更廉价、运行速度更快时,计算机硬件和运行的成本快速下降,而程序员开发程序、维护程序的费用却急剧上升,程序设计的目标也就发生了变化。

在程序正确的前提下,可读性、易维护、可移植是程序设计首要的目标。所谓可读,就是使用良好的书写风格和易懂的语句编写程序。所谓易维护,是指当业务需求发生变化时,不需要太多的开销就可以扩展和增强程序的功能。所谓可移植,是指编写的程序在各种计算机和操作系统上都能运行,并且运行结果一样。

3. 高级语言和低级语言

C++语言是高级语言,机器语言是低级语言,汇编语言基本上是低级语言。例如,对于C++语言的语句:

写成汇编语言和对应的机器语言为:

第一条命令是将a放入寄存器eax中(ebp是数据段的指针,a_$是变量a的偏移位置)。

第二条命令是将eax的内容加上2倍的eax内容放到eax中,即eax中值为3*a。

第三条命令是将b放入寄存器ecx中。

第四条命令是将ecx的内容加上ecx,即ecx中的值为2*b。

第五条命令是将eax减去ecx的值(3*a-2*b)放入eax。

第六条命令是eax的值加1。此时,eax中的值为3*a-2*b+1。

最后一条命令是将寄存器eax的值放入a变量中,即实现a=3*a-2*b+1。

可以看出,程序语言越低级,描写程序越复杂,指令越难懂。语言越低级,就越靠近机器;语言越高级,就越靠近人的表达与理解。

程序语言的发展,总是从低级到高级,直到可以用人的自然语言来描述。

程序语言的发展,也是从具体到抽象的发展过程。编制一个表达式,无须将表达式的具体操作过程描述出来,否则人会感到太累,大量的精力会被无谓地浪费,无法进行更大规模的设计与思考;而低级语言则必须详尽地描述任何操作。所以,抽象表达能力越强,语言越高级。

4. C与C++

C++语言包括过程性语言部分和类部分。过程性语言部分与C并无本质的差别,无非版本提高了,功能增强了。类部分是C中所没有的,它是面向对象程序设计的主体。要学习面向对象程序设计,首先必须具有过程性语言的基础。所以学习C++,必先学习其过程性语言部分,然后再学类部分。也就是说,先学高版本的C,再学类。从过程性语言的共同具有这个意义上来说,学习C++,无须先学C。

过程化程序设计基于结构化程序设计理论。在特定的C++语境中,则必须依赖其函数框架、循环控制、分支结构与数据说明等描述。在本质上,过程化程序设计与结构化程序设计是一致的,只是前者比较率性,直接把编码当作设计;后者更规范,强调分析设计应走流程。而在方法上,结构化或过程化程序设计则作为独立的方法,区别于面向对象程序设计和基于模板程序设计。

从语言的能耐上来说,C能很好地支持结构化程序设计,而C++既能很好地支持结构化程序设计,又能很好地支持面向对象程序设计甚至模板化程序设计。所以,正如C++的泰斗Bjarne Stroustrup所说:“先学C没有必要。”

然而,C语言程序设计的经验非常有益。因为C程序设计开发锻炼了程序员进行抽象程序设计的能力,这正是C++更为抽象的概念和技术的基础。而且,C++是C语言的扩展,它分享了C的许多技术风格。C程序设计特性在C++中得到频繁使用。一个人使用C的经验越丰富,编写C++程序也就越容易。所以,学过C能够促进C++的学习。

1.3 结构化程序设计

以前,人们把程序看成是处理数据的一系列过程。过程或函数定义为一个接一个顺序执行的一组指令。数据与程序分开存储,编程的主要技巧在于追踪哪些函数调用哪些函数,哪些数据发生了变化。为解决其中可能存在的问题,结构化编程应运而生。

结构化程序设计的主要思想是功能分解并逐步求精。当一些任务十分复杂以致无法描述时,可以将它拆分为一系列较小的功能部件,直到这些自完备的子任务小到易于理解的程度。例如,计算一个公司中每一个职员的平均工资是一项较为复杂的任务,可以将其拆分为以下的子任务:

(1)找出一个人的收入。

(2)计算总共有多少职员。

(3)计算工资总额。

(4)用职员人数去除工资总额。

计算工资总额本身又可分为一系列子任务:

(1)找出每个职员的档案。

(2)读出工资数额。

(3)把工资加到部分和上。

(4)读出下一个职员的档案。

类似地,读出每个职员档案中的记录又可以分解为一系列子任务:

(1)打开职员的档案。

(2)找出正确记录。

(3)从存储设备中读取数据。

结构化程序设计成功地为处理复杂问题提供了有力的手段。然而到20世纪80年代末,它的一些缺点越来越突出。

当数据量增大时,数据与处理这些数据的方法之间的分离使程序变得越来越难以理解。对数据处理能力的需求越强,这种分离所造成的负作用越显著。

采用结构化程序设计方法的程序员发现,每一种相对于老问题的新方法都要带来额外的开销,与可重用性相对,通常称之为重复投入。基于可重用性的思想是指建立一些具有已知特性的部件,在需要时可以插入到程序之中。这是一种模仿硬件组合方式的做法,当工程师需要一个新的晶体管时,他不用自己去发明,只要到仓库去找就行了。对于软件工程师来说,在面向对象程序设计出现之前,虽然市面上有些代码的功能看上去很像是自己需要的,但是修修改改最后还得自己动手重做。

1.4 面向对象程序设计

面向对象程序设计的本质是把数据和处理数据的过程当成一个整体——对象。

C++充分支持面向对象程序设计。面向对象程序设计的实现需要封装和数据隐藏技术,需要继承和多态性技术。

1. 封装和数据隐藏

当一个技术员要安装一台计算机时,他将各个设备组装起来。当他想要一个声卡时,不需要用原始的集成电路芯片和材料去制作一个声卡,而是购买一个他所需要的某种功能的声卡。技术员关心的是声卡的功能,并不关心声卡内部的工作原理。声卡是自成一体的。这种自成一体性称为封装性。无须知道封装单元内部是如何工作就能使用的思想称为数据隐藏。

声卡的所有属性都封装在声卡中,不会扩展到声卡之外。因为声卡的数据隐藏在该电路板上。技术员无须知道声卡的工作原理就能有效地使用它。

C++通过建立用户定义类型(类)支持封装性和数据隐藏。完好定义的类一旦建立,就可看成是完全封装的实体,可以作为一个整体单元使用。类的实际内部工作应当隐藏起来,使用完好定义的类的用户不需要知道类是如何工作的,只要知道如何使用它就行。

2. 继承和重用

要制造新的电视机,可以有两种选择:一种是从草图开始;另一种是对现有的型号加以改进。也许现有的型号已经令人满意,但如果再加一个功能,会更加完美。电视机工程师肯定不想从头开始,而是希望制造另一种新型电视机,该机是在原有的型号基础上增加一组电路做成的。新的电视机很快就制造出来了,被赋予一种新的型号,于是新型电视机就诞生了。这是继承和重用的实例。

C++采用继承支持重用的思想,程序可以在扩展现有类型的基础上声明新类型。新子类是从现有类型派生出来的,称为派生类。新型电视机是在原有型号的电视机上增加若干种功能而得到的,所以新型电视机是原有电视机的派生,继承了原有电视机的所有属性,并在此基础上增加了新的功能。

3. 多态性

通过继承的方法构造类,采用多态性为每个类指定表现行为。例如,学生类应该有一个计算成绩的操作。大学生继承了中学生,或者说是中学生的延伸。对于中学生,计算成绩的操作表示语文、数学、英语等课程的成绩计算;而对于后继的大学生,计算成绩的操作表示高等数学、计算机、普通物理等课程的成绩计算。

继承性和多态性的组合,可以轻易地生成一系列虽类似但独一无二的对象。由于继承性,这些对象共享许多相似的特征。但由于多态性,一个对象可以有独特的表现方式,而另一个对象有另一种表现方式。

1.5 程序开发过程

大多数现代的编译程序都提供了一个集成开发环境。在这样一个环境中,一般是从菜单中选定compile或make或build命令,来生成可执行的计算机程序。

程序员编制的源程序被编译(compile)后,会生成一个目标文件,这个文件通常以.obj作为文件扩展名。该目标文件为源程序的目标代码,即机器语言指令。但这仍然不是一个可执行的程序,因为目标代码只是一个个的程序块,需要相互衔接成为一个适应一定的操作系统环境的程序整体。为了把它转换为可执行程序,必须进行连接(link)。

C++程序通常是通过同时连接一个或几个目标文件与一个或几个库而创建的。库(.lib)是一组由机器指令构成的程序代码,是可连接文件。库有标准库和用户生成的库。标准库是由C++提供的,用户生成的库是由软件开发商或程序员提供的。文件与库连接的结果,即生成计算机可执行的程序。

程序员首先在集成开发环境中编辑源程序,或在其他编辑器中输入源程序,然后,在集成环境中启动编译程序将源程序转化成目标文件。编译之后,很有可能产生一些编译错误,于是程序员回到编辑状态重新开始编辑程序和编译。同样在紧接着的连接和运行中也会遇到连接或运行错误,此时,又回到编辑状态修改程序,见图1-1。

图1-1 开发C++程序的步骤

1.6 最简单的程序

我们从最简单的程序例子来分析C++的程序构成。

运行结果为:

     I am a student.

C++的程序结构由注释、编译预处理和程序主体组成。

注释是程序员为读者作的说明,是提高程序可读性的一种手段。一般可将其分为两种:序言注释和注解性注释。前者用于程序开头,说明程序或文件的名称、用途、编写时间、编写人以及输入输出说明等;后者用于程序中难懂的地方。

C++的注释为“//”之后的内容,直到换行。注释仅供阅读程序使用,是程序的可选部分。在生成可执行程序之前,C++忽略注释,并把每个注释都视为一个空格

另外,C++还兼容了C语言的注释,即一对符号“/*”与“*/”之间的内容。它可以占多行,例如:

每个以符号“#”开头的行,称为编译预处理行。如“#include”称为文件包含预处理命令。编译预处理是C++组织程序的工具,有关内容在6.8节中介绍。

“#include<iostream>”的作用是在编译之前将文件“iostream”的内容增加(包含)到程序ch2_1.cpp中,以作为其一部分。iostream.h是系统定义的一个“头文件”,它设置了C++的I/O相关环境,定义输入输出流对象cin与cout等。cin与cout的使用方法将在2.6节中介绍,其意义将在第19章中介绍。

main()表示主函数,每一个C++程序都必须有一个main()函数。main()作为从计算机操作系统进入(调用)程序的入口。main前面的int表示函数的返回类型。既然main()函数被操作系统调用,其最终也将返回到操作系统。main()函数用int作为返回类型是C和C++的共同规定。函数体用大括号{}括起来。描述一个函数所执行算法的过程称为函数定义。例如,这里的main()函数头和函数体构成了一个完整的函数定义。

函数名main全部都是由小写字母构成。C++程序中的名字是大小写“敏感”的,所以在书写标识符的时候要注意其大小写。

在main()函数体中,cout(全是小写字母)是一个代表标准输出的流设备,它是C++预定义的对象(在iostream中定义),前面包含的头文件就是为了能在这里使用输出设备cout。当程序要在设备上进行输出时,就需要在程序中指定该对象。输出操作由操作符“≪”来表达,它表示将该操作符右边的数据送到显示设备上。

程序中用双引号括起的数据“I am a student.\n”被称为字符串。其中字符“\n”表示一个回车控制符。字符串在2.4节中介绍。

“;”表示一个语句的结束。

例如,下面的程序求一个表达式的值:

运行结果为:

该程序从main()开始运行。C++中,一个变量必须在声明之后才能使用,所以程序首先进行变量定义。“int a,b,result;”表示分别定义a、b、result这3个int(整型)变量。C++语言提供的标准数据类型之一是int。定义变量时,要求在变量之前声明变量的类型。在C++中定义变量,意味着给变量分配内存空间,用来存放变量值。

随后,在显示“please input two numbers:”之后,执行“cin>>a>>b;”,它从标准输入设备(键盘)中输入两个整型数a和b。运行中,屏幕将等待输入,直至输入了两个数123和45。输入时,两个数之间用空格隔开。这两个数分别赋给了变量a和b。

“result=3*a-2*b+1;”是赋值语句,*是乘号,将表达式3*a-2*b+1的值(280)赋给变量result,使之等于280。然后,在接下来的语句中将result值输出。在cout语句中,有3个“≪”符号,表示各项内容的连续输出。“≪result”表示输出变量的值,“≪endl”表示输出一个回车符,与“≪'\n'”是等价的。

在输出格式中,<ENTER>表示输入的回车符,在以后的例子中将省略之。

1.7 函数

1. C++用函数组织程序

虽然main()也是函数,但它并不是普通的函数。其他函数都是在程序运行时被调用。程序命令按照它们在源代码中出现的顺序一句一句地顺序执行,直到碰到新的函数调用。然后程序调头去执行函数调用。当函数完成时,程序控制立即返回到调用函数的下一行代码。

这一过程可比喻为查字典。如果你在看书时有一个字不认识,你就要停止阅读,去查字典。字典查完后,再接着看书。

当程序需要服务时,它可以调用函数实现所需要的服务,然后当函数返回时再从它原来的地方继续执行。

2. C++程序是函数驱动的

例如,下面的程序实现一个简单的用户函数max()的调用,来求两个数中的较大值,并调用了标准库函数sqrt()来求两个数中较大值的平方根:

运行结果为:

主函数main()的开始是3个double型(双精度类型)变量的定义语句,C++为此分配3个double型变量的内存空间。在输入了两个变量a、b的值(运行中输入的123赋给a,456赋给b)后,调用了用户自定义的函数max()。

C++中,一个函数必须在函数声明后才能使用(被调用),所以在主函数main()的前面,有max()函数的声明。函数声明告诉编译器该函数是存在的。然后编译器在看到该函数被调用时就不会觉得大惊小怪了。同时编译器还对函数调用进行正确性检查。C++函数声明总是由函数原型构成的。函数原型在5.2节介绍。

max()函数调用使程序执行max()函数中的语句,并将该函数的返回值赋给变量c。

max()函数是求两个double型数中的大者,然后将结果返回给调用它的函数。所以在函数的头上写有double的返回类型。如果一个函数不需要返回值,则可以在头上声明为void。

函数定义由函数头和函数体构成。函数头又由返回类型、函数名和函数参数构成。上例中的“double max(double x,double y)”就是函数头。函数体是由紧随函数头之后的大括号构成。

函数头中的函数参数允许向函数传递值。max()函数中,x、y就是函数的参数。参数声明时,要指出其类型。函数max()中的参数声明为“double x,double y”,它指出x和y的类型都是双精度型。

函数定义中的参数称为形式参数,简称形参。函数max()中的x和y就是形参。调用函数时实际传递的值称为实际参数,简称实参。主函数main()在对max()函数的调用时,用的a和b就是实参。函数在调用时,将实参值复制给形参,使得形参变量也具有实参的值。实参可以是表达式,它代表赋值的一方。形参只能是变量,因为它要接受赋值。

函数头有返回类型说明时,函数体中要用return返回值。同时,return语句也使函数退出。max()函数中执行“return x”或“return y”即返回一个double值到主函数main()中。

如果函数体中没有return语句,函数将在结尾处自动无值返回。如果有返回值,则该返回值应该具有函数头中声明的返回类型。

在ANSI C++98中,main函数的返回类型规定为int,但为了兼容老旧C++,main函数的返回类型可以是任何已有的数据类型,例如,编译器接受void main(){}函数。

main()函数是唯一不是void返回类型,而可以在函数结束时忽略return语句的函数。因为只有这个函数不能被其他函数调用,仅由操作系统直接控制,其值返回给操作系统,在技术上独立实现返回过程,不影响C/C++的函数返回机制。C++标准对main()函数中不写return语句予以了默许。

函数有两种:标准库函数和用户定义函数。上例中的max()函数是用户定义函数,sqrt()函数是标准库函数。标准库函数简称库函数,它是C++提供的,可以为任何程序所使用。库函数无须用户声明和定义,但要将含有其函数声明的头文件包含在程序中。sqrt()函数的声明在cmath头文件中,所以在上例程序的开头写有#include<cmath>。

一般的数学函数,在C语言中,是放在头文件math.h中,而在C++则是放在cmath头文件中,C++对C的函数库进行了些许改造。因为C与老旧的C++的头文件都要带.h后缀,而C++又是兼容了C,所以,ch2_3.cpp代码中包含的头文件语句还可以写成#include<math.h>。

一个C++程序由一个主函数和若干个函数构成。由主函数调用其他函数,其他函数也可以互相调用。同一个函数可以被一个或多个函数调用任意多次,见图1-2。

图1-2 程序中的函数调用

函数定义包含函数声明,所以可以将函数定义放在函数应该声明的位置,而将其他函数的定义放在主函数main()之前。一个程序中主函数main()的位置是没有特殊要求的,由此,程序ch2_3.cpp可以写成下面的程序:

该程序的功能和ch2_3.cpp是一样的。但通常我们习惯将主函数main()放在程序的前面,以便阅读程序时很快找到。

C++中,每个函数对于程序的其他函数总是可见的。也就是说,任何函数都可以被包括它自己的所有函数所调用。用户不能定义函数的唯一之处是在另一个函数的定义之中。函数定义可以以任何顺序出现在程序中。由于main()启动和终止程序运行,所以main()函数通常第一个出现在程序中,而其他函数定义紧随其后。

小结

学习C++,不一定非要学过C,但学过C能促进C++的学习。

C++程序经过编辑、编译和连接,产生可运行的exe文件。

C++程序由函数构成,它总是从主函数main()开始运行。但并不是说,main()函数非得要写在程序的最前面。

函数有两种:标准库函数和用户定义函数。main()函数是特殊的用户定义函数。每个程序只能有一个main()函数,并且必须要有一个main()函数。

函数调用前必须要有函数声明。

函数定义包含函数声明。函数定义由函数头和函数体组成。关于函数,在第5章中将详细介绍。

一个语句可以写在多个程序行上,一个程序行可以写多个语句。语句以分号结束。

C++通过标准输入/输出流进行输入/输出。

程序ch2_3.cpp是C++的简单程序结构之样板。认识C++程序从该程序开始。

程序设计的目标在正确的前提下,其重要性排列次序依次为可读、可维护、可移植和高效。