版权信息

书名：C++从入门到精通（第4版）
作者：明日科技
出版社：清华大学出版社
出版时间：2019-10-01
ISBN：9787302535836

版权所有·侵权必究

内容简介

《C++从入门到精通（第4版）》从初学者的角度出发，以通俗易懂的语言，丰富多彩的实例，详细讲解了C++的基础知识。本书包括4篇18章：第1篇是基础知识，包括绪论，数据类型，表达式与语句，条件判断语句，循环语句，函数，数组、指针和引用，构造数据类型；第2篇是核心技术，包括面向对象编程、类和对象、继承与派生；第3篇是高级应用，包括模板、标准模板库、RTTI与异常处理、程序调试、文件操作、网络通信；第4篇是项目实战，结合人事考勤管理系统，依照软件工程的开发流程，讲述如何进行实际开发。书中所有知识都结合具体实例进行介绍，涉及的程序代码给出了详细的注释，可以使读者轻松领会C++的强大，快速提高开发技能。

另外，本书除了纸质内容之外，配书资源包中还给出了海量开发资源库，主要内容如下：

语音视频讲解：总时长17小时，共91集

实例资源库：881个实例及源码详细分析

模块资源库：15个经典模块开发过程完整展现

项目案例资源库：15个企业项目开发过程完整展现

测试题库系统：616道能力测试题目

面试资源库：371个企业面试真题

PPT电子教案

本书可作为软件开发入门者的自学用书，也可作为高等院校相关专业的教学参考书，还可供开发人员查阅、参考。

如何使用本书开发资源库

在学习《C++从入门到精通（第4版）》时，随书附配资源包中提供了“Visual C++开发资源库”系统，可以帮助读者快速提升编程水平和解决实际问题的能力。《C++从入门到精通（第4版）》和“Visual C++开发资源库”配合学习的流程如图1所示。

图1　本书与附配开发资源库配合学习流程

打开资源包的“开发资源库”文件夹，运行Visual C++开发资源库.exe程序，即可进入“Visual C++开发资源库”系统，主界面如图2所示。

图2　“Visual C++开发资源库”主界面

在学习本书某一章节时，可利用实例资源库对应章节提供的大量热点实例和关键实例巩固所学编程技能，提高编程兴趣和自信心；也可以利用能力测试题库的对应章节进行测试，检验学习成果。具体流程如图3所示。

图3　使用实例资源库和能力测试题库

对于数学逻辑能力和英语基础较为薄弱的读者，本书提供了数学及逻辑思维能力测试和编程英语能力测试，以供读者进行练习和测试，如图4所示。

图4　数学及逻辑思维能力测试和编程英语能力测试目录

本书学习完毕后，读者可通过模块资源库和项目资源库中的30个经典模块和项目，全面提升个人综合编程技能和解决实际开发问题的能力，为成为C++软件开发工程师打下坚实基础。具体模块和项目目录如图5所示。

万事俱备，该到软件开发的主战场上接受洗礼了。面试资源库中提供了大量国内外软件企业的常见面试真题，同时还提供了程序员职业规划、程序员面试技巧、企业面试真题汇编和虚拟面试系统等精彩内容，是程序员求职面试的绝佳指南。面试资源库的具体内容如图6所示。

图5　模块资源库和项目资源库目录

图6　面试资源库具体内容

前言

丛书说明 ：“软件开发视频大讲堂”丛书（第1版）于2008年8月出版，因其编写细腻，易学实用，配备海量学习资源和全程视频等，在软件开发类图书市场上产生了很大反响，绝大部分品种在全国软件开发零售图书排行榜中名列前茅，2009年多个品种被评为“全国优秀畅销书”。

“软件开发视频大讲堂”丛书（第2版）于2010年8月出版，第3版于2012年8月出版，第4版于2016年10月出版。十年锤炼，打造经典。丛书迄今累计重印500多次，销售200多万册。不仅深受广大程序员的喜爱，还被百余所高校选为计算机、软件等相关专业的教学参考用书。

“软件开发视频大讲堂”丛书（第5版）在继承前4版所有优点的基础上，进一步修正了疏漏，优化了图书内容，更新了开发环境和工具，并根据读者建议替换了部分学习视频。同时，提供了从“入门学习→实例应用→模块开发→项目开发→能力测试→面试”等各个阶段的海量开发资源库，使之更适合读者学习、训练、测试。为了方便教学，还提供了教学课件PPT。

C++语言是在C语言基础上发展起来的，它在C语言基础上融入了许多新的编程理念，这些理念有利于程序的开发。从语言角度来说，C++语言是一个规范，它规范程序员如何进行面向对象程序开发。C++具有C语言操作底层的能力，同时还具有提高代码复用率的面向对象编程技术，是一种语句更加灵活、使用更加简捷、技术更加全面的编程利器。

本书内容

本书提供了从入门到编程高手所必备的各类知识，共分4篇，大体结构如下图所示。

第1篇：基础知识。 本篇讲解C++语句基础部分，只有具备了牢固的基础知识才能更快地掌握更高级的技术内容。通过对C++语言的历史、常用开发环境、数据类型、运算符与表达式、条件判断语句、循环语句、函数等这些内容的介绍，结合流程图和实例，并通过视频的指导讲解，为以后编程奠定坚实的基础。

第2篇：核心技术。 本篇介绍了C++语言的关于面向对象方面的内容，理解面向对象这个概念，应用类类型创建对象，掌握什么是继承和派生，利用多态进行面向对象开发。

第3篇：高级应用。 模板是标准模板库（STL）的基础，通过对模板的介绍，使读者能够理解STL的构造。文件操作也是程序开发过程中必不可少的技术，掌握文件操作方法是奠定开发大项目的基础，通过对运行时类型识别（RTTI）的介绍使读者对面向对象开发有更深的理解。网络通信是仅次于文件技术的另一项关键技术，通过实例，读者可以掌握基本的网络通信知识。

第4篇：项目实战。 本篇运用软件工程的设计思想，通过人事考勤管理系统的开发练习，学习如何进行软件项目开发的知识。书中按照“编写需求分析→系统设计→公共模块设计→主窗体设计→实现项目模块功能→总结开发技巧与难点”的流程进行介绍，带领读者一步一步亲身体验开发项目的全过程。

本书特点

由浅入深，循序渐进。 本书以初、中级程序员为对象，先从C++语言基础学起，再到C++语言的程序结构，然后学习C++语言的高级应用，最后学习开发一个完整的项目。讲解过程中步骤详尽，版式新颖，并且在程序中会有相应的实例帮助读者更好地理解所讲解的知识，在实例讲解时分步分析，可使读者在阅读时一目了然，从而快速把握书中内容。

微课视频，讲解详尽。 为便于读者直观感受程序开发的全过程，书中配备了教学微视频，使用手机扫描二维码，即可观看学习，能快速引导初学者入门，感受编程的快乐和成就感，进一步增强学习的信心。

实例典型，轻松易学。 通过例子学习是最好的学习方式，本书通过“一个知识点、一个例子、一个结果、一段评析、一个综合应用”的模式，透彻详尽地讲述了实际开发中所需的各类知识。另外，为了便于读者阅读程序代码，快速学习编程技能，书中几乎每行代码都给出了注释。

精彩栏目，贴心提醒。 本书根据需要在各章使用了很多“注意”“说明”等小栏目，让读者可以在学习过程中更轻松地理解相关知识点及概念，更快地掌握个别技术的应用技巧。

应用实践，随时练习。 书中几乎每章都提供了“实践与练习”，读者能够通过对问题解答的重新回顾，熟悉所学的知识，并举一反三，从而为进一步学习做好充分的准备。

读者对象

初学编程的自学者

编程爱好者

大中专院校的老师和学生

相关培训机构的老师和学员

进行毕业设计的学生

初、中级程序开发人员

程序测试及维护人员

参加实习的“菜鸟”程序员

读者服务

本书所附赠的各类学习资源，读者可登录清华大学出版社网站（www.tup.com.cn），在对应图书页面下获取其下载方式。也可扫描图书的“文泉云盘”二维码，获取其下载方式。

为了方便解决本书疑难问题，读者朋友可加我们的企业QQ：4006751066（可容纳10万人），也可以登录www.mingrisoft.com留言，我们将竭诚为您服务。

致读者

本书由明日科技C++程序开发团队组织编写，明日科技是一家专业从事软件开发、教育培训以及软件开发教育资源整合的高科技公司，其编写的教材既注重选取软件开发中的必需、常用内容，又注重内容的易学、方便以及相关知识的拓展，深受读者喜爱。其编写的教材多次荣获“全行业优秀畅销品种”“中国大学出版社优秀畅销书”等奖项，多个品种长期位居同类图书销售排行榜的前列。

在编写本书的过程中，我们始终本着科学、严谨的态度，力求精益求精，但错误、疏漏之处在所难免，敬请广大读者批评指正。

感谢您购买本书，希望本书能成为您编程路上的领航者。

“零门槛”编程，一切皆有可能。

祝读书快乐！

编者
2019年6月

说明：“文中需扫码阅读的请扫此付费二维码”。

资源包“开发资源库”目录

第1大部分　实例资源库

（881个完整实例分析，资源包路径：开发资源库／实例资源库）

第2大部分　模块资源库

（15个经典模块，资源包路径：开发资源库／模块资源库）

第3大部分　项目资源库

（15个企业开发项目，资源包路径：开发资源库／项目资源库）

第4大部分　能力测试库

（616道能力测试题目，资源包路径：开发资源库／能力测试）

第5大部分　面试系统资源库

（371项面试真题，资源包路径：开发资源库／面试系统）

第1篇

基础知识

第1章　绪论

第2章　数据类型

第3章　表达式与语句

第4章　条件判断语句

第5章　循环语句

第6章　函数

第7章　数组、指针和引用

第8章　构造数据类型

本篇通过绪论，数据类型，表达式与语句，条件判断语句，循环语句，函数，数组、指针和引用，构造数据类型等内容结合大量图示、举例、录像等使您快速掌握C++语言，并为以后编程奠定坚实的基础。

第1章

绪论

（视频讲解： 1小时18分钟）

C++是当今流行的编程语言，它是在C语言基础上发展起来的，随着面向对象编程思想的发展，C++也融入了新的编程理念，这些理念有利于程序的开发。C++从语言角度来说也是一个规范，随着规范的发布，许多C++编译器不断涌现，不同的C++编译器也带来不同的语言特性，这给程序员带来了广阔的选择空间。

通过阅读本章，您可以：

了解C++的发展历程

了解为C++发展做出杰出贡献的人物

掌握主要的C++编译器及开发环境

掌握C++项目文件及编译工程

1.1　C++历史背景

要学一门语言，首先要对这门语言有一定的了解，要知道这门语言能做什么，要怎样才能学好。本节将对C++语言的历史背景进行简单的介绍，使读者对C++语言有一个简单而直接的印象。

1.1.1　20世纪最伟大的发明

计算机的出现给我们的生活带来了巨大的变化，它是如何发展起来的呢？开始时人们致力研究的能够进行四则运算的机器，是通过机械齿轮运作的加法器，而后是精度只有12位的乘法计算器，直到1847年Charles Babbages开发出能计算31位精度的机械式差分机，这台差分机被普遍认为是世界上第一台机械式计算机。随着电子物理的发展，真空二极管、真空三极管问世，到1939年第一部用真空管计算的机器被研制出来，该机器是能进行16位加法运算的机器；随后，用氖气灯（霓虹灯）的存储器、复杂数字计算机（断电器计数机）、可编写程序的计数机，被一一研制出来。1946年，第一台电子管计算机ENIAC在美国被研制出来，这台计算机占地170平方米，重30吨，有1.8万个电子管，用十进制计算，每秒运算5000次。计算机从此进入了电子计算机时代，经历了真空管计算机、晶体管计算机、集成电路计算机、大规模集成电路计算机4个阶段，每一个阶段都是随着电子物理的发展而发展的，晶体管的出现取代了电子管，将电子元件结合到一片小小的硅片上，形成集成电路（IC），在一块芯片上容纳几百个或是几千个电子元件形成了大规模集成电路（LSI），直到现在已经出现了很薄的电子芯片，可谓是发展迅速。

现在计算机已经应用到各个领域，科学计算、信号检测、数据管理、辅助设计都在使用计算机，人们的生活已经渐渐离不开它，所以说计算机是20世纪最伟大的发明。

1.1.2　C++发展历程

早期的计算机程序语言就是计算机控制指令，每条指令就是一组二进制数，不同的计算机都有不同的计算机指令集。使用二进制指令集开发程序是一件很头痛的事，需要记住大量的二进制数，为了便于记忆，人们将二进制数用字母组合代替，以字符串关键字代替二进制机器码的编程语言称为汇编语言，汇编语言被称为是低级语言，虽然汇编语言比机器码容易记忆，但仍然具有可读性差的缺点，大量的跳转指令和地址值很难让程序员在很短的时间理解程序的意思，于是编程语言进入了高级语言时代。

第一种高级语言是美国尤尼法克公司在1952年研制成功的Short Code，但被广泛使用的高级语言是FORTRAN，它是由美国科学家John Backus设计并在IBM公司的计算机上实现的，但FORTRAN语言和ALGOL60主要应用于科学和工程计算，随后出现了Pascal和C语言。C语言是在其他语言基础上发展起来的。首先是Richard Martin开发一种高级语言BCPL，随后Ken Thompson使用BCPL语言对其进行了简化，形成一门新的语言—B语言，但B语言没有类型的概念，Dennis Ritchie对B语言进行研究和改进，在B语言基础上添加了结构和类型，并将这个改进后的语言命名为C语言，寓意很简单，因为字母C是字母B的下一个字母，预示着语言的发展。

本书所讲述的C++语言就是从C语言发展过来的。Stroustrup经过钻研在C语言中加入了类的概念，C++最初的名字是C with Class，到1983年12月由Rick Mascitti建议改名为C Plus Plus，即C++。最开始提出类概念的语言是Simula，它具有很高的灵活性，但无法胜任比较大型的程序。此后在Simula语言基础上发展的语言Smalltalk才是真正的面向对象语言，但Smalltalk-80不支持多继承。

C++从Simula继承了类的概念，从Algol68继承了运算符重载、引用以及在任何地方声明变量的能力，从BCPL获得了／／注释，从Ada得到了模板、名字空间，从Ada、Clu和ML取来了异常。

1.1.3　C++中的杰出人物

Dennis M. Ritchie

Dennis M. Ritchie被称为C语言之父，UNIX之父，生于1941年9月9日，哈佛大学数学博士，曾任朗讯科技公司贝尔实验室（原AT&T实验室）下属的计算机科学研究中心系统软件研究部的主任一职。他开发了C语言，并著有《C程序设计语言》（ The C Programming Language ）一书，还和Ken Thompson一起开发了UNIX操作系统。他因杰出的工作得到了众多计算机组织的公认和表彰，1983年，获得美国计算机协会颁发的图灵奖（又称计算机界的诺贝尔奖），还获得过C&C基金奖、电气与电子工程师学会优秀奖章、美国国家技术奖章等多项大奖。

Bjarne Stroustrup

Bjarne Stroustrup 1950年出生于丹麦，先后毕业于丹麦阿鲁斯大学和英国剑桥大学，AT&T大规模程序设计研究部门负责人，AT&T贝尔实验室和ACM成员。1979年，Stroustrup开始开发一种语言，当时称为C with Class，后来演化为C++。1998年，ANSI/ISO C++标准建立，同年，Stroustrup推出其经典著作 The C ++ Programming Language 的第3版。

Scott Meyers

Scott Meyers是世界顶级的C++软件开发技术权威之一，他拥有Brown University的计算机科学博士学位，其著作 Effective C ++和 More Effective C ++很受编程人员的喜爱。Scott Meyers曾经是C++ Report的专栏作家，为 C / C ++ Users Journal 和 Dr. Dobb’s Journal 撰过稿，为全球范围内的客户提供咨询活动。他还是Advisory Boards for NumeriX LLC和InfoCruiser公司的成员。

Andrei Alexandrescu

Andrei Alexandrescu被认为是新一代C++天才的代表人物，2001年撰写了经典名著 Modern C ++ Design ，其中对Template技术进行了精湛运用，第一次将模板作为参数在模板编程中使用，该书震撼了整个C++社群，开辟了C++编程领域的Modern C++新时代。此外，他还与Herb Sutter合著了C++ Coding Standards。他在对象拷贝（object copying）、对齐约束（alignment constraint）、多线程编程、异常安全和搜索等领域做出了巨大贡献。

Herb Sutter

Herb Sutter是C++ Standard Committee的主席，作为ISO/ANSI C++标准委员会的委员，Herb Sutter是C++程序设计领域屈指可数的大师之一。他的Exceptional系列3本书（ Exceptional C ++、 More Exceptional C ++和 Exceptional C ++ Style ）成为C++程序员必读书。他是深受程序员喜爱的技术讲师和作家，是 C / C ++ Users Journal 的撰稿编辑和专栏作者，曾发表了上百篇软件开发方面的技术文章和论文。他还担任Microsoft Visual C++架构师，和Stan Lippman一道在微软公司主持VC 2005（即C++/CLI）的设计。

Andrew Koenig

Andrew Koenig是AT&T公司Shannon实验室大规模编程研究部门中的成员，同时也是C++标准委员会的项目编辑，是一位真正的C++内部权威。Andrew Koenig的编程经验超过30年，其中有15年在使用C++，已经出版了超过150篇和C++有关的论文，并且在世界范围内就这个主题进行过多次演讲，对C++的最大贡献是带领Alexander Stepanov将STL引入C++标准。

1.2　常用开发环境

在使用C++语言时，需要选择一款开发环境，那么有哪些环境可供用户选择呢？下面就对一些常用的C++语言开发环境进行简单的介绍。

1.2.1　Visual C++ 6.0

Visual C++ 6.0是由微软公司开发的C++开发环境，它是Visual Studio集成开发环境中的一员。Visual C++ 6.0可以创建Windows应用程序、DLL动态链接库、COM组件以及ActiveX控件等。Visual C++ 6.0开发环境如图1.1所示。

图1.1　Visual C++ 6.0开发环境

1.2.2　Visual Studio 2017

Visual Studio 2017是目前比较流行的开发环境，它更加支持C++标准规范，而且还支持托管程序的编译和WebService。应该说Visual Studio 2017更加强大，无论是功能上还是编译速率上都有很大提高。Visual Studio 2017开发环境如图1.2所示。

图1.2　Visual Studio 2017开发环境

1.2.3　GCC/G++

Linux系统中一般都会带有C/C++的编译器，能够编译C代码的是GCC，能够编译C++代码的是G++，如果是在没有GDK或GDE等界面系统的Linux系统下，编写代码需要使用VI文本命令，它和DOS下的TYPE命令很像，由于没有鼠标，只能顺序地编写代码，使用它编写代码对于程序员来说相当地不方便。但在有界面的Linux系统下，系统都会提供可视化的文本编辑器，其中比较有名的就是emacs，因为emacs集成了编译代码的菜单，通过emacs可以直接编写代码并编辑，并且可以直接执行编译后的程序。emacs开发环境如图1.3所示。

图1.3　emacs开发环境

1.2.4　Dev-C

Dev-C是将Linux系统的GCC/G++移到Windows系统后的产物，具体来讲，编译代码的程序是GCC.exe和G++.exe，这两个程序属于开源项目mingw, Dev-C只是能够调用GCC.exe和G++.exe来编译程序的代码编辑器。Dev-C开发环境如图1.4所示。

图1.4　Dev-C开发环境

1.2.5　Eclipse

Eclipse是IBM开发的、早期用来编写Java代码的编辑器，但由于CDT插件的出现，使Eclipse也可以用来编写C/C++代码，同Dev-C一样，Eclipse也使用开源项目下的GCC.exe和G++.exe来编译代码，所以在使用Eclipse前一定要注意设置好相关路径。Eclipse开发环境如图1.5所示。

图1.5　Eclipse开发环境

1.3　认知C++程序代码

C++程序代码是由预编译指令、宏定义指令、注释、主函数、自定义函数等很多部分组成的，这些部分都是后文讲述的主要内容。下面是一段很短但涉及C++语言概念比较多的代码，如图1.6所示。

如图1.6所示的代码中还有头文件引用、函数作用空间、库函数调用、赋值运算、关系判断、流输出等很多C++语言方面的概念，各概念之间通过一定规则罗列在一起，编译器会根据这些规则将代码编译成能够在机器上执行的应用程序。

图1.6　代码介绍

1.4　C++工程项目文件

Windows操作系统主要是用来管理数据的，而数据是以文件的形式存储在磁盘上的。文件可以通过扩展名来区分不同的类型，C++的代码文件就有两种类型，一种是源文件，一种是头文件。头文件中添加的是定义和声明函数部分，源文件中则是在头文件中定义函数的实现部分；源文件主要以cpp为扩展名，而头文件主要以h为扩展名。有的开发环境可能使用cxx、cHH来作为源文件的扩展名。

对一个比较大的工程而言，它的源文件和头文件可能会比较多，为了管理这些源文件，不同的编译器还提供了管理代码的工程项目文件，不同开发环境的工程项目文件也会不同。

（1）Dev-C的工程项目文件如图1.7所示。

main.cpp：源文件。

Sample.dev：工程文件。

Makefile.win：make程序执行时使用的文件，用于自动编译源代码文件。

main.o：编译后的目标文件。

Sample.exe：连接后生成的程序。

（2）Visual C++ 6.0的工程项目文件如图1.8所示。

图1.8　Visual C++ 6.0的工程项目文件

Debug：存储编译后程序文件夹，带有调试信息的程序。

Release：存储编译后程序文件夹，最终程序。

Sample.cpp：源文件。

Sample.dsp: Visual C++ 6.0的工程文件。

Sample.dsw: Visual C++ 6.0的工作空间文件。

Sample.ncb: Visual C++ 6.0的用于声明的数据库文件。

Sample.opt: Visual C++ 6.0存储用户选项的文件。

StdAfx.cpp：向导生成的标准源文件，代码中涉及MFC类库内容时使用该文件。

StdAfx.h：向导生成的标准头文件。

注意

Debug与Release的区别在于，Debug是含有调试信息的应用程序，Debug文件夹下的程序可以设置断点调试，而且Debug文件夹下的程序要比Release文件夹下的程序大。

（3）Eclipse的工程项目文件如图1.9所示。

图1.9　Eclipse的工程项目文件

.cproject: Eclipse工程相关信息文件。

.project: Eclipse工程文件。

Makefile:make程序执行时使用的文件，用于自动编译源代码文件。

Sample.cpp：源文件。

Sample.exe：连接后生成的程序。

Sample.o：编译后的目标文件。

通过工程项目文件的扩展名，就可以知道代码文件使用哪种开发环境管理。

1.5　使用Visual C++ 6.0创建程序

Visual C++ 6.0可以通过两种方式创建输出Hello World！程序，一种是使用向导直接创建，一种是创建空工程后，手动向工程中添加源文件并写入代码。

1．使用Visual　C++ 6.0创建输出Hello World！程序

具体创建步骤如下：

（1）启动Visual C++ 6.0，选择File→New命令，弹出创建工程向导，如图1.10所示。

图1.10　Visual C++ 6.0创建工程向导

（2）在左侧的工程列表中选择Win32 Console Application工程类型，在Project name中输入工程名Sample，在Location中设置工程的保存路径D:\桌面\LIZI\Sample。然后单击OK按钮，弹出Win32 Console Application-Step 1of 1对话框，如图1.11所示。

图1.11　Visual C++ 6.0工程向导对话框

（3）向导可以创建4种类型的工程。

An empty project：创建一个空的工程，工程中没有任何源文件和头文件。

A simple application：创建的工程中含有两个源文件（Sample.cpp和StdAfx.cpp）和一个头文件（StdAfx.h），并且Sample.cpp源文件中有一个不做任何操作的main函数。

A "Hello,World!" application：创建的工程中也含有两个源文件（Sample.cpp和StdAfx.cpp）和一个头文件（StdAfx.h），但Sample.cpp源文件中的main函数有一条输出Hello,World!字符的printf语句。

An application that supports MFC：创建了支持MFC类库的工程。MFC类库由微软公司开发，使用MFC类库可以加快程序开发的速度。

（4）选择A "Hello,World!" application工程类型，单击Finish按钮，向导会创建能够在控制台输出Hello World!字符串的应用程序。创建完的工程如图1.12所示。

图1.12　创建完的工程

（5）此时通过选择Build→Execute命令执行应用程序就可以看到程序运行结果，如图1.13所示。

图1.13　程序运行结果

2．创建空工程，手动添加代码文件实现在控制台输出字符串Hello　World!

创建步骤如下：

（1）启动Visual C++ 6.0，选择File→New命令，弹出创建工程向导。

（2）在列表中选择Win32 Console Application工程类型，在Project name中输入工程名Sample，在Location中设置工程的保存路径D:\桌面\LIZI\SAMPLE\Sample。然后单击OK按钮，弹出Win32 Console Application-Step 1of 1对话框。

（3）在弹出的对话框中选择An empty project工程类型，单击Finish按钮，向导会创建一个空的工程。

（4）通过向导向工程中添加源文件。选择File→New命令，弹出创建工程向导，选择Files选项卡，在列表中选择C++ Source File，在File中输入文件名Sample，如图1.14所示。

图1.14　添加文件对话框

（5）单击OK按钮后，向导会向工程中添加Sample.cpp文件。

（6）在Sample文件中输入如下代码：

（7）选择Build→Execute命令执行应用程序就可以看到程序运行结果。

1.6　编译与连接过程

开发应用程序可以分为编辑、编译、连接、执行4个步骤。

（1）编辑

编辑就是在文本编辑器中输入代码，并对代码字符进行增、删、改，然后将输入的内容保存成文件。如图1.15所示，输入Hello World程序代码，并将代码保存成Sample.cpp文件。

图1.15　编辑代码

（2）编译

编译就是将代码文件编译成目标文件。如图1.16所示，编译过程就是将Sample.cpp编译成Sample.obj。

在Visual C++ 6.0开发环境中，单击编译按钮后Visual C++ 6.0开发环境对输入的代码进行编译，如图1.17所示。

图1.16　编译文件

图1.17　执行编译命令

单击编译按钮后Visual C++ 6.0开发环境自动对代码进行编译和连接，整个编译过程如图1.18所示。

图1.18　编译过程

（3）连接

连接就是将编译后的目标文件连接成可执行的应用程序。如将Sample.obj和lib库文件连接成Sample.exe可执行程序。lib库是编译好的提供给用户使用的目标模块，在有多个源文件的工程，例如Sample1.cpp、Sample2.cpp、Sample3.cpp，会编译成多个目标模块Sample1.obj、Sample2.obj、Sample3.obj，链接器会将程序所涉及的目标模块连接成可执行程序，如图1.19所示。

图1.19　连接过程

（4）执行

执行就是执行生成的应用程序。Visual C++ 6.0开发环境下集成了运行按钮，单击运行按钮后开发环境自动执行生成的程序，运行按钮如图1.20所示。

图1.20　运行按钮

1.7　C++的特点

C++语言的运算符十分丰富，共有30多个，有算术、关系、逻辑、位、赋值、指针、条件、逗号、下标、类型转换等多种类型。

C++语言的数据结构多样，有整型、实型、字符型、枚举类型等基本类型，有数组、结构体、共用体等构造类型以及指针类型，还为用户提供了自定义数据类型，能够实现复杂的数据结构，还可以定义类实现面向对象编程，类和指针结合可以实现高效的程序。

C++语言的控制语句形式多样，使用方便。有两路分支、多路分支和虚幻结构几种控制语句，便于结构化模块的实现和控制，结合面向对象编程便于程序的编制和维护。

C++语言是一种面向对象的程序设计语言，采用抽象和实际相结合，各对象间使用消息进行通信，对象通过继承方法增加了代码的复用。

C++语言继承了C语言的特性，可以直接访问地址，进行位运算，从而能对硬件进行操作。C++语句具有编写简单方便、便于理解的优点，还具有低级语言的与硬件结合紧密的优点。

C++语句具有很强的移植性，用C++编写的程序基本不用太多修改就可以用于不同型号的计算机上，C++标准可在多种操作系统下使用。

1.8　小结

任何编程语言都有它的时代性，都是不断发展的，C++现在是一个成熟的语言，首先要理解C++语言新的编程理念，然后选择自己喜欢的开发环境，可以选择微软公司的Visual C++ 6.0，还可以选择Dev-C或Eclipse。

第2章

数据类型

（视频讲解： 1小时25分钟）

数据类型是C++语言的基础，要学习一门编程语言首先要掌握它的数据类型，不同的数据类型所占用的内存空间不同，合理定义数据类型可以优化程序的运行，本章将介绍数据类型及数据类型的输出。

通过阅读本章，您可以：

了解C++程序的各部分

掌握常量和变量使用

掌握数据类型的分类

了解流的输入与输出

2.1　第一个C++程序

学习编程的第一步是先写一个最简单的程序。学习任何编程语言的开始都需要写一个输出Hello World！程序。下面是一个最简单的C++程序，同样也是一个输出Hello World！程序。

最简单的程序输出结果如图2.1所示。

图2.1　最简单的程序输出结果

最简单的C++程序中包含了头文件引用、应用命名空间、主函数、字符串常量、数据流等几部分，这些都是C++程序中经常用到的。下面对C++常用的概念进行介绍。

2.1.1　#include指令

C++程序中带“#”号的语句被称为宏定义或预编译指令，关于什么是C++中的语句，什么是宏定义或预编译指令会在后面章节讲到。#include在代码中是包含和引用的意思，第一行代码#include<iostream>是说明代码要引用iostream文件内容，编译器在编译程序的时候会将iostream中的内容在#include <iostream>处展开。

2.1.2　注释

代码注释是禁止语句的执行，编译器不会对注释的语句进行编译。C++中有两种注释方法，其中“／／”是单行注释，单行注释只能注释符号“／／”后面的内容，到本行代码结束的位置结束；“／**／”是多行注释，多行注释的使用方法是符号“／*”放在将要注释代码的前面，符号“*／”放在将要注释代码的末尾，符号“／*”和“*／”中间的内容就会被注释，另外多行注释中不允许嵌套多行注释，例如／*／**／*／，最后出现的“*／”符号将会无效。在第一个C++程序中加入注释，代码如下：

注释不仅是在调试时使用，开发人员也可以在代码中加入注释，用来说明代码的用意，这样方便日后自己或别人查看。

2.1.3　main函数

单词main代表主函数的意思，main函数是程序执行的入口，程序从main函数的一条指令开始执行，直到main函数结束，整个程序也将执行结束。注意函数的格式，单词main后面有个小括号“（）”，小括号内是放参数的地方。函数相关的内容将在后面章节讲到。

2.1.4　函数体

大括号“{ }”中的内容是需要执行的内容，称为函数体，函数体是按代码的先后顺序执行的，写在前面的代码先执行，写在后面的代码后执行。代码cout << "Hello World！\n"；表示通过输出流输出英文语句Hello World！，Hello World！两边的双引号代表该英文语句是字符串常量，cout表示输出流，<<表示将字符串传送到输出流中。

2.1.5　函数返回值

单词void表示函数的返回值，函数的返回值是用来判断函数执行情况以及返回函数执行结果的。void代表不返回任何数据，如果要返回数据还需要使用return语句。

2.2　常量及符号

在程序运行过程中，其值不能改变的量称为常量。常量可分为整型常量、实型常量、字符常量、字符串常量和其他常量。

上面代码通过cout向屏幕输出4行内容。cout是输出流，实现向屏幕输出不同类型的数据。代码中2009是整型常量，2.14是浮点常量（实型常量），'a'是字符常量，"Hello World"是字符串常量。

2.2.1　整型常量

整型常量可以分为有符号整型常量和无符号整型常量。

-225代表一个负数，+1024代表一个正整数，正整数前面的“+”符号可以省略，即+1024和1024表示的意义相同。

由于基本的数据类型里除了整型外，还有长整型和短整型，因此整型常量也有长整型常量和短整型常量之别。长整型常量不是可以无限大的，它的最大值是有限定的，根据CPU寄存器位数的不同以及编译器的不同，最大的整型常量值也会不同。

注意

4294967295是32位CPU寄存器以及Visual C++ 6.0编译器所允许的最大正整数。

整型常量在编写代码时不仅可以写成十进制整数形式，也可以写成十六进制或八进制整数形式。

（1）八进制形式整型常量必须以0开头，即以0作为八进制数的前缀，每位取值范围是0～7。八进制数通常是无符号数。

以下各数是合法的八进制数：

016、0101、0127。

以下各数不是合法的八进制数：

256无前缀0，它代表十进制整型常量。

0396中数字9不是八进制应有的取值。

（2）十六进制整型常量的前缀为0X或0x，其数码取值范围为0～9，以及A～F或a～f。

以下各数是合法的十六进制整常数：

0X2A1、0XC5、0XFFFF。

以下各数不是合法的十六进制整常数：

5A无前缀0X。

0X3N中含有非十六进制数N。

注意

合法主要指能通过编译器编译，非法就是不能通过编译器编译。

2.2.2　实型常量

实型常量也称为浮点数，只能采用十进制形式表示。它有两种表示形式，即小数表示法和指数表示法。

（1）小数表示法

使用小数表示法，实型常量由整数部分和小数部分组成，整数部分和小数部分每位取值范围是0～9，中间用小数点分隔。例如，0.0、2.25、0.00596、5.0、536.、-5.3、-0.002等均为合法的实型常量。

整数部分和小数部分有时可以不必同时出现，例如.2和2.。

（2）指数表示法

指数表示法也称科学记数法，指数部分以符号“e”或“E”开始，但必须是整数，并且符号“e”或“E”两边都必须有一个数，例如，1.2e20和-2.4e-2。

以下不是合法的实型常量：

E5是E之前无数字。

3E2.5是E后面有小数。

说明

在字母e（或E）之前的小数部分中，小数点左边应有一位（且只能有一位）非零的数字，称为规范化的指数形式。

指数表示法中2.3e3表示2.3×10 3 。

L代表是长整型。L可以是大写也可以是小写，在编写代码的时候可以不写。此类的符号还有U和u代表无符号。例如，255U或255u都代表无符号整型常量255。

符号L或l与符号U或u可以一起使用。例如，65536lu代表无符号长整型常量65536。

C++编译系统把这种带有L或U形式表示的浮点数按双精度常量处理，在内存中占8个字节。如果在实数的数字之后加字母F或f，表示此数为单精度浮点数，如1234F和-43f，占4个字节。如果加字母L或l，表示此数为长双精度数（long double）。

2.2.3　字符常量

字符常量是用单引号括起来的一个字符，例如，'a'和'?'都是合法字符常量。在对代码编译时，编译器会根据ASCII码表将字符常量转换成整型常量。字符'a'的ASCII码值是97，字符'A'的ASCII码值是65，字符'?'的ASCII码值是63。ASCII码表中还有很多通过键盘无法输入的字符，可以使用'\ddd'或'\xhh'来引用这些字符。可以使用'\ddd'或'\xhh'来引用所有ASCII码表中的字符。ddd是1～3位八进制数所代表的字符，\xhh是1～2位十六进制数所代表的字符。例如，'\101'表示ASCII码A,\xOA表示换行等。

转义字符应用。示例如下：

示例运行结果如图2.2所示。

图2.2　运行结果

转义字符是特殊的字符常量，使用时以字符“\”代表开始转义，和后面不同的字符表示转义后的字符。转义字符见表2.1。

表2.1　转义字符说明

2.2.4　字符串常量

字符串常量是由一对双引号括起来的零个或多个字符序列，例如"welcome to our school" "hello girl"。" "可以表示一个空字符串。

同样，' ' 可以表示空字符，而NULL是一种特殊的数据类型，表示空的意思。有的编译器把它编译成零，有的则编译成其他值。

字符串常量实际上是一个字符数组，可以将字符串分解成若干个字符，字符的数量是字符串的长度。字符串常量一般都是用来给字符数组变量赋值或是直接作为实参传递，为告知编译器字符串已经结束，一般在给字符数组赋初值时在字符串的末尾加上字符'\0'，表示字符结束，如果不加字符结束标志，有可能会出现意想不到的错误。

字符常量'A'与字符串常量"A"是不同的，字符串常量"A"是由'A'和'\0'两个字符组成的，字符串的长度是2，字符常量'A'只是一个字符，没有长度。

2.2.5　其他常量

前面讲到的都是普通的常量，常量还包括布尔（bool）常量、枚举常量、宏定义常量等。

布尔常量：布尔常量只有两个，一个是true，表示真；一个是false，表示假。

枚举常量：枚举型数据中定义的成员也都是常量，这将在后文介绍。

宏定义常量：通过#define宏定义的一些值也是常量，例如：

其中PI就是常量。

2.3　变量

2.2节介绍了常量，知道常量的值是不可改变的，所以称为常量，那么值可以改变的量是否叫变量呢？答案是肯定的，本节将介绍变量的相关知识。

2.3.1　标识符

标识符（identifier）是用来对C++程序中的常量、变量、语句标号以及用户自定义函数的名称进行标识的符号。

标识符命名规则如下：

（1）由字母、数字及下画线组成，且不能以数字开头。

（2）大写和小写字母代表不同意义。

（3）不能与关键字同名。

（4）尽量“见名知义”，应该受一定规范的约束。

以下是不合法的标识符：

6A、ABC*（不能使用*）、case（是保留字）。

C++有许多保留关键字，见表2.2。

表2.2　C++保留关键字

2.3.2　变量与变量声明

变量是指程序在运行时其值可改变的量。每个变量都由一个变量名标识，每个变量又具有一个特定的数据类型。

变量使用之前一定要定义或声明，变量声明的一般形式如下：

类型是变量类型说明符，说明变量的数据类型。修饰符是任选的，可以没有。

多个同一类型的变量可以在一行中声明，不同变量名用逗号运算符隔开，例如：

与：

两者等价。

2.3.3　整型变量

整型变量可以分为短整型、整型和长整型，变量类型说明符分别是short、int、long。根据是否有符号还可分为以下6种。

整型：[signed] int。

无符号整型：unsigned [int]。

有符号短整型：[signed] short [int]。

无符号短整型：unsigned short [int]。

有符号长整型：[signed] long [int]。

无符号长整型：unsigned long [int]。

方括号中的关键字表示可以省略，例如，[signed] int可以写成int。

短整型short，在内存中占用两个字节的空间，可以表示数的范围是-32768～32767，如果是无符号短整型unsigned short，表示数的范围是0～65535。整型int占用4个字节的空间，有符号整型表示数的范围是-2147483648～2147483648，无符号整型unsigned int表示数的范围是0～4294967295。长整型与整型占用字节数相同，表示数的范围也相同，具体见表2.3。

表2.3　整型变量范围

2.3.4　实型变量

实型变量又可称为浮点型变量，可分为单精度（float）、双精度（double）和长双精度（long double）3种。

Visual C++ 6.0中，对float提供6位有效数字，对double提供15位有效数字，并且float和double的数值范围不同。对float分配4个字节，对double和long double分配8个字节。

（1）单精度

变量类型说明符为float，该实型数据在内存中占用4个字节，表示的数值范围是-3.4×10 -38 ～3.4×10 38 。例如：

（2）双精度

变量类型说明符为double，该实型数据在内存中占用8个字节，表示的数值范围是-1.7e308～1.7e308。例如：

（3）长双精度

变量类型说明符为long double，该实型数据在内存中占用10个字节，表示的数值范围是-1.1e4932～1.1e4932。例如：

2.3.5　变量赋值

变量值是动态改变的，每次改变都需要进行赋值运算。变量赋值的形式如下：

变量名标识符是声明变量时定义的，表达式将在后面的章节中讲到，例如：

声明i是一个整型变量，100是一个常量。

以上代码首先声明了两个变量i，j，然后为变量i赋初值，最后再将i的值赋给j，最终j的值也是100。

2.3.6　变量赋初值

可以在声明变量的时候就把数值赋给变量，这个过程叫变量赋初值，赋初值的情况有以下几种：

（1）int x=5；

表示定义x为有符号的基本整型变量，赋初值为5。

（2）int x，y，z=6；

表示定义x、y、z为有符号的基本整型变量，z赋初值为6。

（3）int x=3，y=3，z=3；

表示定义x、y、z为有符号的基本整型变量，且赋予的初值均为3。

注意

定义变量并赋初值时可以写成int x=3，y=3，z=3；，但不可写成int a=b=c=3；这种形式。

2.3.7　字符变量

字符变量的类型说明符为char，一个字符变量在内存中占用1个字节，例如：

字符变量值在内存中存储的是字符的ASCII码，即一个无符号整数，其形式与整型变量的存储形式一样，字符型数据与整型数据之间通用。

（1）一个字符型数据，既可以字符形式输出，也可以整数形式输出。

【实例2.1】 字符型数据与整型数据间运算。 （实例位置：资源包\TM\sl\2\1）

程序运行结果如图2.3所示。

图2.3　字符型数据与整型数据间运算

（2）允许对字符数据进行算术运算，此时就是对它们的ASCII码值进行算术运算。

【实例2.2】 字符型数据进行算术运算。 （实例位置：资源包\TM\sl\2\2）

程序运行结果如图2.4所示。

图2.4　字符型数据进行算术运算

2.4　常用数据类型

计算机能够运行高低电平组成的二进制数据，开发人员编写的程序代码需要经过编译器转换成二进制数据，才能被计算机识别并执行，这种由编译器将代码转换成计算机能识别的二进制数据的过程称为编译过程。

能被计算机识别的二进制数据被称为机器码，汇编语言是最接近机器码的一种低级语言，汇编语言的编译器是将汇编代码直接解释成机器码，所谓解释就是一种对应关系，一条语句代表一个机器码。通过汇编代码就可以很清楚地了解程序在CPU的执行过程。

计算机的运算是通过CPU完成的，执行运算过程时首先需要将数据存放到CPU的寄存器中，然后CPU根据机器码指令执行运算。存放到CPU寄存器中的数据都是从内存中读取的，内存是存储数据的地方，程序运行时，被编译器编译后的二进制数据会被读取到计算机内存中，然后由CPU执行。

数据类型就是决定用多大的内存来存储用户的数据。汇编语言中没有数据类型这个概念，汇编语言中除了直接操作用户数据外，都是通过地址值来操作用户数据。在高级语言中通过地址查找用户数据的任务都由编译器来完成，开发人员不用亲自管理地址空间，不用思考哪些数据应该存储在哪块内存地址下。

C++是数据类型非常丰富的语言，常用数据类型如图2.5所示。

图2.5　常用数据类型

掌握C++语言的数据类型和运算符是学习C++语言的基础。本节将详细介绍这些数据类型。

2.4.1　定义数值类型

C++语言中数值类型主要分为整型和实型（浮点类型）两大类。其中，整型按符号划分，可以分为有符号和无符号两大类；按长度划分，可以分为普通整型、短整型和长整型3类，见表2.4所示。

表2.4　整数类型

实型主要包括单精度型、双精度型和长双精度型，见表2.5。

表2.5　实数类型

在程序中使用实型数据时需要注意以下几点：

（1）实数的相加

实型数据的有效数字是有限制的，如单精度float的有效数字是6位或7位，如果将数字86041238.78赋值给float类型，显示的数字可能是86041240.00，个位数8被四舍五入，小数位被忽略。如果将86041238.78与5相加，输出的结果为86041245.00，而不是86041243.78。

（2）实数与零的比较

在开发程序的过程中，经常会进行两个实数的比较，此时尽量不要使用“==”或“!=”运算符，而应使用“>=”或“<=”之类的运算符，许多程序开发人员在此经常犯错，例如：

上述代码并不是高质量的代码，如果程序要求的精度非常高，可能会产生未知的结果。通常在比较实数时需要定义实数的精度。

利用实数精度进行实数比较，示例如下：

执行结果如图2.6所示。

图2.6　执行结果

2.4.2　字符类型

在C++语言中，字符数据使用“' '”来表示，如'A'、'B'、'C'等。定义字符变量可以使用char关键字，例如：

在计算机中字符是以ASCII码的形式存储的，因此可以直接将整数赋值给字符变量，例如：

输出结果为a，因为97对应的ASCII码为a。

2.4.3　布尔类型

在逻辑判断中，结果通常只有真和假两个值。C++语言中提供了布尔类型bool来描述真和假。bool类型共有两个取值，分别为true和false。顾名思义，true表示真，false表示假。在程序中，bool类型被作为整数类型对待，false表示0，true表示1。将bool类型赋值给整型是合法的，反之，将整型赋值给bool类型也是合法的，例如：

2.5　数据输入与输出

在用户与计算机进行交互的过程中，数据输入和数据输出是必不可少的操作过程，计算机需要通过输入获取来自用户的操作指令，并通过输出来显示操作结果。本节将介绍数据输入与输出的相关内容。

2.5.1　控制台屏幕

在NT内核的Windows操作系统中为保留DOS系统的风格，提供了控制台程序，通过系统的“开始”→“运行”命令，运行cmd.exe程序，可以启动控制台程序。在控制台中可以运行DIR、CD、DELETE等DOS系统中的文件操作命令，也可以用来启动Windows的程序。控制台屏幕如图2.7所示。

图2.7　控制台

使用Visual C++ 6.0创建的控制台工程的程序都将运算结果输出到如图2.7所示的控制台屏幕上，它是程序显示输出结果的地方。

2.5.2　C++语言中的流

在C++语言中，数据的输入和输出包括标准输入／输出设备（键盘、显示器）、外部存储介质（磁盘）上的文件和内存的存储空间3个方面的输入／输出。对标准输入设备和标准输出设备的输入／输出简称为标准I/O，对在外存磁盘上文件的输入／输出简称为文件I/O，对内存中指定的字符串存储空间的输入／输出简称为串I/O。

C++语言中把数据之间的传输操作称为流。C++中的流既可以表示数据从内存传送到某个载体或设备中，即输出流；也可以表示数据从某个载体或设备传送到内存缓冲区变量中，即输入流。C++中所有流都是相同的，但文件可以不同（文件流会在后面讲到）。使用流以后，程序用流统一对各种计算机设备和文件进行操作，使程序与设备、文件无关，从而提高了程序设计的通用性和灵活性。

C++语言定义了I/O类库供用户使用，标准I/O操作有4个类对象，它们分别是cin、cout、cerr和clog。其中cin代表标准输入设备键盘，也称为cin流或标准输入流。cout代表标准输出显示器，也称为cout流或标准输出流，当进行键盘输入操作时使用cin流，当进行显示器输出操作时使用cout流，当进行错误信息输出操作时使用cerr或clog。

C++的流通过重载运算符“<<”和“>>”执行输入和输出操作。输出操作是向流中插入一个字符序列，因此，在流操作中，将左移运算符“<<”称为插入运算符。输入操作是从流中提取一个字符序列，因此，将右移运算符“>>”称为提取运算符。

（1）cout语句的一般格式

cout代表显示器，执行cout << x操作就相当于把x的值输出到显示器。

先把x的值输出到显示器屏幕上，在当前屏幕光标位置显示出来，然后cout流恢复到等待输出的状态，以便继续通过插入操作符输出下一个值。当使用插入操作符向一个流输出一个值后，再输出下一个值时将被紧接着放在上一个值的后面，所以为了让流中前后两个值分开，可以在输出一个值后接着输出一个空格，或一个换行符，或是其他所需要的字符或字符串。

一个cout语句可以分写成若干行，例如：

可以写成：

也可写成多个cout语句：

以上3种情况的输出均为正确。

（2）cin语句的一般格式

cin代表键盘，执行cin>>x就相当于把键盘输入的数据赋值给变量。

当从键盘上输入数据时，只有当输入完数据并按下回车键后，系统才把该行数据存入到键盘缓冲区，供cin流顺序读取给变量。另外，从键盘上输入的每个数据之间必须用空格或回车符分开，因为cin为一个变量读入数据时是以空格或回车符作为其结束标志的。

当cin>>x操作中的x为字符指针类型时，则要求从键盘的输入中读取一个字符串，并把它赋值给x所指向的存储空间，若x没有事先指向一个允许写入信息的存储空间，则无法完成输入操作。另外，从键盘上输入的字符串，其两边不能带有双引号定界符，若有则只作为双引号字符看待。对于输入的字符也是如此，不能带有单引号定界符。

cin相当于C库函数的scanf，将用户的输入赋值给变量，示例如下：

示例将用户输入的数打印出来。

【实例2.3】 简单输出字符。 （实例位置：资源包\TM\sl\2\3）

程序运行将向控制台屏幕输出变量i值和HelloWorld字符串，运行效果如图2.8所示。

图2.8　向控制台屏幕输出HelloWorld字符串

endl是向流的末尾部位加入换行符。i是一个整型变量，在输出流中自动将整型变量转换成字符串输出。

2.5.3　流操作的控制

在头文件iomanip.h中定义了一些控制流输出格式的函数，默认情况下整型数按十进制形式输出，也可以通过hex将其设置为十六进制输出。流操作的控制具体函数如下：

（1）long setf(long f);

根据参数f设置相应的格式标志，返回此前的设置。该参数f所对应的实参为无名枚举类型中的枚举常量（又称格式化常量），可以同时使用一个或多个常量，每两个常量之间要用按位或操作符连接。如需要左对齐输出，并使数值中的字母大写时，则调用该函数的实参为ios::left|ios::uppercase。

（2）long unsetf(long f);

根据参数f清除相应的格式化标志，返回此前的设置。如果要清除此前的左对齐输出设置，恢复默认的右对齐输出设置，则调用该函数的实参为ios::left。

（3）int width();

返回当前的输出域宽。若返回数值为0则表明没为刚才输出的数值设置输出域宽。输出域宽是指输出的值在流中所占有的字节数。

（4）int width(int w);

设置下一个数据值的输出域宽为w，返回为输出上一个数据值所规定的域宽，若无规定则返回0。注意，此设置不是一直有效，而只是对下一个输出数据有效。

（5）setiosflags(long f);

设置f所对应的格式标志，功能与setf(long f)成员函数相同，当然，在输出该操作符后返回的是一个输出流。如果采用标准输出流cout输出它时，则返回cout。输出每个操作符后都是如此，即返回输出它的流，以便向流中继续插入下一个数据。

（6）resetiosflags(long f);

清除f所对应的格式化标志，功能与unsetf(long f)成员函数相同。输出后返回一个流。

（7）setfill(int c);

设置填充字符的ASCII码为c的字符。

（8）setprecision(int n);

设置浮点数的输出精度为n。

（9）setw(int w);

设置下一个数据的输出域宽为w。

数据输入／输出的格式控制还有更简便的形式，就是使用头文件iomainip.h中提供的操作符。使用这些操作符不需要调用成员函数，只要把它们作为插入操作符“<<”的输出对象即可。

dec：转换为按十进制输出整数，是默认的输出格式。

oct：转换为按八进制输出整数。

hex：转换为按十六进制输出整数。

ws：从输出流中读取空白字符。

endl：输出换行符\n并刷新流。刷新流是指把流缓冲区的内容立即写入到对应的物理设备上。

ends：输出一个空字符\0。

flush：只刷新一个输出流。

【实例2.4】 控制打印格式程序。 （实例位置：资源包\TM\sl\2\4）

程序运行结果如图2.9所示。

图2.9　控制打印格式程序

【实例2.5】 整数输出的例子。 （实例位置：资源包\TM\sl\2\5）

程序运行结果如图2.10所示。

图2.10　整数输出

【实例2.6】 输出大写的十六进制。（ 实例位置：资源包\TM\sl\2\6）

程序运行结果如图2.11所示。

图2.11　输出大写的十六进制

【实例2.7】 控制输出精确度。 （实例位置：资源包\TM\sl\2\7）

程序运行结果如图2.12所示。

图2.12　控制输出精确度

【实例2.8】 流输出小数控制。 （实例位置：资源包\TM\sl\2\8）

程序运行结果如图2.13所示。

图2.13　流输出小数控制

C++语言中还保留着C语言中的屏幕输出函数printf。使用printf可以将任意数量类型的数据输入到屏幕。printf函数的声明形式如下：

printf是变参函数，数值列表中可以有多个数值，数值的个数不是确定的，每个数值之间用逗号运算符隔开；控制格式表示数值以哪种格式输出，控制格式的数量要与数值的个数一致，否则程序运行时会产生错误。

控制格式由“%+特定字符”构成，形式如下：

*代表可以使用占位符，域宽表示输出的长度。如果输出的内容没有域宽长，用占位符占位；如果比域宽长，就按实际内容输出，以适应域宽。长度决定输出内容的长度，例如，%d代表以整型数据格式输出。输出类型见表2.6。

表2.6　输出类型

（1）d格式符，以十进制形式输出整数。有以下几种用法：

%d，按整型数据的实际长度输出。

%*md，m为指定的输出字段的宽度。如果数据的位数小于m，用*所指定的字符占位，如果*未指定用空格占位，若大于m，则按实际位数输出。

%ld，输出长整型数据。

【实例2.9】 输出占位符。（ 实例位置：资源包\TM\sl\2\9）

程序运行结果如图2.14所示。

图2.14　输出占位符

（2）o格式符，以八进制形式输出整数。有以下几种用法：

%o，按整型数据的实际长度输出。

%*mo，m为指定的输出字段的宽度。如果数据的位数小于m，用*所指定的字符占位，如果*未指定用空格占位，若大于m，则按实际位数输出。

%lo，输出长整型数据。

（3）x格式符，以十六进制形式输出整数。有以下几种用法：

%x，按整型数据的实际长度输出。

%*mx，m为指定的输出字段的宽度。如果数据的位数小于m，用*所指定的字符占位，如果*未指定用空格占位，若大于m，则按实际位数输出。

%lx，输出长整型数据。

（4）s格式符，用来输出一个字符串。有以下几种用法：

%s，将字符串按实际长度输出。

%*ms，输出的字符串占m列，如字符串本身长度大于m，则突破m的限制，将字符串全部输出，如果*未指定用空格占位，若字符串长度小于m，则左补空格。

%-ms，如果字符串长度小于m，则在m列范围内，字符串向左靠，右补空格。

%m.ns，输出占m列，但只取字符串中左端n个字符。这n个字符输出在m列的右侧，左补空格。

%-m.ns，输出长整型数据。输出占m列，但只取字符串中左端n个字符。这n个字符输出在m列的左侧，右补空格。

【实例2.10】 输出格式控制字符串。 （实例位置：资源包\TM\sl\2\10）

程序运行结果如图2.15所示。

图2.15　输出格式控制字符串

（5）f格式符，以小数形式输出实型数据。有以下几种用法：

%f，不指定字段宽度，整数部分全部输出，小数部分输出6位。

%m.nf，输出的数据占m列，其中有n位小数。如果数值长度小于m，则左端补空格。

%-m.nf，输出的数据占m列，其中有n位小数。如果数值长度小于m，则右端补空格。

【实例2.11】 输出格式控制。 （实例位置：资源包\TM\sl\2\11）

程序运行结果如图2.16所示。

图2.16　输出格式控制

（6）e格式符，以指数形式输出实型数据。有以下几种用法：

%e，不指定输出数据所占的宽度和小数位数。

%m.ne，输出的数据占m位，其中有n位小数。如果数值长度小于m，则左端补空格。

%-m.ne，输出的数据占m位，其中有n位小数。如果数值长度小于m，则右端补空格。

【实例2.12】 科学计数法输出。 （实例位置：资源包\TM\sl\2\12）

程序运行结果如图2.17所示。

图2.17　科学计数法输出

2.6　小结

本章主要讲述了C++语言的第一个程序以及常用的数据类型，掌握数据类型对以后编程十分重要。本章还介绍了cin、cout两种流，可以使用流的输出来调试程序，查看输出结果。

2.7　实践与练习

（1）使用字符变量，在控制台屏幕输出Nice Work！。 （实例位置：资源包\TM\sl\2\13）

（2）在自定义的函数中使用static静态局部整型变量，计算3的立方等于多少。（实例位置：资源 包\ TM\sl\2\14）

（3）定义一个整型变量，为其赋值为123，使用两种方法进行输出，一种是用printf函数进行输出，另一种是使用C++语言中的流输出。 （实例位置：资源包\TM\sl\2\15）

第3章

表达式与语句

（视频讲解： 55分钟）

程序是由不同语句组成的，掌握程序语句的写法，也就等于掌握了编程语言的语法。程序主要是用来完成计算的，避免不了要使用运算符。C++语言提供了丰富的运算符，方便开发人员使用，也是C++语言灵活性的体现。本章将讲述程序开发的关键部分——表达式与语句。

通过阅读本章，您可以：

掌握常用的运算符

掌握运算符之间的优先级

了解由不同运算符组成的表达式

了解什么是语句

3.1　运算符

运算符就是具有运算功能的符号。C++语言中有丰富的运算符，其中有很多运算符都是从C语言继承下来的，它新增的运算符有::（作用域运算符）和->（成员指针运算符）。

和C语言一样，根据使用运算符的对象个数，C++语言将运算符分为单目运算符、双目运算符和三目运算符。根据使用运算符的对象之间的关系，将运算符分为算术运算符、关系运算符、逻辑运算符、赋值运算符、位运算符、移位运算符、sizeof运算符、条件运算符和逗号运算符。

3.1.1　算术运算符

算术运算主要指常用的加（+）、减（-）、乘（*）、除（／）四则运算，算术运算符中有单目运算符和双目运算符。算术运算符见表3.1。

表3.1　算术运算符

说明

expr表示使用运算符的对象，可以是表达式、变量和常量。

（1）+是加法运算符，可以进行两个对象的加法运算，示例如下。

1+1：两个常量相加。

i+1：变量和常量相加。

x+y：两个变量相加。

+100：有符号的常量，强调常量是正数。

（2）-是减法运算符，可以进行两个对象的减法运算，示例如下。

1-1：两个常量相减。

j-1：变量和常量相减。

x-y：两个变量相减。

-100：有符号的常量，强调常量是负值。

（3）*是乘法运算符，可以进行两个对象的乘法运算，示例如下。

2*3：两个常量相乘。

（4）／是除法运算符，可以进行两个对象的除法运算，示例如下。

2/3：两个常量相除，／运算符左侧的是被除数，也称分子；／运算符右侧的是除数，也称为分母。

在进行除法运算时，除数或分母不可以为0，为0会产生溢出，处理器抛出异常，示例如下。

2/0：不合法运算。

0/2：合法运算，计算结果是0。

两个整型数值进行除法运算时返回的结果可能是一个小数，小数点后的数值会被舍去。

（5）%是模运算符，求两个整型的数值或变量在进行除法运算后的余数，示例如下。

5/2：两个常量进行求模运算，计算结果是1。

（6）++是自增加运算符，属于单目运算符。有++expr和expr++两种形式，++expr表示expr自身加1后再进行其他运算；expr++表示expr先参加完其他运算后再进行自身加1，expr只能是变量，示例如下。

i++：i参与运算后，i的值再自增。

++i： i自增1后再参与其他运算。

1++：不合法。

（7）--是自减少运算符，属于单目运算符。有--expr和expr--两种形式，--expr表示expr自身减1后再进行其他运算；expr--表示expr先参加完其他运算后再进行自身减1，expr只能是变量，示例如下。

i--：i参与运算后，i的值再自减。

--i：i自减1后再参与其他运算。

1--：不合法。

3.1.2　关系运算符

关系运算主要是对两个对象进行比较，运算结果是逻辑常量真或假。关系运算符见表3.2。

表3.2　关系运算符

（1）<是比较两个对象的大小，前者小于后者，运算结果为真，示例如下。

a<b：两个变量进行比较，如果变量a的值小于变量b的值，运算结果为真。

2<1：运算结果为假。

（2）>是比较两个对象的大小，前者大于后者，运算结果为真，示例如下。

a>b：两个变量进行比较，如果变量a的值大于变量b的值，运算结果为真。

2>1：运算结果为真。

（3）>=是比较两个对象的大小，前者大于或等于后者，运算结果为真，示例如下。

3>=2：运算结果为真。

2>=2：运算结果为真。

（4）<=是比较两个变量和常量的大小，前者小于或等于后者，运算结果为真，示例如下。

1<=2：运算结果为真。

（5）==是对两个对象进行判断，前者恒等于后者，运算结果为真，示例如下。

a==b：两个变量进行比较，如果变量a的值恒等于变量b的值，运算结果为真。

（6）!=是对两个对象进行判断，前者不等于后者，运算结果为真，示例如下。

a!=b：两个变量进行比较，如果变量a的值不等于变量b的值，运算结果为真。

关系运算符都是双目运算符，其结合性均为左结合。关系运算符的优先级低于算术运算符，高于赋值运算符。在6个关系运算符中，<，<=，>，>=的优先级相同，高于==和!=，==和!=的优先级相同。

关于结合性和优先级将在3.2节具体介绍。

3.1.3　逻辑运算符

逻辑运算符是对真和假这两种逻辑值进行运算，运算后的结果仍是一个逻辑值。逻辑运算符见表3.3。

表3.3　逻辑运算符

（1）&&是对两个对象进行与运算，当两个对象都为真时，结果为真；有一个对象为假或两个对象都为假时，结果为假，示例如下。

真 && 假：结果为假。

真 && 真：结果为真。

假 && 假：结果为假。

（2）||是对两个对象进行或运算，当两个对象都为假时，结果为假；有一个对象为真或两个对象都为真时，结果为真，示例如下。

真||假：结果为真。

真 || 真：结果为真。

假||假：结果为假。

（3）!是对一个对象取反运算，当对象为真时，运算结果为假；当对象为假时，运算结果为真，示例如下。

!真：运算结果为假。

!假：运算结果为真。

变量a和b的逻辑运算见表3.4。

表3.4　逻辑运算结果

说明

用1代表真，用0代表假。

其中的表达式仍可以是逻辑表达式，从而组成嵌套的情形，例如，(a||b)&&c是根据逻辑运算符的左结合性。

【实例3.1】 求逻辑表达式的值。 （实例位置：资源包\TM\sl\3\1）

程序运行结果如图3.1所示。

图3.1　运行结果

3.1.4　赋值运算符

赋值运算符分为简单赋值运算符和复合赋值运算符，复合赋值运算符又称为带有运算的赋值运算符，简单赋值运算符就是给变量赋值的运算符，示例如下。

=（等号）就是简单赋值运算符。

C++提供了很多复合赋值运算符，见表3.5所示。

表3.5　复合赋值运算符

复合赋值运算符都有等同的简单赋值运算符和其他运算的组合：

a+=b等价于a=a+b。

a-=b等价于a=a-b。

a*=b等价于a=a*b。

a/=b等价于a=a/b。

a%=b等价于a=a%b。

a<<=b等价于a=a<<b。

a>>=b等价于a=a>>b。

a&=b等价于a=a&b。

a^=b等价于a=a^b。

a|=b等价于a=a|b。

复合赋值运算符都是双目运算符，C++采用这种运算符可以更高效地进行加运算，编译器在生成目标代码时能够直接优化，可以使程序代码更小。这种书写形式也非常简洁，使得代码更紧凑。

复合赋值运算符将运算结果返回，作为表达式的值，同时把expr1对应的变量设为运算结果值，例如：

运算结果是：a的值为30。

a*=5等价于a=a*5，a*5的运算结果作为临时变量赋给了变量a。

3.1.5　位运算符

位运算符有&（位逻辑与）、|（位逻辑或）、^（位逻辑异或）和～（取反运算符），其中位逻辑与、位逻辑或、位逻辑异或为双目运算符，取反运算符为单目运算符。位运算符见表3.6。

表3.6　位运算符

在双目运算符中，位逻辑与优先级最高，位逻辑或次之，位逻辑异或最低。

（1）位逻辑与实际上是将操作数转换成二进制表示方式，然后将两个二进制操作数对象从低位（最右边）到高位对齐，每位求与，若两个操作数对象同一位都为1，则结果对应位为1，否则结果中对应位为0。例如，12和8经过位逻辑与运算后得到的结果是8。

说明

十进制在用二进制表示时有原码、反码、补码多种表示方式。

（2）位逻辑或实际上是将操作数转换成二进制表示方式，然后将两个二进制操作数对象从低位（最右边）到高位对齐，每位求或，若两个操作数对象同一位都为0，则结果对应位为0，否则结果中对应位为1。例如，4和8经过位逻辑或运算后的结果是12。

（3）位逻辑异或实际上是将操作数转换成二进制表示方式，然后将两个二进制操作数对象从低位（最右边）到高位对齐，每位求异或，若两个操作数对象同一位不同时为1，则结果对应位为1，否则结果中对应位为0。例如，31和22经过位逻辑异或运算后得到的结果是31。

（4）取反运算符，实际上是将操作数转换成二进制表示方式，然后将各位二进制位由1变为0，由0变为1。例如，41883取反运算后得到的结果是23652。

位运算符实际上是算术运算符，用该运算符组成的表达式的值是算术值。

3.1.6　移位运算符

移位运算符有两个，分别是<<（左移）和>>（右移），这两个运算符都是双目的。

（1）左移是将一个二进制操作数对象按指定的移动位数向左移，左边（高位端）溢出的位被丢弃，右边（低位端）的空位用0补充。左移相当于乘以2的幂，如图3.2所示。

图3.2　左移位运算

例如，操作数41883的二进制是1010 0011 1001 1011，左移一位变成18230，左移两位变成36460，运行过程如图3.3所示。

图3.3　左移位运算过程

（2）右移是将一个二进制的数按指定的位数向右移动，右边（低位端）溢出的位被丢弃，左边（高位端）的空位或者一律用0填充，或者用被移位操作数的符号位填充，运算结果和编译器有关，在使用补码的机器中，正数的符号位为0，负数的符号为1。右移位运算相当于除以2的幂，如图3.4所示。

图3.4　右移位运算

例如，操作数41883的二进制是1010 0011 1001 1011，右移一位变成20941，右移两位变成10470，运行过程如图3.5所示。

图3.5　右移位运算过程

【实例3.2】 左移运算。 （实例位置：资源包\TM\sl\3\2）

程序运行结果如下：

由于位运算的速度很快，在程序中遇到表达式乘以或除以2的幂的情况，一般采用位运算来代替。

【实例3.3】 使用移位运算。 （实例位置：资源包\TM\sl\3\3）

程序运行结果如图3.6所示。

图3.6　运行结果

3.1.7　sizeof运算符

sizeof运算符是一个很像函数的运算符，也是唯一一个用到字母的运算符。该运算符有以下两种形式：

功能是返回指定的数据类型或表达式值的数据类型在内存中占用的字节数。

说明

由于CPU寄存器的位数不同，同种数据类型占用的内存字节数目可能不同。

示例如下。

返回1，说明char类型占用1个字节。

返回4，说明空指针占用4个字节。

返回4，说明常量占用4个字节。

3.1.8　条件运算符

条件运算符是C++中仅有的一个三目运算符，该运算符需要3个运算数对象，形式如下：

表达式1是一个逻辑值，可以为真或假。若表达式1为真，则运算结果是表达式2，如果表达式1为假，则运算结果是表达式3。这个运算相当于一个if语句。

3.1.9　逗号运算符

C++语言中逗号“，”也是一种运算符，称为逗号运算符。逗号运算符的优先级别最低，结合方向自左至右，其功能是把两个表达式连接起来组成一个表达式。逗号运算符是一个多目运算符，并且操作数的个数不限定，可以将任意多个表达式组成一个表达式。

例如：

3.2　结合性和优先级

运算符优先级决定了在表达式中各个运算符执行的先后顺序。高优先级运算符要先于低优先级运算符进行运算。例如，根据先乘除后加减的原则，表达式a+b*c会先计算b*c，得到结果再与a相加。在优先级相同的情况下，则按从左到右的顺序进行计算。

当表达式中出现了括号时，会改变优先级。先计算括号中的子表达式值，再计算整个表达式的值。

运算符的结合方式有两种，左结合和右结合。左结合表示运算符优先与其左边的标识符结合进行运算，如加法运算；右结合表示运算符优先与其右边的表示符结合，例如单目运算符 +、-。

同一优先级的优先级别相同，运算次序由结合方向决定。例如，1*2/3，*和／的优先级别相同，其结合方向自左向右，则等价于(1*2)/3。

运算符优先级见表3.7。

表3.7　运算符优先级

3.3　表达式

表达式由运算符、括号、数值对象或变量等几个元素构成。一个数值对象是最简单的表达式，一个表达式可以看成一个数学函数，带有运算符的表达式通过计算将返回一个数值，例如：

当表达式有两个或多个运算符时，表达式称为复杂表达式，运算符执行的先后顺序由它们的优先级和结合性决定，例如：

一个表达式的值的数据类型由运算符的种类和操作数的数据类型决定。

带运算符的表达式根据运算符的不同，可以分成算术表达式、关系表达式、条件表达式、赋值表达式、逻辑表达式和逗号表达式等几类。

3.3.1　算术表达式

算术表达式的一般形式如下：

算术表达式由算术运算符把表达式连接而成，其值的计算很简单，其值的数据类型按下述规定确定：若所有运算符数量类型相同，则表达式运算结果的数据类型和操作数的数据类型相同；若操作数的数据类型不同，就需要转换，表达式运算结果的数据类型取最高的数据类型。

3.3.2　关系表达式

关系表达式的一般形式如下：

关系表达式一般只出现在三目运算符、if语句和循环语句的判断条件中。关系表达式的运算结果都是逻辑型，只能取真或假。数值0表示假，非0代表真。

3.3.3　条件表达式

条件表达式的一般形式如下：

条件表达式的值和数据类型取决于？号前表达式的真假，若为真，则整个表达式的运算结果和数据类型和冒号前的操作数相同；若为假，则整个表达式的值和数据类型与冒号后的操作数相同。

3.3.4　赋值表达式

赋值表达式的一般形式如下：

赋值表达式的结果是最左边赋值运算符左边变量（或者表达式）的值。由于赋值运算符的结合性是从右至左，因此可以出现连续赋值的表达式。

3.3.5　逻辑表达式

逻辑表达式的一般形式如下：

逻辑表达式用逻辑运算符将关系表达式连接起来。逻辑表达式的值也是逻辑型，只能取真或假。

其中的表达式可以又是逻辑表达式，从而组成嵌套的情形。例如，(a||b)&&c根据逻辑运算符的左结合性，也可写为a||b&&c。逻辑表达式的值是式中各种逻辑运算的最后值，以1和0分别代表真和假。

逻辑表达式注意事项如下：

（1）逻辑运算符两侧的操作数，除可以是0和非0的整数外，也可以是其他任何类型的数据，如实型、字符型等。

（2）在计算逻辑表达式时，只有在必须执行下一个表达式才能求解时，才求解该表达式，也就是说并不是所有的表达式都被求解。

对于逻辑与运算，如果第一个操作数被判定为假，系统不再判定或求解第二操作数。

对于逻辑或运算，如果第一个操作数被判定为真，系统不再判定或求解第二操作数。

3.3.6　逗号表达式

C++语言中逗号“，”也是一种运算符，称为逗号运算符。逗号运算符的优先级别最低，结合方向自左至右，其功能是把两个表达式连接起来组成一个表达式，称为逗号表达式。

其一般形式如下：

其求值过程是先求解表达式1，再求解表达式2，并以表达式2的值作为整个逗号表达式的值。

逗号表达式的一般形式可以扩展为：

该逗号表达式的值为表达式n的值。

整个逗号表达式的值和类型由最后一个表达式决定。计算一个逗号表达式的值时，从左至右依次计算各个表达式的值，最后计算的一个表达式的值和类型便是整个逗号表达式的值和类型。

逗号表达式的用途仅在于解决只能出现一个表达式的地方却要出现多个表达式的问题。

【实例3.4】 逗号运算符应用。 （实例位置：资源包\TM\sl\3\4）

程序运行结果如图3.7所示。

图3.7　逗号运算符应用

实例3.4，中，在变量赋初值时、for循环语句中、printf打印语句中，多处用到了逗号表达式。其中res1=a,res2=b+c;比较难理解，res2等于整个逗号表达式的值，也就是表达式2的值，res1是第一个表达式的值。

逗号表达式的注意事项如下。

（1）逗号表达式可以嵌套。

嵌套的逗号表达式可以转换成扩展形式，扩展形式如下：

整个逗号表达式的值等于表达式n的值。

（2）程序中使用逗号表达式，通常是要分别求逗号表达式内各表达式的值，并不一定要求整个逗号表达式的值。

（3）并不是在所有出现逗号的地方都组成逗号表达式，如在变量说明中，函数参数表中逗号只是用作各变量之间的间隔符。

3.3.7　表达式中的类型转换

变量的数据类型转换的方法有两种：一种是隐式转换，一种是强制类型转换。

1．隐式转换

隐式转换发生在不同数据类型的量混合运算时，由编译系统自动完成。

隐式转换遵循以下规则：

（1）若参与运算量的类型不同，则先转换成同一类型，然后进行运算。赋值时会把赋值类型和被赋值类型转换成同一类型，一般赋值号右边量的类型将转换为左边量的类型。如果右边量的数据类型长度比左边长时，将丢失一部分数据，这样会降低精度，丢失的部分按四舍五入向前舍入。

（2）转换按数据由低到高顺序执行，以保证精度不降低。

int型和long型运算时，先把int量转成long型后再进行运算。

所有的浮点运算都是以双精度进行的，即使仅含float单精度量运算的表达式，也要先转换成double型，再进行运算。

char型和short型参与运算时，必须先转换成int型。

类型转换的顺序如图3.8所示。

图3.8　数据类型转换

【实例3.5】 隐式类型转换。 （实例位置：资源包\TM\sl\3\5）

程序运行结果如下：

2．强制类型转换

强制类型转换是通过类型转换运算来实现的，其一般形式如下：

或：

其功能是把表达式的运算结果强制转换成类型说明符所表示的类型，例如：

表示把x转换为单精度型。

表示把x+y的结果转换为整型。

表示一个整数。

强制类型转换后不改变数据说明时对该变量定义的类型，例如：

x仍为双精度类型。

使用强制转换的优点是编译器不必自动进行两次转换，而由程序员负责保证类型转换的正确性。

【实例3.6】 强制类型转换应用。 （实例位置：资源包\TM\sl\3\6）

程序运行结果如下：

3.4　语句

在C++程序中，语句是最小的可执行单元，一条语句由一个分号结束。

C++程序语句按其功能可以划分为两类：一类是用于描述计算机执行操作运算的，称为操作运算语句；另一类是用于控制操作运算执行顺序的，称为流程控制语句。任何程序设计语句都具备流程控制的功能。基本的控制结构有3种：顺序结构、选择结构和循环结构。

顺序结构指按照语句在程序中的先后次序一条一条顺次执行。顺序结构是自然形成的，不需要控制，按默认的顺序执行，顺序控制语句就是一条简单的语句。

（1）表达式语句

表达式语句是由表示式后面加上一个分号组成的。表达式有很多种，如关系表达式、逻辑表达式、算术表达式等，但关系表达式、逻辑表达式多用于循环或选择结构中，只有赋值表达式多用于赋值语句。赋值语句是赋值表达式后面加上一个分号可以形成赋值语句，将右边的表达式（算术表达式）的结果赋给左边的变量。一个赋值语句中可以包含多个赋值表达式。

（2）空语句

空语句只有一个分号，表示什么也不做。空语句经常出现在选择或循环语句中，表示某个分支或循环体不执行具体的操作，也用于编制程序的初始阶段，在搭建程序的模块框架中，先用空语句占位，接下来再逐步细化和补充，例如：

上面是一个循环语句，表示当变量a小于变量b时，在括号中的循环体中要进行什么操作，但不确定循环体应该实现什么功能，所以需要使用空语句占位。空语句语法上是正确的。

（3）复合语句

复合语句是若干条语句的一个集合，它在语法上是一个整体，相当于一个语句，其语法形式是由一对大括号{}将若干条语句括起来。复合语句经常出现在选择或循环结构中，选择语句的分支和循环语句的循环体由多条语句组成时，用大括号括起来形成一个复合语句，起到层次划分的作用。一个大括号形成了一个范围，这个范围也是变量的作用范围，也可以将大括号内的代码称之为程序段。在能使用简单语句的地方，都能够使用复合语句。在一个复合语句中可以包含另外一个或多个复合语句，例如：

一个复合语句的大括号外面不能再写分号。

（4）函数调用语句

函数由函数名、带实际参数表的圆括号组成，函数调用语句就是在函数后加上一个分号。调用主要指，程序执行到函数调用语句时，会跳转到相应的函数体中去执行，执行该函数体中的内容，执行完所有内容后返回到函数调用语句处，执行调用语句下面的语句。可以调用的函数主要有用系统库函数和自定义函数。

顺序、选择、循环是结构化程序的3种基本结构。选择结构语句、循环结构语句会在后面章节讲到。

3.5　判断左值与右值

C++中的每个语句、表达式的结果分为左值与右值两类，左值指的是内存当中持续储存的数据，而右值是临时储存的结果。

在程序中，我们声明过的独立的变量，举例如下：

它们都是左值。又如：

c-b是一个储存表达式结果的临时数据，它的结果将被复制到a中，它是一个右值。a++自增的过程实质上是一个临时变量执行了表达式，而a的值已经自增了。++a恰好相反，它是自增之后的a，是一个左值。由此可见，c--是一个右值。

左值都可以出现在表达式等号的左边，所以称为左值，若表达式的结果不是一个左值，那么表达式的值一定是个右值。

3.6　小结

本章介绍了C++语言中的运算符，以及由运算符组成的表达式和语句。不同运算符有不同的运算规则，掌握这些规则是开发程序的关键，运算符的相关规则关系到程序的运算结果。运算符的优先级是开发人员必须掌握的，学习时要多加注意。与运算符相关的表达式及语句，都是程序的基本组成部分，要理解各语句之间的关系。

3.7　实践与练习

（1）不借助第3个变量交换两个变量的值。 （实例位置：资源包\TM\sl\3\7）

（2）使用复合赋值运算符计算a+=a*=a/=a-6的结果。 （实例位置：资源包\TM\sl\3\8）

第4章

条件判断语句

（视频讲解： 35分钟）

判断语句是重要的程序控制语句，开发程序会大量运用判断语句。判断语句有很多形式，灵活运用各种形式的判断语句可以提高软件的效率，并且逻辑性强的判断语句容易阅读，可以起到简化代码的作用。

通过阅读本章，您可以：

掌握3种形式的判断语句

了解条件运算符与判断语句的转换

掌握swtich分支语句

掌握判断语句的嵌套

4.1　决策分支

计算机的主要功能是提供用户计算功能，但在计算的过程中会遇到各种各样的情况，针对不同的情况会有不同的处理方法，这就要求程序开发语言要有处理决策的能力。汇编语言使用判断指令和跳转指令实现决策，高级语言使用选择判断语句实现决策。

一个决策系统就是一个分支结构，这种分支结构就像一个树形结构，每到一个节点都需要做决定，就像人走到十字路口，是向前走，还是向左走或是向右走都需要做决定。不同的分支代表不同的决定。例如十字路口的分支结构如图4.1所示。

为描述决策系统的流通，设计人员开发了流程图。流程图使用图形方式描述系统不同状态的不同处理方法。开发人员使用流程图表现程序的结构。

主要的流程图符号如图4.2所示。

图4.1　分支结构

图4.2　主要的流程图符号

使用流程图描述十字路口转向的决策，利用方位做决定，判断是否是南方，如果是南方则向前行，如果不是南方则寻找南方，如图4.3所示。

图4.3　流程图

程序中使用选择判断语句来做决策，选择判断是编程语言的基础语句，在C++语言中有3种形式选择判断语句，同时提供了switch语句，简化多分支决策的处理。下面对选择判断语句进行介绍。

说明

选择判断语句可以简称为判断语句，有的书中也称其为分支语句。

4.2　判断语句

4.2.1　第一种形式的判断语句

C++语言中使用if关键字来组成判断语句，第一种判断语句的形式如下：

表达式一般为关系表达式，表达式的运算结果应该是真或假（true或false）。如果表达式为真，执行语句，如果表达式的值为假就跳过，执行下一条语句。用流程图表示第一种判断语句，如图4.4所示。

图4.4　第一种形式的判断语句

【实例4.1】 判断输入数是否为奇数。 （实例位置：资源包\TM\sl\4\1）

用流程图来描述判断语句的执行过程，如图4.5所示。

图4.5　判断语句的执行过程

程序分以下两步执行。

（1）定义一个整型变量iInput，然后使用cin获得用户输入的整型数据。

（2）对变量iInput的值与2进行%运算，如果运算结果不为0，表示用户输入的是奇数，输出字符串The value is odd number。如果运算结果为0，则不进行任何输出，程序执行完毕。

说明

整数与2进行%运算，结果只有0或1两种情况。

要注意第一种形式的判断语句的书写格式。

判断语句：

可以写成：

但不建议使用“if（a>b）max=a;”这种书写方式，这种方式不便于阅读。

判断形式中的语句可以是复合语句，也就是说可以用大括号括起多条简单语句，例如：

4.2.2　第二种形式的判断语句

第二种形式的判断语句使用了else关键字。第二种判断语句的形式如下：

表达式是一个关系表达式，表达式的运算结果应该是真或假（true或false），如果表达式的值为真，执行语句1，为假则执行语句2。

第二种形式的判断语句相当于汉语里的“如果……那么……”，用流程图来表示第二种判断语句，如图4.6所示。

图4.6　第二种形式的判断语句

【实例4.2】 根据分数判断是否优秀。 （实例位置：资源包\TM\sl\4\2）

用流程图来描述判断语句的执行过程，如图4.7所示。

图4.7　判断语句的执行过程

程序需要和用户交互，用户输入一个数值，将该数值赋值给iInput变量，然后判断用户输入的数据是否大于90，如果大于90，输出字符串It is Good，否则输出字符串It is not Good。

【实例4.3】 改进的奇偶性判别。 （实例位置：资源包\TM\sl\4\3）

程序分以下两步执行：

（1）定义一个整型变量iInput，然后使用cin获得用户输入的整型数据。

（2）对变量iInput的值与2进行%运算，如果运算结果不为0，表示用户输入的是奇数，输出字符串The value is odd number；如果运算结果为0，表示用户输入的是偶数，输出字符串The value is even number，最后程序执行完毕。

else使用时的注意事项如下：

（1）else不能单独使用，必须和关键字if一起出现。

else (a>b) max=a是不合法的。

（2）else后跟的语句可以是复合语句，例如：

4.2.3　第三种形式的判断语句

第三种形式的判断语句是可以进行多次判断的语句，每判断一次就缩小一定的检查范围，它的形式如下：

表达式一般为关系表达式，表达式的运算结果应该是真或假（true或false）。如果表达式为真，执行语句，如果表达式的值为假，就跳过，执行下一条语句。用流程图来表示第三种判断语句，如图4.8所示。

图4.8　第三种形式的判断语句

【实例4.4】 根据成绩划分等级。 （实例位置：资源包\TM\sl\4\4）

程序需要用户输入整型数值，然后判断数值是否大于90，如果大于90，输出very good字符串，否则继续判断，判断是否小于90大于80，如果小于90大于80，输出good字符串，否则继续判断，依此类推，最后判断是否小于60，如果小于60，输出failure字符串，最后没有使用else再进行判断。

4.3　使用条件运算符进行判断

条件运算符是一个三目运算符，它能像判断语句一样完成判断，例如：

首先比较iA和iB的大小，如果iA大于iB，就取iA的值，否则取iB的值。

可以将条件运算符改为判断语句，例如：

【实例4.5】 用条件运算符完成判断数的奇偶性。 （实例位置：资源包\TM\sl\4\5）

实例4.5使用条件运算符完成判断数的奇偶性，比使用判断语句时的代码要简洁。程序同样完成由用户输入整型数，然后和2进行%运算，如果运算结果不为0，是奇数，否则是偶数。

【实例4.6】 条件表达式判断一个数是否是3和5的整倍数。 （实例位置：资源包\TM\sl\4\6）

程序需要用户输入一个整型数，然后用%运算判断能否被3整除，以及能否被5整除，如果同时能被3和5整除，说明输入的整型数是3和5的整倍数。

条件运算符可以嵌套，例如：

【实例4.7】 用条件表达式判断一个数是否是3和5的整倍数。 （实例位置：资源包\TM\sl\4\7）

实例4.7和实例4.6完成同一个目标，都是通过%运算来判断输入的整型数是否是3和5的整倍数。但实例4.7中使用了条件运算符的嵌套。由于条件运算符嵌套后的代码不容易阅读，一般不建议使用。

4.4　switch语句

C++语言提供了一种用于多分支选择的switch语句。可以使用if判断语句做多分支结构程序，但当分支较多的时候，if判断语句容易造成代码混乱，可读性差，如果使用不当就会产生表达式上的错误，因此建议在仅有两个分支或分支数少的时候使用if判断语句，而在分支比较多的时候使用switch语句。

switch语句的一般形式如下：

表达式是一个算术表达式，需要计算出表达式的值，该值应该是一个整型数或字符，如果是浮点数，可能会因为精度的不精确而产生错误。

switch是分支的入口，开始判断是在case分语句中，用表达式的值逐一和case语句中的值进行比较，有匹配成功的就使用break;跳出switch语句，如果没有匹配成功的，就执行default分句。

default分句是可以不写的，如果不写default分句，case分语句中没有匹配成功的就不进行任何操作。

【实例4.8】 根据输入的字符输出字符串 。（实例位置：资源包\TM\sl\4\8）

程序需要用户输入一个字符，当用户输入字符A时，向屏幕输出very good字符串；输入字符B时，向屏幕输出good字符串；输入字符C时，向屏幕输出normal字符串；输入字符D时，向屏幕输出failure字符串；输入其他字符时，向屏幕输出input error字符串。

可以将switch的判断结构，改为第一种形式的判断语句。

【实例4.9】 根据输入的字符输出字符串。 （实例位置：资源包\TM\sl\4\9）

实例4.9和实例4.8完成的功能基本相同。当用户输入字符A后，输出字符串very good，所不同的是，输出完字符串后，使用return跳出主函数，并结束程序，不执行下面的语句。同样输入字符B、C和D后也输出对应的字符串后跳出主函数并结束程序。

也可以将switch的判断结构，改为第三种形式的判断语句。

【实例4.10】 根据输入的字符输出字符串。 （实例位置：资源包\TM\sl\4\10）

同样，实例4.10也是根据输入不同的字符输出不同的字符串。

switch语句中每个case语句都使用break;语句跳出，该语句可以省略。由于程序默认执行程序是顺序执行，当语句匹配成功后，其后面的每条case语句都会被执行，而不进行判断，例如：

当输入1时，程序运行结果如图4.9所示。

当输入7时，程序运行结果如图4.10所示。

图4.9　运行结果1

图4.10　运行结果2

上面的程序想要实现根据输入的1～7中的任意整型数，然后输出整型数对应的英文星期名称，但由于switch语句中的各case分句没有及时使用break;语句跳出，导致意想不到的结果输出。

4.5　判断语句的嵌套

前面讲过3种形式的判断语句，这3种形式的判断语句都可以嵌套判断语句，例如，在第一种形式的判断语句中嵌套第二种形式的判断语句，形式如下：

在第二种形式的判断语句中嵌套第二种形式的判断语句，形式如下：

判断语句可以有多种嵌套方式，可以根据具体需要进行设计，但一定要注意逻辑关系的正确处理。

【实例4.11】 判断是否是闰年 。（实例位置：资源包\TM\sl\4\11）

闰年的判断条件是：能被4整除但是不能被100整除，或者能被400整除，这样的年份才是闰年。实例4.11使用判断语句对上述条件逐一判断，先判断年份能否被4整除（iYear%4==0），如果不能整除输出字符串It is not a leap year，如果能整除，继续判断能否被100整除（iYear%100==0），如果不能整除输出字符串It is a leap year，如果能整除，继续判断能否被400整除（iYear%400==0），如果能整除输出字符串It is a leap year，不能整除输出字符串It is not a leap year。

可以简化判断是否是闰年的实例代码，用一条判断语句来完成。

【实例4.12】 判断是否是闰年。 （实例位置：资源包\TM\sl\4\12）

实例4.12将判断是否是闰年的条件用一个表达式来完成。表达式是一个复合表达式，进行了3次算术运算和两次逻辑运算，算术运算判断能否被整除，逻辑运算判断是否满足条件。

使用判断语句嵌套时要注意else关键字要和if关键字成对出现，并且遵守临近原则，else关键字和自己最近的if语句构成一对。另外，判断语句应尽量使用复合语句，以免产生二义性，导致书写格式的运行结果和设计时的不一致。

4.6　小结

本章主要讲解了C++语言中各种形式的分支语句，每种形式的语句都可以用另外一种格式代替，这增加了开发程序的灵活性。如果是简单的判断，建议用条件运算符，如果是分支较多的逻辑判断，建议使用switch语句，还要特别注意判断语句的书写格式，避免产生二义性。

4.7　实践与练习

（1）编写一个程序，计算增加后的工资。 （实例位置：资源包\TM\sl\4\13）

要求：基本工资大于或等于5000元，增加10%工资；若大于或等于2500元，且小于5000元，则增加15%工资；若小于2500元，则增加20%工资。

（2）设计一个程序，要求从键盘上输入两个任意的整数，输出其中较大的数。 （实例位置：资源包\ TM\sl\4\14）

（3）任意输入3个整数，编程实现对这3个整数进行由小到大排序。 （实例位置：资源包\TM\sl\4\15）

第5章

循环语句

（视频讲解： 55分钟）

循环控制就是控制程序重复执行，当不符合循环条件时停止循环。使用循环结构可以使程序代码更加简洁，减少冗余。掌握循环结构是程序设计的最基本要求，本章将主要介绍while循环、do…while循环和for循环语句，这3种循环语句可以相互转换，达到同一目标可以运用多种方法。

通过阅读本章，您可以：

了解3种循环语句

掌握各种循环的区别

了解循环的跳转

掌握循环的嵌套

5.1　while循环

while循环语句的一般形式如下：

表达式一般是一个关系表达式或一个逻辑表达式，其值应该是一个逻辑值真或假（true和false），当表达式的值为真时开始循环执行语句，当表达式的值为假时退出循环，执行循环外的下一条语句。循环每次都是执行完语句后回到表达式处重新开始判断，重新计算表达式的值，一旦表达式的值为假时就退出循环，为真时就继续执行语句。while循环可以用流程图来演示执行过程，如图5.1所示。

语句可以是复合语句，也就是用大括号括起多条简单语句，大括号及其所包括的语句，被称为循环体，循环主要指循环执行循环体的内容。

【实例5.1】 使用while循环计算1～10的累加。 （实例位置：资源包\TM\sl\5\1）

1～10的累加就是计算1+2+…+10，需要有一个变量从1变化到10，将该变量命名为i，还需要另外一个临时变量不断和该变量进行加法运算，并记录运算结果，将临时变量命名为sum，变量i每增加1时，就和变量sum进行一次加法运算，变量sum记录的是累加的结果。程序需要使用循环语句，使用while循环需要将循环语句的结束条件设置为i<=10，程序执行顺序如图5.2所示。

图5.1　while循环

图5.2　while循环计算1～10的累加

程序代码如下：

程序运行结果如图5.3所示。

图5.3　程序运行结果

程序先对变量sum和i进行初始化，while循环语句的表达式是i<=10，所要执行的循环体是一个复合语句，是由sum=sum+i；和i++；两条简单语句完成，语句sum=sum+i；完成累加，语句i++；完成由1～10的递增变化。

使用while循环的注意事项如下：

（1）表达式不可以为空，表达式为空不合法。

（2）表达式可以用非0代表逻辑值真（true），用0代表逻辑值假（false）。

（3）循环体中必须有改变条件表达式值的语句，否则将成为死循环，例如：

是一个无限循环语句。

例如：

是一个不会进行循环的语句。

5.2　do...while循环

do...while循环语句的一般形式如下：

do为关键字，必须与while配对使用。do与while之间的语句称为循环体，该语句同样是用大括号括起来的复合语句。循环语句中的表达式与while语句中的相同，也多为关系表达式或逻辑表达式。但特别值得注意的是do...while语句后要有分号“；”。do...while循环可以用流程图来演示执行过程，如图5.4所示。

do...while循环的执行顺序是执行循环体的内容，然后判断表达式的值，如果表达式的值为真就跳到循环体处继续执行循环体，循环一直到表达式的值为假，表达式的值为假时跳出循环，执行下一条语句。

【实例5.2】 使用do...while循环计算1～10的累加。 （实例位置：资源包\TM\sl\5\2）

1～10的累加就是计算1+2+…+10，前面的例子使用while循环语句实现了1～10的累加，do...while循环和while循环实现累加的循环体语句相同，只是执行循环体的先后顺序不同，程序执行顺序如图5.5所示。

图5.4　do...while循环

图5.5　do...while循环计算1～10的累加

程序代码如下：

程序运行结果如图5.6所示。

图5.6　程序运行结果

程序使用变量sum作为记录累加的结果，变量i完成由1～10的变化，程序先将变量sum初始化为0，将变量i初始化为1，先执行循环体变量sum和变量i的加法运算，并将运算结果保存到变量sum，然后变量i进行自加运算，接着判断循环条件，看是否变量i的值已经大于10了，如果变量i大于10就跳出循环，小于或等于10就继续执行循环体语句。

do…while循环的注意事项如下：

（1）循环先执行循环体，如果循环条件不成立，循环体已经执行一次了，使用时注意变量变化。

（2）表达式不可以为空，表达式为空不合法。

（3）表达式可以用非0代表逻辑值真（true），用0代表逻辑值假（false）。

（4）循环体中必须有改变条件表达式值的语句，否则将成为死循环。

（5）注意循环语句后要有分号“；”。

5.3　while与do…while比较

可以通过设置起始循环条件不成立循环语句，来观察while和do...while的不同。将变量i初始值设置为0，然后循环表达式设置为i>1，显然循环条件不成立。循环体执行的是对变量j的加1运算，通过输出变量j在循环前的值和循环后的值来进行比较。

【实例5.3】 使用do...while循环进行计算。 （实例位置：资源包\TM\sl\5\3）

使用do...while循环进行计算，程序代码如下：

程序运行结果如图5.7所示。

图5.7　do...while循环运行结果

【实例5.4】 使用while循环进行计算。 （实例位置：资源包\TM\sl\5\4）

使用while循环进行计算的程序代码如下：

程序运行结果如图5.8所示。

图5.8　while循环运行结果

使用do...while循环后变量j的值为1，而使用while循环后变量j的值仍为0。

5.4　for循环

for循环语句的一般格式如下：

表达式1：该表达式通常是一个赋值表达式，负责设置循环的起始值，也就是给控制循环的变量赋初值。

表达式2：该表达式通常是一个关系表达式，用控制循环的变量和循环变量允许的范围值进行比较。

表达式3：该表达式通常是一个赋值表达式，对控制循环的变量进行增大或减小。

语句：语句仍然是复合语句。

for循环语句的执行过程如下：

（1）先求解表达式1。

（2）求解表达式2，若其值为真，则执行for语句中指定的内嵌语句，然后执行（3）。若表达式2值为0，则结束循环，转到（5）。

（3）求解表达式3。

（4）返回（2）继续执行。

（5）循环结束，执行for语句下面的一个语句。

上面的5个步骤也可以用图5.9表示。

【实例5.5】 用for循环计算1～10的累加。 （实例位置：资源包\TM\sl\5\5）

for循环不同于while循环和do...while循环，它有3个表达式，需要正确设置这3个表达式。计算累加需要一个能由1～10递增变化的变量i和一个记录累加和的变量sum，for循环的表达式中可以对变量进行初始化，以及实现变量由1～10的递增变化。程序执行顺序如图5.10所示。

图5.9　for循环执行过程

图5.10　for循环计算1～10的累加

程序代码如下：

程序运行结果如图5.11所示。

图5.11　程序运行结果

程序中for（i=1;i<=10;i++）sum+=i; 是一个循环语句，sum+=i是循环体语句，其中i是控制循环的变量，i=1是表达式1，i<=10是表达式2，i++是表达式3，sum +=i;是语句；表达式1将循环控制变量i赋初始值为1，表达式2中10是循环变量允许的范围，也就是说i不能大于10，大于10时将不执行语句sum +=i;。语句sum +=i;是使用了带运算的赋值语句，它等同于语句sum = sum +i;。sum +=i语句一共执行了10次，i的值是从1～10变化。

for循环的注意事项如下：

（1）for循环语句可以在表达式1中直接声明变量。

在表达式外声明变量，例如：

在表达式内声明变量，例如：

在循环语句中声明变量，也相当于在函数内声明了变量，如果在表达式1中声明两个相同变量，编译器将报错，例如：

（2）for循环中的表达式1、表达式2、表达式3都可以省略。

省略表达式1

如果省略表达式1，且控制变量在循环外声明了并赋初值，程序能编译通过并且正确运行，例如：

程序仍是计算1～10的累加。

如果控制变量在循环外声明了但没有赋初值，程序能编译通过，但程序运行结果不是用户所期待的。因为编译器会为变量赋一个默认的初值，该初值一般为一个比较大的负数，所以会造成程序运行结果不正确。

省略表达式2

省略了表达2也就是省略了循环判断语句，没有循环的终止条件，循环变成无限循环。

省略表达式3

省略表达式3后循环也是无限循环，因为控制循环的变量永远都是初始值，永远符合循环条件。

省略表达式1和表达式3

for循环语句如果省略表达式1和表达式3，就和while循环一样了，例如：

3个表达式同时省略

for循环语句如果省略3个表达式，就会变成无限循环。无限循环就是死循环，它会使程序进入瘫痪状态。使用循环时，建议使用计数控制，也就是说循环执行到指定次数，就跳出循环，例如：

5.5　循环控制

循环控制包含两方面的内容，一方面是控制循环变量的变化方式，一方面是控制循环的跳转。控制循环的跳转需要用到break和continue两个关键字，这两条跳转语句的跳转效果不同，break是中断循环，continue是跳出本次循环体的执行。

5.5.1　控制循环的变量

无论是for循环还是while、do...while循环，都需要循环一个控制循环的变量，while、do...while循环的控制变量变化可以是显式的，也可以是隐式的，例如，在读取文件时，在while循环中循环读取文件内容，但程序中没有出现控制变量。程序代码如下：

程序中while循环中的表达式是判断文件指针是否指向文件末尾，如果文件指针指向文件末尾，就跳出循环，起始程序中控制循环的变量是文件的指针，文件的指针在读取文件时不断变化。

for循环的循环控制变量的变化方式有两种，一个是递增方式，一个是递减方式。使用递增方式还是递减方式和变量的初值和范围值的比较有关。

如果初值大于限定范围的值，表达式2是大于关系（>）判定的不等式，使用递减方式。

如果初值小于限定的范围值，表达式2是小于关系（<）判定的不等式，使用递增方式。

前文使用for循环计算1～10累计和使用的是递增方式，也可以使用递减方式计算1～10累计和。程序代码如下：

程序中for循环的表达式1中声明变量并赋初值10，表达式2中限定范围的值就是1，不等式是循环控制变量i是否大于等于1，如果小于1就停止循环，循环控制变量就是由10～1递减变化，程序输出结果仍是the result :55。

5.5.2　break语句

使用break语句可以跳出switch结构。在循环结构中，同样也可用break语句跳出当前循环体，从而中断当前循环。

在3种循环语句中使用break语句的形式如图5.12所示。

图5.12　break语句的使用形式

【实例5.6】 使用break跳出循环。 （实例位置：资源包\TM\sl\5\6）

程序中需要用户输入10个数，然后计算10个数的和。当输入数为负数时，就停止循环不再进行累加，输出前面累加结果。例如，输入4次数字1，最后输入数字-1，程序运行结果如图5.13所示。

图5.13　程序运行结果

注意

如果遇到循环嵌套的情况，break语句将只会使程序流程跳出包含它的最内层的循环结构，只跳出一层循环。

5.5.3　continue语句

continue语句是针对break语句的补充。continue不是立即跳出循环体，而是跳过本次循环结束前的语句，回到循环的条件测试部分，重新开始执行循环。在for循环语句中遇到continue后，首先执行循环的增量部分，然后进行条件测试。在while和do…while循环中，continue语句使控制直接回到条件测试部分。

在3种循环语句中使用continue语句的形式如图5.14所示。

图5.14　continue语句的使用形式

【实例5.7】 使用continue跳出循环。 （实例位置：资源包\TM\sl\5\7）

程序中需要用户输入10个数，然后计算10个数的和。当输入数为负数时，不执行sum+=n;语句，也就是不对负数进行累加，例如，输入10个数全为1，输出结果为10。

5.5.4　goto语句

goto语句又称为无条件跳转语句，用于改变语句的执行顺序。goto语句的一般格式如下：

其中，标号是用户自定义的一个标识符，以冒号结束。下面利用goto语句实现1～10的累加求和。

【实例5.8】 使用goto语句实现循环。 （实例位置：资源包\TM\sl\5\8）

程序中利用标签实现循环功能。当语句执行到if（ivar <10）时，如果条件为真，跳转到标签定义label:处。这是一种古老的跳转语句，它会使程序的执行顺序变得混乱，CPU需要不停地进行跳转，效率比较低，因此，在开发程序时慎用goto语句。

在使用goto语句时的说明如下：

（1）使用goto语句时，应注意标签的定义。在定义标签时，其后不能紧接着出现“}”，例如，下面的程序代码是非法的：

在上述代码中定义标签时，其后没有执行代码了，所以出现编译错误。如果程序中出现上述情况，可以在标签后添加一个语句，以解决编译错误。

（2）在使用goto语句时还应注意goto语句不能越过复合语句之外的变量定义的语句，例如，下面的goto语句是非法的：

在上述代码中goto语句试图越过变量i的定义，导致编译错误。解决上述问题的方法是将变量的声明放在复合语句中，例如，下面的代码将是合法的：

5.6　循环嵌套

循环有for、while、do...while 3种方式，这3种循环可以相互嵌套，例如，在for循环中套用for循环：

在while循环中套用while循环：

在while循环中套用for循环：

【实例5.9】 打印三角形。 （实例位置：资源包\TM\sl\5\9）

使用嵌套的for循环来输出由字符*组成的三角形。

程序中一共输出5行字符，最外面的for循环控制输出的行数，嵌套的第一个循环控制字符*前的空格数，第二个for循环控制输出字符*的个数。第一个循环随着行数的增加，字符*前的空格数越来越少，第二个循环输出和行号有关的奇数个字符*。程序运行结果如图5.15所示。

图5.15　程序运行结果

【实例5.10】 输出乘法口诀表。 （实例位置：资源包\TM\sl\5\10）

使用嵌套的for循环输出乘法口诀表。

程序使用了两层for循环，第一个循环由1～9变化，第二个循环则是控制随着行数的增加，列数也增加，最后形成第9行有9列。程序运行结果如图5.16所示。

图5.16　输出乘法口诀表

5.7　循环应用实例

本节通过几个实例来演示不同循环的具体应用。

5.7.1　阿姆斯壮数

【实例5.11】 阿姆斯壮数。 （实例位置：资源包\TM\sl\5\11）

在3位的整数中，形如153=1 3 +5 3 +3 3 这样的数称为阿姆斯壮数。求阿姆斯壮数需要分别计算出3位整数的百位、十位和个位，然后对3个数分别求立方，最后判断是否为阿姆斯壮数。

程序运行结果如图5.17所示。

图5.17　阿姆斯壮数

5.7.2　巴斯卡三角形

【实例5.12】 巴斯卡三角形。 （实例位置：资源包\TM\sl\5\12）

巴斯卡三角形是两个边全输出1，三角形的内部用上行相邻两个数之和，各相加后的和数等分地放在三角形的各行内。

程序使用了循环嵌套来控制显示格式，变量n代表行数，变量r代表每行元素数，自定义函数combi计算每个位置应该放置的数。程序运行结果如图5.18所示。

图5.18　巴斯卡三角形

5.7.3　对输入的分数进行排名

【实例5.13】 对输入的分数进行排名。 （实例位置：资源包\TM\sl\5\13）

通过输入不同的分数，然后计算不同分数之间的排名。

程序使用数组score记录输入的分数，使用数组juni记录排名。首先通过do...while循环获取输入的分数，最多只能输入100个分数，并且输入的分数不能超过100分，然后使用for循环进行排序，有100分就在juni数组100的位置标1，接着使用for循环对juni数组中的数相邻两项进行累加，最后使用for循环将分数和排名输出。

程序运行结果如图5.19所示。

图5.19　对输入的分数进行排名

5.8　小结

本章主要介绍了for、while、do...while这3种循环，3种循环中使用比较灵活的是for循环，比较简单的是while循环。同样一个目标使用3种循环都可以实现，最终选择哪种循环来实现要根据每个开发人员对需求的理解，但一般建议使用for循环。

5.9　实践与练习

（1）要求使用for循环，打印出大写字母的ASCII码对照表。 （实例位置：资源包\TM\sl\5\14）

（2）输出0～100之间不能被3整除的数。提示：使用for进行循环检查操作，使用continue结束不符合条件的情况。 （实例位置：资源包\TM\sl\5\15）

（3）用*打印菱形，如图5.20所示。 （实例位置：资源包\TM\sl\5\16）

图5.20　打印菱形

第6章

函数

（视频讲解： 1小时14分钟）

程序是由函数组成的，一个函数就是程序中的一个模块。函数可以相互调用，可以将相互联系密切的语句都放到一个函数内，也可以将复杂的函数分解成多个子函数。函数本身也有很多特点，熟练掌握函数的特点可以将程序的结构设计得更合理。

通过阅读本章，您可以：

了解函数工作机制

掌握函数调用

掌握函数重载

了解内联函数

6.1　函数概述

函数就是能够实现特定功能的程序模块，它可以是只有一条语句的简单函数，也可以是包含许多子函数的复杂函数；函数有别人写好的存放在库里的库函数，也有开发人员自己写的自定义函数；函数根据功能可以分为字符函数、日期函数、数学函数、图形函数、内存函数等。一个程序可以只有一个主函数，但不可以没有函数。

6.1.1　函数的定义

函数定义的一般形式如下：

类型标识符：用来标识函数的返回值类型，可以根据函数的返回值判断函数的执行情况，通过返回值也可以获取想要的数据。类型标识符可以是整型、字符型、指针型、对象的数据类型。

形式参数列表：由各种类型变量组成的列表，各参数之间用逗号间隔，在进行函数调用时，主调函数对变量进行赋值。

关于函数定义的一些说明如下：

（1）形式参数列表可以为空，这样就定义了不需要参数的函数，例如：

函数ShowMessage通过cout流输出变量i的值。

（2）函数后面的大括号表示函数体，在函数体内进行变量的声明和添加实现语句。

6.1.2　函数的声明

调用一个函数前必须先声明函数的返回值类型和参数类型，例如：

函数声明被称为函数原型，函数声明时可以省略变量名，例如：

下面通过实例来介绍如何在程序中声明、定义和调用函数。

【实例6.1】 声明、定义和调用函数。 （实例位置：资源包\TM\sl\6\1）

程序运行结果如图6.1所示。

图6.1　程序运行结果

程序定义和声明了函数ShowMessage、ShowAge、ShowIndex，并进行了调用，通过函数中的输出语句进行输出。

6.2　函数参数及返回值

6.2.1　返回值

函数的返回值是指函数被调用之后，执行函数体中的程序段所取得的并返回给主调函数的值，函数的返回值通过return语句返回给主调函数。return语句的一般形式如下：

语句将表达式的值返回给主调函数。

关于返回值的说明如下：

（1）函数返回值的类型和函数定义中函数的类型标识符应保持一致。如果两者不一致，则以函数类型为准，自动进行类型转换。

（2）如函数值为整型，在函数定义时可以省去类型标识符。

（3）在函数中允许有多个return语句，但每次调用只能有一个return语句被执行，因此只能返回一个函数值。

（4）不返回函数值的函数，可以明确定义为“空类型”，类型标识符为void，例如：

（5）类型标识符为void的函数不能进行赋值运算及值传递，例如：

说明

为了降低程序出错的概率，凡不要求返回值的函数都应定义为空类型。

6.2.2　空函数

没有参数和返回值，函数的作用域为空的函数就是空函数。

调用此函数时，什么工作也不做，没有任何实际意义。在主函数main函数中调用setWorkSpace函数时，这个函数没有起到任何作用，例如：

空函数存在的意义是：在程序设计中往往需要根据功能分为若干模块，分别由一些函数来实现。而在第一阶段只设计最基本的模块，其他一些次要功能或锦上添花的功能则在以后需要时陆续补上。在编写程序的开始阶段，可以在将来准备扩充功能的地方写上一个空函数，这些函数没有开发完成，先占一个位置，以后用一个编好的函数代替它。这样做，程序的结构清楚，可读性好，以后扩充新功能方便，对程序结构影响不大。

6.2.3　形参与实参

函数定义时如果参数列表为空，说明函数是无参函数；如果参数列表不为空，就称为带参数函数。带参数函数中的参数在函数声明和定义时被称为“形式参数”，简称形参，在函数被调用时被赋予具体值，具体的值被称为“实际参数”，简称实参。形参与实参如图6.2所示。

图6.2　形参与实参

实参与形参的个数应相等，类型应一致。实参与形参按顺序对应，函数被调用时会一一传递数据。

形参与实参的区别如下：

（1）在定义函数中指定的形参，在未出现函数调用时，它们并不占用内存中的存储单元。只有在发生函数调用时，函数的形参才被分配内存单元，在调用结束后，形参所占的内存单元也被释放。

（2）实参应该是确定的值。在调用时将实参的值赋值给形参，如果形参是指针类型，就将地址值传递给形参。

（3）实参与形参的类型应相同。

（4）实参与形参之间是单项传递，只能由实参传递给形参，而不能由形参传回来给实参。

实参与形参之间存在一个分配空间和参数值传递的过程，这个过程是在函数调用时发生的，C++支持引用型变量，引用型变量则没有值传递的过程，这将在后文讲到。

6.2.4　默认参数

在调用带参数函数时，如果经常需要传递同一个值到调用函数，在定义函数时，可以为参数设置一个默认值，这样在调用函数时可以省略一些参数，此时程序将采用默认值作为函数的实际参数。下面的程序代码定义了一个具有默认值参数的函数。

【实例6.2】 调用默认参数的函数。 （实例位置：资源包\TM\sl\6\2）

输出两行字符串，一行是函数默认参数，一行是通过传字符串实参。程序代码如下：

程序运行结果如图6.3所示。

图6.3　调用默认参数的函数

在定义函数默认值参数时，如果函数具有多个参数，应保证默认值参数出现在参数列表的右方，没有默认值的参数出现在参数列表的左方，即默认值参数不能出现在非默认值参数的左方。例如，下面的函数定义是非法的。

程序中默认值参数y出现在非默认值参数z的左方，导致了编译错误，正确的做法是将默认值参数放置在参数列表的右方，例如：

6.2.5　可变参数

库函数printf就是一个可变参数的函数，它的参数列表会显示省略号“...”。printf函数原型格式如下：

省略号参数代表的含义是函数的参数是不固定的，可以传递一个或多个参数。对于printf函数来说，可以输出一项信息，也可以同时输出多项信息，例如：

声明可变参数的函数和声明普通函数一样，只是参数列表中有一个省略号“...”，例如：

对于可变参数的函数，在定义函数时需要一一读取用户传递的实际参数。可以使用va_list类型和va_start、va_arg、va_end 3个宏读取传递到函数中的参数值。使用可变参数需要引用STDARG.H头文件。下面以一个具体的示例介绍可变参数的函数的定义及使用。

【实例6.3】 定义省略号形式的函数参数。 （实例位置：资源包\TM\sl\6\3）

程序运行结果如图6.4所示。

图6.4　定义省略号形式的函数参数

6.3　函数调用

声明完函数后就需要在源代码中调用该函数。整个函数的调用过程被称为函数调用。标准C++是一种强制类型检查的语言，在调用函数前，必须把函数的参数类型和返回值类型告知编译器。

函数调用的一些说明如下：

（1）首先被调用的函数必须是已经存在的函数（是库函数或用户自己定义的函数）。

（2）如果使用库函数，还需要将库函数对应的头文件引入，这需要使用预编译指令#include。

（3）如果使用用户自定义函数，一般还应该在主调函数中对被调用的函数做声明。

注意

C++和C不同，简单地用不同的原型来说明同一个函数是无法通过C++语法检查的。

6.3.1　传值调用

主调函数和被调用函数之间有数据传递关系，换句话说，主调函数将实参数值复制给被调用函数的形参处，这种调用方式被称为传值调用，如果传递的实参是结构体对象，值传递方式的效率是低下的，可以通过传指针或使用变量的引用来替换传值调用。传值调用是函数调用的基本方式。

【实例6.4】 使用传值调用。 （实例位置：资源包\TM\sl\6\4）

程序运行结果如图6.5所示。

图6.5　使用传值调用

程序本意是想实现当x小于y时交换x和y的值，但结果并没有实现，主要原因是调用swap函数时复制了变量x和y的值，而并非变量本身。如果将函数swap在调用处展开，程序本意就可以实现，例如，代码修改如下：

程序运行结果如图6.6所示。

图6.6　展开函数调用

程序代码是开发人员模拟函数调用时展开swap函数的代码，函数的调用就是由编译器来完成代码的展开工作，但不是真的展开，而是移动swap函数处执行，执行过程类似展开。调用函数调用时要注意函数调用时有值传递过程。通过函数调用的方式实现交换变量的值，可以通过使用指针传地址和变量引用的方式实现，这在后面的章节会讲到。

函数调用中发生的数据传递是单向的，只能把实参的值传递给形参，在函数调用过程中，形参的值发生改变，实参的值不会发生变化。

6.3.2　嵌套调用

在自定义函数中调用其他自定义函数，这种调用方式称为嵌套调用，例如：

在函数嵌套调用时要注意，不要在函数体内定义函数，如下代码是错误的：

嵌套调用对调用的层数是没有要求的，但个别的编译器可能会有一些限制，使用时应注意。

6.3.3　递归调用

直接或间接调用自己的函数被称为递归函数（recursive funciton）。

使用递归方法解决问题的特点是：问题描述清楚，代码可读性强，结构清晰，代码量比使用非递归方法少。缺点是递归程序的运行效率比较低，无论是从时间角度还是从空间角度都比非递归程序差。对于时间复杂度和空间复杂度要求较高的程序，使用递归函数调用要慎重。

递归函数必须定义一个停止条件，否则函数永远递归下去。

【实例6.5】 汉诺（Hanoi）塔问题。 （实例位置：资源包\TM\sl\6\5）

有3个立柱垂直矗立在地面，给这3个立柱分别命名为A、B、C。开始的时候立柱A上有64个圆盘，这64个圆盘大小不一，并且按从小到大的顺序依次摆放在立柱A上，如图6.7所示。现在的问题是要将立柱A上的64个圆盘移到立柱C上，并且每次只允许移动一个圆盘，在移动过程中始终保持大盘在下，小盘在上。

分析程序：

先假设移动4个圆盘，立柱A上的圆盘按由上到下的顺序分别命名为a、b、c、d，如图6.7所示。

图6.7　圆盘原始状态

先考虑将a和b移动到立柱C上。移动顺序是a->B，b->C，a->C，移动结果如图6.8所示。

图6.8　移动两个圆盘到目标

如果要将c也移动到C上，就要暂时将c移动到B，然后再移动a和b。移动顺序是c->B，a->A，b->B，a->B，d->C，移动结果如图6.9所示。

图6.9　移动3个圆盘到目标

最后是完成4个圆盘的移动，移动顺序是a->C，b->A，a->A，c->C，a->B，b->C，a->C。

总结一下：

要将4个圆盘移动到指定立柱总共需要移动15次。

在移动过程中将两个圆盘移动到指定立柱需要移动3次，分别是a->B，b->C，a->C。

在移动过程中将3个圆盘移动到指定立柱需要移动7次，分别是a->B，b->C，a->C，c->B，a->A，b->B，a->B。

移动次数可以总结为是2 n -1次。

在移动过程中可以将a、b、c这3个圆盘看成是一个圆盘，移动4个圆盘的过程就像是在移动两个圆盘。还可以将a、b、c这3个圆盘中的a、b两个圆盘看成是一个圆盘，移动3个圆盘也像是在移动两个圆盘。可以使用递归的思路来移动n个圆盘。

移动n个圆盘可以分成3个步骤：

（1）把A上的n-1个圆盘移到B上。

（2）把A上的一个圆盘移到C上。

（3）把B上的n-1个圆盘移到C上。

程序代码如下：

程序运行结果如图6.10所示。

图6.10　程序运行结果

输入数字3，表示移动3个圆盘，程序打印出挪动3个圆盘的步骤。

【实例6.6】 求n的阶乘 。（实例位置：资源包\TM\sl\6\6）

程序运行结果如图6.11所示。

图6.11　计算阶乘

程序中Fac函数实现了计算n的阶乘。以n等于4为例，4!等于4*3!，3!等于3*2!，……，1!等于1。当计算4的阶乘时，只要知道3的阶乘就可以了，4*3!等于4!。同理，计算3的阶乘，只要知道2的阶乘就可以了，依此类推。1的阶乘为1，知道了1的阶乘，就可以计算2的阶乘，知道2的阶乘就可以计算3的阶乘……

在上面的递归函数中，如果传递一个很大的数作为参数，会导致堆栈溢出，因为每调用一个函数，系统会为函数的参数分配堆栈空间。对于上述的递归函数Fac，完全可以用连续乘积的方式实现。

【实例6.7】 利用循环求n的阶乘。 （实例位置：资源包\TM\sl\6\7）

程序运行结果如图6.12所示。

图6.12　利用循环求n的阶乘

6.4　变量作用域

根据变量声明的位置可以将变量分为局部变量及全局变量，在函数体内定义的变量称为局部变量，在函数体外定义的变量称为全局变量，例如：

程序运行结果如图6.13所示。

图6.13　局部变量与全局变量

变量都有它的生命期，全局变量在程序开始的时候创建并分配空间，在程序结束的时候释放内存并销毁；局部变量是在函数调用的时候创建，并在栈中分配内存，在函数调用结束后销毁并释放。

6.5　重载函数

定义同名的变量，程序会编译出错；定义同名的函数也会带来冲突的问题。C++中使用了名字重组的技术，通过函数的参数类型来识别函数，所谓重载函数就是指多个函数具有相同的函数名，而参数类型或参数个数不同。函数调用时，编译器以参数的类型及个数来区分调用哪个函数。下面的实例定义了重载函数。

【实例6.8】 使用重载函数。 （实例位置：资源包\TM\sl\6\8）

程序运行结果如图6.14所示。

图6.14　使用重载函数

程序中定义了两个相同函数名的函数，函数名都为Add，在main调用Add函数时实参类型不同，语句int ivar = Add（5,2）；的实参类型是整型，语句float fvar = Add（10.5,11.4）；的实参类型是双精度，编译器可以区分这两个函数，会正确调用相应的函数。

在定义重载函数时，应注意函数的返回值类型不作为区分重载函数的一部分。下面的重载函数是非法的。

6.6　内联函数

通过inline关键字可以把函数定义为内联函数，编译器会在每个调用该函数的地方展开一个函数的副本。

在下面的程序中创建了一个IntegerAdd函数，并进行了调用。

IntegerAdd函数被定义为内联函数，其执行代码如下。

使用内联函数可以减少函数调用带来的开销（在程序所在文件内移动指针寻找调用函数地址带来的开销），但它只是一种解决方案，编译器可以忽略内联的声明。

应该在函数实现代码很简短或者调用该函数次数相对较少的情况下将函数定义为内联函数，一个递归函数不能在调用点完全展开，一个一千行代码的函数也不大可能在调用点展开，内联函数只能在优化程序时使用。在抽象数据类设计中，它对支持信息隐藏起着主要作用。

如果某个内联函数要作为外部全局函数，即它将被多个源代码文件使用，那么就把它定义在头文件里，在每个调用该inline函数的源文件中包含该头文件，这种方法保证对每个inline函数只有一个定义，防止在程序的生命期中引起无意的不匹配。

6.7　变量的存储类别

存储类别是变量的属性之一，C++语言中定义了4种变量的存储类别，分别是auto变量、静态变量、register变量和extern变量。变量存储方式不同会使变量的生存期不同，生存期表示了变量存在的时间。生存期和变量作用域是从时间和空间这两个不同的角度来描述变量的特性。

静态存储变量通常是在变量定义时就分配固定的存储单元并一直保持不变，直至整个程序结束。前面讲过的全局变量即属于此类存储方式，它们存放在静态存储区中。动态存储变量是在程序执行过程中使用它时才分配存储单元，使用完毕立即将该存储单元释放。前面讲过的函数的形式参数，在函数定义时并不给形参分配存储单元，只是在函数被调用时才予以分配，调用函数完毕立即释放，此类变量存放在动态存储区中。从以上分析可知，静态存储变量是一直存在的，而动态存储变量则时而存在时而消失。

6.7.1　auto变量

auto（自动）变量存储类型是C++语言程序中默认的存储类型。函数内未加存储类型说明的变量均视为自动变量，也就是说自动变量可省去关键字auto，例如：

等价于：

自动变量具有以下特点：

（1）自动变量的作用域仅限于定义该变量的个体内。在函数中定义的自动变量，只在该函数内有效，在复合语句中定义的自动变量只在该复合语句中有效，例如：

（2）自动变量属于动态存储方式，变量分配的内存是在栈中，当函数调用结束后，自动变量的值会被释放。同样在复合语句中定义的自动变量，在退出复合语句后也不能再使用，否则将引起错误。

（3）由于自动变量的作用域和生存期都局限于定义它的个体内（函数或复合语句内），因此不同的个体中允许使用同名的变量而不会混淆。即使在函数内定义的自动变量也可与该函数内部的复合语句中定义的自动变量同名。

【实例6.9】 输出不同生命期的变量值。 （实例位置：资源包\TM\sl\6\9）

程序运行结果如图6.15所示。

图6.15　输出不同生命期的变量值

程序两次输出变量k为自动变量。第一次输出的是i-j的值，第二次输出的是i+j的值。虽然变量名都为k，但其实是两个不同的变量。

6.7.2　静态变量

在声明变量前加关键字static，可以将变量声明成静态变量。静态局部变量的值在函数调用结束后不消失，静态全局变量只能在本源文件中使用，例如，声明变量为静态变量：

静态变量属于静态存储方式，它具有以下特点：

（1）静态变量在函数内定义，在程序退出时释放，在程序整个运行期间都不释放，也就是说它的生存期为整个源程序。

（2）静态变量的作用域与自动变量相同，在函数内定义就在函数内使用，尽管该变量还继续存在，但不能使用它，如再次调用定义它的函数时，它又可继续使用。

（3）编译器会为静态局部变量赋予0值。

下面通过实例介绍静态变量的用法。

【实例6.10】 使用静态变量实现累加。 （实例位置：资源包\TM\sl\6\10）

程序运行结果如图6.16所示。

图6.16　使用静态变量实现累加

程序中n是静态局部变量，每次调用函数add时，静态局部变量n都保存了前次被调用后留下的值，所以当输入循环次数3时，变量sum累加的结果是6，而不是3。

如果去除static关键字，则程序运行结果如图6.17所示。

图6.17　程序运行结果

当输入循环次数3时，变量sum累加的结果是3。变量n不再使用静态存储区空间，每次调用后变量n的值都被释放，再次调用时n的值为初始值0。

6.7.3　register变量

通常变量的值存放在内存中，当对一个变量频繁读写时，需要反复访问内存储器，则花费大量的存取时间。为了提高效率，C++语言可以将变量声明为寄存器变量，这种变量将局部变量的值存放在CPU中的寄存器中，使用时不需要访问内存，而直接从寄存器中读写。寄存器变量的说明符是register。

对寄存器变量的说明如下：

（1）寄存器变量属于动态存储方式。凡需要采用静态存储方式的量不能定义为寄存器变量。

（2）编译程序会自动决定哪个变量使用寄存器存储。register起到程序优化的作用。

6.7.4　extern变量

在一个源文件中定义的变量和函数只能被本文件中的函数调用，一个C++程序中会有许多源文件，如果使用非本源文件的全局变量呢？C++提供了extern关键字来解决这个问题。在使用其他源文件的全局变量时，只需要在本源文件使用extern关键字来声明这个变量即可，示例如下。

在Sample1.cpp源文件中定义全局变量a、b、c，程序代码如下：

在Sample2.cpp源文件中要使用Sample1.cpp源文件中全局变量a、b、c，程序代码如下：

在Sample2.cpp源文件中，编译系统不再为全局变量a、b、c分配内存空间，而是改变全局变量a、b、c的值，在Sample1.cpp源文件中输出值也会发生变化。

6.8　小结

本章主要介绍函数的使用，调用函数要了解函数的返回值、函数的参数以及函数的调用方式。变量的作用域和函数有关，函数的递归调用可以帮助开发人员设计出思路明了的程序，内联函数可以提高程序的运算效率，函数重载则解决了代码复用中函数名冲突的问题。

6.9　实践与练习

（1）开发一个程序，要求设计一个函数进行减法运算 。（实例位置：资源包\TM\sl\6\11）

（2）定义一个函数名为Max的函数，其函数功能是判断两个整数的大小，并将较大的整数显示出来。 （实例位置：资源包\TM\sl\6\12）

第7章

数组、指针和引用

（视频讲解： 1小时27分钟）

数组是有序数据的集合，可以减少同种类型变量的声明，指针则是可以操作内存数据的变量，引用则是变量的别名。数组的首地址可以看作是指针，而通过指针也可以操作数组，指针和引用在函数的参数传递时可以替代。指针是一柄双刃剑，能够带来效率的提升，也会给程序带来意想不到的灾难。

通过阅读本章，您可以：

掌握指针和数组的应用

掌握指针在函数中的应用

掌握引用

掌握字符数组

7.1　一维数组

7.1.1　一维数组的声明

在程序设计中，将同一数据类型的数据按一定形式有序地组织起来，这些有序数据的集合就称为数组。一个数组有一个统一的数组名，可以通过数组名和下标来唯一确定数组中的元素。

一维数组的声明形式如下：

例如：

使用一维数组的说明如下：

（1）数组名的定名规则和变量名相同。

（2）数组名后面的括号是方括号，方括号内是常量表达式。

（3）常量表达式表示元素的个数，即数组的长度。

（4）定义数组的常量表达式不能是变量，因为数组的大小不能动态定义。

7.1.2　一维数组元素的引用

一维数组元素引用的一般形式如下：

例如：

a[0]、a[1]、a[2]、a[3]、a[4]、a[5]、a[6]、a[7]、a[8]、a[9]，是对数组a中10个元素的引用。

一维数组元素引用的说明如下：

（1）数组元素的下标起始值为0而不是1。

（2）a[10]是不存在的数组元素，引用a[10]非法。

注意

a[10]属于下标越界，下标越界容易造成程序瘫痪。

7.1.3　一维数组的初始化

数组元素初始化的方式有两种，一种是对单个元素逐一赋值，另一种是使用聚合方式赋值。

（1）单一数组元素赋值

a[0]=0就是对单一数组元素赋值，也可以通过变量控制下标的方式进行赋值，例如：

程序运行结果如图7.1所示。

图7.1　单一数组元素赋值

（2）聚合方式赋值

数组不仅可以逐一对数组元素赋值，还可以通过大括号进行多个元素的赋值，例如：

或：

或：

下面通过实例来看一下如何为一维数组的数组元素赋值。

【实例7.1】 一维数组赋值 。（实例位置：资源包\TM\sl\7\1）

程序运行结果如图7.2所示。

图7.2　一维数组赋值

程序实现通过for循环将int a[10]定义的数组中的每个元素赋值，然后再循环通过cout函数将数组中的元素值输出到显示设备。

7.2　二维数组

7.2.1　二维数组的声明

二维数组声明的一般形式如下：

例如：

一个一维数组描述的是一个线性序列，二维数组描述的则是一个矩阵。常量表达式1代表行的数量，常量表达式2代表列的数量。

二维数组可以看作是一种特殊的一维数组，如图7.3所示，虚线左侧为3个一维数组的首元素，二维数组是由A[0]、A[1]、A[2]这3个一维数组组成，每个一维数组都包含4个元素。

图7.3　二维数组

使用二维数组的说明如下：

（1）数组名的定名规则和变量名相同。

（2）二维数组有两个下标，所以要有两个中括号。

（3）下标运算符中的整数表达式代表数组每一个维的长度，它们必须是正整数，其乘积确定了整个数组的长度，例如：

其长度就是3×4=12。

（4）定义数组的常量表达式不能是变量，因为数组的大小不能动态定义。

7.2.2　二维数组元素的引用

二维数组元素的引用形式如下：

二维数组元素的引用和一维数组基本相同，例如：

7.2.3　二维数组的初始化

二维数组元素初始化的方式和一维数组相同，也分为单个元素逐一赋值和使用聚合方式赋值。例如：

使用聚合方式给数组赋值等同于分别对数组中的每个元素进行赋值，例如：

等同于执行如下语句：

二维数组中元素排列的顺序是按行存放，即在内存中先顺序存放第一行的元素，再存放第二行的元素，例如，int a[3][4]={1,2,3,4,5,6,7,8,9,10,11,12}；的赋值顺序如下。

先给第一行元素赋值：a[0][0]->a[0][1]->a[0][2]->a[0][3]。

再给第二行元素赋值：a[1][0]->a[1][1]->a[1][2]->a[1][3]。

最后给第三行元素赋值：a[2][0]->a[2][1]->a[2][2]->a[2][3]。

数组元素的位置以及对应数值如图7.4所示。

图7.4　数组元素的位置以及对应数值

使用聚合方式赋值，还可以按行进行赋值，例如：

二维数组可以只对前几个元素赋值，例如：

数组元素是左值，可以出现在表达式中，也可以对数组元素进行计算，例如：

下面通过实例来熟悉一下二维数组的操作，实例将实现将二维数组中行数据和列数据相互置换的功能。

【实例7.2】 将二维数组行列对换。 （实例位置：资源包\TM\sl\7\2）

程序运行结果如图7.5所示。

图7.5　将二维数组行列对换

程序首先输出二维数组array中的元素，然后调用自定义函数fun将数组中的行元素转换为列元素，最后输出转换后的结果。

7.3　字符数组

用来存放字符数据的数组是字符数组，字符数组中的一个元素存放一个字符。字符数组具有数组的共同属性。由于字符串应用广泛，C和C++专门为它提供了许多方便的用法和函数。

（1）声明一个字符数组

（2）字符数组赋值方式

数组元素逐一赋值

聚合方式赋值

如果大括号中提供的初值个数大于数组长度，则按语法错误处理。如果初值个数小于数组长度，则只将这些字符赋给数组中前面那些元素，其余的元素自动定义为空字符。如果提供的初值个数与预定的数组长度相同，在定义时可以省略数组长度，系统会自动根据初值个数确定数组长度。

（3）字符数组的一些说明

聚合方式赋值只能在数组声明的时候使用，例如：

字符数组不能给字符数组赋值。

（4）字符串和字符串结束符

字符数组常作字符串使用，作为字符串要有字符串结束符'\0'。

可以使用字符串为字符数组赋值，例如：

等同于：

字符串结束符'\0'主要告知字符串处理函数字符串已经结束了，不需要再输出了。

下面通过实例来看一下使用字符串结束符'\0'和不使用字符串结束符'\0'的区别。

【实例7.3】 使用字符串结束符'\0'防止出现非法字符。 （实例位置：资源包\TM\sl\7\3）

未使用字符串结束符'\0'，程序代码如下：

程序运行结果如图7.6所示。

图7.6　未使用字符串结束符'\0'

使用字符串结束符'\0'，程序代码如下：

程序运行结果如图7.7所示。

图7.7　使用字符串结束符'\0'

printf函数使用%s格式可以输出字符串，如果字符串中没有结束符，函数会按整个字符数组输出。array字符数组中只有前两个字符初始化了，所以未使用字符串结束符'\0'的程序会出现乱码。

下面通过实例来熟悉一下在程序中对字符数组的操作。

【实例7.4】 输出字符数组中内容。 （实例位置：资源包\TM\sl\7\4）

程序运行结果如图7.8所示。

图7.8　输出字符数组中内容

（5）字符串处理函数

strcat函数

字符串连接函数strcat格式如下：

把字符数组2中的字符串连接到字符数组1中字符串的后面，并删去字符串1后的字符串结束符'\0'。

下面通过实例使用strcat函数将两个字符串连接在一起。

【实例7.5】 连接字符串。 （实例位置：资源包\TM\sl\7\5）

程序运行结果如图7.9所示。

图7.9　连接字符串

说明

在使用strcat函数的时候要注意，字符数组1的长度要足够大，否则不能装下连接后的字符串。

连接两个字符串时，可以不使用strcat函数。

下面通过实例来实现不使用strcat函数连接两个字符串的功能。

【实例7.6】 不使用strcat函数连接两个字符串。 （实例位置：资源包\TM\sl\7\6）

本例的关键是在将后一个字符串连接到前一个字符串的时候，要先判断前一个字符串的结束符在什么位置，只有找到了前一个字符串的结束符才能连接后一个字符串。

strcpy函数

字符串复制函数strcpy格式如下：

把字符数组2中的字符串复制到字符数组1中，字符串结束符'\0'也一同复制。

说明

要求字符数组1应有足够的长度，否则不能全部装入所复制的字符串。

②“字符数组1”必须写成数组名形式，而“字符数组2”可以是字符数组名，也可以是一个字符串常量，这时相当于把一个字符串赋予一个字符数组。

为了使读者更好地了解strcpy函数，下面通过实例使用strcpy函数来实现字符串复制的功能。

【实例7.7】 字符串复制。 （实例位置：资源包\TM\sl\7\7）

程序运行结果如图7.10所示。

图7.10　字符串复制

提示

strcpy函数实质上是用字符数组2中的字符串覆盖字符数组1中的内容，而strcat函数则不存在覆盖等问题，只是单纯地将字符数组2中的字符串连接到字符数组1中的字符串后面。

strcmp函数

字符串比较函数strcmp格式如下：

按照ASCII码顺序比较两个数组中的字符串，并由函数返回值返回比较结果。

字符串1=字符串2，返回值为0。

字符串1>字符串2，返回值为一正数。

字符串1<字符串2，返回值为一负数。

可用于比较两个字符串常量，或比较数组和字符串常量，例如：

该语句是两个数组进行比较。

该语句是一个数组与一个字符串进行比较。

该语句是两个字符串进行比较。

说明

进行比较的时候若出现不同的字符，则以第一个不同的字符的比较结果作为整个比较的结果。

下面通过实例来看一下如何使用strcmp函数对字符串进行比较。

【实例7.8】 字符串比较。 （实例位置：资源包\TM\sl\7\8）

程序运行结果如图7.11所示。

图7.11　字符串比较

strlen函数

测字符串长度函数strlen格式如下：

测字符串的实际长度（不含字符串结束符'\0'），函数返回值为字符串的实际长度。

下面通过实例使用strlen函数来实现获取字符串长度的功能。

【实例7.9】 获取字符串长度。 （实例位置：资源包\TM\sl\7\9）

程序运行结果如图7.12所示。

图7.12　获取字符串长度

7.4　指针

7.4.1　变量与指针

内存就像是带有编号的小房间，如果想使用就需要得到房间号。如图7.13所示，定义一个整型变量i，它需要4个字节，所以编译器为变量i分配了编号4001～4004的房间，每个房间代表一个字节。

图7.13　整型变量i

各个变量连续地存储在内存中，如图7.14所示，两个整型变量i和j存储在内存中。

图7.14　整型变量i和j

在程序代码中是通过变量名来对内存单元进行存取操作，但是代码经过编译后已经将变量名转换为该变量在内存的存放地址，对变量值的存取都是通过地址进行的。例如，语句i+j;的计算过程是根据变量名与地址的对应关系，找到变量i的地址4001，然后从4001开始读取4个字节数据放到CPU寄存器中，再找到变量j的地址4005，从4005开始读取4个字节的数据放到CPU另一个寄存器中，通过CPU的加法中断计算出结果。

在低级语言的汇编语言中都是直接通过地址来访问内存单元，而在高级语言中才使用变量名访问内存单元，C语言作为高级语言却提供了通过地址来访问内存单元的方法，C++也继承了这一特性。

由于通过地址能访问指定的内存存储单元，可以说地址“指向”该内存单元，例如，房间号4001指向系统内存中的一个字节。地址可以形象地称为指针，意思是通过指针能找到内存单元。一个变量的地址称为该变量的指针。如果有一个变量专门用来存放另一个变量的地址，它就是指针变量。在C++语言中有专门用来存放内存单元地址的变量类型，就是指针类型。

指针是一种数据类型，通常所说的指针就是指针变量，它是一个专门用来存放地址的变量，而变量的指针主要指变量在内存中的地址。变量的地址在编写代码时无法获取，只有在程序运行时才可以得到。

1．指针的声明

声明指针的一般形式如下：

例如：

2．指针的赋值

指针可以在声明的时候赋值，也可以后期赋值。

（1）在初始化时赋值

（2）在后期赋值

说明

通过变量名访问一个变量是直接的，而通过指针访问一个变量是间接的。

3．关于指针使用的说明

（1）指针变量名是p，而不是*p。

p=&i的意思是取变量i的地址赋给指针变量p。

下面的实例可以获取变量的地址，并将获取的地址输出。

【实例7.10】 输出变量的地址值。 （实例位置：资源包\TM\sl\7\10）

程序运行结果如图7.15所示。

图7.15　输出变量的地址值

实例7.10通过printf函数直接将地址值输出。由于变量是由系统分配空间，因此变量的地址不是固定不变的。

注意

在定义一个指针之后，一般要使指针有明确的指向。与常规的变量未赋值相同，没有明确指向的指针不会引起编译器出错，但是对于指针则可能导致无法预料的或者隐藏的灾难性后果，所以指针一定要赋值。

（2）指针变量不可以直接赋值，例如：

编译不能通过，有error C2440: '=' : cannot convert from 'const int' to 'int *'错误提示。

如果强行赋值，使用指针运算符*提取指针所指变量时会出错，例如：

（3）不能将*p当变量使用，例如：

上面代码可以编译通过，但运行时会弹出错误对话框，如图7.16所示。

图7.16　错误提示

下面的实例通过指针来实现数据大小比较的功能。

【实例7.11】 使用指针比较两个数的大小。 （实例位置：资源包\TM\sl\7\11）

程序运行结果如图7.17所示。

图7.17　使用指针比较两个数的大小

7.4.2　指针运算符和取地址运算符

1．指针运算符和取地址运算符简介

*和&是两个运算符，*是指针运算符，&是取地址运算符。

取地址运算符，如图7.18所示，变量i的值为100，存储在内存地址为4009的地方，取地址运算符&使指针变量p得到地址4009。

图7.18　取地址

指针运算符，如图7.19所示，指针变量存储的是地址编号4009，指针通过指针运算符可以得到地址。

图7.19　通过地址取值

下面的实例通过指针实现输出指针对应的数值的功能。

【实例7.12】 输出指针对应的数值。 （实例位置：资源包\TM\sl\7\12）

2．指针运算符和取地址运算符的说明

声明并初始化指针变量时同时用到了*和&这两个运算符，例如：

该语句等同于如下语句：

如果写成*p=&a;程序会报错。

&*p中的p只能是指针变量，如果将*放在变量名前，编译的时候会有逻辑错误，例如：

编译程序会出现error C2100: illegal indirection的错误提示。

3．&*p和*&a的区别

&和*的运算符优先级别相同，按自右而左的方向结合，因此&*p是先进行*运算，*p相当于变量a；再进行&运算，&*p就相当于取变量a的地址。*&a是先计算&运算符，&a就是取变量a的地址，然后计算*运算，*&a就相当于取变量a所在地址的值，实际就是变量a。

7.4.3　指针运算

指针变量存储的是地址值，对指针做运算就等于对地址做运算。下面通过实例来使读者了解指针的运算。

【实例7.13】 输出指针运算后地址值。 （实例位置：资源包\TM\sl\7\13）

程序运行结果如图7.20所示。

图7.20　输出指针运算后地址值

程序首先输出的是指向变量a的指针地址值1 703 740，然后对指针分别进行自加运算、自减运算、自减运算，输出的结果分别是1 703 744、1 703 740、1 703 736。

指针进行一次加1运算，其地址值并没有加1，而是增加了4，这和声明指针的类型有关。

p++是对指针做自加运算，相当于语句p=p+1，地址是按字节存放数据，但指针加1并不代表地址值加1个字节，而是加上指针数据类型所占的字节宽度，要获取字节宽度需要使用sizeof关键字，例如，整型的字节宽度是sizeof(int)，sizeof(int)的值为4。双精度整型的字节宽度是sizeof(double)，其值为8。将实例7.13中的int指针类型改为double，看看运行结果，程序代码如下：

程序运行结果如图7.21所示。

图7.21　程序运行结果

说明

定义指针变量时必须指定一个数据类型。指针变量的数据类型用来指定该指针变量所指向数据的类型。

7.4.4　指向空的指针与空类型指针

指针可以指向任何数据类型的数据，包括空类型（void）：

空类型指针可以接受任何类型的数据，当使用它时，可以将其强制转化为所对应数据类型。

【实例7.14】 空类型指针的使用。 （实例位置：资源包\TM\sl\7\14）

程序运行结果如图7.22所示。

图7.22　程序运行结果

可以看到空指针赋值后，转化为对应类型的指针才能得到我们所期望的结果。若将它转换为其他类型的指针，得到的结果将不可预知，非空类型指针同样具有这样的特性。在本实例中，出现了一个符号NULL，它表示空值。空值无法用输出语句表示，而且赋空的指针无法被使用，直到它被赋予其他的值。

7.4.5　指向常量的指针与指针常量

同其他数据类型一样，指针也有常量，使用const关键字形式如下：

将关键字const放在标识符前，表示这个数据本身是常量，而数据类型是int*，即整型指针。与其他常量一样，指针常量必须初始化。我们无法改变它的内存指向，但是可以改变它指向内存的内容。

若将关键字const放到指针类型的前方，形式如下：

这是指向常量的指针，虽然它所指向的数据可以通过赋值语句进行修改，但是通过该指针修改内存内容的操作是不被允许的。

当const以如下形式使用时：

该指针是一个指向常量的指针常量。既不可以改变它的内存指向，也不可以通过它修改指向内存的内容。

【实例7.15】 指针与const。 （实例位置：资源包\TM\sl\7\15）

程序运行结果如图7.23所示。

图7.23　程序运行结果

7.5　指针与数组

7.5.1　数组的存储

数组，作为同名、同类型元素的有序集合，被顺序存放在一块连续的内存中，而且每个元素存储空间的大小相同。数组第一元素的存储地址就是整个数组的存储首地址，该地址放在数组名中。

对于一维数组而言，其结构是线性的，所以数组元素按下标值由小到大的顺序依次存放在一块连续的内存中。在内存中存储一维数组如图7.24所示。

图7.24　一维数组的存储

对于二维数组而言，用矩阵方式存储元素，在内存中仍然是线性结构。

7.5.2　指针与一维数组

系统需要提供一定量连续的内存来存储数组中的各元素，内存都有地址，指针变量就是存放地址的变量，如果把数组的地址赋给指针变量，就可以通过指针变量来引用数组。引用数组元素有两种方法：下标法和指针法。

通过指针引用数组，就要先声明一个数组，再声明一个指针。

然后通过&运算符获取数组中元素的地址，再将地址值赋给指针变量。

把a[0]元素的地址赋给指针变量p，即p指向a数组的第0号元素，如图7.25所示。

图7.25　指针指向数组元素

下面通过实例使读者了解指针和数组间的操作，实例将实现通过指针变量获取数组中元素的功能。

【实例7.16】 通过指针变量获取数组中元素。 （实例位置：资源包\TM\sl\7\16）

如果指针变量p已指向数组中的一个元素，则p+1指向同一数组中的下一个元素。

p+i和a+i是a[i]的地址。a代表首元素的地址，a+i也是地址，对应数组元素a[i]。

(p+i)或*(a+i)是p+i或a+i所指向的数组元素，即a[i]。

程序中使用指针获取数组首元素的地址，也可以将数组名赋值给指针，然后通过指针访问数组。程序代码如下：

程序运行结果如图7.26所示。

图7.26　通过指针变量获取数组中元素

说明

在处理字符串函数的章节中，数组名为何能作为函数参数呢？原因如同看到的一样，它其实是一个指针常量。在数组声明之后，C++分配给了数组一个常指针，始终指向数组的第一个元素。而本章中出现的字符串处理函数中接受数组名的参数列表，也接受字符指针。关于字符数组和指针的详细问题，在后边的章节还会介绍。

程序中使用数组地址来进行计算，a+i表示数组a中的第i个元素，然后通过指针运算符就可以获得数组元素的值。

指针操作数组的一些说明如下：

（1）*(p--)相当于a[i--]，先对p进行*运算，再使p自减。

（2）*(++p)相当于a[++i]，先使p自加，再进行*运算。

（3）*(--p)相当于a[--i]，先使p自减，再进行*运算。

7.5.3　指针与二维数组

可以将一维数组的地址赋给指针变量，同样也可以将二维数组的地址赋给指针变量，因为一维数组的内存地址是连续的，二维数组的内存地址也是连续的，可以将二维数组看成是一维数组。二维数组各元素的地址如图7.27所示。

图7.27　二维数组各元素的地址

因为多维数组可以看成是一维数组，本实例实现将多维数组转换成一维数组的功能。

【实例7.17】 将多维数组转换成一维数组。 （实例位置：资源包\TM\sl\7\17）

程序运行结果如图7.28所示。

图7.28　将多维数组转换成一维数组

使用指针引用二维数组和引用一维数组相同。首先声明一个二维数组和一个指针变量：

a[0]是二维数组的第一个元素的地址，可以将该地址值直接赋给指针变量。

此时使用指针p就可以引用二维数组中的元素了。

为了更好地操作二维数组，下面通过实例来实现使用指针变量遍历二维数组的功能。

【实例7.18】 使用指针变量遍历二维数组。 （实例位置：资源包\TM\sl\7\18）

程序运行结果如图7.29所示。

图7.29　使用指针变量遍历二维数组

程序中通过*p对二维数组中的所有元素都进行了引用，如果想对二维数组中某一行中的某一列元素进行引用，就需要将二维数组不同行的首元素地址赋给指针变量。如图7.30所示，可以将4个行首元素地址赋给变量p。

a代表二维数组的地址，通过指针运算符可以获取数组中的元素。

（1）a+n表示第n行的首地址。

（2）&a[0][0]既可以看作数组0行0列的首地址，同样还可以看作是二维数组的首地址。&a[m][n]就是第m行n列元素的地址。

（3）&a[0]是第0行的首地址，当然&a[n]就是第n行的首地址。

（4）a[0]+n表示第0行第n个元素地址。

（5）*(*(a+n)+m)表示第n行第m列元素。

（6）*(a[n]+m)表示第n行第m列元素。

图7.30　指针指向二维数组

【实例7.19】 使用数组地址将二维数组输出。 （实例位置：资源包\TM\sl\7\19）

程序运行结果如图7.31所示。

图7.31　使用数组地址将二维数组输出

为什么指向二维数组的指针要以如此的形式表示出来？函数名a是一个指向数组的指针。直接依照a偏移a+n所得到的是行数组a[n]的地址&a[n]，也就是行地址。a[n]是一个指针，它也是第n行的一维数组的名字。获得具体元素加上偏移求值，得到*(a[n]+m)，也就是*(*(a+n)+m)。

下面通过一个实例，使读者更好地理解二维数组的原理。

【实例7.20】 数组指针与指针数组。 （实例位置：资源包\TM\sl\7\20）

程序运行结果如图7.32所示。

图7.32　数组指针与指针数组

7.5.4　指针与字符数组

字符数组是一个一维数组，使用指针同样也可以引用字符数组。引用字符数组的指针为字符指针，字符指针就是指向字符型内存空间的指针变量，其一般的定义语句如下：

字符数组就是一个字符串，通过字符指针可以指向一个字符串。

语句：

等价于下面两个语句：

为了使读者更好地了解指针与字符数组间的操作，下面通过两个实例来实现连接两个字符数组的功能。

【实例7.21】 通过指针偏移连接两个字符串。 （实例位置：资源包\TM\sl\7\21）

程序运行结果如图7.33所示。

图7.33　通过指针偏移连接两个字符串

程序需要用户输入两个字符数组，然后通过字符指针将两个字符串连接起来。

同样地，还可以使用处理字符串函数strcat来实现连接两个字符串。

【实例7.22】 通过字符串函数strcat连接两个字符串。 （实例位置：资源包\TM\sl\7\22）

程序运行结果如图7.34所示。

图7.34　使用strcat函数连接两个字符串

7.6　指针在函数中的应用

7.6.1　传递地址

以前所接触到的函数都是按值传递参数，也就是说实参传递进函数体内后，生成的是实参的副本。当在函数内改变副本的值并不影响到实参。而指针传递参数时，指针变量产生了副本，但副本与原变量所指向的内存区域是同一个。对指针副本指向的变量进行改变，就是改变原指针变量所指向的变量。

【实例7.23】 调用自定义函数交换两变量值。 （实例位置：资源包\TM\sl\7\23）

程序运行结果如图7.35所示。

图7.35　调用自定义函数交换两变量值

从图7.35中结果可以看出，使用指针传递参数的函数真正实现了x与y的交换，而按值传递函数只是交换了x与y的副本。

swap函数是用户自定义的重载函数，在main函数中调用该函数交换变量a和b的值。按指针传参的swap函数的两个形参被传入了两个地址值，也就是传入了两个指针变量，在swap函数的函数体内使用整型变量tmp作为中转变量，将两个指针变量所指向的数值进行交换。在main函数内首先获取输入的两个数值，分别传递给变量x和y，将x和y的地址值传递给swap函数。在按指针传递的swap函数内，两个指针变量的副本a和b所指向的变量正是x与y，而按值传递的swap函数并没有实现交换x与y的功能。

7.6.2　指向函数的指针

指针变量也可以指向一个函数。一个函数在编译时被分配给一个入口地址，这个函数入口地址就称为函数的指针。可以用一个指针变量指向函数，然后通过该指针变量调用此函数。

一个函数可以带回一个整型值、字符值、实型值等，也可以带回指针型的数据，即地址。其概念与以前类似，只是带回的值的类型是指针类型而已。返回指针值的函数简称为指针函数。

定义指针函数的一般形式如下：

例如，定义一个具有两个参数和一个返回值的函数的指针：

函数指针能指向返回值与参数列表的函数，当使用函数指针时形式如下：

下面通过实例实现使用指针函数进行平均值计算的功能。

【实例7.24】 使用指针函数进行平均值计算。 （实例位置：资源包\TM\sl\7\24）

在未使用指针函数计算平均值时，可以通过自定义函数avg来进行平均值计算。计算哪两个整型数的平均值，直接将两个整型数传递给avg函数，avg函数完成计算后将计算结果传出。程序代码如下：

avg函数是一个具有两个参数和一个返回值的函数，可以定义一个指针函数指向该函数，指针函数必须是具有两个整型参数和一个整型返回值的形式。使用指针函数进行平均值计算的程序代码如下：

指针pFun是指向avg函数的函数指针，调用pFun函数指针，就和调用函数avg一样。

7.6.3　空指针调用函数

空类型指针指向任意类型函数或者将任意类型的函数指针赋值给空类型指针都是合法的。使用空指针调用自身所指向的函数，仍然按照强制转换的形式使用。

【实例7.25】 使用空类型指针执行函数。 （实例位置：资源包\TM\sl\7\25）

程序运行结果如图7.36所示。

图7.36　使用空类型指针执行函数

注意

当函数被重载时，不要使用直接将函数名赋给空类型指针的操作（如上例），这会使编译器无法确定将哪个重载函数交给空类型指针。

7.6.4　从函数中返回指针

定义一个返回指针类型的函数，形式如下：

p是一个指针变量，也可以是形式如 &value的地址值。当函数返回一个指针变量，我们得到的是地址值。值得注意的是，返回指针的内存内容并不随返回的地址一样经过复制成为临时变量。如果操作不当，后果将难以预料。

【实例7.26】 指针做返回值 。（实例位置：资源包\TM\sl\7\26）

程序运行结果如图7.37所示。

图7.37　指针做返回值

可以看到，函数返回的是函数中定义的i的地址。函数执行后，i的内存被销毁，值变成了一个不可预知的数。

注意

值为NULL的指针地址为0，但并不意味着这块内存可以使用。将指针赋值为NULL也是基于安全而考虑的，以后的章节还将详细讨论内存的安全问题。

7.7　指针数组

数组中的元素均为指针变量的数组称为指针数组，一维指针数组的定义形式如下：

例如：

指针数组中的数组名也是一个指针变量，该指针变量为指向指针的指针。

例如：

p是一个指针数组，它的每一个元素是一个指针型数据（其值为地址），指针数组p的第一个值是变量a的地址。指针数组中的元素可以使用指向指针的指针来引用，例如：

*运算符表示p是一个指针变量，*(*p)表示指向指针的指针，*运算符的结合性是从右到左，因此int *(*p); 可写成int **p;。

指向指针的指针获取指针数组中的元素和利用指针获取一维数组中的元素方法相同，如图7.38所示。

图7.38　指向指针的指针

第一次*运算获取到的是一个地址值，再进行一次*运算，就可以获取到具体值。

【实例7.27】 用指针数组中各个元素分别指向若干个字符串。 （实例位置：资源包\TM\sl\7\27）

程序运行结果如图7.39所示。

图7.39　用指针数组中各个元素分别指向若干个字符串

程序中的print函数中，数组名name代表该指针数组首元素的地址，name+i是name[i]的地址。由于name[i]的值是地址（即指针），因此name+i就是指向指针型数据的指针。还可以设置一个指针变量p，它指向指针数组的元素。p就是指向指针型数据的指针变量，它所指向的字符串如图7.40所示。

图7.40　p所指向的字符串

利用指针变量访问另一个变量就是间接访问。如果在一个指针变量中存放一个目标变量的地址，这就是单级间址。指向指针的指针用的是二级间址方法，还有三级间址和四级间址，但二级间址应用最为普遍。

7.8　安全使用指针

7.8.1　内存分配

1．堆与栈

在程序中定义一个变量，它的值会被放入内存当中。如果没有申请动态分配的方式，它的值将放到栈中。在栈中的变量所属的内存大小是无法被改变的，它们的产生与消亡也与变量定义的位置和储存方式有关。与栈相对应，堆是一种动态分配方式的内存。当申请使用动态分配方式去储存某个变量，那么这个变量会被放入堆中。根据需要，这个变量的内存大小可以发生改变，内存的申请和销毁的时机则由编程者来操作。

2．关键字new与delete

创建变量之前，编译器没有获取到变量的名称，只具有指向该变量的指针。那么，申请变量的堆内存即是申请自身指向堆。new是C++语言申请动态内存的关键字，形式如下：

其中，p1表示指针，new是关键字，type是类型名。new返回新分配的内存单元的地址。

这样，pI指针就申请了动态方式，使用它在堆内申请的内存储存int类型的值。

【实例7.28】 动态分配空间。 （实例位置：资源包\TM\sl\7\28）

可以看到指针pI1创建后申请了动态分配，程序自动交给了它一块堆内存。而指针pI2则是获取了栈中的内存地址，属于静态分配。

程序运行结果如图7.41所示。

图7.41　动态分配空间

动态分配方式虽然很灵活，但是随之带来新的问题。申请一块堆内存后，系统不会在程序执行时依据情况自动销毁它。若想释放该内存空间，则需要使用delete关键字。

【实例7.29】 动态内存的销毁。 （实例位置：资源包\TM\sl\7\29）

程序运行结果如图7.42所示。

图7.42　动态内存的销毁

变量p接受了newGet返回的指针的堆内存地址，所以内存的内容并没被销毁，而栈内存则由系统控制。程序最后使用delete语句释放了堆内存。

7.8.2　内存安全

指针是C++提供给我们的强大而灵活的工具，如何安全地使用它们对内存安全的操作是编程者必须要掌握的。在前面的章节中讨论到过指针所指向内存销毁的问题，当一块内存被销毁时，该区域不可复用。若有指针指向该区域，则需要将该指针置空值（NULL）或者指向未被销毁的内存。

内存销毁实质上是系统判定该内存不是编程人员正常使用的空间，系统也会将它分配给别的任务。若擅自使用被销毁内存的指针更改该内存的数据，很可能会造成意想不到的结果。

【实例7.30】 被销毁的内存。 （实例位置：资源包\TM\sl\7\30）

这是一个反例，也许它会造成内存出错。

程序运行结果如图7.43所示。

图7.43　被销毁的内存

指针pI从sum函数中得到了一个临时指针，该指针是指针pS的临时复制品，操作完成后消失，它所保留的地址交给了pI。在函数sum执行完毕后，该域使用的栈内存会被系统销毁甚至挪用。本程序尝试通过pI继续使用、修改它，结果是系统会再次销毁它。在某些场合下，该程序也许会引起内存报错，甚至造成多个程序崩溃。所以对于栈内存的指针一定要明白其何时销毁，不再重复利用它。

与此相对应的另一个安全问题叫内存泄漏。如同我们所知道的，在申请动态分配内存后，系统不会主动销毁该堆内存，需要编程者使用delete关键字通知系统销毁。如果不这样做，系统将浪费很多资源，使程序执行时变得臃肿，只需占用数十MB内存的程序可能为此占用上百MB的内存。可见，回收堆内存空间是很重要的。销毁内存时，需要保留指向该堆内存的指针。当没有指针指向一块没被回收的堆内存时，此块内存犹如丢失了一般，称之为内存泄漏。

【实例7.31】 丢失的内存。 （实例位置：资源包\TM\sl\7\31）

这是一个反例，它会造成内存出错。

程序运行结果如图7.44所示。

图7.44　丢失的内存

程序中动态分配的内存开始由pF指向。当pF改变指向后，此块内存再也无法回收了。

一般情况下，我们无法通过调试程序发现内存泄漏。所以，使用动态分配时一定要注意形成良好的习惯。

【实例7.32】 回收动态内存的一般处理步骤。 （实例位置：资源包\TM\sl\7\32）

程序运行结果如图7.45所示。

图7.45　程序运行结果

注意

指针是一种灵活高效的内存访问机制，它可以通过变量在内存中的地址来对变量直接操作。但是指针却不能访问寄存器变量，因为寄存器变量并没有保存在内存中，而是保存在寄存器中（从寄存器中读取数据要比从内存中读取数据速度快，所以有些要求频繁使用的数据可以被放在寄存器中）。指针只能访问内存，不能访问寄存器，所以指针访问不到寄存器变量。

7.9　引用

7.9.1　引用概述

在C++11标准中提出了左值引用的概念，如果不加特殊声明。一般认为引用指的都是左值引用。

引用实际上是一种隐式指针，它为对象建立一个别名，通过操作符&来实现。&是取地址操作符，通过它可以获得地址。

引用的形式如下：

例如：

定义了一个引用变量ia，它是变量a的别名，对ia的操作与对a的操作完全一样。ia=2把2赋给a，&ia返回a的地址。执行ia=2和执行a=2等价。

使用引用的说明如下：

（1）一个C++引用被初始化后，无法使用它再去引用另一个对象，它不能被重新约束。

（2）引用变量只是其他对象的别名，对它的操作与原来对象的操作具有相同作用。

（3）指针变量与引用有两点主要区别：一是指针是一种数据类型，而引用不是一个数据类型，指针可以转换为它所指向变量的数据类型，以便赋值运算符两边的类型相匹配；而在使用引用时，系统要求引用和变量的数据类型必须相同，不能进行数据类型转换。二是指针变量和引用变量都用来指向其他变量，但指针变量使用的语法要复杂一些；而在定义了引用变量后，其使用方法与普通变量相同。

例如：

（4）引用应该初始化，否则会报错。

例如：

编译器会报出references must be initialized这样的错误，造成编译不能通过。

下面通过实例使读者更好地了解引用的使用，实例输出引用的功能。

【实例7.33】 输出引用。 （实例位置：资源包\TM\sl\7\33）

程序声明了变量a和一个对变量a的引用ref_a，通过不断地改变变量a和引用ref_a的值使读者了解引用的使用，然后将改变的结果输出，程序运行结果如图7.46所示。

图7.46　输出引用

右值引用是C++11（即C++0x）新增加的一个非常量的引用类型，它的形式如下：

左值与右值的区别在于，右值是临时变量，如函数的返回值，并且无法被改变，例如：

那么，什么是右值引用呢？右值引用可以理解为右值的引用，当右值引用初始化后，临时变量消失。

关于右值引用的定义，示例如下：

程序运行结果如图7.47所示。

图7.47　程序运行结果

右值引用只可以初始化于右值，但右值引用实质上是一个左值，它具有临时变量的数据类型。

与左值引用相同的是：

一个右值引用被初始化后，无法使用它再去引用另一个对象，它不能被重新约束。

右值引用初始化后，具有该类型数据的所有操作。

7.9.2　使用引用传递参数

在C++语言中，函数参数的传递方式主要有两种，分别为值传递和引用传递。所谓值传递，是指在函数调用时，将实际参数的值赋值一份传递到调用函数中，这样如果在调用函数中修改了参数的值，其改变不会影响到实际参数的值。而引用传递则恰恰相反，如果函数按引用方式传递，在调用函数中修改了参数的值，其改变会影响到实际参数。

【实例7.34】 通过引用交换数值。 （实例位置：资源包\TM\sl\7\34）

程序运行结果如图7.48所示。

图7.48　通过引用交换数值

程序中自定义函数swap，函数定义了两个引用参数，用户输入两个值，如果第一次输入的数值比第二次输入的数值小，则调用swap函数交换用户输入的数值。如果使用值传递方式，swap函数就不能实现交换。

7.9.3　指针传递参数

指针变量可以作为函数参数。使用指针变量传递参数和使用引用传递方式的执行效果相同，可以对同一个函数使用两种方式传递参数进行对比，观察程序的运行结果。

为了使读者更好地了解指针作为参数进行传递的操作，下面通过指针作为参数实现变量值的交换功能。

【实例7.35】 调用自定义函数交换两变量值。 （实例位置：资源包\TM\sl\7\35）

程序运行结果如图7.49所示。

图7.49　调用自定义函数交换两变量值

从图7.49中可以看出，程序运行结果为输入3、6，输出6、3。

swap函数是用户自定义函数，在main函数中调用该函数交换变量x和y的值。swap函数的两个形参a、b被传入了两个地址值，也就是传入了两个指针变量，在swap函数的函数体内使用整型变量tmp作为中转变量，将两个指针变量所指向的数值进行交换。在main函数内首先获取输入的两个数，分别传递给变量x和y，然后比较两个变量x和y的大小，如果x小于y，就将x和y的地址值传递给swap函数，调用swap函数将变量x和y的数值互换。

通过指针传递参数和使用引用方式传递参数一样，都可以减少值传递带来的开销。在开发程序时是使用指针还是使用引用类型作为函数参数呢？实际上，使用指针和引用类型作为函数参数各有优缺点，视具体环境而定。对于引用类型，引用必须被初始化为一个对象，并且不能使它再指向其他对象，因为对应用赋值实际上是对目标对象赋值。这是引用类型的缺点，但也是引用类型的优点，因为在函数中不用验证引用参数的合法性，例如，下面的函数调用是非法的。

程序中，如果ValuePass采用指针作为函数参数，使用ValuePass(0); 语句调用是合法的，但却带来了隐患，因为0被认为是空指针，对空指针操作必然会导致地址访问错误，因此对于指针对象作为函数参数，函数体中需要验证指针参数是否为空，这是使用指针类型作为函数参数的缺点，但是，使用指针对象作为函数参数，用户可以随意修改指针参数指向的对象，这是引用类型参数所不能的。

7.9.4　数组做函数参数

在函数调用过程中，有时需要传递多个参数，如果传递的参数都是同一类型，则可以通过数组的方式来传递参数，作为参数的数组可以是一维数组，也可以是多维数组。使用数组做函数参数最典型的就是main函数。带参数的main函数形式如下：

main函数中的参数可以获取程序运行的命令参数，命令参数就是执行应用程序时后面带的参数。例如，在CMD控制台执行dir命令，可以带上“/w”参数，“dir /w”命令是以多列的形式显示出文件夹内的文件名。main函数中参数argc是获取命令参数的个数，argv是字符指针数组，可以获取具体的命令参数。

【实例7.36】 获取命令参数。 （实例位置：资源包\TM\sl\7\36）

上面的代码在工程sample中将生成sample.exe应用程序，在执行sample.exe时在后面加上参数，程序就会输出命令参数，例如，执行命令及运行结果如图7.50所示。

图7.50　获取命令参数

程序执行时输入命令参数“/a /b /c”，程序运行以后将3个命令参数输出，每个参数都是以空格隔开，应用程序后有3个空格，代表程序有3个命令参数，argc的值就为3。

二维数组在作为函数参数时，可以将二维数组转换成一个一维的指针数组。main函数中的argv数组就可以是一个二维的字符数组。

【实例7.37】 输出每行数组中的最小值。 （实例位置：资源包\TM\sl\7\37）

程序运行结果如图7.51所示。

图7.51　输出每行数组中的最小值

程序需要用户输入12个数值来作为一个3行4列数组的元素，然后按行进行比较，输出每行最小元素。*(a+i)代表数组每行第一个元素，*(*(a+i)+j)代表数组指定行中的某个列元素。函数mix对数组每行元素逐一进行比较，将最小值赋给变量value，然后输出变量value的值。

7.10　小结

指针是C++中的难点，它可以控制变量，操作数组，指向函数，所以一定要理解指针的基本用法。本章讲述的都是指针的基本用法，要从概念上区分指针与变量，要区分指针和数组首元素，既要学会指针传递参数，也要学会使用引用传递参数。

7.11　实践与练习

（1）使用字符数组和实型数组分别存储学生姓名和成绩，并通过对学生成绩的排序，按照名次输出字符数组中对应的学生姓名。 （实例位置：资源包\TM\sl\7\38）

（2）开发一个程序，通过二维数组存储星期信息，当输入0～6之间的数字时输出对应的星期。 （实例位置：资源包\TM\sl\7\39）

（3）要求设计一个3行4列的二维数组，依次保存数据1、3、5、7、9、11、13、15、17、19、21、23，然后使用指针的方式将数组中的数据输出显示。 （实例位置：资源包\TM\sl\7\40）

第8章

构造数据类型

（视频讲解： 59分钟）

结构体可以将不同类型组合在一起形成一个新的类型，这个类型是对数据的整合，使代码更加简洁。共用体和结构体很相近，它像一个存储空间可变的数据类型，使程序设计更加灵活。枚举则是特殊的常量，增加代码的可读性。自定义类型更是增加了代码的复用性。

通过阅读本章，您可以：

掌握结构体

了解共用体

掌握枚举类型

掌握自定义数据类型

8.1　结构体

8.1.1　结构体定义

整型、长整型、字符型、浮点型这些数据类型只能记录单一的数据，它们只能被称作基本数据类型。如果要描述一个人的信息，就需要定义多个变量来记录这些信息，例如，身高需要一个变量，体重需要一个变量，姓名需要一个变量，年龄需要一个变量。如果有一个类型可以将这些变量包含在一起，则会大大减少程序代码的离散性，使程序代码阅读更加符合逻辑，结构体则是实现这一功能的类型。

结构体的定义如下：

struct是定义结构体的关键字。结构体类型名是一种标识符，该标识符代表一个新的变量。结构体使用大括号将成员括起来，每个成员都有自己的类型，成员类型可以是常规的基础类型，也可以是自定义类型，同时还可以是一个类类型。

例如，定义一个简单员工信息的结构体。

结构体类型名是PersonInfo，在结构体中定义了3个不同类型的变量。这3个变量就好像是3个球放到了一个盒子里，只要能找到这个盒子就能找到这3个球。同样，找到名字为PersonInfo的结构体，就可以找到结构体下的变量。这3个变量的数据类型各不相同，有字符串型，有整型，分别定义了员工的编号、姓名和年龄。

说明

给结构体下个定义，就是由多个不同类型的数据组成的数据集合。而数组是相同元素的集合。

8.1.2　结构体变量

结构体是一个构造类型，前面只是定义了结构体，形成一个新的数据类型，还需要使用该数据类型来定义变量。结构体变量有两种声明形式。

第一种声明形式是在定义结构体后，使用结构体类型名声明，例如：

另一种是定义结构体时直接声明，例如：

直接声明结构体变量时，可以声明多个变量，例如：

8.1.3　结构体成员及初始化

引用结构体成员有两种方式，一种是声明结构体变量后，通过成员运算符“．”引用；另一种是声明结构体指针变量，使用指向运算符“->”引用。

（1）使用成员运算符“．”引用结构体成员，一般形式如下：

例如：

引用到结构体成员后，就可以分别对结构体成员进行赋值，对于每个结构体成员就和使用普通变量一样。

下面通过实例来看一下如何为结构体成员赋值。

【实例8.1】 为结构体成员赋值。 （实例位置：资源包\TM\sl\8\1）

程序运行结果如图8.1所示。

图8.1　为结构体成员赋值

程序分别为引用结构体的每个成员，然后赋值，其中为字符数组赋值需要使用strcpy字符串复制函数。结构体可以在定义时直接对结构体变量赋值，例如：

（2）在定义结构体时，可以同时声明结构体指针变量，例如：

如果要引用指针结构体变量的成员，需要使用指向运算符“->”，一般形式如下：

例如：

注意

结构体指针变量只有初始化后才可以使用。

下面通过实例来看一下如何通过结构体指针变量来引用结构体成员。

【实例8.2】 使用结构体指针变量引用结构体成员。 （实例位置：资源包\TM\sl\8\2）

程序运行结果如图8.2所示。

图8.2　程序运行结果

8.1.4　结构体的嵌套

定义完结构体后就形成一个新的数据类型，C++语言在定义结构体时可以声明其他已定义好的结构体变量，也可以在定义结构体时定义子结构体。

（1）在结构体中定义子结构体，例如：

（2）在定义时声明其他已定义好的结构体变量，例如：

通过上面的两种形式都可以完成结构体的嵌套，下面通过第一种方式来实现结构体的嵌套。

【实例8.3】 使用嵌套的结构体。 （实例位置：资源包\TM\sl\8\3）

程序运行结果如图8.3所示。

图8.3　使用嵌套的结构体

程序在PersonInfo结构体中嵌套了WorkPlace结构体，然后分别对WorkPlace子结构体中的成员进行赋值，最后将WorkPlace子结构体中的成员输出。

8.1.5　结构体大小

结构体是一种构造的数据类型，数据类型都与占用内存多少有关。在没有字符对齐要求或结构成员对齐单位为1时，结构体变量的大小是定义结构体时各成员大小之和，例如PersonInfo结构体：

PersonInfo结构体的大小是成员name、index和age大小之和。成员name是字符数组，一个字符占用1个字节，name占用30个字节；成员index是整型数据，在32位系统中占4个字节；成员age是短整型，在32位系统中占2个字节。所以PersonInfo结构体的大小是30+4+2=36字节。

可以使用sizeof运算符获取结构体大小，例如：

程序使用sizeof运算符输出的结果仍然是36字节。

如果更改结构成员对齐单位，PersonInfo结构体实际占用的内存空间就不是36字节了。在Visual C++6.0中可以通过修改工程属性来改变结构成员对齐单位。通过选择Project→Settings命令，打开Project Settings对话框，如图8.4所示，选择C/C++选项卡，在Category下拉列表框中选择Code Generation，通过Struct member alignment下拉列表框就可以改变结构成员对齐单位。

图8.4　Project Settings对话框

默认结构成员对齐单位是8个字节，结构成员对齐单位在使用结构体变量传送数据时能看到其差异。

8.2　重命名数据类型

C++允许使用关键字typedef给一个数据类型定义一个别名，例如：

这样，程序中flag就可以作为int的数据类型来使用：

a实质上是int类型的数据，此时int类型的别名就是flag。

类或者结构在声明的时候使用typedef：

这样就令声明的类拥有myClass和ClassA两个别名。

typedef主要的用途如下：

（1）代替很复杂的基本类型名称，例如函数指针int (*)(int i)。

（2）使用其他人开发的类型时，使它的类型名符合自己的代码习惯（规范）。

typedef关键字具有作用域，范围是别名声明所在的区域（包含名称空间）。

【实例8.4】 三只宠物犬。 （实例位置：资源包\TM\sl\8\4）

程序运行结果如图8.5所示。

图8.5　执行结果

在pet名称空间中定义了多种类型别名。这些别名的实际类型不发生改变，在主函数内演示了如何使用名称空间中的类别名。

宠物狗dog类中使用string类来区分小狗的种类，通过setVoice函数设定每种小狗的声音，那么，有没有比使用string对象更简便的办法呢？除了建立3个子类之外，有没有更简便一些的方法呢？下一节将继续讨论。

8.3　结构体与函数

结构体数据类型在C++语言中是可以作为函数参数传递的，可以直接使用结构体变量做函数的参数，也可以使用结构体指针变量做函数的参数。

8.3.1　结构体变量做函数参数

可以把结构体变量当普通变量一样作为函数参数，这样可以减少函数参数的个数，使代码看起来更简洁。

下面通过实例来了解一下如何使用结构体变量做函数参数进行传递。

【实例8.5】 使用结构体变量做函数参数 。（实例位置：资源包\TM\sl\8\5）

程序运行结果如图8.6所示。

图8.6　使用结构体变量做函数参数

程序自定义了函数ShowStuctMessage，该函数使用PersonInfo结构体作为参数。如果不使用结构体作为参数，函数需要将index、name、age 3个成员分别定义为参数。

8.3.2　结构体指针做函数参数

使用结构体指针变量做函数参数时传递的只是地址，减少了时间和空间上的开销，能够提高程序的运行效率。这种方式在实际应用中效果比较好。

下面通过实例来看一下如何使用结构体指针做函数参数进行传递。

【实例8.6】 使用结构体指针变量做函数参数。 （实例位置：资源包\TM\sl\8\6）

程序运行结果如图8.7所示。

图8.7　使用结构体指针变量做函数参数

实例8.6和实例8.5的运行结果相同，但在程序执行效率上，使用结构体指针做函数参数方式要快很多。

8.4　结构体数组

数组的元素也可以是结构体类型的，因此可以构成结构体类型数组。结构体数组的每一个元素都是具有相同结构类型的下标结构变量。

8.4.1　结构体数组声明与引用

结构体数组可以在定义结构体时声明，可以使用结构体变量声明，也可以直接声明结构体数组而无须定义结构体名。

（1）在定义结构体时直接声明，例如：

（2）使用结构体变量声明，例如：

（3）直接声明结构体数组，例如：

可以在声明结构体数组时直接对数组进行初始化。

说明

当对全部元素进行初始化赋值时，也可不给出数组长度。

8.4.2　指针访问结构体数组

指针变量可以指向一个结构体数组，这时结构体指针变量的值是整个结构体数组的首地址。结构体指针变量也可指向结构体数组的一个元素，这时结构体指针变量的值是该结构体数组元素的首地址。

【实例8.7】 使用指针访问结构体数组。 （实例位置：资源包\TM\sl\8\7）

程序运行结果如图8.8所示。

图8.8　使用指针访问结构体数组

程序的关键在于pPersonInfo++的运算上，pPersonInfo指针开始指向数组的首元素，结构体指针自加1，其结果使pPersonInfo指针指向了数组的下一个元素。

8.5　共用体

共用体数据类型是指将不同的数据项组织为一个整体，它和结构体有些类似，但共用体在内存中占用首地址相同的一段存储单元。因为共用体的关键字为union，中文意思为联合，所以共用体也称为联合体。

8.5.1　共用体的定义与声明

定义共用体类型的一般形式如下：

union是定义共用体数据类型的关键字，共用体类型名是一个标识符，该标识符以后就是一个新的数据类型，成员类型是常规的数据类型，用来设置共用体成员存储空间。

声明共用体数据类型变量有以下几种方式：

（1）先定义共用体，然后声明共用体变量，例如：

（2）可以直接在定义时声明共用体变量，例如：

（3）也可以直接声明共用体变量，例如：

第三种方式省略了共用体类型名，直接声明了变量u。

引用共用体对象成员和引用结构体对象类型的方式相同，也是使用“．”运算符，例如，引用共用体u的成员：

上面是对共用体u的3个成员的引用，但要注意不能引用共用体变量，而只能引用共用体变量中的成员。例如，直接引用u是错误的。

8.5.2　共用体的大小

共用体每个成员分别占有自己的内存单元。共用体变量所占的内存长度等于最长的成员的长度。一个共用体变量不能同时存放多个成员的值，某一时刻只能存放其中的一个成员的值，这就是最后赋予它的值。

【实例8.8】 使用共用体变量。 （实例位置：资源包\TM\sl\8\8）

程序运行结果如图8.9所示。

图8.9　使用共用体变量

程序中按不同顺序为uStruct变量的3个成员赋值，结果显示只有最后赋值的成员能正确显示。

8.5.3　共用体的特点

共用体数据类型有以下几个特点：

（1）使用共用体变量的目的是希望用同一个内存段存放几种不同类型的数据，但请注意，在每一瞬时只能存放其中一种，而不是同时存放几种。

（2）能够访问的是共用体变量中最后一次被赋值的成员，在对一个新的成员赋值后原有的成员就失去作用。

（3）共用体变量的地址和它的各成员的地址都是同一地址。

（4）不能对共用体变量名赋值；不能企图引用变量名来得到一个值；不能在定义共用体变量时对它初始化；不能用共用体变量名作为函数参数。

8.6　枚举类型

枚举就是一一列举的意思，在C++语言中枚举类型是一些标识符的集合，从形式上看枚举类型就是用大括号将不同标识符名称放在一起。用枚举类型声明的变量，其变量的值只能取自括号内的这些标识符。

8.6.1　枚举类型的声明

枚举类型定义有以下两种声明形式。

（1）枚举类型的一般形式如下：

例如：

enum是定义枚举类型的关键字，weekday是新定义的类型名，大括号内就是枚举类型变量应取的值。

（2）带赋值的枚举类型声明形式如下：

例如：

使用枚举类型的说明如下：

（1）编译器默认将标识符自动赋上整型常数，例如：

（2）可以自行修改整型常数的值，例如：

（3）如果只给前几个标识符赋整型常数，编译器会给后面标识符自动累加赋值，例如：

相当于：

8.6.2　枚举类型变量

在声明了枚举类型之后，可以用它来定义变量，例如：

myworkday是weekday的变量。在C++语言中，枚举类型名包括关键字enum，在C++中允许不写enum关键字。

关于使用枚举类型变量的说明如下：

（1）枚举变量的值只能是Sunday到Saturday之一，例如：

（2）一个整数不能直接赋给一个枚举变量，例如：

则day=(enum weekday)3; 等价于day=Wednesday;。day=3;是错误的。

整型虽然不能直接为枚举类型变量赋值，但是可以通过强制类型转换，将整数转换为合适的枚举型数值。

【实例8.9】 枚举变量的赋值。 （实例位置：资源包\TM\sl\8\9）

程序运行结果如图8.10所示。

图8.10　枚举变量的赋值

程序中使用了各种形式的赋值，其原理都是一样的，都是通过强制转换为枚举变量赋值。

（3）可以直接定义枚举变量，例如：

8.6.3　枚举类型的运算

枚举值相当于整型变量，可以用枚举值来进行一些运算。

（1）利用枚举值做判断比较。

枚举值可以和整型变量一起比较，枚举值和枚举值之间也可以比较。

【实例8.10】 枚举值的比较运算。 （实例位置：资源包\TM\sl\8\10）

程序运行结果如图8.11所示。

图8.11　枚举值的比较运算

程序进行变量n和枚举变量day1的比较以及枚举变量day1和day2的比较。

（2）利用枚举类型进行减法运算。枚举值直接可以进行减法运算。

【实例8.11】 使用枚举值进行减法运算。 （实例位置：资源包\TM\sl\8\11）

程序运行结果如图8.12所示。

图8.12　使用枚举值进行减法运算

程序直接将两个枚举变量m_eType1和m_eType2进行减法运算，然后将运算结果输出。

8.7　自定义数据类型

C++语言提供了丰富的数据类型，对于一些由用户自己定义的构造型数据类型，还允许用户自己定义类型说明符，即由用户为定义的数据类型名另外再取一个别名，以便简化对类型名的引用，或增加程序的可读性。这个功能由类型定义符typedef完成。typedef的使用形式如下：

原类型名是任意已定义的数据类型，包括系统的各种基本数据类型名以及用户自定义的构造类型名。

新类型名是用户自己命名的标识符，在以后变量的声明中可以直接使用该名称，例如：

等价于：

下面通过实例来介绍一下如何在程序中使用自定义的数据类型。

【实例8.12】 使用自定义数据类型。 （实例位置：资源包\TM\sl\8\12）

程序运行结果如图8.13所示。

图8.13　使用自定义数据类型

程序将字符指针重命名为CString, CString就代表字符指针。

自定义数据类型的大小同原数据类型一样。例如：

程序使用sizeof运算得到数据类型的大小。CString同char *一样，都是占用4个字节空间。

8.8　异常处理

异常处理是程序设计中除调试之外的另一种错误处理方法，它往往被大多数程序设计人员在实际设计中忽略。异常处理引起的代码膨胀将不可避免地增加程序阅读的困难，这对于程序设计人员来说是十分烦恼的。异常处理与真正的错误处理有一定区别，异常处理不但可以对系统错误做出反应，还可以对人为制造的错误做出反应并处理。本章将向读者介绍C++语言对于异常处理的使用方法。

8.8.1　抛出异常

当程序执行到某一函数或方法内部时，程序本身出现了一些异常，但这些异常并不能由系统所捕获，这时就可以创建一个错误信息，再由系统捕获该错误信息并处理。创建错误信息并发送这一过程就是抛出异常。

最初异常信息的抛出只是定义一些常量，这些常量通常是整型值或是字符串信息。下面的代码是通过整型值创建的异常抛出：

在C++中，异常的抛出是使用throw关键字来实现的，在这个关键字的后面可以跟随任何类型的值。在上面的代码中将整型值1作为异常信息抛出，当异常捕获时就可以根据该信息进行异常的处理。

异常的抛出还可以使用字符串作为异常信息进行发送，程序代码如下：

可以看到，字符串形式的异常信息适合于异常信息的显示，但并不适合于异常信息的处理，那么是否可以将整型信息与字符串信息结合起来作为异常信息进行抛出呢？之前说过，throw关键字后面跟随的是类型值，所以不但可以跟随基本数据类型的值，还可以跟随类类型的值，这就可以通过类的构造函数将整型值与字符串结合在一起，并且还可以同时应用更加灵活的功能。

例如，将错误ID和错误信息以类对象的形式进行异常抛出。

【实例8.13】 使用自定义异常类。 （实例位置：资源包\TM\sl\8\13）

程序运行结果如图8.14所示。

图8.14　使用自定义异常类

程序中定义了一个异常类，这个类包含了两个内容，一个是异常ID，也就是异常信息的编号；另一个是异常信息，也就是异常的说明文本。通过throw关键字抛出异常时，需要指定这两个参数。

8.8.2　捕获异常

异常捕获是指当一个异常被抛出时，不一定就在异常抛出的位置来处理这个异常，而是可以在别的地方通过捕获这个异常信息后再进行处理。这样不仅增加了程序结构的灵活性，也提高了异常处理的方便性。

如果在函数内抛出一个异常（或在函数调用时抛出一个异常），将在异常抛出时退出函数。如果不想在异常抛出时退出函数，可在函数内创建一个特殊块用于解决实际程序中的问题。这个特殊块由try关键字组成，例如：

异常抛出信号发出后，一旦被异常处理器接收到就被销毁。异常处理器应具备接收任何异常的能力。异常处理器紧随try块之后，处理的方法由关键字catch引导。

异常处理部分必须直接放在测试块之后。如果一个异常信号被抛出，异常处理器中第一个参数与异常抛出对象相匹配的函数将捕获该异常信号，然后进入相应的catch语句，执行异常处理程序。catch语句与switch语句不同，它不需要在每个catch语句后加入break去中断后面程序的执行。

下面通过try…catch语句来捕获一个异常，程序代码如下：

在上面的代码中可以看到try语句块中用于捕获throw所抛出的异常。对于throw异常的抛出，可以直接写在try语句块的内部，也可以写在函数或类方法的内部，但函数或方法必须写在try语句块的内部才可以捕获到异常。程序运行结果如图8.15所示。

图8.15　捕获一个异常

异常处理器可以成组地出现，同时根据try语句块获取的异常信息处理不同的异常。

【实例8.14】 获取不同异常的try…catch。 （实例位置：资源包\TM\sl\8\14）

程序运行结果如图8.16和图8.17所示。

图8.16　捕获异常1

图8.17　捕获异常2

有时并不一定在列出的异常处理中包含所有可能发生的异常类型，所以C++提供了可以处理任何类型异常的方法，就是在catch后面的括号内添加“…”。程序代码如下：

有时需要重新抛出刚接收到的异常，尤其是在程序无法得到有关异常的信息，而用省略号捕获任意的异常时。这些工作通过加入不带参数的throw就可完成：

如果一个catch语句忽略了一个异常，那么这个异常将进入更高层的异常处理环境。由于每个异常抛出的对象是被保留的，因此更高层的异常处理器可抛出来自这个对象的所有信息。

8.8.3　异常匹配

当程序中有异常抛出时，异常处理系统会根据异常处理器的顺序找到最近的异常处理块，并不会搜索更多的异常处理块。

异常匹配并不要求异常与异常处理器进行完美匹配，一个对象或一个派生类对象的引用将与基类处理器进行匹配。若抛出的是类对象的指针，则指针会匹配相应的对象类型，但不会自动转换成其他对象的类型，示例如下：

从上面的代码可以认为，第一个异常处理器会使用构造函数进行转换，将CExcept1转换为CExcept2对象，但实际上系统在异常处理期间并不会执行这样的转换，而是在CExcept1处终止。

通过下面的实例演示基类处理器如何捕获派生类的异常。

【实例8.15】 捕获派生类异常。 （实例位置：资源包\TM\sl\8\15）

程序运行结果如图8.18所示。

图8.18　捕获派生类异常

从上面的结果可以看出，虽然抛出的异常是CDerive类，但由于异常处理器的第一个是CExcept类，该类是CDerive类的基类，因此将进入此异常处理器内部。为了正确地进入指定的异常处理器，在对异常处理器进行排列时应将派生类排在前面，而将基类排在后面。

8.8.4　标准异常

用于C++标准库的一些异常可以直接应用到程序中，应用标准异常类会比应用自定义异常类容易得多。如果系统提供的标准异常类不能满足需要，就不可以在这些标准异常类基础上进行派生。下面给出了C++提供的一些标准异常：

注意观察上述类的层次结构可以看出，标准异常都派生自一个公共的基类exception。基类包含必要的多态性函数提供异常描述，可以被重载。下面是exception类的原型：

8.9　使用宏定义替换复杂的数据

在前面的学习中，经常遇到用#define命令定义符号常量的情况，其实使用#define命令就是要定义一个可替换的宏，宏定义是预处理命令的一种。它提供了一种可以替换源代码中字符串的机制。根据宏定义中是否有参数，可以将其分为不带参数的宏定义和带参数的宏定义两种，下面分别进行介绍。

1．不带参数的宏定义

宏定义指令#define用来定义一个标识符和一个字符串，以这个标识符来代表这个字符串，在程序中每次遇到该标识符时，就用所定义的字符串替换它。它的作用相当于给指定的字符串起一个别名。

不带参数的宏定义一般形式如下：

“#”表示这是一条预处理命令。

宏名是一个标识符，必须符合C++语言标识符的规定。

字符串可以是常数、表达式、格式字符串等。

例如：

它的作用是在程序中用PI替代3.14159，在编译预处理时，每当在源程序中遇到PI就自动用3.14159代替。

使用#define进行宏定义的好处是，需要改变一个常量的时候只需改变#define命令行，整个程序的常量都会改变，大大提高了程序的灵活性。

宏名要简单且意义明确，一般习惯用大写字母表示，以便与变量名相区别。

注意

宏定义不是C++语句，不需要在行末加分号。

宏名定义后，即可成为其他宏名定义中的一部分。例如，下面的程序代码定义了正方形的边长SIDE、周长PERIMETER及面积AREA的值。

前面强调过宏替换是以字符串代替标识符，因此，如果希望定义一个标准的邀请语，可编写如下程序代码：

编译程序遇到标识符STANDARD时，就用You are welcome to join us.替换。

对于以上的编译程序，与printf语句的如下形式是等效的：

关于不带参数的宏定义要强调以下几点：

（1）如果在字符串中含有宏名，则不进行替换，例如：

程序运行结果如图8.19所示。

图8.19　在字符串中含有宏名

注意

上面的程序字符串中的TEST并没有用this is an example来替换，所以说如果字符串中含有宏名，则不进行替换。

（2）如果字符串长于一行，可以在该行末尾用一反斜杠“\”续行。

（3）#define命令出现在程序中函数的外面，宏名的有效范围为定义命令之后到此源文件结束。

注意

在编写程序时通常将所有的#define放到文件的开始处或独立的文件中，而不是将它们分散到整个程序中。

（4）可以用#undef命令终止宏定义的作用域。

（5）宏定义用于预处理命令，它不同于定义的变量，只进行字符替换，不分配内存空间。

2．带参数的宏定义

带参数的宏定义不是简单的字符串替换，它还要进行参数替换，一般形式如下：

【实例8.16】 使用带参数的宏实现求两个数的乘积 。（实例位置：资源包\TM\sl\8\16）

程序运行结果如图8.20所示。

图8.20　使用带参数的宏实现乘法运算

当编译该程序时，由MUL(x,y)定义的表达式被替换，a和b用作操作数，即c=MUL(a,b)；语句被代替后变为如下形式：

用宏替换代替实在的函数的一个好处是，宏替换增加了代码的速度，因为不存在函数调用，但增加速度也有代价，即由于重复编码而增加了程序长度。

对于带参数的宏定义要强调以下几点：

（1）宏定义时参数要加括号。如不加括号，有时结果是正确的，有时结果便是错误的，那么什么时候是正确的，什么时候是错误的，下面具体说明。

如实例8.16中，当参数x=8，y=9时，在参数不加括号的情况下调用MUL(x,y)，可以正确地输出结果；当x=8，y=5+4时，若参数不加括号，即定义成#define MUL(x,y) x*y，这种情况下调用MUL(x,y)，则输出的结果是错误的，因为此时调用的MUL(x,y)执行情况如下：

此时计算出的结果是44，而实际上希望得出的结果是72，为了避免出现上面这种情况，在进行宏定义时要在参数外面加上括号。

（2）宏扩展必须使用括号来保护表达式中低优先级的操作符，以确保调用时达到想要的效果，例如，有如下宏定义：

如下调用该宏定义时：

则会被扩展为：

而本意是希望得到：

解决的办法就是宏扩展时加上括号，此时就能避免这种错误发生。

（3）对带参数的宏的展开，只是将语句中的宏名后面括号内的实参字符串代替#define命令行中的形参。

（4）在宏定义时，宏名与带参数的括号之间不可以加空格，否则将空格以后的字符都作为替代字符串的一部分。

（5）在带参宏定义中，形式参数不分配内存单元，因此不必做类型定义。

8.10　小结

本章主要介绍了两种构造类型：结构体和共用体，自定义类型及枚举类型。使用C++语言开发的程序一般都大量使用结构体，在C++中更是增加了结构体的功能，程序设计阶段应多将关联紧密的数据组合成一个结构体，便于阅读及二次开发。

8.11　实践与练习

（1）输入学生期中成绩、期末成绩、平时考核成绩，按30%、50%、20%计算学生的综合成绩。 （实例位置：资源包\TM\sl\8\17）

（2）设计一个选票程序。假设有3个候选人，首先输入要选择的候选人姓名，然后输出每个人的得票结果。 （实例位置：资源包\TM\sl\8\18）

第2篇

核心技术

第9章　面向对象编程

第10章　类和对象

第11章　继承与派生

本篇通过面向对象编程、类和对象、继承与派生等内容结合大量的实例讲解，能够让您学会C++的面向对象的编程思想，为以后的C++编程培养面向对象的思维。

第9章

面向对象编程

（视频讲解： 32分钟）

面向对象编程可以有效解决代码复用问题，它不同于以往的面向过程编程，面向过程编程需要将功能细分，而面向对象需要将不同功能抽象到一起。本章通过具体UML（统一建模语言）建模来演示面向对象编程思想，通过对一个程序的前期分析了解如何使用面向对象编程。

通过阅读本章，您可以：

了解面向对象模式

了解面向对象编程与面向过程编程的区别

了解面向对象编程开发过程

掌握简单建模

9.1　面向对象概述

面向对象（Object Oriented, OO），它是一种设计思想，现在这种思想已经不单应用在软件设计上，数据库设计、计算机辅助设计（CAD）、网络结构设计、人工智能算法设计等领域都开始应用这种思想。

面向对象中的对象（Object），指的是客观世界中存在的对象，这个对象具有唯一性，对象之间各不相同，各有各的特点，每一个对象都有自动的运动规律和内部状态。对象与对象之间又是可以相互联系、相互作用的。概括地讲，面向对象技术是一种从组织结构上模拟客观世界的方法。

针对面向对象思想应用的不同领域，面向对象又可以分为面向对象分析（Object Oriented Analysis, OOA）、面向对象设计（Object Oriented Design, OOD）、面向对象编程（Object Oriented Programming, OOP）、面向对象测试（Object Oriented Test, OOT）和面向对象维护（Object Oriented Soft Maintenance, OOSM）。

客观世界中任何一个事物都可以看成一个对象，每个对象有属性和行为两个要素。属性就是对象的内部状态及自身的特点，行为就是改变自身状态的动作。

面向对象中的对象也可以是一个抽象的事物，可以从类似的事物中抽象出一个对象，如圆形、正方形、三角形，可以抽象得出的对象是简单图形，简单图形就是一个对象，它有自己的属性和行为，图形中边的个数是它的属性，图形的面积也是它的属性，输出图形的面积就是它的行为。

面向对象有3大特点，即封装、继承和多态。

（1）封装

封装有两个作用，一个是将不同的小对象封装成一个大对象，另一个是把一部分内部属性和功能对外界屏蔽。例如，一辆汽车是一个大对象，它由发动机、底盘、车身和轮子等这些小对象组成。在设计时可以先对这些小对象进行设计，然后小对象之间通过相互联系确定各自大小等方面的属性，最后就可以安装成一辆汽车。

（2）继承

继承是和类密切相关的概念。继承性是子类自动共享父类数据结构和方法的机制，这是类之间的一种关系。在定义和实现一个类的时候，可以在一个已经存在的类的基础之上进行，把这个已经存在的类所定义的内容作为自己的内容，并加入若干新的内容。

在类层次中，子类只继承一个父类的数据结构和方法，称为单重继承，子类继承了多个父类的数据结构和方法，则称为多重继承。

在软件开发中，类的继承性使所建立的软件具有开放性、可扩充性，这是信息组织与分类的行之有效的方法，它简化了对象、类的创建工作量，增加了代码的可重用性。

继承性是面向对象程序设计语言不同于其他语言的最重要的特点，是其他语言所没有的。采用继承性，使公共的特性能够共享，提高了软件的重用性。

（3）多态

多态性是指相同的行为可作用于多种类型的对象上并获得不同的结果。不同的对象，收到同一消息可以产生不同的结果，这种现象称为多态性。多态性允许每个对象以适合自身的方式去响应共同的消息。

9.2　面向对象与面向过程编程

9.2.1　面向过程编程

过程编程的主要思想是先做什么后做什么，在一个过程中实现特定功能。一个大的实现过程还可以分成各个模块，各个模块可以按功能进行划分，然后组合在一起实现特定功能。在过程编程中，程序模块可以是一个函数，也可以是整个源文件。

过程编程主要以数据为中心，传统的面向过程的功能分解法属于结构化分析方法。分析者将对象系统的现实世界看作一个大的处理系统，然后将其分解为若干个子处理过程，解决系统的总体控制问题。在分析过程中，用数据描述各子处理过程之间的联系，整理各个子处理过程的执行顺序。

面向过程编程一般流程如下：

现实世界→面向过程建模（流程图、变量、函数）→面向过程语言→执行求解。

过程编程的稳定性、可修改性和可重用性都比较差。

（1）软件重用性差

重用性是指同一事物不经修改或稍加修改就可多次重复使用的性质。软件重用性是软件工程追求的目标之一。处理不同的过程都有不同的结构，当过程改变时，结构也需要改变，前期开发的代码无法得到充分的再利用。

（2）软件可维护性差

软件工程强调软件的可维护性，强调文档资料的重要性，规定最终的软件产品应该由完整、一致的配置成分组成。在软件开发过程中，始终强调软件的可读性、可修改性和可测试性是软件的重要的质量指标。面向过程编程由于软件的重用性差，造成维护时其费用和成本也很高，而且大量修改的代码存在着许多未知的漏洞。

（3）开发出的软件不能满足用户需要

大型软件系统一般涉及各种不同领域的知识，面向过程编程往往描述软件的各个最低层的，针对不同领域设计不同的结构及处理机制，当用户需求发生变化时，就要修改最低层的结构。当处理用户需求变化较大时，过程编程将无法修改，可能导致软件的重新开发。

9.2.2　面向对象编程

面向过程编程有费解的数据结构、复杂的组合逻辑、详细的过程和数据之间的关系、高深的算法，面向过程开发的程序可以描述成算法加数据结构。面向过程开发是分析过程与数据之间的边界在哪里，进而解决问题。面向对象则是从另一种角度思考，将编程思维设计成符合人的思维逻辑。

面向对象程序设计者的任务包括两个方面：一是设计所需的各种类和对象，即决定把哪些数据和操作封装在一起；二是考虑怎样向有关对象发送消息，以完成所需的任务。这时它如同一个总调度，不断地向各个对象发出消息，让这些对象活动起来（或者说激活这些对象），完成自己职责范围内的工作。

各个对象的操作完成了，整体任务也就完成了。显然，对一个大型任务来说，面向对象程序设计方法是十分有效的，它能大大降低程序设计人员的工作难度，减少出错机会。

面向对象开发的程序可以描述成“对象+消息”。面向对象编程一般流程如下：

现实世界→面向对象建模（类图、对象、方法）→面向对象语言→执行求解。

9.2.3　面向对象的特点

面向对象技术充分体现了分解、抽象、模块化、信息隐藏等思想，可有效提高软件生产率，缩短软件开发时间，提高软件质量，是控制复杂度的有效途径。

面向对象不仅适合普通人员，也适合经理人员。降低维护开销的技术可以释放管理者的资源，将其投入到待处理的应用中。在经理们看来，面向对象不是纯技术的，它既能给企业的组织也能给经理的工作带来变化。

当一个企业采纳了面向对象，其组织将发生变化。类的重用需要类库和类库管理人员，每个程序员都要加入到两个组中的一个：一个是设计和编写新类组，另一个是应用类创建新应用程序组。面向对象不太强调编程，需求分析相对地将变得更加重要。

面向对象编程主要有代码容易修改、代码复用性高、满足用户需求3个特点。

（1）代码容易修改

面向对象编程的代码都是封装在类里面，如果类的某个属性发生变化，只需要修改类中成员函数的实现即可，其他的程序函数不发生改变。如果类中属性变化较大，则使用继承的方法重新派生新类。

（2）代码复用性高

面向对象编程的类都是具有特定功能的封装，需要使用类中特定的功能，只需要声明该类并调用其成员函数即可。如果需要的功能在不同类，还可以进行多重继承，将不同类的成员封装到一个类中。功能的实现可以像积木一样随意组合，大大提高了代码的复用性。

（3）满足用户需求

由于面向对象编程的代码复用性高，用户的要求发生变化时，只需要修改发生变化的类。如果用户的要求变化较大时，就对类进行重新组装，将变化大的类重新开发，功能没有发生变化的类可以直接拿来使用。面向对象编程可以及时地响应用户需求的变化。

9.3　统一建模语言

9.3.1　统一建模语言概述

模型是用某种工具对同类或其他工具的表达方式，是系统语义的完整抽象。模型可以分解为包的层次结构，最外层的包对应于整个系统。模型的内容是从顶层包到模型元素的包所含关系的闭包。

模型可以用于捕获精确地表达项目的需求和应用领域中的知识，以使各方面的利益相关者能够相互理解并达成一致。

UML（统一建模语言）是一种直观化、明确化、构建和文档化软件系统产物的通用可视化建模语言。UML记录了被构建系统的有关决定和理解，可用于对系统的理解、设计、浏览、配置以及信息控制。UML的应用贯穿在系统开发的需求分析、分析、设计、构造、测试5个阶段，它包括概念的语义、表示法和说明，提供静态、动态、系统环境及组织结构的建模。建模语言是一种图形化的文档描述性语言，解决的核心问题是沟通障碍的问题，但UML是总结了以往建模技术的经验并吸收当今优秀成果的标准建模方法。

9.3.2　统一建模语言的结构

UML由图和元模型共同组成，其中图是UML的语法，而元模型则是给出的图的意思，它是UML的语义。UML的语义定义在一个4层抽象级建模概念框架中，这4层结构如下。

元介质模型层

该层描述基本的类型、属性、关系，这些元素都用于定义UML元模型。元介质模型强调用少数功能较强的模型成分来组合表达复杂的语义。每一个方法和技术都应在相对独立的抽象层次上。

元模型层

该层组成了UML的基本元素，包括面向对象和面向组件的概念，这一层的每个概念都在元介质模型的“事物”的实例中。

模型层

该层组成了UML的模型，这一层中的每个概念都是在元模型层中概念的一个实例，这一层的模型通常叫作类模型或类型模型。

用户模型层

该层中的所有元素都是UML模型的例子。这一层中的每个概念都是模型层的一个实例，也是元模型层的一个实例。这一层的模型通常叫作对象模型或实例模型。

UML使用模型来描述系统的结构或静态特征，以及行为或动态特征，它通过不同的视图来体现行为或动态特征。常用的视图有以下几种：

（1）用例视图

该视图强调以用户的角度所看到的或需要的系统功能为出发点建模。这种视图有时也被称为用户模型视图。

（2）逻辑视图

该视图用于展现系统的静态和结构组成及其特征，它也被称为结构模型视图或静态视图。

（3）并发视图

该视图体现了系统的动态或者行为特征，它也被称为行为模型视图、过程视图、写作视图或者动态视图。

（4）组件视图

该视图体现了系统实现的结构和行为特征，它有时也被称为模型实现视图。

（5）开发视图

该视图体现了系统实现环境的结构和行为特征，它也被称为物理视图。

UML的视图都是由一个或多个图共同组成。一个图体现一个系统架构的某个功能，所有的图一起组成了系统的完整视图。UML提供了9种不同的图，分别是用例图、类图、对象图、组件图、配置图、序列图、写作图、状态图和活动图。

活动图如图9.1所示。一名图书借阅者使用图书管理系统要先进行查找图书动作，然后确定想要的书，接着是取走图书，最后查询和修改图书在系统中的状态。

UML除提供了9种视图以外，还提供了包图和交互图，包图如图9.2所示。

图9.1　活动图

图9.2　包图

图书管理系统中的查询模块，包括了查询抽象类包、通过书名查询包、通过作者查询包。通过书名查询包和通过作者查询包都派生于查询抽象类包，并且都调用其他子系统下的数据库包。

包图描述了类的结构，交互图则描述了类对象的交互步骤，交互图如图9.3所示。

图9.3　交互图

交互图中演示的是建立连接动作对象和连接对象的交互过程，首先发送“创建连接对象”消息，当连接对象创建完后，返回“连接建立消息”给建立连接动作对象。

9.3.3　面向对象的建模

面向对象的建模是一种新的思维方式，是一种关于计算机和信息结构化的新思维。面向对象的建模，把系统看作相互协作的对象，这些对象是结构和行为的封装，都属于某个类，那些类具有某种层次化的结构。系统的所有功能通过对象之间相互发送消息来获得。面向对象的建模可以视为一个包含以下元素的概念框架：抽象、封装、模块化、层次、分类、并行、稳定、可重用性和可扩展性。

9.4　小结

了解面向对象编程与面向过程编程的区别可以更好地理解面向对象编程，面向对象编程需要通过好的模型才能发挥其优点，而好的模型需要大量的代码积累和反复的测试才能形成。读者可以通过本章了解面向对象编程的思路以及使用UML来描述编程思路，掌握面向对象编程的方法。

第10章

类和对象

（视频讲解： 1小时1分钟）

C++既可以开发面向过程的应用程序，也可以开发面向对象的应用程序。类是对象的实现，面向对象中的类是抽象概念，而类是程序开始过程中定义一个对象，用类定义对象可以是现实生活中的真实对象，也可以是从现实生活中抽象的对象。

通过阅读本章，您可以：

了解对象的声明

了解构造函数和析构函数

了解类成员的使用

掌握友元

掌握命名空间

10.1　C++类

10.1.1　类概述

面向对象中的对象需要通过定义类来声明，对象一词是一种形象的说法，在编写代码过程中则是通过定义一个类来实现。

C++类不同于汉语中的类、分类、类型，它是一个特殊的概念，可以是对统一类型事物进行抽象处理，也可以是一个层次结构中的不同层次节点。例如，将客观世界看成一个Object类，动物是客观世界中的一小部分，定义为Animal类，狗是一种哺乳动物，是动物的一类，定义为Dog类，鱼也是一种动物，定义为Fish类，类的层次关系如图10.1所示。

图10.1　类的层次关系

类是一个新的数据类型，它和结构体有些相似，是由不同数据类型组成的集合体，但类要比结构体增加了操作数据的行为，这个行为就是函数。

10.1.2　类的声明与定义

在10.1.1节中已经对类的概念进行说明，可以看出类是用户自己指定的类型。如果程序中要用到类这种类型，就必须自己根据需要进行声明，或者使用别人设计好的类。下面来看一下如何设计一个类。

类的声明格式如下：

类的声明格式的说明如下：

class是定义类结构体的关键字，大括号内被称为类体或类空间。

类名标识符指定的就是类名，类名就是一个新的数据类型，通过类名可以声明对象。

类的成员有数据和函数两种类型。

大括号内是定义和声明类成员的地方，关键字public、private、protected是类成员访问的修饰符。

类中的数据成员的类型可以是任意的，包含整型、浮点型、字符型、数组、指针和引用等，也可以是对象。另一个类的对象可以作为该类的成员，但是自身类的对象不可以作为该类的成员，而自身类的指针或引用又是可以作为该类的成员的。

注意

定义类结构体和定义结构体时大括号后要有分号。

例如，给出一个员工信息类声明：

在上面的代码中，class关键字是用来定义类这种类型的，CPerson是定义的员工信息类名称，在大括号中包含了4个数据成员分别表示CPerson类的属性，包含了8个成员函数表示CPerson类的行为。

10.1.3　类的实现

10.1.2节只是在CPerson类中声明了类的成员，然而要使用这个类中的方法，即成员函数，还要对其进行定义具体的操作。下面来看一下是如何定义类中的方法的。

第一种方法是将类的成员函数都定义在类体内。

以下代码都在Person.h头文件内，类的成员函数都定义在类体内。

第二种方法，也可以将类体内的成员函数的实现放在类体外，但如果类成员定义在类体外，需要用到域运算符“::”，放在类体内和类体外的效果是一样的。

前面两种方式都是将代码存储在同一个文件内。C++语言可以实现将函数的声明和函数的定义放在不同的文件内，一般在头文件放入函数的声明，在实现文件放入函数的实现文件。同样可以将类的定义放在头文件中，将类成员函数的实现放在实现文件内。存放类的头文件和实现文件最好和类名相同或相似，例如，将CPerson类的声明部分放在Person.h文件内，程序代码如下：

将CPerson类的实现部分放在Person.cpp文件内，程序代码如下：

此时整个工程所有文件如图10.2所示。

图10.2　工程所有文件

关于类的实现有以下两点说明：

（1）类的数据成员需要初始化，成员函数还要添加实现代码。类的数据成员不可以在类的声明中初始化。

上面的代码是不能通过编译的。

（2）空类是C++中最简单的类，其声明方式如下：

空类只是起到占位的作用，需要的时候再定义类成员及实现。

10.1.4　对象的声明

定义一个新类后，就可以通过类名来声明一个对象，声明的形式如下：

类名是定义好的新类的标识符，对象名表中是一个或多个对象的名称，如果声明的是多个对象就用逗号运算符分隔。

例如，声明一个对象如下：

声明多个对象如下：

声明完对象，就是对象的引用了，对象的引用有两种方式，一种是成员引用方式，一种是对象指针方式。

（1）成员引用方式

成员变量引用的表示如下：

这里“．”是一个运算符，该运算符的功能是表示对象的成员。

成员函数引用的表示如下：

例如：

（2）对象指针方式

对象声明形式中的对象名表，除了是用逗号运算符分隔的多个对象名外，还可以是对象名数组、对象名指针和引用形式的对象名。

声明一个对象指针：

但要想使用对象的成员，需要“->”运算符，它是表示成员的运算符，与“．”运算符的意义相同。“->”用来表示对象指针所指的成员，对象指针就是指向对象的指针，例如：

下面的对象数据成员的两种表示形式是等价的：

与

同样，下面的成员函数的两种表示形式是等价的：

与

例如：

【实例10.1】 对象的引用。 （实例位置：资源包\TM\sl\10\1）

在本实例中，利用前文声明的类定义对象，然后使用该对象引用其中成员。

在实例中可以看到，首先使用CPerson类定义对象p，然后使用p引用类中的成员函数。

p.setAge(25)引用类中的setAge成员函数，将参数中的数据赋值给数据成员，设置对象的属性。函数的返回值赋给iResult变量，通过iResult变量值判断函数setAge为数据成员赋值是否成功。如果成功再使用p.getAge（）得到赋值的数据，然后将其输出显示。

之后使用对象p依次引用成员函数setIndex、setName和setSalary，然后通过对iResult变量的判断，决定是否引用成员函数getIndex、getName和getSalary。

10.2　构造函数

10.2.1　构造函数概述

在类的实例进入其作用域时，也就是建立一个对象，构造函数就会被调用，那么构造函数的作用是什么呢？当建立一个对象时，常常需要做某些初始化的工作，例如，对数据成员进行赋值设置类的属性，而这些操作刚好放在构造函数中完成。

前文介绍过结构的相关知识，在对结构进行初始化时，可以使用下面的方法，例如：

但是类不能像结构体一样初始化，其构造方法如下：

CPerson（）是默认构造函数，如果不显式地写上函数的声明也可以。

构造函数是可以有参数的，修改上面的代码，使其构造函数带有参数，例如：

【实例10.2】 使用构造函数进行初始化操作。 （实例位置：资源包\TM\sl\10\2）

程序运行结果如图10.3所示。

图10.3　程序运行结果

程序声明了两个对象p1和p2，p1使用默认构造函数初始化成员变量，p2使用带参数的构造函数初始化，所以在调用同一个类成员函数getIndex时输出结果不同。

10.2.2　复制构造函数

在开发程序时可能需要保存对象的副本，以便在后面执行的过程中恢复对象的状态，那么如何用一个已经初始化的对象来新生成一个一模一样的对象？答案是使用复制构造函数来实现。复制构造函数就是函数的参数是一个已经初始化的类对象。

【实例10.3】 使用复制构造函数。 （实例位置：资源包\TM\sl\10\3）

在头文件Person.h中声明和定义类，程序代码如下：

在主程序文件中实现类对象的调用，程序代码如下：

程序运行结果如图10.4所示。

图10.4　使用复制构造函数

程序中先用带参数的构造函数声明对象p1，然后通过复制构造函数声明对象p2，因为p1已经是初始化完成的类对象，可以作为复制构造函数的参数，通过输出结果可以看出，两个对象是相同的。

10.3　析构函数

构造函数和析构函数是类体定义中比较特殊的两个成员函数，因为它们两个都没有返回值，而且构造函数名标识符和类名标识符相同，析构函数名标识符就是在类名标识符前面加“～”符号。

构造函数主要是用来在对象创建时，给对象中的一些数据成员赋值，主要目的就是来初始化对象。析构函数的功能是用来释放一个对象的，在对象删除前，用它来做一些清理工作，它与构造函数的功能正好相反。

【实例10.4】 使用析构函数。 （实例位置：资源包\TM\sl\10\4）

在头文件Person.h中声明和定义类，程序代码如下：

在主程序文件中实现类对象的调用，程序代码如下：

程序运行结果如图10.5所示。

图10.5　使用析构函数

程序在构造函数中使用new为成员m_pMessage分配空间，在析构函数中使用delete释放由new分配的空间。成员m_pMessage为字符指针，在ShowStartMessage成员函数中输出字符指针所指向的内容。

使用析构函数的注意事项如下：

一个类中只可能定义一个析构函数。

析构函数不能重载。

构造函数和析构函数不能使用return语句返回值，不用加上关键字void。

构造函数和析构函数的调用环境：

（1）自动变量的作用域是某个模块，当此模块被激活时，自动变量调用构造函数，当退出此模块时，会调用析构函数。

（2）全局变量在进入main函数之前会调用构造函数，在程序终止时会调用析构函数。

（3）动态分配的对象在使用new为对象分配内存时会调用构造函数；使用delete删除对象时会调用析构函数。

（4）临时变量是为支持计算，由编译器自动产生的。临时变量的生存期的开始和结尾会调用构造函数和析构函数。

10.4　类成员

10.4.1　访问类成员

类的三大特点之一就是具有封装性，封装在类里面的数据可以设置成对外可见或不可见，通过关键字public、private、protected可以设置类中数据成员对外是否可见，也就是其他类是否可以访问该数据成员。

关键字public、private、protected说明类成员是公有的、私有的，还是保护的。这3个关键字将类划分为3个区域，在public区域的类成员可以在类作用域外被访问，而private区域和protected区域只能在类作用域内被访问，如图10.6所示。

图10.6　类成员属性

这3种类成员的属性如下：

public属性的成员对外可见，对内可见。

private属性的成员对外不可见，对内可见。

protected属性的成员对外不可见，对内可见，且对派生类是可见的。

如果在类定义的时候没有加任何关键字，默认状态类成员都在private区域。

例如，在头文件Person.h中：

实现文件Person.cpp中：

在编译上面的代码时，会发现编译不能通过，这是什么原因呢？

因为在默认状态下，类成员的属性为private，这样的话类成员只能被类中的其他成员访问，而不能被外部访问。例如，CPerson类中的m_iIndex数据成员，只能在类体的作用域内被访问和赋值，数据类型为CPerson类的对象p，就无法对m_iIndex数据成员进行赋值。

有了不同区域，开发人员可以根据需求来进行封装，将不想让其他类访问和调用的类成员定义在private区域和protected区域，这就保证了类成员的隐蔽性。需要注意的是，如果将成员的属性设置为protected，那么继承类也可以访问父类的保护成员，但是不能访问类中的私有成员。

关键字的作用范围是，直到下一次出现另一个关键字为止，例如：

在上面的代码中，private访问权限控制符设置m_iIndex成员变量为私有。public关键字下面的成员函数设置为公有，从中可以看出private的作用域到public出现时为止。

10.4.2　内联成员函数

在定义函数时，可以使用inline关键字将函数定义为内联函数。在定义类的成员函数时，也可以使用inline关键字将成员函数定义为内联成员函数。其实，对于成员函数来说，如果其定义是在类体中，即使没有使用inline关键字，该成员函数也被认为是内联成员函数，例如：

程序中，使用inline关键字将类中的成员函数设置为内联成员函数。此外，也可以在类成员函数的实现部分使用inline关键字标识函数为内联成员函数，例如：

程序中的代码演示了在何处使用关键字inline。对于内联函数来说，程序会在函数调用的地方直接插入函数代码，如果函数体语句较多，则会导致程序代码膨胀。如果将类的析构函数定义为内联函数，可能会导致潜在的代码膨胀。

10.4.3　静态类成员

本节之前所定义的类成员，都是通过对象来访问的，不能通过类名直接访问。如果将类成员定义为静态类成员，则允许使用类名直接访问。静态类成员是在类成员定义前使用static关键字标识，例如：

在定义静态数据成员时，通常需要在类体外部对静态数据成员进行初始化，例如：

对于静态成员来说，不仅可以通过对象访问，还可以直接使用类名访问，例如：

在一个类中，静态数据成员是被所有的类对象所共享的，这就意味着无论定义多少个类对象，类的静态数据成员只有一份，同时，如果某一个对象修改了静态数据成员，其他对象的静态数据成员（实际上是同一个静态数据成员）也将改变。

对于静态数据成员，还需要注意以下几点：

（1）静态数据成员可以是当前类的类型，而其他数据成员只能是当前类的指针或应用类型。

在定义类成员时，对于静态数据成员，其类型可以是当前类的类型，而非静态数据成员则不可以，除非数据成员的类型为当前类的指针或引用类型，例如：

（2）静态数据成员可以作为成员函数的默认参数。

在定义类的成员函数时，可以为成员函数指定默认参数，其参数的默认值也可以是类的静态数据成员，但是不同的数据成员则不能作为成员函数的默认参数。例如：

在介绍完类的静态数据成员之后，下面介绍类的静态成员函数。定义类的静态成员函数与定义普通的成员函数类似，只是在成员函数前添加static关键字，例如：

类的静态成员函数只能访问类的静态数据成员，而不能访问普通的数据成员。例如：

在上述代码中，语句cout < < m_Pages < < endl; 是错误的，因为m_Pages是非静态数据成员，不能在静态成员函数中访问。

此外，对于静态成员函数不能定义为const成员函数，即静态成员函数末尾不能使用const关键字，例如，下面的静态成员函数的定义是非法的。

在定义静态数据成员函数时，如果函数的实现代码处于类体之外，则在函数的实现部分不能再标识static关键字，例如，下面的函数定义是非法的。

上述代码如果去掉static关键字则是正确的，例如：

10.4.4　隐藏的this指针

对于类的非静态成员，每一个对象都有自己的一份拷贝，即每个对象都有自己的数据成员，不过成员函数却是每个对象共享的，那么调用共享的成员函数是如何找到自己的数据成员的呢？答案就是通过类中隐藏的this指针。下面通过对例子的讲解来说明this指针的作用。

例如，每一个对象都有自己的一份拷贝。

程序运行结果如图10.7所示。

图10.7　访问对象的数据成员

从图10.7中可以发现，vbBook和vcBook两个对象均有自己的数据成员m_Pages，在调用OutputPages成员函数时输出的均是自己的数据成员。在OutputPages成员函数中只是访问了m_Pages数据成员，每个对象在调用OutputPages方法时是如何区分自己的数据成员呢？答案是通过this指针。在每个类的成员函数（非静态成员函数）中都隐含包含一个this指针，指向被调用对象的指针，其类型为当前类类型的指针类型，在const方法中，为当前类类型的const指针类型。当vbBook对象调用OutputPages成员函数时，this指针指向vbBook对象；当vcBook对象调用OutputPages成员函数时，this指针指向vcBook对象。在OutputPages成员函数中，用户可以显式地使用this指针访问数据成员，例如：

实际上，编译器为了实现this指针，在成员函数中自动添加了this指针对数据成员的方法，类似于上面的OutputPages方法。此外，为了将this指针指向当前调用对象，并在成员函数中能够使用，在每个成员函数中都隐含包含一个this指针作为函数参数，并在函数调用时将对象自身的地址隐含作为实际参数传递。例如，以OutputPages成员函数为例，编译器将其定义为：

在对象调用成员函数时，传递对象的地址到成员函数中。以vc.OutputPages（）；语句为例，编译器将其解释为vbBook.OutputPages（&vbBook）；，这就使得this指针合法，并能够在成员函数中使用。

10.4.5　嵌套类

C++语言允许在一个类中定义另一个类，这被称为嵌套类。例如，下面的代码在定义CList类时，在内部又定义了一个嵌套类CNode。

上述代码在嵌套类CNode中定义了一个私有成员m_Tag，定义了一个公有成员m_Name，对于外围类CList来说，通常它不能够访问嵌套类的私有成员，虽然嵌套类是在其内部定义的。但是，上述代码在定义CNode类时将CList类作为自己的友元类，这使得CList类能够访问CNode类的私有成员。

对于内部的嵌套类来说，只允许其在外围的类域中使用，在其他类域或者作用域中是不可见的，例如，下面的定义是非法的。

上述代码在main函数的作用域中定义了一个CNode对象，导致CNode没有被声明的错误。对于main函数来说，嵌套类CNode是不可见的，但是可以通过使用外围的类域作为限定符来定义CNode对象。如下的定义将是合法的。

上述代码通过使用外围类域作为限定符访问到了CNode类。但是这样做通常是不合理的，也是有限制条件的。因为既然定义了嵌套类，通常都不允许在外界访问，这违背了使用嵌套类的原则。其次，在定义嵌套类时，如果将其定义为私有的或受保护的，即使使用外围类域作为限定符，外界也无法访问嵌套类。

10.4.6　局部类

类的定义可以放在头文件中，也可以放在源文件中。还有一种情况，类的定义也可以放置在函数中，这样的类被称为局部类。

例如，定义一个局部类CBook。

上述代码在LocalClass函数中定义了一个类CBook，该类被称为局部类。对于局部类CBook，在函数之外是不能够被访问的，因为局部类被封装在了函数的局部作用域中。

10.5　友元

10.5.1　友元概述

在讲述类的内容时说明了隐藏数据成员的好处，但是有些时候，类会允许一些特殊的函数直接读写其私有数据成员。

使用friend关键字可以让特定的函数或者别的类的所有成员函数对私有数据成员进行读写，这既可以保持数据的私有性，又能够使特定的类或函数直接访问私有数据。

有时候，普通函数需要直接访问一个类的保护或私有数据成员。如果没有友元机制，则只能将类的数据成员声明为公共的，从而任何函数都可以无约束地访问它。

普通函数需要直接访问类的保护或私有数据成员的原因主要是为提高效率。

例如，没使用友元函数的情况如下：

在代码中可以看到，ComputerRectArea函数定义时只能对类中的函数进行引用，因为类中的函数属性都为公有属性，对外是可见的，但是数据成员的属性为受保护属性，对外是不可见的，所以只能使用公有成员函数得到想要的值。

下面来看一下使用友元函数的情况：

在ComputerRectArea函数的定义中可以看到使用CRectangle的对象可以直接引用其中的数据成员，这是因为在CRectangle类中将ComputerRectArea函数声明为友元了。

从中可以看到使用友元保持了CRectangle类中数据的私有性，起到了隐藏数据成员的好处，又使得特定的类或函数可以直接访问这些隐藏数据成员。

10.5.2　友元类

对于类的私有方法，只有在该类中允许访问，其他类是不能访问的，但在开发程序时，如果两个类的耦合度比较紧密，能够在一个类中访问另一个类的私有成员会带来很大的方便。C++语言提供了友元类和友元方法（或者称为友元函数）来实现访问其他类的私有成员。当用户希望另一个类能够访问当前类的私有成员时，可以在当前类中将另一个类作为自己的友元类，这样在另一个类中就可以访问当前类的私有成员了，例如，定义友元类：

在定义CItem类时，使用friend关键字将CList类定义为CItem类的友元，这样CList类中的所有方法都可以访问CItem类中的私有成员了。在CList类的OutputItem方法中，语句m_Item.OutputName（）演示了调用CItem类的私有方法OutputName。

10.5.3　友元方法

在开发程序时，有时需要控制另一个类对当前类的私有成员的方法，例如，假设需要实现只允许CList类的某个成员访问CItem类的私有成员，而不允许其他成员函数访问CItem类的私有数据，这可以通过定义友元函数来实现。在定义CItem类时，可以将CList类的某个方法定义为友元方法，这样就限制了只有该方法允许访问CItem类的私有成员。

【实例10.5】 定义友元方法。 （实例位置：资源包\TM\sl\10\5）

在上面的代码中，在定义CItem类时，使用friend关键字将CList类的OutputItem方法设置为友元函数，在CList类的OutputItem方法中访问了CItem类的私有方法OutputName。程序运行结果如图10.8所示。

图10.8　定义友元方法

对于友元函数来说，不仅可以是类的成员函数，还可以是一个全局函数，例如：

在上面的代码中，定义全局函数OutputItem，在CItem类中使用friend关键字将OutputItem函数声明为友元函数，而CItem类中OutputName这个函数的属性是私有的，那么对外是不可见的。因为OutputItem是CItem类的友元函数，所以可以引用类中的私有成员。

10.6　命名空间

10.6.1　使用命名空间

在一个应用程序的多个文件中可能会存在同名的全局对象，这样会导致应用程序的链接错误。使用命名空间是消除命名冲突的最佳方式。

例如，下面的代码定义了两个命名空间：

在上面的代码中，namespace是关键字，而MyName1和MyName2是定义的两个命名空间名称，大括号中是所属命名空间中的对象。虽然在两个大括号中定义的变量是一样的，但是因为在不同的命名空间中，所以避免了标识符的冲突，保证了标识符的唯一性。

总而言之，命名空间就是一个命名的范围区域，程序员在这个特定的范围内创建的所有标识符都是唯一的。

10.6.2　定义命名空间

在10.6.1节中了解到有关命名空间的作用和使用的意义，本节具体来看一下是如何定义命名空间的。命名空间的定义格式如下：

定义命名空间要使用关键字namespace，例如：

在上面的代码中，MyName是定义的命名空间的名称，在大括号中定义了两个整型变量iInt1和iInt2，那么这两个整型变量就是属于MyName这个命名空间范围内的。

命名空间定义完成，如何使用其中的成员呢？在讲解类时曾介绍过使用作用域限定符“::”来引用类中的成员，在这里依然使用“::”来引用空间中的成员。引用空间成员的一般形式如下：

例如，引用MyName命名空间中的成员：

【实例10.6】 定义命名空间。 （实例位置：资源包\TM\sl\10\6）

在本实例中，定义命名空间包含变量成员，使其具有唯一性。

程序中使用namespace关键字定义两个命名空间，分别是MyName1和MyName2。在两个命名空间范围中，都定义了变量iValue，不过对其赋值分别为10和20。

在源文件中又定义一个全局变量iValue，赋值为30。在主函数main中分别调用命名空间中的iValue变量和全局变量，将值进行输出显示。MyName1::iValue表示引用MyName1命名空间中的变量，而MyName2::iValue表示引用MyName2命名空间中的变量，iValue是全局变量。

通过使用命名空间的方法，虽然定义相同名称的变量表示不同的值，但是也可以正确地进行引用显示。

程序运行结果如图10.9所示。

图10.9　定义命名空间

还有另一种引用命名空间中成员的方法，就是使用using namespace语句，一般形式如下：

例如，在源程序中包含MyName命名空间：

如果使用using namespace语句，则在引用空间中的成员时直接使用就可以。

【实例10.7】 使用using namespace语句。 （实例位置：资源包\TM\sl\10\7）

在本实例中，使用using namespace语句将命名空间包含在程序中，引用命名空间中的变量。

在程序中先定义命名空间MyName，之后使用using namespace语句，使用MyName命名空间，这样在main函数中使用的iValue变量就是指MyName命名空间中的iValue变量。

程序运行结果如图10.10所示。

需要注意的是，如果定义多个命名空间，并且在这些命名空间中都有相同标识符的成员，那么使用using namespace语句进行包含在引用成员时就会产生歧义性，这时最好还是使用作用域限定符来进行引用。

图10.10　使用using namespace语句

10.6.3　在多个文件中定义命名空间

在定义命名空间时，通常在头文件中声明命名空间中的函数，在源文件中定义命名空间中的函数，将程序的声明与实现分开，例如，在头文件中声明命名空间函数：

在源文件中定义函数：

在源文件中定义函数时，注意要使用命名空间名作为前缀，表明实现的是命名空间中定义的函数，否则将是定义一个全局函数。

【实例10.8】 在多个文件中定义命名空间。 （实例位置：资源包\TM\sl\10\8）

在本实例中，将命名空间的定义放在头文件中，而将命名空间中有关成员的定义放在源文件中。

将命名空间中的定义和命名空间中成员的具体操作分开，这样更符合程序编写规范并且非常易于修改和观察。

程序运行结果如图10.11所示。

图10.11　在多个文件中定义命名空间

在Detach.cpp源文件中还可以定义Output命名空间，例如：

此时，命名空间Output中的内容为两个文件Output命名空间内容的“总和”，因此，如果在Detach.cpp文件的Output命名空间中定义一个函数名称为Demo是非法的，因为进行了重复定义，这时编译器会提示Demo已经有一个函数体。

10.6.4　定义嵌套的命名空间

命名空间可以定义在其他的命名空间中，在这种情况下，仅仅通过使用外部的命名空间作为前缀，程序便可以引用在命名空间之外定义的其他标识符，然而，在命名空间内不定的标识符需要作为外部命名空间和内部命名空间名称的前缀出现，例如：

上述代码中，在Output命名空间中又定义了一个命名空间MyName，如果程序中访问MyName命名空间中的对象，可以使用外层的命名空间和内层的命名空间作为前缀，例如：

用户也可以直接使用using命令引用嵌套的MyName命名空间，例如：

上述代码中，using namespace Output::MyName; 语句只是引用了嵌套在Output命名空间中的MyName命名空间，并没有引用Output命名空间，因此试图访问Output命名空间中定义的对象是非法的，例如：

【实例10.9】 定义嵌套的命名空间。 （实例位置：资源包\TM\sl\10\9）

在本实例中，定义嵌套命名空间，使用命名空间名称选择调用函数。

在程序中定义Output命名空间，在其中又定义命名空间MyName。

Show函数属于Output命名空间中的成员，而Demo函数属于MyName命名空间中的成员。在main函数中调用Show和Demo函数时，要将所属的命名空间的作用范围写出。Output::Show表示在Output命名空间范围的Show函数，Output::MyName::Demo表示在嵌套命名空间MyName中的成员函数。

程序运行结果如图10.12所示。

图10.12　定义嵌套的命名空间

10.6.5　定义未命名的命名空间

尽管为命名空间指定名称是有益的，但是C++中也允许在定义中省略命名空间的名称来简单地定义未命名的命名空间。

例如，定义一个包含两个整型变量的未命名的命名空间：

事实上在无命名空间中定义的标识符被设置为全局命名空间，不过这样就违背了命名空间的设置原则，所以，未命名的命名空间没有被广泛应用。

10.7　小结

通过本章的学习，使得读者进入了有关面向对象的程序设计。在面向对象的程序设计中，其设计思路和人们日常生活中处理问题的方法相同，类是实现面向对象程序设计的基础。在本章中介绍了有关C++中类的基础概念，讲解了如何声明类并且介绍了如何实现一个类。之后介绍了有关类中构造函数和析构函数的作用，还有类成员的相关内容。最后介绍了使用友元来访问类中不可见的成员，讲解了C++语言中命名空间的使用。

10.8　实践与练习

（1）求解一个圆柱体的体积。编写一个基于对象的程序，并要求实现下面的功能：

①根据输入，得到圆柱体的半径、高（其中π为3.14）。

②计算圆柱体的体积，并输出。

（实例位置：资源包\TM\sl\10\10）

（2）开发一个程序，分别定义学生类（CStudent）。要求：

①在CStudent类中包含“姓名”“性别”“年级”“班级”数据成员。

②在CStudent类中声明成员函数SetInfor，该函数的作用是设置成员变量的数值。

③在CStudent类中声明ShowInfor函数，将其中数据信息输出。

（实例位置：资源包\TM\sl\10\11）

第11章

继承与派生

（视频讲解： 57分钟）

继承与派生是面向对象程序设计的两个重要特性，继承是从已有的类那里得到已有的特性，已有的类为基类或父类，新类被称为派生类或子类。继承与派生是从不同角度说明类之间的关系，这种关系包含了访问机制、多态和重载等。

通过阅读本章，您可以：

了解访问控制

掌握重载运算符

掌握虚函数

掌握多态

掌握抽象类

11.1　继承

继承（inheritance）是面向对象的主要特征（此外还有封装和多态）之一，它使得一个类可以从现有类中派生，而不必重新定义一个新类。继承的实质就是用已有的数据类型创建新的数据类型，并保留已有数据类型的特点，以旧类为基础创建新类，新类包含了旧类的数据成员和成员函数，并且可以在新类中添加新的数据成员和成员函数。旧类被称为基类或父类，新类被称为派生类或子类。

11.1.1　类的继承

类继承的形式如下：

继承方式有3种派生类型，分别为公有（public）型、保护（protected）型和私有（private）型，访问控制修饰符也是public、protected、private 3种类型，成员声明列表中包含类的成员变量及成员函数，是派生类新增的成员。“：”是一个运算符，表示基类和派生类之间的继承关系，如图11.1所示。

图11.1　继承关系

例如，定义一个继承员工类的操作员类。

定义一个员工类，它包含员工ID、员工姓名、所属部门等信息。

定义一个操作员类，通常操作员属于公司的员工，它包含员工ID、员工姓名、所属部门等信息，此外还包含密码信息、登录方法等。

操作员类是从员工类派生的一个新类，新类中增加密码信息、登录方法等信息，员工ID、员工姓名、所属部门等信息直接从员工类中继承得到。

【实例11.1】 以公有方式继承。 （实例位置：资源包\TM\sl\11\1）

程序中CEmployee类是COperator类的基类，也就是父类。COperator类将继承CEmployee类的所有非私有成员（private类型成员不能被继承）。optr对象初始化m_Name和m_Password成员后，调用了Login成员函数，程序运行结果如图11.2所示。

图11.2　以公有方式继承

用户在父类中派生子类时，可能存在一种情况，即在子类中定义了一个与父类同名的成员函数，此时称为子类隐藏了父类的成员函数，例如，重新定义COperator类，添加一个OutputName成员函数。

11.1.2　继承后可访问性

继承方式有public、private、protected 3种，它们的说明如下：

（1）公有型派生

公有型派生表示对于基类中的public数据成员和成员函数，在派生类中仍然是public；对于基类中的private数据成员和成员函数，在派生类中仍然是private，例如：

COperator类无法访问CEmployee类中的private数据成员m_ID、m_Name和m_Depart，如果将CEmployee类中的所有成员都设置为public后，COperator类才能访问CEmployee类中的所有成员。例如：

（2）私有型派生

私有型派生表示对于基类中的public、protected数据成员和成员函数，在派生类中可以访问。基类中的private数据成员，在派生类中不可以访问，例如：

（3）保护型派生

保护型派生表示对于基类中的public、protected数据成员和成员函数，在派生类中均为protected。protected类型在派生类的定义时可以访问，用派生类声明的对象不可以访问，也就是说在类体外不可以访问。protected成员可以被基类的所有派生类使用，这一性质可以沿继承树无限向下传播。

因为保护类的内部数据不能被随意更改，实例类本身负责维护，这就起到很好的封装性作用。把一个类分作两部分，一部分是公共的，另一部分是保护，保护成员对于使用者来说是不可见的，也是不需了解的，这就减少了类与其他代码的关联程度。类的功能是独立的，它不依赖于应用程序的运行环境，既可以放到这个程序中使用，也可以放到那个程序中使用，这就能够非常容易地用一个类替换另一个类。类访问限制的保护机制使人们编制的应用程序更加可靠和易维护。

11.1.3　构造函数访问顺序

由于父类和子类中都有构造函数和析构函数，因此子类对象在创建时是父类先进行构造，还是子类先进行构造？同样在子类对象释放时，是父类先进行释放，还是子类先进行释放？这都有个先后顺序问题。答案是当从父类派生一个子类并声明一个子类的对象时，它将先调用父类的构造函数，然后调用当前类的构造函数来创建对象；在释放子类对象时，先调用的是当前类的析构函数，然后是父类的析构函数。

【实例11.2】 构造函数访问顺序。 （实例位置：资源包\TM\sl\11\2）

程序运行结果如图11.3所示。

图11.3　构造函数访问顺序

从图11.3中可以发现，在定义COperator类对象时，首先调用的是父类CEmployee的构造函数，然后是COperator类的构造函数。子类对象的释放过程则与其构造过程恰恰相反，先调用自身的析构函数，然后再调用父类的析构函数。

在分析完对象的构建、释放过程后，会考虑这样一种情况：定义一个基类类型的指针，调用子类的构造函数为其构建对象，当对象释放时，需要调用父类的析构函数还是先调用子类的析构函数，再调用父类的析构函数呢？答案是如果析构函数是虚函数，则先调用子类的析构函数，然后再调用父类的析构函数；如果析构函数不是虚函数，则只调用父类的析构函数。可以想象，如果在子类中为某个数据成员在堆中分配了空间，父类中的析构函数不是虚成员函数，将使子类的析构函数不被调用，其结果是对象不能被正确地释放，导致内存泄漏的产生。因此，在编写类的析构函数时，析构函数通常是虚函数。构造函数调用顺序不受基类在成员初始化表中是否存在以及被列出的顺序的影响。

11.1.4　子类显示调用父类构造函数

当父类含有带参数的构造函数时，子类创建的时候会调用它么？答案是通过显示方式才可以调用。

无论创建子类对象时，调用的是哪种子类构造函数，都会自动调用父类默认构造函数。若想使用父类带参数的构造函数，则需要显示的方式。

【实例11.3】 子类显示调用父类的构造函数。 （实例位置：资源包\TM\sl\11\3）

程序运行如图11.4所示。

图11.4　子类显示调用父类的构造函数

在父类无参构造函数中初始化成员字符串数组m_Name为MR。从运行结果上看，子类对象创建时没有调用父类无参构造函数，调用的是带参数的构造函数。

注意

当父类只有带参数的构造函数时，子类必须以显示方法调用父类带参数的构造函数，否则编译会出现错误。

11.1.5　子类隐藏父类的成员函数

如果子类中定义了一个和父类一样的成员函数，那么一个子类对象是调用父类中的成员函数，还是调用子类中的成员函数呢？答案是调用子类中的成员函数。

【实例11.4】 子类隐藏父类的成员函数。 （实例位置：资源包\TM\sl\11\4）

程序运行结果如图11.5所示。

图11.5　子类隐藏父类的成员函数

从图11.5中可以发现，语句optr.OutputName（）；调用的是COperator类的OutputName成员函数，而不是CEmployee类的OutputName成员函数。如果用户想要访问父类的OutputName成员函数，需要显式使用父类名，例如：

如果子类中隐藏了父类的成员函数，则父类中所有同名的成员函数（重载的函数）均被隐藏，因此下面黑体部分代码是错误的，例如：

程序中，在CEmployee类中定义了重载的OutputName成员函数，而在COperator类中又定义了一个OutputName成员函数，导致父类中的所有同名成员函数被隐藏。语句optr.OutputName（"MR"）；是错误的。如果用户想要访问被隐藏的父类成员函数，依然需要指定父类名称，例如：

在派生完一个子类后，可以定义一个父类的类型指针，通过子类的构造函数为其创建对象，例如：

如果使用pWorker对象调用OutputName成员函数，例如，执行pWorker->OutputName（）；语句，调用的是CEmployee类的OutputName成员函数还是COperator类的OutputName成员函数呢？答案是调用CEmployee类的OutputName成员函数。编译器对OutputName成员函数进行的是静态绑定，即根据对象定义时的类型来确定调用哪个类的成员函数。由于pWorker属于CEmployee类型，因此调用的是CEmployee类的OutputName成员函数。那么是否有成员函数执行pWorker->OutputName（）；语句调用COperator类的OutputName成员函数呢？答案是通过定义虚函数可以实现。虚函数会在后面章节讲到。

11.2　重载运算符

运算符实际上是一个函数，所以运算符的重载实际上是函数的重载。编译程序对运算符重载的选择，遵循函数重载的选择原则。当遇到不很明显的运算时，编译程序会去寻找与参数相匹配的运算符函数。

11.2.1　重载运算符的必要性

C++语言中的数据类型分为基本数据类型和构造数据类型，基本数据类型可以直接完成算术运算。例如：

程序中实现了两个整型变量的相加，可以正确输出运行结果30，通过两个浮点变量、两个双精度变量都可以直接运用加法运算符“+”求和，但是类属于新构造的数据类型，类的两个对象就无法通过加法运算符来求和，例如：

当编译器编译到语句bk1+bk2时会报错，因为编译器不知道如何进行两个对象的相加。要实现两个类对象的加法运算有两种方法，一种是通过成员函数，另一种是重载运算符。

首先看通过成员函数的方法实现求和的例子：

程序可以正确输出运行结果30。使用成员函数实现求和形式比较单一，并且不利于代码复用，如果要实现多个对象的累加其代码的可读性会大大降低。使用重载运算符方法就可以解决这些问题。

11.2.2　重载运算符的形式与规则

重载运算符的声明形式如下：

operator是需要重载的运算符，整个语句没有返回类型，因为类型名就代表了它的返回类型。重载运算符将对象转换成类型名规定的类型，转换时的形式就像强制转换一样，但如果没有重载运算符定义，直接用强制转换编译器将无法通过编译。

重载运算符不可以是新创建的运算符，只能是C++语言中已有的运算符。可以重载的运算符如下：

算术运算符：+、-、*、/、%、++、--。

位操作运算符：&、|、～、^、>>、<<。

逻辑运算符：!、&&、||。

比较运算符：<、>、>=、<=、==、!=。

赋值运算符：=、+=、-=、*=、/=、%=、&=、|=、^=、<<=、>>=。

其他运算符：[]、()、->、逗号、new、delete、new[]、delete[]、->*。

并不是所有的C++语言中已有的运算符都可以重载，不允许重载的运算符有“.”“*”“::”“?”和“:”。

重载运算符时不能改变运算符操作数的个数，不能改变运算符原有的优先级，不能改变运算符原有的结合性，不能改变运算符原有的语法结构，即单目运算符只能重载为单目运算符，双目运算符只能重载为双目运算符。重载运算符含义必须清楚，不能有二义性。

【实例11.5】 通过重载运算符实现求和。 （实例位置：资源包\TM\sl\11\5）

程序运行结果如图11.6所示。

图11.6　通过重载运算符实现求和

类CBook重载了求和运算符后，由它声明的两个对象bk1和bk2可以像两个整型变量一样相加。

11.2.3　重载运算符的运算

重载运算符后可以完成对象和对象之间的运算，同样也可以通过重载运算实现对象和普通类型数据的运算，例如：

通过修改运算符的参数为整数类型，可以实现CBook对象与整数相加。

对于两个整型变量相加，用户可以调换加数和被加数的顺序，因为加法符合交换律。但是，对于通过重载运算符实现的两个不同类型的对象相加，则不可以，因此下面的语句是非法的：

对于++和--运算符，由于涉及前置运算和后置运算，在重载这类运算符时如何区分呢？默认情况下，如果重载运算符没有参数，则表示是前置运算，例如：

如果重载运算符使用了整数作为参数，则表示是后置运算，此时的参数值可以被忽略，它只是一个标识，标识后置运算。

默认情况下，将一个整数赋值给一个对象是非法的，可以通过重载赋值运算符将其变为合法的。例如：

10.2.2节介绍了通过复制构造函数将一个对象复制成另一个对象，那么通过重载赋值运算符也可以实现将一个整型数复制给一个对象，例如：

程序中重载了赋值运算符，给mybk对象赋值100，并通过OutputPages成员函数将该值输出。也可以通过重载构造函数将一个整数赋值给一个对象，例如：

程序中定义了一个重载的构造函数，以一个整数作为函数参数，这就可以将一个整数赋值给一个CBook类对象。语句vbBook = 200; 将调用构造函数CBook(int page)重新构造一个CBook对象，并将其赋值给vbBook对象。

11.2.4　转换运算符

C++语言中普通的数据类型可以进行强制类型转换，例如：

程序中将整型数i强制转换成双精度。

语句：

等同于：

double（）在C++语言中被称为转换运算符。通过重载转换运算符可以将类对象转换成想要的数据。

【实例11.6】 转换运算符 。（实例位置：资源包\TM\sl\11\6）

程序运行结果如图11.7所示。

图11.7　转换运算符

程序重载了转换运算符double（），然后将类CBook的两个对象强制转换为double类型后再进行求和，最后输出求和的结果。

11.3　多重继承

前文介绍的继承方式属于单继承，即子类只从一个父类继承公有的和受保护的成员。与其他面向对象语句不同，C++语言允许子类从多个父类继承公有的和受保护的成员，这被称为多重继承。

11.3.1　多重继承定义

多重继承是指有多个基类名标识符，其声明形式如下：

声明形式中有“：”运算符，基类名标识符之间用“，”运算符分开。

例如，鸟能够在天空飞翔，鱼能够在水里游，而水鸟既能够在天空飞翔，又能够在水里游，那么在定义水鸟类时，可以将鸟和鱼同时作为其基类。

程序运行结果如图11.8所示。

图11.8　多重继承

程序中定义了鸟类CBird，定义了鱼类Cfish，然后从鸟类和鱼类派生了一个子类水鸟类CWaterBird。水鸟类自然继承了鸟类和鱼类的所有公有和受保护的成员，因此CWaterBird类对象能够调用FlyInSky和SwimInWater成员函数。在CBird类中提供了一个Breath成员函数，在CFish类中同样提供了Breath成员函数，如果CWaterBird类对象调用Breath成员函数，将会执行哪个类的Breath成员函数呢？

答案是将会出现编译错误，编译器将产生歧义，不知道具体调用哪个类的Breath成员函数。为了让CWaterBird类对象能够访问Breath成员函数，需要在Breath成员函数前具体指定类名，例如：

在多重继承中存在这样一种情况，假如CBird类和CFish类均派生于同一个父类，如CAnimal类，那么当从CBird类和CFish类派生子类CWaterBird时，在CWaterBird类中将存在两个CAnimal类的复制。能否在派生CWaterBird类时，使其只存在一个CAnimal基类呢？为了解决该问题，C++语言提供了虚继承的机制，虚继承会在后面章节讲到。

11.3.2　二义性

派生类在调用成员函数时，先在自身的作用域内寻找，如果找不到，会到基类中寻找，但当派生类继承的基类中有同名成员时，派生类中就会出现来自不同基类的同名成员，例如：

CBaseA和CBaseB都是CDeriveC的父类，并且两个父类中都含有function成员函数，CDeriveC将不知道调用哪个基类的function成员函数，这就产生了二义性。

11.3.3　多重继承的构造顺序

单一继承是先调用基类的构造函数，然后调用派生类的构造函数，但多重继承将如何调用构造函数呢？多重继承中的基类构造函数被调用的顺序以类派生表中声明的顺序为准。派生表就是多重继承定义中继承方式后面的内容，调用顺序就是按照基类名标识符的前后顺序进行的。

【实例11.7】 多重继承的构造顺序。 （实例位置：资源包\TM\sl\11\7）

程序运行结果如图11.9所示。

图11.9　多重继承的构造顺序

程序中基类的声明顺序是先CBicycle类，后CAirplane类，所以对象的构造顺序就是先CBicycle类，后CAirplane类，最后是CAirBicycle类。

11.4　多态

多态性（polymorphism）是面向对象程序设计的一个重要特征，利用多态性可以设计和实现一个易于扩展的系统。在C++语言中，多态性是指具有不同功能的函数可以用同一个函数名，这样就可以用一个函数名调用不同内容的函数，发出同样的消息被不同类型的对象接收时，导致完全不同的行为。这里所说的消息主要指类的成员函数的调用，而不同的行为是指不同的实现。

多态性通过联编实现。联编是指一个计算机程序自身彼此关联的过程。按照联编所进行的阶段不同，可分为两种不同的联编方法：静态联编和动态联编。在C++中，根据联编的时刻不同，存在两种类型多态性，即函数重载和虚函数。

11.4.1　虚函数概述

在类的继承层次结构中，在不同的层次中可以出现名字、参数个数和类型都相同而功能不同的函数。编译器按照先自己后父类的顺序进行查找覆盖，如果子类有父类相同原型的成员函数时，要想调用父类的成员函数，需要对父类重新引用调用。虚函数则可以解决子类和父类相同原型成员函数的函数调用问题。虚函数允许在派生类中重新定义与基类同名的函数，并且可以通过基类指针或引用来访问基类和派生类中的同名函数。

在基类中用virtual声明成员函数为虚函数，在派生类中重新定义此函数，改变该函数的功能。在C++语言中虚函数可以继承，当一个成员函数被声明为虚函数后，其派生类中的同名函数都自动成为虚函数，但如果派生类没有覆盖基类的虚函数，则调用时调用基类的函数定义。

覆盖和重载的区别是，重载是同一层次函数名相同，覆盖是在继承层次中成员函数的函数原型完全相同。

11.4.2　利用虚函数实现动态绑定

多态主要体现在虚函数上，只要有虚函数存在，对象类型就会在程序运行时动态绑定。动态绑定的实现方法是定义一个指向基类对象的指针变量，并使它指向同一类族中需要调用该函数的对象，通过该指针变量调用此虚函数。

【实例11.8】 利用虚函数实现动态绑定。 （实例位置：资源包\TM\sl\11\8）

上述代码中，在CEmployee类中定义了一个虚函数OutputName，在子类COperator中改写了OutputName成员函数，其中COperator类中的OutputName成员函数即使没有使用virtual关键字仍为虚函数。接着定义了一个CEmployee类型的指针，调用COperator类的构造函数构造对象。

程序运行结果如图11.10所示。

图11.10　利用虚函数实现动态绑定

从图11.10中可以发现，pWorker->OutputName（）；语句调用的是COperator类的OutputName成员函数。虚函数有以下几方面限制：

（1）只有类的成员函数才能为虚函数。

（2）静态成员函数不能是虚函数，因为静态成员函数不受限于某个对象。

（3）内联函数不能是虚函数，因为内联函数是不能在运行中动态确定其位置的。

（4）构造函数不能是虚函数，析构函数通常是虚函数。

11.4.3　虚继承

11.3.1节讲到从CBird类和CFish类派生子类CWaterBird时，在CWaterBird类中将存在两个CAnimal类的复制，那么如何在派生CWaterBird类时使其只存在一个CAnimal基类呢？C++语言提供的虚继承机制，解决了这个问题。

【实例11.9】 虚继承。 （实例位置：资源包\TM\sl\11\9）

程序运行结果如图11.11所示。

图11.11　虚继承

上述代码在定义CBird类和CFish类时使用了关键字virtual从基类CAnimal派生而来。实际上，虚继承对于CBird类和CFish类没有多少影响，却对CWaterBird类产生了很大影响。CWaterBird类中不再有两个CAnimal类的复制，而只存在一个CAnimal的复制。

通常，在定义一个对象时，先依次调用基类的构造函数，最后才调用自身的构造函数，但是对于虚继承来说，情况有些不同。在定义CWaterBird类对象时，先调用基类CAnimal的构造函数，然后调用CBird类的构造函数，这里CBird类虽然为CAnimal的子类，但是在调用CBird类的构造函数时将不再调用CAnimal类的构造函数。对于CFish类也是同样的道理。

在程序开发过程中，多继承虽然带来了很多方便，但是很少有人愿意使用，因为它会带来很多复杂的问题，并且它能够完成的功能通过单继承同样可以实现。如今流行的C#、Delphi、Java等面向对象编程语言没有提供多继承的功能而是只采用单继承，这是经过设计者充分考虑的，因此，读者在开发应用程序时，如果能够使用单继承实现，尽量不要使用多继承。

11.5　抽象类

包含有纯虚函数的类称为抽象类，一个抽象类至少具有一个纯虚函数。抽象类只能作为基类派生出的新的子类，而不能在程序中被实例化（即不能说明抽象类的对象），但是可以使用指向抽象类的指针。在开发程序过程中并不是所有代码都是由软件构造师自己写的，有时候需要调用库函数，有时候分给别人写。一名软件构造师可以通过纯虚函数建立接口，然后让程序员填写代码实现接口，而自己主要负责建立抽象类。

11.5.1　纯虚函数

纯虚函数（pure virtual function）是指被标明为不具体实现的虚成员函数，它不具备函数的功能。许多情况下，在基类中不能给虚函数一个有意义的定义，这时可以在基类中将它说明为纯虚函数，而其实现留给派生类去做。纯虚函数不能被直接调用，仅起到提供一个与派生类相一致的接口的作用。声明纯虚函数的形式如下：

纯虚函数不可以被继承。当基类是抽象类时，在派生类中必须给出基类中纯虚函数的定义，或在该类中再声明其为纯虚函数。只有在派生类中给出了基类中所有纯虚函数的实现时，该派生类才不再成为抽象类。

【实例11.10】 创建纯虚函数。 （实例位置：资源包\TM\sl\11\10）

程序运行结果如图11.12所示。

图11.12　创建纯虚函数

程序定义了矩形类CRectangle和圆形类CCircle，两个类都派生于图形类Cfigure。图形类是一个在现实生活中不存在的对象，抽象类面积的计算方法不确定，所以，将图形类CFigure的面积计算方法设置为纯虚函数，这样圆形有圆形面积的计算方法，矩形有矩形面积的计算方法，每个继承自Cfigure的对象都有自己的面积，通过getArea成员函数就可以获取面积值。

注意

对于包含纯虚函数的类来说，是不能够实例化的，CFigure figure; 是错误的。

11.5.2　实现抽象类中的成员函数

抽象类通常用于作为其他类的父类，从抽象类派生的子类如果不是抽象类，则子类必须实现父类中的所有纯虚函数。

【实例11.11】 实现抽象类中的成员函数。 （实例位置：资源包\TM\sl\11\11）

程序中从CEmployee类派生了两个子类，分别为COperator和CSystemManager。这两个类分别实现了父类的纯虚成员函数OutputName。同样的一条语句pWorker->OutputName（）；，由于pWorker指向的对象不同，其行为也不同。程序运行结果如图11.13所示。

图11.13　实现抽象类中的成员函数

11.6　小结

本章介绍了面向对象程序设计中的关键技术—继承与派生，继承和派生在使用上还涉及二义性、访问顺序、运算符重载等许多技术问题，正确理解和处理这些技术有利于掌握继承的使用方法。继承中还涉及多重继承，这增加了面向对象开发的灵活性。面向对象可以建立抽象类，由抽象类派生新类，可以形成对类的一定管理。

11.7　实践与练习

（1）开发一个程序，定义一个类，包含“商品”和“售价”两个成员变量，要求重载运算符“<<”，可对类中的成员进行输出。 （实例位置：资源包\TM\sl\11\12）

（2）开发一个程序，分别定义教师类（Teacher）和职位类（Level），采用多重继承方式由这两个类派生出新类Teacher_Level（教师和职位信息）。要求：

①在Teacher类中包含“职称”数据成员，在Level类中包含“职务”数据成员，在Teacher_Level类中包含“工资”数据成员。

②在Teacher_Level类中使用Show函数将信息输出。 （实例位置：资源包\TM\sl\11\13）

第3篇

高级应用

第12章　模板

第13章　标准模板库

第14章　RTTI与异常处理

第15章　程序调试

第16章　文件操作

第17章　网络通信

本篇主要介绍了模板、标准模板库、RTTI与异常处理、程序调试、文件操作、网络通信等内容。另外，还介绍了很多编程者会遇到的困难，结合这些困难也讲解了一些解决方法。本篇最后介绍了网络通信的内容，让您能够在网上实现聊天功能。

第12章

模板

（视频讲解： 49分钟）

模板是c++的高级特性，分为函数模板和类模板，对于程序员来说，要完全掌握C++模板的用法并不容易。模板使程序员能够快速建立具有类型安全的类库集合和函数集合，它的实现大大方便了大规模软件开发。本章将介绍C++模板的基本概念、函数模板和类模板，使读者有效地掌握模板的用法，正确使用C++系统日益庞大的标准模板库（STL）。

通过阅读本章，您可以：

掌握函数模板

掌握类模板

了解链表

掌握链表类模板

12.1　函数模板

函数模板不是一个实在的函数，编译器不能为其生成可执行代码。定义函数模板后只是一个对函数功能框架的描述，当它具体执行时，将根据传递的实际参数决定其功能。

12.1.1　函数模板的定义

函数模板定义的一般形式如下：

template为关键字，表示定义一个模板，尖括号“< >”表示模板参数，模板参数主要有两种，一种是模板类型参数，另一种是模板非类型参数。上述代码中定义的模板使用的是模板类型参数，模板类型参数使用关键字class或typedef开始，其后是一个用户定义的合法标识符。模板非类型参数与普通参数定义相同，通常为一个常数。

可以将声明函数模板分成template部分和函数名部分，例如：

定义一个求和的函数模板，例如：

在定义完函数模板之后，需要在程序中调用函数模板。下面的代码演示了Sum函数模板的调用。

如果采用如下的形式调用Sum函数模板，将会出现错误。

上述代码中为函数模板传递了两个类型不同的参数，编译器产生了歧义。如果用户在调用函数模板时显式标识模板类型，就不会出现错误了，例如：

用函数模板生成实际可执行的函数又称为模板函数。函数模板与模板函数不是一个概念。从本质上讲，函数模板是一个“框架”，它不是真正可以编译生成代码的程序，而模板函数是把函数模板中的类型参数实例化后生成的函数，它和普通函数本质是相同的，可以生成可执行代码。

12.1.2　函数模板的作用

假设求两个函数之中最大者，如果想求整型数和实型数需要定义两个函数，两个函数定义如下：

能不能通过一个max函数来完成既求整型数之间最大者又求实型数之间最大者呢？答案是使用函数模板以及#define宏定义。

#define宏定义可以在预编译期对代码进行替换，例如：

上述代码可以求整数最大值和实型数最大值。但宏定义#define只是进行简单替换，它无法对类型进行检查，有时计算结果可能不是预计的，例如：

程序运行结果如图12.1所示。

程序运行的预期结果应该是1和0，为什么输出这样的结果呢？原因在于宏替换之后++n被执行了两次，因此n的值是2不是1。

图12.1　利用宏定义求最大值

宏是预编译指令，很难调试，无法单步进入宏的代码中。模板函数和#define宏定义相似，但模板函数是用模板实例化得到的函数，它与普通函数没有本质区别，可以重载模板函数。

使用模板求最大值的代码如下：

调用max可以正确计算整型数和实型数最大值，例如：

【实例12.1】 使用数组作为模板参数。 （实例位置：资源包\TM\sl\12\1）

程序运行结果如图12.2所示。

图12.2　使用数组作为模板参数

程序中定义一个函数模板Max，用来求数组中元素的最大值，其中模板参数使用模板类型参数type和模板非类型参数len，参数type声明了数组中的元素类型，参数len声明了数组中的元素个数，给定数组元素后，程序将数组中的最大值输出。

12.1.3　重载函数模板

整型数和实型数编译器可以直接进行比较，所以使用函数模板后也可以直接进行比较，但如果是字符指针指向的字符串该如何比较呢？答案是通过重载函数模板来实现。通常字符串需要库函数来进行比较，通过重载函数模板实现字符串的比较。

【实例12.2】 求出字符串的最小值。 （实例位置：资源包\TM\sl\12\2）

程序运行结果如图12.3所示。

图12.3　求出字符串的最小值

程序在重载的函数模板min的实现中，使用strcmp库函数来完成字符串的比较，此时使用min函数可以比较整型数据、实型数据、字符数据和字符串数据。

12.2　类模板

使用template关键字不但可以定义函数模板，也可以定义类模板。类模板代表一族类，是用来描述通用数据类型或处理方法的机制，它使类中的一些数据成员和成员函数的参数或返回值可以取任意数据类型。类模板可以说是用类生成类，减少了类的定义数量。

12.2.1　类模板的定义与声明

类模板的一般定义形式如下：

类模板成员函数定义形式如下：

template是关键字，类型形式参数表与函数模板定义相同。类模板的成员函数定义时的类模板名与类模板定义时要一致，类模板不是一个真实的类，需要重新生成类，生成类的形式如下：

用新生成的类定义对象的形式如下：

其中类型实在参数表应与该类模板中的类型形式参数表匹配。用类模板生成的类称为模板类。类模板和模板类不是同一个概念，类模板是模板的定义，不是真实的类，定义中要用到类型参数，模板类本质上与普通类相同，它是类模板的类型参数实例化之后得到的类。

定义一个容器的类模板，程序代码如下：

和普通类一样，需要对类模板成员函数进行定义，程序代码如下：

将模板类的参数设置为整型，然后用模板类声明对象，程序代码如下：

声明对象后，就可以调用类成员函数，程序代码如下：

在类模板定义中，类型形式参数表中的参数也可以是其他类模板，例如：

类模板也可以进行继承，例如：

T是一个类，CDerived继承自该类，CDerived可以对类T进行扩展。

12.2.2　简单类模板

类模板中的类型形式参数表可以在执行时指定，也可以在定义类模板时指定。下面看类型参数如何在执行时指定。

【实例12.3】 简单类模板。 （实例位置：资源包\TM\sl\12\3）

程序运行结果如图12.4所示。

图12.4　简单类模板

程序中的MyTemplate是一个模板类，它使用整型类型和双精度作为参数。

12.2.3　默认模板参数

默认模板参数就是在类模板定义时设置类型形式参数表中一个类型参数的默认值，该默认值是一个数据类型，有默认的数据类型参数后，在定义模板新类时就可以不进行指定。

【实例12.4】 默认模板参数。 （实例位置：资源包\TM\sl\12\4）

程序运行结果如图12.5所示。

图12.5　默认模板参数

12.2.4　为具体类型的参数提供默认值

默认模板参数是类模板中由默认的数据类型做参数，在模板定义时还可以为默认的数据类型声明变量，并且为变量赋值。

【实例12.5】 为具体类型的参数提供默认值。 （实例位置：资源包\TM\sl\12\5）

程序运行结果如图12.6所示。

图12.6　为具体类型的参数提供默认值

12.2.5　有界数组模板

C++语言不能检查数组下标是否越界，如果下标越界会造成程序崩溃，程序员在编辑代码时很难找到下标越界错误，那么如何能让数组进行下标越界检测呢？答案是建立数组模板，在模板定义时对数组的下标进行检查。

在模板中想要获取下标值，需要重载数组下标运算符“[]”，重载数组下标运算符后使用模板类实例化的数组，就可以进行下标越界检测了，例如：

程序中使用了assert来进行警告处理，当有下标越界情况发生时就弹出对话框警告，然后输出出现错误的代码位置。assert函数需要使用cassert头文件。

数组模板的应用示例如下：

程序运行结果如图12.7所示。

图12.7　数组模板

程序能够及时发现dateArray已经越界，因为定义数组时指定数组的长度为3，当数组下标为3时说明数组中有4个元素，所以程序执行到dateArray[3]时，弹出错误警告。

12.3　模板的使用

定义完模板类后如果想扩展模板新类的功能，需要对类模板进行覆盖，使模板类能够完成特殊功能。覆盖操作可以针对整个类模板、部分类模板以及类模板的成员函数，这种覆盖操作称为定制。

12.3.1　定制类模板

定制一个类模板，然后覆盖类模板中所定义的所有成员。

【实例12.6】 定制类模板。 （实例位置：资源包\TM\sl\12\6）

程序运行结果如图12.8所示。

图12.8　定制类模板

程序中定义了Set类模板，该模板中有一个构造函数和一个Display成员函数。Display成员函数负责输出成员的值。使用类Date定制了整个类模板，也就是说模板类中构造函数中的参数是Date对象，Display成员函数输出的也是Date对象。定制类模板相当于实例化一个模板类。

12.3.2　定制类模板成员函数

定制一个类模板，然后覆盖类模板中指定的成员。

【实例12.7】 定制类模板成员函数。 （实例位置：资源包\TM\sl\12\7）

程序运行结果如图12.9所示。

图12.9　定制类模板成员函数

程序中定义了Set类模板，该模板中有一个构造函数和一个Display成员函数。程序对模板类中的Display函数进行覆盖，使其参数类型设置为Date类，这样在使用Display函数输出时就会调用Date类中的Display函数进行输出。

12.4　链表类模板

链表是一种常用的数据结构，创建链表类模板就是创建一个对象的容器，在容器内可以对不同类型的对象进行插入、删除和排序等操作。C++标准模板库中有链表类模板，本节将主要实现简单的链表类模板。

12.4.1　链表

在介绍类模板之前，先来设计一个简单的单向链表。链表的功能包括向尾节点添加数据、遍历链表中的节点和在链表结束时释放所有节点。例如，定义一个链表类，程序代码如下：

链表类CList以CNode作为元素，通过MoveTrail成员函数将链表指针移动到末尾，通过AddNode成员函数添加一个节点。

声明一个链表对象，向其中添加节点，并遍历链表节点，程序代码如下：

程序运行结果如图12.10所示。

图12.10　简单链表

程序向链表中添加了5个节点，然后调用PassList成员函数完成对链表节点的遍历。

12.4.2　链表类模板

链表类Clist的一个最大缺陷就是链表不够灵活，其节点只能是CNode类型。让CList能够适应各种类型的节点的最简单方法就是使用类模板。类模板的定义与函数模板类似，以关键字template开始，其后是由尖括号“< >”构成的模板参数。下面重新修改链表类CList，以类模板的形式进行改写，程序代码如下：

上述代码利用类模板对链表类CList进行了修改，实际上是在原来链表的基础上将链表中出现CNode类型的地方替换为模板参数Type。下面再定义一个节点类CNet，演示模板类CList是如何适应不同的节点类型的。例如，使用CList类模板，程序代码如下：

程序运行结果如图12.11所示。

图12.11　使用CList类模板

类模板CList虽然能够使用不同类型的节点，但是对节点的类型是有一定要求的。第一，节点类必须包含一个指向自身的指针类型成员m_pNext，因为在CList中访问了m_pNext成员；第二，节点类中必须包含数据成员m_Data，其类型被限制为数字类型或有序类型。

12.4.3　类模板的静态数据成员

在类模板中可以定义静态的数据成员，类模板中的每个实例都有自己的静态数据成员，而不是所有的类模板实例共享静态数据成员。为了说明这一点，笔者对模板类CList进行简化，向其中添加一个静态数据成员，并初始化静态数据成员。

【实例12.8】 在类模板中使用静态数据成员。 （实例位置：资源包\TM\sl\12\8）

程序运行结果如图12.12所示。

图12.12　在类模板中使用静态数据成员

由于模板实例nodelist和netlist均有各自的静态数据成员，因此m_ListValue的值是不同的，但是对于同一类型的模板实例，其静态数据成员是共享的。

12.5　小结

模板是C++的高级特性，一个模板可以定义一组函数或类，它使用数据类型和类名作为参数，建立具有类型安全的类库集合和函数集合。模板可以对作为模板参数的数据类型进行相同的操作，大大减少了代码量，提高了代码效率，更是方便了大规模软件的开发。标准C++库在很大程度上依赖于模板。通过本章的学习，可以使读者对C++语言有更深入的了解。

12.6　实践与练习

（1）要求设计一个函数模板，该函数模板功能是计算两个数相加的结果，并将结果返回。 （实例位置：资源包\TM\sl\12\9）

（2）设计一个类模板，在类模板中声明一个成员函数，用该函数得到两个数据，然后再设计成员函数max得到两个数据间的最大值。 （实例位置：资源包\TM\sl\12\10）

第13章

标准模板库

（视频讲解： 35分钟）

标准模板库（Standard Template Library, STL）主要目的是为标准化组件提供类模板进行范型编程。STL技术是对原有C++技术的一种补充，具有通用性好、效率高、数据结构简单、安全机制完善等特点。STL是一些容器的集合，这些容器在算法库的支持下使程序开发变得更加简单和高效。

通过阅读本章，您可以：

了解STL

掌握容器

掌握算法

掌握迭代器

13.1　几种常见数据结构

13.1.1　简述STL

标准模板库(Standard Template Library, STL)，是根据本地C++标准规范定义的一套功能强大、适用范围广的函数模板、类模板的库。它主要包含容器、算法、函数对象等内容。

容器实现了多个类型的数据结构和一些相关操作。

算法提供了排序、查找、替换功能等的函数。

函数对象是C++运算符重载的进一步应用，类模板实例化后很多工作都需要它的支持。

首先简单介绍一下一些数据结构的基本概念。如果您学习过数据结构，请跳过本节。

13.1.2　顺序线性结构

数据结构本身是一种集合，包含的各项称为元素。

数组是顺序线性结构的实现，如图13.1所示，通过指针来访问所有元素（对象），之后对它执行相应的操作。

图13.1　数组的结构示意图

13.1.3　基本操作

操作主要分为以下4种。

查询：通过输入或者已知条件在数据结构中找到相应的元素。

插入：在各个元素之间，逻辑插入一个元素。

删除：在各个元素中，逻辑删除一个元素。

修改：通过输入或者已知条件在数据结构中修改相应的元素。

注意

逻辑插入意味着元素所在的储存单元并不一定真的发生插入操作，例如，数组本身所在的内存分布是连续的，无法在中间插入任何内存单元。实现插入的方法如下：将数组所有元素和待插入的元素依照相应的次序复制给另外一个容量+1的数组，之后销毁原来的内存空间。新的数组可以被认为是插入完成之后的数组（一般需要动态分配）。逻辑删除与它的原理相同。

13.1.4　栈结构

栈的结构如图13.2所示。它很像一个从上方打开的箱子，所有的对象都遵循着后进先出的原则。在栈中随机位置插入和删除的实现通常需要辅助方法实现，相对较为缓慢。若所储存的对象超过自身容量，则会出现栈溢出。

图13.2　栈结构示意图

13.1.5　队列结构

队列的结构和现实生活中的排队一样。所有对象只能从队列的后方加入，从前方离开队列，即先进先出。和栈相似的是，队列不擅长处理随机位置的插入和删除操作，如图13.3所示。

图13.3　队列结构示意图

13.1.6　链表结构

链表与以上几种结构都是线性的结构。与它们不同的是，链表的储存方式不是顺序的。每个对象中含有下一个对象所在位置的信息（可认为是地址），链表的第一个元素的位置由头指针（也称为Head）储存，链表尾部的元素储存的位置信息为空。由于链表的储存区不是连续的，因此无法通过指针偏移量的方案查找相应对象，只能够通过以遍历的方法访问：链表由头指针所储存的地址信息访问到第一个元素，之后依次由所储存的位置信息（地址）访问后面的元素。链表的优点是在随机位置的插入和删除较为迅速，如图13.4所示。

图13.4　链表结构示意图

链表可分为单向链表、双向链表和循环链表。

13.1.7　图结构

图结构是一种复杂的数据结构，每个对象都可以和其他对象相关联。图一般采取映射的方式表示内部元素。映射通常指的是事物间的联系，如图13.5所示，每条边代表着两个物体所对应的关系。使用表达式<k,t>表示的是这种关系，k和t可以代表线的两端，也可以是线与物体，随着不同的定义，所代表的关系也不同。

图13.5　图结构示意图

以上简单介绍了几种常用的数据结构，在标准模板库中，容器类对它们进行了实现、扩展和封装。

13.2　序列容器

STL提供了很多容器，每种容器都提供一组操作行为。序列（sequence）容器提供插入功能，其中的元素都是有序的，但并未排序。序列容器包括向量（vector）、双端队列（deque）和双向链表（list）。

13.2.1　对比容器适配器与容器

标准模板库的容器适配器与容器都是用来储存和组织对象的模板类。容器适配器与容器相比，限制的条件更多。容器适配器定义在相应的头文件中，见表13.1。

表13.1　容器适配器头文件内容

容器适配器还被定义在容器的头文件中，这通常与容器的内部实现有关。容器头文件内容见表13.2。

表13.2　容器头文件内容

在C++中使用标准模板库提供的容器，需要加入相应的头文件并使用名称空间std。容器适配器与容器在使用限制上的最大区别为是否支持迭代器。迭代器的行为类似于指针，通过它能够遍历容器中的所有元素，但容器适配器不支持它。通常情况下，我们更倾向于使用容器而非容器适配器。

13.2.2　对比迭代器与容器

标准模板库中提供了4种迭代器，见表13.3。

表13.3　迭代器的分类

这4种迭代器在功能上都是“向上兼容”的，越来越强大。容器自身的迭代器种类是依照容器的结构来决定的，vector包含的迭代器是随机访问迭代器，list包含的是双向迭代器，在queue中的迭代器则是前向迭代器。

13.2.3　向量类模板

向量（vector）是一种随机访问的数组类型，提供了对数组元素的快速、随机访问，以及在序列尾部快速、随机的插入和删除操作。它是大小可变的向量，在需要时可以改变其大小。

使用向量类模板需要创建vector对象，创建vector对象有以下几种方法。

std::vector<type> name;

该方法创建了一个名为name的空vector对象，该对象可容纳类型为type的数据。例如，为整型值创建一个空std::vector对象可以使用这样的语句：

std::vector<type> name(size);

该方法用来初始化具有size元素个数的vector对象。

std::vector<type> name(size,value);

该方法用来初始化具有size元素个数的vector对象，并将对象的初始值设为value。

std::vector<type> name(myvector);

该方法使用复制构造函数，用现有的向量myvector创建了一个vector对象。

std::vector<type> name(first,last);

该方法创建了元素在指定范围内的向量，first代表起始范围，last代表结束范围。

vector对象的主要成员继承于随机接入容器和反向插入序列，主要成员函数及说明见表13.4。

表13.4　vector对象主要成员函数及说明

下面通过实例进一步学习vector模板类的使用方法。

【实例13.1】 vector模板类的操作方法。 （实例位置：资源包\TM\sl\13\1）

程序运行结果如图13.6所示。

图13.6　vector模板类的操作方法

实例演示了vector<int>容器的初始化，以及插入、删除等操作。在本例中v1和v2均用resize分配了空间。当分配的空间小于自身原来的空间大小时，删除掉原来的末尾元素。当分配的空间大于自身的空间时，自动在末尾元素后边添加相应个数的0值。同理，若vector模板使用的是某一个类，则增加的会是以默认构造函数创建的对象。同时可以看到，向v1添加元素时，v1的容量从8增加到了12，这个就是vector提供的特性，在需要的时候可以扩大自身的容量。

注意

虽然vector支持insert函数插入，但与链表数据结构的容器比较而言效率较差，不推荐经常使用。

13.2.4　双端队列类模板

双端队列（deque）是一种随机访问的数据类型，提供了在序列两端快速插入和删除操作的功能，它可以在需要的时候修改其自身的大小，主要完成标准C++数据结构中队列的功能。

使用双端队列类模板需要创建deque对象，创建deque对象有以下几种方法。

std::deque<type> name;

该方法创建了一个名为name的空deque对象，该对象可容纳数据类型为type的数据，例如，为整型值创建一个空std:: deque对象可以使用这样的语句：

std::deque<type> name(size);

该方法创建一个大小为size的deque对象。

std::deque<type> name(size,value);

该方法创建一个大小为size的deque对象，并将对象的每个值设为value。

std::deque<type> name(mydeque);

该方法使用复制构造函数，用现有的双端队列mydeque创建一个deque对象。

std::deque<type> name(first,last);

该方法创建了元素在指定范围内的双端队列，first代表起始范围，last代表结束范围。

deque对象的主要成员函数及说明见表13.5。

表13.5　deque对象主要成员函数及说明

【实例13.2】 双端队列类模板应用。 （实例位置：资源包\TM\sl\13\2）

程序运行结果如图13.7所示。

图13.7　双端队列类模板应用

程序定义了一个空的类型为int的deque对象，然后用函数push_back把值插入deque对象中，并把deque对象显示出来，最后删除deque对象中的第一个元素，并把删除后的deque对象中的第二个元素赋值。

13.2.5　链表类模板

链表（list），即双向链表容器，它不支持随机访问，访问链表元素要指针从链表的某个端点开始，插入和删除操作所花费的时间是固定的，和该元素在链表中的位置无关。list在任何位置插入和删除动作都很快，不像vector只在末尾进行操作。

使用链表类模板需要创建list对象，创建list对象有以下几种方法。

std::list<type> name;

该方法创建了一个名为name的空list对象，该对象可容纳数据类型为type的数据，例如，为整型值创建一个空std::list对象可以使用这样的语句：

std::list<type> name(size);

该方法初始化具有size元素个数的list对象。

std::list<type> name(size,value);

该方法初始化具有size元素个数的list对象，并将对象的每个元素设为value。

std::list<type> name(mylist);

该方法使用复制构造函数，用现有的链表mylist创建了一个list对象。

std::list<type> name(first,last);

该方法创建了元素在指定范围内的链表，first代表起始范围，last代表结束范围。

list对象的主要成员函数及说明见表13.6。

表13.6　list对象主要成员函数及说明

可以发现，list<T>所支持的操作与vector<T>很相近，但这些操作的实现原理不尽相同，执行效率也不一样。list（双向链表）的优点是插入元素的效率很高，缺点是不支持随机访问，也就是说，链表无法像数组一样通过索引来访问，形如：

对list各个元素的访问，通常使用的是迭代器。

迭代器的使用方法类似于指针，下面用一个实例演示一下用迭代器访问list中的元素。

【实例13.3】 list和vector中的迭代器。 （实例位置：资源包\TM\sl\13\3）

程序运行结果如图13.8所示。

图13.8　迭代器的应用

通过程序可以观察到，迭代器iterator类和指针用法很相似，支持自增操作符，并且通过“*”可以访问相应的对象内容，但list中的迭代器不支持“+”号运算符，而指针与vector中的迭代器都支持。

13.3　结合容器

结合（associative）容器是STL提供的容器的一种，其中的元素都是经过排序的，它主要通过关键码的方式来提高查询的效率。结合容器包括set、multiset、map、multimap和hash table，本章主要介绍set、multiset、map和multimap。

13.3.1　set类模板

set类模板又称为集合类模板，一个集合对象像链表一样顺序地存储一组值。在一个集合中，集合元素既充当存储的数据，又充当数据的关键码。可以使用下面的几种方法来创建set对象。

std::set<type,predicate> name;

这种方法创建了一个名为name，并且包含type类型数据的set空对象。该对象使用predicate（谓词）所指定的函数来对集合中的元素进行排序，例如，要给整数创建一个空set对象，可以这样写：

std::set<type,predicate> name(myset);

这种方法使用了复制构造函数，从一个已存在的集合myset中生成一个set对象。

std::set<type,predicate> name(first,last);

这种方法从一定范围的元素中根据多重指示器所指示的起始与终止位置创建一个集合。

set类中的方法说明见表13.7。

表13.7　set类中的方法说明

下面通过一些操作来实现对set对象的应用。

创建整型类型的集合，并在该集合中实现数据的插入。

【实例13.4】 创建整型类集合，并插入数据。 （实例位置：资源包\TM\sl\13\4）

程序运行结果如图13.9所示。

图13.9　插入数据

创建一个整型类型的集合，并删除集合中的元素。

【实例13.5】 删除元素。 （实例位置：资源包\TM\sl\13\5）

程序运行结果如图13.10所示。

图13.10　删除元素

创建一个字符型的set对象，插入元素值，并通过指定的字符在集合中查找元素。

【实例13.6】 通过指定的字符在集合中查找元素。 （实例位置：资源包\TM\sl\13\6）

程序运行结果如图13.11所示。

图13.11　查找元素

创建两个集合，分别向集合中插入数据，并对集合进行比较。

【实例13.7】 对集合进行比较。 （实例位置：资源包\TM\sl\13\7）

程序运行结果如图13.12所示。

图13.12　比较集合

13.3.2　multiset类模板

multiset（多重集合）使程序能顺序存储一组数据。与集合类类似，多重集合的元素既可以作为存储的数据又可以作为数据的关键码。然而，与集合类不同的是多重集合类可以包含重复的数据。下面列出了几种创建多重集合的方法。

std::multiset<type,predicate> name;

这种方法创建了一个名为name，并且包含type类型数据的multiset空对象。该对象使用predicate（谓词）所指定的函数来对集合中的元素进行排序，例如，要给整数创建一个空multiset对象，可以这样写：

注意

less<int>表达式后要有空格。

std:: multiset <type,predicate> name(mymultiset)；

这种方法使用了复制构造函数，从一个已经存在的集合mymultiset中生成一个multiset对象。

std:: multiset <type,predicate> name(first,last)；

这种方法从一定范围的元素中根据指示器所指示的起始与终止位置创建一个集合。

multiset类中的方法说明见表13.8。

表13.8　multiset类中的方法说明

下面通过一些操作来实现对multiset对象的应用。

创建整型类型的集合，并在该集合中实现数据的插入。

【实例13.8】 在集合中实现数据插入。 （实例位置：资源包\TM\sl\13\8）

程序运行结果如图13.13所示。

图13.13　数据插入

创建一个整型类型的集合，并删除集合中的元素。

【实例13.9】 删除集合中的元素。 （实例位置：资源包\TM\sl\13\9）

程序运行结果如图13.14所示。

图13.14　删除元素

创建一个字符型的multiset对象，插入元素值，并通过指定的字符在集合中查找元素。

【实例13.10】 在集合中查找元素。 （实例位置：资源包\TM\sl\13\10）

程序运行结果如图13.15所示。

图13.15　查找元素

创建两个多重集合，分别向集合中插入数据，并对集合进行比较。

【实例13.11】 比较集合。 （实例位置：资源包\TM\sl\13\11）

程序运行结果如图13.16所示。

图13.16　比较集合

13.3.3　map类模板

map对象按顺序存储一组值，其中每个元素与一个检索关键码关联。map与set和multiset不同，set和multiset中元素既被作为存储的数据，又被作为数据的关键码，而map类型中元素的数据和关键码是分开的。创建map对象的方法如下：

map<key,type,predicate> name;

这种方法创建了一个名为name，并且包含type类型数据的map空对象。该对象使用predicate（谓词）所指定的函数来对集合中的元素进行排序。例如，要给整数创建一个空map对象，可以这样写：

map<key,type,predicate> name(mymap);

这种方法使用了复制构造函数，从一个已存在的映射mymap中生成一个map对象。

map<key,type,predicate> name(first,last);

这种方法从一定范围的元素中根据多重指示器所指示的起始与终止位置创建一个映射。

map类中的方法说明见表13.9所示。

表13.9　map类中的方法说明

下面通过一些操作来实现对map对象的应用。

创建一个map对象，并使用下标插入新的元素。

【实例13.12】 使用下标插入新元素。 （实例位置：资源包\TM\sl\13\12）

程序运行结果如图13.17所示。

图13.17　使用下标插入元素

13.3.4　multimap类模板

multimap能够顺序存储一组值，它与map相同的是每一个元素都包含一个关键码以及与之联系的数据项，与map不同的是多重映射可以包含重复的数据值，并且不能使用[]操作符向多重映射中插入元素。创建multimap对象的方法如下：

multimap<key,type,predicate> name;

这种方法创建了一个名为name，并且包含type类型数据的multimap空对象。该对象使用predicate（）所指定的函数来对集合中的元素进行排序，例如，要给整数创建一个空multimap对象，可以这样写：

multimap<key,type,predicate> name(mymap);

这种方法使用了复制构造函数，从一个已存在的映射mymap中生成一个multimap对象。

multimap<key,type,predicate> name(first,last);

这种方法从一定范围的元素中根据多重指示器所指示的起始与终止位置创建一个多重映射。

下面通过一些操作来实现对multimap对象的应用。

创建multimap对象，并向该映射中插入新的元素。

【实例13.13】 向multimap映射中插入新元素。 （实例位置：资源包\TM\sl\13\13）

程序运行结果如图13.18所示。

图13.18　插入新元素

13.4　算法

算法（algorithm）是STL的中枢，STL提供了算法库，算法库中都是模板函数。迭代器主要负责从容器中获取一个对象，算法与具体对象在容器中的什么位置等细节无关。每种算法都是参数化一个或多个迭代器类型的函数模板。

标准算法分4种类别：非修正序列算法、修正序列算法、排序算法和数值算法。

13.4.1　非修正序列算法

非修正序列算法不修改它们所作用的容器，如计算元素个数或查找元素的函数。STL中提供的非修正序列算法见表13.10。

表13.10　STL的非修正序列算法

adjacent_find(first,last)

返回一个迭代器，指向第一对同值元素对的第一个元素。函数在迭代器first和last指明的范围内查找。此函数还有一个谓词版本，其第3个实参是一个比较函数。

【实例13.14】 应用adjacent_find算法搜索相邻的重复元素。 （实例位置：资源包\TM\sl\13\14）

程序运行结果如图13.19所示。

图13.19　应用adjacent_find算法搜索相邻的重复元素

程序中定义了整型的multiset容器，以及该容器的迭代器it，在multiset容器中有两个重复的值，使用adjacent_find算法将这两个元素输出。

count(first,last,val)

返回容器中值为val的元素个数。函数在迭代器first和last指明的范围内查找。

【实例13.15】 应用count算法计算相同元素的个数。 （实例位置：资源包\TM\sl\13\15）

程序运行结果如图13.20所示。

图13.20　应用count算法计算相同元素的个数

程序中multiset容器有两个相同元素，使用count算法获取容器中重复的元素。

for_each(first,last,func)

对first到last范围内的各个元素执行函数func定义的操作。

【实例13.16】 使用for_each算法输出容器内的元素。 （实例位置：资源包\TM\sl\13\16）

程序运行结果如图13.21所示。

图13.21　使用for_each算法输出容器内的元素

程序定义Output函数用来输出变量值，调用for_each算法将multiset容器中的值不断地传输给Output函数，执行for_each算法相当于执行了一个循环语句。

13.4.2　修正序列算法

修正序列算法的有些操作会改变容器的内容，例如，把一个容器的部分内容复制到同一个容器的另一部分，或者用指定值填充容器。STL的修正序列算法提供了这类操作，见表13.11。

表13.11　STL的修正序列算法

fill(first,last,val)

把值val复制到迭代器first和last指明范围内的各个元素中。

【实例13.17】 应用fill算法对容器元素赋值。 （实例位置：资源包\TM\sl\13\17）

程序运行结果如图13.22所示。

图13.22　应用fill算法对容器元素赋值

程序向vector容器中添加0～9共10个元素，然后使用fill算法将前5个元素的值全改为0，通过在修改前输出全部元素和在修改后输出全部元素，可以观察到fill算法的执行效果。

random_shuffle(first,last)

把迭代器first和last指明范围内的元素顺序随机打乱。

【实例13.18】 应用random_shuffle算法将元素顺序随机打乱。（ 实例位置：资源包\TM\sl\13\18）

程序运行结果如图13.23所示。

图13.23　应用random_shuffle算法将元素顺序随机打乱

程序中应用random_shuffle算法将vector容器内元素的排列顺序打乱，原来vector容器内的元素是从0～9顺序排列的，打乱后元素顺序没有任何规律。

partition(first,last,pred)

把一个容器划分成两部分，第1部分包含令谓词pred返回true值的元素，第2部分包含令谓词pred返回false值的元素。函数返回的迭代器指向两部分的分界点元素。

【实例13.19】 应用partition算法将容器分组。 （实例位置：资源包\TM\sl\13\19）

程序运行结果如图13.24所示。

图13.24　应用partition算法将容器分组

rotate(first,middle,last)

把从middle到last范围内的元素做旋转运算，并放置到从first开始的子序列中。

【实例13.20】 应用rotate算法。 （实例位置：资源包\TM\sl\13\20）

程序运行结果如图13.25所示。

图13.25　应用rotate算法

rotate算法就是程序使用旋转运算对容器内的元素重新排序。旋转运算就是以一个元素为中心，前后对应的两个元素对调位置。

13.4.3　排序算法

排序算法的特点是对容器的内容进行不同方式的排序，如sort（）。STL中提供的排序算法见表13.12。

表13.12　STL的排序算法

sort(first,last)

对迭代器first和last指明范围内的元素排序。此函数的另一个版本以谓词作为第3个实参。

【实例13.21】 应用sort算法。 （实例位置：资源包\TM\sl\13\21）

程序运行结果如图13.26所示。

图13.26　应用sort算法

程序中应用sort算法对vector容器内的元素进行递增排序。

partial_sort(first,middle,last)

对迭代器first和last指明范围内的元素进行排序，把排序后的前一部分元素放至序列的前一部分（由first和middle指明的范围），其余元素放在后一部分（由middle和last指明的范围）。此函数的另一个版本以谓词作为第4个实参。

【实例13.22】 应用partial_sort算法。 （实例位置：资源包\TM\sl\13\22）

程序运行结果如图13.27所示。

图13.27　应用partial_sort算法

程序应用partial_sort算法对vector容器内元素进行排序，将排序后的第2～第4个元素放到容器的前一部分。

nth_element(first,nth,last)

在迭代器first和last指明范围的已排序元素中查找Vect。如果找到值为Vect的元素，返回true，否则返回false值。此函数的另一个版本以谓词作为第4个实参。

【实例13.23】 应用nth_element算法。 （实例位置：资源包\TM\sl\13\23）

程序运行结果如图13.28所示。

图13.28　应用nth_element算法

程序应用nth_element算法查找容器内的最后一个元素，nth_element算法返回true值。应用完nth_element算法后，容器内的元素被重新排序。

merge(first,last,first2,last2,result)

把迭代器first和last指明范围内的已排序元素与first2和last2指明范围内的已排序元素合并，并把重新排序后的元素放在从result开始的序列中。此函数的另一个版本以谓词作为第6个实参。

【实例13.24】 应用merge算法。 （实例位置：资源包\TM\sl\13\24）

程序运行结果如图13.29所示。

图13.29　应用merge算法

程序应用merge算法将两个容器内的元素重新组合到另一个容器内。

includes(first,last,first2,last2)

在迭代器first和last指明的范围内，如果含有一个用first2和last2指明范围的已排序序列，则返回true值。此函数的另一个版本以谓词作为第5个实参。

【实例13.25】 应用includes算法。 （实例位置：资源包\TM\sl\13\25）

程序运行结果如图13.30所示。

图13.30　应用includes算法

程序判断在strVect1内指定的两个元素在strVect2内是否含有其他元素。

13.4.4　数值算法

数值算法是对容器的内容进行数值计算。STL的数值算法实现了4种类型的计算，可以在一个值序列上进行这些计算。数值算法见表13.13。

表13.13　STL的数值算法

accumulate(first,last,init)

计算init与迭代器first和last指明范围内各元素值的总和，并返回结果。

【实例13.26】 应用accumulate算法。 （实例位置：资源包\TM\sl\13\26）

程序运行结果如图13.31所示。

图13.31　应用accumulate算法

程序对容器内的元素值进行累加，结果是(1+2)+(2+3)+(3+4)。

inner_product(first,last,first2,init)

先计算迭代器first和last指明的序列与从first2开始的相同长度的序列的内积，然后返回内积与init的和。计算内积的步骤为：依次把第1个序列的各个元素值与第2个序列对应位置的元素值相乘，再计算各个乘积的和。

【实例13.27】 应用inner_product算法。 （实例位置：资源包\TM\sl\13\27）

程序运行结果如图13.32所示。

图13.32　应用inner_product算法

partial_sum(first,last,result)

对迭代器first和last指明范围内的元素进行部分求和，然后把计算结果赋予从result开始的序列。部分求和的计算步骤为：首先把原序列的第1个元素赋值为新序列的第1个元素，然后把原序列的前两个元素相加，并把结果赋值给新序列的第2个元素，再把原序列的第2个和第3个元素相加，并把结果赋值给新序列的第3个元素，依此类推，直到计算完原序列的全部元素为止。

【实例13.28】 应用partial_sum算法实现局部总和。 （实例位置：资源包\TM\sl\13\28）

程序运行结果如图13.33所示。

图13.33　应用partial_sum算法实现局部总和

adjacent_difference(first,last,result)

计算迭代器first和last指明范围内相邻元素的差值，并把结果赋予从result开始的序列。相邻差值的计算步骤为：首先把原序列的第1个元素赋值给新序列的第1个元素，再把原序列的第2个元素减去第1个元素，并把结果赋值给新序列的第2个元素，然后把原序列的第3个元素减去第2个元素，并把结果赋值给新序列的第3个元素。依此类推，直到计算完原序列的全部元素为止。此函数的另一个版本以谓词作为第4个实参。

【实例13.29】 应用adjacent_difference算法。（ 实例位置：资源包\TM\sl\13\29）

程序运行结果如图13.34所示。

图13.34　应用adjacent_difference算法

13.5　迭代器

迭代器相当于指向容器元素的指针，它在容器内可以向前移动，也可以做向前或向后双向移动。有专为输入元素准备的迭代器，有专为输出元素准备的迭代器，还有可以进行随机操作的迭代器，这为访问容器提供了通用方法。

13.5.1　输出迭代器

输出迭代器只用于写一个序列，它可以进行递增和提取操作。

【实例13.30】 应用输出迭代器。 （实例位置：资源包\TM\sl\13\30）

程序运行结果如图13.35所示。

图13.35　应用输出迭代器

程序使用整型向量的输出迭代器，输出向量中的所有元素。

13.5.2　输入迭代器

输入迭代器只用于读一个序列，它可以进行递增、提取和比较操作。

【实例13.31】 应用输入迭代器。 （实例位置：资源包\TM\sl\13\31）

程序运行结果如图13.36所示。

图13.36　应用输入迭代器

程序使用输入迭代器向向量容器内添加元素，最后将添加的元素输出到屏幕。

13.5.3　前向迭代器

前向迭代器既可用于读，也可用于写，它不仅具有输入和输出迭代器的功能，还具有保存其值的功能，从而能够从迭代器原来的位置开始重新遍历序列。

【实例13.32】 应用前向迭代器。 （实例位置：资源包\TM\sl\13\32）

程序运行结果如图13.37所示。

图13.37　应用前向迭代器

程序中使用saveIt迭代器保存了it迭代器的内容，并使用it迭代器向容器中添加元素，通过saveIt迭代器将容器内的元素输出。

13.5.4　双向迭代器

双向迭代器既可用于读，也可用于写，它与前向迭代器类似，只是双向迭代器可做递增和递减操作。

【实例13.33】 应用双向迭代器。 （实例位置：资源包\TM\sl\13\33）

程序运行结果如图13.38所示。

图13.38　应用双向迭代器

程序中使用saveIt迭代器保存了it迭代器的内容，并使用it迭代器向容器中添加元素，通过saveIt迭代器以从前向后和从后向前两种顺序将容器内的元素输出。

13.5.5　随机访问迭代器

随机访问迭代器是最强大的迭代器类型，不仅具有双向迭代器的所有功能，还能使用指针的算术运算和所有比较运算。

【实例13.34】 应用随机访问迭代器。 （实例位置：资源包\TM\sl\13\34）

程序运行结果如图13.39所示。

图13.39　应用随机访问迭代器

13.6　小结

本章主要介绍了标准模板库中的容器、算法和迭代器，这三者是标准模板库的核心内容，并且相互联系非常密切。迭代器是访问容器中的元素，算法是对容器中的元素进行操作。每种容器都有各自的特点，只有熟练掌握这些特点才能将标准模板库的作用充分发挥。应尽可能多地使用标准模板库提供的算法，这样可以节省许多开发时间。

13.7　实践与练习

（1）使用元素颠倒算法reverse，实现对vector容器内的元素进行颠倒排序。 （实例位置：资源包\TM\sl\13\35）

（2）使用删除容器中重复元素算法unique，实现对vector容器内重复元素的删除。 （实例位置：资源包\TM\sl\13\36）

第14章

RTTI与异常处理

（视频讲解： 22分钟）

面向对象编程的一个特点是运行时进行类型识别，这是对面向对象中多态的支持，使用RTTI（运行时类型识别）能够使类的设计更加抽象，更加符合人们的思维。对象的动态生成，能够增加设计的灵活性，而异常处理则是在程序运行时对可能发生的错误进行控制，防止系统灾难性错误的发生。

通过阅读本章，您可以：

掌握RTTI的使用

了解什么是异常

掌握异常捕获

掌握标准异常

14.1　RTTI

运行时类型识别（Run-time Type Identification, RTTI）是在只有一个指向基类的指针或引用时所确定的一个对象的类型。

在编写程序的过程中，往往只提供了一个对象的指针，但通常在使用时需要明确这个指针的确切类型。利用RTTI就可以方便地获取某个对象指针的确切类型并进行控制。

14.1.1　什么是RTTI

RTTI可以在程序运行时通过某一对象的指针确定该对象的类型。许多程序设计人员都使用过虚基类编写面向对象的功能，通常在基类中定义了所有子类的通用属性或行为。但有些时候子类会存在属于自己的一些公有的属性或行为，这时通过基类对象的指针如何调用子类特有的属性和行为呢？首先，需要确定的就是这个基类对象属于哪个子类，然后将该对象转换成子类对象再进行调用。

图14.1所示展示了具有特有功能的类。

图14.1　具有特有功能的类

从图14.1中可以看出CBint类和CBString类都继承于CBase，这3个类存在于一个公共方法GetName（），而CBint类有自己的方法GetInt（），CBString类有自己的方法GetString（）。如果想通过CBase类的指针调用CBint类或CBString类的特有方法时就必须确定指针的具体类。下面的代码完成了这样的功能：

从上面的代码可以看出，基类CBase的指针B1和C1分别指向了CBint类与CBString类的对象，并且在程序运行时基类通过static_cast进行了转换，这样就形成了一个运行时类型识别的过程。

14.1.2　RTTI与引用

RTTI必须能与引用一起工作。指针与引用存在明显不同，因为引用总是由编译器逆向引用，而一个指针的类型或它指向的类型可能要检测，例如，下面的代码定义了一个子类和一个基类：

通过下面的代码可以看出，typeid（）获取的指针是基类类型，而不是子类类型或派生类类型，typeid（）获取的引用是子类类型。

与此相反，指针指向的类型在typeid（）看来是派生类而不是基类，而用一个引用的地址时产生的是基类而不是派生类。

14.1.3　RTTI与多重继承

RTTI具有非常强大的功能，对于面向对象的编程方法，如果在类继承时使用了virtual虚基类，RTTI仍然可以准确地获取对象在运行时的信息。

例如，下面的代码通过虚基类的形式继承了父类，通过RTTI获取基类指针对象的信息。

即使只提供一个virtual基类指针，typeid（）也能准确地检测出实际对象的名字。用动态映射同样也会工作得很好，但编译器不允许试图用原来的方法强制映射：

编译器知道这不可能正确，所以它要求用户使用动态映射。

14.1.4　RTTI映射语法

无论什么时候用类型映射，都是在打破类型系统，这实际上是在告诉编译器，即使知道一个对象的确切类型，还是可以假定认为它是另外一种类型。这本身就是一件很危险的事情，也是一个容易发生错误的地方。

为了解决这个问题，C++用保留字dynamic_cast、const_cast、static_cast和reinterpret_cast提供了一个统一的类型映射语法，为需要进行动态映射时提供了一个解决问题的可能。这意味着那些已有的映射语法已经被重载得太多，不能再支持任何其他的功能了。

dynamic_cast：用于安全类型的向下映射。

例如，通过dynamic_cast实现基类指针的向下转型。

const_cast：用于映射常量和变量。

如果想把一个const转换为非const，就要用到const_cast。这是可以用const_cast的唯一转换，如果还有其他的转换牵涉进来，它必须分开来指定，否则会有一个编译错误。

例如，在常方法中修改成员变量和常量的值。

static_cast：为了行为良好和行为较好使用的映射，如向上转型和类型自动转换。

例如，通过static_cast将子类指针向上转成基类指针。

reinterpret_cast：将某一类型映射回原有类型时使用。

例如，将整型转成字符型，再由reinterpret_cast转换回原类型。

14.2　小结

本章主要介绍了RTTI的使用以及如何进行异常处理。通过RTTI的学习，可以丰富类的设计思路，加强对面向对象的理解，有助于理解类间的类型转换。程序中出现异常是不可避免的，异常处理则能够帮助程序开发人员尽快发现错误所在。为了减少错误的发生，应尽量掌握更多的异常处理方式。

第15章

程序调试

（视频讲解： 33分钟）

程序调试是程序开发人员必不可少的一项工作，甚至占去了程序开发人员大部分的开发时间。正确地使用调试方法不但可以提高工作效率，而且可以减轻程序开发人员的工作负担。本章将主要介绍程序错误的常见类型、常用的调试工具、调试的基本应用和高级应用。

通过阅读本章，您可以：

了解程序错误常见的4种类型

掌握调试工具的使用

掌握调试的基本应用

熟悉调试的高级应用

15.1　选择正确的调试方法

在程序设计的过程中，无论您有多么丰富的编程经验，或是多么熟练的技术水平，都会不可避免地出现程序错误。有经验的程序员早就认识到，解决程序错误的最好方法就是使用程序调试。任何一种程序开发工具都具有其独立的调试系统，而且十分成熟，所以只要掌握好开发工具自身的调试系统，就可以很好地解决编程过程中出现的错误。

在程序开发的过程中，逻辑错误和运行时错误通常是最普遍而且是最容易发生的，所以当程序出现错误时，首先要确定程序是因逻辑错误还是运行时错误而导致的，再根据情况选择适当的调试方法解决错误。

15.2　程序错误常见的4种类型

应用程序可能遇到的错误类型主要有4种：语法错误、连接错误、运行时错误和逻辑错误。这些错误中的大多数均发生在所编写的C++程序向其可执行形式转化的过程中。另外，在程序编译的过程中由于所要建立的可执行文件版本的不同（调试版本和发行版本），可能出现不同的错误。

15.2.1　语法错误

在程序设计过程中，不论是初学者还是经验丰富的程序员都会或多或少地发生语法错误。语法错误就是在编写程序代码时违反了C++的语法规则，一旦违反了语法规则，在程序进行编译时开发工具就会提示编译出错的信息。

例如，在编写代码时最常使用的字符串输出功能：

通过编译上面的代码，系统会提示这样的错误信息：

当编译器将源代码翻译为可以在计算机上运行的可执行程序时，编译器将试图定位和报告出尽可能多的语法错误，并对合法但以后可能引起错误的潜在问题给出警告。Visual C++6.0可以报告出声明但从未使用过的变量和在初始化之前对变量的使用，甚至可以指出所产生的逻辑流程将禁止某些代码段被执行—这种问题被标记为unreachable code（不可到达的代码）。

上面的提示信息明确地指出在printf语句中缺少“；”号，但有些初学编程的读者却根本不看编译器所提示的信息，而是花费大量时间来找程序出错的原因，而有经验的程序员看到编译器提示的信息后，可以立刻修正程序中的错误。

15.2.2　连接错误

出现连接错误最常见的一种情况就是在使用动态链接库（DLL）时，虽然已对lib文件进行了载入，但lib文件与动态链接库所在的位置和可执行文件并不在同一个目录下，导致程序在进行编译时出现连接错误。连接错误与其他的错误类型在本质上是有区别的，例如，语法错误是由于违反语法规则产生的，而连接错误则是由于在编译可执行文件时缺少外部连接文件所产生的。

例如，定义了一个名为demodll.dll的动态链接库，通过应用程序调用此动态链接库中的函数。在应用程序的头文件中添加如下代码：

在对程序进行编译时，编译器会寻找名为demodll.lib的文件，这个文件是与demodll.dll文件一起编译生成的，但如果这个lib文件并没有放在可执行文件的相同目录下，在进行编译时会提示下面的错误信息：

通过上面的错误信息可以看出，在编译器连接demodll.lib文件时没有找到相应的文件，所以提示“文件无法打开”的错误信息。

15.2.3　运行时错误

运行时错误并不是在程序进行编译时产生的，而是在程序编译后没有出现任何错误提示的情况下程序运行时发生的异常现象。运行时错误与语法错误或连接错误不同，它在编译时并没有给出错误提示，所以不能简单地从提示信息上进行处理。但Visual C++ 6.0为编程人员提供了强大的错误处理能力，可以通过在程序代码中设置错误断点来检测并处理运行时的错误。

例如，定义一个数组变量，并向这个数组变量中赋值。

程序运行过程中，在为第11个元素赋值时将提示异常信息，如图15.1所示。

图15.1　运行时错误信息

通过图15.1中显示的错误信息可以知道是使用了不存在的内存地址，但不能确定是什么地方使用了不存在的内存地址，所以需要设置断点，跟踪程序执行的每一步，直到遇到程序出错的位置再判断异常产生的原因并处理。利用断点进行程序调试的方法将在后文介绍。

15.2.4　逻辑错误

逻辑错误是最难处理的一种错误类型，因为导致这种错误出现的原因是对某一问题解决方案的错误理解，更糟糕的是编译器并不能捕获并处理逻辑错误。对于这种错误并不能看到任何错误信息，只能看到错误的结果或是导致程序的终止。在这种情况下就只能通过各种不同的数据来测试程序的运行结果是否正确。

虽然编译器不能捕获程序中的逻辑错误，但可以使用调试器或其他方法来解决逻辑错误。这里介绍两种常用的逻辑错误处理方法：

一是利用调试器设置断点，并跟踪程序执行的每条语句。在跟踪的同时对程序中的变量值进行验证，查看程序出现逻辑错误的位置。

二是利用字符串输出语句，在程序中需要输出验证信息的位置将变量值以字符串的形式进行输出，这样就可以很快查看出程序中出现逻辑错误的位置。

15.3　调试工具的使用

初学编程的读者在实际的编程过程中会遇到许多问题，能快速找到并解决这些问题的唯一方法就是使用程序调试。程序调试在每种语言的集成开发环境中都存在，只是一些初学者将这一功能忽略，导致遇到程序错误时不知所措。

15.3.1　创建调试程序

在进入程序调试的学习之前，先来创建一个用于程序调试的程序，创建步骤如下：

（1）选择File→New命令，打开New对话框。

（2）在Project name文本框中输入DebugProject，在左侧的工程列表中选择Win32 Console Application选项用于创建一个控制台应用程序，如图15.2所示。

图15.2　New对话框

（3）单击OK按钮进入下一个页面，在这个页面中选中A "Hello, World!" application单选按钮，创建一个Hello World工程，单击Finish按钮完成工程的创建，如图15.3所示。

图15.3　应用程序向导

（4）修改程序代码用于程序调试时使用，程序代码如下：

程序运行结果如图15.4所示。

图15.4　程序运行结果

15.3.2　进入调试状态

按F5键即可进入调试状态，但此时由于没有指定断点，程序的运行与普通的运行没有什么区别，因此，在进入调试状态前应先将光标定位在断点所在行，然后按F9键添加断点，如图15.5所示。

图15.5　添加断点

再按F5键，当程序运行到断点所在行时就会停下来，这时就可以查看程序运行时的信息了。

15.3.3　Watch窗口

Watch窗口用于在调试期显示程序中所定义的变量或表达式的值，在代码编辑区中拖放选中的变量到该窗口，即可显示该变量的值，操作步骤如下：

（1）进入调试状态，单击Debug工具栏中的Watch按钮打开Watch窗口。

（2）选中变量s，然后将变量拖放到Watch窗口中，如图15.6所示。

（3）按F10键单步执行到变量被赋值后的代码，即可看到变量值的变化。

图15.6　Watch窗口

15.3.4　Call Stack窗口

Call Stack窗口用来显示栈中被调用但还未返回的函数，操作步骤如下：

（1）进入调试状态，单击Debug工具栏中的Call Stack按钮打开Call Stack窗口。

（2）由于在main函数中设置了断点，因此程序运行到断点处停止，Call Stack窗口将显示main函数的信息，如图15.7所示。

图15.7　Call Stack窗口

15.3.5　Memory窗口

Memory窗口用于显示内存中的数据，操作步骤如下：

（1）进入调试状态，单击Debug工具栏中的Memory按钮打开Memory窗口。

（2）单步运行程序到strcpy函数所在行，此时在Watch窗口中可看到str变量的地址，将该地址输入到Memory窗口中按Enter键，单步执行到下一行即可看到str变量在内存中的值，如图15.8所示。

图15.8　Memory窗口

15.3.6　Variables窗口

Variables窗口用来显示变量的值，在这个窗口中所显示的变量是由系统自动生成的，不可以进行修改。进入调试状态，单击Debug工具栏上的Variables按钮打开该窗口，在窗口中有3个选项卡：Auto、Locals和this，如图15.9所示。

图15.9　Variables窗口

选项卡的功能说明如下。

Auto（自动）：显示程序运行当前行和前一行所使用的变量的值。

Locals（本地）：显示程序运行的当前函数中所包含的变量值。

this（this指针）：显示当前this指针所指向的类对象的信息。

15.3.7　Registers窗口

Registers窗口显示了当前CPU各个寄存器的值，运行程序进入调试状态，在Debug工具栏上单击Registers按钮打开该窗口，通过该窗口可以查看寄存器信息，如图15.10所示。

图15.10　Registers窗口

15.3.8　Disassembly窗口

Disassembly窗口用来显示程序源代码的反汇编代码，运行程序进入调试状态，单击Debug工具栏上的Disassembly按钮打开该窗口，如图15.11所示。

图15.11　Disassembly窗口

15.4　调试的基本应用

调试是解决程序出现错误的有效途径，通过简单的调试操作可以解决程序设计中出现的大部分错误。

15.4.1　变量的跟踪与查看

变量的跟踪查看是在设置了断点后进入调试运行状态，按F10或F11键单步执行到变量所在的代码行时，在Variables窗口中将显示当前代码行中变量的值，这样就可以在程序运行时查看当前行变量的值，再根据这个值判断程序计算是否正确。

例如，循环输出一组数的累加和。

当断点位于sum +=i;语句上时，通过Variables窗口可以同时看到变量sum和i的值，如图15.12所示。

图15.12　跟踪变量

在Variables窗口中，Name代表变量的名称，Value代表变量的值，而且每次相应变量发生改变时，Value中的值会自动发生改变。

使用Auto选项卡观察时存在一个问题，即其自动跟踪的范围过于狭窄，此时，可以单击Variables窗口的Locals选项卡，观察所有的局部变量。

Variables窗口所显示的变量信息是由系统决定的，所以不能控制变量的显示范围，但通过Watch窗口可以自由地添加或删除当前需要显示的变量。同样使用上面的代码，进入调试运行状态，在Debug工具栏单击Watch按钮，打开Watch窗口，在Name中输入需要显示的变量的名称，如图15.13所示。

图15.13　查看变量值

在Watch窗口中可以看到，sum是由程序设计人员添写的，这样就可以根据需要在窗口中显示需要查看的变量。当需要删除变量的显示时，按Delete键即可。同时，还可以看到在Watch窗口的下方有多个选项卡，在每一个选项卡中都可以设置不同的变量进行跟踪查看。

15.4.2　位置断点的使用

位置断点就是在指定的位置上设置断点。通常程序设计人员使用的断点都属于位置断点，也就是在指定的代码行上按F9键添加一个断点，但这类断点只起到在程序调试运行到代码所在行时中断程序运行的作用。

位置断点的使用有一定的技巧，程序设计人员可以控制位置断点在什么条件下有效，这样就使断点和调试更加灵活地结合起来，更容易处理复杂的调试情况。

例如，在一个循环的过程中可能会出现异常，此时就可以使用位置断点。

在上面的代码执行时，for循环语句中会出现异常，但由于循环的次数非常多，因此不能简单地使用单步调试。为了快速地达到调试的效果，在sum += i;这行代码上按F9键添加一个调试断点，然后按Ctrl + B键弹出Breakpoints对话框，选中断点，如图15.14所示。

图15.14　Breakpoints对话框

然后，单击Condition按钮弹出Breakpoint Condition对话框，在最下面的文本框中输入循环的最大次数，单击OK按钮，如图15.15所示。

图15.15　Breakpoint Condition对话框

位置断点设置完成后，按F5键开始调试运行，此时程序运行报错。按Ctrl + B键弹出Breakpoints对话框，在对话框的下方可以看到运行的总次数为100次，有76次没有运行，如图15.16所示。

图15.16　Breakpoints对话框

关闭Breakpoints对话框，按Shift + F5键退出调试状态。按Ctrl + B键弹出Breakpoints对话框，单击Condition按钮将skip的值由100修改为23，单击OK按钮，如图15.17所示。

图15.17　修改skip值

再按F5键调试运行，此时按F10键单步运行就可以立刻定位到程序出错的位置。

15.4.3　数据断点的使用

数据断点是对程序代码中的数据进行判断或操作引发的中断行为。数据断点与位置断点不同，是在程序的调试时期根据某变量的数据条件添加的断点，最常用于解决数据的溢出而导致的程序错误。

例如，在对字符串数组进行赋值时，很容易出现内存溢出的现象。

程序运行结果如图15.18所示。

图15.18　数组赋值

从上面代码的运行结果看出程序存在错误，即str1在第二次输出时结果不正确。在程序中只为str1赋了一次值，为什么会出现两个结果呢？当问题不好解决时，可以使用数据断点跟踪所有对str1赋值的位置，然后查看出现错误的原因。

下面介绍如何使用数据断点解决字符数组赋值时内存溢出的问题。首先在第一个strcpy出现的位置添加一个普通断点，按F5键进入调试运行状态，然后按Ctrl + B键打开Breakpoints对话框。在Data选项卡中设置需要查看变量的名字str1，单击OK按钮，如图15.19所示。

图15.19　设置数据断点

当数据断点设置完成后，按F10键执行正确的数据赋值与数据输出语句，这是因为这些语句都会触发数据断点，所以需要跳过这些正确的修改语句，当断点定位在第二个strcpy函数所在的语句执行时，按F5键将会弹出str1数据发生改变的提示信息，如图15.20所示。

图15.20　str1数据发生改变

这个strcpy是给str2赋值的，为什么会修改str1的值呢？原来str2的长度只有4个字节，而在赋值时却超出了范围，所以就修改了str1的值。就这样，通过数据断点，快速地解决了程序中出现的异常错误。

15.5　调试的高级应用

程序调试是编程过程中必不可少的一项工作，简单的程序错误通过简单的调试方法就可以解决，但有些程序错误只凭一些基本的调试技术是无法解决的，本节将向读者介绍一些高级的调试方法用于解决更为复杂的程序错误。

15.5.1　在调试时修改变量的值

Visual C++ 6.0提供了强大的程序调试功能，不但可以满足各种调试的需要，还可以在程序的调试时期修改某一变量的值并继续调试。这一功能不但提高了程序调试的效率，还增加了调试的灵活性，使程序设计人员更好地解决程序中出现的错误。

例如，通过对字符的判断显示不同的结果。

在代码的if语句上添加一个普通断点，按F5键调试运行，此时需要将flgstr的值由'Y'修改成'N'，在Variables窗口中找到这个变量直接修改其值为'N'，该变量将以红色显示，如图15.21所示。

图15.21　修改变量值

按F10键继续运行，可以看到outstr的值是条件为false时的结果，如图15.22所示。

图15.22　outstr变量输出结果

通过上面的功能，程序设计人员就可以在调试过程中任意修改变量的值，而且不需要重新编译程序就可以继续调试。对于这一功能同样可以在Watch窗口中使用，如图15.23所示。

图15.23　在Watch窗口中修改变量值

15.5.2　在循环中调试

在循环语句中进行调试是一项十分复杂的工作，由于循环的次数通常都会很多，因此单步调试可能需要花费大量的时间，这样就必须寻找另外的方法对循环进行调试。在15.4节中介绍位置断点的时候讲解了循环语句中调试的方法，使用这种方法虽然可以快速定位到程序出错的位置，但并不能跟踪变量的值。在本节中将介绍另一种循环调试的方法，这种方法就是利用TRACE宏将循环中所使用的变量结果显示在Debug窗口中。

例如，在一个循环的过程中可能会出现异常，就可以使用TRACE进行调试查看。

这段代码必须写在支持MFC的应用程序中，因为TRACE宏是定义在MFC中的。运行此程序时不用设置断点，直接按F5键调试运行，变量i的值就会显示在Debug窗口中，如图15.24所示。

图15.24　在Debug窗口中输出变量的值

通过图15.24就可以看出循环在什么位置出错，并能同时看到相应变量的值，这有助于对程序中的错误进行分析。这种方法不但适用于循环程序，也适用于其他错误的处理。

15.6　小结

对于程序开发人员来说，程序调试是必不可少的能力之一。因为任何程序在开发完成以后，都需要进行调试，以修改运行中的错误，达到用户的需求标准，所以本章着重对程序错误的常见类型和调试工具进行了讲解，并且对调试的应用进行了举例介绍。

第16章

文件操作

（视频讲解： 58分钟）

文件操作是程序开发中不可缺少的一部分，任何需要数据存储的软件都需要进行文件操作。文件操作包括打开文件、读文件和写文件，掌握读文件和写文件的同时，还要理解文件指针的移动，这能够控制读文件和写文件的位置。

通过阅读本章，您可以：

了解文件流

掌握文件的打开方式

掌握文件的读写操作

掌握文件随机访问

16.1　文件流

16.1.1　C++中的流类库

C++语言中为不同类型数据的标准输入和输出定义了专门的类库，类库中主要有ios、istream、ostream、iostream、ifstream、ofstream、fstream、istrstream、ostrstream和strstream等类。ios为根基类，它直接派生4个类，输入流类istream、输出流类ostream、文件流基类fstreambase和字符串流基类strstreambase。输入文件流类ifstream同时继承了输入流类和文件流基类，输出文件流类ofstream同时继承了输出流类和文件流基类，输入字符串流类istrstream同时继承了输入流类和字符串流基类，输出字符串流类ostrstream同时继承了输出流类和字符串流基类，输入／输出流类iostream同时继承了输入流类和输出流类，输入／输出文件流类fstream同时继承了输入／输出流类和文件流基类，输入／输出字符串流类strstream同时继承了输入／输出流类和字符串流基类。类库关系如图16.1所示。

图16.1　类库关系图

16.1.2　类库的使用

C++系统中的I/O（输入／输出）标准类，都定义在iostream.h、fstream.h和strstream.h这3个头文件中，各头文件包含的类如下：

进行标准I/O操作时使用iostream.h头文件，它包含有ios、iostream、istream和ostream等类。

进行文件I/O操作时使用fstream.h头文件，它包含有fstream、ifstream、ofstream和fstreambase等类。

进行串I/O操作时使用strstream.h头文件，它包含有strstream、istrstream、ostrstream、strstreambase和iostream等类。

要进行什么样的操作，只要引入头文件就可以使用类进行操作了。

16.1.3　ios类中的枚举常量

在根基类ios中定义了用户需要使用的枚举类型，由于它们是在公用成员部分定义的，因此其中的每个枚举类型常量在加上ios::前缀后都可以被本类成员函数和所有外部函数访问。

在3个枚举类型中有一个无名枚举类型，其中定义的每个枚举常量都是用于设置控制输入／输出格式的标志的。该枚举类型定义如下：

主要枚举常量的含义如下。

skipws：利用它设置对应标志后，从流中输入数据时跳过当前位置及后面的所有连续的空白字符，从第一个非空白字符起读数，否则不跳过空白字符。空格、制表符\t、回车符\r和换行符\n统称为空白符。默认为设置。

left：靠左对齐输出数据。

right：靠右对齐输出数据。

insternal：显示占满整个域宽，用填充字符在符号和数值之间填充。

dec：用十进制输出数据。

hex：用十六进制输出数据。

showbase：在数值前显示基数符，八进制基数符是0，十六进制基数符是0x。

showpoint：强制输出的浮点数中带有小数点和小数尾部的无效数字0。

uppercase：用大写输出数据。

showpos：在数值前显示符号。

scientific：用科学记数法显示浮点数。

fixed：用固定小数点位数显示浮点数。

16.1.4　流的输入／输出

通过前文的学习，相信读者已经对流有了一定的了解，现在就通过实例来看一下如何在程序中使用流进行输出。

【实例16.1】 字符相加。 （实例位置：资源包\TM\sl\16\1）

程序运行结果如图16.2所示。

图16.2　字符相加

16.2　文件打开

16.2.1　打开方式

只有使用文件流与磁盘上的文件进行连接后才能对磁盘上的文件进行操作，这个连接过程称为打开文件。

打开文件的方式有以下两种：

（1）在创建文件流时利用构造函数打开文件，即在创建流时加入参数，语法结构如下：

其中，文件流类可以是fstream、ifstream和ofstream中的一种。文件名指的是磁盘文件的名称，包括磁盘文件的路径名。打开方式在ios类中定义，有输入方式、输出方式、追加方式等，具体如下。

ios::in：用输入方式打开文件，文件只能读取，不能改写。

ios::out：以输出方式打开文件，只能改写，不能读取。

ios::app：以追加方式打开文件，打开后文件指针在文件尾部，可改写。

ios::ate：打开已存在的文件，文件指针指向文件尾部，可读可写。

ios::binary：以二进制方式打开文件。

ios::trunc：打开文件进行写操作，如果文件已经存在，清除文件中的数据。

ios::nocreate：打开已经存在的文件，如果文件不存在，打开失败，不创建。

ios::noreplace：创建新文件，如果文件已经存在，打开失败，不覆盖。

参数可以结合运算符“|”使用，例如：

ios::in|ios::out：以读写方式打开文件，对文件可读可写。

ios::in|ios::binary：以二进制方式打开文件，可进行读操作。

使用相对路径打开文件test.txt进行写操作：

使用绝对路径打开文件test.txt进行写操作：

注意

字符“\”表示转义，如果使用“c:\”则必须写成“c:\\”。

（2）利用open函数打开磁盘文件，语法结构如下：

文件流对象名是一个已经定义了的文件流对象。

使用两种方式中的任意一种打开文件后，如果打开成功，文件流对象为非0值，如果打开失败，则文件流对象为0值。检测一个文件是否打开成功可以用以下语句：

prot决定文件的访问方式，取值说明如下。

0：普通文件。

1：只读文件。

2：隐含文件。

4：系统文件。

16.2.2　默认打开模式

如果没有指定打开方式参数，编译器会使用默认值。

文件打开模式根据用户的需要有不同的组合，下面就对各个模式的效果进行介绍。文件打开模式见表16.1。

表16.1　文件打开模式

16.2.3　打开文件同时创建文件

通过前文的学习，相信读者已经对文件操作的知识有了一定的了解。为了使读者更好地掌握前面学习的知识，下面通过实例进一步介绍。

【实例16.2】 创建文件。 （实例位置：资源包\TM\sl\16\2）

程序运行将会创建两个文件，由于ofstream默认打开方式是std::ios::out | std::ios::trunk，因此当文件夹内没有test.txt文件和test2.txt文件时，会创建这两个文件，并向文件写入字符串。向test.txt文件写入字符串name1 sex1 age1，向test2.txt文件写入字符串name2 sex2 age2。如果文件夹内有test.txt文件和test2.txt文件时，程序会覆盖原有文件而重新写入。

16.3　文件的读写

在对文件进行操作时，必然离不开读写文件。在使用程序查看文件内容时，首先要读取文件，而要修改文件内容时，则需要向文件中写入数据，本节主要介绍通过程序对文件的读写操作。

16.3.1　文件流

（1）文件流分类

流可以分为3类，即输入流、输出流和输入／输出流，相应地必须将流说明为ifstream、ofstream和fstream类的对象。

说明了流对象之后，可以使用open函数打开文件。文件的打开即是在流与文件之间建立一个连接。

（2）文件流成员函数

ofstream和ifstream类有很多用于磁盘文件管理的函数。

attach：在一个打开的文件与流之间建立连接。

close：刷新未保存的数据后关闭文件。

flush：刷新流。

open：打开一个文件并把它与流连接。

put：把一个字节写入流中。

rdbuf：返回与流连接的filebuf对象。

seekp：设置流文件指针位置。

setmode：设置流为二进制或文本模式。

tellp：获取流文件指针位置。

write：把一组字节写入流中。

（3）fstream成员函数

fstream成员函数见表16.2。

表16.2　fstream成员函数

说明

表中参数c、str为char型，参数n为int型。

通过上面的介绍，读者已经对写入流有了一定的了解，下面就通过使用ifstream和ofstream对象实现读写文件的功能。

【实例16.3】 使用ifstream和ofstream对象实现读写文件的功能 。（实例位置：资源包\TM\sl\16\3）

程序运行结果如图16.3所示。

图16.3　程序运行结果

程序首先使用ofstream类创建并打开test.txt文件，然后需要用户输入5次数据，程序把这5次输入的数据全部写入test.txt文件，接着关闭ofstream类打开的文件，用ifstream类打开文件，将文件中的内容输出。

16.3.2　写文本文件

文本文件是程序开发经常用到的文件，使用记事本程序就可以打开。文本文件以.txt作为扩展名，16.3.1节已经使用ifstream和ofstream类创建并写入了文本文件，本节主要应用fstream写入文本文件。

【实例16.4】 向文本文件写入数据。 （实例位置：资源包\TM\sl\16\4）

程序通过fstream类的构造函数打开文本文件test.txt，然后向文本文件写入了字符串name sex age。

16.3.3　读取文本文件

前面介绍了如何写入文件信息，下面通过实例来介绍如何读取文本文件的内容。

【实例16.5】 读取文本文件内容。 （实例位置：资源包\TM\sl\16\5）

程序打开文本文件test.txt，文件的内容如图16.4所示。

程序读取文本文件test.txt中的内容，并将其输出，程序运行结果如图16.5所示。

图16.4　文本文件内容

图16.5　读取文本文件

16.3.4　二进制文件的读写

文本文件中的数据都是ASCII码，如果要读取图片的内容，就不能使用读取文本文件的方法了。以二进制方式读写文件，需要使用ios::binary模式，下面通过实例来实现这一功能。

【实例16.6】 使用read读取文件。 （实例位置：资源包\TM\sl\16\6）

程序运行结果如图16.6所示。

图16.6　读取文件

程序需要用户输入两次数据，然后通过fstream以二进制方式写入到文件，再通过fstream以二进制方式读取出来并输出。对二进制数据读取需要使用read方法，写入二进制数据需要使用write方法。

说明

cout遇到结束符“\0”就停止输出。在以二进制存储数据的文件中会有很多结束符“\0”，遇到结束符“\0”并不代表数据已经结束。

16.3.5　实现文件复制

用户在进行程序开发时，有时需要用到复制等操作，下面就来介绍复制文件的方法。

【实例16.7】 文件复制。 （实例位置：资源包\TM\sl\16\7）

程序需要用户输入一个文件名，然后使用infile打开文件，接着在文件名后加上“复本”两个字，并用outfile创建该文件，然后通过一个循环将原文件中的内容复制到目标文件内，完成文件的复制。

16.4　文件指针移动操作

在读写文件的过程中，有时用户可能不需要对整个文件进行读写，而是对指定位置的一段数据进行读写操作，这时就需要通过移动文件指针来完成。

16.4.1　文件错误与状态

在I/O流的操作过程中可能出现各种错误，每一个流都有一个状态标志字，以指示是否发生了错误及出现了哪种类型的错误，这种处理技术与格式控制标志字是相同的。ios类定义了以下枚举类型：

对应于标志字各状态位，ios类还提供了以下成员函数来检测或设置流的状态。

为提高程序的可靠性，应在程序中检测I/O流的操作是否正常，例如，用fstream默认方式打开文件时，如果文件不存在，fail方法就能检测到错误发生，然后通过rdstate方法获得文件状态。

16.4.2　文件的追加

在写入文件时，有时用户不会一次性写入全部数据，而是在写入一部分数据后再根据条件向文件中追加写入，以下实例将实现这一功能。

【实例16.8】 文件追加。 （实例位置：资源包\TM\sl\16\8）

程序将字符串Mary girl 20追加到文本文件test.txt中，文本文件test.txt中的内容没有被覆盖。如果test.txt文件不存在，则创建该文件，并写入字符串Mary girl 20。

追加可以使用其他方法实现，例如，先打开文件，然后通过seekp方法将文件指针移到末尾，再向文件中写入数据，整个过程和使用参数取值一样。使用seekp方法实现追加的代码如下：

程序打开test.dat文件，查找文件的末尾，在末尾加入字符串，然后再将文件指针移到文件开始处，输出文件的内容。

16.4.3　文件结尾的判断

在操作文件时，经常需要判断文件是否结束，使用eof方法可以实现。另外也可以通过其他方法来判断，例如，使用流的get方法，如果文件指针指向文件末尾，get方法获取不到数据就返回-1，这也可以作为判断结束的方法，例如：

程序实现输出test.txt文件的内容，同样的功能使用eof方法也可以实现，例如：

程序仍然是输出test.txt文件中的内容，但使用eof方法需要多判断一步。

很多地方需要使用eof方法来判断文件是否已经读取到末尾，下面通过实例来讲述如何使用eof方法判断文件是否结束。

【实例16.9】 记录并输出空格位置。 （实例位置：资源包\TM\sl\16\9）

程序打开文本文件test.txt，文件的内容如图16.7所示。

程序运行结果如图16.8所示。

图16.7　文本文件内容

图16.8　记录并输出空格位置

16.4.4　在指定位置读写文件

要实现在指定位置读写文件的功能，首先要了解文件指针是如何移动的，下面将介绍用于设置文件指针位置的函数。

seekg：位移字节数，相对位置用于输入文件中指针的移动。

seekp：位移字节数，相对位置用于输出文件中指针的移动。

tellg：用于查找输入文件中的文件指针位置。

tellp：用于查找输出文件中的文件指针位置。

位移字节数是移动指针的位移量，相对位置是参照位置，取值如下。

ios::beg：文件头部。

ios::end：文件尾部。

ios::cur：文件指针的当前位置。

例如，seekg(0,ios::beg)是将文件指针移动到相对于文件头0个偏移量的位置，即指针在文件头。

【实例16.10】 输出文件指定位置的内容。 （实例位置：资源包\TM\sl\16\10）

如果用户输入的文件名是test.txt，在test.txt文件中含有字符串www.mingrisoft.com，则程序运行结果如图16.9所示。

图16.9　输出文件指定位置的内容

通过maxpos可以获得文件长度，实例16.10就通过maxpos获得了文件长度，并输出了文件指定位置后的内容。

16.5　文件和流的关联和分离

一个流对象可以在不同时间表示不同文件。在构造一个流对象时，不用将流和文件绑定。使用流对象的open成员函数动态与文件关联，如果要关联其他文件就调用close成员函数关闭当前文件与流的连接，再通过open成员函数建立与其他文件的连接。下面通过实例来实现文件和流的关联和分离功能。

【实例16.11】 文件和流的关联和分离。 （实例位置：资源包\TM\sl\16\11）

程序打开文本文件test.txt，文件的内容如图16.10所示。

图16.10　文件内容

程序运行结果如图16.11所示。

图16.11　程序运行结果

程序需要用户输入文件名，然后使用fstream的open方法打开文件，如果文件不存在，就通过在open方法中指定ios::in| ios::out| ios::trunc参数取值创建该文件，然后向文件中写入数据，接着将文件指针指向开始处，最后输出文件内容。程序在第一次调用open方法打开文件后，如果文件存在，则调用close方法将文件流与文件分离，接着再调用open方法建立文件流与文件的关联。

16.6　删除文件

前文介绍了文件的创建以及文件的读写，本节通过一个具体实例来讲述如何在程序中将一个文件删除。

【实例16.12】 删除文件。 （实例位置：资源包\TM\sl\16\12）

程序通过remove函数将用户输入的文件删除。remove函数是系统提供的函数，可以删除指定的磁盘文件。

16.7　小结

本章主要介绍了使用文件流进行文件操作，文件在打开时可以控制文件是为写打开还是为读打开，控制打开模式可以控制执行效率，掌握文件的随机读取就可以快速读取想要的数据，可以实现文件中数据的修改及插入。另外，本章还介绍了使用一个文件流打开多个文件的实现方法，使读者掌握文件流和文件的区别。

16.8　实践与练习

（1）将一个文件中的字母复制到指定的文件中。 （实例位置：资源包\TM\sl\16\13）

（2）设计一个登录功能，使用文件记录登录用户信息。 （实例位置：资源包\TM\sl\16\14）

第17章

网络通信

（视频讲解： 39分钟）

随着使用网络的群体日益庞大，对网络软件的需求也越来越大，网络通信已成为程序员必须掌握的技术。网络通信技术中涉及的协议和概念很多，包括阻塞函数和非阻塞函数、TCP/IP协议、客户端和服务端等。本章将通过具体实例讲述如何建立socket通信。

通过阅读本章，您可以：

予觥罔绝樨弑

掎揣埽硅逛俣卑讯

予觥妃佗侃甩逛俣凿敲

掎揣逛俣迟控皆廾竖

17.1　TCP/IP协议

17.1.1　OSI参考模型

开放式系统互联（Open System Interconnection, OSI），是国际标准化组织（ISO）为了实现计算机网络的标准化而颁布的参考模型。OSI参考模型采用分层的划分原则，将网络中的数据传输划分为7层，每一层使用下层的服务，并向上层提供服务。表17.1描述了OSI参考模型的结构。

表17.1　OSI参考模型

OSI参考模型的建立不仅创建了通信设备之间的物理通道，还规划了各层之间的功能，为标准化组合和生产厂家定制协议提供了基本原则，它有助于用户了解复杂的协议，如TCP/IP、X.25协议等。用户可以将这些协议与OSI参考模型对比，进而了解这些协议的工作原理。

17.1.2　TCP/IP参考模型

TCP/IP（Transmission Control Protocal/Internet Protocal）即传输控制协议／网际协议，是互联网上最流行的协议，但它并不完全符合OSI的7层参考模型。传统的开放式系统互联参考模型，是一种通信协议的7层抽象的参考模型，其中每一层执行某一特定任务，该模型的目的是使各种硬件在相同的层次上相互通信，这7层是物理层、数据链路层、网路层、传输层、会话层、表示层和应用层。而TCP/IP通信协议采用了4层的层级结构，每一层都呼叫它的下一层所提供的网络来完成自己的需求，这4层分别如下。

应用层：应用程序间沟通的层，如简单电子邮件传输协议（SMTP）、文件传输协议（FTP）、网络远程访问（Telnet）协议等。

传输层：在此层中提供了节点间的数据传送服务，如传输控制协议（TCP）、用户数据包协议（UDP）等，TCP和UDP给数据包加入传输数据，并把它传输到下一层中。这一层负责传送数据，并且确定数据已被送达并接收。

互联网络层：负责提供基本的数据封包传送功能，让每一块数据包都能够到达目的主机（但不检查是否被正确接收），如网际协议（IP）。

网络接口层：对实际的网络媒体的管理，定义如何使用实际网络（如Ethernet、Serial Line等）来传送数据。

17.1.3　IP地址

IP被称为网际协议，Internet上使用的一个关键的底层协议就是IP协议。我们利用一个共同遵守的通信协议，使Internet成为一个允许连接不同类型的计算机和不同操作系统的网络。要使两台计算机彼此之间进行通信，必须使两台计算机使用同一种“语言”。通信协议正像两台计算机交换信息所使用的共同语言，它规定了通信双方在通信中所应共同遵守的规定。

IP协议具有能适应各种各样网络硬件的灵活性，对底层网络硬件几乎没有任何要求，任何一个网络只要可以从一个地点向另一个地点传送二进制数据，就可以使用IP协议加入Internet。

如果希望在Internet上进行交流和通信，则每台连上Internet的计算机都必须遵守IP协议，为此，使用Internet的每台计算机都必须运行IP软件，以便时刻准备发送或接收信息。

IP地址是由IP协议规定的，由32位的二进制数表示。最新的IPv6协议将IP地址升为128位，这使得IP地址更加广泛，能够很好地解决目前IP地址紧缺的情况。但是IPv6协议距离实际应用还有一段距离，目前多数操作系统和应用软件都是以32位的IP地址为基准。

32位的IP地址主要分为两部分：前缀和后缀。前缀表示计算机所属的物理网络，后缀确定该网络上的唯一一台计算机。在互联网上，每一个物理网络都有一个唯一的网络号，根据网络号的不同，可以将IP地址分为5类，即A类、B类、C类、D类和E类。其中，A类、B类和C类属于基本类，D类用于多播发送，E类属于保留类。表17.2描述了各类IP地址的范围。

表17.2　各类IP地址的范围

在上述IP地址中，有几个IP地址是特殊的，有其单独的用途。

网络地址

在IP地址中主机地址为0的表示网络地址，如128.111.0.0。

广播地址

在网络号后所有位全是1的IP地址，表示广播地址。

回送地址

127.0.0.1表示回送地址，用于测试。

17.1.4　数据包格式

TCP/IP协议的每层都会发送不同的数据包，常用的有IP数据包、TCP数据包、UDP数据包和ICMP数据包。

（1）IP数据包

IP数据包是在IP协议间发送的，主要在以太网与网际协议模块之间传输，提供无链接数据包传输。IP协议不保证数据包的发送，但最大限度地发送数据。IP协议结构定义如下：

理论上，IP数据包的最大长度是655535字节，这是由IP首部16位总长度字段所限制的。

（2）TCP数据包

传输控制协议（TCP）是一种提供可靠数据传输的通行协议，它在网际协议模块和TCP模块之间传输，TCP数据包分TCP包头和数据两部分。TCP包头包含了源端口、目的端口、序列号、确认序列号、头部长度、码元比特、窗口、校验和、紧急指针、可选项、填充位和数据区，在发送数据时，应用层的数据传输到传输层，加上TCP的TCP包头，数据就构成了包文。报包是网际层IP的数据，如果再加上IP首部，就构成了IP数据包。TCP包头结构定义如下：

TCP提供了一个完全可靠的、面向连接的、全双工的（包含两个独立且方向相反的连接）流传输服务，允许两个应用程序建立一个连接，并在全双工方向上发送数据，然后终止连接，每一个TCP连接可靠地建立并完善地终止，在终止发生前，所有数据都会被可靠地传送。

TCP比较有名的概念就是3次握手，所谓3次握手指通信双方彼此交换3次信息。3次握手是在数据包丢失、重复和延迟的情况下，确保通信双方信息交换确定性的充分必要条件。

注意

可靠传输服务软件都是面向数据流的。

（3）UDP数据包

用户数据包协议（UDP）是一个面向无连接的协议，采用该协议后，两个应用程序不需要先建立连接，它为应用程序提供一次性的数据传输服务。UDP工作在网际协议模块与UDP模块之间，不提供差错恢复，不能提供数据重传，所以使用UDP的应用程序都比较复杂，如DNS（域名解析服务）应用程序。UDP数据包包头结构如下：

UDP数据包分为伪首部和首部两个部分，伪首部包含原IP地址、目标IP地址、协议字、UDP长度、源端口、目的端口、包文长度、校验和、数据区，是为了计算和检验而设置的。伪首部包含IP首部一些字段，其目的是让UDP两次检查数据是否正确到达目的地。使用UDP时，协议字为17，包文长度包括头部和数据区的总长度，最小8个字节。校验和是以16位为单位，各位求补（首位为符号位）将和相加，然后再求补。现在的大部分系统都默认提供了可读写大于8192字节的UDP数据包（使用这个默认值是因为8192是NFS读写用户数据数的默认值）。因为UDP是无差错控制的，所以发送过程与IP协议类似，即IP分组，然后用ARP来解析物理地址，最后发送。

（4）ICMP数据包

ICMP被称为网际控制包文协议。作为IP协议的附属协议，ICMP用来与其他主机或路由器交换错误包文和其他重要信息，可以将某个设备的故障信息发送到其他设备上。ICMP数据包包头结构如下：

17.2　套接字

所谓套接字，实际上是一个指向传输提供者的句柄。在Winsock中，就是通过操作该句柄来实现网络通信和管理的。根据性质和作用的不同，套接字可以分为3种，即原始套接字、流式套接字和数据包套接字。原始套接字是在Winsock2规范中提出的，它能够使程序开发人员对底层的网络传输机制进行控制，在原始套接字下接收的数据中含有IP头。流式套接字提供了双向、有序、可靠的数据传输服务，该类型套接字在通信前需要双方建立连接，大家熟悉的TCP协议采用的就是流式套接字。与流式套接字对应的是数据包套接字，数据包套接字提供双向的数据流，但是它不能保证数据传输的可靠性、有序性和无重复性，UDP协议采用的就是数据包套接字。

17.2.1　Winsocket套接字

套接字是网络通信的基石，是网络通信的基本构件，最初由加利福尼亚大学Berkeley学院为UNIX开发的网络通信编程接口。为了在Windows操作系统上使用套接字，20世纪90年代初，微软公司和第三方厂商共同制定了一套标准，即Windows socket规范，简称Winsock。1993年1月起，Winsock1.1成为业界的一项标准，它为通用的TCP/IP应用程序提供了超强并灵活的API，但Winsock1.1把API限定在TCP/IP的范畴里，它不像Berkerly模型一样可以支持多种协议，所以Winsock2进行了扩展，开始支持IPX/SPX和DECNet等协议。Winsock2允许多种协议栈的并存，可以使应用程序适用于不同的网络名和网络地址。

17.2.2　Winsocket的使用

Windows系统提供的套接字函数通常封装在ws2_32.dll动态链接库中，其头文件winsock2.h提供了套接字函数的原型，库文件ws2_32.lib提供了ws2_32.dll动态链接库的输出。在使用套接字函数前，用户需要引用winsock2.h头文件，并链接ws2_32.lib库文件，例如：

此外，在使用套接字函数前还需要初始化套接字，可以使用WSAStartup函数来实现，例如：

常用的套接字函数如下：

（1）WSAStartup函数

该函数用于初始化ws2_32.dll动态链接库。在使用套接字函数之前，一定要初始化ws2_32.dll动态链接库。语法如下：

wVersionRequested：表示调用者使用Windows socket的版本，高字节记录修订版本，低字节记录主版本，例如，如果Windows socket的版本为2.1，则高字节记录1，低字节记录2。

lpWSAData：是一个WSADATA结构指针，该结构详细记录了Windows套接字的相关信息，其定义如下：

wVersion：调用者使用的ws2_32.dll动态库的版本号。

wHighVersion: ws2_32.dll支持的最高版本，通常与wVersion相同。

szDescription：套接字的描述信息，通常没有实际意义。

szSystemStatus：系统的配置或状态信息，通常没有实际意义。

iMaxSockets：最多可以打开多少个套接字。在套接字版本2或以后的版本中，该成员将被忽略。

iMaxUdpDg：数据包的最大长度。在套接字版本2或以后的版本中，该成员将被忽略。

lpVendorInfo：套接字的厂商信息。在套接字版本2或以后的版本中，该成员将被忽略。

（2）socket函数

该函数用于创建一个套接字。语法如下：

af：一个地址家族。通常为AF_INET。

type：套接字类型。如果为SOCK_STREAM，表示创建面向连接的流式套接字；如果为SOCK_DGRAM，创建面向无连接的数据包套接字；如果为SOCK_RAW，表示创建原始套接字。对于这些值，用户可以在winsock2.h头文件中找到。

protocol：套接口所用的协议。如果用户不指定，可以设置为0。

返回值：函数返回值是创建的套接字句柄。

（3）bind函数

该函数用于将套接字绑定到指定的端口和地址上。语法如下：

s：套接字标识。

name：一个sockaddr结构指针。该结构中包含了要结合的地址和端口号。

namelen：确定name缓冲区的长度。

返回值：如果函数执行成功，返回值为0，否则为SOCKET_ERROR。

（4）listen函数

该函数用于将套接字设置为监听模式。对于流式套接字，必须处于监听模式才能够接收客户端套接字的连接。语法如下：

s：套接字标识。

backlog：等待连接的最大队列长度。例如，如果backlog被设置为2，此时有3个客户端同时发出连接请求，那么前两个客户端连接会放置在等待队列中，第3个客户端会得到错误信息。

（5）accept函数

该函数用于接受客户端的连接。在流式套接字中，套接字处于监听状态才能接受客户端的连接。语法如下：

s：是一个套接字，它应处于监听状态。

addr：是一个sockaddr_in结构指针，包含一组客户端的端口号、IP地址等信息。

addrlen：用于接收参数addr的长度。

返回值：一个新的套接字，它对应于已经接受的客户端连接，对于该客户端的所有后续操作，都应使用这个新的套接字。

（6）closesocket函数

该函数用于关闭套接字。语法如下：

s：标识一个套接字。如果参数s设置有SO_DONTLINGER选项，则调用该函数后会立即返回，但此时如果有数据尚未传送完毕，会继续传递数据，然后才关闭套接字。

（7）connect函数

该函数用于发送一个连接请求。语法如下：

s：是一个套接字。

name：套接字s想要连接的主机地址和端口号。

namelen：name缓冲区的长度。

返回值：如果函数执行成功，返回值为0，否则为SOCKET_ERROR。用户可以通过WSAGETLASTERROR得到其错误描述。

（8）htons函数

该函数将一个16位的无符号短整型数据由主机排列方式转换为网络排列方式。语法如下：

hostshort：一个主机排列方式的无符号短整型数据。

返回值：是16位的网络排列方式数据。

（9）htonl函数

该函数将一个无符号长整型数据由主机排列方式转换为网络排列方式。语法如下：

hostlong：一个主机排列方式的无符号长整型数据。

返回值：32位的网络排列方式数据。

（10）inet_addr函数

该函数将一个由字符串表示的地址转换为32位的无符号长整型数据。语法如下：

cp：一个IP地址的字符串。

返回值：32位无符号长整数。

（11）recv函数

该函数用于从面向连接的套接字中接收数据。语法如下：

s：一个套接字。

buf：接收数据的缓冲区。

len: buf的长度。

flags：函数的调用方式。如果为MSG_PEEK，表示查看传来的数据，在序列前端的数据会被复制一份到返回缓冲区中，但是这个数据不会从序列中移走。如果为MSG_OOB，表示用来处理Out-Of-Band数据，也就是外带数据。

（12）send函数

该函数用于在面向连接方式的套接字间发送数据。语法如下：

s：一个套接字。

buf：存放要发送数据的缓冲区。

len：缓冲区长度。

flags：函数的调用方式。

（13）select函数

该函数用来检查一个或多个套接字是否处于可读、可写或错误状态。语法如下：

nfds：无实际意义，只是为了和UNIX下的套接字兼容。

readfds：一组被检查可读的套接字。

writefds：一组被检查可写的套接字。

exceptfds：被检查有错误的套接字。

timeout：函数的等待时间。

（14）WSACleanup函数

该函数用于释放为ws2_32.dll动态链接库初始化时分配的资源。语法如下：

（15）WSAAsyncSelect函数

该函数用于将网络中发生的事件关联到窗口的某个消息中。语法如下：

s：一个套接字。

hWnd：接收消息的窗口句柄。

wMsg：窗口接收来自套接字中的消息。

lEvent：网络中发生的事件。

（16）ioctlsocket函数

该函数用于设置套接字的I/O模式。语法如下：

s：待更改I/O模式的套接字。

cmd：对套接字的操作命令。如果为FIONBIO，当argp为0时，表示禁止非阻塞模式；当argp非0时，表示设置非阻塞模式。如果为FIONREAD，表示从套接字中可以读取的数据量。如果为SIOCATMARK，表示所有的外带数据都已被读入。这个命令仅适用于流式套接字，并且该套接字已被设置为可以在线接收外带数据（SO_OOBINLINE）。

argp：命令参数。

以下是Winsock2新增的函数。

WSAAccept：accept函数扩展版本，它支持条件接收和套接口分组。

WSACloseEvent：释放一个事件对象。

WSAConnect：connect函数的扩展版本，它支持连接数据交换和QOS规范。

WSACreateEvent：创建一个事件对象。

WSADuplicateSocket：为一个共享套接口创建一个新的套接口描述字。

WSAEnumNetworkEvents：检查是否有网络事件发生。

WSAEnumProtocols：得到每个可以使用的协议信息。

WSAEventSelect：把网络事件和一个事件对象连接。

WSAGetOverlappedResu：得到重叠操作的完成状态。

WSAGetQOSByName：对于一个传输协议服务名字提供相应的QOS参数。

WSAHtonl：htonl函数的扩展版本。

WSAHtons：htons函数的扩展版本。

WSAIoctl：ioctlsocket函数的允许重叠操作的版本。

WSAJoinLeaf：在多点对话中计入一个叶节点。

WSANtohl：ntohl函数的扩展版本。

WSANtohs：ntohs函数的扩展版本。

WSARecv：recv函数的扩展版本，它支持分散／聚焦I／O和冲抵套接口操作。

WSARecvDisconnect：终止套接口的接收操作。如果套接口是基于连接的，得到拆除数据。

WSARecvFrom：recvfrom函数的扩展版本，它支持分散／聚焦I／O和冲抵套接口操作。

WSAResetEvnet：重新初始化一个数据对象。

WSASend：send函数的扩展版本，它支持分散／聚焦I／O和冲抵套接口操作。

WSASendDisconnect：启动一系列拆除套接口连接的操作，并且可以选择发送拆除数据。

WSASendTo：sendto函数的扩展版本，它支持分散／聚焦I／O和冲抵套接口操作。

WSASetEvent：设置一个数据对象。

WSASocket：socket函数的扩展版本，它以一个PROTOCOL_INFO结构作为输入参数，并且允许创建重叠套接口，它还允许创建套接口组。

WSAWaitFoMultipleEvent：阻塞多个事件对象。

17.2.3　套接字阻塞模式

依据套接字函数执行方式的不同，可以将套接字分为两类，即阻塞套接字和非阻塞套接字。在阻塞套接字中，套接字函数的执行会一直等待，直到函数调用完成才返回。这主要出现在I/O操作过程中，在I/O操作完成之前不会将控制权交给程序。这也意味着在一个线程中同时只能进行一项I/O操作，其后的I/O操作必须等待正在执行的I/O操作完成后才会执行。在非阻塞套接字中，套接字函数的调用会立即返回，将控制权交给程序。默认情况下，套接字为阻塞套接字。为了将套接字设置为非阻塞套接字，需要使用ioctlsocket函数，例如，下面的代码在创建一个套接字后，将套接字设置为非阻塞套接字。

将程序设置成非阻塞套接字后，Winsock通过异步选择函数WSAAsyncSelect来实现非阻塞通信。方法是由该函数指定某种网络事件（如有数据到达、可以发送数据、有程序请求连接等），当被指定的网络事件发生时，由Winsock发送程序事先约定的消息，程序就可以根据这些消息做相应的处理。

17.2.4　字节顺序

有时不同的计算机结构使用不同的字节顺序存储数据，例如，基于Intel的计算机存储数据的顺序与 Macintosh（Motorola）计算机相反。通常，用户不必为在网络上发送和接收数据的字节顺序转换担心，但在有些情况下，必须转换字节顺序，例如，程序中将指定的整数设置为套接字的端口号，在绑定端口号之前，必须将端口号从主机顺序转换为网络顺序。

17.2.5　面向连接流

面向连接流主要指通信双方在通信前先建立连接。建立连接的步骤如下：

创建套接字socket。

将创建的套接字绑定（bind）到本地的地址和端口上。

服务端设置套接字的状态为监听状态（listen），准备接受客户端的连接请求。

服务端接受请求（accept），同时返回得到一个用于连接的新套接字。

使用这个新套接字进行通信（通信函数使用send/recv）。

释放套接字资源（closesocket）。

整个过程分为客户端和服务端，两端连接过程如图17.1所示。

图17.1　面向连接流

17.2.6　面向无连接流

所谓面向无连接流主要指通信双方通信前不需要建立连接，服务端和客户端使用相同的处理过程，如图17.2所示。

图17.2　面向无连接流

17.3　简单协议通信

通过前面的学习，读者可能对使用socket建立通信应用有了一定了解，下面通过具体实例进一步讲述如何使用socket进行通信。实例主要完成一个简单协议的通信过程，使用的是面向连接方式建立的连接，并且是阻塞的方式。实例分为服务端和客户端。

17.3.1　服务端

服务端主要使用多线程技术建立连接，也就是说一个服务端可以连接多个客户端，连接客户端的数据可以进行限定，程序中设置最大连接数为20。当客户端有连接请求发过来时，向客户端发送字符串THIS IS SERVER，并启动一个线程等待客户端发送消息过来。

如果客户端发送字符A过来后，服务端返回B，发送字符C过来后，服务端返回D，发送exit后，服务端关闭线程。

【实例17.1】 服务端。 （实例位置：资源包\TM\sl\17\1）

程序中建立连接的IP只限制在本机，可以通过修改inet_addr("127.0.0.1")表达式的值，来设置需要的IP。

17.3.2　客户端

客户端程序主要完成向服务端发送连接请求，然后由用户输入要发送的字符，发送的字符限定为A、C和exit。

【实例17.2】 客户端。 （实例位置：资源包\TM\sl\17\2）

17.3.3　实例的运行

首先启动服务端，然后启动客户端，在客户端输入字符A，然后输入C，最后输入exit退出客户端，服务端运行如图17.3所示。

客户端运行如图17.4所示。

图17.3　服务端

图17.4　客户端

17.4　小结

本章主要介绍了TCP/IP通信协议、OSI参考模型和Windows系统提供的建立socket通信的函数，可以结合实例了解socket通信函数的具体使用情况。网络技术是一门学科，在使用网络编程之前，应该掌握基础的网络技术，理解TCP/IP协议，为网络编程打好基础。

17.5　实践与练习

（1）设计程序，要求当客户端连接到服务端时，服务器会显示连接的提示信息，并反馈信息给客户。 （实例位置：资源包\TM\sl\17\3）

（2）修改实例17.1和实例17.2，使其程序是基于UCP的网络聊天程序。 （实例位置：资源包\TM\sl\17\4）

第4篇

项目实战

第18章　人事考勤管理系统

为了能巩固前面所学的知识，本篇利用SQL Server 2014数据库和C++相关知识实现了一个完整的项目——人事考勤管理系统。将按照实现的模块，包括主窗体模块、用户登录模块、用户管理模块、部门管理模块、人员信息管理模块、考勤管理模块、考勤汇总查询模块以及开发技术与难点分析进行详细讲解。

第18章

人事考勤管理系统

（视频讲解： 1小时31分钟）

小型企业由于员工不多，员工的出勤管理可能算不上一个问题，但随着企业的不断壮大，员工不断增加，员工的出勤管理就会成为非常大的问题，人事考勤管理系统就是为了解决这个问题而产生的。有了人事考勤管理系统就可以轻松掌握每个员工的出勤状况。

通过阅读本章，您可以：

了解如何使用ADO连接数据库

学习如何利用ADO封装类进行数据操作

了解数据库与程序间日期类型数据的操作

学习如何使用SQL查询语句进行数据表的汇总查询

18.1　开发背景

随着业务的不断发展，某公司的员工数量不断增加，人事考勤方面的管理已成为公司管理中的重要部分。传统的人事考勤制度已不能有效管理员工的出勤状况，所以人事考勤管理系统就成为人事考勤管理的必要选择。

18.2　需求分析

在使用人事考勤管理系统时，用户首先需要输入用户名和密码进入系统，才能对人事考勤管理系统的部门、员工的基本信息进行维护和管理。在考勤管理模块中可录入员工当天的考勤信息，同时可按年、月、员工进行查询，还可通过考勤汇总查询对员工某月的考勤记录进行汇总，计算出员工月工作天数、早退天数、迟到天数等。通过对人事考勤管理过程的研究和分析，要求本系统应该具有以下功能：

用户登录。

部门信息录入。

人员信息管理。

考勤信息录入。

考勤信息汇总。

18.3　系统设计

18.3.1　系统目标

人事考勤管理系统以实现员工日常出勤信息管理为设计目标，加以强大的数据库管理功能，可以方便对考勤信息进行管理，大大提高人事部门的日常工作效率。本系统在设计时应该满足以下几点：

采用人机对话的操作方式，信息查询灵活、方便、快捷、准确，数据存储安全可靠。

对考勤信息的操作简单，可以方便地进行添加、修改和删除。

可以录入员工信息、部门信息。

对员工的考勤信息可按月进行汇总计算。

对用户输入的数据，系统进行严格的数据检验，尽可能排除人为的错误。

系统最大限度地实现了易维护性和易操作性。

系统运行稳定、安全可靠。

18.3.2　系统功能结构

人事考勤管理系统的功能结构图如图18.1所示。

图18.1　系统功能结构图

18.3.3　系统预览

人事考勤管理系统由多个功能模块组成，下面仅列出几个典型的功能模块，其他模块请参见资源包中的源程序。

部门管理模块如图18.2所示，该模块用于管理各部门之间的结构信息；人员信息管理模块如图18.3所示，该模块用于维护员工的基本信息。

图18.2　部门管理模块运行效果

图18.3　人员信息管理模块运行效果

考勤管理模块如图18.4所示，该模块用于记录人事考勤的信息情况；考勤汇总查询模块如图18.5所示，该模块用于对员工的考勤信息进行汇总统计。

图18.4　考勤管理模块运行效果

图18.5　考勤汇总查询模块运行效果

18.3.4　业务流程图

人事考勤管理系统的业务流程图如图18.6所示。

图18.6　人事考勤管理系统的业务流程图

18.3.5　数据库设计

1．数据库分析

人事考勤管理系统使用Microsoft SQL Server 2014数据库来满足系统的要求，数据库名称为tb_person，在数据库中创建4张表用于存储各种不同信息，如图18.7所示。

图18.7　数据库中的表

2．数据库概念设计

根据前面介绍的需求分析和系统设计规划出本系统中使用的数据库实体对象，分别为管理员实体、部门实体、员工实体和考勤实体等。下面将给出以上实体的E-R图。

（1）管理员实体

管理员实体包括编号、管理员姓名、密码信息。管理员实体E-R图如图18.8所示。

（2）部门实体

部门实体包括部门编号、部门名称、备注信息和上级部门编号。部门实体E-R图如图18.9所示。

图18.8　管理员实体E-R图

图18.9　部门实体E-R图

（3）员工实体

员工实体包括自动编号、员工编号、员工姓名、照片、性别、民族和生日等信息。员工实体E-R图如图18.10所示。

（4）考勤实体

考勤实体包括人员姓名、考勤日期、上班时间、下班时间、上班考勤时间和下班考勤时间等信息。考勤实体E-R图如图18.11所示。

图18.10　员工实体E-R图

图18.11　考勤实体E-R图

3．数据库逻辑结构设计

根据实体E-R图创建各数据表，下面给出人事考勤管理系统数据库中主要表的表结构。

（1）tab_User（管理员信息表）

管理员信息表用于保存管理员的信息，如图18.12所示。

（2）tab_Dept（部门信息表）

部门信息表用于记录部门的信息情况，如图18.13所示。

图18.12　tab_User（管理员信息表）

图18.13　tab_Dept（部门信息表）

（3）tab_Employees（员工信息表）

员工信息表用来保存公司员工信息，如图18.14所示。

图18.14　tab_Employees（员工信息表）

（4）tab_Check（考勤信息表）

考勤信息表用来记录员工每天的考勤信息，如图18.15所示。

图18.15　tab_Check（考勤信息表）

18.4　公共模块设计

人事考勤管理系统是使用ADO连接数据库的，为了能更方便地在程序中使用ADO建立数据库连接与数据表的操作，就在公共类中对系统中所使用的ADO操作进行了封装。在系统中建立了ADO的两个公共类—CADOConnection和CADODataSet，这两个类定义在ADO.h头文件中，实现在ADO.cpp文件中。

CADOConnection类是用来连接数据库的，实现了对_Connection接口的封装。CADOConneciton类在头文件中的定义如下：

定义两个全局变量ConCount和g_Connection, ConCount变量是一个整型变量，用来记录在工程中所创建的CADOConnection类的实例个数。在构造方法中，当此变量为0时，调用CoInitialize函数实现OLE的初始化；在析构方法中，当此变量为0时，调用CoUninitialize方法取消OLE的初始化。

GetConnection函数是一个全局函数，用于返回全局数据库连接对象的指针。程序代码如下：

CADOConnection方法是构造函数，用于初始化OLE和创建_Connection接口的指针实例。程序代码如下：

~CADOConnection方法是析构函数，用于取消OLE的初始化和释入_Connection接口指针。程序代码如下：

InitADO方法是一个静态方法，用于初始化OLE。程序代码如下：

UnInitADO方法是一个静态方法，用于取消OLE的初始化。程序代码如下：

Open方法通过指定的数据库连接字符串与SQL数据库建立连接。程序代码如下：

GetSQLConStr方法用来生成与数据库连接所需要的连接字符串。程序代码如下：

GetConnection方法用于返回_Connection接口指针。程序代码如下：

IsOpen方法用来判断当前数据库连接对象与数据库的连接状态。程序代码如下：

CADODataSet类是用来存储数据的数据集类，实现了_Recordset接口的实例。该类在头文件中的定义如下：

CADODataSet方法为记录集实现类的构造方法，在该方法中实现记录集接口对象的创建。程序代码如下：

~CADODataSet类为记录集实现类的析构方法，在该方法中实现记录集的关闭与接口的释放。程序代码如下：

SetConnection方法用来设置记录集所连接的数据库连接类的对象。程序代码如下：

GetRecordCount方法用来获取记录集中数据的数量。程序代码如下：

Open方法通过SQL查询语句打开数据集。程序代码如下：

IsOpen方法用来判断数据集是否处于打开状态。程序代码如下：

GetFields方法用来获取记录集中字段的集合。程序代码如下：

Next方法将记录集指针下移一位。程序代码如下：

AddNew方法用于向记录集中添加一个新行。程序代码如下：

SetFieldValue方法用来向记录集中指定的字段赋值。程序代码如下：

Save方法用来保存对记录集中所做的任何数据更改。程序代码如下：

move方法将记录集的当前指针移动到指定的索引位置。程序代码如下：

GetRecordNo方法用来获取记录集中的当前行号。程序代码如下：

Delete方法用来删除记录集中的当前行。程序代码如下：

18.5　主窗体设计

人事考勤管理系统的主窗体由菜单和客户区域组成，其中，客户区域显示了一幅位图，主窗体运行效果如图18.16所示。

图18.16　人事考勤管理系统的主窗体

主窗体设计步骤如下：

（1）启动Visual C++ 6.0，选择File→New命令，打开New对话框。在对话框左方的工程列表中选择MFC AppWizard[exe]选项，在Project name文本框中输入工程名称，在Location文本框中设置工程保存的路径，如图18.17所示。

图18.17　New对话框

（2）单击OK按钮进入MFC AppWizard-Step1对话框，选中Diglog based选项，如图18.18所示。

图18.18　MFC应用程序向导步骤1

（3）单击Finish按钮完成工程的创建。

（4）在菜单中选择Insert→Resource命令，弹出Insert Resource对话框，如图18.19所示。

图18.19　Insert Resource对话框

（5）选择Menu选项，单击New按钮创建菜单资源，在“资源视图”中双击新创建的菜单资源，编辑菜单资源，程序代码如下：

18.6　用户登录模块设计

18.6.1　用户登录模块概述

用户登录模块是所有管理系统所应具备的基础模块之一，该模块实现了用户使用系统的检验工作，使没有权限的用户不能使用该系统，增加了系统的安全性。用户登录模块如图18.20所示。

图18.20　用户登录模块运行效果

18.6.2　用户登录技术分析

用户登录窗体是整个系统中创建并显示的第一个窗体，所以该窗体应在主窗体创建前创建并显示。在登录窗体创建的同时应该创建数据库连接。这些操作都应在应用程序类的初始化方法中实现，该方法名为InitInstance。程序代码如下：

18.6.3　用户登录实现过程

本模块使用的数据表：tab_User。

（1）创建一个对话框，打开对话框属性窗口，将对话框的ID改为IDD_DLGLOGIN，将对话框标题改为“登录”。

（2）向对话框中添加两个静态文本控件、一个列表框控件、一个编辑框控件和两个按钮控件。分别设置两个静态文本控件的Caption属性为“用户名：”和“密码：”。设置编辑框控件的类型为password。分别设置两个按钮的Caption属性为“确定”和“取消”。

（3）在窗体的初始化方法中创建管理员信息表的数据集，并将用户名添加到列表框控件中。程序代码如下：

（4）在“确定”按钮的事件中实现用户名和密码的验证。程序代码如下：

说明

① Format方法：该方法用于格式化字符串。

② OnOK方法：该方法用于关闭当前窗口。

18.7　用户管理模块设计

18.7.1　用户管理模块概述

用户管理模块实现了对系统登录用户的添加、修改和删除操作。用户管理模块如图18.21所示。

图18.21　用户管理模块运行效果

18.7.2　用户管理技术分析

在用户管理模块中使用列表视图（CListCtrl）控件显示用户信息，当对某一记录进行编辑或删除操作时，必须要获取一个与记录对应的标识，所以在对用户列表进行添加时，利用列表视图控件的SetItemData方法将记录集对应的行号添加到每一行对应的数据中。当对记录进行修改时，就可以通过获取对应的行号对数据集中的数据进行修改了。获取数据时，使用列表视图控件中的GetItemData方法。

（1）SetItemData方法用于设置与指定项相关的32位应用指定的值。

语法如下：

nItem：要设定数据的列表项的索引。

dwData：与项相关联的32位值。

（2）GetItemData方法用于获取与指定项相关的32位应用指定的值。

语法如下：

nItem：要获取数据的列表项的索引值。

18.7.3　用户管理实现过程

本模块使用的数据表：tab_User。

（1）创建一个对话框，打开对话框属性窗口，将对话框的ID改为IDD_DLGUSER，将对话框标题改为“用户管理”。

（2）向对话框中添加一个列表视图控件和4个按钮控件，各控件的属性设置见表18.1。

表18.1　控件属性设置

（3）定义UpdateGrid方法，用来更新列表视图中显示的用户信息。程序代码如下：

说明

① DeleteAllItems方法：该方法用于删除当前列表视图控件中所有的列表项。

② InsertItem方法：该方法用于向列表视图控件中插入列表项。

（4）向对话框中添加OnInitDialog方法，在对话框的初始化方法中添加列表视图控件应显示的列头，并向列表视图控件中添加数据。程序代码如下：

（5）在“添加”按钮的事件中弹出用户编辑窗体，输入用户名后单击“确定”按钮，实现对用户的添加。程序代码如下：

（6）在“修改”按钮的事件中弹出用户编辑窗体，输入用户名后单击“确定”按钮，实现对用户的修改。程序代码如下：

（7）在“删除”按钮的单击事件中获取当前记录进行删除操作。程序代码如下：

18.7.4　单元测试

在测试用户管理模块时，曾出现这样的问题，由于用户在操作中，不小心将所有的用户全部删除了，却没有重新创建新的用户，导致在下次登录时，无法登录。下面来看一下原始的删除代码：

为了解决上述问题，可以设置一个超级用户，该用户为mr，当用户要删除该用户时，提示不能删除，程序代码如下：

18.8　部门管理模块设计

18.8.1　部门管理模块概述

部门管理模块记录了部门间的层次结构和部门信息，所以通常部门管理窗体中对于部门的显示是使用树列表显示的。部门管理模块如图18.22所示。

图18.22　部门管理模块运行效果

18.8.2　部门管理技术分析

由于部门通常都是存在层次级别的，因此在设计数据表结构时应至少创建3个字段，即“编号”“父编号”和“名称”。而在程序中显示部门信息时，也是根据“父编号”作为查询条件不断查找下一级的部门。

在本系统中，由于部门信息通常不会太多，因此可以用嵌套的方式将部门信息一次性读入树列表控件中。程序代码如下：

18.8.3　部门管理实现过程

本模块使用的数据表：tab_Dept。

（1）创建一个对话框，打开对话框属性窗口，将对话框的ID改为IDD_DLGDEPT，将对话框标题改为“部门管理”。

（2）向对话框中添加一个树列表控件、4个按钮控件，各控件的属性设置见表18.2。

表18.2　控件属性设置

（3）定义GetNode方法用来按层级关系获取部门信息表中的所有数据，并添加到树列表控件中。该方法由UpdateDept方法进行调用。程序代码如下：

（4）当单击“添加”按钮时将弹出部门编辑窗体，输入部门信息后单击“确定”按钮，将添加一个新的部门。程序代码如下：

（5）当单击“修改”按钮时将弹出部门编辑窗体，输入部门信息后单击“确定”按钮，将修改部门信息。程序代码如下：

（6）当单击“删除”按钮时将删除当前选中的节点。程序代码如下：

18.9　人员信息管理模块设计

18.9.1　人员信息管理模块概述

人员信息管理模块根据部门分类显示，同时可对人员信息进行维护，人员信息管理模块如图18.23所示。

图18.23　人员信息管理模块运行效果

18.9.2　人员信息管理技术分析

在人员信息管理界面中可以看到，窗体的左侧是部门信息，右侧是人员信息。当选中某一部门信息分类时，右侧的人员信息会根据选中的部门进行人员信息的分类显示。这一操作主要是通过树列表控件中的OnSelchanged事件完成的，当树列表中的选中节点发生改变时就会触发该事件。程序代码如下：

18.9.3　人员信息管理实现过程

本模块使用的数据表：tab_Dept、tab_Employees。

（1）创建一个对话框，打开对话框属性窗口，将对话框的ID改为IDD_DLGPERSON，将对话框标题改为“人员信息管理”。

（2）向对话框中添加两个群组控件、一个树列表控件、一个列表视图控件和4个按钮控件，各控件的属性设置见表18.3。

表18.3　控件属性设置

（3）添加GetNode方法获取部门信息表中的数据信息添加到树列表控件中。该方法由UpdateDept方法调用。程序代码如下：

（4）定义UpdatePerson方法用来更新人员信息，将其显示在列表视图控件中。程序代码如下：

（5）添加OnInitDialog方法，用于初始化人员信息管理窗体中的数据。在该方法中显示部门信息、人员信息。程序代码如下：

说明

① InsertColumn方法：该方法用于向当前列表视图控件中插入列标题。

② SetColumnWidth方法：该方法用于设置列表视图控件的扩展风格。

（6）单击“添加”按钮，弹出人员编辑窗体，输入人员信息后单击“保存”按钮实现人员信息添加。程序代码如下：

（7）单击“修改”按钮，弹出人员编辑窗体，输入人员信息后单击“保存”按钮实现人员信息修改。程序代码如下：

（8）单击“删除”按钮，实现当前选中的人员信息记录删除的操作。程序代码如下：

18.10　考勤管理模块设计

18.10.1　考勤管理模块概述

考勤管理模块将所有人员当天的考勤信息录入该模块中，并且可以根据年、月和人员对已录入的考勤记录进行查询。考勤管理模块如图18.24所示。

图18.24　考勤管理模块运行效果

18.10.2　考勤管理技术分析

在进行程序设计时，日期型数据可以使用字符串的形式存入日期类型的数据库字段中，但字符串类型的日期数据要想转换成日期类型的数据就必须自己实现其转换功能。在该模块中实现了字符串形式的日期和时间分别转换成日期类型的数据。

GetTimeForStr方法用来将字符串形式的时间转换成日期类型。程序代码如下：

GetDateForStr方法用来将字符串类型的日期值转换成日期类型的数据。程序代码如下：

在该模块中还实现了一个时间相减的方法，在这个方法中实现相减都是转换成秒后进行减法计算的，然后再将秒转换成对应的时间类型数据。程序代码如下：

18.10.3　考勤管理实现过程

本模块使用的数据表：tab_Check。

（1）创建一个对话框，打开对话框属性窗口，将对话框的ID改为IDD_DLGCHECK，将对话框标题改为“考勤管理”。

（2）向对话框中添加一个复选框控件、3个静态文本框控件、3个组合框控件、4个按钮控件和一个列表视图控件，各控件的属性设置见表18.4。

表18.4　控件属性设置

（3）添加UpdateList方法，用于更新显示人员的考勤信息。程序代码如下：

（4）向对话框中添加OnInitDialog方法，在对话框初始化时设置列表视图控件的表头和列宽度，以及查询条件选择控件的设置。程序代码如下：

说明

① GetCurrentTime方法：该方法用于获得当前系统时间。

② GetYear方法：该方法用于获得CTime对象中的年数据。

（5）添加OnAdd方法，用于向考勤信息表中添加员工的日考勤数据。程序代码如下：

（6）添加OnEdit方法，用于编辑考勤信息表中员工的日考勤数据。程序代码如下：

（7）添加OnDelete方法，用于删除当前选择的考勤记录。程序代码如下：

18.11　考勤汇总查询模块设计

18.11.1　考勤汇总查询模块概述

考勤汇总查询模块用于将日常录入的员工考勤信息，根据时间范围和人员进行汇总查询，并显示员工的月出勤天数、迟到天数、请假天数等。考勤汇总查询模块如图18.25所示。

图18.25　考勤汇总查询模块运行效果

18.11.2　考勤汇总查询技术分析

在考勤汇总查询模块中，汇总查询是通过SQL语句来实现的，这个汇总查询主要通过一些SQL子句组成一个SQL汇总查询语句。这些子句分别用来获取员工工作总天数、迟到总天数、早退总天数、病假总天数和事假总天数。

在进行天数计算时，是将第一天的考勤时间转换成秒，再计算所使用的总秒数，最后根据总秒数计算出天数。考勤汇总查询的SQL语句如下：

18.11.3　考勤汇总查询实现过程

本模块使用的数据表：tab_Employees、tab_Check。

（1）创建一个对话框，打开对话框属性窗口，将对话框的ID改为IDD_DLGCHECKSUM，将对话框标题改为“考勤汇总查询”。

（2）向对话框中添加3个静态文本框控件、3个组合框控件、一个按钮控件和一个列表视图控件，各控件的属性设置见表18.5。

表18.5　控件属性设置

（3）添加UpdateList方法，用于更新考勤汇总查询的数据。程序代码如下：

（4）向对话框中添加OnInitDialog方法，在对话框初始化时，设置列表视图控件的表头和列宽度，以及汇总查询条件选择控件的设置。程序代码如下：

说明

① _itoa函数：该函数用于将整型数据转换为字符串类型。

② SetCurSel方法：该方法用于设置组合框中的选中项。

18.12　开发技巧与难点分析

18.12.1　调用动态链接库设计界面

为了使人事考勤管理系统在界面上更加美观，笔者特别重绘了程序界面，并且将其封装成了动态链接库，使读者可以方便地使用。下面就来看一下如何调用动态链接库美化界面，步骤如下：

（1）将资源包中提供的SkinHook.h、SkinLib.dll、SkinLib.lib文件复制到程序根目录下，如图18.26所示。

图18.26　复制动态链接库文件

（2）在工作区窗口选择FileView选项卡，右击Header Files节点，在弹出的快捷菜单中选择Add Files to Folder命令，在弹出的对话框中找到SkinHook.h文件并导入。

（3）在Person.cpp文件中引用SkinHook.h文件，并在InitInstance方法中调用动态链接库中的LoadSkin函数进行界面的绘制。

18.12.2　主窗体的界面显示

在开发本系统的主窗体时，为了使程序看起来更加美观，在程序的背景部分绘制了一幅位图，但是在程序进行最大化时，却出现了问题，程序的背景位图没有被重绘，导致左上角的部分显示一个小图，这要怎么解决呢？

可以先捕获最大化消息，然后调用Invalidate函数进行刷新就可以了。上一个问题的思路有了，可是多了一个新的问题，怎样捕获最大化消息，可以在主窗口的OnSize消息处理函数中进行捕获。程序代码如下：

通过上述代码就可以解决调整主窗体大小时背景位图显示不正确的问题了。

18.13　小结

本章通过使用SQL Server 2014数据库向读者介绍了如何开发人事考勤管理系统。通过本章的学习，读者可以更好地掌握SQL Server 2014数据库开发技术，增强对于数据库管理系统开发流程的了解，从而使读者可以向独立开发软件迈出一大步。

附录A

Visual C++ 6.0开发环境是一种主要针对C++语言的开发平台，对该环境的熟悉程度将直接影响到开发人员开发项目的效率。为了加速项目开发进程，开发人员必须更好地了解和适应Visual C++ 6.0开发环境，为此，这里列出了Visual C++ 6.0开发环境中菜单栏的所有菜单命令及功能，以及常用快捷键，以供读者参考。

A1　Visual C++ 6.0菜单命令及功能

Visual C++ 6.0开发环境中菜单栏的全部菜单命令及功能，见附表A.1。

附表A.1　菜单命令及功能

续表

A2　Visual C++ 6.0常用快捷键

在程序开发过程中，合理使用快捷键，不但可以减少代码的错误率，还可以提高开发效率。Visual C++ 6.0提供了很多快捷键，其常用快捷键见附表A.2。

附表A.2　常用快捷键

附录B

Visual Studio 2017开发环境是一种主要针对C++语言的开发平台，对该环境的熟悉程度将直接影响开发人员开发项目的效率。为了加速项目开发进程，开发人员必须更好地了解和适应Visual Studio 2017开发环境，为此，这里详细介绍了Visual Studio 2017的安装方法及开发环境，并且还列出了Visual Studio 2017开发环境中菜单栏的所有菜单命令及功能，以及常用快捷键，以供读者参考。

B1　安装Visual Studio 2017

1．安装Visual Studio 2017的必备条件

安装Visual Studio 2017的系统及硬件要求见附表B.1。

附表B.1　安装Visual Studio 2017所需的必备条件

2．安装Visual Studio 2017

下面以Visual Studio 2017社区版的安装为例讲解具体的安装步骤。

说明

Visual Studio 2017社区版是完全免费的，其下载地址为https://www.visualstudio.com/zh-hans/downloads/。

（1）Visual Studio 2017社区版的安装文件是exe可执行文件，其命名格式为vs_community__编译版本号.exe，笔者在写作本书时，下载的安装文件名为vs_community__1230733315.1531385802.exe的文件，双击该文件开始安装。

说明

安装Visual Studio 2017开发环境时，计算机上要求必须安装了.NET Framework 4.6框架，如果没有安装，请先到微软官方网站下载并安装，下载地址为https://www.microsoft.com/zh-CN/download/details.aspx?id=48130。

（2）程序首先跳转到Visual Studio 2017安装程序界面，在该界面中单击“继续”按钮，自动跳转到安装选择项界面，如图B.1所示，该界面主要将“使用C++的桌面开发”复选框选中，其他的复选框，读者可以根据自己的开发需要确定是否选择安装；选择完要安装的功能后，在下面“位置”处选择要安装的路径，这里建议不要安装在系统盘上，可以选择一个其他磁盘进行安装，例如，这里笔者将其安装到了D盘。设置完成后，单击“安装”按钮。

注意

在安装Visual Studio 2017开发环境时，选择“使用C++的桌面开发”这一选项，可能会与书上的位置不同，读者一定要看清楚再选择。

图B.1　Visual Studio 2017安装界面1

注意

在安装Visual Studio 2017开发环境时，计算机一定要确保处于联网状态，否则无法正常安装。

（3）跳转到如图B.2所示的安装进度界面，该界面显示当前的安装进度，等待安装进度条完成后，自动进入安装完成页，此时就可以在系统的开始菜单中，选择Visual Studio 2017菜单来启动并使用开发环境了。

图B.2　Visual Studio 2017安装界面2

（4）等待安装完成后，也就是进度条为100%后，单击“重启”按钮，完成Visual Studio 2017的安装。然后在Windows的“开始”菜单中找到Visual Studio 2017，如图B.3所示，双击Visual Studio 2017，如果是第一次打开Visual Studio 2017，则会出现如图B.4所示的界面，直接单击“以后再说”按钮。

图B.3　找到Visual Studio 2017

图B.4　欢迎界面

（5）进入到Visual Studio 2017的开发设置界面，如图B.5所示，“开发设置”选择Visual C++，颜色根据自己喜欢的颜色，笔者选了蓝色，最后单击“启动Visual Studio(S)”按钮。

图B.5　开发设置界面

（6）进入Visual Studio 2017的启动界面，等待几秒钟后，进入Visual Studio 2017的起始页界面，如图B.6所示。

图B.6　起始页界面

至此，Visual Studio 2017就安装成功了，并且已经启动了。

B2　Visual Studio 2017开发环境介绍

1．创建项目

初期学习C++语言编程主要在Windows控制台应用程序环境下完成，下面将按步骤介绍控制台应用程序的创建过程。

（1）选择“开始”→“所有程序”→Visual Studio 2017命令，进入Visual Studio 2017的起始页界面，如图B.6所示。

（2）选择“文件”→“新建”→“项目”命令，如图B.7所示，或者按Ctrl+Shift+N键，进入“新建项目”对话框。

图B.7　创建一个新项目

（3）在“新建项目”对话框中选择要创建的文件夹类型，操作的过程如图B.8所示。

图B.8　“新建项目”对话框

首先选择“Windows桌面”选项，这时在右侧列表框中显示可以创建的不同类型文件夹。这里选择 选项，在下侧的“名称”文本框中输入要创建的文件夹名称，如Dome。在“位置”文本框设置文件夹的保存地址，可以通过单击右边的 按钮修改源文件的存储位置。

（4）指定好文件夹的保存地址和名称后，单击“确定”按钮，弹出如图B.9所示的界面，选中“空项目”，取消选中“安全开发生命周期（SDL）检查”，然后单击“确定”按钮。

图B.9　创建应用程序向导

（5）自动跳转到如图B.10所示的界面。

图B.10　创建项目界面

（6）选择“解决方案“Dome”1个项目”中的“源文件”，右击“源文件”，选择“添加”→“新建项”命令，如图B.11所示，或者按Ctrl+Shift+A键，进入“添加新项”对话框。

图B.11　添加新项

（7）完成步骤6就会自动跳转到如图B.12所示的“添加新项”对话框。

图B.12　“添加新项”对话框

添加项目时首先选择Visual C++选项，这时在右侧列表框中显示可以创建的不同文件。因为要创建C++文件，因此这里选择 选项，在下侧的“名称（N）”文本框中输入要创建的C-+文件名称，如Dome.cpp。“位置（L）”文本框是文件夹的保存地址，这里默认是在步骤（3）创建的文件夹位置，不做更改。

（8）单击“添加”按钮，这样就添加了一个C++文件，如图B.13所示。

图B.13　完成添加C++文件

2．菜单栏

菜单栏显示了所有可用的Visual Studio 2017命令，除了“文件”“编辑”“视图”“窗口”和“帮助”菜单之外，还提供编程专用的功能菜单，如“项目”“生成”“调试”“工具”和“测试”等，如图B.14所示。

图B.14　Visual Studio 2017菜单栏

每个菜单项中都包含若干个菜单命令，分别执行不同的操作，例如，“调试”菜单包括调试程序的各种命令，如“开始调试”“开始执行”和“新建断点”等，如图B.15所示。

图B.15　“调试”菜单

3．工具栏

为了操作更方便、快捷，菜单项中常用的命令按功能分组分别放入相应的工具栏中。通过工具栏可以快速访问常用的菜单命令。常用的工具栏有标准工具栏和调试工具栏，下面分别介绍。

（1）标准工具栏包括大多数常用的命令按钮，如新建项目、打开文件、保存、全部保存等。标准工具栏如图B.16所示。

图B.16　Visual Studio 2017标准工具栏

（2）调试工具栏包括对应用程序进行调试的命令按钮，如图B.17所示。

图B.17　Visual Studio 2017调试工具栏

说明

在调试程序或运行程序的过程中，通常可用以下4种快捷键来操作：

（1）按F5键实现调试运行程序；

（2）按Ctrl+F5键实现不调试运行程序；

（3）按F11键实现逐语句调试程序；

（4）按F10键实现逐过程调试程序。

4．解决方案资源管理器

解决方案资源管理器提供了项目及文件的视图，并且提供对项目和文件相关命令的便捷访问，如图B.18所示。与此窗口关联的工具栏提供了适用于列表中突出显示项的常用命令。若要访问解决方案资源管理器，可以选择“视图”→“解决方案资源管理器”命令打开。

图B.18　解决方案资源管理器

5．“选项”对话框

“选项”对话框可以对环境进行设置，选择“工具”→“选项”命令，就能打开如图B.19所示的“选项”对话框。可以在这里设置开发环境主题等其他设置。

图B.19　“选项”对话框

6．错误输出窗口

错误输出窗口为代码中的错误提供了即时的提示和可能的解决方法，例如，当某句代码结束时忘记了输入分号时，错误列表中会显示如图B.20所示的错误。错误列表就好像是一个错误提示器，它可以将程序中的错误代码及时地显示给开发人员，并通过提示信息找到相应的错误代码。

图B.20　错误输出窗口

说明

双击错误列表中的某项，Visual Studio 2017开发环境会自动定位到发生错误的代码。

B3　Visual Studio 2017菜单功能

Visual Studio 2017开发环境中菜单栏的全部菜单命令及功能，见附表B.2。

附表B.2　菜单命令及功能

续表

续表

续表

B4　Visual Studio 2017常用快捷键

在程序开发过程中，合理使用快捷键，不但可以减少代码的错误率，还可以提高开发效率，Visual Studio 2017提供了很多快捷键，其常用快捷键见附表B.3。

附表B.3　常用快捷键

Table of Contents

版权信息

内容简介

如何使用本书开发资源库

前言

本书内容

本书特点

读者对象

读者服务

致读者

资源包“开发资源库”目录

第1大部分 实例资源库

第2大部分 模块资源库

第3大部分 项目资源库

第4大部分 能力测试库

第5大部分 面试系统资源库

第1篇

第1章 绪论

1.1 C++历史背景

1.2 常用开发环境

1.3 认知C++程序代码

1.4 C++工程项目文件

1.5 使用Visual C++ 6.0创建程序

1.6 编译与连接过程

1.7 C++的特点

1.8 小结

第2章 数据类型

2.1 第一个C++程序

2.2 常量及符号

2.3 变量

2.4 常用数据类型

2.5 数据输入与输出

2.6 小结

2.7 实践与练习

第3章 表达式与语句

3.1 运算符

3.2 结合性和优先级

3.3 表达式

3.4 语句

3.5 判断左值与右值

3.6 小结

3.7 实践与练习

第4章 条件判断语句

4.1 决策分支

4.2 判断语句

4.3 使用条件运算符进行判断

4.4 switch语句

4.5 判断语句的嵌套

4.6 小结

4.7 实践与练习

第5章 循环语句

5.1 while循环

5.2 do...while循环

5.3 while与do…while比较

5.4 for循环

5.5 循环控制

5.6 循环嵌套

5.7 循环应用实例

5.8 小结

5.9 实践与练习

第6章 函数

6.1 函数概述

6.2 函数参数及返回值

6.3 函数调用

6.4 变量作用域

6.5 重载函数

6.6 内联函数

6.7 变量的存储类别

6.8 小结

6.9 实践与练习

第7章 数组、指针和引用

7.1 一维数组

7.2 二维数组

7.3 字符数组

7.4 指针

7.5 指针与数组

7.6 指针在函数中的应用

7.7 指针数组

7.8 安全使用指针

7.9 引用

7.10 小结

7.11 实践与练习

第8章 构造数据类型

8.1 结构体

8.2 重命名数据类型

8.3 结构体与函数

8.4 结构体数组

8.5 共用体

8.6 枚举类型

8.7 自定义数据类型

8.8 异常处理

8.9 使用宏定义替换复杂的数据

8.10 小结

8.11 实践与练习

第2篇

第9章 面向对象编程

9.1 面向对象概述

9.2 面向对象与面向过程编程

9.3 统一建模语言

9.4 小结

第10章 类和对象

10.1 C++类

10.2 构造函数

10.3 析构函数

10.4 类成员

10.5 友元

10.6 命名空间

10.7 小结

10.8 实践与练习

第11章 继承与派生

11.1 继承

11.2 重载运算符

11.3 多重继承

11.4 多态

11.5 抽象类

11.6 小结

11.7 实践与练习

第3篇

第12章 模板

12.1 函数模板

12.2 类模板

12.3 模板的使用

12.4 链表类模板

12.5 小结

12.6 实践与练习

第13章 标准模板库

13.1 几种常见数据结构

13.2 序列容器

13.3 结合容器

13.4 算法

13.5 迭代器

13.6 小结

13.7 实践与练习

第14章 RTTI与异常处理

14.1 RTTI

14.2 小结

第15章 程序调试

15.1 选择正确的调试方法

15.2 程序错误常见的4种类型

15.3 调试工具的使用

15.4 调试的基本应用

15.5 调试的高级应用

15.6 小结

第16章 文件操作

16.1 文件流

16.2 文件打开

16.3 文件的读写

16.4 文件指针移动操作

16.5 文件和流的关联和分离

16.6 删除文件

16.7 小结

16.8 实践与练习

第17章 网络通信

17.1 TCP/IP协议

17.2 套接字

17.3 简单协议通信

17.4 小结

17.5 实践与练习

第4篇

第18章 人事考勤管理系统

18.1 开发背景

18.2 需求分析

18.3 系统设计

18.4 公共模块设计

18.5 主窗体设计

18.6 用户登录模块设计

18.7 用户管理模块设计

18.8 部门管理模块设计

18.9 人员信息管理模块设计

18.10 考勤管理模块设计

18.11 考勤汇总查询模块设计

18.12 开发技巧与难点分析

18.13 小结

附录A

A1 Visual C++ 6.0菜单命令及功能

A2 Visual C++ 6.0常用快捷键

附录B

B1 安装Visual Studio 2017

B2 Visual Studio 2017开发环境介绍

B3 Visual Studio 2017菜单功能

B4 Visual Studio 2017常用快捷键

Table of Contents

版权信息

内容简介

如何使用本书开发资源库

前言

本书内容

本书特点

读者对象

读者服务

致读者

资源包“开发资源库”目录

第1大部分 实例资源库

第2大部分 模块资源库

第3大部分 项目资源库

第4大部分 能力测试库

第5大部分 面试系统资源库

第1篇

第1章 绪论

1.1 C++历史背景

1.2 常用开发环境

1.3 认知C++程序代码

1.4 C++工程项目文件

1.5 使用Visual C++ 6.0创建程序

1.6 编译与连接过程

1.7 C++的特点

1.8 小结

第2章 数据类型

2.1 第一个C++程序

2.2 常量及符号

2.3 变量

2.4 常用数据类型

2.5 数据输入与输出

2.6 小结

2.7 实践与练习

第3章 表达式与语句

3.1 运算符

3.2 结合性和优先级

3.3 表达式

3.4 语句

3.5 判断左值与右值

3.6 小结

3.7 实践与练习

第4章 条件判断语句

4.1 决策分支

4.2 判断语句

4.3 使用条件运算符进行判断

4.4 switch语句

4.5 判断语句的嵌套

4.6 小结

4.7 实践与练习

第5章 循环语句

5.1 while循环

5.2 do...while循环

5.3 while与do…while比较

5.4 for循环

5.5 循环控制

5.6 循环嵌套

5.7 循环应用实例

5.8 小结

5.9 实践与练习

第6章 函数

6.1 函数概述

6.2 函数参数及返回值

6.3 函数调用

6.4 变量作用域

6.5 重载函数

6.6 内联函数

6.7 变量的存储类别

6.8 小结

6.9 实践与练习

第7章 数组、指针和引用

7.1 一维数组

7.2 二维数组

7.3 字符数组

7.4 指针

7.5 指针与数组

7.6 指针在函数中的应用

7.7 指针数组

7.8 安全使用指针

7.9 引用

7.10 小结

7.11 实践与练习

第8章 构造数据类型

8.1 结构体

8.2 重命名数据类型

8.3 结构体与函数

8.4 结构体数组

8.5 共用体

8.6 枚举类型

8.7 自定义数据类型

8.8 异常处理

8.9 使用宏定义替换复杂的数据

8.10 小结

8.11 实践与练习

第2篇

第9章 面向对象编程

9.1 面向对象概述

9.2 面向对象与面向过程编程

9.3 统一建模语言

9.4 小结

第10章 类和对象

10.1 C++类

10.2 构造函数

10.3 析构函数

10.4 类成员

10.5 友元

10.6 命名空间

10.7 小结

10.8 实践与练习

第11章 继承与派生

11.1 继承

11.2 重载运算符

11.3 多重继承

11.4 多态

11.5 抽象类

11.6 小结

11.7 实践与练习

第3篇

第12章 模板

12.1 函数模板

12.2 类模板

12.3 模板的使用

12.4 链表类模板

12.5 小结

12.6 实践与练习

第13章 标准模板库

13.1 几种常见数据结构

13.2 序列容器

13.3 结合容器

13.4 算法

13.5 迭代器

13.6 小结

13.7 实践与练习

第14章 RTTI与异常处理

14.1 RTTI

14.2 小结

第15章 程序调试

15.1 选择正确的调试方法

15.2 程序错误常见的4种类型

15.3 调试工具的使用

15.4 调试的基本应用

15.5 调试的高级应用

15.6 小结

第16章 文件操作

16.1 文件流

16.2 文件打开

16.3 文件的读写

16.4 文件指针移动操作

16.5 文件和流的关联和分离

16.6 删除文件

16.7 小结

16.8 实践与练习

第17章 网络通信

17.1 TCP/IP协议

17.2 套接字

17.3 简单协议通信

17.4 小结

17.5 实践与练习

第4篇

第18章 人事考勤管理系统

18.1 开发背景

18.2 需求分析

18.3 系统设计

18.4 公共模块设计

18.5 主窗体设计

18.6 用户登录模块设计

18.7 用户管理模块设计

18.8 部门管理模块设计

18.9 人员信息管理模块设计

18.10 考勤管理模块设计

18.11 考勤汇总查询模块设计

18.12 开发技巧与难点分析

18.13 小结

附录A

A1 Visual C++ 6.0菜单命令及功能

A2 Visual C++ 6.0常用快捷键

附录B

B1 安装Visual Studio 2017

B2 Visual Studio 2017开发环境介绍

B3 Visual Studio 2017菜单功能

B4 Visual Studio 2017常用快捷键