零基础学编程

零基础学C++ 第2版

杨彦强　刘袁红　王浩　著

ISBN：978-7-111-37396-4

本书纸版由机械工业出版社于2012年出版，电子版由华章分社（北京华章图文信息有限公司）全球范围内制作与发行。

版权所有，侵权必究

客服热线：+ 86-10-68995265

客服信箱：service@bbbvip.com

官方网址：www.hzmedia.com.cn

新浪微博 @研发书局

腾讯微博 @yanfabook

目　录

前言

第一篇　C++基础

第1章　C++概述

1.1　了解计算机

1.1.1　计算机硬件

1.1.2　计算机软件

1.2　程序设计语言

1.2.1　机器语言

1.2.2　汇编语言

1.2.3　高级语言

1.2.4　数据结构和算法

1.2.5　面向过程的程序设计（POP）

1.2.6　面向对象的程序设计（OOP）

1.3　C++简介

1.3.1　C++语言发展历程

1.3.2　C++语言特点

1.3.3　C++程序开发基本过程

1.4　C++开发环境的搭建

1.4.1　Visual C++6开发环境简介

1.4.2　开发步骤

1.5　第一个C++程序

1.6　小结

1.7　上机实践习题

第2章　开始C++之旅

2.1　C++程序的结构

2.1.1　注释

2.1.2　编译预处理与新旧标准

2.1.3　主函数

2.1.4　名称空间

2.1.5　C++语素

2.2　变量与基本类型

2.2.1　变量

2.2.2　整型

2.2.3　浮点型

2.2.4　基本字符型

2.2.5　宽字符型

2.2.6　布尔型

2.3　常量

2.3.1　整型常量和浮点型常量

2.3.2　字符型常量

2.3.3　字符串常量

2.3.4　符号常量

2.3.5　枚举常量

2.4　运算符与表达式

2.4.1　算术运算

2.4.2　逻辑运算

2.4.3　短路表达式

2.4.4　关系运算

2.4.5　大有文章：变量是否为“0”

2.4.6　条件运算

2.4.7　位运算

2.4.8　赋值运算

2.4.9　++和--

2.4.10　逗号表达式

2.4.11　sizeof运算符与sizeof表达式

2.4.12　运算符的优先级和结合性

2.5　类型转换

2.5.1　赋值转换

2.5.2　表达式中的转换

2.5.3　强制类型转换

2.5.4　函数调用和传递参数时的类型转换

2.6　流程控制语句

2.6.1　if……else……选择结构

2.6.2　switch结构

2.6.3　if……else……结构和switch结构的比较

2.6.4　for循环结构

2.6.5　for循环结构嵌套

2.6.6　while循环结构

2.6.7　do……while循环结构

2.6.8　循环语句的效率

2.6.9　流程转向控制语句之break

2.6.10　流程转向控制语句之continue

2.6.11　自由转向语句goto

2.6.12　程序中止函数exit（）

2.7　小结

2.8　上机实践习题

第二篇　C++过程开发

第3章　数组和C风格字符串

3.1　什么是数组

3.2　一维数组

3.2.1　一维数组的声明

3.2.2　初始化一维数组

3.2.3　一维数组应用举例

3.2.4　数组操作注意事项

3.3　C风格字符串

3.3.1　C风格字符串的声明

3.3.2　字符数组的cin和cout

3.3.3　get（）函数和getline（）函数

3.3.4　cin与get（）/getline（）函数的搭配问题

3.3.5　访问C风格字符串中的某个元素

3.3.6　C风格字符串处理函数

3.4　多维数组

3.4.1　声明一个多维数组

3.4.2　初始化多维数组

3.4.3　多维数组应用举例

3.4.4　多维数组在内存中是如何排列元素的

3.5　小结

3.6　上机实践习题

第4章　指针和引用

4.1　指针的定义与使用

4.1.1　声明一个指针变量

4.1.2　初始化指针变量

4.1.3　指向指针的指针

4.1.4　指针赋值

4.2　指针的运算

4.2.1　指针与整数的加减

4.2.2　同类型指针间的比较

4.2.3　同类型指针相减

4.3　动态内存分配

4.3.1　使用new动态分配内存

4.3.2　使用delete动态释放及动态申请的内存

4.3.3　使用new申请动态数组

4.3.4　不要使用或释放已经释放的内存块

4.3.5　使用malloc和free动态申请内存

4.3.6　动态内存申请并不一定能成功

4.4　指针和const

4.4.1　禁止改写指针（常量指针或常指针）

4.4.2　禁止改写间接引用

4.4.3　既禁止改写指针，又禁止改写间接引用

4.5　指针与数组

4.5.1　数组名指针

4.5.2　数组元素的指针形式

4.5.3　指向数组的指针

4.5.4　指针数组

4.6　引用

4.6.1　引用的声明

4.6.2　引用的特点

4.6.3　引用的使用限制

4.6.4　其他要说明的问题

4.7　小结

4.8　上机实践习题

第5章　结构、共用体和链表

5.1　结构

5.1.1　如何定义一个结构

5.1.2　结构变量的声明和使用

5.1.3　结构变量的初始化

5.1.4　结构变量间是否可以相互赋值

5.1.5　结构体变量的sizeof

5.2　共用体

5.2.1　共用体的定义

5.2.2　共用体和结构的区别

5.2.3　共用体变量的声明和初始化

5.2.4　共用体使用举例

5.2.5　共用体的sizeof

5.3　结构数组和共用体数组

5.3.1　结构数组的声明和初始化

5.3.2　共用体数组的声明和初始化

5.4　指向结构的指针

5.4.1　声明一个结构指针

5.4.2　结构指针的初始化

5.4.3　使用指针访问结构成员

5.5　链表

5.5.1　链表的结构

5.5.2　创建链表

5.5.3　链表与数组的区别

5.5.4　链表的遍历和查找

5.5.5　链表的插入和删除

5.6　小结

5.7　上机实践习题

第6章　用函数合理组织程序

6.1　模块化带来的好处

6.1.1　函数的调用过程

6.1.2　抽象和封装

6.1.3　实现一个函数

6.2　函数定义

6.2.1　函数头

6.2.2　函数体

6.2.3　函数定义补充说明

6.3　函数声明

6.3.1　为什么要进行函数声明

6.3.2　如何声明一个函数

6.3.3　分割程序文件

6.4　函数调用

6.4.1　形参和实参

6.4.2　参数类型转换

6.4.3　值传递

6.4.4　指针传递

6.4.5　引用传递

6.4.6　对3种传递的补充

6.4.7　默认参数调用

6.4.8　inline函数

6.5　递归

6.6　函数的重载

6.6.1　何时使用函数重载

6.6.2　如何实现函数重载

6.6.3　陷阱：隐式转换导致重载函数出现二义性

6.7　C++如何使用内存

6.7.1　自动存储（栈存储）

6.7.2　静态存储（编译器预分配）

6.8　作用域与可见域

6.8.1　作用域

6.8.2　可见域

6.8.3　函数的作用域和可见域

6.9　小结

6.10　上机实践习题

第7章　关于函数的高级专题

7.1　内存使用错误剖析

7.1.1　内存泄露

7.1.2　野指针

7.1.3　试图修改常量

7.1.4　用错sizeof

7.1.5　内存越界访问

7.1.6　变量的初始化

7.2　重申：函数参数传递和返回机制

7.2.1　参数传递时的“副本”

7.2.2　函数返回时的“副本”

7.3　函数与指针

7.3.1　指向函数的指针

7.3.2　typedef

7.3.3　通过函数指针将函数作为另一个函数的参数

7.3.4　函数指针数组

7.3.5　返回函数指针的函数

7.3.6　带参主函数

7.4　函数与数组

7.4.1　数组名作为函数参数

7.4.2　通过指针得到多于1个的回传值

7.5　函数与结构体、共用体及类对象

7.5.1　3种参数调用

7.5.2　3种返回机制

7.6　函数编写的建议

7.6.1　合理使用const

7.6.2　检查输入参数的有效性

7.6.3　函数返回类型的判断

7.7　小结

7.8　上机实践习题

第三篇　面向对象的C++

第8章　面向对象技术基础

8.1　面向对象基本概念

8.1.1　类的概念

8.1.2　类是分层的

8.1.3　类和对象的关系

8.2　C++类的定义

8.2.1　类定义的基本形式

8.2.2　类定义示例

8.2.3　class和struct

8.3　C++类的实现

8.3.1　在类定义时定义成员函数

8.3.2　在类定义的外部定义成员函数

8.4　C++类的使用

8.4.1　声明一个对象

8.4.2　对象的作用域、可见域和生存期

8.5　对象的创建和撤销

8.5.1　构造函数的作用

8.5.2　构造函数可以有参数

8.5.3　构造函数支持重载

8.5.4　构造函数允许按参数默认方式调用

8.5.5　初始化表达式

8.5.6　析构函数

8.5.7　显式调用析构函数

8.6　复制构造函数

8.6.1　复制构造函数调用机制

8.6.2　默认复制构造函数带来的问题

8.6.3　解决方案—显式定义复制构造函数

8.6.4　关于构造函数和复制构造函数

8.7　特殊数据成员

8.7.1　const数据成员

8.7.2　引用成员

8.7.3　类对象成员

8.7.4　特别说明

8.7.5　static数据成员

8.8　特殊函数成员

8.8.1　静态成员函数

8.8.2　const与成员函数

8.9　对象的组织

8.9.1　const对象

8.9.2　指向对象的指针

8.9.3　对象的大小

8.9.4　this指针

8.9.5　对象数组

8.9.6　对象链表

8.10　为对象动态分配内存

8.10.1　使用new和delete为单个对象分配/释放动态内存

8.10.2　使用new和delete为对象数组分配/释放动态空间

8.10.3　malloc和free能否为对象动态申请内存

8.11　小结

8.12　上机实践习题

第9章　关于对象的高级专题

9.1　类的作用域

9.2　类定义的作用域与可见域

9.3　对象的生存期、作用域和可见域

9.3.1　先定义，后实例化

9.3.2　对象内存的释放与堆内存

9.4　友元

9.4.1　友元的非成员函数

9.4.2　友元的成员函数

9.4.3　友元函数的重载

9.4.4　友元类

9.4.5　友元是否破坏了封装性

9.5　运算符重载

9.5.1　运算符重载规则

9.5.2　以成员函数形式重载运算符

9.5.3　以友元函数形式重载运算符

9.5.4　友元函数形式和成员函数形式的比较

9.5.5　对运算符重载的补充说明

9.6　运算符重载范例

9.6.1　赋值运算符

9.6.2　函数调用运算符

9.6.3　下标运算符

9.7　类型转换

9.7.1　由其他类型向定义类的转换

9.7.2　由自定义类向其他类型的转换

9.7.3　隐式转换带来的二义性

9.8　重载函数选择规则

9.8.1　重载解析

9.8.2　什么是完全匹配

9.9　小结

9.10　上机实践习题

第10章　继承

10.1　什么是继承

10.1.1　简单示例

10.1.2　继承的层次性

10.2　派生类

10.2.1　public派生与private派生

10.2.2　protected成员与protected派生

10.3　多基派生

10.3.1　多基派生的声明和定义

10.3.2　二义性问题

10.3.3　解决方案

10.4　虚基类

10.4.1　共同基类带来的二义性

10.4.2　解决方案

10.4.3　虚基派生二义性与多基派生二义性不同

10.5　派生类的构造函数和析构函数

10.5.1　派生类的构造函数

10.5.2　派生类的析构函数

10.5.3　多基派生类的构造函数和析构函数

10.5.4　虚基派生类的构造函数和析构函数

10.6　分清继承还是组合

10.6.1　继承不是万金油

10.6.2　组合

10.7　基类与派生类对象间的相互转换

10.7.1　类型适应

10.7.2　多基继承时的情况

10.7.3　公共基类

10.7.4　虚基类的情况

10.8　小结

10.9　上机实践习题

第11章　多态

11.1　多态与虚函数

11.1.1　静态联编

11.1.2　动态联编

11.1.3　为什么需要虚函数

11.1.4　虚函数的声明和定义

11.2　虚函数的访问

11.2.1　对象名访问

11.2.2　指针访问

11.2.3　引用访问

11.2.4　类内访问

11.2.5　在构造函数或析构函数中进行访问

11.3　纯虚函数与抽象类

11.3.1　纯虚函数的声明和定义

11.3.2　抽象类

11.3.3　另一种抽象类：类中只定义了protected型的构造函数

11.3.4　延伸：构造函数能否为private型

11.3.5　虚析构函数

11.4　虚函数引入的二义性

11.4.1　多基派生

11.4.2　共同基类和虚继承

11.5　重载、覆盖与隐藏

11.5.1　重载

11.5.2　覆盖

11.5.3　隐藏

11.6　小结

11.7　上机实践习题

第四篇　泛型编程

第12章　模板

12.1　为什么要定义模板

12.1.1　类型参数化

12.1.2　模板的定义

12.2　函数模板

12.2.1　函数模板的使用

12.2.2　隐式实例化

12.2.3　显式实例化

12.2.4　特化

12.2.5　重载

12.2.6　优先级与执行顺序

12.3　类模板

12.3.1　定义类模板

12.3.2　隐式实例化

12.3.3　显式实例化

12.3.4　显式特化

12.3.5　部分特化

12.3.6　重载和优先级

12.4　模板的嵌套

12.4.1　函数成员模板

12.4.2　对象成员模板

12.5　模板参数

12.6　小结

12.7　上机实践习题

第13章　标准模板库

13.1　理解STL

13.1.1　容器

13.1.2　适配器

13.1.3　迭代器

13.1.4　算法

13.2　使用序列式容器

13.2.1　序列式容器的创建和元素的访问

13.2.2　所有容器都支持的特征

13.2.3　序列式容器中元素的插入和删除

13.2.4　vector容器

13.2.5　deque容器

13.2.6　list容器

13.3　使用关联式容器

13.3.1　set容器

13.3.2　multiset容器

13.3.3　map容器

13.3.4　multimap容器

13.4　关联式容器支持的成员函数操作

13.4.1　元素的插入

13.4.2　元素的删除

13.4.3　元素的查找与访问

13.5　迭代器

13.5.1　理解迭代器本质

13.5.2　迭代器类型

13.5.3　为什么要定义这么多迭代器

13.5.4　容器中定义的迭代器类型与5种类型的对应

13.5.5　流迭代器

13.5.6　迭代器失效

13.6　泛型算法

13.6.1　什么是函数对象

13.6.2　算法分类

13.7　适配器

13.7.1　容器适配器

13.7.2　迭代器适配器

13.7.3　函数适配器

13.8　小结

13.9　上机实践习题

第五篇　输入输出处理和编程规范

第14章　输入输出和文件

14.1　输入输出概述

14.1.1　什么是文件

14.1.2　流

14.1.3　缓冲区

14.1.4　重定向

14.1.5　3种输入输出机制

14.2　高层I/O

14.2.1　标准输出函数printf

14.2.2　标准输入函数scanf

14.2.3　扫描集

14.2.4　sprintf函数和sscanf函数

14.2.5　fprintf函数和fscanf函数

14.2.6　文件访问机制

14.3　流类库

14.3.1　流类库更安全、更高效

14.3.2　流类库层次

14.4　输出流

14.4.1　操作符＜＜

14.4.2　其他ostream方法

14.4.3　格式状态字

14.4.4　格式控制值的默认值

14.4.5　flag函数读取和设定格式状态字

14.4.6　使用setf函数和unsetf函数设定格式关键字

14.4.7　设置域宽

14.4.8　填充字符

14.4.9　浮点数输出和显示精度

14.4.10　控制符

14.5　输入流

14.5.1　＞＞操作符

14.5.2　输入流与格式状态字

14.5.3　输入流与域宽

14.5.4　使用get函数读取单个字符

14.5.5　使用get函数和getline函数读取C风格字符串

14.5.6　其他istream方法

14.6　流状态

14.6.1　什么是流状态

14.6.2　读取流状态

14.6.3　管理流状态

14.7　重载＞＞和＜＜

14.7.1　插入符的重载

14.7.2　抽取符的重载

14.8　文件操作

14.8.1　文件操作基本过程

14.8.2　文件的打开

14.8.3　取消文件和流的关联

14.8.4　文件的读写

14.8.5　文本文件的读写

14.8.6　二进制文件的读写

14.8.7　文件定位指针和随机读取

14.9　字符串流

14.9.1　strstream类族

14.9.2　sstream类族

14.10　小结

14.11　上机实践习题

第15章　名称空间

15.1　什么是名称空间

15.1.1　名称空间范例

15.1.2　定义名称空间

15.2　实体的作用域与可见域

15.2.1　实体可见域

15.2.2　可见域的扩展

15.2.3　using声明机制

15.2.4　using声明带来的多重声明问题（二义性）

15.2.5　空间内的屏蔽

15.2.6　先声明，后使用

15.3　名称空间的作用域与可见性

15.3.1　名称空间的定义策略

15.3.2　推荐用法

15.3.3　名称空间嵌套

15.3.4　using编译指令

15.3.5　未命名的名称空间

15.4　对名称空间的思考

15.5　小结

15.6　上机实践习题

第16章　异常和错误

16.1　编码时的防错

16.1.1　调用abort函数或exit函数

16.1.2　返回错误标志

16.2　异常机制

16.2.1　关键字throw

16.2.2　异常处理程序

16.2.3　自定义异常对象

16.2.4　有继承关系的类异常

16.2.5　terminate函数和set_terminate函数

16.2.6　unexpected函数与set_unexpected函数

16.2.7　标准异常

16.2.8　对unexpected函数的补充

16.3　异常发生时的内存管理

16.3.1　堆栈解退

16.3.2　异常处理机制与函数的不同

16.3.3　构造函数中抛出异常

16.3.4　内存泄露

16.3.5　析构函数中可否抛出异常

16.4　auto_ptr类

16.4.1　使用auto_ptr类模板

16.4.2　关于auto_ptr的若干问题

16.5　小结

16.6　上机实践习题

第17章　RTTI和类型转换操作符

17.1　RTTI机制

17.1.1　dynamic_cast操作符

17.1.2　typeinfo类和typeid操作符

17.1.3　补充说明

17.2　类型转换操作符

17.2.1　const_cast操作符

17.2.2　static_cast操作符

17.2.3　reinterpret_cast操作符

17.3　小结

17.4　上机实践习题

第18章　string字符串类

18.1　为什么要使用string类

18.2　声明一个字符串

18.3　字符串的输入输出

18.4　string类功能

18.4.1　string字符串和C风格字符串的转换

18.4.2　赋值和清空

18.4.3　元素删除

18.4.4　元素追加与相加

18.4.5　元素插入

18.4.6　大小和容量

18.4.7　元素存取

18.4.8　字符串比较

18.4.9　提取子串

18.4.10　搜索与查找

18.5　小结

18.6　上机实践习题

第19章　编码风格

19.1　程序的排版

19.1.1　注释

19.1.2　空行的用法

19.1.3　代码行

19.1.4　空格

19.1.5　缩进与对齐

19.1.6　＆和*的位置

19.2　命名规则

19.2.1　Windows风格和Unix风格

19.2.2　如何对程序实体命名

19.3　项目文件的组织

19.3.1　模块划分

19.3.2　头文件

19.3.3　定义文件

19.3.4　目录结构

19.4　小结

19.5　上机实践习题

第20章　程序编译

20.1　程序的编译流程

20.2　预处理

20.2.1　头文件包含

20.2.2　宏

20.2.3　条件编译

20.2.4　#ifdef、#ifndef与重复包含

20.2.5　使用const代替#define定义常量

20.2.6　inline与#define的比较

20.3　VC6调试入门

20.3.1　断点的设置与去除

20.3.2　条件断点

20.3.3　数据断点

20.3.4　消息断点

20.3.5　观察视图（Watch）

20.3.6　内存视图（Memory）

20.3.7　变量视图（Variables）

20.3.8　寄存器视图（Registers）

20.3.9　调用堆栈视图（Call Stack）

20.3.10　反汇编视图（Disassembly）

20.3.11　进程控制

20.4　其他调试手段

20.4.1　Debug模式和Release模式

20.4.2　assert宏

20.4.3　输出字符串

20.5　小结

20.6　上机实践习题

第六篇　面试题精选

第21章　常见面试题

21.1　常见基本知识问答题

面试题1：简述面向对象的3个基本特征

面试题2：局部变量能否和全局变量重名

面试题3：类成员函数的重载、覆盖和隐藏的区别是什么

面试题4：用变量a给出下面的定义

面试题5：在C++中，下面的结构是合法的吗？如果是，其作用是什么

面试题6：在非C++中建类A和B，在哪几种情况下B能隐式转化为A

面试题7：C++中的空类，默认产生哪些类成员函数

面试题8：C++有哪些性质（面向对象特点）

面试题9：子类析构时要调用父类的析构函数吗

面试题10：什么是引用？声明和使用“引用”要注意哪些问题

面试题11：将引用作为函数返回值类型的格式及优点有哪些

面试题12：引用与指针的区别是什么

面试题13：重载和重写（覆盖）的区别

面试题14：多态的作用是什么

面试题15：请指出const与#define相比有何优点

面试题16：C语言中static关键字的具体作用有哪些

面试题17：如何判断程序是由C编译程序还是由C++编译程序编译的

面试题18：关键字const的含义是什么

21.2　世界500强公司的智力问答题

面试题19：工人分金条

面试题20：分蛋糕

面试题21：过桥问题

面试题22：黑白帽子

面试题23：电梯与钻石

面试题24：手电筒传递

面试题25：烧绳问题

面试题26：圆的下水道盖子

面试题27：选建筑师

面试题28：天平分盐

面试题29：分不开的钱

面试题30：小鸟追火车

面试题31：给红色最大的机会

面试题32：镜子中的影像

面试题33：污染的药丸

面试题34：称出不是标准重量的水

面试题35：多彩的果冻

面试题36：电子游戏机

面试题37：监狱里的囚犯

面试题38：灯的状态

面试题39：时针和分针

面试题40：3个奇数

面试题41：屋内的灯

面试题42：找出最重的球

面试题43：盲人分袜子

面试题44：烧香计时

面试题45：平分蜂蜜

面试题46：猜经理女儿的年龄

面试题47：两个圆环

面试题48：喝啤酒

面试题49：算24点

面试题50：聪明人

面试题51：海盗分金币

面试题52：到底是什么牌

面试题53：聪明的老板娘

面试题54：找出不同重量的球

21.3　分析题

面试题55：分析下面的程序段，显示最后的结果

面试题56：分析程序最后的输出结果

面试题57：分析最后输出的结果

面试题58：求下面函数的返回值

面试题59：分析最后输出的结果

面试题60：分析最后输出的结果

21.4　编程题

面试题61：结果输出到文件

面试题62：String类的具体实现

面试题63：链表题，一个链表的节点结构

面试题64：写一个函数并找出在一个整数数组中第二大的数

面试题65：字符串的逆序输出

面试题66：判断操作系统的位数

面试题67：实现对数组的降序排序

面试题68：斐波那契数列1、1、2、3、5等，编写程序求第10项

前言

欢迎进入C++语言的世界，虽然说C++是在C语言的基础上发展而来的，但不同于C语言的面向结构编程机制，C++语言引入了面向对象编程和泛型编程机制，所以，完全可以把C++语言当成一门全新的语言来看，因此，在阅读本书时，并不要求读者有C语言的基础。

C++语言是由贝尔实验室的Stroustrup于20世纪80年代初开发的，可以说C++是一门很年轻的语言，对C++的研究也一度十分活跃，各种新的特性被提出并尝试加入到这门语言中，为了C++的有序发展，ANSI/ISO C++国际标准第1版于1998年正式发布，2003年又发布了C++国际标准第2版，新标准在老标准的基础上补充了很多内容，现今市面上大部分相关书籍，包括网络上的一些学习资料还停留在1998年的老标准上，虽然现有编译器对1998年的标准和2003年的标准都提供了支持，但从效率和未来的发展方向来看，让初学者从开始就接触较新标准，并掌握C++语言的思维方式是非常重要的，本书就是在这样一个背景下编写的。

为了使读者能循序渐进地掌握C++的语法机制和编程思想，笔者精心编写了本书。本书根据读者的一般学习习惯，以循序渐进的方式，从最简单的“Hello,World”程序写起，逐步深化、细化，对书中每个知识和技术要点都给予了详细的程序示例及代码分析，这些示例代码不仅一针见血地指明了技术要点的本质，而且短小精练，方便复制和调试。

本书特色

本书合理控制了知识点的深度和讲解的进度，给读者展示出C++语言全貌，书中所给示例代码条理清晰、简洁且直透本质，读者可以迅速掌握技术要点的内涵。笔者从事多年项目开发，经验丰富，对技术要点进行了深入浅出的阐述。本书面向初、中级读者，以大量的示例进行示范和解说，其特点主要体现在以下几个方面。

❑本书的编排采用循序渐进的方式，每章主题鲜明，要点突出，适合初、中级学者逐步掌握C++的语法规则和编程思想。

❑范例丰富，包含几百个代码示例，关键知识点都辅以示例帮助读者理解。范例程序简洁，不是简单的代码罗列，而是采用短小精练的代码紧扣所讲的技术细节，并配以详细的代码解释和说明，使读者印象鲜明，理解透彻。

❑内容全面，兼顾了C++语言所有的特性。

❑所有示例都十分简洁，具有代表性和理解意义，剪除旁枝末节，揭示技术要点的本质，每个示例集中说明一个概念或要点。

❑对于学习中经常遇到的问题与需要注意的关键点予以特别注释。

❑大量使用了图例、表格等直观的表达方式。

❑本书采用技术要点、详细介绍、示例运行等多种方式进行讲解，系统性强、可用性强，能够给读者留下深刻的印象。

❑精选习题，为了给读者在学习知识之余提供更多的实践机会，每一章都配备了各种类型的习题。习题紧扣本章知识点，突出知识的理解和应用。同时，在习题中配备了上机操作题，结合本章的重点来考查，给读者应用所学知识的机会。

❑紧扣职场，为了给有可能参加职场面试的朋友提供更多的帮助，在本书最后一章还专门提供了常见面试题。在该章中精选了多年来著名IT公司面试题中与C++语言相关的题目，除了提供最佳答案之外，还对题目进行了深入浅出的分析，帮助读者了解题目特点，把握面试重点。

本书内容

本书分为六篇，共21章。

第一篇（第1～2章）是基础知识介绍，涵盖了程序设计语言的基本概念、Visual C++6开发环境的搭建、C++程序的构成、变量、常量、运算符与表达式、数据类型转换以及流程控制语句等内容。

第二篇（第3～7章）介绍的是面向过程的C++程序开发，分别介绍了数组和字符串，指针和引用，以及结构、共用体和链表的使用，详细且视角独到地讲述了函数的使用，详细讨论了变量的类型、生存期、作用域和可见域，总结了在编写函数时经常出现的问题，提供了函数编写的建议。

第三篇（第8～11章）介绍的是面向对象（OOP）的C++程序开发，结合精练的代码讲述了类的设计和使用、多态、虚函数、继承、代码复用、友元等内容，向读者展示了C++面向对象编程的全貌和独特魅力。

第四篇（第12～13章）讨论的是泛型编程，这是由模板来实现的，使数据结构和算法的定义可以脱离开具体类型的限制，据此，C++提供了标准模板库STL，模板和STL是该篇介绍的重点。

第五篇（第14～20章）介绍了文件存储和编程规范方面的内容，文件存储从基本的高层I/O机制讲起，而后介绍了C++流类库的使用、异常与错误机制、RTTI及转换类型符的应用，程序编码风格和C++程序编译相关的内容。

第六篇（第21章）讲解了面试题精选方面的内容，介绍了著名的IT公司关于C++语言程序开发的各种常见面试题，用来帮助读者提高笔试能力，找到满意的工作。

本书读者

本书作为C++语言的基础教程，适合于：

❑C++的初学者

❑想从C语言跨越到C++语言的人员

❑了解C++但所学不全面的人员

❑想了解C++技术及最新进展的其他人员

❑高等院校理科学习C++课程的学生

❑使用C++语言进行毕业设计的学生

❑使用C++进行项目开发的人员

❑供熟悉其他语言的开发人员参考

❑企业和相关单位的培训班学员

❑掌握C++编程技术想通过此找工作的人员

❑拥有很多实践经验的高级读者，可作为参考资料

本书作者

本书主要由杨彦强、刘袁红、王浩编著，其他参与编著和资料整理的人员有冯华君、刘博、刘燕、叶青、张军、张立娟、张艺、彭涛、徐磊、戎伟、朱毅、李佳、李玉涵、杨利润、杨春娇、武鹏、潘中强、王丹、王宁、王西莉、石淑珍、程彩红、邵毅、郑丹丹、郑海平、顾旭光。

作者

2012年1月

第一篇　C++基础

第1章　C++概述

21世纪是个信息爆炸的时代，计算机、信息技术的发展日新月异，极大地改变了我们的沟通方式和生活方式。“足不出户天下知”已成为现实，网上冲浪和聊天早已不是什么新名词，网上炒股、网上办公和网络会议，也因其方便快捷，得到了广泛的应用。撇开网络不谈，计算机在其他方面也早已成为人类不可缺少的工具与伙伴，有了计算机，学生可以事半功倍地完成课程的学习，仓库管理员能更高效地完成工作，家庭主妇能保证收支的平衡，更好地管理财务，这样的例子数不胜数。

计算机极大地改变了我们的生活，这些都是由不同的软件来完成的，这些软件称为计算机程序。有了程序，计算机才能为人类工作，没有软件的计算机纯粹是废铁一堆。可以从商场里或网络上方便地得到各种软件，如用来编辑文章的字处理软件，用来合理组织数据的数据库软件，学习英语的软件，供人娱乐的游戏软件等，这些软件是通过计算机程序设计语言编写的。本书介绍的C++就是一门功能强大的程序设计语言，掌握了C++语言，我们便可以根据需要编写自己的软件。

计算机对大多数人来说已经不是什么新鲜事物了，花大力气讲计算机的详细结构似乎没有必要，但为了读者能对计算机程序开发过程有个总体上的认识，先简要介绍下计算机的组成、原理及计算机语言的一些基本概念。

本章主要涉及以下知识点。

❑计算机组成：介绍计算机组成的相关知识。

❑程序设计语言：介绍程序设计语言的分类。

❑C++程序设计语言：介绍关于C++语言的基础知识。

❑开发环境搭建：通过图片和文字介绍如何搭建Visual C++开发环境。

❑实例讲解：通过一个实例程序让读者认识C++程序设计。

1.1　了解计算机

通俗地讲，计算机是个“超级工具”，是个能执行命令的电子设备。几乎所有的计算机都遵循冯・诺伊曼的“存储—运行”结构，它有以下基本功能：输入、输出、存储、执行算术和逻辑运算。计算机主要由硬件和软件两大部分组成，首先看一下什么是硬件。

注意

硬件相当于人类的身体，而软件相当于人类的思想。

1.1.1　计算机硬件

计算机的硬件主要有中央处理器（Central Processing Unit,CPU）、内存（或称主存、随机处理器、Random Access Memory,RAM）、输入/输出设备（Input/Output Devices）和外部存储设备（Secondary Storage，多数为具备海量存储功能的硬盘），图1.1说明了计算机的硬件结构。

图　1.1　计算机硬件结构图

1.中央处理器

如果把计算机比喻成一个人，中央处理器相当于大脑，是整个计算机的控制中心，中央处理器有以下几部分组成：控制单元（Central Unit,CU）、程序计数器（Program Counter,PC）、指令寄存器（Instruction Register,IR）、算术逻辑单元（Algorithm Logic Unit,ALU）和累加器（Accumulator,ACC），这几个单元相互配合完成程序指令的执行。

❑控制单元有3个主要作用：获取并解释指令，控制数据或指令信息在内存中的读取，控制中央处理器内部各单元的工作。

❑程序计数器用来记录下一步要执行指令的位置。

❑指令寄存器用来暂存当前正在执行的指令。

❑算术逻辑单元执行所有的算术和逻辑运算。

❑累加器用来存储算术逻辑单元的计算结果。

2.内存

程序在执行前都要被装载到内存中，才能被中央处理器执行，以Windows系统为例，若执行安装在硬盘上的某个程序，实际上是将该程序的指令和数据导入内存，供中央处理器执行的过程。

内存是由按顺序编号的一系列存储单元组成的，在内存中，每个存储单元都有唯一的地址，通过地址可以方便地在内存单元中存取信息。内存中的数据要靠供电来维持，当计算机关机或意外断电时，其中的所有数据就永久地消失了。

3.输入/输出设备

输入/输出设备是计算机与人交流的手段，负责读入指令和数据的设备称为输入设备，如键盘和鼠标，负责显示计算结果的设备是输出设备，如显示器和打印机。这里要特别说明的是外部存储器，它既可以将指令和数据送入中央处理器，又可以存储计算结果，所以外部存储器既可以是输入设备，又可以是输出设备。

4.外部存储器

程序的指令和数据要装入内存才能执行，但内存中的数据在断电时或关机后就丢失了，为了长久保持有用的指令和数据，需要一个不依赖于供电、能“永久性”存储数据的设备，我们把它称为外部存储器，典型的外部存储器包括硬盘、软盘、ZIP盘、光盘和磁带等。

1.1.2　计算机软件

软件是能够完成特定功能的程序，软件大体上可以分为两类：系统软件和应用软件。

1.系统软件

系统软件是用来控制计算机，管理计算机上的资源，使计算机的硬件有效发挥作用的程序。在系统启动时装载入内存的程序集合称为操作系统，没有操作系统的计算机什么也干不了，操作系统除了进行内存管理、文件管理和输入输出管理外，一般都提供了很多系统级的服务供用户调用。但不要认为操作系统程序是多么的高深莫测，从本质上讲，它也是用程序设计语言编写出来的。只要掌握了程序设计的原理和硬件系统的相关知识，就可以编写出自己的操作系统。除了操作系统外，设备驱动程序和其他一些管理工具也可以归入系统软件的范畴。

2.应用软件

尽管系统软件能有效地处理计算机的内部功能，协助计算机使用外围设备，但却没有把计算机转化成可以写报告、练打字和看电影等多种用途的机器，想要实现这些功能，需要安装应用软件。应用软件程序面向特定的应用，如Word软件用来处理文字，Excel软件用来记录数据，还有供娱乐的游戏软件等。

注意

对计算机而言，所有的软件都是由程序设计语言编写的，只是应用软件要在操作系统软件的支持下才能运行。

1.2　程序设计语言

语言在人类的交流中的作用无可取代，要想交流通畅，有共同的语言是首要条件，例如，一个中国人想和一个英国人聊天，光靠比划手势是不行的，有两种方法可以选择，要么学习英语，要么找一个翻译。计算机程序设计同样如此，计算机是无生命、无知觉的机器，不懂人类的语言，不管是汉语还是英语，计算机都不能接受，人类要想与其交流，让计算机完成特定的任务，必须解决“语言”的问题。同样，人类和计算机交流同样有两种方法：要么学习机器语言，要么找一个“翻译”。机器语言是什么？“翻译”又是什么？下面将给出解答。

1.2.1　机器语言

计算机是一种电子设备，计算机处理的是数字信号。数字信号采用0和1记录信息，那么计算机的语言，称为机器语言（Machine Language），自然也就是0和1序列。数字0和1称为二进制数据（Binary Digit），或者称为位（Bit），这种0和1的序列被称为二进制代码。

注意

二进制代码只能由0和1组成，没有其他数据。

即便完成相近的功能，不同的计算机的设计者也可能会采用不同的二进制代码集来表示程序指令，换句话说，不同的计算机使用的机器语言并不一定相同，不过可以肯定的是，现代计算机都是以二进制代码的形式存储和处理数据的。

在早期的开发工作中，程序都是用机器语言编写的，为了方便读者理解用机器语言编写指令的过程，假定要执行下列操作：

1+2=？

计算机处理这个简单的运算要经过以下步骤。

❑将1载入累加器。

❑把1暂存到内存的某个地址ADDR1中。

❑将2载入累加器。

❑将内存ADDR1中的数据和累加器中的数据相加。

❑输出结果（一般是输出到内存的某个地址，如ADDR2中）。

假定在所使用的计算机中，装载操作（将数据载入累加器）用二进制代码00000001表示，加法运算用00000010表示，存储操作（对内存某个位置赋值）用00000011表示，ADDR1为1000，ADDR2为1001，如果用机器语言，需要编写如下的指令序列。

00000001 00000001（将1载入累加器）

00000011 1000（把1暂存到内存中地址为1000的单元）

00000001 00000010（将2载入累加器）

00000010 1000（将内存地址为1000的单元中的数据和累加器中的数据相加）

00000011 1001（将运算结果输出到内存中地址为1001的单元中）

可见，为了使用机器语言编写程序，程序员需要记住各种操作的二进制代码，还要记住所有数据在内存中的位置，这种编码方式难度很大，极易出错，编写程序的工作效率很低。

注意

现在程序员已经很少使用机器语言编写程序，而是采用下面介绍的汇编语言。

1.2.2　汇编语言

汇编语言（Assembly Language）的出现简化了程序员的工作，在汇编语言中，用便于记忆的方法定义程序指令，表1.1给出了汇编语言指令与机器语言指令的对应关系。

使用汇编语言计算“1+2=？”，则上面的机器语言指令序列可以改写如下。

LOAD 1

SAVE ADDR1

LOAD 2

ADD ADDR1

SAVE ADDR2

与机器语言相比，使用汇编语言编程要容易很多，但是，计算机不能直接执行由汇编语言写成的程序，必须借助“翻译”将汇编语句序列转化为计算机可以识别的二进制序列，也就是说汇编语言指令要首先转换成机器语言指令的形式，才能被计算机执行。这个“翻译”的名字就叫汇编语言编译器（Assembler）。

不同的计算机可能会采用不同的二进制代码集来表示程序指令，因此，汇编语言和汇编语言编译器也和所用的计算机密切相关。

1.2.3　高级语言

既然机器语言和汇编语言都是计算机可以理解的语言，用其可以完全控制计算机的行为，那么为什么人们还要创造并使用高级程序设计语言呢？因为机器语言和汇编语言都是低级语言，是面向机器的，与具体的计算机相关。学习起来困难，编程效率也低，可读性、可维护性也差。

例如，一个说普通话的北京人想要去欧洲旅行，可是他不懂任何一门欧洲语言；恰好，一个说粤语的广东人也去欧洲旅行，而且，他懂得德语、英语、法语及西班牙语等所有欧洲语言，于是他们结伴同行。北京人想要同欧洲人交流，有两种方式：一是学习各种欧洲语言，然后同各个国家的人直接交流；二是让广东人当翻译，将自己的意思讲给欧洲人听，并将欧洲人的回答反馈给自己。如果您是那个北京人，您会选择哪种方式？我想答案是确定的，肯定要请广东人当翻译。虽然普通话和粤语也有一定的差异，但是比起同欧洲语言的差异来，这种差异小了很多。况且，如果直接同欧洲人交流，需要学习很多种语言。

高级语言和计算机的关系，与北京人的语言和欧洲人的关系相似。高级语言编写的程序借助于编译器就可以在特定的机器上运行，不同的欧洲人相当于不同的计算机，广东人相当于编译器，粤语相当于计算机的高级语言。

高级语言的优点主要有以下两个。

❑高级语言编写的程序是由一系列语句（或函数）组成的，其中每一条语句都对应着几条、几十条甚至上百条机器指令的序列，这样的一条语句，功能显然增强了，所以用它开发程序比用低级语言开发效率高得多。同时，由于高级语言的编写方式更接近人们的思维习惯，这样的程序易读、易懂、易于维护。

❑用高级语言编写的程序具有一定的通用性。同样的一句话，广东人根据听众的不同将其翻译成德语、英语和法语等不同形式，达到交流和沟通的目的。高级语言的一条语句，经由不同的编译器加工编译后，生成针对特定的计算机的二进制代码。低级语言涉及计算机硬件细节，所以不具有通用性。要使用高级语言编写的程序在某一台计算机上运行，只要该计算机提供该语言的编译系统即可。

思考既然高级语言有着低级语言无法比拟的优势，是不是可以完全放弃低级语言呢？回答是否定的。

首先，机器语言是最终操作计算机硬件的语言，任何高级语言程序想要在计算机上执行，首先必须编译成机器指令，也就是编译成二进制代码的形式。

其次，虽然高级语言执行速度比不上同样功能的低级语言，并且在对硬件的操作上，也不如低级语言灵活，但是在如实时控制系统这样对程序运行速度要求高的情况下，或者编写某种新硬件的驱动程序时，仍然会用到低级语言（主要是汇编语言）。

同汇编语言类似，高级语言也需要专门的编译程序（称为编译器或解释器），将其编译成机器语言后，才能运行。

1.2.4　数据结构和算法

计算机程序规定了计算机要执行哪些动作及这些动作应当按什么顺序来执行，如同菜谱规定了厨师做菜的材料和步骤。一个程序主要有以下两个要素。

❑数据结构。即数据的存储形式，程序用到的信息。

❑算法。操作步骤，对操作的描述，程序用什么方法解决问题。

著名的计算机科学家Nikiklaus Wirth提出了一个公式：程序=数据结构+算法。

1.2.5　面向过程的程序设计（POP）

在20世纪60年代计算机发展的初期，计算机和编程是少数聪明人的玩具，程序员可以根据自己的喜好随心所欲地进行程序设计，大多数程序代码组织混乱，可以说只有程序员本人可以看懂，被称为“意大利面条式编程”，随着计算机的发展和程序规模的不断扩大，一大堆的问题凸显出来：程序质量低下，进度缓慢，预算严重超支，这就是“软件危机”。为此，人们提出了结构化程序设计方法，探讨了面向过程编程的3种基本结构，即顺序、分支和循环，大大提高了程序的清晰度和可靠性，在一定程度上缓解了“软件危机”。

注意

顺序、分支和循环是现代程序设计语言的3大基本语法结构。

结构化程序设计方法建立在Bohm、Jacopini证明的结构定理的基础上，结构定理指出：任何程序逻辑都可以用顺序、选择和循环3种基本结构来表示，如图1.2所示。

图　1.2　3种基本结构：顺序、分支和循环

问题并没有被彻底解决，尽管结构化程序设计技术具有这样和那样的优点，它的局限性注定了这种程序设计方法对于规模较小的软件，结构化程序设计是适用的；但当软件规模大到一定程度，这种程序设计方法就显现出了稳定性低、可修改性和可重用性差的弊端。20世纪70年代末到80年代初，面向对象的程序设计方式的出现给人们带来了新的希望。

1.2.6　面向对象的程序设计（OOP）

面向对象的程序与结构化的程序不同，由C++编写的结构化的程序是由一个个的函数组成的，而由C++编写的面向对象的程序是由一个个的对象组成的，对象之间通过消息而相互作用。

在结构化的程序设计中，我们要解决某一个问题，就是要确定这个问题能够分解为哪些函数，数据能够分解为哪些基本的类型，如int、double等。也就是说，思考方式是面向机器结构的，不是面向问题结构的，需要在问题结构和机器结构之间建立联系。面向对象的程序设计方法的思考方式是面向问题的结构，它认为现实世界是由对象组成的。面向对象的程序设计方法解决某个问题，要确定这个问题是由哪些对象组成的，对象间的相互关系是什么。

纯粹的面向对象程序设计方法如下。

❑所有的东西都是对象。可以将对象想象成为一种新型变量，它保存着数据，而且还可以对自身数据进行操作。

❑程序是一大堆对象的组合。通过消息传递，各对象知道自己应该做些什么。如果需要让对象做些事情，则须向该对象“发送一条消息”。具体来说，可以将消息想象成为一个调用请求，它调用的是从属于目标对象的一个方法。

❑每个对象都有自己的存储空间。可容纳其他对象，或者说通过封装现有的对象，可以产生新型对象。因此，尽管对象的概念非常简单，但是经过封装以后却可以在程序中达到任意高的复杂程度。

❑每个对象都属于某个类。根据语法，每个对象都是某个“类”的一个“实例”。一个类的最重要的特征就是“能将什么消息发给它？”，也就是类本身有哪些操作。

1.3　C++简介

C++语言是美国贝尔实验室的Bjarne Stroustrup博士及同事在C语言的基础上，借鉴了Simula语言面向对象的机制，于20世纪80年代初开发出来的一种过程性和对象性结合的程序设计语言，最初称为“带类的C”，1983年Rick Mascitti将其更名为一个有双关语的符号—“C++”。

1.3.1　C++语言发展历程

最初，Stroustrup使用了一个C++到C的编译器程序，将C++源代码翻译成C代码，然后通过标准的C编译器将其转化成二进制代码，随着C++的日渐普及，才出现了独立的C++编译器，直接将C++代码编译成目标代码，这种方式，突出了C++“由C语言而来，却不同于C语言”的独立地位。

C++灵活而强大的功能，很快就受到了计算机软件商的青睐，并不断对C++进行细化，促进了C++的发展。虽然多数公司和科研单位都希望自己的C++版本能和其他版本兼容，但如果没有一个标准的出台，这将很难做到。为此，美国国家标准局在1990年设立了一个委员会（ANSI X3J16），专门负责C++标准的制定。ANSI/ISO C++国际标准第一版于1998年正式发布，2003年又发布了C++国际标准第2版，新的标准是对第1版的整理—修订错误，减少歧义等，并没有改变语言特性，本书所有介绍和实例代码都是基于新的C++国际标准的。当然，C++还在不断地发展中，下一版的C++标准制定工作已经开始，该版本被称为C++0X，预期完工时间为2009年前后。

遵循ANSI/ISO C++标准编写的C++程序具有很好的可移植性，换言之，使C++程序的设计和编码不再拘泥于不同的操作系统，在Windows下编译无误的代码，在UNIX下同样可以顺利编译并运行。

1.3.2　C++语言特点

C++语言是以C语言为基础扩充、发展起来的一种优秀的通用程序设计语言，其保存了C语言的紧凑、灵活、高效和移植性好的特点，又吸收了其他程序设计语言的优秀特性，从Simula中吸收了类的机制，从Algol中吸收了运算符重载、引用和在局部的任何地方声明变量，综合了Ada的类属和异常处理机制。

C++语言是C语言的一个超集，它是一门混合型的语言，既支持传统的结构化程序设计，又支持面向对象的程序设计，这是C++语言成功流行的一个重要原因。

读者可能会产生疑问，既然面向对象的程序设计方法比结构化的程序设计方法先进许多，为什么C++语言仍旧支持后者，而不愿意成为一门纯粹的面向对象的程序设计语言呢？

事实上，程序员们长期采用结构化的程序设计方法，积累了许多宝贵的经验，而且结构化程序设计方法在小型软件项目的开发设计上仍然很适用，完全否定这种设计方法也是不恰当的。C++作为一门混合型语言，在增加了对于面向对象方法的支持的同时，还继承了传统程序设计语言C的优点，克服了其不足之处，使得自身既适用于结构化程序设计，又能满足面向对象程序设计的要求，因此符合广大程序员逐步更新其程序设计观念和方法的要求，从而很快流行起来。

说明

在学习C++前，是否需要学习C语言呢？

答案是不需要，把C++当成一门新的语言来学习，本书完全适用于没有C语言基础的读者，甚至是没有学习过编程、没有写过一行代码的读者。本书致力于培养读者用C++的思维方式去解决问题的能力，欢迎走进C++的世界。

1.3.3　C++程序开发基本过程

下面开始我们的编程之旅，开发一个C++程序，首先要建立问题的模型，根据具体问题的特点，选择过程模型或者对象模型，并将模型实现为源程序。

同其他高级语言一样，要想得到可以执行的C++程序，必须对C++源程序进行编译和链接，该过程如图1.3所示，大体有以下几个步骤。

❑编辑（Edit）：使用文本编辑工具编写C++程序，其文件后缀为.cpp，这种形式的程序称为源代码（Source Code）。

❑编译（Compile）：用编译器将源代码转换成主机使用的内部语言—二进制形式的机器语言，文件后缀为.obj，这种形式的程序称为目标代码（Objective Code）。

❑链接（Link）：将若干目标代码和现有的二进制代码库经过链接器连接，产生可执行代码（Executable Code），文件后缀为.exe。

说明

在不同的计算机平台（如Windows,UNIX,Linux等）上，C++源文件和目标文件代码的后缀名可能会有所不同，不过都要遵循编辑、编译和链接3个步骤以生成可执行文件，本书以使用范围最广的Windows操作系统为示例平台。

图　1.3　C++程序编译链接过程

1.4　C++开发环境的搭建

C++的流行使得许多软件厂商都提供了自己的C++集成开发环境，称为C++IDE。著名的有Borland公司的C++Builder（BCB），Microsoft公司的Visual C++（VC）等。所谓集成开发环境，即在同一个环境下，能够完成C++源程序的编写、连编、运行和调试。

说明

对初学者而言，不要被VC、BCB、BC（Borland C++）、MC（Microsoft C++）和TC（Turbo C++）等词汇所迷惑，它们都是集成开发环境，而我们要学的是一门语言。

本文以Windows操作系统下流行的C++集成开发环境Visual C++6（VC6）+sp6为示例开发环境，所有示例代码都在（Windows XP+VC6/Windows 2000+VC6）下编译通过。

1.4.1　Visual C++6开发环境简介

Visual C++6集成开发环境，被划分成4个主要区域：菜单和工具栏、工作区窗口、代码编辑窗口和输出窗口，如图1.4所示。

图　1.4　Visual C++6集成开发环境主界面

1.4.2　开发步骤

一般情况下，开发一个应用程序按照如下步骤来进行。

（1）建立一个工程。使用Visual C++6集成开发环境进行C++程序开发，必须为程序创建一个工程（Project），并将组成程序的一个或多个文件加入工程中，工程文件名后缀为dsp（保存工程设置），维护应用程序中所有的源代码文件，以及Visual C++如何编译、连接应用程序，以便创建可执行程序。Visual C++6的集成开发环境中，可通过“File”菜单中的“New”命令创建一个新的工程。创建一个工程的同时，也创建了一个工作区（Workspace），工作区文件的后缀名为dsw（保存项目工作区的设置）。如图1.5所示，一个工作区可包含多个工程，每个工程对应一个可执行程序。

图　1.5　工作区与工程关系示意图

执行“File”菜单中的“New……”命令后，出现如图1.6所示的对话框。单击“Projects”选项卡，左侧列出了供选择的工程类型，就本书中的示例程序而言，应当选择“Win32 Console Application”。

在右侧“Project name：”文本框中输入要建立的工程名，在“Location：”文本框中可手动输入或通过按钮“……”选择工程文件的存放位置。选择右侧中部的“Create new workspace”单选按钮建立一个新的工作区。因为编译完成的可执行文件运行在Windows平台上，“Platforms：”中的“Win32”复选框是默认选中的，无须更改。

说明

本书讲解的是C++语言，示例代码都是通用的，与具体的系统环境关系不大，如无特别说明，本书示例代码中所有工程都是“Win32 Console Application”类型。

单击“OK”按钮进入下一步，如图1.7所示。

图　1.6　使用“File”|“New……”命令创建工程

图　1.7　建立空工程

选择“An empty project”选项，然后单击“Finish”按钮，则工程建立完毕。

（2）向工程中添加源代码文件。源代码文件一般由以下类型文件组成。

❑头文件，也称为include文件。

❑源文件，扩展名为cpp。

向工程中添加源文件方法如下。

❑创建新的源代码文件，并将其添加到工程中去。选择“File”菜单中的“New……”命令，单击“Files”选项卡，单击“C++Source File”（参见图1.8）。选中“Add To Project”复选框，在“File”文本框中键入文件名（如main.cpp）。可以在“Location：”文本框中为要创建的文件指定目录，或直接采用当前目录，然后单击“OK”按钮即可完成源文件的创建。按照同样的方式，选择“C/C++Header File”可创建新的头文件。

❑添加一个已存在的源代码文件和资源文件到工程中。选择“Project”菜单中的“Add To Project”命令，再选“Files”命令，在弹出的“Insert Files Into Project”对话框中选择要添加的文件，单击“OK”按钮即可。

技巧在添加已存在的源代码文件和资源文件到工程中时，按住“Shift”或“Ctrl”键可同时选择多个要添加的文件。

从工程中删除一个文件。打开“FileView”文件视图，选择要删除的文件，按下“Del”键即可。这仅是将文件从项目中移去，并非真正地从硬盘中把文件删除。

（3）编辑源代码。代码编辑窗口中对建立的源代码文件进行编辑，在“FileView”文件视图中双击文件名即可打开右侧的源代码编辑器，对其进行编辑。

（4）项目配置。在开发应用程序时，一般将项目设置为Debug模式。在该模式中，编译器将Visual C++Debug所需的调试信息一同编译。当程序调试完毕准备运行时，将项目设置为Release模式。选择“Build”菜单中的“Set Active configurations”命令，在弹出的“Set Active Project Configuration”对话框中进行选择即可实现Debug模式和Release模式间的相互切换。

图　1.8　新建源代码文件

（5）编译链接。选择“Build”菜单中的“Build工程名”命令，或直接按F7键即可实现对整个工程所有源代码文件的编译和链接。编译链接无误即可生成一个后缀为exe的可执行文件。如果程序违反了语言规则，编译器将生成错误信息，指出所在的行，并在输出窗口中显示出来，可以通过单击输出窗口中的错误信息在代码编辑窗口中迅速定位出错位置。

提示理解错误提示的意义很重要，有时，真正的问题或错误可能在标识出的错行之前，并且，一个错误可能引发一串的错误消息，因此，改正错误时，应首先改正第一个消息。

（6）运行应用程序。从“Build”菜单中选择“Execute工程名.exe”，或者按“Ctrl+F5”键，或用鼠标左键单击按钮便可运行该应用程序。

以上是用Visual C++6创建一个工程的简单过程，实际上，Visual C++6功能强大，能做的还有很多，随着我们C++编程水平的提高，会对其有更深刻的理解和认识，也能更好地运用这个工具。

1.5　第一个C++程序

在屏幕上显示Hello World，几乎是所有编程教科书在开始时所要编写的一个程序，那就让这个最简单的程序带我们走进C++的圣殿吧。

按照1.4节的介绍建立工程，添加源代码文件HelloWorld.cpp，见代码1.1所示。

代码1.1　第一个C++程序：HelloWorld

＜----------------------------文件名：example101.cpp--------------------------＞

01　#include＜iostream＞//编译预处理：头文件

02　int main（）//主函数

03　{

04　using namespace std；//using编译机制，引入名称空间

05　cout＜＜"Hello,World\n"；//输出“Hello,World”，'\n'代表换行

06　return 0；//返回

07　}

【代码解析】代码第5行是关键，调用输出对象cout，在屏幕上显示“Hello World”字符串。

注意

如果读者没有使用VC6，或所用的编译器较老，可能需要使用“#include＜iostream.h＞”代替“#include＜iostream＞”，同时去掉代码行“using namespace std；”。

编译链接并运行该程序，出现如图1.9所示窗口。怎么样，看着自己编译出来的第一个C++程序，是不是很有成就感呢？关于上面一段程序的意义，以及C++程序的结构和语法，在以后的章节中将会有更加详细的介绍，本书的写作初衷，就是带领读者一步一步徜徉在博大精深的C++世界中。

图　1.9　第一个C++程序：Hello,World输出

1.6　小结

本章主要讲述计算机和程序设计的基本概念、方法，并简单介绍了C++语言的特点以及Visual C++集成开发环境的使用。

面向过程的结构化程序设计和面向对象的程序设计是两种主要的程序设计方法。结构化程序设计主张只采用顺序、循环和选择3种基本的程序结构以及自顶向下逐步求精的设计方法，实现单入口单出口的结构化程序；面向对象的程序设计主张按人们通常的思维方式建立问题区域的模型，对象、消息、类和方法是为实现这一目标而引入的基本概念。与结构化程序设计相比较，面向对象的程序设计具有更多的优点，适合开发大规模的软件工程项目。

C++语言是当今最流行的高级程序设计语言之一，它既支持结构化的程序设计方法，又支持面向对象的程序设计方法。使用Microsoft Visual C++提供的集成开发环境，程序员可以轻松地完成C++工程的创建、编译、调试和运行。

1.7　上机实践习题

修改一下代码1.1的输出内容，并查看输出结果。

【提示】上述题目主要是要求读者认识一下C++语言，重点是掌握输出字符技术。

【关键代码】

cout＜＜"My Name is Computer！"＜＜endl

第2章　开始C++之旅

古语说，“欲速则不达”，不论盖房还是学知识，打牢基础十分重要。没有坚实的地基，摩天大楼就无法耸立，学习C++同样需要深入掌握基础知识和基本结构，由浅入深地把握全局，下面就从C++大厦的基本框架和一砖一瓦说起。

本章主要涉及以下知识点。

❑C++程序的结构：介绍C++程序中注释、主函数及名称空间。

❑变量与基本数据类型：介绍C++程序中的各种变量及基本数据类型。

❑常量：介绍C++中如何定义常量及其类型。

❑运算符与表达式：介绍如何定义与使用C++中的基本运算符与表达式。

❑类型转换：讲解如何将C++中的基本数据类型进行转换及其注意事项。

❑流程控制语句：讲解如何使用C++中的用于控制程序流程的基本语句。

2.1　C++程序的结构

来看下面这个两数相乘的程序，计算机首先要求用户输入两个整数，而后计算两数的乘积并输出在屏幕上，程序如代码2.1所示。

代码2.1　两数相乘MultiplyTwoNumber

＜-------------------------文件名：example201.cpp-----------------------------＞

01　#include＜iostream＞//编译预处理

02　int main（）//主函数

03　{

04　using namespace std；//名称空间编译指令

05　/*

06　声明3个代表整数的符号，num1和num2为乘数和被乘数，

07　resultNum为乘积，它们的初值都为0

08　*/

09　int num1=0，num2=0，resultNum=0；

10　cout＜＜"请输入要相乘的两个整数，用空格键分开："；//输出提示语句

11　cin＞＞num1＞＞num2；//接收用户输入

12　resultNum=num1*num2；//乘法运算

13　cout＜＜"计算结果为"＜＜resultNum；//输入计算结果

14　cout＜＜endl；//输出一个空行

15　return 0；//main（）函数返回

16　}

【代码解析】代码第12行是关键，其是使用算术运算符（*乘法）来求解两个数的乘积，并将结果保存到resultNum中。

编译运行，输出结果如下所示。

请输入要相乘的两个整数，用空格键分开：5 6（键盘输入）

计算结果为30

每个C++程序由注释、编译预处理和程序主体3个部分组成，下面分别进行讨论。

2.1.1　注释

C++支持两种风格的注释。

❑C++风格：以//打头，作用范围为一行。

❑C风格：以/*开始，以*/结束，两个符号间的内容都会被注释掉，因此可以跨越多行。使用时，应注意/*和*/的正确配对，C风格注释不支持嵌套。

注释是程序员为读者提供的说明，是提高程序可读性的一种手段。注释仅供他人阅读程序时使用，是程序的可选部分，C++编译器忽略所有的注释，将其视为空白。

提示应养成在编码同时随手写注释的好习惯，程序越复杂，就越有必要写注释，注释不仅仅方便他人阅读代码，也有助于程序员的总结、检查与回顾。

2.1.2　编译预处理与新旧标准

以符号#开头的行，称为编译预处理行，代码2.1中使用了#include编译指令，其作用是在编译之前将iostream文件的内容添加到程序中，iostream设置了C++的输入输出环境，如cin和cout是在iostream中定义的C++标准输入、输出设备标识符，endl是iostream中定义的换行符。关于编译预处理的详细介绍请参考第19章。

注意

使用cin和cout进行输入和输出时，程序中必须包含iostream文件（对旧式编译器，程序中应包含iostream.h文件）。

前面已经讲过，C++语言是C语言的一个超集，早期的C和C++的头文件都采用扩展名为.h形式，随着C++的发展，关于头文件的标准也在不断地变化。图2.1给出了新旧标准的对比，C++一栏中灰色区域代表纯粹的C++头文件，白色区域代表C头文件，现在，对老式的C文件头文件保留了.h扩展名（C++程序仍可使用这种文件）；而C++的头文件去掉了扩展名。有些C头文件（如图2.1中的X2.h）被转换成C++头文件，去掉了扩展名，并在文件名称前加上了前缀C（表明来自C语言），对应于图2.1中的网格状区域（CX2）。较旧的编译器可能只支持旧标准格式，符合ANSI/ISO C++最新标准的编译器既支持旧标准格式，又可以使用新的头文件格式，对纯粹的C++头文件来说（如iostream），去掉扩展名.h并不仅仅是形式的变化，没有.h的头文件使用了名称空间机制。

2.1.3　主函数

C++用函数组织过程，每个相对独立的过程都可组织成一个函数，程序一般由不同的函数按层次结构组织而成，因此，函数的定义是C++的主体，这里可先对函数有个大体的了解，详细的介绍和说明请参考第6章。

图　2.1　头文件新旧标准变化对比图

函数由函数头和函数体两部分组成，其基本结构如下代码所示。

返回值类型函数名（参数列表）

{

语句1；

语句2；

……

语句N；

return返回值；

}

通常，C++函数被其他函数调用（激活），第一行“返回值类型函数名（参数列表）”称为函数头，定义了函数和调用其函数之间的接口，返回类型定义了从函数返回给调用函数的信息，参数列表描述的是从调用函数传递给被调用函数的信息。花括号之间的部分称为函数体，说明了函数应当执行的计算机指令，在C++中，一条完整的指令称为语句（Statement），每个语句都以分号结尾。

注意

忘掉语句结尾的分号是初学者常犯的错误，时刻提醒自己，不要省略分号。

花括号中的最后一条语句叫做返回语句（Return Statement），C++中用其标志着一个函数的结束。

本例中只有一个main（）函数，称为主函数，该函数会自动被启动代码调用，而启动代码是在编译阶段由编译器添加到可执行文件中的，是程序与操作系统的桥梁。因此，main（）函数是C++程序的入口，每个C++程序有且仅有一个main（）函数，main函数在文件中的位置并没有特别的要求，它可以在文件的头部、中部或尾部，其基本形式如下所示。

int main（）

{

……//语句序列

return 0；

}

main（）函数的函数头表明：main（）函数可以给操作系统返回一个int类型的值，且没有输入参数。实际上，main（）函数可以接受操作系统传来的命令行参数，关于带参主函数与命令行参数的问题，在第7章中会作详细的介绍。

说明

有些参考书中使用void main（）这样的函数头，并省略了返回语句，这在逻辑上是没有问题的，void意味着函数不返回任何值，但这样的写法不符合ANSI/ISO C++标准格式，在某些系统中可能无法正常运行，因此，建议使用符合标准的int main（）函数头格式。有时读者也会碰到main（）这样的函数头，采用这种定义方式时，当编译器到达main（）函数的末尾而没有遇到返回语句时，则认为main（）函数以“return 0；”结尾，这种写法也符合ANSI/ISO C++标准。

2.1.4　名称空间

在代码2.1中，使用iostream头文件，应使用名称空间编译指令“using namespace std；”使得定义cout和cin对程序可见，这叫做using编译指令，这里，先对名称空间作出简要的说明，详细的介绍请参考第15章。

在使用计算机软件厂商提供的源代码时，经常会遇到函数重名或变量重名的情况，编译器不知道该使用哪个版本，人为对函数和变量进行改名也不现实，为了解决这一问题，新的ANSI/ISO C++标准引入了名称空间这一特性。允许厂商将其产品封装在一个叫名称空间的单元中，使用名称空间来对函数和变量进行管理，编译器也因此可以决定使用哪个版本。如A公司和B公司提供的源代码中都有C（）函数，并将各自的代码都定义在名称空间NamespaceA和NamespaceB中，使用下面的调用方式可以将两个版本无误地区分开。

NamespaceA：C（）；//调用A公司提供的C函数

NamespaceB：C（）；//调用B公司提供的C函数

C++标准库中提供的函数和变量都放置在名称空间std中，在iostream中定义的cin、cout和endl实际上是std：cin、std：cout和std：endl，在代码2.1中，使用了using编译指令（“using namespace std；”），使得std名称空间中的所有名称都可用，也就可以省略掉前缀“std：”。换言之，代码2.2与代码2.1是等价的。

代码2.2　两数相乘的另一种写法AnotherMultiply2Number

＜-------------------------------文件名：example202.cpp-----------------------＞

01　#include＜iostream＞//编译预处理

02　int main（）//主函数

03　{

04　int num1=0，num2=0，resultNum=0；

05　std：cout＜＜"请输入要相乘的两个整数，用空格键分开："；//输出提示语句

06　std：cin＞＞num1＞＞num2；//接收用户输入

07　resultNum=num1*num2；//乘法运算

08　std：cout＜＜"计算结果为"＜＜resultNum；//输入计算结果

09　std：cout＜＜std：endl；//输出一个空行

10　return 0；//main（）函数返回

11　}

【代码解析】代码第5行就是调用名称空间“std”中的cout对象来输出提示。

使用“using namespace std；”是个偷懒的办法，这使得std名称空间中所有的名称都可用，更好的办法是使用using声明语句，只让需要的名称可用。

将“using namespace std；”用下列语句替换。

using std：cin；

using std：cout；

using std：endl；

便可以使用cin、cout和endl，而不用加“std：”前缀，不过要使用iostream中的其他名称，同样必须先进行声明。

using声明语句既可以如代码2.1一样放在main（）函数内部，这样，std名称空间中所有的名称只能在main（）函数内使用，也可以放在main（）函数外部，使其获得全局的可见性，这样，可以在文件中任何地方使用std名称空间中所有的名称。

说明

名称空间是C++一种新的特性，只针对新标准的C++头文件（即没有.h后缀，如#include＜iostream＞）才有效，对旧标准的头文件（如“#include＜iostream.h＞”），using语句没有意义。

2.1.5　C++语素

从编译角度看，组成C++的最小逻辑单位是单词，单词好比建屋盖房的砖瓦，C++中的单词有以下几类。

（1）直接常量。如代码2.1中的0。

（2）字符串。一对双引号之间的字符序列，是常量的一种，如代码2.1中的“"请输入要相乘的两个整数，用空格键分开："”和“"计算结果为"”。

（3）关键字。系统定义的一些对编译程序有特别意义的名字，表2.1列出了ANSI/ISO标准C++的关键字。

关键字是一种特殊的标识符，关键字具有特定的含义，不能对它们再定义。

（4）一般标识符。由程序员定义的名字，主要包括类名、变量名（如代码2.1中的num1、num2和numResult）和函数名等。

自定义的标识符有一定的命名规则，一般来说有以下几点。

❑由大小写字母（a～z或A～Z）、下划线和数字组成（中间不允许有空格和标点符号），并且必须以一个字母或下划线（_）开头，即数字不能打头。

❑大小写字母表示不同的意义，如num1和Num1是两个不同的标识符。

❑不能使用关键字作标识符，所以标识符也称为保留字。

❑在定义标识符时，虽然语法上允许用下画线开头，但是，我们最好避免定义用下画线开头的标识符，因为编译器常常定义一些下画线开头的标识符。

❑C++没有限制一个标识符中的字符个数，但是，不同的编译器可识别的长度有一定的限制，不过，我们在定义标识符时，通常并不用担心标识符中字符数会不会超过编译器的限制，因为编译器限制的数字通常很大。

注意

一个写得好的程序，标识符应该尽量直观，可以拼读，可“望文知意”，关于编码风格的问题，在第19章会有详细的介绍。

（5）运算符。代码2.1中的“＜＜”和“＞＞”分别称为“输出运算符”和“输入运算符”，此外，C++中主要的运算符还有如下几种。

❑算术运算符：加减乘除（+、-、*、/）、取模（%）、自增（++）和自减（--）。

❑关系运算符：小于（＜）、小于等于（＜=）、大于（＞）、大于等于（＞=）、等于（==）和不等于（！=）。

❑逻辑运算符：与（＆＆）、或（||）和非（！）。

❑赋值运算符：赋值（=）。

❑其他运算符。

（6）标点符号。有#、（）、{}、，、：和；等，其中圆括号和花括号必须成对使用，空白符是一种不被编译的符号，编译器认为全部的注释、换行符、空格及制表符等都是空白符，不对程序的语义和语法产生任何的影响。

注意

程序中所有标点符号都是英文标点。

2.2　变量与基本类型

在数学课中我们曾学过未知数的概念，未知数是对解的一种标识，换种思考的角度，未知数也是存储解的数据的一种手段，设计C++程序来解决问题，也需要存储和标识信息，这就要用到变量的概念。

2.2.1　变量

变量就是机器一个内存位置的符号名，在该内存位置可以保存数据，并可通过符号名对数据进行访问。变量有以下3个特征。

❑每一个变量有一个名字，其命名规则与一般标识符相同。

❑每一个变量有一个类型。

❑每一个变量代表一个值。如果需要变量代表某一个值，就把该值赋给变量。

在使用一个变量之前，必须先定义。定义变量的一般格式如下。

数据类型变量名1[=初始值1]，变量名2[=初始值2]，……；

方括号中的内容是可选的，它在定义变量时，给变量赋初值。“数据类型”是指C++有效的数据类型，如int、double和long等，每个变量属于一个特定的类型，类型是对一组数据的抽象概括，这些数据具有相同的取值范围、运算和存储方式，C++中的数据类型可分为基本数据类型和复合数据类型两大类，如图2.2所示。

图　2.2　C++数据类型示意图

定义一个变量需要明确以下3个方面的问题。

（1）变量在内存中的存放位置。

（2）需要多少个内存单元（变量的类型）。

（3）存储的信息有什么意义（变量名、标记）。

以代码2.1中的“int num1=0”为例，这个语句的主要作用是定义了一个变量num1，告诉编译器，要在内存中开辟一块区域，存放int型的数据，该区域用num1来标记。但这个语句并未体现出该区域在内存中的具体位置，变量内存的开辟由编译器和操作系统自动完成的，使用指令“＆num1”可以检索到num1在内存中的位置。变量定义完毕后，通过赋值语句将其初始值设为0。代码2.3给出了如何定义一个变量并对其进行赋值的示例。

代码2.3　定义一个变量并赋值DefineAVariable

＜---------------------------------文件名：example203.cpp---------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　int i；//声明变量i

06　i=5；//对变量i赋值为5

07　cout＜＜"当前的i值是"＜＜i＜＜endl；//输出变量i的值

08　i=6；//对变量i再次赋值为6

09　cout＜＜"当前的i值是"＜＜i＜＜endl；//输出变量i的值

10　return 0；

11　}

【代码解析】代码第5行是声明整型变量i，第6行是对变量i进行赋值。

运行结果如下所示。

当前的i值是5

当前的i值是6

变量的值可以由程序进行修改（赋值或输入等），是可变的，但在某个确定的时刻，变量的值是确定的，并一直保持到下次被修改，这一特征可以概括为“用之不尽，新来旧去”。

技巧变量也可以理解为可以变化的量，它刚好与常量相对，常量是在程序运行时不可以变化的量，将在下一节中进行介绍。

2.2.2　整型

整型变量用来存储不带小数的数值，根据存储数值和分配存储空间的大小，整型变量可分为短整型（short int，可简写为short）、整型（int）和长整型（long int，可简写为long）。

表2.2说明了整型的分类及其表示范围，方括号里的内容是可选的，代表默认的选项。

整型量在内存中是用二进制补码以定点形式存放的，以short类型（2字节）为例，图2.3示意了整数是如何存储在内存单元中的。每个整型量是由符号位和数值位组成，符号占一位，0代表正，1代表负。在默认的情况下，整型变量假定为是有符号的，当使用unsigned关键字时，也可以把整型变量定义为无符号的。当然，定义整型变量时，也可以使用signed关键字，但这是多余的。

图　2.3　用2个字节表示一个整型数

提示

对int和unsigned int类型来说，在16位操作系统中，占用2个字节；在32位操作系统中，占用4个字节。换言之，对不同的机器和操作系统，int型所占的字节数可能不同，而short和long型则是固定的，因此，从可移植性的角度来说推荐使用short和long型来表示整型数。

简要讨论下size_t类型，这是为了方便系统之间的移植而定义的。举例来说，在32位系统中定义为unsigned int，在64位系统中定义为unsigned long，换言之，在32位系统中是32位无符号整型，在64位系统中是64位无符号整型。size_t一般用来表示一种计数，比如有多少东西被复制等，后面要介绍的sizeof运算符的返回结果类型便是size_t。除此之外，size_t类型对应着signed int和signed long版本。

说明

在学习了typedef的用法后可以发现，size_t往往是在头文件中通过“typedef size_t unsigned int”来实现的。

2.2.3　浮点型

浮点型变量又称实型变量，用来存储带小数的数值。浮点型变量在内存中用二进制浮点形式表示，如图2.4所表示。每个浮点型量是由符号位、阶码和尾数3部分组成的，符号占一位，0代表正，1代表0，没有无符号浮点数这一用法。

图2.4所表示的浮点型量的大小（不考虑符号位）为C×2B。C++中提供了3种字长的浮点类型：float、double和long double，其各自的字节数和精度如表2.3所示。

图　2.4　浮点型内存表示模型

说明

Visual C++中long double的长度为8个字节，等同于double。

2.2.4　基本字符型

计算机内部处理的信息都是用0和1表示的，而人对信息和数据的处理是基于字符的。字符与0和1之间按照一定的规则进行转换，这些转换规则被称为信息交换代码，目前应用最广泛的是ANSI制定的ASCII码（美国信息交换标准代码），如A的ASCII编码为01000001，用'A'（用一对单引号包起来）标记，如图2.5所示。

图　2.5　字符型量内存存储示例

注意

在内存中，字符数据以ASCII码存储，即以整数表示，'0'和0是不同的。

C++的字符由下列字符组成：

❑大小写英文字母（a～z,A～Z）。

❑数字字符（0～9）。

❑特殊符号（空格、！、#、%、^、＆、*、_、-、+、=、～、＜、＞、/、\、|、.、，、；、?、'、"、（、）、[、]、{、}、）。

❑C++中还有一些不能打印的特殊字符，称之为转义字符，常用的转义字符如表2.4所示。

说明

对于单引号、双引号和反斜杠，各自的ASICII编码为'\''，'\"'和'\\'，以便与程序中的标点符号相区分。大小写英文字母，数字字符和除单引号、双引号和反斜杠外的特殊符号，称为可显示字符。

字符型变量用char来定义，占用一个字节，存放该字符的ASCII编码，如“char z='A'；”，定义了一个字符型变量z，并初始化为'A'（01000001）。

C++把字符型变量当做一个较小的整型量，可以像整型量一样使用它，如“char z=65；”作用与“char z='A'；”相同，在下面字符型常量的介绍中会有关于字符型数字形式的详细介绍。字符型数字同样有unsigned和signed之分，在大多数机器上，char类型默认为有符号，与signed char意义相同，但在某些机器上，char类型默认为无符号，因此，在使用char类型时最好能说明是否有符号。一个有符号的字符变量可以保存-128～127之间的整数，而一个无符号的字符变量可以保存0～255之间的整数。

2.2.5　宽字符型

8位char型字符最初是为了处理拉丁字母而提出的，可实际上，某些字符集可能无法用一个8位字节来表示，比如东亚字符集，为此，C++引入了wchar_t，即宽字符类型来表示扩展字符集，可以将其设置为16位或32位（即用多个字节来表示一个字符），这主要取决于不同的操作系统和编译系统。

前面介绍过的cout和cin默认处理的是char型数据，因此，不能直接用于处理wchar_t字符，iostream中提供了与此类似的wcout和wcin，专门处理wchar_t型字符，此外，可以通过前缀L指明wchar_t型常量或wchar_t型字符串。

注意

与宽字符对应的字符集是Unicode，此外还由ISO 10646，这也是宽字符编码标准，这两个现都在不断发展中，感兴趣的读者可自行查阅相关资料。

2.2.6　布尔型

布尔类型变量用bool关键字定义，其值为true或false,true和false都是C++的关键字。C++将非零值解释为true，将零解释为false。

可以用下面的语句来定义一个bool类型的变量。

bool is OK=false；

试一试仿照代码2.1声明一个浮点变量，一个字符型变量，赋值或用键盘输入一个值，并显示出来。

2.3　常量

有了前面对变量的说明，再来讲解常量显然就轻松了很多。常量同样对应着内存中的一块存储区域，通过常量名可对该区域进行访问，同样有整型、浮点型、字符型和布尔型等数据类型，和变量唯一的不同之处在于，通过程序或输入无法改变常量的值，这也是常量取名的由来。

2.3.1　整型常量和浮点型常量

可以说，带有小数点的数，如0.5和2.69等，称为浮点型常量，不带小数点的数称为整型常量。

1.表示一个整型常量

C++允许用十进制、八进制或十六进制数字书写整型常量，在这3种计数制中使用的符号分别为如下所示。

十进制数字：0、1、2、3、4、5、6、7、8、9。

八进制数字：0、1、2、3、4、5、6、7。

十六进制数字：0、1、2、3、4、5、6、7、8、9、a、b、c、d、e、f（字母亦可大写）。

例如，我们说某个整型变量的值是100，这是生活中常用的十进制用法，用八进制数表示为124（八进制），而用十六进制表示为64（十六进制），我们这里采用加括号注释的方法来区分不同的计数制，对于编译器来说，如何区分所给的数据是什么数制呢？答案就是使用前缀。

十进制：无前缀，如“int i=100；”。

八进制：0前缀，如“int i=0124；”。

十六进制：0X或0x前缀，如“int i=0X64；”。

2.表示一个浮点型常量

和数学上的用法一样，浮点型常量有两种表示方式：常规表示法和科学计数法。常规表示法由整数部分、小数点和小数部分组成，其中整数部分和小数部分可以省略，如1.2、.2、3等。科学表示法由整数部分、小数点、小数部分，指数部分（e或E及一个带符号的整型指数）组成，整数部分和小数部分也可省略，如1.2E8代表1.2×10^8、.3e-7代表0.3×10-7、4e23代表4×10^23。

3.使用后缀来区分整型常量和浮点型常量的长度

C++中允许用后缀声明一个整型常量或浮点型常量的长度，有以下几种表示方法。

❑L或l后缀，声明整型常量为long型，如1000L，或声明浮点型常量为long double型，如1.2L。

❑U或u后缀，声明整型常量为unsigned（无符号类型），如506U。

❑UL/LU/ul/lu/Ul/lU/uL/Lu后缀，声明整型常量为unsigned long类型。

❑F或f后缀，声明浮点型常量为float型，如1.2F。

未加后缀的整型常数一律默认为int型，未加后缀的浮点型常量一律默认为double型，但当其值超出double型字数范围则默认为long double型，超出long double字数范围的便产生数据“溢出”错误。

2.3.2　字符型常量

用一对单引号括起来的一个或多个字符称为字符型常量，如'A'、'AA'、'\n'和'5'等，其中'A'、'?'和'5'称为单字符常量，'AA'称为双字符常量，'\n'称为转义字符序列。

由代码2.4可以看出，对双字符常量来说，系统将其解释为一个int类型的数据，前面的字符作为低位字节，后面的字符作为高位字节，这种方式仅适用于可显示字符。

代码2.4　单字符常量和双字符常量的用法CharSample1

＜----------------------------文件名：example204.cpp--------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　cout＜＜"\'A\'is"＜＜'A'＜＜endl；//单字符常量'A'

06　cout＜＜"\'B\'is"＜＜'B'＜＜endl；//单字符常量'B'

07　cout＜＜"\'AB\'is"＜＜'AB'＜＜endl；//双字符常量'AB'

08　return 0；

09　}

【代码解析】代码第7行就是输出双字符常量'AB'的值。

输出结果为：

'A'is A

'B'is B

'AB'is 16706

同样可以使用“'ABA'”和“'ABAB'”这样的形式，唯一的限制来自于int的字节数，对有些系统来说，int型占两个字节，则“'ABA'”和“'ABAB'”不合法，对int类型占4个字节的系统来说，“'ABA'”和“'ABAB'”完全可用。但不管对什么样的系统，“'ABABA'”及更多字母组合的写法都是错误的。

注意

多字符常量除了增加程序阅读难度外，似乎没有其他的好处，不推荐使用。

字符型常量在C++中可作为单字节的整型常量来使用，因此字符型常量有其对应的八进制、十进制和十六进制的形式，见示例代码2.5。

代码2.5　字符型常量的数值形式CharSample2

＜-------------------------------文件名：example205.cpp-----------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　char letter1='A'；

06　char letter2=65；//十进制

07　char letter3=0101；//八进制

08　char letter4=0X41；//十六进制

09　cout＜＜"letter1 is"＜＜letter1＜＜endl；

10　cout＜＜"letter2 is"＜＜letter2＜＜endl；

11　cout＜＜"letter3 is"＜＜letter3＜＜endl；

12　cout＜＜"letter4 is"＜＜letter4＜＜endl；

13　return 0；

14　}

【代码解析】代码第7行，是使用八进制数来给变量letter3赋值，其值为65，即字符'A'。

输出结果如下所示。

letter1 is A

letter2 is A

letter3 is A

letter4 is A

有的读者会有这样的疑问，代码2.5中的65、0101、0X41到底是字符呢，还是数呢？一般情况下，这些都是数，只有在对字符型变量赋值时，才代表对应的ASCII字符。但是，如果对字符型进行算术运算，实际上是对字符代表的数值进行操作，如代码2.6所示。

代码2.6　字符型的算术运算CharSample3

＜---------------------------文件名：example206.cpp---------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　char letter1='A'；

06　char letter2='A'+2；//字符型常量被当成数值来运算

07　cout＜＜"letter1+2 is"＜＜（letter1+2）＜＜endl；//输出表达式（letter1+2）时，

08　//输出的是数值而不是字符

09　cout＜＜"letter2 is"＜＜letter2＜＜endl；//输出字符

10　return 0；

11　}

【代码解析】代码第6行，是将字符常量'A'当成数字（65）来运算（增加2），变成67赋值给变量letter2，即数值67，用字符'C'表示。

输出结果如下所示。

letter1+2 is 67

letter2 is C

在代码2.6中，对字符型常量'A'进行加2操作，实际上对数值65加2，只有在对字符型变量letter2赋值时，67（'A'+2）才被当做字符（'C'）对待，否则，输出结果是个数值。

此外，字符型常量还可以通过转义代码来表示，反斜杠后给一个八进制或十六进制的ASCII代码，用单引号括起来，表示ASCII表中与代码对应的值，如'A'又可表示成'\0101'（八进制）或'\0X41'（十六进制）。

注意

使用转义代码表示字符常量时，代码数值不能超出char类型表示的范围。

2.3.3　字符串常量

字符串常量是用一对双引号括起来的零个或多个ASCII字符的序列，并以NULL（ASCII码值为0）结束，如"Hello,world"，其在内存中的存储如图2.6所示。

图　2.6　字符串内存模型

关于字符串常量有以下问题需要注意。

❑字符串与字符不同，例如："A"与'A'不等价，前者由两个字节组成（字符'A'与字符'\0'），而后者只有一个字节。

❑最短的字符串是空字符串（""），它仅由一个结尾符'\0'组成。

在前面的示例中已经使用了字符串常量以输出提示信息，一个长字符串可以占两行或多行，但最后一行之前的各行要以反斜杠结尾。

关于字符串的详细介绍，将在第3章中进行说明。

2.3.4　符号常量

在声明语句中，用const修饰的标识符指向一个“只读”的程序实体，称为符号常量，如“const int PEOPLE=5；”，在程序中便可以用PEOPLE来代表5。符号常量与普通常量的不同之处在于其像变量一样有标识符（名字），有效提高了系统的可修改型和可读性，见代码2.7。

代码2.7　计算边长为4.0的正方形的周长与面积Square1

＜----------------------------文件名：example207.cpp-------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　float circle,area；//声明两个浮点型变量，表示周长和面积

06　circle=4. 0*4.0；//计算周长

07　cout＜＜"正方形的周长是："＜＜circle＜＜endl；//输出

08　area=4. 0*4.0；//计算面积

09　cout＜＜"正方形的面积是："＜＜area＜＜endl；//输出

10　return 0；

11　}

【代码解析】代码第6行和第8行都直接使用了整型常量4.0来作为半径，计算圆的周长和面积。

输出结果如下所示。

正方形的周长是：16

正方形的面积是：16

代码2.7　是计算一个半径为4.0的正方形的周长和面积的程序，现在正方形的半径变为5.0，程序中有4个4.0，到底要修改哪一个呢，弄不好就会出错，这还是个非常简单的程序，如果程序的规模再大一点，要分清哪个数需修改，哪个数需保留，是要下一番工夫的。使用符号常量能有效地解决这一问题，如代码2.8所示，当正方形边长变化时，只要修改SIDE符号常量的值即可，有效地提高了程序的可读性和可修改性。

代码2.8　符号常量的声明与使用Square2

＜---------------------------文件名：example208.cpp--------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　const float SIDE=5. 0；//声明一符号常量SIDE，表示边长

06　float circle,area；//声明两个浮点型变量，表示边长和面积

07　circle=4. 0*SIDE；//计算周长

08　cout＜＜"正方形的周长是："＜＜circle＜＜endl；//输出

09　area=SIDE*SIDE；//计算面积

10　cout＜＜"正方形的面积是："＜＜area＜＜endl；//输出

11　return 0；

12　}

【代码解析】代码第5行，是定义了符号常量SIDE，其值为5.0。

输出结果如下所示。

正方形的周长是：20

正方形的面积是：25

此外，C++还可用C语言提供的#define语句来完成同样的工作，不过，推荐读者采用const声明符号常量，关于#define的详细用法，以及const与#define的详细比较参见第20章。

注意

应在声明时对const常量进行初始化，因为一旦声明，const常量的值无法修改。

2.3.5　枚举常量

枚举（Enum）常量是一种用户自定义的类型，定义的基本格式如下所示。

enum枚举类型名{枚举常量1[=整型常数]，枚举常量2[=整型常数]，……}[变量名列表]花括号中的内容称做枚举表，其中的每一项称为枚举常量，换言之，枚举表是枚举常量的集合。枚举表中每项后的“=整型常数”是给枚举常量赋初值，用方括号代表这步可以省略，如果不给枚举常量赋初值，编译器会为每一个枚举常量赋一个不同的整型值，第一个为0，第二个为1，等等。当枚举表中某个常量赋值后，其后的成员则按依次加1的规则确定其值。在定义枚举类型时，可同时定义一些变量属于这种类型。

enum day{Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday}currentDay；这样就定义了一个称做day的枚举类型，该类型包含7个枚举常量（Sunday到Saturday），编译器自动令Sunday为0，Monday为1，依次类推。在定义枚举变量的同时，声明了枚举变量currentDay为day类型。currentDay只能取枚举表中的某项作为其值，枚举变量不允许在其枚举表成员之外取值。当然，也可以先定义枚举类型，而后再声明枚举变量，如下所示。

enum day{Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday}；

day currentDay；

那枚举常量到底是什么类型呢？我们来看代码2.9给出的例子。

代码2.9　使用枚举常量Enum

＜-------------------------------文件名：example209.cpp----------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　//定义枚举类型day

06　enum day{Sunday,Monday,Tuesday,Wednesday,Thursday,Friday,Saturday}；

07　day currentDay；//声明一day型变量currentDay

08　currentDay=Tuesday；//为currentDay赋值

09　cout＜＜"Current day is"＜＜currentDay＜＜endl；//输出

10　return 0；

11　}

【代码解析】代码第6行使用了enum关键字，定义枚举常量day。

输出结果如下所示。

Current day is 2可见，枚举常量实际上是一些整型数的名称，但是，不允许直接用数字来定义枚举类型，必须使用合法的C++标识符作为枚举常量的名字，下列用法是错误的。

enum day{0，1，2，3，4，5，6}；

使用枚举常量主要有以下两个方面的好处。

❑提高程序的可读性，用有意义的名字来代替数字。

❑限定了枚举变量的取值范围，如代码2.9中的currentDay只能取列表中Sunday到Saturday中的一个。

技巧在对枚举常量赋初值的时候，允许几个枚举常量具有相同的值。

2.4　运算符与表达式

运算符指的是具有运算意义的符号，例如加法运算符（+），减法运算符（-）等，表达式，是C++编译器能读懂的计算机语句，由运算符和操作数按一定语法规则组合而成，根据运算符决定对操作数进行何种运算，并得出唯一的运算结果。

C++提供的运算符有以下几种：算术运算符、关系运算符、逻辑运算符、位运算符、条件运算符、赋值运算符、逗号运算符、sizeof运算符及其他运算符。同时，按照操作数的个数，运算符又可以分为单目运算符（1个操作数）、双目运算符（两个操作数）和三目运算符（3个操作数）。

2.4.1　算术运算

C++中提供的算术运算符如表2.5所示。

注意

取余操作（%）要求两个操作数都为整型数，而且，取余操作（A%B）和除操作（A/B）都要求被除数B非0。

在算术运算中，单目运算符（-）优先级最高，其次为乘、除、取余操作，加减优先级最低，如果有括号，则括号内的运算先执行，这和数学式里的运算顺序基本一致。此外，C++要求算术运算符的两个操作数是数值类型，由于char型用其ASCII码表示，因此也可以将算术运算符应用于char型，代码2.10给出了算术运算符的使用范例。

代码2.10　算术运算符和算术表达式Arithmetic

＜------------------------------------文件名：example210.cpp------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　int numA=2，numB=3；//声明两个int变量

06　cout＜＜"-2*（3/2+2）-3="＜＜（-numA*（numB/numA+numA）-numB）＜＜endl；//四则运算

07　cout＜＜"2/3="＜＜numA/numB＜＜endl；//除法运算

08　cout＜＜"2%3="＜＜numA%numB＜＜endl；//取模运算

09　return 0；

10　}

【代码解析】代码第6行，就是使用算术运算中的加、减、乘和除来求解的结果。

输出结果如下所示。

-2*（3/2+2）-3=-9

2/3=0

2%3=2

注意

使用整型数进行除法运算会带来较大的误差。

改写代码2.10，如下所示。

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　int numA=2；

06　float numB=3；

07　cout＜＜"-2*（3/2+2）-3="＜＜（-numA*（numB/numA+numA）-numB）＜＜endl；

08　cout＜＜"2/3="＜＜（numA/numB）＜＜endl；

09　return 0；

10　}

【代码解析】代码第6行，定义的变量numB是浮点数类型，所以最后结果也与整数不同。

输出结果如下所示。

-2*（3/2+2）-3=-10

2/3=0.666667

可见，同样是除法运算，使用浮点型数和整型数有很大的不同，两个整型数进行算术运算，结果仍然是个整型数，不带小数，但只要操作数中有一个是浮点型数，运算结果就是个浮点型数，允许带小数，结果自然也就精确了。从本质上说，这涉及C++类型转换的问题，在本章稍后的章节中有关于这个专题的详细介绍。

2.4.2　逻辑运算

对简单电路有所了解的人可能更容易理解逻辑（与或非）的概念，C++中提供了3个逻辑运算符，分别是：＆＆（“与”），||（“或”）和！（“非”）。

其中，！为单目操作符，结合顺序为从右向左，＆＆和||为双目运算符，结合顺序为从左向右，逻辑运算符要求其操作数为bool型，即有true和false两种取值，但实际上，借助于C++的类型转换机制，普通的数值量也可以进行逻辑运算，非0量转换为true，0转换为false。逻辑运算返回一个bool型量，表2.6为操作数取值与逻辑表达式取值之间的关系。

！（“非”）实际上是个取反操作，而只有当两个操作数都为true时，＆＆（“与”）表达式取值才为true，否则取false；只要两个操作数中有一个为true，||（“或”）表达式取值便为true，换句话说，只有两个操作数都为false时，||（“或”）表达式取值才为false。

注意

运算符的组合使用可以实现复杂的逻辑关系。

2.4.3　短路表达式

在由＆＆和||运算符组成的逻辑表达式中，C++规定：只对能够确定整个表达式值所需要的最少数目的子表达式进行计算。也就是说，当计算出一个子表达式的值后便可确定整个逻辑表达式的值时，后面的子表达式就不需要再计算了，这种表达式也称为短路表达式。

如果满足下列条件：

❑在逻辑与表达式“表达式1＆＆表达式2”中，表达式1为false；

❑在逻辑或表达式“表达式1||表达式2”中，表达式1为true；

则保证不会计算表达式2。

来看下面的语句。

int a=0，b=3；

a＆＆（b++）；

表达式中a为0，在进行逻辑运算时转换为false，则整个表达式的值为false，不会再对b进行增1运算，可能就会带来意想不到的错误。

2.4.4　关系运算

关系运算，就是比较运算，C++提供了6种比较运算，如表2.7所示。

从优先级上来说，“==”和“！=”两种运算符的优先级比其他关系运算符的优先级低。和算术运算符一样，关系运算要求两个操作数为数字，可用于整型、浮点型和字符型。但不可用于比较两个字符串常量，因为实际上比的是两个字符串在内存中的地址。

如果比较结果成立，如“1＜2”，返回bool型常量true，否则返回bool型常量false。

关系运算符比算术运算符的优先级低，这说明

x+5＞y-7 等价于

（x+5）＞（y-7）而不是

x+（5＞y）-7

或许有的读者会对“x+（5＞y）-7”这个表达式感兴趣，不管“5＞y”是true还是false，后面减7又是怎么回事呢？实际上这也属于类型转换的范畴，在进行C++算术运算时，true值会自动转换为1，而false则自动转换为0，详细的介绍同样可参考稍后的类型转换专题。

注意

不要混淆了“==”和“=”，前者用于比较两个操作数是否相等，后者用于赋值。

2.4.5　大有文章：变量是否为“0”

关系运算是C++中常用的语句，常用于决定程序的流程和分支选取，其中，变量和零值的比较是应用很广，最可能出错的地方，本节的内容将帮助读者正确理解变量和零值比较的本质，写出高质量的代码。

1.整型变量是否为0

应当使用“==”或“！=”将整型变量直接与0进行比较。假设整型变量的名字为num，它与0比较的代码如下所示。

num==0；//或num！=0；

2.浮点型变量是否为0

无论是float型还是double型变量都存在精度的限制，所以，使用浮点型变量的一个原则就是避免将浮点型变量用“==”或“！=”与数值精确比较，应该借鉴误差区间的概念，转化成“＞=”或“＜=”形式，假设浮点型变量的名字为num，下面这种写法是存在隐患的。

num==0.0；正确的写法如下所示。

（（num＜=delta）＆＆（num＞=-delta））；

其中，delta是允许的误差范围，如0.000001等。

注意

该方法不仅仅适用于浮点型变量和0的比较，与其他数值的比较同样如此。

3.bool类型不用比较

bool类型本身只有true和false两种取值，在进行判断时，不用与0或1进行比较。

假设bool型变量名字为isOK，判断其是否为真的标准语句如下所示。

if（isOK）{……}//或if（！isOK）不推荐下列写法。

if（isOK==1）；//或if（isOK==0）；

2.4.6　条件运算

条件运算符是C++中唯一一个三目运算符，条件表达式的一般形式如下所示。

S1?S2：S3；其含义为：若S1为true，则条件表达式取S2的值，否则，取S3的值，条件运算符的优先级比赋值运算符略高。

min=A＞B?B：A；

上面例句的意义为：如果A大于B成立（true），变量min取值为B，否则，变量min取值为A。恰当地使用条件运算符可以写出精练的C++语句。

2.4.7　位运算

无论在什么情况下，数据都是用0和1来存储的，这就是数据的机内表示，有时按位操作数据是很必要的，也就是说，程序员可能希望通过改变内存中某单元的某一位来改变其值，这就是位运算的由来。C++中的位运算符有以下两类。

❑位逻辑运算符：＆（位“与”）、^（位“异或”）、|（位“或”）和～（位“取反”）。

❑移位运算符：＜＜（左移）和＞＞（右移）。

下面分开进行介绍，首先来看＜＜和＞＞，可能有的读者会说，这不是前面代码中经常出现的，跟在cin和cout后面的输入输出符吗？没错，但它们在这里起的是移位的作用，而不是用来输入输出的。

注意

一个操作符具有多种功能，代表两种及以上不同运算的情形，称为运算符重载。

1.移位运算符

移位运算表达式的基本形式为“A＜＜n；”或“A＞＞n”，分别代表将数字型量A（整型、浮点型和char型，可以是变量也可以是常量）中存储的0、1序列向左或右移动n位，移动后的值作为整个表达式的输出，但要注意的是，执行移位运算后A的值并没有发生变化，见代码2.11所示。

代码2.11　移位运算Shift

＜-----------------------------文件名：example211.cpp-------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　short A=521，B,C；//声明两个short型变量

06　B=A＜＜3；//A左移3位，赋值给B

07　C=768＞＞4；//768右移4位赋值给C

08　cout＜＜"A is"＜＜A＜＜endl；//输出

09　cout＜＜"B is"＜＜B＜＜endl；//输出

10　cout＜＜"C is"＜＜C＜＜endl；//输出

11　return 0；

12　}

【代码解析】代码第6行是将A（521）左移3位，赋值给B，而第7行是将768右移4位赋值给C。

输出结果如下所示。

A is 521

B is 4168

C is 48

说明

代码2.11　中，我们对short型变量A和常量768进行了左移位和右移位的操作，之所以采用short而没有采用int来声明变量A，是因为short型变量在内存中占用两个字节，而int型变量不确定，采用short类型抛除了平台相关性的影响。

代码2.11　中，语句“C=A＜＜3；”的执行过程如图2.7所示，short型变量A占用两个内存字节，大小为521，其机内形式为00000010 00001001，“A＜＜3”是将其中的每位都向左移3位，后面的位补以0，并把移位后的结果赋值给变量C。右移运算本质上和左移运算是相通的，这里就不再讲解。

注意

移位运算中，左侧的操作符不发生变化，如代码2.11中的A，移位前后都是521。

图　2.7　左移位示意图

移位运算符都是双目的，从左向右结合，优先级与标准的输入输出符相同，关于移位运算有以下两点需要特别强调。

❑对空白位补0这一操作是针对无符号数或有符号正数而言，对于有符号的负数，编译器会对符号位进行特殊的处理。

❑移位运算符右边的操作数（n）为负数，或者n比左边操作数位数还要长时，会产生不可预期的结果，这取决于该内存区域的存储情况。

图　2.8　位取反运算示意图

2.位取反运算

位取反运算即对一个数的各位都进行非运算，如图2.8所示。

位取反运算符是单目的，从右向左结合，其优先级与前面介绍的单目运算符优先级相同，和移位运算一样，位取反运算并不改变操作数的值。

3.位“与”、位“或”和位“异或”

位“与”、位“或”和位“异或”是对两个操作数的每一位进行“与”、“或”和“异或”运算如代码2.12所示。

代码2.12　位“与”与位“或”BitLogic

＜--------------------------------文件名：example212.cpp----------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　short A=521，B=123，C,D；//声明4个short型变量

06　C=A＆B；//位与

07　D=A|B；//位或

08　cout＜＜"C is"＜＜C＜＜endl；//输出

09　cout＜＜"D is"＜＜D＜＜endl；//输出

10　return 0；

11　}

【代码解析】代码第6行是进行位与运算，将A位与B，然后赋值给C。代码第7行是将A位或B，然后赋值给D。

输出结果如下所示。

C is 9

D is 635代码2.12中语句“C=A＆B；”的执行过程如图2.9所示，A和B中的对应位分别进行“与”运算，图中的4条竖线是从图中16个位的“与”运算选出的最有代表性的，对应了位“与”运算的准则，如下所示。

1＆1=1 1＆0=0 0＆1=0 0＆0=0位“或”运算的执行过程与之类似，对应的准则如下所示。

1|1=1 1|0=1 0|1=1 0|0=0通俗地讲，如果位“异或”运算的两个位相同（同为0或同为1），结果为0，若两个位不同（一个为0，另一个为1），结果为1，对应的准则为如下所示。

1^1=0 1^0=1 0^1=1 0^0=0

位“与”、位“或”和位“异或”运算符都是双目运算符，其结合性都是从左向右的，优先级高于逻辑运算符，低于比较运算符，且从高到低依次为＆、^、|。

注意

和移位运算符不同的是，位“反”、位“与”、位“或”和位“异或”运算符不关心操作数的符号，只是按操作数所在字节的0、1序列进行比对，符号位会被当成普通的0或1进行处理。

图　2.9　位与运算示意图

2.4.8　赋值运算

由赋值运算符组成的表达式为赋值表达式，赋值运算符的结合性是由右至左的，因此，C++程序中允许出现连赋值的情况。

int A,B，C,D，E；

A=B=C=D=E=9；

上述语句是合法的，整型变量A、B、C、D和E都被赋值为9。

讨论到赋值运算，有必要提及程序实体和左值这两个概念，程序实体是内存中的一块可标识的区域，左值是左值表达式的简称，是指一个程序实体的表达式。判断一个表达式是否左值的方法是看其能否放在等号的左边。

如“float a；”声明了一个浮点型变量a，则a是左值，因为其指明了一个程序实体，可放在赋值号的左边，但表达式“a+3”和“a=1”就不能放在赋值号的左边，不是左值。

能放在赋值号左边的表达式都是左值，它指明了一块内存区域，而赋值运算实质上是改变了这一区域内容的操作。

关于赋值运算需要注意以下3个问题。

（1）赋值号两边的数据类型不一致时，编译器会在赋值前将右操作数转换成左操作数的类型。

int numA；

numA=1.25；

上述语句声明了一个整型变量numA，并对其赋值1.25，1.25是个浮点型常量，因此，在赋值时，编译器会对0.25进行转换后再对numA赋值，赋值后numA的实际值为1。

（2）声明语句使用的“=”符号，称为“初始化符”，与赋值运算符不同。

int numA；

numA=3；与

int numA=3；

虽然效果相同，但本质不同，前者是声明一个变量numA，并对其进行赋值操作，而后者则是声明一个变量numA，并对其初始化为3。

下面这个同时声明两个变量并初始化的语句是非法的。

int numA=numB=9；但这并不是说不能用一个变量对另一个变量进行初始化。

int numB=1；

int numA=numB=9；

使用一个已经定义的变量对另一个变量初始化是没有问题的。

（3）前面我们讲过符号常量，事实上，const也可以修饰变量，用const修饰的变量是左值，但不能放在赋值号的左边，只有能被修改的左值才可以放在赋值号的左边，换言之，只有当表达式指定的内存区域的值可以被程序修改时，表达式才可放在赋值号的左边。

赋值运算符除了"="之外还有10个复合赋值运算符，这是赋值和运算相结合的运算符。举例来说，语句“x=x+y；”代表的意义是将x和y相加，结果放入变量x中，这可以简洁地表示为“x+=y；”，运算符“+=”既有加的功能，也有赋值的功能，其他一些类似的复合赋值运算符如表2.8所示。

复合赋值运算符的结合性都是从右向左的，它们的优先级和简单的赋值运算符一样，同时，复合运算符是把右边的表达式作为一个整体来进行运算的，见代码2.13。

代码2.13　复合赋值运算符的用法CompoundAssign

＜--------------------文件名：example213.cpp---------------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　int x=2，y=7；//声明两个int型变量x和y

06　x*=y+3；//x=x*（y+3）

07　cout＜＜"x*=y+3："＜＜x＜＜endl；//输出

08　x=2，y=7；//恢复原始值

09　x=x*y+3；//注意与"x*=y+3；"对比

10　cout＜＜"x=x*y+3："＜＜x＜＜endl；//输出

11　x=2，y=7；

12　x=x*（y+3）；

13　cout＜＜"x=x*（y+3）："＜＜x＜＜endl；

14　return 0；

15　}

【代码解析】代码6行使用了复合运算符，其相当于x=x*（y+3）。

输出结果如下所示。

x*=y+3：20

x=x*y+3：17

x=x*（y+3）：20

可见，“x*=y+3”等价于“x=x*（y+3）”，复合运算符右边的部分会当成一个整体来计算，这是因为复合赋值运算符的优先级比算术运算符低。

2.4.9　++和--

对于表达式“x=x+1；”、“x=x-1”（或写为“x+=1”、“x-=1”），有种更简洁的写法“x++”（或“++x”）、“x--”（或“--x”）。“++”、“--”称为加1运算符和减1运算符，其都是单目运算符，优先级高于任何双目运算符，结合性为从右到左。

“++”和“--”多有两种形式：前缀形式（“++x”、“--x”）和后缀形式（“x++”、“x--”）。前后缀运算的效果是不同的，见代码2.14。

代码2.14　加1运算符的前缀形式和后缀形式PrefixAndPostfix

＜----------------------------文件名：example214.cpp-------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　int A=10，B；//声明两个int型变量

06　B=A++；//变量A后缀形式的自增，赋值给B

07　cout＜＜"A is"＜＜A＜＜endl；

08　cout＜＜"B is"＜＜B＜＜endl；

09　A=10；

10　B=++A；//变量A前缀形式的自增，赋值给B

11　cout＜＜"A is"＜＜A＜＜endl；

12　cout＜＜"B is"＜＜B＜＜endl；

13　return 0；

14　}

【代码解析】代码第6行是变量A的后缀形式的自增，其意义是将A的数值先赋值给B，然后增加A的值。代码第10行是变量A的前缀形式自增，其意义是将A的数值先增加1，然后再赋值给B。

输出结果如下所示。

A is 11

B is 10

A is 11

B is 11

读者可以很明显地看出前后缀运算的区别，前缀形式是先进行增1或减1运算，再参与到表达式运算中，而后缀形式是先参与表达式的运算，后增1或减1。

从本质上讲，++和--运算属于复合赋值运算符，因此要求操作数为可修改的左值，如“++（A+B）”这样的写法是非法的，因为A+B不是左值。

注意

像“x++”或“x--”这样的写法都不是左值，因此类似“++x++”或“x++++”这样的写法都是非法的，但“++x”和“--x”是左值，像“++++x”这样的写法是正确的。

2.4.10　逗号表达式

逗号表达式由用逗号分隔的一组表达式组成，这些表达式从左向右计算，逗号表达式的结果是最右边表达式的值，其类型也是最后一个表达式的类型。

代码2.15　是逗号表达式的使用范例，其中“A++，--B,A*B”是逗号表达式，按照从左到右的顺序分别进行对A的加1操作（后缀++）、对B的减1操作（前缀--）、A和B相乘，并将最后一个表达式（A*B）的值返回，赋值给C。

代码2.15　逗号表达式Comma

＜--------------------------文件名：example215.cpp----------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　int A=3，B=7，C；//声明3个int型变量

06　C=（A++，--B,A*B）；//逗号表达式

07　cout＜＜"C is"＜＜C＜＜endl；//输出

08　return 0；

09　}

【代码解析】代码第6行，在表达式中使用了逗号表达式，其结果是将A*B的值，赋值给C。

输出结果如下：

C is 24

技巧合理应用逗号表达式可以写出精练的代码。

2.4.11　sizeof运算符与sizeof表达式

sizeof是个单目运算符，用来计算操作数在内存中占据的字节数，其操作数可以是括在圆括号中的类型标识符，其返回值是size_t类型，即无符号整数，如下所示。

sizeof（short）；//返回2

sizeof（long）；//返回4

sizeof（int）；//不确定，取决于不同的系统也可以是一个表达式，如下所示。

short x；

sizeof（x）；//返回2

2.4.12　运算符的优先级和结合性

实际在书写程序的时候，一个表达式中往往会有多个运算符，这时的运算顺序主要由以下两种因素决定。

（1）运算符的优先级：程序总是先执行优先级较高的运算符；

（2）运算符的结合性：当同优先级的操作符并列时，运算符的结合性决定运行顺序。对从左到右的运算符，先执行左侧的部分，对从右向左的运算符，则先执行右侧的部分。

表2.9给出了C++中的运算符的优先级和结合性，从上到下，优先级依次降低。

总地来说，有以下6点规律。

❑操作数多的运算符优先级别相对低一点，从高到低依次为单目→双目（不包含赋值运算符）→三目→赋值→逗号；

❑双目运算符个数最多，双目运算符优先级从高到低依次为算术运算符→比较运算符→位运算符→逻辑运算符；

❑算术运算符中，*、/和%的优先级高于+和-；

❑位运算符优先级从高到低依次为～→＆→^→|；

❑逻辑运算符优先级从高到低依次为！→＆＆→||；

❑赋值运算具有相同的优先级。

运算符使用不当会给程序带来这样或那样的问题，除了前面所讲的短路表达式外，还会出现一些程序员经常疏忽或理解有误的地方。

1.二元操作符的潜在缺点

二元操作符左右操作数的计算顺序在C++的标准中并没有明确的规定，如下所示。

int A=5，B；

B=（A=2）+（++A）；

“A=2”和“++A”的执行顺序是不确定的，而这个顺序却关乎B的结果，先执行语句“A=2”的话，B=5，如先执行“++A”的话，B=8。

可通过分解表达式的方法解决这一问题，将语句“B=（A=2）+（++A）；”分解如下。

A=2；

++A；

B=A+A；//或B=2*A；

2.程序员对运算符的错误理解

错例1：如果A为0，则对A加1，否则，对A加10，下列语句能否达到目的。

A==0?A+=1：A+=10；上述写法是错误的，程序员的本意如下所示。

A==0?（A+=1）：（A+=10）；而编译器认为的结果如下所示。

（A==0?A+=1：A）+=10；

因为三目条件运算符的优先级高于赋值运算符。

错例2：判断A、B和C是否相等，下列语句能否达到目的。

A==B==C程序员可能认为：只有在A、B和C相等的时候，上述表达式才为真，而编译器所认为的结果如下。

A==（B==C）；

因为赋值符的结合性是从右向左的，上述语句在“A为0、B不等于C”时恒为真。

加括号是个有效的途径，既能强调程序员的意图，提高程序的可读性，又能避免意想不到的错误，如错例1中的语句可写为如下形式。

A==0?（A+=1）：（A+=10）；而对错例2这种类型，要靠多用多练来克服，以深入理解操作符的含义。为了达到目的，错例2中的代码可改写为如下形式。

（A==B）＆＆（A==C）＆＆（B==C）；

注意

为避免出错，请尽量写出简单的代码，不要在一个语句中进行太多的处理，做到一行语句只做一件事情。

2.5　类型转换

C++中有整型、浮点型、布尔型和字符型等基本类型，在后面的章节中还会介绍到复杂数据类型和用户自定义的类型。在进行运算，尤其是对不同的类型进行运算时，可能会引发混乱，为此，C++引入了类型转换机制，一种数据类型能够被转换为另一种数据类型。

2.5.1　赋值转换

赋值转换指的是将一种类型的值赋给另一种类型的变量，这时，值将会转换为接收变量的类型，如下面的语句所示。

A=B；

如果A是long型，B是short型，则程序会将16位（short占两字节）的B提升为32位（long占4字节），并赋给A。直观上讲，类型的表达能力取决于该类型所占的内存位数，从表达能力低的类型转换为表达能力高的类型，即进行字节的扩充通常不会带来什么弊端，将short型值赋值给long型变量并不会改变这个值，只是占用的字节多了而已，但是在其他的一些情况下可能会出现转换问题。

1.将较大的整型（表达能力强）转换为较小的整型

当原来的值不超出目标类型的取值范围时是没有问题的，否则，只能将右侧较低字节赋值给较小的整型，超出的高端部分将被舍弃，见示例代码2.16。

代码2.16　赋值转换：较大整型转换为较小整型AssigmentConversion1

＜------------------------------文件名：example216.cpp------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　long x=0X12345678；//声明一个长整型变量x

06　short y=x；//将x赋值给一个短整型变量y，转换

07　short z=0X5678；

08　cout＜＜"y is"＜＜y＜＜endl；

09　cout＜＜"z is"＜＜z＜＜endl；

10　return 0；

11　}

【代码解析】代码第6行，即将变量x的值，转换并赋值给短整型变量y。

输出结果如下所示。

y is 22136

z is 22136

可见，只有低端的两个字节0X5678复制给了变量y，高端字节0X1234被舍弃了。

2.浮点型转换为整型

此时，浮点型的小数部分会丢失，将整数部分复制给目标整型，当浮点型的整数值不超过目标整型的表示范围时，没有转换问题，否则，整型的结果将是不确定的，见代码2.17。

代码2.17　类型转换：浮点型转换为整型AssigmentConversion2

＜---------------------文件名：example217.cpp---------------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　int x；//声明一个int型变量x

06　float y=2. 7；//声明一个float型变量y

07　double z=5E15；//声明一个double型变量z

08　x=y；//赋值转换

09　cout＜＜"x is"＜＜x＜＜endl；

10　x=z；//赋值转换

11　cout＜＜"x is"＜＜x＜＜endl；

12　return 0；

13　}

【代码解析】代码第8和第10行都是浮点型转换为整型数据。

输出结果如下所示。

x is 2

x is 937459712

C++采取截取（直接将小数部分丢弃）而不是四舍五入，当浮点型值过大时（代码2.17为5×1015），这是，整型量的值是不确定的，在不同的系统中可能为不同的值。

注意

发现代码中可能存在精度缺失时，编译器可能会给出相关警告，关于浮点型的精度和表述范围，参见表2.3。

3.较大的浮点型转换为较小的浮点型

不同的浮点型精度不同，这里的精度可通俗地理解成小数的位数，所以，用较大的浮点型给较小的浮点型赋值时（如用double型值赋给float型变量），数据的精度（有效位数）降低，而且，如果较大浮点型的值超出目标浮点型的表示范围，将得到一个不确定的结果，见代码2.18。

代码2.18　较大的浮点型转换为较小的浮点型AssigmentConversion3

＜--------------------------文件名：example218.cpp----------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　double x=2E50；//较大的浮点数

06　float z=x；//转换

07　cout＜＜"z is"＜＜z＜＜endl；

08　return 0；

09　}

【代码解析】代码第6行是较大的浮点型数据转换为较小的浮点型数据。

输出结果如下所示。

z is 1.#INF

4.整型转换为浮点型

受浮点型的精度限制，将一个很大的整型，如long型的123456789赋值给float型变量时，同样会发生数据丢失，降低精度，见代码2.19。

代码2.19　类型转换：整型转换为浮点型AssigmentConversion4

＜--------------------------文件名：example219.cpp----------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　long x=123456789；//声明一个长整型变量x

06　float y=x；//赋值转换

07　cout＜＜"y is"＜＜y＜＜endl；

08　return 0；

09　}

【代码解析】代码第6行，是整型数据转换为浮点型数据。

输出结果如下所示。

y is 1.23457e+008

可见，y的实际值为123457000，这可能会给运算带来很大的误差。

5.对bool类型进行赋值

将0赋给bool变量时，将被转换为false，而非零值将转换为true。

注意

赋值转换是由编译器自动完成的，比较隐蔽，在丢失数据时，有的编译器可能会给出警告，有的却不会，理解不同类型在内存中的表示规律，有助于克服弊端，写出高质量的代码。

2.5.2　表达式中的转换

当同一个表达式中出现不同类型的量时，C++会根据不同的情况对操作数进行自动转换，这些转换可分为“整型提升”（Integral Promotion）和“运算时的转换”两类。

1.整型提升

在表达式计算中，C++将bool、char、unsigned char、signed char、short和signed short型值自动转换成int型，对bool类型而言，true转换为1，false则转换为0。注意unsigned short向int的转换比较特殊。如果系统中int占4字节，则unsigned char会转换成int，但若系统中int占两字节（即int和short同样长度），则unsigned short会转换成unsigned int，则就避免了数据的丢失。

注意

在表达式中，所有的float类型都转换为double型以提高运算精度。

2.运算时的转换

当运算涉及两种类型时，较小的类型将会被转换成较大的类型，换言之，表达力低的类型将会被转换成表达力高的类型。各类型表达能力从低到高排列顺序如下所示。

int（等价于signed int）→unsigned int→long（等价与signed long）→unsigned

long→float→double→long double

在此要注意一个特殊情况，如果两个操作数分别为long和unsigned int，要根据long和unsigned int的相对长度决定如何转换。如果long能表示unsigned int的值，则将unsigned int转换为long，否则，将两个操作数都转换成unsigned long。

和赋值转换一样，表达式中的转换也是由编译器自动完成的，如下面的代码所示。

double x=4.5；

int y=3；

int z=x-y；

表达式“int z=x-y；”中x和y的类型不同，首先进行表达式中的转换，将int型变量y转换为double型，再与x相加，得到一个double型的结果，在赋值给int型变量z时，赋值号两边的类型不匹配，则进行赋值转换，将double型结果转换为int型。

2.5.3　强制类型转换

C++引入了强制类型转换机制来显式地进行类型转换，强制类型转换的格式有两种，举例来说，为了将double型变量x转换为long型，可使用下列语句中的任意一个。

（long）x；

long（x）；

第1种格式，即“（类型名）值”这种写法是C语言的用法，而第2种格式，即“类型名（值）”是纯粹的C++用法，这样，类型名等价于一个函数，而要转换的值等价于该函数的参数，函数返回值即是目的类型值。

注意

不论是强制类型转换，还是隐式的赋值转换和表达式中的转换，都不会改变变量的值，而是创建一个新的、指定类型的值，因此，不仅可对变量进行类型转换，对常量的显式转换也是合法的，如下所示。

int（3.14）；

float（3.1415926535897）；

以上两种写法都是正确的。

在显式类型转换中，应该特别注意从较高级别的类型转换为较低级别的类型时，容易引起数据的丢失，见代码2.20。

代码2.20　显式转换中的数据丢失Conversion

＜-------------------------------文件名：example220.cpp----------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　unsigned int x=unsigned char（300）；//显式转换

06　cout＜＜"x is"＜＜x＜＜endl；

07　return 0；

08　}

【代码解析】代码第5行，是进行显式数据转换，会导致数据丢失。

输出结果如下所示。

x is 44

代码2.20　中，unsigned char类型在内存中只占一个字节，表述范围0～255，对300只能采取截断处理，只保留低字节，故返回结果为44。

不加限制的显式类型转换往往会给程序带来很多的安全隐患，为此，C++引入了4个强制类型转换操作符，为数据类型的转换提供了相对安全的方案，关于这部分的内容请参考第17章。

2.5.4　函数调用和传递参数时的类型转换

在函数调用时，C++函数的原型声明决定了传递参数的类型及类型转换，这需要对C++的函数机制有足够的了解，因此，关于这部分内容将在第6章讲述函数时详细介绍。

2.6　流程控制语句

程序由数据结构和算法组成，算法是一系列语句的集合，而这一条条的语句，便是C++中的语句，语句是构造程序最基本的单位，程序运行的过程就是执行程序语句的过程，程序语句执行的次序称之为流程控制或控制流程。

结构定理指出：任何程序逻辑都可以用顺序、选择和循环等3种基本结构来表示，在前面给出的示例代码中，程序语句都是顺序执行的，那有没有可能改变程序语句的执行次序呢？答案是肯定的，只要借助于C++提供的分支语句（if……else……语句和switch语句）、循环语句（for循环和while循环）以及转向控制语句（continue、break和goto等）就能够实现。

2.6.1　if……else……选择结构

if……else……的基本结构形式如下所示。

if（表达式1）

语句A；

else

语句B；

程序执行到if语句时，首先计算表达式1，如果结果为true（非0），则执行语句A，否则，执行语句B。此处的语句A、语句B既可以是单条语句，又可以是由多条语句组成的由花括号包裹的块语句，如示例代码2.21所示。

代码2.21　if……else……选择结构IfElse1

＜---------------------------------文件名：example221.cpp---------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　int x；//声明一个int型变量

06　cout＜＜"请输入一个整数："＜＜endl；

07　cin＞＞x；//接收用户键盘输入

08　if（x%3==0）//如果输入的数可被3整除

09　cout＜＜"该数可以被3整除"＜＜endl；

10　else//否则

11　cout＜＜"该数不可被3整除"＜＜endl；

12　return 0；

13　}

【代码解析】代码第8和第10行，就构成了if……else……结构。

输出结果如下所示。

请输入一个整数：

11　（注：键盘输入）

该数不可被3整除当输入另一个数时，有如下结果。

请输入一个整数：

9（注：键盘输入）

该数可以被3整除

说明

按层次格式使用缩进结构，能使代码清晰易读。

在代码2.21中，程序运行到“if（x%3==0）”语句时，首先计算表达式“x%3==0”的值，当x除以3余数等于0时，表达式的值为true,if结构选择“cout＜＜"该数可以被3整除"＜＜endl；”执行，否则为false,if结构选择“cout＜＜"该数不可被3整除"＜＜endl；”执行。

除了if……else……这种基本结构外，C++还支持if……else……结构嵌套，或只有if……的结构。

1.if……else……结构嵌套

一个结构中包含有另一个同样的结构，这称为嵌套，if……else……结构允许嵌套，以实现更多的程序分支，代码2.22用于选出用户输入的3个数中最小的一个。

代码2.22　if……else……结构嵌套IfElse2

＜---------------------------文件名：example222.cpp---------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　int x,y，z；//声明3个int型变量

06　cout＜＜"请输入3个不同的整数："＜＜endl；

07　cin＞＞x＞＞y＞＞z；//接收用户键盘输入

08　if（x＜y）//比较x和y的大小

09　if（x＜z）//比较x和z的大小

10　cout＜＜"最小的数是"＜＜x＜＜endl；

11　else

12　cout＜＜"最小的数是"＜＜z＜＜endl；

13　else

14　if（y＜z）//比较y和z的大小

15　cout＜＜"最小的数是"＜＜y＜＜endl；

16　else

17　cout＜＜"最小的数是"＜＜z＜＜endl；

18　return 0；

19　}

【代码解析】代码第8行到第13行，即一个……if……else结构嵌套另外一个……if……else结构。第14行到第17行，同样如此。

输出结果如下所示。

请输入3个不同的整数：

12　50 6（注：用户输入）

最小的数是6

用户输入的3个整数分别存储在变量x、y和z中，第一层if……else……结构首先比较x和y的大小，如果x小于y，则进入if（x＜z）……else……结构，否则，进入if（y＜z）……else……结构。

注意

多重嵌套时if和else对应的匹配规则，编译器从最前面的else开始，使每个else和前面最近的没有配对的if匹配成if……else……结构。

2.if……结构

if……else……结构中的else……部分是可选的，当else……部分默认时，就成了if……结构，形式如下。

if（表达式1）

语句A

程序执行到if语句时，首先计算表达式1，如果结果为true（非0），则执行语句A，否则，跳过语句A向下执行。

对代码2.23来说，只有当用户正确输入字母Y，才输出“恭喜答对！”的提示，否则，程序不进行任何操作而退出。

代码2.23　if……结构IfElse3

＜--------------------------------文件名：example223.cpp----------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　char x；//声明一个字符变量

06　cout＜＜"猜字母游戏，请输入一个字母："＜＜endl；

07　cin＞＞x；//接收用户键盘输入

08　if（x=='Y'）//如果输入字符为大写字母Y，输出提示

09　cout＜＜"恭喜答对！"＜＜endl；

10　return 0；

11　}

【代码解析】代码第8行，即单独一个if语句的结构。

注意

if和if……else……结构嵌套时，if和else对应的匹配规则不变，从最前面的else开始，使每个else和前面最近的没有配对的if匹配成if……else……结构。

2.6.2　switch结构

switch结构又称开关结构，其基本形式如下所示。

switch（开关表达式）

{

case常量1：

语句序列1；

case常量2：

语句序列2；

……

case常量N：

语句序列N；

default：

语句序列N+1；

}

开关表达式必须是结果为整数值的表达式，case后的常量也必须是整型、char型或枚举常量中的一种。当程序执行到switch结构时，首先对开关表达式进行计算，并从上到下在所有case常量中寻找与表达式的值匹配者，匹配的那条case语句即是switch结构的入口，如果所有的case语句都不匹配，则以default为入口，当没有default且所有的case常量都不匹配时，程序不进入该switch结构。

注意

虽然default部分可以省略，但不推荐这种用法，大部分编译器也会给出相应的警告，即使不进行任何default操作，最好也用如下的形式注明。

default：

；//（空语句。不要忘记分号）

举例而言，一个班级有5个人分别为A、B、C、D和E，对应的学号分别是1、2、3、4和5，现设计程序，由用户输入学号，程序给出对应姓名，见代码2.24。

代码2.24　switch结构用法SwitchSample1

＜--------------------------文件名：example224.cpp---------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　int x；//声明一个int型变量x，用来表示学号

06　cout＜＜"请输入学号（1～5）："＜＜endl；

07　cin＞＞x；//接收用户输入

08　switch（x）//开关结构

09　{

10　case 1：

11　cout＜＜"姓名：A"＜＜endl；

12　case 2：

13　cout＜＜"姓名：B"＜＜endl；

14　case 3：

15　cout＜＜"姓名：C"＜＜endl；

16　case 4：

17　cout＜＜"姓名：D"＜＜endl；

18　case 5：

19　cout＜＜"姓名：E"＜＜endl；

20　default：

21　cout＜＜"请检查输入是否正确"；

22　}

23　return 0；

24　}

【代码解析】代码第8行，使用了switch语句来判断用户输入的数据应该输出什么内容到屏幕。

输出结果如下所示。

请输入学号：

2（注：键盘输入）

姓名：B

姓名：C

姓名：D

姓名：E

请检查输入是否正确

代码2.24　并没有获得正确的结果，本来只打算输出“姓名：B”，却将“case 2：”后的语句都执行了一遍，原因在于switch结构中一个个的case只是行标签，不是选项之间的界限，程序运行到switch结构入口处的case语句时，将依次执行之后的所有语句，不会在到达下一个case处停止，一直到switch结构的闭花括号才会停止。

为使流程提前转出switch结构，可使用break语句，见代码2.25。

代码2.25　break在switch中的应用BreakInSwitch

＜---------------------------文件名：example225.cpp---------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　int x；

06　cout＜＜"请输入学号："＜＜endl；

07　cin＞＞x；

08　switch（x）

09　{

10　case 1：

11　cout＜＜"姓名：A"＜＜endl；

12　break；//break语句跳出当前switch结构，后面的语句不再执行

13　case 2：

14　cout＜＜"姓名：B"＜＜endl；

15　break；

16　case 3：

17　cout＜＜"姓名：C"＜＜endl；

18　break；

19　case 4：

20　cout＜＜"姓名：D"＜＜endl；

21　break；

22　case 5：

23　cout＜＜"姓名：E"＜＜endl；

24　break；

25　default：

26　cout＜＜"请检查输入是否正确"＜＜endl；

27　}

28　return 0；

29　}

【代码解析】代码第12行，使用了break语句跳出当前switch结构，其后面的语句都不再执行。

输出结果如下所示。

请输入学号：

2（注：键盘输入）

姓名：B

使用break的目的是跳出当前switch结构，不再执行后面的语句，break是流程转向语句，关于流程转向语句的介绍，请参考本章稍后的内容。

从上面的描述中，我们可以知道，case标签只起到入口的作用，为了程序的精练，经常采用多个入口共用一个语句的写法，见代码2.26。

代码2.26　多个case标签共用一个语句SwitchSample2

＜---------------------------文件名：example226.cpp---------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　int x；

06　cout＜＜"请输入学号："＜＜endl；

07　cin＞＞x；

08　switch（x）

09　{

10　case 1：case 3：case 5：//case起到的是入口作用

11　cout＜＜"及格"＜＜endl；//多个入口执行的代码相同

12　break；//break语句跳出

13　case 2：case 4：

14　cout＜＜"不及格"＜＜endl；

15　break；

16　default：

17　cout＜＜"请检查输入是否正确"＜＜endl；

18　}

19　return 0；

20　}

【代码解析】代码第10行，即多个case入口都使用一个执行语句。

代码2.26　简单模拟了一个成绩查询系统，当输入的学号为1、3或5时，输出“及格”，当输入的学号为2或4时，输出“不及格”，否则，提醒用户检查输入是否正确。多个case标签共用一个语句，使代码在形式上和逻辑上都十分精练。

关于switch结构还有以下几点要特别注意。

（1）最先匹配的case才是switch结构的入口，因此，switch结构中匹配case前的语句是永远不被执行的。

（2）在同一个switch结构中，不能有相同的case常量。

（3）case常量和表达式值必须是整型、char型和枚举型中的一种，原则上要求两者类型一致，但大多数编译器允许类型不一致的情况发生，此时需比较两者数值是否相等。

（4）switch结构同样支持嵌套。

2.6.3　if……else……结构和switch结构的比较

两者最大的不同在于switch结构只进行相等与否的判断，而if……else……结构还可以进行大于或小于等范围上的判断。

此外，switch无法处理浮点数，只能进行整数的判断，而且，case标签值必须是常量，如果涉及浮点数和变量的判断，应当使用if……else……结构。

只有合理搭配两种结构，发挥各自的优势和长处，才能写出有效率的代码。对某个特定场合，若既能用switch结构，又能用if……else……结构，当分支数大于2时，推荐采用switch结构。

2.6.4　for循环结构

如果需要不断地重复执行某个动作，那么循环结构才会派上用场。for循环结构的基本形式如下所示。

for（初始化表达式；判断表达式；修正表达式）

语句；//循环体

for语句执行过程如下：首先计算“初始化表达式”（循环初值），且仅计算一次；每一次循环之前计算“判断表达式”（循环条件），如果其结果为true（或非0值），则执行“语句”（循环体），并计算“修正表达式”（循环增量），否则，循环终止，程序跳出for结构。

此处的循环体可以是单条语句，也可以是由花括号包裹的块语句，图2.10是for循环执行过程的示意图。

代码2.27　用于输出0～9这10个数字，首先执行初始化表达式，将int型变量i初始化为0，接下来计算判断表达式，i＜10，因为1＜10成立，返回结果为true，所以执行循环体，输出1，然后执行修正表达式，i++，将i的值更新为2，再次计算判断表达式，i＜10，因为i为2，2＜10依旧成立，结果仍为true，执行循环体，依次类推，直到i=10，判断表达式不再满足，for循环结构终止，程序跳到for循环结构后面的语句。

图　2.10　for循环执行过程示意图

代码2.27　for循环结构ForSample1

＜--------------------------文件名：example227.cpp----------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　int i；

06　for（i=0；i＜10；i++）//从0～9，循环执行10次

07　{

08　cout＜＜i＜＜""；

09　}

10　return 0；

11　}

【代码解析】代码第6行，使用了for语句来实现循环结构。

输出结果如下所示。

0 1 2 3 4 5 6 7 8 9

注意

代码2.27　中的循环体只有一行语句，即cout输出操作，for循环结构中的花括号可省略，但for语句后加花括号是一种良好的编程风格，能对程序员起到提醒作用，方便代码阅读和修改，但本书为了缩减篇幅，对单条语句不再使用花括号。

实际上，循环控制变量（或称索引变量）的声明也可以放在初始化表达式中，代码2.27中如下所示。

int i；

for（i=0；i＜10；i++）

{

……

}

等价于

for（int i=0；i＜10；i++）

{

……

}

关于for结构有以下几点要特别注意。

（1）for结构中的3个表达式语句只是语法上的要求。如果不需要循环控制变量，或其声明及其他一些初始化已经由for结构前的语句完成，初始化表达式可以为空。当判断表达式为空时，默认为true。修正操作也可以放在循环体中，而将修正表达式留空。代码2.27与下列代码是等价的。

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　int i=0；

06　for（；i＜10；）

07　{

08　cout＜＜i＜＜""；

09　i++；

10　}

11　return 0；

12　}

（2）新的C++标准规定：在初始化表达式中声明的循环控制变量，只存在于for结构中，也就是说，当程序离开for循环时，变量就消失了。但有些C++遵循的是以前的规则，把在初始化表达式中声明的变量视为在循环之前声明的，因此，在循环结束后，变量仍可用。关于变量的生存期、作用域和可见域的详细介绍请参考第6章。

说明

对VC系列来说，7.1版之前的版本默认遵守老式规则，这主要是因为原有的很多代码都是基于旧式规则编写的，而7.1版之后的编译器能够接受新旧两种规则。

2.6.5　for循环结构嵌套

for循环结构同样允许嵌套，本节通过一个for循环嵌套的经典例子帮助读者理解for循环的嵌套。代码2.28可以分为打印表头、打印一条分隔线和打印表体3个部分，前两个部分都是简单的for循环结构，表体为一个9行9列的结构，通过两层for循环嵌套来完成。

代码2.28　for循环结构嵌套ForSample2

＜---------------------------文件名：example228.cpp---------------------------＞

01　#include＜iostream＞

02　#include＜iomanip＞

03　int main（）

04　{

05　using namespace std；

06　for（int i=1；i＜10；i++）

07　cout＜＜setw（4）＜＜i；//打印表头

08　cout＜＜endl；//换行

09　for（int k=1；k＜10；k++）

10　cout＜＜"——"；//打印一条虚线

11　cout＜＜endl；//换行

12　for（int m=1；m＜10；m++）

13　{

14　for（int n=1；n＜10；n++）

15　cout＜＜setw（4）＜＜m*n；//打印表体

16　cout＜＜endl；

17　}

18　return 0；

19　}

【代码解析】代码第14行，该for语句就嵌套在第12行的for语句中，变成了多重循环语句。

输出结果如下所示。

1 2 3 4 5 6 7 8 9

——

1 2 3 4 5 6 7 8 9

2 4 6 8 10 12 14 16 18

3 6 9 12 15 18 21 24 27

4 8 12 16 20 24 28 32 36

5 10 15 20 25 30 35 40 45

6 12 18 24 30 36 42 48 54

7 14 21 28 35 42 49 56 63

8 16 24 32 40 48 56 64 72

9 18 27 36 45 54 63 72 81

在程序中，为了使输出的数字按行对齐，我们使用了域宽函数setw，用以设置数值或字符串应采用的字符个数（域宽），并按右对齐方式放置，使用setw函数，必须包括头文件iomanip。关于这方面的内容将在第14章“输入输出和文件”中进行详细介绍。

2.6.6　while循环结构

while结构也是一种常用的循环结构，其基本形式如下所示。

while（判断表达式）

语句；//循环体

当程序进入while结构时，先计算判断表达式，若其值为true（或非0），便执行循环体，否则跳出while结构，执行后面的语句，每执行完一次循环体，都要再次计算判断表达式，以决定是执行循环体还是跳出while结构。while结构中的循环体既可以是单条语句，又可以是花括号包裹的块语句。

可以说，while结构是没有初始化表达式和修正表达式的for结构，同样是输出0～9这10个数字，用while循环结构来实现，如代码2.29所示。

代码2.29　while循环结构WhileSample

＜---------------------------文件名：example229.cpp---------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　int i=0；

06　while（i＜10）//从0～9，循环执行10次

07　{

08　cout＜＜i＜＜""；

09　i++；//自增

10　}

11　return 0；

12　}

【代码解析】代码第6行，即是while语句构成的循环结构。

2.6.7　do……while循环结构

do……while结构是C++提供的第3种循环结构，不同于for结构和while结构，do……while结构常被称为出口条件循环，其基本形式如下所示。

do

循环体

while（判断表达式）；

当程序进入do……while结构时，首先执行循环体，然后计算判断表达式，若其值为true（或非0），则继续执行循环体，否则，循环终止，程序跳出。这意味着，使用do……while结构，循环体至少会被执行一次。do……while结构中的循环体既可以是单条语句，也可以是花括号包裹的块语句。

注意

for结构和while结构中的循环体有可能一次也执行不了，但do……while中的循环体至少会被执行一次，使用哪种结构，应视具体情况而定。

2.6.8　循环语句的效率

以下是循环语句书写时的一些准则。

（1）除非你有意如此，否则应避免死循环，即判断表达式一直为true的情况，for循环结构将一直执行下去，设计合适的终止条件十分重要。

（2）for结构中“for（初始化表达式；判断表达式；修正表达式）”和while结构中“while（判断表达式）”后没有分号，当在这两句后添加分号时，编译器认为循环体是空语句。与此相对，do……while结构中，“while（判断表达式）”的后面要有一个分号，不可省略。

（3）鉴于不同系统对小数的操作和解释不尽相同，涉及浮点型变量的比较运算应特别注意，具体请参考2.4.5节。

技巧在设计循环时，要牢记首次判断前的初始化、修正过程和循环终止的条件。

在多重循环中，如果有可能，应当将重复次数多的循环放在里层，循环次数少的循环放在外层，以减少CPU跨切循环层的次数，提高程序的效率。

2.6.9　流程转向控制语句之break

在前面介绍switch结构时，我们提到了流程控制语句break，该语句主要有以下两个用途。

❑跳过switch结构的剩余部分。

❑提早从循环结构（for结构、while结构和do……while结构）中跳出。

注意

break语句对if……else……（包括if结构）无效。

当执行到break语句时，程序将跳转到结构后面的第一条语句开始执行。当有结构嵌套时，break语句只能往外跳一层，破一层壳。

代码2.30　中有一个恒为true的while结构，不断提示用户输入一个整数，当输入的数可以被3整除时，执行break语句跳出循环，否则，循环将一直进行下去。

代码2.30　break流程转向控制语句BreakSample

＜---------------------------文件名：example230.cpp---------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　int num；

06　while（true）//死循环，一直执行下去

07　{

08　cout＜＜"请输入一个整数："＜＜endl；

09　cin＞＞num；

10　if（num%3==0）//当num可被3整除时

11　break；//break用于跳出while循环结构

12　}

13　return 0；

14　}

【代码解析】代码第11行，使用了break语句用于跳出while循环结构。

2.6.10　流程转向控制语句之continue

continue语句称为循环继续语句，用在循环结构（for结构、while结构和do……while结构）中，当执行continue语句时，程序将跳过该循环中的剩余部分，去执行下一次的循环，continue相当于对其后的循环体进行了短路处理。

与代码2.30相似，代码2.31同样采用了一个恒为true的while结构，不断提示用户输入一个整数，当输入的数不可被3整除时，执行continue语句，将后面的循环体短路，直接进行下一次循环，提醒用户输入一个整数，只有当输入的数可被3整除时，程序输出“你输入的数可以被3整除”，并执行break语句跳出循环。

代码2.31　continue流程转向控制语句ContinueSample

＜---------------------------文件名：example231.cpp---------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　int num；

06　while（true）//死循环，一直执行下去

07　{

08　cout＜＜"请输入一个整数："＜＜endl；

09　cin＞＞num；

10　if（num%3！=0）

11　continue；//如果输入的数字不能被3整除，重新输入

12　cout＜＜"你输入的数可以被3整除"＜＜endl；

13　break；//当输入的数字可被3整除时，跳出while结构

14　}

15　return 0；

16　}

【代码解析】代码第11行，调用continue语句重新回到循环入口处执行。

2.6.11　自由转向语句goto

goto语句可以让程序员自由地将流程转到程序的任何地方，程序员只要在程序的某一行前注以标号，便可以使用“goto+标号”的形式将流程转到该行。

很多程序员反对使用goto，甚至建议将其废除，认为其破坏了程序的结构，降低了程序的可读性，已经证明，不使用goto，用顺序、分支和循环3种基本结构足以实现任何流程的算法。

但goto语句至少可以在一个方面大显神通，它能从多重循环中一下子跳到外面，避免了运用很多次break的麻烦，如代码2.32所示。

代码2.32　goto自由转向语句GotoSample

＜---------------------------文件名：example232.cpp---------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　int num；

06　while（true）

07　while（true）

08　while（true）

09　{

10　cout＜＜"请输入一个整数："＜＜endl；

11　cin＞＞num；

12　if（num%3==0）

13　goto ext；//如果输入的数字num可被3整除，一次性跳出

14　//多个while嵌套结构

15　}

16　ext：return 0；//标签ext所在行

17　}

【代码解析】代码第13行，调用goto语句，直接使当前执行的程序跳到第16行ext处执行。

代码2.32　中，goto的合理使用能够使程序一次性跳出3个循环。

2.6.12　程序中止函数exit（）

exit（）是C语言函数库stdlib.h中的一个函数（在新的C++标准中，头文件为cstdlib），它的功能是中止程序的执行，并在退出前对程序占用的资源进行必要的清理。exit（）是一个无返回值的函数，其参数称为退出码，用以通知操作系统当前程序是正常终止（一般为0）还是非正常终止（一般为-1）。

代码2.33　用以计算正方形的面积，边长由用户输入，当输入数小于0时，调用exit（-1）中止程序，当输入数非负时，输出结果。

代码2.33　exit（）程序中止函数Terminate

＜---------------------------文件名：example233.cpp---------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　double x；//声明一个double型变量用于表示边长

06　cout＜＜"请输入正方形的边长："＜＜endl；

07　cin＞＞x；//接收用户键盘输入

08　if（x＜0）//如果输入边长为负

09　{

10　cout＜＜"边长不能为负"＜＜endl；//输出提示信息

11　exit（-1）；//程序异常中止程序并退出

12　}

13　cout＜＜"该正方形的面积是"＜＜x*x＜＜endl；

14　return 0；

15　}

【代码解析】代码第11行，即调用了exit（）函数，异常中止程序，并退出。

输出结果如下所示。

请输入正方形的边长：

-5（注：键盘输入）

边长不能为负或

请输入正方形的边长：

6（注：键盘输入）

该正方形的面积是36

关于异常中止程序和错误的处理机制请进一步参考第16章中的内容。

2.7　小结

本章讲述了C++语言的基本知识，在C++语言中，语句、标识符、变量、常量、函数、预处理指令、输入和输出等重要的概念，应该在实践中逐渐掌握这些概念，合理地加以应用。同时，介绍了C++基本的各种运算符的构成（算术运算符、关系运算符、逻辑运算符、位运算符、条件运算符、赋值运算符、逗号运算符及其他运算符）、各自的优先级和结合性。详细地讲解了由运算符组成的各种表达式、表达式中操作数类型的显式及隐式转换，剖析了C++的流程控制语句，包括分支结构（if……结构、if……else……结构和switch结构）、循环结构（for结构、while结构和do……while结构）、流程转向控制语句（break和continue）和自由转向语句goto等，只有扎实地掌握了这些最基本的内容，才能进一步深入地学好C++，用好C++。

2.8　上机实践习题

1.定义不同类型的变量，并对其进行赋值，最后输出变量结果。

【提示】上述题目主要是要求读者学习变量的类型知识，重点是掌握如何赋值。

【关键代码】

01　int num1=100；

02　longnum2=100000；

03　floatnum3=3. 333；

04　doublenum4=3. 1415926；

05　charc='a'；

06　cout＜＜num1＜＜""＜＜num2＜＜""＜＜num3＜＜""＜＜num4＜＜""＜＜c＜＜endl；

2.利用算术运算符计算（100+303×630-290/4）/24的余数。

【提示】上述题目主要是要求读者学习算术运算符的相关知识，重点是掌握算术运算符的优先级及使用技术。

【关键代码】

01　float a=0；

02　a=（100+303*630-290/4）/24；

03　cout＜＜"a="＜＜a＜＜endl；

3.编写一个程序，要求可以接收用户输入的数值，当数值大于0时，输出“＞0”；当数值小于0时，输出“＜0”；当数值等于0时，输出“=0”。

【提示】上述题目主要是要求读者熟悉比较运算符和选择控制语句。

【关键代码】

01　cout＜＜"please input number："；

02　cin＞＞num；

03　if（num==0）

04　{

05　cout＜＜"输入的数值等于0"＜＜endl；

06　}

07　else

08　{

09　if（num＜0）

10　{

11　cout＜＜"输入的数值小于0"＜＜endl；

12　}

13　else

14　{

15　cout＜＜"输入的数值大于0"＜＜endl；

16　}

17　}

4.利用循环语句，求1～100中的奇数。

【提示】上述题目主要是要求读者熟悉算术运算符和循环控制语句。

【关键代码】

01　int i=1；

02　for（i=1；i＜100；i++）

03　{

04　if（i%2！=0）

05　{

06　cout＜＜i＜＜""；

07　}

08　}

第二篇　C++过程开发

第3章　数组和C风格字符串

除了前面介绍的基本数据类型外，C++还提供了复合数据类型以解决相对复杂的问题，这些数据类型是基于基本的整型、浮点型、char型和bool型等创建的，本章将介绍数组的相关知识和使用方法，以及数组与C风格字符串的关系。

本章主要涉及以下知识点。

❑数组的概念：介绍什么是数组。

❑一维数组：介绍如何声明、初始化及使用一维数组。

❑字符串：介绍如何声明和使用字符串及字符数组。

❑多维数组：介绍如何声明、初始化及使用多维数组。

3.1　什么是数组

将固定数目的同类型数据有序地组合在一起，在内存中连续排列，并用同一个名字来标识，这样一个结构称为数组。数组有以下几个特征。

❑用一个名字命名一组数据。

❑这组数据类型相同。

❑这组数据的数目是确定的。

❑每个数据称为数组的元素，每个元素在数组中都有一个位置，即该元素在数组中的顺序关系，元素在内存中是连续排列的，换言之，数组占有一片连续的内存空间。

❑程序依靠元素在数组中的位置信息对元素进行访问，即数组名加下标的形式。

和基本数据类型一样，要使用一个数组，必须先对其进行声明，声明的格式如下所示。

类型数组名[N1]，[N2]，……，[Nn]；

该声明主要用于提供给编译器如下的信息。

（1）类型：数组中各元素的类型，既可以是基本数据类型，又可以是复合数据类型。

（2）数组名：用什么名字来标识这块连续的内存区域。

（3）维数：标识元素所用到的下标个数，上面声明的是一个n维数组。

（4）元素的数目：可容纳的元素个数，上面声明的数组元素个数为N1，N2，……，Nn。

编译器在接到声明一个数组的指令后，将开辟一个与数组大小相同的、连续存储的内存空间来存放数组中的元素，并用数组名和这块内存区域相关联。

3.2　一维数组

一维数组也称向量，用以组织具有一维顺序关系的一组同类型数据，如某个班级所有人的成绩信息，它和基本数据类型一样，在使用数组前必须先对其进行声明。

3.2.1　一维数组的声明

一维数组的声明形式如下所示。

类型数组名[N1]；其中，N1必须是个整型常量，如5、10或const整型量。假定班级里有10个人，可以用下列形式声明一个数组以存储每个人的成绩。

int age[10]；

这样，编译器将一次性开辟10个存放int型数据的连续内存空间，省去了声明10个int变量的麻烦，代码3.1采用一维数组存储用户输入的10个数据，很显然，用一维数组处理具有顺序关系的数据比用简单变量要方便得多。

代码3.1　使用一维数组计算平均成绩ArraySample1

＜------------------------------文件名：example301.cpp------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　int age[10]；//创建一个int型数组，大小为10

06　int sum=0；//声明一个int型变量sum，初始化为0

07　float aver；//声明一个float型变量aver，表示平均数

08　cout＜＜"请依次输入10个人的成绩："＜＜endl；

09　for（int i=0；i＜10；i++）

10　{

11　cin＞＞age[i]；//接收用户从键盘输入的10个数字

12　sum+=age[i]；//加和

13　}

14　aver=sum/10. 0；//计算平均值

15　cout＜＜"这10个人的平均成绩为："＜＜aver＜＜endl；

16　return 0；

17　}

【代码解析】代码第5行，使用了关键字int声明一个整数型数组，其可以包含10个元素的数据。

输出结果如下所示。

请依次输入10个人的成绩：

95　66 73 82 81 90 67 64 79 98（注：键盘输入）

这10个人的平均成绩为：79.5

从代码3.1中可以看出，采用“数组名[索引（编号）]”可以单独访问数组元素，C++数组从0开始编号，上例中的10个数据可用age[0]，age[1]，……，age[9]来访问，数组中最后一个元素的编号比数组的长度小1，值得注意的是，C++的编译器不会检查程序使用的下标是否有效，例如，如果访问或赋值给一个不存在的元素（如代码3.1中的age[10]），编译器不会指出错误，但程序在运行的过程中可能会出现意想不到的问题。因此，保证下标不越界，保持下标的有效性是十分重要的。

注意

保证下标不越界的方法很简单，数组下标index只要满足“index大于等于0，并且小于（注意不是小于等于）数组元素的个数”即可，否则，必定会出现下标越界的情况。

3.2.2　初始化一维数组

代码3.1　通过用户输入数据对数组中的运算进行赋值，其实，在声明一个数组的同时也可以对各元素进行初始化，初始化表达式按元素的顺序依次写在一对花括号中，元素中间用逗号隔开，代码3.2与代码3.1是等价的。

代码3.2　声明一个数组的同时对其初始化ArraySample2

＜-------------------------------文件名：example302.cpp-----------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　int age[]={95，66，73，82，81，90，67，64，79，98}；//创建一个int型数据，并初始化，大小为10

06　int sum=0；//声明int型变量sum，代表总和

07　float aver；//声明一个float型变量aver，表示平均成绩

08　for（int i=0；i＜10；i++）

09　sum+=age[i]；//求总和

10　aver=sum/10. 0；//求平均成绩

11　cout＜＜"这10个人的平均成绩为："＜＜aver＜＜endl；

12　return 0；

13　}

【代码解析】代码第5行，表示在声明一个数组的同时也对各元素进行初始化。这样在后面的程序中就可以直接使用，而不用再赋值。

输出结果如下所示。

这10个人的平均成绩为：79.5

代码3.2　中第5行并没有指定数组的大小，这在C++中是合法的，编译器会根据初始值的个数自动决定数组的大小。

注意

虽然在声明时对数组进行初始化可以不指定数组的大小，但在声明时将数组大小显式注明是个很好的习惯。

在声明数组时对数组进行初始化，也可以只给出部分元素的初始值，C++编译器根据花括号中的数值对前面的元素依次进行初始化，例如语句“int age[10]={95，66，73}；”在声明一个有10个元素的int型数组的同时，对前3个元素（age[0]、age[1]和age[2]）初始化（赋值）为95、66和73，其他的元素初始化为0，应当注意，此时应指定数组的大小，否则，如果写成“int age[]={95，66，73}；”，编译器会自动定为所声明的数组长度为3。

对一个大数组而言，如果要在数组声明时把所有元素初始化为0，那么可采用如“int name[1000]={0}”的形式；若在声明一个可存放1000个int型元素的数组时，只对第一个元素初始化为0，则编译器会自动将其他元素也初始化为0。

3.2.3　一维数组应用举例

对数组中的元素进行处理，最有效的途径就是使用循环结构，在前面的代码示例中已经简单地体现了如何访问数组元素，下面的例子用以寻找数组中元素的最大值和最小值，见代码3.3。

代码3.3　寻找数组中元素的最大值和最小值ArraySample3

＜------------------------------文件名：example303.cpp------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　int score[10]={95，66，73，82，81，90，67，64，79，98}；//创建包含10个int型的数组，并初始化

06　int maxScore=score[0]，minScore=score[0]；//声明int型变量，用首个元素初始化

07　for（int i=0；i＜10；i++）

08　{

09　if（maxScore＜score[i]）//如果有比maxScore更大的，替换之

10　maxScore=score[i]；

11　if（minScore＞score[i]）//如果有比minScore更小的，替换之

12　minScore=score[i]；

13　}

14　cout＜＜"数组中的最大值是："＜＜maxScore＜＜endl；

15　cout＜＜"数组中的最小值是："＜＜minScore＜＜endl；

16　return 0；

17　}

【代码解析】代码第9～12行，表示在循环遍历数组时，判断当前所指向的元素值，是否比设定的最大值大，如果是则把它保存到maxScore中；若比设定的最小值小则把它保存到minScore中。

输出结果如下所示。

数组中的最大值是：98

数组中的最小值是：64

代码3.3　声明了一个存放10个int型元素的数组，并对其进行了初始化，声明了两个int型变量maxScore和minScore用以记录数组中元素的最大值和最小值，两个变量都用数组中的第一个元素进行初始化，将maxScore和minScore依次和数组中的每个元素进行比较，如果maxScore大于数组中的其他元素，则用该元素对maxScore赋值，同理，如果minScore小于数组中的其他元素，用该元素对minScore赋值，完成对数组的遍历后，即可找出整个数组中的最大值和最小值。

3.2.4　数组操作注意事项

C++不允许对数组进行整体操作，如数组比较和数组数据的输入输出等，必须通过逐一访问数组元素来完成，下列一些用法是不合法的。

（1）用一个已经初始化的数组对另一个数组赋值，即便是元素类型相同，数组大小也相同，这样的用法也是不允许的。

int x[3]={7，8，9}；

int y[3]；

y=x；//错误（2）对数组进行整体的输入输出。

int x[4]={2，3，4，5}；

cout＜＜x；//错误

cin＞＞x；//错误

但是，对后面所介绍的字符数组来说，用cout和cin等进行整体的输入输出却是合法的。

（3）数组比较。

int x[3]={1，2，3}；

int y[3]={4，5，6}；

if（x＜y）//错误

{……}（4）数组整体运算。

int x[5]={5，6，7，8，9}；

int y[5]={2，3，4，5，6}；

x+=y；//错误，其他运算与此同

3.3　C风格字符串

在第2章中已经介绍了字符串常量的知识，C语言中的字符串是用一维字符型数组来实现的，编译器把每个字符串理解为一个以'\0'（空字符，null character）为结束符的一维字符数组，这种类型的字符数组常被称为C风格字符串，C++语言借鉴了这一用法。

说明

如无特别说明，本书中用到的字符串均是C风格字符串。

字符数组指的是所有元素都是char型的数组，C风格字符串是字符数组的一种特例。

3.3.1　C风格字符串的声明

声明一个C风格字符串，使其内容为“I Love C++！”，代码如下所示。

char str[]={'I'，''，'L'，'o'，'v'，'e'，''，'C'，'+'，'+'，'！'，'\0'}；

上述代码在声明数组时没有指定大小，这样，编译器就会自行计算决定数组的大小。对于C风格字符串的声明，推荐采用这种方式，免去了程序员自行计数的麻烦，如果由程序员指定的数组太小，编译器会报错，太大就会浪费空间。但若在声明语句中无初始化表达式，则应该指出该字符数组的大小。

可以看出，存储一个字符串所需字节数比该串的字符数多1，如存储“Hello”，该串有5个字母，但C风格字符串需要6个字节。而且，上述声明方式看上去很麻烦，要一个字母一个字母地用单引号包裹起来，还要记着后面的'\0'，实际上，C++还提供了另一种声明C风格字符串的方法—使用字符串常量，如下所示。

char str[]="I Love C++！"；

注意

所有的字符串常量隐式地包含了结尾的空字符'\0'，不用再显式地注明，使用时十分方便。

3.3.2　字符数组的cin和cout

仅仅由字符组成，但结尾不是'\0'的数组不是C风格字符串，如下所示。

char str[]={'H'，'e'，'l'，'l'，'o'}；

如此声明的str只能称为字符数组，空字符'\0'对C风格字符串十分重要，很多与之对应的处理函数和对象（包括cout和cin），都逐个处理C风格字符串的字符，直到遇到空字符为止，因此，对于“char str[]={'H'，'e'，'l'，'l'，'o'}；”，cout在输出5个字符后，还会继续输出内存中后面字节的内容，直到遇到空字符为止，这时输出结果是不确定的，但由于内存中存在大量的空字节，所以这个过程应该可以很快停止。

注意

在使用中，应特别留意这种非C风格字符串的普通字符数组的用法。

可以将C风格字符串（甚至是普通的字符数组）当成一个整体来进行输入输出操作，见代码3.4。

代码3.4　C风格字符串的输入输出InputAndOutputAC-String

＜----------------------文件名：example304.cpp--------------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　char str[]="Welcome to the C++world！"；//创建字符数组，用C风格字符串为其初始化

06　cout＜＜str＜＜endl；//输出C风格字符串

07　cout＜＜"请重新输入一个字符串："＜＜endl；

08　cin＞＞str；//字符数组整体输入

09　cout＜＜str＜＜endl；

10　return 0；

11　}

输出结果如下所示。

Welcome to the C++world！

请重新输入一个字符串：

Hello everyone（注：键盘输入）

Hello

【代码解析】代码第5行声明了一个C风格字符串，大小为26个字节（25个字符加1个空字符），并进行了初始化。第6行“cout＜＜str＜＜endl；”将str作为标识，对C风格字符串进行了整体输出，第8行“cin＞＞str；”可以向C风格字符串中存入一个新的字符串，该字符串的长度不能超出初始化时所开辟的内存空间字节数减1，因为还要留出一个字节存放空字符'\0'。

在上例中输入了“Hello everyone”，并没有超过25字节（26个字节-1），但为什么输出结果只有“Hello”呢，这是因为用cin＞＞进行输入操作时，跳过前导空白，从第一个有效字符起，向对应的字符数组中依次存入字符，直到遇到一个空白字符才结束，这便完成了一个字符串的输入，由于“Hello everyone”中“Hello”后面是空格，所以，只存储了“Hello”到C风格字符串str中，“everyone”则被忽略掉了。

注意

代码3.4　中，如果输入的连续字符串超过了26个字节，会引发内存错误。

3.3.3　get（）函数和getline（）函数

那么如何将含空格的字符串读到C风格字符串中呢？这要用到istream类中提供的getline（）和get（）函数，两个函数都用于读入一行输入，直至遇到换行符，但getline（）函数丢弃了换行符，而get（）函数也将换行符存入了C风格字符串中，关于istream类的详细介绍请参考第14章，这里先介绍两个函数的用法，见代码3.5。

代码3.5　get（）和getline（）的用法GetC-String

＜-----------------------文件名：example305.cpp-------------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　char name[16]；//创建一字符数组，表示姓名

06　char school[50]；//创建一字符数组，表示学校名

07　cout＜＜"请输入你的姓名："＜＜endl；

08　cin. get（name，16）；//使用cin提供的get函数读取一串字符

09　cin. get（）；//从输入流中读出换行符

10　cout＜＜"请输入你所在的学校名："＜＜endl；

11　cin. getline（school，50）；//使用getline函数读取一串字符

12　cout＜＜"你的名字是："＜＜name＜＜endl；

13　cout＜＜"你所在的学校是："＜＜school＜＜endl；

14　return 0；

15　}

输出结果如下所示。

请输入你的姓名：

Nedved Pavel（注：键盘输入）

请输入你所在的学校名：

Juventus Italy（注：键盘输入）

你的名字是：Nedved Pavel

你所在的学校是：Juventus Italy

【代码解析】代码第11行，在读入学校名时，用了这样的语句：“cin.getline（school，50）；”，getline（）函数有两个参数，一个是字符数组名，另一个是要读取的字符数，如果这个参数为50，那么函数最多读取49个字符，保留的一个空间用于存储自动在末尾添加的空字符'\0'，在读取指定数目的字符（第2个参数减1）后，或者是遇到换行符时，getline（）函数停止读取。

“cin. get（name，16）；”与“cin.getline（school，50）；”用法完全一致，两者唯一的区别在于对换行符（用户按回车键输入的字符）的处理。getline（）在遇到换行符时，将换行符从输入队列中提取出来，抛弃掉，而get（）函数不再提取并抛弃换行符，仍将其留在输入队列中。

所以，代码3.5中第9行语句“cin.get（）；”不可缺少，其用以从输入队列中提取一个字符，也就是把“cin.get（name，16）；”遗留在输入队列中的换行符提取出来，否则，若没有“cin.get（）；”，在执行“cin.getline（school，50）；”时，输入队列中的第一个字符就是换行符，因此，getline（）函数会认为已经到达行尾，却没有任何可以读取的内容，所以school将会成为只有一个'\0'的空字符串。因此，在使用get（）函数时，一定要采用如下调用序列。

cin.get（数组名，数组大小）；

cin.get（）这与

cin.getline（数组名，数组大小）；

是等价的。

注意

对get（）函数而言，有一个更简洁的写法是“cin.get（数组名，数组大小）.get（）；”。

3.3.4　cin与get（）/getline（）函数的搭配问题

在cin和get（）/getline（）函数搭配使用时，常常会出现问题，见代码3.6。

代码3.6　cin与get（）/getline（）函数的搭配CinAndGet

＜--------------------------------文件名：example306.cpp----------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　char name[16]；

06　char school[50]；

07　cout＜＜"请输入你的姓名："＜＜endl；

08　cin＞＞name；//使用cin读取一串字符到字符数组name中

09　cout＜＜"请输入你所在的学校名："＜＜endl；

10　cin. getline（school，50）；//使用getline函数读取一串字符到字符数组

11　//school中

12　cout＜＜"你的名字是："＜＜name＜＜endl；

13　cout＜＜"你所在的学校名："＜＜school＜＜endl；

14　return 0；

15　}

输出结果如下所示。

请输入你的姓名：

Nedved（注：键盘输入）

请输入你所在的学校名：

你的名字是：Nedved

你所在的学校是：

【代码解析】代码第10行，调用cin.getline（）时，用户还没来得及输入学校名，程序就结束了，问题在于，在使用cin读取C风格字符串或其他变量时，用户敲击回车键输入的换行符留在了输入队列中，后面的get（）或getline（）函数看到换行符时，会认为已经到达行尾，却没有任何可读取的内容，因此，school会是只有一个'\0'的空字符串，解决的方法同样是在cin语句的后面使用“cin.get（）；”，即采用下面的语句序列。

cin＞＞……

cin.get（）；

注意

上述语句的一个简洁写法为“（cin＞＞……）.get（）；”。

3.3.5　访问C风格字符串中的某个元素

C风格字符串是一类特殊的字符数组（最后一个元素是空字符'\0'），可以通过“数组名+下标索引”的方式对某个元素（即字符串中的某个字符）进行访问和读写，见代码3.7。

代码3.7　访问C风格字符串中的某个元素ElementAccess

＜-------------------------------文件名：example307.cpp-----------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　char str[]="Hello,C++"；//创建一字符数组str，常用C-String为其初始化

06　cout＜＜"修改前："＜＜str＜＜endl；

07　str[6]='J'；//将str中第7个元素修改为J

08　cout＜＜"修改后："＜＜str＜＜endl；

09　str[5]='\0'；//将str中的第6个字符修改为空字符'\0'

10　cout＜＜"截断处理："＜＜str＜＜endl；

11　return 0；

12　}

输出结果如下所示。

修改前：Hello,C++

修改后：Hello,J++

截断处理：Hello

【代码解析】代码第7行，通过修改C风格字符串str中的第7个元素（str[0]为第1个元素），将字符'C'成功地修改成了字符'J'，将其从“Hello,C++”转换成了“Hello,J++”。代码第9行，将C风格字符串中的某个字符替换为空字符'\0'，该字符串将被截断，语句“str[5]='\0'；”将str中的第6个字符替换为空字符，则cout只输出空字符之前的5个字符“Hello”。

3.3.6　C风格字符串处理函数

作为一种特殊类型的字符数组，C风格字符串可以使用cout和cin做整体的输入输出，但是，其他整体操作，如赋值、比较和连接等都是不允许的，如下所示。

char x[10]，y[10]="ABCDEFGHI"；

x="123456789"；//错误

x=y；//错误

if（x＜y）//错误

{……}

x+=y；//错误

和普通数组一样，原则上，这些操作都必须逐个对字符串中的元素进行操作，但为了方便程序员对C风格字符串进行处理，系统提供了相应的库函数来完成这些操作，标准头文件cstring（旧标准中为string.h）提供了很多相关函数的声明，常用的字符串处理函数如表3.1所示。

注意

char*指的是函数返回值是字符指针型，关于指针的详细介绍请参考第4章。实际上，用指针处理C风格字符串是很多函数经常采用的方式，第4章会探讨数组名和指针的关系以及这些C风格字符串处理函数的写法，本节讨论的仅是这些函数的用法。函数中的数组，指的都是最后一个元素是空字符的字符数组，即C风格字符串。

C风格字符串处理函数使用范例如代码3.8所示。

代码3.8　C风格字符串处理函数使用范例C-StringFunctionSample

＜--------------------------------文件名：example308.cpp---------------------＞

01　#include＜iostream＞

02　#include＜cstring＞

03　int main（）

04　{

05　using namespace std；

06　char password[]="CHINA"；//创建字符数组，用字符串常量初始化，大小为6

07　char input[6]，copyInput[6]；

08　char catInput[]="I LOVE"；//创建字符数组，用字符串常量初始化，大小为7

09　cout＜＜"请输入密码（不超过5位）："＜＜endl；

10　cin＞＞input；

11　cout＜＜"你输入的密码位数为："＜＜strlen（input）＜＜endl；//长度计算

12　cout＜＜"转换成大写形式："＜＜strupr（input）＜＜endl；//全部转换成大写形式

13　if（strcmp（strupr（input），password）==0）//比较两个字符串是否相等

14　cout＜＜"密码正确"＜＜endl；

15　strcpy（copyInput,input）；//复制函数

16　cout＜＜"执行字符串复制操作后："＜＜copyInput＜＜endl；

17　strcat（catInput,input）；//拼接函数，将input接在catInput后

18　cout＜＜"执行字符串连接操作后："＜＜catInput＜＜endl；

19　return 0；

20　}

【代码解析】代码第11行是调用strlen（）函数计算字符串长度；代码第12行是调用strupr（）函数将全部的字符转换成大写形式；代码第13行是调用strcmp（）函数进行字符串比较；代码第15行是调用strcpy（）函数进行函数复制；代码第17行是调用strcat（）函数将两个字符串拼接在一起。

输出结果如下所示。

请输入密码（不超过5位）：

china（注：键盘输入）

你输入的密码位数为：5

转换成大写形式为：CHINA

密码正确

执行字符串复制操作后：CHINA

执行字符串拼接操作后：I LOVE CHINA

代码3.8　演示了如何使用库函数来处理C风格字符串，cstring头文件中有更多处理函数的声明，在需要对字符串进行操作时，请尽量使用库函数。

C++通过添加string类扩展了C++库。现在不仅仅可以使用字符数组来处理字符串，还多了一种选择—使用string类型的对象。之所以一直强调“C风格字符串”，主要是为了与功能更强大的string对象进行区分，关于这方面的详细内容，请参考第18章。

3.4　多维数组

如果一个一维数组中的每个元素都是同类型同大小的一维数组，情况会是什么样子呢？图3.1表示了一个（M+1）×（N+1）的二维数组，二维数组实质上是对一维数组的扩展，其中的每个元素要用两个下标来表示，前一个称为行下标，后一个称为列下标。

依次类推，如果二维数组的每个元素都是同类型同大小的一维数组时，可以表示成三维数组的形式，用3个下标来表示其中的元素。维数可以逐渐增加，对n维数组来说，需要用n个下标来表示其中的元素。

图　3.1　二维数组示意图

3.4.1　声明一个多维数组

和一维数组一样，声明的主要作用就是提供给编译器足够多的信息，以便编译器在内存中开辟一块连续的，满足大小要求的内存区域，并将数组名和这块区域关联起来，这些信息包括：数组名、元素类型、数组的维数和数组的大小。多维数组的大小是各维大小之积。

一个二维数组可以用下列语句来声明。

int sz[2][3]；这声明了一个2×3的二维数组，有2行3列共计6个元素，对多维数组来说，元素的编号仍旧是从0开始的，所以，对上面这个二维数组来说，这6个元素分别如下所示。

sz[0][0]、sz[0][1]、sz[0][2]、sz[1][0]、sz[1][1]和sz[1][2]

3.4.2　初始化多维数组

多维数组同样可以在声明的同时对其中的元素进行初始化，如下所示。

int sz[3][4]={{1，2，3，4}，{5，6，7，8}，{9，10，11，12}}；或对部分元素进行初始化，如下所示。

int sz[3][4]={{1，2}，{5，6}，{9，10}}；

上面的语句仅初始化数组每一行的前两个元素，而未给其他元素赋初值，在这种情况下，其他元素将自动初始化为0。

注意

在声明和初始化其中的元素时，同一行中的元素用花括号包裹，并且用逗号隔开。

当声明语句中提供有全部元素的初始值时，第一维的大小可以缺省，如下所示。

int sz[][4]={{1，2，3，4}，{5，6，7，8}，{9，10，11，12}}；只提供部分元素的初始化值时，必须能让编译器判断出第一维的大小，这个参数才可省略，如下所示。

int sz[][4]={{1，2，3，4}，{2，3}，}；//等价于int sz[][4]={{1，2，3，4}，{2，3，0，0}}；编译器认为是2×4维

注意

除第一维大小外，其余维的大小一定不能省略，否则程序编译时会出错。因为多数组元素在存取时，需要在一维和多维之间进行转换，如果省略其余维的大小，编译器则无法计算出它们之间的对应关系。

3.4.3　多维数组应用举例

下面给出的示例代码3.9演示了如何转置一个二维数组。所谓转置，指的是将数组中的元素关于对角线互换。

代码3.9　将二维数组中的数据关于对角线互换TwoDimentionArray

＜----------------------文件名：example309.cpp--------------------------------＞

01　#include＜iostream＞

02　#include＜iomanip＞

03　int main（）

04　{

05　using namespace std；

06　const int row=4；

07　int temp；

08　int i,j；//创建一个2维数组sz，并初始化

09

10　int sz[row][row]={{1，2，3，4}，{5，6，7，8}，{9，10，11，12}，{13，14，15，16}}；

11　cout＜＜"初始状态："＜＜endl；

12　for（i=0；i＜row；i++）//初始状态输出

13　{

14　for（j=0；j＜row；j++）

15　cout＜＜setw（4）＜＜sz[i][j]；

16　cout＜＜endl；

17　}

18　for（i=0；i＜row；i++）

19　for（j=i+1；j＜row；j++）//关于对角线对称交换元素

20　{

21　temp=sz[i][j]；

22　sz[i][j]=sz[j][i]；

23　sz[j][i]=temp；

24　}

25　cout＜＜"翻转后状态："＜＜endl；

26　for（i=0；i＜row；i++）//交换后状态输出

27　{

28　for（j=0；j＜row；j++）

29　cout＜＜setw（4）＜＜sz[i][j]；

30　cout＜＜endl；

31　}

32　return 0；

33　}

输出结果如下所示。

初始状态：

1 2 3 4

5 6 7 8

9 10 11 12

13　14 15 16

翻转后状态：

1 5 9 13

2 6 10 14

3 7 11 15

4 8 12 16

【代码解析】代码以二维数组为例子，如代码第18～19行对多维数组元素的访问和处理也是通过循环结构来实现的，多维数组与一维数组最大的不同在于访问元素时下标的个数，以及运算在内存中的排列机制。

3.4.4　多维数组在内存中是如何排列元素的

维数决定了数组中元素的组织方式及访问元素说用的下标个数，从本质上讲，所有的数组在内存中都是一维线性的。以二维数组为例，内存中是先放第一行的元素，再放第二行的元素，依次类推。下面给出了大小为3×4的二维数组A的排列顺序。

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]

多维数组的存储方式与此类似，可以将下标看成是一个计数器，像计数的万位、千位、百位、十位和个位一样，右边的下标（靠后的下标）是低位，每一位都在上下界间变化，变化的范围是0到声明时指定的下标值减1，当某一低位计数器超出范围时（达到声明时指定的下标值），左边下标加1，同时该低位计数器及其右边的更低位计算器置0（回到下界）。这样，最左边一维下标变化是最慢的，最右边一维下标变化最快，下面给出3×3×3的三维数组B中元素在内存中的排列顺序。

B[0][0][0]-＞B[0][0][1]-＞B[0][0][2]-＞

B[0][1][0]-＞B[0][1][1]-＞B[0][1][2]-＞

B[0][2][0]-＞B[0][2][1]-＞B[0][2][2]-＞

B[1][0][0]-＞B[1][0][1]-＞B[1][0][2]-＞

B[1][1][0]-＞B[1][1][1]-＞B[1][1][2]-＞

B[1][2][0]-＞B[1][2][1]-＞B[1][2][2]-＞

B[2][0][0]-＞B[2][0][1]-＞B[2][0][2]-＞

B[2][1][0]-＞B[2][1][1]-＞B[2][1][2]-＞

B[2][2][0]-＞B[2][2][1]-＞B[2][2][2]-＞

由于多维数组在内存中是线性存储的，元素存放的顺序也是确定的。在声明一个数组的同时对其进行初始化时，也可以采取下列形式（以二维数组为例）。

int x[3][4]={1，2，3，4，5，6，7，8，9，10，11，12}；

技巧仍旧推荐前面介绍的按行初始化的方式，那样更直观、通用。

3.5　小结

本章主要介绍了数组与C风格字符串的知识。数组是同类型变量组成的集合，通过下标运算符可访问数组中特定的元素。C风格字符串是一类特殊的一维字符数组，除存放字符串中的各个字符外，C风格字符串最后一个字符为空字符"\0"。除字符数组可以用作整体的输入输出外，数组都不支持整体的操作，必须从元素的角度对数组进行控制。为了方便C风格字符串的处理，C++的库函数提供了一些常用的处理函数，这些函数的声明可以在头文件cstring中找到。

数组越界是一个严重的错误，使用数组时一定不能越界。由于编译器不对程序中出现的数组越界进行检查，所以，最好在使用时，对数组是否越界进行检查。

3.6　上机实践习题

1.声明一个包含10个元素的一维数组，并对其进行初始化，最后输出结果。

【提示】上述题目主要是要求读者学习一维数组的知识，重点是掌握如何声明、初始化及使用。

【关键代码】

01　int array[]={1，1，2，3，5，8，13，21，34，55}；

02　int i=0；

03　for（i=0；i＜10；i++）

04　{

05　cout＜＜array[i]＜＜""；

06　}

2.利用cin和cout得到用户输入的字符串，在输出内容后输出长度。

【提示】上述题目主要是要求读者学习字符串相关知识，重点是掌握字符串函数的使用过程。

【关键代码】

01　char str[100]；

02　cout＜＜"Please input string："＜＜endl；

03　cin＞＞str；

04　cout＜＜str＜＜"length is"＜＜strlen（str）＜＜endl；

3.声明一个包含20个元素的二维数组（2×10），并对其进行初始化，最后输出结果。

【提示】上述题目主要是要求读者熟悉多维数组的知识，重点掌握多维数组的使用。

【关键代码】

01　int array[2][10]={{1，1，2，3，5，8，13，21，34，55}，{1，1，2，3，5，8，13，21，34，55}}；

02　int n=0；

03　int i=0；

04　for（n=0；n＜2；n++）

05　{

06　for（i=0；i＜10；i++）

07　{

08　cout＜＜array[n][i]；

09　}

10　cout＜＜endl；

11　}

12　return 0；

第4章　指针和引用

指针和引用是C++中两个重要的复合数据类型，使用范围十分广泛，若使用得当，它们就是程序员手中的神兵利器，但如果程序员对其理解肤浅，胡乱应用，只会让事情一团糟，本章将带领你一步步学会使用指针和引用。

本章主要涉及以下知识点。

❑指针的定义与使用：介绍关于指针变量的概念及其使用。

❑指针的运算：介绍对于指针的一些常用操作方法。

❑动态内存分配：介绍C++中的内存管理与分配方法。

❑常量指针：介绍关于C++中的常量指针的概念及特点。

❑指针与数组：讲解指针数组的声明与使用，同时对指针数组的概念进行了介绍。

❑特殊的引用：介绍C++中引用的概念、特点及其使用方法。

4.1　指针的定义与使用

通过前面的学习，我们了解到内存是按字节排列的存储空间，每个字节都有一个编号，被称为“地址”，程序中用到的数据和声明的变量就存放在这一个个的字节中，不同类型的数据和变量占用的字节数不同，如short型变量占用两个内存字节，习惯上将某个变量占用的字节数称为内存单元，内存单元占用的字节数随其存储变量的数量而有所不同。

通过变量名可以访问该变量对应的内存单元，实际上，我们还可以直接通过地址来访问某个内存单元，为形象地说明这个问题，请看下面这个比喻。

假设我们要去A公司给B经理送信，有两种方法。一是告诉传达室人员，这封信要送给B经理，由传达室人员转送，二是知道B经理在C楼D层E房间，通过这个地址直接将信送给B经理。此例将内存比做A公司，B经理就是某个变量，C楼D层E房间是变量所占用的内存空间的地址，传达室人员可以理解为编译器。在程序中，我们可以直接通过名字来访问某个变量，也是借助了编译器的帮助，编译器维护了一个变量名和地址的映射表，同时，如果知道了某个变量内存单元的地址，我们就可以直接对这块内存进行访问，为了存储地址信息，C++提供了指针这个复合数据类型。

与使用变量名相比，使用地址（指针）访问内存单元有更大的灵活性。通过传达室人员只能转发消息，转送东西，而直接到办公室则可以和B经理直接进行沟通，但这种灵活性也可能会带来一定的危险，有人可能会直接到办公室影响B经理的正常工作。因此，指针是把“双刃剑”。

4.1.1　声明一个指针变量

指针是一种数据类型，基于该类型声明的变量称为指针变量，该变量存放在内存中的某个地址，和普通的变量一样，在使用指针变量之前应先对指针变量进行声明。

类型*指针变量名；//如int*pNum；

“*”表示语句声明的是一个指针变量，类型指定了指针所指的内存单元的数据类型。

注意

可以将int*理解成一种复合类型，是指向int型数据的指针。

应当注意下面的语句。

int*pNum1，pNum2；

声明了一个指针（pNum1）和一个int型变量（pNum2），在一次性声明多个指针时，每个指针变量名前都要加*，“int*pNum1，*pNum2；”便能一次性声明了两个指针变量。

注意

有的程序员提倡传统的用法，将星号与指针变量名靠近，这样，“int*pNum1，pNum2；”的意义似乎更好理解，使用哪种写法取决于你的习惯。

指针变量存储的内容是内存中某个字节的地址，指针变量占用的内存字节数随系统的不同有所不同，在普通的windows 32平台上，指针变量占4个字节，但在其他一些内存模块中，指针变量可能只占用两个字节。简单地说，指针变量占用的字节多少完全取决于程序的内存空间的大小。

代码4.1　指针变量的值及其占用的字节数PointerSample

＜---------------------------------文件名：example401.cpp---------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　double num=3；//声明一个double型变量num

06　double*pNum；//声明一个double型指针变量pNum

07　pNum=＆num；//用num的地址为pNum赋值

08　cout＜＜"pNum在内存中的位址是："＜＜＆pNum＜＜endl；

09　cout＜＜"pNum的值为："＜＜pNum＜＜endl；

10　cout＜＜"num在内存中的地址为："＜＜＆num＜＜endl；

11　cout＜＜"可以使用指针访问num："＜＜*pNum＜＜endl；//输出指针pNum占据的内存大小

12

13　cout＜＜"pNum（指针类型）在内存中的字节数："＜＜sizeof（pNum）＜＜endl；

14　//输出double变量num占据的内存大小

15　cout＜＜"num（double型）在内存中的字节数："＜＜sizeof（num）＜＜endl；

16　return 0；

17　}

输出结果如下所示。

pNum在内存中的位址是：0012FF74

pNum的值为：0012FF78

num在内存中的地址为：0012FF78

可以使用指针访问num：3

pNum（指针类型）在内存中的字节数：4

num（double型）在内存中的字节数：8

【代码解析】代码第5行，代码“double num=3；”声明了一个double型变量num，并对其赋初值为3，编译器将在内存中开辟一块8个字节的区域，将num和这8个字节关联起来，并将这8个字节初始化为3，代码第6行double*pNum声明了一个指向double类型的指针，但并没有对其进行初始化，编译器将在内存中开辟一块4个字节的内存区域，并将其和pNum关联起来，同时，代码第7行，语句“pNum=＆num；”是对指针变量赋值，“＆”称为取地址符，含义是将num在内存中的地址“0012FF78”赋值给pNum，代码4.1的内存模型示意图如图4.1所示，每个字节有8个二进制位，用内存地址来标识，对指针变量而言，其中存储的内容恰恰是某个数据在内存中的地址。

可以通过“*指针变量名”来对指针所指向的内存单元进行访问，代码4.1将pNum声明为一个指向double类型的指针。因此，编译器知道*pNum是一个以double类型存储的值。这种访问方式称为间接访问，以区别于通过变量名“num”访问该内存单元的字节访问方式，从本质上说，两者是等价的。对本例而言，都是对0012FF78～0012FF7F这8个字节（double型）进行访问。

图　4.1　指针和变量内存模型示意图

注意

声明语句“double*pNum；”和间接访问“*pNum”中的“*”意义不同，前者称指针运算符，与前面的double结合构成“指向double型变量的指针”类型，后者是间接引用符，不要将两者混淆。

注意

内存单元的分配是由编译器根据程序中的声明语句自动完成的，程序员关心的是借助某个变量的地址来对其进行访问和控制，往往不在意该地址的具体数值。

对同一个系统，指向不同类型的指针变量占用的内存字节数一般是相同的，但也有例外，见代码4.2。编译器通过指针变量的声明来确定其所指数据的类型和长度，从深层次讲，指针指向的是内存中的1个字节，编译器会根据该字节和声明语句中指针变量的类型来决定对那几个字节进行操作，这对所有类型的指针变量是等价的。

代码4.2　指向不同类型的指针变量占用相同的内存字节数SizeofPointer

＜--------------------------文件名：example402.cpp----------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　double*pNum,num=0；//double型指针pNum、double型变量num

06　pNum=＆num；

07　short*pSnum,sNum=0；//short型指针pSnum、short型变量sNum

08　pSnum=＆sNum；

09　char*pChar,chr='A'；//char指针pChar、char型变量chr

10　pChar=＆chr；

11　cout＜＜"指向double型的指针变量pNum占用的内存字节数："＜＜sizeof（pNum）＜＜endl；

12　cout＜＜"指向short型的指针变量pSnum占用的内存字节数："＜＜sizeof（pSnum）＜＜endl；

13　cout＜＜"指向char型的指针变量pChar占用的内存字节数："＜＜sizeof（PChar）＜＜endl；

14　return 0；

15　}

【代码解析】代码第11～13行是调用sizeof（）函数，来取得当前变量所占用的内存字节数，该函数的应用将在后面进行讲解。

输出结果如下所示。

指向double型的指针变量pNum占用的内存字节数：4

指向short型的指针变量pSnum占用的内存字节数：4

指向char型的指针变量pChar占用的内存字节数：4对前面提到的概念进行辨析和总结，如下列代码所示。

double num=3；

double*pNum；

pNum=＆num；

❑num：double类型的变量。

❑pNum：指向double类型的指针变量，其值是num的地址。

❑＆num：返回变量num的地址，与pNum等价。

❑*pNum：pNum所指的变量，间接访问方式，与num等价。

❑＆（*pNum）：与＆num（即pNum）等价，num的地址。

❑*（＆num）：与*pNum（即num）等价，变量num。

注意

指针变量常常以字母p（代表pointer）开头，以增强程序的可读性。

4.1.2　初始化指针变量

声明指针变量时，C++并不会自动对其进行初始化，这时，指针变量的值是随机的，在内存中乱指一气。因此，通过指针间接访问所指的内存区域是十分危险的，因为它完全不知道自己在做些什么。通过取地址符（＆）给指针变量赋值是个有效的手段，它可以在声明一个指针变量的同时完成其初始化。

int num=1；

int*pNum=＆num；

上述语句声明了一个指向int型变量的指针pNum，并用num在内存中的地址对其进行赋值。

在对指针使用间接引用符前，一定要对其进行初始化，在声明的同时初始化或赋值，使其有一个确定的值，对于无处可指的指针变量，也要将其初始化为NULL即0，也就是空指针。

C++编译器根据声明语句创建指针时，只分配用来存储地址的内存，不会分配用来存储指针所指数据的内存，为数据提供空间必须由程序员来完成，这是一个独立的步骤。上述代码中的“int num=1；”通知编译器开辟一块用以存储int型数据的内存区域，才能将num的地址赋给pNum。

注意

没有进行过初始化的指针操作会给程序带来灾难性的后果。

4.1.3　指向指针的指针

指针变量也是变量，要占据一定的内存空间，它也有地址。因此，可以用一个指针指向其地址，这称为指向指针的指针或二级指针。可以通过“**”声明一个二级指针，如下所示。

double num；

double*pN=＆num；

double**ppN=＆pN；

上面的指针可以看成指向double*变量类型的指针，若有需要还可以定义三级，甚至更高级的指针。

4.1.4　指针赋值

C++允许同类型的指针间的赋值，如图4.2所示，pN1和pN2是两个相同类型的指针，执行“pN2=pN1；”这样一个赋值操作后，pN1和pN2指向同样的地址，也就是说，两个指针指向同一个内存单元，对*pN2的任何改动都会影响*pN1的值，反之亦然。

通常来说，不同类型的指针之间是不能进行相互赋值的，第2章已经介绍过。不同类型的变量占用的内存字节数和存放形式不同，不同类型的变量赋值时，C++编译器的类型转换机制实现内存单元的转化，对短字节量进行扩展，对长字节量实施截断。对指针类型而言，尽管可以用显式类型转换通过编译，但内存中的数据格式并不变化，这种不同类型的指针赋值实际上意义不大。

图　4.2　同类型指针间的赋值

代码4.3　不同类型的指针间的赋值PointerAssignment

＜---------------------------文件名：example403.cpp---------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　//声明了一short型指针，并用short变量num1的地址为其初始化

06　short num1=100，*pNum1=＆num1；

07　long*pNum2=（long*）pNum1；//强制转换用pNum1为long型指针pNum2赋值

08　cout＜＜"pNum1的地址："＜＜＆pNum1＜＜endl；

09　cout＜＜"pNum2的地址："＜＜＆pNum2＜＜endl；

10　cout＜＜"pNum1的内容："＜＜pNum1＜＜endl；

11　cout＜＜"pNum2的内容："＜＜pNum2＜＜endl；

12　cout＜＜"pNum2指向的值为："＜＜*pNum2＜＜endl；

13　return 0；

14　}

输出结果如下所示。

pNum1的地址：0012FF78

pNum2的地址：0012FF74

pNum1的内容：0012FF7C

pNum2的内容：0012FF7C

pNum2指向的值为：-859045788

【代码解析】代码4.3的意义可以体现在图4.3中。short型变量num占两个内存字节，代码第7行，当把short型指针pNum1强行赋给long型指针pNum2时，通过pNum2间接访问的是以0012FF7C为基址的4个内存字节，其值和100已经大不相同了。

说明

变量具体存储在哪个内存单元中，取决于你使用的编译器，示例中num变量地址为0012FF74，在其他系统中，这个值可能完全不同。

图　4.3　代码4.3直观示意图

简单地把整数赋给指针也是不允许的，下列代码是错误的。

int*pNum=0X0012FF7C；如果必须对某个内存地址进行访问，可以通过强制类型转换来完成，如下所示。

int*pNum=（int*）0x0012FF7C；

4.2　指针的运算

C++常常把地址当成整数来处理，但这并不意味着程序员可以对地址（指针）进行各种算术操作，事实上，指针所能做的操作是十分有限的，像指针与其他变量的乘除、两个指针间的乘除和两个指针相加都是没有意义、不被编译器所接受的。合法的运算具体包括指针与整数的加减，其中包括指针的自增和自减、同类型指针间的比较和同类型的两指针相减。

4.2.1　指针与整数的加减

指针与整数相加减，表示指针在内存空间中向下或向上移动整数个单位，该单位是多少个内存字节取决于指针所指变量的类型，short型指针每次移动两个字节，double型指针每次移动8个字节。

注意

将指针变量增加1，其增加的值等于指向的类型占用的内存字节数。

图4.4是指针与整数相加减时指针移动的示意图，对指针变量进行加N运算时表示如下。

指针变量新值=指针变量当前值+N*指针所指类型占

用的内存字节数

对指针进行自增和自减（++和--）处理时，应特别注意，因为“++、--”和间接引用符*具有相同的优先级，当它们在同一个表达式中出现时，应准确添加括号，避免歧义，否则，会按结合性来决定运算顺序，对单目运算符来说，其结合性都是自右向左进行的。

4.2.2　同类型指针间的比较

C++提供的6种关系运算对两个同类型的指针依旧适用，两个指针比较的是数值大小，举例来说，两个指针变量p1和p2，p1的值为0X12345678，p2的值为0X12345679，则p1＜p2成立。

图　4.4　指针与整数的加减

4.2.3　同类型指针相减

两个同类型指针相减，返回值是个整数，其值可用下列公式进行计算。

（指针1的值-指针2的值）/指针所指变量占用的内存字节数代码4.4同类型指针相减PointerOperation

＜-------------------------文件名：example404.cpp-----------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　int n=0，*pN=＆n；

06　int m=0，*pM=＆m；

07　cout＜＜"pN的值为："＜＜pN＜＜endl；

08　cout＜＜"pM的值为："＜＜pM＜＜endl；

09　cout＜＜"pN-pM="＜＜pN-pM＜＜endl；//两个同类型指针相减

10　return 0；

11　}

输出结果如下所示。

pN的值为：0012FF7C

pM的值为：0012FF74

pN-pM=2

【代码解析】根据代码第9行pN-pM是两个同类型指针相减，两个指针的值相减是8，而int型在示例系统中占4个内存字节，所以返回结果为2。

注意

当指针间的差值不是指针所指变量占用内存的整数倍时，只保留其整数部分。

在进行指针运算时，程序可能会意外地访问其他变量的内存空间，产生破坏性的后果，因此，要谨慎使用指针运算。

4.3　动态内存分配

除在前面介绍过的将指针初始化为变量的地址，或用变量的地址来对指针变量进行赋值以外，C++允许程序员利用专门的运算符“创建（new）”和“撤销（delete）”对内存进行动态分配，这样便可在程序运行时申请一块未命名的内存用来存储变量或者更为复杂的数据结构，并把该内存的首地址记录下来，以备将来访问。

4.3.1　使用new动态分配内存

new是一个单目运算符，操作数为一个类型名，返回值为指向操作数类型的指针。为一个变量分配动态的内存的基本格式如下所示。

类型名*指针变量名=new类型名；其中的“new类型名”是通知编译器所开辟的内存是用来存储什么类型的值，new操作符能根据这个类型名自动计算要分配的存储空间的大小。

int*pNum=new int；

举例来说，上述代码会在运行时为一个int型数值分配内存，声明了指向int型的指针pNum，并用动态申请内存的首地址为pNum同时将其进行初始化，因此，用指针pNum可访问这块内存区域。

申请内存的同时可对该区域进行初始化，对基本的变量类型，下列语句是合法的。

int*pNum=new int（8）；

动态申请了大小为int型的内存，将这块区域初始化为8，把该区域的首地址赋值给pNum，这种用法不只局限于基本的数据类型。

4.3.2　使用delete动态释放及动态申请的内存

动态申请的内存，在使用完毕后应及时将其归还系统，以供其他程序使用，这项工作必须由程序员来完成，delete语句的使用格式如下所示。

delete指针；

其中的指针指向使用new动态申请的内存块，delete指令会释放动态申请的内存块，但不会删除指针本身，还可以将指针重新指向另一块内存区域。

delete语句不能释放声明变量获得的内存，如下述语句是错误的。

int x=3；

int*p=＆x；

delete p；//错误

4.3.3　使用new申请动态数组

第3章讨论了通过声明建立数组的方法，实际上，可以通过new[]命令动态创建数组，其基本格式如下所示。

类型名*指针变量名=new类型名[元素个数]；

上述语句通知编译器动态开辟足以存储“元素个数”类型为“类型名”的元素的连续内存空间（数组），并声明“指针变量名”，指向数组的第一个元素。

与通过声明建立数组不同，使用new申请动态数组时，元素个数可以是变量，如下所示。

int i=5

int*p=new int[i]；//合法

这样，便可以在程序运行时决定应给数组分配的空间大小，而使用声明建立数组时，为了避免数组越界，一般都会将数组的维数尽量设大一点，因此造成了内存的浪费，使用new动态申请数组可以克服这一弊端。

注意

new无法对动态申请的数组存储区进行初始化。

对于动态申请的数组，在使用完毕后，应使用“delete[]”命令将内存释放，其基本格式如下所示。

delete[]指针；

方括号告诉程序，应释放整个数组，而不仅仅是指针指向的元素。因此，一定要注意new和delete的配对使用，“new＆delete”用于为一个实体分配内存，而“new[]＆delete[]”用于为数组分配内存。

代码4.5　使用new申请动态数组OperatorNew

＜--------------------------文件名：example405.cpp----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　int num=0；

06　cout＜＜"请输入数组个数："＜＜endl；

07　cin＞＞num；

08　cout＜＜"请依次输入"＜＜num＜＜"个整数（用空格隔开）："＜＜endl；

09　//申请一块可存放num个int型数据的动态内存，将首地址赋值给指针pSz

10　int*pSz=new int[num]；

11　for（int i=0；i＜num；i++）

12　cin＞＞pSz[i]；//for循环结构为动态数组中的元素赋值

13　for（i=0；i＜num；i++）

14　cout＜＜"第"＜＜i＜＜"个数为："＜＜pSz[i]＜＜endl；//对数组元素依次输出

15　delete[]pSz；//释放动态内存

16　return 0；

17　}

输出结果如下所示。

请输入数组个数：

3（注：键盘输入）

请依次输入3个整数（用空格隔开）：

12　17 9（注：键盘输入）

第0个数为：12

第1个数为：17

第2个数为：9

【代码解析】代码第10行，接收用户输入一个整数，依此确定要声明的内存块的大小，体现了new命令的优势所在，同时，使用指针pSz来对数组元素进行输入输出操作，最后，使用delete命令对申请的内存区域进行释放。

4.3.4　不要使用或释放已经释放的内存块

使用或释放已经释放了的内存块会出现意想不到的错误。在使用delete释放内存时，delete后的指针并不要求一定是用new命令进行赋值的那个指针，编译器关心的是内存块的地址，而不是用哪个指针来释放，见代码4.6。

代码4.6　使用或释放已经释放的内存OperatorDelete

＜-------------------------文件名：example406.cpp-----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　int*pN1=new int（8）；//申请一个int型大小的动态内存，初始化为8

06　int*pN2=pN1；//指针间的赋值

07　delete pN2；//释放掉pN2指向的动态内存

08　cout＜＜*pN1＜＜endl；

09　delete pN1；//释放掉pN1指向的动态内存

10　return 0；

11　}

【代码解析】代码第7行，“delete pN2；”语句实际上已经释放了语句“int*pN1=new int（8）；”申请的内存，所以，代码第8行“cout＜＜*pN1＜＜endl；”对*pN1的访问已经没有了意义，因为pN1指向的内存已经被释放，所以程序输出值并不为8，而是一个随机值，执行“delete pN1；”相当于对已经释放的内存再次释放，此时，系统报错，出现不可预料的错误。输出结果如图4.5所示。

图　4.5　使用或释放已经释放的内存带来的问题

注意

释放null指针（即空指针）是没有问题的。

4.3.5　使用malloc和free动态申请内存

C++允许使用C标准库函数中的malloc（）和free（）函数申请和释放动态内存，保留这两个函数主要有以下3点考虑。

❑C++程序经常要调用写好的C函数，而在C语言中，只能使用malloc（）和free（）函数。

❑如果C++程序要运行在C语言环境下，必须使用malloc（）和free（）函数。

❑new和delete的功能是通过调用malloc（）和free（）来实现的。

注意

malloc（）和free（）是C标准库函数，而new和delete是C++的运算符。

malloc（）函数的基本调用格式如下所示。

void*malloc（unsigned int size）；举例来说，下列语句用于申请一段长度为len，数据类型为short的动态内存。

short*p=（short*）malloc（len*sizeof（short））；

由于malloc（）函数返回类型是void*，用其返回值对其他类型指针进行赋值时，必须使用显式转换。同时，malloc（）函数参数是个无符号整数，其仅仅关心申请字节的大小，并不管申请的内存块中存储的数据类型，因此，申请内存的长度须由程序员通过“长度×sizeof（类型）”的方式给出。

说明

使用new和delete申请和释放动态内存时，内存的长度是由编译器自动计算的。

代码4.7　使用malloc（）和free（）申请和释放动态内存MallocAndFree

＜--------------------------文件名：example407.cpp----------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　int*p=（int*）malloc（sizeof（int）*5）；//使用malloc申请一块动态内存

06　cout＜＜"请输入5个整数："＜＜endl；

07　for（int i=0；i＜5；i++）{

08　cin＞＞*（p+i）；

09　}

10　cout＜＜"您输入的第3个数是："＜＜p[2]＜＜endl；

11　free（p）；//释放所申请的动态内存

12　return 0；

13　}

输出结果如下所示。

请输入5个整数：

1 3 5 7 9（注：键盘输入）

您输入的第3个数是：5

【代码解析】代码第5行，使用malloc（）函数动态申请了一块可存放5个int型数据的内存，并用for循环结构对5个数据分别赋值，在使用结束后，不要忘记用free（）函数释放所申请的内存。

使用free（）函数释放动态内存的基本格式如下所示。

void free（void*p）；

其中，p是指向所申请内存块的指针。编译器可以完成由其他类型指针向void型指针的转化，因此直接使用“free（指针）；”就可实现内存的释放。与前面介绍的delete一样，程序中应避免释放已经释放了的内存。

注意

如果指针为null，多次释放不会有问题，因为它并不指向任何的内存块。

4.3.6　动态内存申请并不一定能成功

不论是使用标识符new还是用malloc（）函数，都不能保证一定能成功地申请到所需要的动态内存。一旦申请失败，标识符new和malloc（）函数就会返回空指针null，如果在后续代码中不当使用该指针，程序就有崩掉的危险，因此，出于程序健壮性的考虑，在使用动态申请内存块时，应首先判断申请是否会成功（指针是否为null），如下面的代码所表示。

char*pC=new char[10]；

if（pC！=null）

//执行操作

else

//内存申请失败处理

实际上，新的C++标准允许通过抛出异常的方式来判断动态内存是否申请成功，关于这方面的内容将在第16章中进行详细介绍。

free（）和delete一个指针后，该指针所指向的动态内存被释放，但指针的值并不发生变化，常称此时的指针为“野指针”。通常，在内存释放后，将指针赋值为null，这样便不会再次释放已经释放了的内存，并且，通过“if（指针！=null）”也可以进行防错。

4.4　指针和const

第2章中介绍了const常量，用const修饰符声明的程序实体具有只读性。声明一个指针时，通过在声明语句的不同位置使用const可达到以下3个目的。

❑禁止对指针赋值。

❑禁止通过间接引用（*指针）对指针所指的变量赋值。

❑既禁止对指针赋值，又禁止通过间接引用（*指针）对指针所指的变量赋值。

4.4.1　禁止改写指针（常量指针或常指针）

在声明一个指针时，如果在*的右边加const修饰符，所声明的指针称为常量指针（常指针），编译器不允许程序改写指针的值，换言之，该指针恒指向某个内存地址，如下所示。

int x=0；

int*const pInt=＆x；

上述代码声明了一个指向int型变量的常指针pInt，并用int型变量x的地址为其初始化，在整个程序的执行过程中，pInt的值无法改变，也就是说，无法让pInt指向别的内存单元。无法改写pInt并不意味着无法通过间接引用改写pInt指向的变量，下述代码都是合法的。

x=5；

*pInt=6；

注意

声明一个常指针时，必须对其进行初始化，因为常指针的值在声明完毕后无法进行修改，因此，未进行初始化的常指针是没有意义的，编译器将给出错误提示。

4.4.2　禁止改写间接引用

在指针声明时，将const修饰符放在指针类型符之前，便无法通过间接引用改写指针所指变量，如下所示。

int x=5；

const int*pInt=＆x；

与常指针不同的是，此处的pInt并不被禁写，可以用其他变量的地址对其赋值，但是，通过间接访问“*pInt”改写指针所指变量是非法的，如下所示。

*pInt=10；禁止改写间接引用，并不意味着该内存变量无法改写，通过变量名访问和改写该内存区域是合法的，如下所示。

x=10；

注意

将const写在类型符和星号之间也是可以的，如“int const*pInt=＆x；”。

4.4.3　既禁止改写指针，又禁止改写间接引用

将上述两种用法结合起来，便可以将所声明的指针设定为“既禁止改写指针，又禁止改写间接引用”，如下所示。

int x=5；

const int*const pInt=＆x；

上述代码声明了一个常指针pInt，程序运行过程中，其值是恒定的，无法修改，同时，无法通过间接引用的方式改写pInt所指的内存区域。

技巧这种情况下，不要忘记指针的初始化。

4.5　指针与数组

C++中，数组和指针的关系十分密切，两者的内部处理方式几乎是等价的。访问数组中的元素有下标和指针两种形式，两者是通过前面介绍的指针算术机制相对应的。

4.5.1　数组名指针

C++编译器将数组名解释为指针，请看代码4.8。

代码4.8　数组名指针ArrayName

＜--------------------------文件名：example408.cpp----------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　int sz[5]；//创建一个大小为5的int型数组

06　for（int i=0；i＜5；i++）{

07　sz[i]=i；//依次对数组中的元素赋值

08　}

09　cout＜＜"sz："＜＜sz＜＜endl；//输出数组名

10　cout＜＜"＆sz[0]："＜＜＆sz[0]＜＜endl；//输出数组第一个元素的地址

11　cout＜＜"*sz："＜＜（*sz）＜＜endl；//数组名可看成是指向首元素的常量指针

12　cout＜＜"*（sz+3）："＜＜（*（sz+3））＜＜endl；

13　return 0；

14　}

输出结果如下所示。

sz：0012FF6C

＆sz[0]：0012FF6C

*sz：0

*（sz+3）：3

【代码解析】代码中，声明一个大小为5，元素类型为int的数组sz，通过for循环结构对5个元素赋值，代码第9行通过输出数组名sz可以看出，编译器将数组名解释为数组内存区域的首地址，对一维数组来说，要实现对某个元素的访问，既可以用“数组名+下标”的形式，又可以用间接引用，即“*（数组名+偏移）”的形式。

注意

指针与整数的相加已经在前面讨论过，指针的移动单位取决于其指向的变量类型，而对数组这样一块连续的内存区域来说，对指针的加1操作使得指针从当前元素跳转到下一个元素。

对代码4.8中的一维数组sz而言，sz[0]、sz[1]和sz[2]等都是一个变量名，但对二维和多维数组B来说，B[0]等是指针，指向以它们为名的广义向量。在介绍多维数组时，提及了元素在内存中的排列方式，以2×3×4的三维数组为例。

B[0][0][0]-＞B[0][0][1]-＞B[0][0][2]-＞B[0][0][3]

B[0][1][0]-＞B[0][1][1]-＞B[0][1][2]-＞B[0][1][3]

B[0][2][0]-＞B[0][2][1]-＞B[0][2][2]-＞B[0][2][3]

B[1][0][0]-＞B[1][0][1]-＞B[1][0][2]-＞B[1][0][3]

B[1][1][0]-＞B[1][1][1]-＞B[1][1][2]-＞B[1][1][3]

B[1][2][0]-＞B[1][2][1]-＞B[1][2][2]-＞B[1][2][3]

诸如B[1][2][3]、B[0]0][0]都是变量名，对应着某个内存单元，数组名B是指针，指向数组所占整个内存区域的首地址，B的值为＆B[0][0][0]。

实际上，B[0]和B[1]也是指针，B[0]的值为＆B[0][0][0]，B[1]的值为＆B[1][0][0]，换言之，B[0]可以看做是下面一块内存区域的变量名。

B[0][0][0]-＞B[0][0][1]-＞B[0][0][2]-＞B[0][0][3]

B[0][1][0]-＞B[0][1][1]-＞B[0][1][2]-＞B[0][1][3]

B[0][2][0]-＞B[0][2][1]-＞B[0][2][2]-＞B[0][2][3]

说明

将上例中的B[0]看成一个整体，比如替换成A，是不是更好理解一些。

同样，B[1][1]等也是指针，其值为＆B[1][1][0]，B[1][1]可以看成是下面一块内存区域的变量名。

B[1][1][0]-＞B[1][1][1]-＞B[1][1][2]-＞B[1][1][3]对三维数组B而言，有以下关系成立。

B=＆B[0]；

B+1=＆B[1]；

……

B[0]=＆B[0][0]；

B[0]+1=＆（B[0][1]）；

……

B[0][0]=＆B[0][0][0]；

B[0][0]+1=＆B[0][0][1]；

……

更高维的情况可依次类推。

4.5.2　数组元素的指针形式

对代码4.8中的一维数组sz来说，其元素的指针形式十分简单，只要使用“sz+n”的形式即可，如果是二维或多维数组，数组元素的指针形式是怎样的呢？

只要理解了多维数组元素在内存中的排列方式，便可以很容易地通过“指针+整数”的形式实现对数组元素的访问，以前面提及的数组为例，可以通过下列方式之一完成对B[1][1][2]的访问。

❑*（B[1][1]+2）；

❑*（*（B[1]+1）+2）

❑*（*（*（B+1）+1）+2）；

技巧多维数组的情况可以很容易由此进行扩展。

4.5.3　指向数组的指针

在很多场合下，数组名可以当成指针来用，但不容忽视的一点是数组名是个常量，其值是固定的，C++不允许修改数组名的值。因此，在进行数组元素的处理时，数组名显然不够灵活。为此，需要一个指针变量，该变量的值可以修改以指向数组中的某个元素。

数组名指针恒指向数组所占内存区域的第1个字节，对一维数组来说，数组名指针指向第0个元素，对多维数组来说，数组名指针指向第[0][0][0]……[0]个元素。

以一维数组为例引入指针变量的使用方式，见代码4.9。

代码4.9　指向数组的指针PointerToArray

＜--------------------------文件名：example409.cpp----------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　int sz[]={1，2，3，4，5，6，7，8，9}；//创建int型数组sz，大小为9

06　int*pSZ=sz；//用数组名为int型指针pSZ赋值

07　for（int i=0；i＜9；i++）

08　{

09　cout＜＜*pSZ＜＜""；

10　pSZ++；//使用pSZ访问数组元素

11　}

12　cout＜＜endl；

13　return 0；

14　}

输出结果如下所示。

1 2 3 4 5 6 7 8 9

【代码解析】代码第5行，声明了一维数组sz，代码第6行声明了int型指针pSZ，并用数组名指针sz对其进行初始化，实现了指针变量对数组元素的访问。程序中使用了“pSZ++；”，这说明指向数组的指针变量是个可修改的值。

如果数组是多维数组，有两种指针声明方式，即普通指针方式和数组名方式。

1.普通指针方式

代码4.10　使用普通指针变量访问多维数组PointerToMultiDArray1

＜-------------------------文件名：example410.cpp-----------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　//创建一个2×3×4的int型数组sz，并初始化

06　int sz[2][3][4]={{{1，2，3，4}，{5，6，7，8}，{9，10，11，12}}，

07　{{13，14，15，16}，{17，18，19，20}，{21，22，23，24}}}；

08　int*pSZ=sz[0][0]；//为int型指针初始化

09　for（int i=0；i＜2；i++）

10　for（int j=0；j＜3；j++）

11　for（int k=0；k＜4；k++）

12　cout＜＜*（pSZ+3*4*i+4*j+k）＜＜""；

13　cout＜＜endl；

14　return 0；

15　}

输出结果如下所示。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

【代码解析】代码第6行，声明了一个多维数组sz。代码第8行pSZ是指向int型的指针，用“sz[0][0]”初始化，此处以下的语句都可实现pSZ的初始化，使其指向数组所占内存单元的首字节。

int*pSZ=＆sz[0][0][0]；

int*pSZ=（int*）sz[0]；

int*pSZ=（int*）sz；

在访问sz[i][j][k]时，代码4.10采用了语句“*（pSZ+3*4*i+4*j+k）”，也许有的读者会提出，这里能否使用前面数组名指针一节中提到的形式，即“*（*（*（pSZ+i）+j）+k）”呢，答案是不可以，原因在于pSZ是普通的int型指针，对pSZ的算术运算，指针在内存中的移动单位只取决于int型数据的大小，如果想使用数组名指针一节中提到的形式，需要指出从第二维开始的大小，即为下面所要介绍的数组名方式。

2.数组名方式

代码4.11　使用数组名方式的指针变量访问多维数组PointerToMultiDArray2

＜--------------------------文件名：example411.cpp----------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　//创建一个2×3×4的int型数组sz，并初始化

06　int sz[2][3][4]={{{1，2，3，4}，{5，6，7，8}，{9，10，11，12}}，

07　{{13，14，15，16}，{17，18，19，20}，{21，22，23，24}}}；

08　//采用数组名方式声明指针pSZ

09　int（*pSZ）[3][4]=sz；

10　for（int i=0；i＜2；i++）

11　for（int j=0；j＜3；j++）

12　for（int k=0；k＜4；k++）

13　cout＜＜*（*（*（pSZ+i）+j）+k）＜＜""；

14　cout＜＜endl；

15　return 0；

16　}

输出结果如下所示。

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

【代码解析】对比代码4.10和代码4.11以后可以发现，指针声明方式的区别带来了访问方式的区别，合理使用指针访问数组和元素，关键在于对指针运算原则和数组内存模型的了解。

代码第13行，*（*（*（pSZ+i）+j）+k）可以直接写成pSZ[i][j][k]的形式，指针也可以当成数组名。但对代码4.10中声明的是普通指针，这种用法是非法的。

说明

如果在代码4.10中使用pSZ[i][j][k]形式，VC6并不会指出任何错误，反而会输出正确的结果，但这种用法却是不符合C++规则的，在其他的编译系统（如Eclipse）下会引起编译错误。

使用下标访问数组元素和使用指针访问数组元素各有优缺点。如果按递增或递减的顺序访问元素，使用指针方式很方便，速度也较快，但如果是随机对某个元素进行访问，使用下标更好一些。

4.5.4　指针数组

指针变量也可以作为数组元素，数组的定义要求这些指针变量必须是相同类型的，也就是说，这些指针指向的程序实体必须是同类型的。

short*sz[4]；

上述语句声明了一个大小为4的一维数组，数组中的每个元素都是指向short型数据（占两个内存字节）的指针，图4.6是指针数组sz的示意图，sz中的每个元素存储的都是short型变量的地址，这些short型变量（x1、x2、x3和x4）在内存中的地址没有要求。这样，便可以通过数组元素间接访问short型变量x1、x2、x3和x4。

注意

“short*sz[4]”与“short（*sz）[4]”的区别。“short（*sz1）[4]；”表示定义了一个指向数组的指针sz1，sz1指向的数组是大小为4的一维整型数组，其结构如图4.7所示。

图　4.6　指针数组sz的示意图

图　4.7　指针数组示意图

用指针数组处理字符串是十分方便的，见示例代码4.12，在用户输入一个0～5的整数后，程序会给出对应的英语单词。

代码4.12　使用指针数组处理字符串ArrayofPointer

＜--------------------------文件名：example412.cpp----------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　//创建一个指针数组words，其中每个元素都是字符串（指针）

06　char*words[]={"zero"，"one"，"two"，"three"，"four"，"five"}；

07　int xInput；

08　cout＜＜"请输入一个0～5（包括0和5）的整数："＜＜endl；

09　cin＞＞xInput；

10　cout＜＜"英语："＜＜words[xInput]＜＜endl；

11　return 0；

12　}

输出结果如下所示。

请输入一个0～5（包括0和5）的整数：

4（用户输入）

英语：four

【代码解析】代码4.12中，声明了一个大小为6的指针数组，其中的每个元素指向一个字符串，程序采用了诸如“zero”、“one”……的形式对char*型数组元素初始化，此处的“one”等称为常量字符串，位于系统的只读存储区，关于存储区的详细介绍参见第6章的内容。编译器将这些常量字符串解释为字符串首字节的内存地址，并对数组元素进行初始化。

说明

“char*p="abcd"；”与“char p[]="abcd"；”，前者是声明了指向常量字符串"abcd"的指针p，后者开辟了一块内存区域，用以存放字符'a'、'b'、'c'、'd'和'\0'。前者的"abcd"解释为常量字符串"abcd"的内存地址，后者为字符数组p的初始化字符串。

代码第10行，“cout＜＜"英语："＜＜words[xInput]＜＜endl；”一句中，字符型指针相当于一个字符串名变量，对指针所指的字符串进行整体输出，直到遇到结束符号（即空格符）'\0'停止。但应注意，“cout＜＜字符指针；”完成的是字符串整体输出，但“cout＜＜*字符指针；”仅仅输出指针所指位置的一个字符，这只是指针的间接引用。

使用指针数组管理字符串能有效地节省内存空间，代码4.13给出了一个对比范例，通过声明一个二维的char型数组可以达到同样的目的。

代码4.13　使用二维char型数组管理字符串TwoDimensionCharArray

＜--------------------------文件名：example413.cpp----------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　//创建二维char型数组，用来存储字符串

06　char words[][6]={"zero"，"one"，"two"，"three"，"four"，"five"}；

07　int xInput；

08　cout＜＜"请输入一个0～5（包括0和5）的整数："＜＜endl；

09　cin＞＞xInput；

10　cout＜＜"英语："＜＜words[xInput]＜＜endl；

11　return 0；

12　}

输出结果如下所示。

请输入一个0～5（包括0和5）的整数：

3

英语：three

【代码解析】代码第6行，声明了一个6×6的二维char型数组才能存储所需要的字符串信息，数组第2维的大小取决于最长的字符串（"three"），如图4.8所示；而使用指针数组则可以按照每个字符串的实际长度存储，如图4.9所示。对于一些要求严格的程序，使用指针数组管理字符串能有效节省内存。

图　4.8　使用二维char型数组管理字符串

图　4.9　使用char型指针数组管理字符串

4.6　引用

引用就是某一变量（对象）的一个别名，对引用的操作与对变量直接操作完全一样，这很好理解，某人名叫郭靖，单位上的人都喊他“小郭”，说“小郭”怎么怎么样，实际上就是说郭靖怎么怎么样，“小郭”可以看成是郭靖的别名。在C++程序中，正确使用引用，可使程序简洁、高效。

4.6.1　引用的声明

换种角度思考，引用可以看成另一种指针，引用的声明方法和指针的声明方法类似，只要把*换作＆，如下所示。

类型标识符＆引用名=初始值；

声明语句中的初始值必须是一个变量或另一个引用。

注意

此处的＆不是取地址符，而是“引用说明符”，这和*在指针声明中的情况类似。

举例如下。

int x=10；

int＆rx=x；

将引用rx声明为int型变量x的别名，在声明引用时，有以下4点需要特别注意。（1）一经声明，不能修改，C++不允许在声明完毕后修改引用的值。

（2）声明引用时，必须同时对其进行初始化。因为“一经声明，无法修改”，所以，引用的初始化是必需的，无初始化的引用是无效的。

（3）引用声明完毕后，相当于目标变量有两个名称，即原变量名和引用名，程序中不能再声明同样的引用作为其他变量名的别名。

（4）声明语句中的类型标识符必须于初始值的变量类型一致。

4.6.2　引用的特点

作为目标变量的别名，对引用的任何操作都等价于对目标变量的操作，同时，声明一个引用，并不是新定义了一个变量，只表示该引用名是目标变量名的一个别名，因此引用本身不占存储单元，编译器不会给引用分配存储单元。所以对引用的取地址，返回结果即是目标变量的内存地址，见代码4.14。

代码4.14　对引用的赋值和取址操作Reference

＜--------------------------文件名：example414.cpp----------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　int num=5；

06　int＆refNum=num；//声明一个引用，必须在声明时初始化

07　refNum=6；//改写引用refNum即是改写变量num

08　cout＜＜"num："＜＜num＜＜"，地址："＜＜＆num＜＜endl；

09　//引用的地址即是变量地址，同一实体

10　cout＜＜"refNum："＜＜refNum＜＜"，地址："＜＜＆refNum＜＜endl；

11　return 0；

12　}

输出结果如下所示。

num：6，地址：0012FF7C

refNum：6，地址：0012FF7C

【代码解析】代码第6行，refNum是int型变量num的引用，因此，对refNum的赋值，其实就是对num的赋值，“refNum=6；”等价于“num=6；”，通过对refNum和num的取址操作可以更容易理解引用的本质，引用refNum和目标变量num对应的是同一块内存。

一个变量可以有多个引用，就像一个人可以有多个别名一样，但在引用声明时，如果在一行内声明多个引用，应注意＆符号的使用，如下所示。

int＆refNum1=num,refNum2=num；上述语句中，refNum2不是num的引用，而是一个int型的变量，并用num对其初始化，正确的写法如下所示。

int＆refNum1=num，＆refNum2=num；

4.6.3　引用的使用限制

关于引用，以下用法是非法的。

1.不能建立数组的引用

数组是由若干个元素所组成的集合，无法建立一个数组的别名。

2.声明一个引用的引用

尽管可以用诸如“int**ppInt；”的形式声明一个指向指针的指针，但声明引用的引用是非法的，如下所示。

int＆＆refNum=num；3.声明指向引用的指针

int＆*pNum=num；上述代码企图声明一个指向引用的指针，编译器将指出这是非法的。应注意的是声明指针的引用是允许的。虽然有点拗口，但“指向引用的指针”和“指针的引用”是不同的概念，如下所示。

int*＆refPnum=pNum；//pNum为一个int型的指针，指针的引用合法

仅仅是*和＆顺序不同，代码的意义却大不相同。

正是由于这诸多限制，才将引用这个工具的威力控制得恰到好处，可以把指针比作一把能削铁，能砍瓜切菜，还能剪指甲的刀，使用这把刀是十分危险的，它有很强的破坏性，搞不好要割破手指流血的，须格外小心。而加了诸多限制的引用，它则是把称心的工具，尤其体现在函数值的传递引用及调用上，关于这部分内容的介绍请参考第6章的内容。

4.6.4　其他要说明的问题

C++程序允许对用new申请的动态无名实体建立一个引用，如下所示。

int＆refNum=*new int；

在撤销动态内存时，可以用“delete＆refNum；”实现，结合前面所讲的限制可知，对new申请的动态数组建立引用是非法的。

在引用声明一小节中提到过声明语句中的类型标识符必须与初始值的变量类型一致，当两者不一致时，编译器会指出错误，除非引用为const修饰的。在这种情况下，C++将生成临时变量，下列用法是合法的。

const int＆refInt=9；//如果去掉const则非法

int x；

const double＆refDouble=x；//如果去掉const则非法

在早期的C++编译器中，去掉两句中的const，并不会引起错误，编译器可能只会给出相应的警告信息，但遵循新标准的编译器都会明确指出，去掉const后的语句是非法的，在两种情况下，必须用const修饰引用。

（1）对非左值的引用，如上述代码中的常数9。

（2）类型声明符与初始值的类型不一致，但可以转换。

const修饰的引用是只读的无法改写，更有价值的应用体现在函数值的传递引用及调用时，关于这方面内容的介绍请参考第6章。

4.7　小结

指针和引用是C++语言的难点，本章主要介绍了指针的概念、指针变量、指针运算、动态内存分配和引用等内容。

指针变量中存储的是某个内存单元的地址信息，通常用“类型符*”的形式来声明一个指向某种类型的指针变量。指针变量支持的运算不多，特别要注意指针和整数的加减。不同类型的指针，对其进行加1或减1时，改变的幅度不同，根据这个性质可以声明指向数组的指针，通过加减指针方便数组元素的管理。

数组名很特殊，编译器将一维数组的数组名解释为指向第一个元素的指针，对多维数组来说，数组名指向的是某对应的广义向量，利用数组名访问数组元素是个经常会出错的环节，理解数组在内存中的存储模型是同类型还是连续将有助于正确使用数组名。

C++中还可以声明元素为指针的数组，称为指针数组。数组中的每个元素都存储着某个内存单元的地址信息，在组织字符串时，指针数组尤其有效。

在C++中还可以根据需要用操作符new和delete，以及标准库函数malloc（）和free（）动态申请和释放内存，同时提高了编程的灵活性。动态申请和释放内存是把“双刃剑”，经常是程序bug的源泉，如未对指针进行初始化或分配内存，就进行各种运算，以及内存的重复释放和未释放动态申请内存导致内存泄露等。

引用是变量的别名，可以看成是另一种形式的指针，它与指针不同的是，引用不占据内存地址空间，引用和目标变量指的是同一内存实体，对引用的取址操作实际上就是对目标变量的取址操作。一般来说，引用比指针要直观，在编程时应尽量使用引用，以提高程序的可读性。

4.8　上机实践习题

1.声明一个指针变量及指向指针的指针，并对其进行初始化，最后输出结果。

【提示】上述题目主要是要求读者了解指针的定义及使用的知识，重点是掌握如何声明、初始化及使用。

【关键代码】

01　int num1=100，*pNum1=＆num1；

02　int**ppNum2=＆pNum1；

03　cout＜＜"pNum1的地址："＜＜＆pNum1＜＜endl；

04　cout＜＜"pNum2的地址："＜＜＆pNum2＜＜endl；

05　cout＜＜"pNum1的内容："＜＜pNum1＜＜endl；

06　cout＜＜"pNum2的内容："＜＜pNum2＜＜endl；

07　cout＜＜"pNum2指向的值为："＜＜*pNum2＜＜endl；

2.利用new和delete进行内存的动态分配并进行初始化，最后输出内容。

【提示】上述题目主要是要求读者了解动态内存分配的相关知识，重点是掌握new和delete的使用过程。

【关键代码】

01　int*p=NULL；

02　int i=0；

03　cin＞＞i；

04　p=new[i]；

05　for（i=0；i＜10；i++）

06　{

07　*（p+i）=i；

08　cout＜＜"*p+"＜＜i＜＜"="＜＜*（p+i）；

09　}

10　delete[]p；

3.声明一个变量的引用并对其进行操作，最后输出结果。

【提示】上述题目主要是要求读者了解引用的相关知识，重点掌握引用的使用。

【关键代码】

01　using namespace std；

02　int num=100；

03　int＆rNum=num；

04　cout＜＜"num="＜＜num＜＜endl；

05　rNum+=100；

06　cout＜＜"num="＜＜num＜＜endl；

07　return 0；

第5章　结构、共用体和链表

在实际应用中，常常需要把类型不同的一系列数据存储在一起，例如要登记一个学生的信息，则需要记录他的姓名（C-string）、学号（整型或字符型）、年龄（整型）、身高和体重（浮点型）等，前面介绍的数组无法完成这一任务，因为数组要求所有的元素属于同一类型。结构（struct）可以满足我们的要求，为了满足程序设计的需要，C++允许我们自己定义数据类型，称之为自定义数据类型，结构是自定义数据类型中的一种，其可将多种数据类型组合在一起使用。

共用体可以看成是一种特殊的结构，与结构不同的地方在于其允许在系统内存的同一块区域保存不同类型的数据。将一个个的结构变量或共用体变量用指针联系起来，便形成了链表。正确使用结构、共用体和链表，才能写出高质量的C++程序。

本章主要涉及以下知识点。

❑结构的定义与使用：介绍如何定义一个结构并进行使用。

❑共用体定义与使用：介绍共用体的概念和使用。

❑结构和共用体数组：介绍结构和共用体数组的声明和使用。

❑结构的指针：介绍如何声明一个结构指针及使用。

❑链表：讲解链表的创建及其相应的操作方法。

5.1　结构

结构是比数组更为灵活的一种数据结构，从字面上分析可将结构看做一组变量，它们可以具有不同的数据类型，将它们集合到一个整体中，这个整体便称之为结构。仍举存储学生信息这个例子，可以定义一个student结构，编译器将该结构看成一个整体或一个类型，与int和double同样。程序中可以声明student型的变量，该变量包含了一个学生的姓名、学号、年龄、身高和体重等一系列信息。使用结构，可以有效地实现相关信息的存储和管理。

5.1.1　如何定义一个结构

结构是用户定义的一种数据类型。定义了结构后，便可以通过声明创建这种类型的变量。因此，结构的使用分为两个步骤，一是结构的定义，二是结构变量的声明。

我们可以采用如下形式定义一个student型的结构。

struct student

{

char name[20]；

int age；

float weight；

}；student结构集合了某个学生姓名（字符数组）、年龄（整型）和体重（浮点型）的信息，关键字struct表明这些代码是一个结构的定义，紧跟在struct后面的student是这个结构的类型（或名称），完成该定义后，便可以像创建int型和double型变量一样创建student型的变量了，花括号内包含的是该结构所包含的信息（存储的数据类型）的列表，其中的每项都是一个声明语句，本例中使用了一个存储姓名的字符数组name，一个存储年龄的int型变量age和一个存储体重的float型变量weight。一般形式的结构定义如下所示。

struct结构名称（或称标识）

{

存储数据列表；（或称成员变量列表）

}；

注意

在定义结构时，一定不要忘记花括号后的分号，因为结构的定义可以看成一条完整的C++语句，否则编译器会报错。

5.1.2　结构变量的声明和使用

在程序中，可以像声明普通的int型变量一样声明一个结构变量，如下所示。

struct student A；

该语句声明了一个student型的变量A，在声明结构变量时，关键词struct可以省略，而直接写成“student A；”，这样，结构类型student的用法就和int和long等基本类型名相同，这说明结构定义了一种新类型。

结构类型和结构变量是两个不同的概念，结构类型就像int和double等类型一样，是一种数据类型，编译器并不对其分配内存空间，只有声明了结构类型的变量时，才对该变量分配存储空间。

变量A是student型，可以使用成员操作符（.）来访问各个成员（内部存储的数据），例如，A.name、A.age和A.weight分别代表变量A中存储的姓名、年龄和体重的信息，实际上，可以将A.name看成一个普通的字符数组，将A.age和A.weight分别看成普通的int型变量和float型变量，总之，通过“结构变量名.成员名”可以访问变量中的成员，见代码5.1。

代码5.1　声明一个结构变量并对其成员进行访问StructSample

＜-----------------------文件名；example501.cpp-------------------------------＞

01　#include＜iostream＞

02　struct student//定义结构student

03　{

04　char name[20]；//字符数组name，表示姓名

05　int age；//int型变量age，表示年龄

06　float weight；//float型变量weight，表示体重

07　}；

08　int main（）

09　{

10　using namespace std；

11　student A；//可以像使用内置类型int一样使用student，创建结构变量A

12　cout＜＜"请输入学生姓名："＜＜endl；

13　cin＞＞A. name；//通过访问A中的成员name

14　cout＜＜"请输入学生年龄："＜＜endl；

15　cin＞＞A. age；//通过访问A中的成员age

16　cout＜＜"请输入学生体重："＜＜endl；

17　cin＞＞A. weight；//通过访问A中的成员weight

18　cout＜＜"姓名："A.name＜＜"，年龄："＜＜A.age＜＜"，体重："＜＜A.weight＜＜endl；

19　return 0；

20　}

输出结果如下所示。

请输入学生姓名：

Zhangsan（注：键盘输入）

请输入学生年龄：

24　（注：键盘输入）

请输入学生体重：

60　（注：键盘输入）

Zhangsan，年龄：24，体重：60

【代码解析】代码第2～7行，声明了一个student型的变量A，并由用户通过键盘依次输入学生的姓名、年龄和体重，由A.name、A.age和A.weight对变量A中的成员进行访问，最后对写入结果进行输出。

注意

结构定义的位置有一定讲究。对本例来说，既可以将student结构定义在main函数的外部（外部声明），也可以将其定义在main函数内部的最前面（内部声明），总之，只要在使用“student A；”语句声明student型结构变量前完成student结构的定义即可。

与结构一样，变量（普通变量、数组和指针等）除了在main函数内部声明外，还可在main函数外部声明，对简单程序来说，两种声明方式可能没有区别，但对于一些复杂的程序，可能会涉及声明的作用域，关于这方面的详细内容请参考第6章。

技巧在使用变量、结构以及后面要介绍的函数前，一定要“先声明，后使用”。

其实，在定义结构的同时完成创建结构变量的工作，只需要把结构变量名放在结构定义结束花括号的后面即可，下列语句在定义student结构的同时声明了两个student型的结构变量stu1和stu2，如下所示。

struct student

{

char name[20]；

int age；

float weight；

}stu1，stu2；

5.1.3　结构变量的初始化

在声明结构变量的同时，可以进行变量的初始化。

student stu1={"Ronaldo"，30，70.5}；或student stu1={{'R'，'o'，'n'，'a'，'l'，'d'，'o'，'\0'}，30，70.5}；

和数组一样，将用逗号分割的值列表用花括号括起来，以完成stu1的初始化。

事实上，可以将结构定义，变量声明和变量初始化放在一起来完成，见代码5.2。

代码5.2　结构变量的初始化InitialofStructVariable

＜----------------------文件名；example502.cpp--------------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　struct computer//定义结构computer

06　{

07　char brand[20]；//成员1：字符数组brand，表示品牌

08　float price；//成员2：float型变量price，表示价格

09　}com1={"Dell"，5000}；//结构定义的同时声明该型变量并初始化

10　cout＜＜"电脑品牌："＜＜com1.brand＜＜"，价格："＜＜com1.price＜＜endl；//输出

11　return 0；

12　}

【代码解析】代码第9行，是在定义结构的同时，声明了一个该结构的变量，并对其进行了初始化。

输出结果如下所示。

电脑品牌：Dell，价格：5000

注意

将结构定义和结构变量声明分开，有利于提高代码的可读性。

C++允许使用匿名结构，我们可以定义一个没有类型名称的结构，方法是省略名称，但一定要在结构定义的同时声明至少一个结构变量，否则这种用法没有意义，如下所示。

struct

{

char brand[20]；

float price；

}com1；

这样将创建一个名为com1的结构变量，可以通过com1.brand和com1.price来访问其内部成员，但这种类型没有名称，因此无法在以后的程序中声明这种类型的变量。

5.1.4　结构变量间是否可以相互赋值

数组可以看成是一组相同类型数据的结合，而结构可以看做是一组不同类型数据构成的整体，在第3章中介绍过，除了可以对字符数组进行整体的输入输出外，C++一般不允许对数组进行整体的操作，必须通过元素遍历的方式实现对数组的整体操作，那么，结构变量间是否允许相互赋值呢？答案是肯定的。

可以使用赋值操作符“=”将一个结构变量B赋值给另一个结构变量A，这样，结构变量A中的每个成员都将被设置成结构变量B中相应成员的值，即使成员是数组类型也不例外，这种赋值方式被称为成员赋值，见代码5.3。

代码5.3　结构变量间的赋值操作AssignmentBetweenVariables

＜---------------------文件名：example503.cpp---------------------------------＞

01　#include＜iostream＞

02　struct computer//定义结构computer

03　{

04　char brand[10]；

05　float price；

06　}；

07　int main（）

08　{

09　using namespace std；

10　//声明两个结构变量com1和com2，并初始化

11　computer com1={"Apple"，7000}；

12　computer com2={"Lenovo"，8000}；

13　cout＜＜"赋值前：电脑品牌为"＜＜com1.brand＜＜"，电脑价格为"＜＜com1.price＜＜endl；

14　com1=com2；//用com2给com1赋值

15　cout＜＜"赋值后：电脑品牌为"＜＜com1.brand＜＜"，电脑价格为"＜＜com1.price＜＜endl；

16　return 0；

17　}

输出结果如下所示。

赋值前：电脑品牌为Apple，电脑价格为7000

赋值后：电脑品牌为Lenovo，电脑价格为8000

【代码解析】在main（）函数外部定义了computer结构，在main（）函数内声明了两个结构变量com1和com2，在声明的同时分别对其进行了初始化，代码第14行，语句“com1=com2；”用结构变量com2对com1赋值，输出结果显示，com1中的数据在赋值后已经被改写，即便是字符数组型的成员brand，也完成了赋值操作。

技巧利用结构来处理数组的整体赋值操作，是个不错的思路。

5.1.5　结构体变量的sizeof

一般来说，结构体变量占据的内存大小是所有成员变量占据内存大小之和，但有些例外需要特别注意，先看一个结构体。

struct ExS1

{

char c1；

short s1；

int i1；

}；

由于char偏移量必须为1的倍数，int型必须为4的倍数，float偏移量必须为4，double偏移量必须为8，short偏移量必须为2。从字面上来看，sizeof（ExS1）的结果似乎应该是1+2+4=7，但实际上，返回结果为8，这涉及字节对齐机制。

字节对齐的细节和编译器实现相关，一般而言有以下3个准则。

（1）结构体变量的首地址能够被其最宽基本类型成员的大小所整除。

（2）结构体每个成员相对于结构体首地址的偏移量（offset）都是成员大小的整数倍，如有需要编译器会在成员之间加上填充字节（Internal Adding）。

（3）结构体的总大小为结构体最宽基本类型成员大小的整数倍，如有需要编译器会在最末一个成员之后加上填充字节（Trailing Padding）。

字节对齐有助于加快计算机的取数速度，节省指令周期。对示例结构ExS1来说，根据准则（2），每个数据成员相对于首地址的偏移量是成员大小的整数倍，需要在char型成员c1和short型成员s1之间加入一个空字节，所以整个结构的长度变为8。

变化一下ExS1定义的顺序，会有以下结果。

struct ExS1

{

short s1；//存放在[0]……[1]，

int i1；//根据第（2）个准则，偏移必须是4的整数，前面填充2～3，存放在[4]……[7]

char c1；//存放在[8]，但根据第（3）个准则，所以整个结构必须是4的整数倍，填充9～11

}；

sizeof（ExS1）的结果变为12，只是交换了下数据顺序，为什么类结构变量的大小又扩大了呢？这还是由上述3个准则决定的，根据准则（2），需要在s1和i1之间加入两个空字节，根据准则（3），需要在c1后加入3个空字节，最后，类变量的大小如下所示。

2+2（插入的空字节）+4+1+3（插入的空字节）=12

说明

成员变量的定义顺序也会影响结构体变量占据的内存大小。

注意

3个字节对齐准则中反复提到了“基本数据类型”的概念，所谓基本类型是指像char、short、int、float及double这样的内置数据类型。数据宽度是指其sizeof的大小。一方面，由于结构体可以嵌套，其成员可以是复合类型，比如另外一个结构体即后面要介绍的共用体及类对象，在寻找最宽基本类型成员时，不是把复合成员看成是一个整体，而是考虑复合类型成员的子成员，另一方面，类内复合成员的内存分配仍要遵守3个准则。来看以下示例。

struct A

{

char c1；

short s1；

int i1；

}；

struct B

{

char B_c1；

A B_a；

char B_c2；

}；

前面的分析已经指出，A的大小为8，在考虑B的最宽简单类型成员时，要将类型为A的复合成员B_a“打散”，所以结构B的最宽简单类型为int，大小为4个字节。根据准则（3）可知，sizeof（B）的值也应该被4整除。准则（2）应解释为“复合成员相对于结构体首地址的偏移量（offset）都是复合成员中最宽简单类型成员大小的整数倍，”，因此，B_a的偏移量应为4的倍数，B_c1和B_a之间插入3个空字节，加上B_c2共计13个字节，13是不能被4整除的，末尾还得补上3个填充字节，最后得到sizeof（B）的值为16。

注意

C++结构除了成员变量外，还可以有成员函数，关于这方面的内容将在第8章中进行介绍。

5.2　共用体

共用体（Union）也称联合，可以看成一种特殊的结构。与结构一样，共用体可以包括多种数据类型，但在共用体中，各种数据类型在内存中占据同一地址，换句话说，在某个确定的时刻，共用体只能表示一种数据类型。与结构的使用方法一致，共用体的使用也分为共用体的定义及共用体变量的声明两个步骤。

5.2.1　共用体的定义

共用体可采用如下形式定义。

union共用体名称（或称标识）

{

存储数据列表（或称成员变量列表）

}；

注意

结束花括号后的分号不要遗漏，这种定义形式是一个完整的C++语句。

举例来说，下列代码定义了computer共用体，根据此定义，便可以像创建结构变量那样创建computer类型的共用体变量。

union computer

{

char brand[10]；

float price；

}；

可以看出，除了把关键字struct换成了union外，共用体的定义与结构完全一样，下面将通过实例说明共用体和结构的区别。

5.2.2　共用体和结构的区别

代码5.4　共用体和结构的区别UnionAndStruct

＜----------------------文件名：example504.cpp--------------------------------＞

01　#include＜iostream＞

02　struct comStruct//定义结构comStruct

03　{

04　char brand[10]；//字符数组brand，表示品牌

05　float price；//float型变量price，表示价格

06　}；

07　union comUnion//定义共用体comUnion

08　{

09　char brand[10]；//字符数组brand，表示品牌

10　float price；//float型变量price，表示价格

11　}；

12　int main（）

13　{

14　using namespace std；

15　union comUnion com1；//声明一个共用体变量com1

16　comStruct com2；//声明一个结构变量com2

17　//共用体变量的成员占据相同的地址

18　cout＜＜"共用体com1.brand地址："＜＜＆com1.brand＜＜endl；

19　cout＜＜"共用体com1.price地址："＜＜＆com1.price＜＜endl；

20　//结构体变量成员占据不同的内存地址

21　cout＜＜"结构com2.brand地址："＜＜＆com2.brand＜＜endl；

22　cout＜＜"结构com2.price地址："＜＜＆com2.price＜＜endl；

23　return 0；

24　}

输出结果如下所示。

共用体com1.brand地址：0013FF74

共用体com1.price地址：0013FF74

结构com2.brand地址：0013FF64

结构com2.price地址：0013FF70

【代码解析】首先在main（）函数外定义了一个结构类型comStruct和一个共用体类型comUnion，它们的存储数据列表是完全一样的，在main（）函数内声明了comStruct型的结构变量com2。代码第15行，声明comUnion型的共用体变量com1，输出com1和com2的成员变量地址可以发现，对共用体来说，其成员变量开始于同一地址单元，也就是说，共用体中的成员变量占据同一块内存空间。com1和com2的内存模型如图5.1所示。

可以看出，在声明结构变量和共用体变量时，编译器的内存分配机制是不同的，对结构变量来说，编译器开辟一块连续的内存区域，为其中的每个数据成员分配相应大小的内存，但对于共用体变量来说，所有的数据成员起始于同一内存单元（示例代码5.4中com1.brand和com1.price都起始于0013FF74），也就是说所有的数据成员共用一块内存，这也是共用体名称的由来。

图　5.1　结构变量和共用体变量内存模型对比图

但是，在某一确定的时刻，共用体数据成员不能同时占有共用体内存空间，否则编译器无法分辨。编译器只按共用体中占内存最大的一个数据成员为共用体分配内存空间，在某个时刻，这个空间只能被一个成员所占有，在代码5.4中，com1中要么存储的是字符数组brand，要么存储float类型的变量price，但com1无法同时存储两者的信息。

5.2.3　共用体变量的声明和初始化

在定义了一个共用体后，同样可以像声明一个int型变量一样声明一个共用体变量，形式如下所示。

union共用体名共用体变量；

其中，关键词union可以省略，如代码5.4中的“comUnion com1；”，这说明共用体定义了一个新类型。

在声明一个共用体的同时，可以完成其初始化，与结构变量的初始化不同的是，只能对共用体变量列表中的一个变量进行初始化，确切地说是对列表中的第一个变量进行初始化，对代码5.4中的comUnion共用体而言，下列语句是合法的。

comUnion com1={"Acer"}；

但如果用“comUnion com1={7000}；”对com1进行初始化，编译器则会给出警告。比较两种写法可以发现，初始化语句针对的是共用体列表中的第一个变量（字符数组brand）。

说明

更改共用体中变量列表的顺序，初始化的方式将有所不同。

与结构类似，可以把共用体定义、共用体变量声明及其初始化放在一起，如下所示。

union computer

{

char brand[20]；

float price；

}com1={"Dell"}；C++允许使用匿名共用体，我们可以定义一个没有类型名称的共用体，方法是省略名称，但一定要在共用体定义的同时声明至少一个共用体变量，否则这种用法没有意义，如下所示。

union

{

char brand[20]；

float price；

}com1；

这样将创建一个名为com1的共用体变量，但这种类型没有名称，因此无法在以后的程序中声明这种类型的变量。

5.2.4　共用体使用举例

为了方便管理，某学校决定为学生和老师制作统一的卡片，其中有一项内容是所在单位，对学生来说，代表其所在年级（int型），但对老师来说，代表学校某个部门（字符数组），这样的数据应如何组织呢？使用数组不行，因为数组中必须存储同类型的数据，使用结构也不行，因为年级和部门只能取其一，两者之间不是并列的关系，对这个问题来说，最好的解决办法是使用共用体，来看一段示例代码。

代码5.5　共用体使用举例UnionSample

＜---------------------文件名：example505.cpp---------------------------------＞

01　#include＜iostream＞

02　union info//定义共用体info

03　{

04　int grade；//年级，面向学生

05　char department[20]；//部门，面向老师

06　}；

07　int main（）

08　{

09　using namespace std；

10　info personInfo；//声明共用体变量personInfo

11　int ans=-1；

12　cout＜＜"老师还是学生?（输入0代表学生，1代表老师）："＜＜endl；

13　cin＞＞ans；

14　//同一时刻，grade和department中只能有一个被写入

15　if（ans==0）

16　{

17　cout＜＜"欢迎你，请输入你所在的年级："＜＜endl；

18　cin＞＞personInfo. grade；

19　cout＜＜"你的年级是："＜＜personInfo.grade＜＜endl；

20　}

21　if（ans==1）

22　{

23　cout＜＜"欢迎你，请输入你所在的部门："＜＜endl；

24　cin＞＞personInfo. department；

25　cout＜＜"你的部门名是："＜＜personInfo.department＜＜endl；

26　}

27　return 0；

28　}

输出结果如下所示。

老师还是学生?（输入0代表学生，1代表老师）：

0（注：键盘输入）

欢迎你，请输入你所在的年级：

4（注：键盘输入）

你的年级是：4或者：

老师还是学生?（输入0代表学生，1代表老师）：

1（注：键盘输入）

欢迎你，请输入你所在的部门：

math（注：键盘输入）

你的部门名是：math

【代码解析】在main（）函数外部定义了一个共用体info，并在main（）函数内代码第10行声明了一个共用体变量personInfo，根据学生和老师身份的不同，personInfo在某个确定的时刻只能取一个值，要么是int型变量grade（代码第18行），要么是字符数组department（代码第24行）。

5.2.5　共用体的sizeof

原则上讲，共用体的变量的大小取决于所有的成员中占用空间最大的一个，举例如下。

union Ex

{

double x；

char y；

}；则sizeof（Ex）等于8，但结构体变量内存分配的准则（3）在此仍旧成立，如下所示。

union Ex1

{

char x[13]；

float y；

}；

此时，sizeof（Ex1）等于16，这是因为在Ex1中，最宽基本类型为float，占4个内存字节，根据准则（3），共用体Ex1的变量大小必须是4的整数倍，所以会在后面填充3个字节，输出结果为16。

说明

和结构一样，除了成员变量外，共用体还可以有成员函数，关于这方面的内容将在第8章中进行介绍。

5.3　结构数组和共用体数组

在程序中定义的结构和共用体相当于一个新的类型，可以像创建int型或double型变量一样创建结构变量，很多个int型变量可以构成整型数组，结构变量和共用体变量同样可以构成数组，称为结构数组或共用体数组。实际应用中，经常用结构数组来表示具有相同数据结构的一个群体，如一个班的学生档案。与以前学习的数组不同，结构数组的每一个元素都是一个结构类型的数据。

例如，要建立10个学生的数据，每个学生的信息由一个结构变量表示，那么这10个结构变量就可以构成一个大小为10的结构数组，其中每一个数组元素都包含该结构存储变量列表中的所有数据，这样可以很方便地对这10个学生的相关数据进行管理。

5.3.1　结构数组的声明和初始化

结构数组的声明方式与一般的数组类似，形式如下所示。

结构类型名结构数组名[元素个数]；以代码5.1中的student结构为例，下列语句就声明了一个大小为10的结构数组stuInfo，数组中的每个元素都是student型的结构变量。

student stuInfo[10]；

结构数组的初始化也与其他数组的初始化方法类似，既可以在声明数组的同时对元素初始化，又可以在数组建立完毕后，对元素进行赋值。

仍以代码5.1中的student结构为例，说明结构数组初始化的方法。

（1）先建立结构数组，后对元素进行赋值。

student stu[3]；

student tempStu={"likuan"，24，60}；

for（int i=0；i＜3；i++）

stu[i]=tempStu；（2）在声明结构数组的同时对元素初始化。

student stu[3]={{"zhangsan"，20，60}，{"lisi"，21，65}，{"wangwu"，19，80}}；在声明了一个大小为3的student型数组stu后，分别对其中的元素进行了初始化。应特别注意下列初始化方式是不合法的。

student tempStu={"likuan"，24，60}；

student stu[3]={tempStu,tempStu,tempStu}；

提示

在声明数组的同时对元素进行初始化，必须采用双层括号的形式，分别对其中的每个元素进行初始化。

与普通数组一样，在声明结构数组时，也可以不指定数组元素的个数，如下所示。

student stu[]={{"zhangsan"，20，60}，{"lisi"，21，65}，{"wangwu"，19，80}}；

编译器会根据给出的初值个数来确定数组元素的个数。

注意

与普通数组一样，可以声明二维结构数组或多维结构数组。

可以将结构定义、结构数组的声明及其初始化放在一起来完成，下列语句是合法的。

struct student

{

char name[20]；

int age；

float weight；

}stu[3]={{"zhangsan"，20，60}，{"lisi"，21，65}，{"wangwu"，19，80}}；

5.3.2　共用体数组的声明和初始化

共用体数组的用法与结构数组的用法几乎一致，唯一的不同之处在于对数组元素进行初始化时，只能对共用体存储变量列表中的一个成员赋值。

共用体数组声明形式如下所示。

共用体类型名共用体数组名[元素个数]；以代码5.4中的共用体comUnion为例，下列语句就声明了一个大小为5的共用体数组sz，数组中的每个元素都是comUnion型的结构变量。

comUnion sz[5]；

与结构数组类似，共用体数组既可以在声明数组的同时对元素初始化，又可以在数组建立完毕后，对元素进行赋值。

（1）先建立共用体数组，后对元素进行赋值。

comUnion com[3]；

comUnion comTemp={"Dell"}；//注：只能对一个成员变量初始化

for（int i=0；i＜3；i++）

com[i]=comTemp；（2）在声明结构数组的同时对元素初始化。

comUnion com[3]={{"Dell"}，{"Lenovo"}，{"Acer"}}；//注：只能对一个成员变量初始化下列用法仍然是不合法的。

comUnion comTemp={"Dell"}；//注：只能对一个成员变量初始化

comUnion com[3]={comTemp,comTemp,comTemp}；在声明结构数组时，也可以不指定数组元素的个数，如下所示。

comUnion com[]={{"Dell"}，{"Lenovo"}，{"Acer"}}；//注：只能对一个成员变量初始化

编译器会根据给出的初值个数来确定数组元素的个数。

注意

同样可以声明二维共用体数组或多维共用体数组。

可以将共用体定义、共用体数组的声明及其初始化放在一起来完成，如下所示。

union comUnion

{

char brand[10]；

float price；

}com[3]={{"Dell"}，{"Lenovo"}，{"Acer"}}；

注意

和普通数组一样，C++不提供结构数组和共用体数组的越界检查机制，所以，程序员应掌握数组下标的取值，避免越界。

5.4　指向结构的指针

在程序中，常常需要为结构动态分配内存，这需要指向结构类型变量的指针。对指向结构变量的指针来说，其使用方法与指向其他类型如int和double等的指针类似。一个指针变量当用来指向一个结构变量时，便称之为结构指针变量。结构指针变量中的值是所指向的结构变量的首地址。通过结构指针便可访问该结构变量。由于共用体可以看成一种特殊的结构，指向共用体的指针与指向结构的指针使用方法一致，因此，本节中将指向共用体的指针与指向结构的指针统称为结构指针。

5.4.1　声明一个结构指针

和普通指针类似，可采用下列形式声明一个结构指针。

结构名*指针变量名；

以代码5.1中定义的的student结构为例，“student*p”声明了一个指向结构student的指针p，给指针变量p赋值是把结构变量的首地址赋予该指针变量，不能把结构名赋予该指针变量。

注意

和普通指针的使用方法一致，可以将“结构名*”看成一种符合类型。

student stu1；

student*p=＆stu1；

上述语句声明了一个student型的结构变量stu1和一个指向student型的结构指针p，并用stu1的首地址对p赋值。

如果要访问stu1中的成员变量name，可使用下列表达式。

（*p）.name；

注意

运算符"*"的优先级低于运算符"."的优先级，所以要对*p加括号，才能使上式等价于“stu1.name；”。

结构指针和普通的指针占用相同个数的内存单元，其中存储的仍旧是某个内存单元（结构变量首字节）的地址。

5.4.2　结构指针的初始化

确切知道一个指针的指向是十分必要的，因此，在声明结构指针后，必须有意识地对其进行初始化，除了前面介绍的用取结构变量地址的方法对结构指针初始化外，再介绍两种其他方式的初始化。

1.指针间的相互赋值

指针变量也可以指向一个结构数组，此时结构指针变量的值是整个结构数组的首地址，可以用数组名指针对结构指针赋值，仍以代码5.1中定义的student结构为例加以说明。

student sz[10]；

student*p=sz；

上述代码声明了一个大小为10的student型结构数组sz，根据第4章的介绍，sz是数组名指针，代表整个结构数组的首地址，因此，可以用sz对结构指针p赋值。p便指向了结构数组sz，即指向了数组中下标为0的元素，可以用（*p）.name、（*p）.age和（*p）.weight对下标为0的元素进行访问，而p+1指向下标为1的元素，可用（*（p+1））.name、（*（p+1））.age和（*（p+1））.weight进行访问，依次类推，p+9指向下标为9的元素，应当注意的是C++仍旧不会对结构数组下标越界进行检查，*（p+10）.name的用法在编译器看来并无不妥，但可能会给程序带来意想不到的错误。

注意

结构指针的算术运算遵循指针运算的一般规则。

2.动态分配内存

下列语句声明了一个指向student型的结构指针p，并用new命令动态申请了一块大小与student型变量相同的内存，将这块内存的首地址赋值给p。

student*p=new student；

这样，便可以通过（*p）.name、（*p）.age及（*p）.weight对该内存区域进行访问。

可以一次性申请一片连续内存区域（足够容纳几个结构变量），如下所示。

student*p=new student[5]；

上述代码动态申请了一片大小为5个student变量的连续内存，并将该内存的首地址赋值给结构指针p，我们可以通过（*p）.name、（*p）.age及（*p）.weight对第一个student内存进行访问，p+1到p+4分别指向其他4个student型内存区域。

注意

对动态申请的连续内存，C++同样不进行越界检查。在本例中编译器并不会指出“（*（p+5））.name”这样的用法存在错误，但这样做确实存在很大隐患。

根据数组名指针的概念，这里的p实际上是一个大小为5的student数组的数组名，可以通过p[0]～p[4]对其中的各个元素进行访问，见代码5.6。

代码5.6　为结构指针动态分配内存StructAndNew

＜-----------------------文件名：example506.cpp-------------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　struct student//定义结构student

06　{

07　char name[20]；

08　int age；

09　float weight；

10　}；

11　//使用new申请一块能存放5个student型变量的动态内存，并赋值给结构指针p

12　student*p=new student[5]；

13　cout＜＜"请依次输入第3名学生的姓名、年龄和体重"＜＜endl；

14　cin＞＞（*（p+2））. name＞＞（*（p+2））.age＞＞（*（p+2））.weight；//第3名学生的姓名、年龄和体重

15　cout＜＜"姓名："＜＜p[2].name＜＜"，年龄："＜＜p[2].age＜＜"，体重："＜＜p[2].weight＜＜endl；

16　delete[]p；//释放动态内存p

17　return 0；

18　}

输出结果如下所示。

请依次输入第3名学生的姓名、年龄和体重

Ronaldo 30 75

姓名：Ronaldo，年龄：30，体重：75

【代码解析】代码第12行，就是为结构指针动态分配内存的示例，它是调用new操作符来申请一块能存放5个student型变量的动态内存，并赋值给结构指针p。

从代码5.6可以看出，（*（p+2））与p[2]是等价的。

注意

及时释放动态申请的内存是十分必要的，能有效防止内存泄露。

5.4.3　使用指针访问结构成员

C++提供了使用一种结构类型指针访问结构成员的方法，就是使用运算符"-＞"。

指针变量-＞成员名

这与“（*指针变量）.成员名”是完全等价的，见代码5.7。

代码5.7　使用指针访问结构成员StructMemberAccess

＜-----------------------文件名：example507.cpp-------------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　struct student//定义结构体student

06　{

07　char name[20]；

08　int age；

09　float weight；

10　}stu1={"Kaka"，23，70}；//在结构体定义的同时声明一个结构变量stu1，并初始化

11　student*p=＆stu1；//使用结构变量stu1的地址为student型指针p初始化

12　//使用结构变量名访问结构成员

13　cout＜＜"姓名："＜＜stu1.name＜＜"，年龄："＜＜stu1.age＜＜"，体重："＜＜stu1.weight＜＜endl；

14　//使用指针的间接形式访问结构成员

15　cout＜＜"姓名："＜＜（*p）.name＜＜"，年龄："＜＜（*p）.age＜＜"，体重："＜＜（*p）.weight＜＜endl；

16　//直接使用指针和-＞访问结构成员

17　cout＜＜"姓名："＜＜p-＞name＜＜"，年龄："＜＜p-＞age＜＜"，体重："＜＜p-＞weight＜＜endl；

18　return 0；

19　}

输出结果如下所示。

姓名：Kaka，年龄：23，体重：70

姓名：Kaka，年龄：23，体重：70

姓名：Kaka，年龄：23，体重：70

【代码解析】代码第17行，就是使用结构指针访问结构成员，并输出其相应的数值。

输出结果证明，下列3种访问结构成员的方法是完全等效的。

❑结构变量.成员名

❑（*结构指针变量）.成员名

❑结构指针变量-＞成员名

5.5　链表

第3章中已经介绍过数组的概念，数组对应着一个连续存储的内存块，将同类型的元素一个个地排列起来，实际上还可以利用指向变量的指针把一系列的变量组织起来，形成一种新的数据结构，称之为链表。

5.5.1　链表的结构

链表元素常称为链表节点，每一个节点包含两个域即数据域和指针域。数据域保存数据，指针域连接该节点到下一个节点。可以看出，节点数据是一种复合类型即结构以及在第三篇中要介绍的类的对象，每一个节点占用一块存储单元，当要在链表中增加一个节点时，可动态地为该节点分配一个存储单元；当要在链表中删除一个节点时，也可释放该节点的存储单元。

图5.2为3个节点（A、B和C）互相链接形成链表的示意图，其特点是每个节点最多只有一个先驱节点和一个后继节点，首节点A只有一个后继节点，没有前驱节点，而最终节点C只有一个前驱节点没有后继节点，中间的节点B既有一个前驱节点又有一个后继节点。每个节点都由两部分组成，一是用来存放各种实际数据的数据域，另一个是指针域，存放下一个节点的首地址。例如，A的地址域中存放的是B的地址＆B。

图　5.2　链表结构图

另外，每个链表都有一个头指针head，存放的是链表首节点的地址。可以通过链表的head指针找到链表的首节点，由首节点的指针域找到下一个节点的首地址，依次类推，可以找到该链表的所有节点。需要注意的是最终节点的指针域为NULL，据此可以判断一个链表是否结束。如果表节点个数为0，我们称之为空表，此时head指针为NULL。

节点的数据类型可以相同，也可以不同。当节点的类型相同时，形成同质链表，否则形成异质链表。在实际应用中常用的是同质链表。

5.5.2　创建链表

下面通过示例代码5.8来了解链表，同质链表的创建和使用与异质链表的创建和使用与此相同。

代码5.8　同质链表的创建CreateAList

＜----------------------文件名：example508.cpp--------------------------------＞

01　#include＜iostream＞

02　struct student//定义结构student

03　{

04　char name[20]；

05　int age；

06　student*next；//指向下一个元素的指针

07　}；

08　int main（）

09　{

10　using namespace std；

11　student c={"Kaka"，23，NULL}；//声明末尾结构变量，指向为NULL

12　student b={"Deco"，27，＆c}；//声明c之前结构变量b，＆c为b.next初始化

13　student a={"Terry"，30，＆b}；//申明b之前结构变量a，＆b为a.next初始化

14　student*head=＆a；//头指针，指向a

15　student*pointer=head；//使用指针pointer访问链表中的元素

16　cout＜＜"Head-＞"；

17　while（pointer）//while（pointer！=NULL）

18　{

19　cout＜＜（*pointer）. name＜＜"-＞"＜＜（*pointer）.age＜＜"-＞"；

20　pointer=（*pointer）. next；//指向下一个元素

21　}

22　cout＜＜"End"＜＜endl；

23　return 0；

24　}

输出结果如下所示。

Head-＞Terry-＞30-＞Deco-＞27-＞Kaka-＞23-＞End

【代码解析】代码第20行，就是将结构指针指向链表元素中的下一个元素，只有当链表指针为NULL时，整个循环结束。

链表的创建有以下两个步骤。

❑在节点数据中增加一个指针next，以指向逻辑上的下一个节点。

❑设置一个指向首结点的指针（代码5.8中的head指针），将最终节点的指针置为NULL（代码5.8中的c.next）。

注意

链表中的先后是逻辑上的关系，不代表节点在内存中的位置先后。

5.5.3　链表与数组的区别

链表和数组各有特点，表5.1从内存占用、元素类型、组织形式和插入删除元素4个方面直观地体现了两者的区别。

5.5.4　链表的遍历和查找

链表的结构相对而言比较特殊，在对链表中的节点进行访问和查找的时候，必须从链表的头节点开始，按照链表节点指针域所指的顺序逐个查找节点，直到找到为止。同对数组元素的访问一样，利用循环结构对链表节点进行遍历是一种常用的方法，代码5.8中使用的就是while循环结构，循环的终止条件便是判断next指针是否为NULL，即是否已经到达了最终节点。

举例而言，对代码5.8创建的链表，想要知道name成员为“Deco”的节点中age成员的值，通过代码5.9可以完成该功能。

代码5.9　链表的遍历和查找ListOperation

＜-----------------------文件名：example509.cpp-------------------------------＞

01　#include＜iostream＞

02　#include＜string＞

03　struct student//定义结构student

04　{

05　char name[20]；

06　int age；

07　student*next；//指向下一个元素的指针

08　}；

09　int main（）

10　{

11　using namespace std；

12　student c={"Kaka"，23，NULL}；//声明末尾结构变量，指向为NULL

13　student b={"Deco"，27，＆c}；//声明c之前结构变量b，＆c为b.next初始化

14　student a={"Terry"，30，＆b}；//申明b之前结构变量a，＆b为a.next初始化

15　student*head=＆a；//头指针，指向a

16　student*pointer=head；//使用指针pointer访问链表中的元素

17　bool isFind=false；//设置标志，bool型变量isFind

18　//从头到尾遍历链表，当到达末尾时pointer等于NULL，退出for循环

19　for（；pointer；pointer=（*pointer）. next）

20　{

21　if（strcmp（"Deco"，（*pointer）. name）==0）//看元素中name成员是否为Deco

22　{

23　cout＜＜"Deco's age："＜＜（*pointer）. age＜＜endl；//如果有此项，输出年龄

24　isFind=true；//设置标志为true

25　break；//跳出for结构

26　}

27　}

28　if（！isFind）

29　{

30　cout＜＜"没有找到该项"＜＜endl；

31　}

32　return 0；

33　}

输出结果如下所示。

Deco's age：27

【代码解析】为了和代码5.8有所区分，我们利用了for循环结构对链表进行了遍历查找，代码第21行，应用C风格字符串处理函数strcmp（）判断节点结构的成员变量name（字符数组）是否和“Deco”一致，如果一致证明已经找到所需数据，用“break；”语句跳出循环，否则，将下一节点的指针赋给pointer，继续查找，直到到达链表的最终节点，此时，pointer指针的值为NULL,for循环测试条件不满足，遍历结束。

5.5.5　链表的插入和删除

与数组相比，链表可以很方便地进行数据元素的插入和删除，在一个链表中插入一个节点或删除一个节点，只需要知道待插入或删除节点的位置及指向前一个节点的指针。图5.3是链表节点插入删除操作的形象示意图。

图　5.3　链表节点的插入删除示意图

从图5.3可以看出，向一个链表中插入一个节点，只需要以下两步操作。

❑将插入位置前一节点（B）的指针域赋值给待插入节点（E）的指针域，使其指向插入位置后一节点（C）。

❑将待插入节点（E）的地址赋值给前一节点（B）的指针域。

从一个链表中删除一个节点，只需将待删除节点（C）的指针域赋值给删除位置前一节点（B）的指针域即可。

注意

将节点插入在链表头或删除首节点时，要对head指针重新赋值。

举例来说，有一个存储学生信息的链表，是按照年龄大小也就是节点的成员变量age的大小升序排列的，现在要插入一个新节点或删除一个旧节点，但要保持链表的排序关系，代码5.10和代码5.11分别演示了链表节点的插入和删除操作。

代码5.10　链表节点的插入InsertANode

＜-----------------------文件名：example510.cpp-------------------------------＞

01　#include＜iostream＞

02　struct student

03　{

04　char name[20]；

05　int age；

06　student*next；

07　}；

08　int main（）

09　{

10　using namespace std；

11　student c={"Terry"，30，NULL}；

12　student b={"Deco"，27，＆c}；

13　student a={"Kaka"，23，＆b}；

14　int age；

15　cout＜＜"请输入likuan的年龄，你输入的数值将决定该节点插入链表的位置："＜＜endl；

16　cin＞＞age；

17　student insertD={"likuan"，age,NULL}；

18　student*head=＆a；

19　student*pointer=head；

20　student*before=NULL；

21　while（pointer）

22　{

23　if（insertD. age＜=（*pointer）.age）//寻找插入位置

24　break；

25　before=pointer；//记录插入位置前一节点的地址

26　pointer=（*pointer）. next；

27　}

28　if（before==NULL）

29　{

30　insertD. next=head；//在链表头部插入

31　head=＆insertD；

32　}

33　else

34　{

35　insertD. next=（*before）.next；//在链表中间或末尾插入

36　（*before）. next=＆insertD；

37　}

38　student*coutPointer=head；//输出链表数据

39　cout＜＜"Head-＞"；

40　while（coutPointer）//while（coutPointer！=NULL）

41　{

42　cout＜＜（*coutPointer）. name＜＜"-＞"＜＜（*coutPointer）.age＜＜"-＞"；

43　coutPointer=（*coutPointer）. next；

44　}

45　cout＜＜"End"＜＜endl；

46　return 0；

47　}

输出结果如下所示。

请输入likuan的年龄，你输入的数值将决定该节点插入链表的位置：

21　（注：键盘输入）

Head-＞likuan-＞21-＞Kaka-＞23-＞Deco-＞27-＞Terry-＞30-＞End或

请输入likuan的年龄，你输入的数值将决定该节点插入链表的位置：

24　（注：键盘输入）

Head-＞Kaka-＞23-＞likuan-＞24-＞Deco-＞27-＞Terry-＞30-＞End

【代码解析】代码中声明了一个struct型指针pointer用于链表的遍历，声明了struct型指针before用以记录要插入位置前一个节点的地址，并对before初始化为NULL，执行完毕第一个while结构后，如果before仍为NULL，说明insertD.age小于首节点的age变量，应当将insertD插入在链表的头部，将head指针赋值给insertD的指针域，并将insertD的指针赋值给head指针，否则，应将insertD插入到before所指的节点后，将before所指节点的指针域赋值给insertD的指针域，并将insertD的地址赋值给before所指节点的指针域，后一个while结构用于输出链表数据，说明节点插入正确。

代码5.11　链表节点的删除RemoveANode

＜-----------------------文件名：example511.cpp-------------------------------＞

01　#include＜iostream＞

02　#include＜string＞

03　struct student

04　{

05　char name[20]；

06　int age；

07　student*next；

08　}；

09　int main（）

10　{

11　using namespace std；

12　char deleteName[20]；

13　cout＜＜"请输入要删除的节点姓名（Kaka,Deco或Terry）："＜＜endl；

14　cin＞＞deleteName；

15　student c={"Terry"，30，NULL}；

16　student b={"Deco"，27，＆c}；

17　student a={"Kaka"，23，＆b}；

18　student*head=＆a；

19　student*pointer=head；

20　student*before=NULL；

21　bool isFind=false；

22　while（pointer）

23　{

24　if（strcmp（deleteName，（*pointer）. name）==0）//寻找删除位置

25　{

26　isFind=true；//已经找到

27　break；

28　}

29　before=pointer；//记录删除位置前一节点地址

30　pointer=（*pointer）. next；

31　}

32　if（isFind）

33　{

34　if（before==NULL）

35　head=（*head）. next//删除链表首记录

36　else

37　{

38　（*before）. next=（*pointer）.next；//在链表中间或末尾删除

39　}

40　}

41　else

42　cout＜＜"未找到你输入的项"＜＜endl；//未找到输入数据

43　student*coutPointer=head；//输出链表数据

44　cout＜＜"Head-＞"；

45　while（coutPointer）//while（coutPointer！=NULL）

46　{

47　cout＜＜（*coutPointer）. name＜＜"-＞"＜＜（*coutPointer）.age＜＜"-＞"；

48　coutPointer=（*coutPointer）. next；

49　}

50　cout＜＜"End"＜＜endl；

51　return 0；

52　}

输出结果如下所示。

请输入要删除的节点姓名（Kaka,Deco或Terry）：

Kaka（注：键盘输入）

Head-＞Deco-＞27-＞Terry-＞30-＞End或

请输入要删除的节点姓名（Kaka,Deco或Terry）：

Terry（注：键盘输入）

Head-＞Kaka-＞23-＞Deco-＞27-＞End

【代码解析】代码5.11声明了一个struct型指针pointer用于链表的遍历，查找成员变量name与用户输入的字符串相同的节点，声明了struct型指针before用以记录待删除前一个节点的地址，并对before初始化为NULL,bool型变量isFind用来判断在链表中是否存在包含用户输入字符串的节点，如果“strcmp（deleteName，（*pointer）.name）==0”，isFind为true，证明找到了成员变量name与用户输入的字符串相同的节点。

执行完毕第一个while结构后，如果before仍为NULL，说明用户要删除的是链表的首节点，将首节点的指针域赋值给head指针，否则，说明要删除的是中间节点或最终节点，执行“（*before）.next=（*pointer）.next；”即可，输出结果证明程序进行的删除操作是正确的。

5.6　小结

结构是自定义数据类型中的一种，其可将多种数据类型组合在一起使用，方便了程序对一些复杂数据的处理。共用体是一种特殊的结构，在某个确定的时刻，共用体只能表示一种数据类型，各种数据类型在内存中占据同一地址。

结构和共用体的使用都可分为定义和变量声明两个步骤，在声明结构变量或共用体变量以前，必须先进行结构类型或共用体类型的定义。访问数据成员，可用的成员运算符有两个即"."和"-＞"。“（*指针变量）.成员名”、“变量.成员名”和“指针变量-＞成员名”3种方式是等价的。最后介绍了链表，链表综合了结构和指针的有关内容，与数组相比链表有诸多优点，使用好链表，可以更好地将程序需要的数据组织起来。

5.7　上机实践习题

1.定义一个学生信息的结构，并对其进行初始化，最后输出结果。

【提示】上述题目主要是要求读者了解结构的定义及使用的知识，重点是掌握如何声明、初始化及使用。

【关键代码】

01　struct student//定义结构体

02　{

03　char name[20]；

04　int age；

05　float weight；

06　}；

07　//主要代码

08　using namespace std；

09　student stu1={"Ronaldo"，30，70. 5}；

10　cout＜＜"姓名："name"＜＜"，年龄："age"＜＜"，体重："＜＜weight"＜＜endl；

11　cout＜＜stu1.name＜＜stu1.age＜＜＜＜stu1.weight＜＜endl；

2.定义共用体info，可以保存学生的年级或者学校的部门，然后根据用户输入的数据进行相应的输出。

【提示】上述题目主要是要求读者了解共用体的相关知识，重点是掌握共用体的特点及其使用过程。

【关键代码】

01　union info//定义共用体info

02　{

03　int grade；//年级，面向学生

04　char department[20]；//部门，面向老师

05　}；

06　using namespace std；

07　info personInfo；//声明共用体变量personInfo

08　int ans=-1；

09　cout＜＜"老师还是学生?（输入0代表学生，1代表老师）："＜＜endl；

10　cin＞＞ans；

11　//同一时刻，grade和department中只能有一个被写入

12　if（ans==0）

13　{

14　cout＜＜"欢迎你，请输入你所在的年级："＜＜endl；

15　cin＞＞personInfo. grade；

16　cout＜＜"你的年级是："＜＜personInfo.grade＜＜endl；

17　}

18　if（ans==1）

19　{

20　cout＜＜"欢迎你，请输入你所在的部门："＜＜endl；

21　cin＞＞personInfo. department；

22　cout＜＜"你的部门名是："＜＜personInfo.department＜＜endl；

23　}

3.建立一个学生信息链表，接收用户输入的信息，然后根据用户需要查询的数据输出结果。

【提示】上述题目主要是要求对结构及指针有所了解，重点掌握链表的使用。

【关键代码】

01　struct student//定义结构student

02　{

03　char name[20]；

04　int age；

05　student*next；//指向下一个元素的指针

06　}；

07

08　using namespace std；

09　int i=0；

10　int bfound=0；

11　student c={"Kaka"，23，NULL}；//声明末尾结构变量，指向为NULL

12　student b={"Deco"，27，＆c}；//声明c之前结构变量b，＆c为b.next初始化

13　student a={"Terry"，30，＆b}；//申明b之前结构变量a，＆b为a.next初始化

14　student*head=＆a；

15　student*pointer=head；

16　char name[128]={0}；

17

18　cout＜＜"请输入学生姓名"＜＜endl；

19　cin＞＞name；

20

21　while（pointer！=NULL）

22　{

23　if（strcmp（pointer-＞name,name）==0）

24　{

25　bfound=1；

26　break；

27　}

28

29　pointer=pointer-＞next；

30　}

31

32　if（bfound）

33　{

34　cout＜＜"name"＜＜pointer-＞name＜＜"age"＜＜pointer-＞age＜＜endl；

35　}

36　else

37　{

38　cout＜＜"no found"＜＜endl；

39

}

第6章　用函数合理组织程序

函数是构成C++程序的基石，使用函数可以将程序分解成一个个的模块，每个模块实现一个相对独立的功能，这些模块相互联系，共同组成了完成某项任务的程序。在前几章给出的示例代码都是相对简单的程序，只有一个main（）函数，实际上，随着问题规模的加大，要进行的操作和处理的变量会有很多，程序的复杂度很高，如果程序不分模块，所有的操作都在main（）函数内完成，会使程序变成一团乱麻。因此，函数是合理组织过程式程序的有效手段。

本章主要涉及以下知识点。

❑模块化的优点：介绍什么是函数及如何实现函数。

❑函数的定义：介绍什么是函数头、函数体及如何定义。

❑函数的声明：介绍函数声明的重要性及如何声明一个函数。

❑函数的调用：介绍参数的几种传递方式及内联函数。

❑函数的重载：介绍函数重载如何使用及其实现。

❑C++的内存使用：介绍自动与静态存储。

❑作用与可见域：介绍变量及函数的作用和可见域。

6.1　模块化带来的好处

从软件工程的角度来说，降低程序复杂性的有效方法是合理的模块化和局部化。在设计一个复杂的程序时，往往需要把整个程序划分为若干个功能较为单一的模块，分别予以实现，再把所有的模块像搭积木一样搭起来，这种在程序设计中分而治之的策略，被称为模块化程序设计方法。在C++中，这些模块就是一个个的函数，函数也是C++构造程序的重要的基本单位。

注意

语句是构成程序的最基本单位，函数也是由语句构成的。从程序设计的角度来说，函数（一系列语句）常被当成一个整体来看，这就降低了程序的复杂性。

6.1.1　函数的调用过程

在前面的学习中大家已经熟悉了main（）函数，一个C++程序里包含一个主函数，即main（）函数和若干个其他函数，main（）函数处于最顶层，其他函数作为其下层模块，main（）函数调用其他函数，其他函数之间也可以互相调用。图6.1为一个C++程序的模块结构图，程序的执行过程为ABCDEFGHIJKM。

任何一个C++程序都是从main（）函数开始执行，而且是只执行main（）函数，从main（）的前花括号开始，到main（）的后花括号为止。在此过程中，如果碰到函数调用语句（如图6.1中的“函数1；”），便暂时中断main（）的执行，将程序流程转到被调用函数（对应图6.1中的B），执行完被调用函数或遇到return语句则返回main（）函数（对应图6.1中的H），继续执行，一个函数也可以调用其他函数（如图6.1中函数1调用函数3），这时的调用过程与main（）函数调用其他函数过程类似，常称为函数嵌套。

注意

通过函数合理组织程序，将要完成的任务进行有效分割，能大大降低程序开发的复杂度，写出容易修改和维护的高质量代码。

6.1.2　抽象和封装

图　6.1　C++模块结构图

面向过程的程序设计是基于功能分析的，我们最关心的是如何实现一个模块的功能以及如何使用这个模块，至于模块内部的结构，对其他模块来说是不重要的，完全可隐藏的。对函数而言，这个道理同样适用，在第2章主函数一节中提到了函数由函数头和函数体两部分组成，而函数头定义了函数和调用它的函数之间的接口。

在C++程序中，函数可以看成一个封装体，将一系列相关的与实现某一功能的代码封装起来，并提供了一个使用方法（程序中常称接口），通过该接口可以在程序的任何地方使用这些代码完成特定功能，至于函数是如何编写的，可能并不是用户关心的重点，用户真正关心的是这个函数如何使用。

这种抽象和封装机制是程序设计的精髓所在，通俗地讲，假设我们要做一个木推车，用木料做车板、做车轮是最基本的做法，要求有一定的木工基础，但如果手头上已经有现成的车轮和车板可以用呢？只要组装在一起就好了，制作的难度会大大降低。我们可以让有一定木工基础的人做一批车轮，用的时候拿来安装就好了，我们关心的是如何用车轮（比如车轮的半径和车轮轴的大小等），而不是如何做车轮。

在这里，将C++程序比作手推车，木料可以理解为程序中的语句，我们可以不使用模块或函数，只通过语句，用一个main（）函数来完成任务，但这是个笨法子，而且要求编程者有一定能力，不会轻易被像乱麻一样的流程弄迷糊，车轮可理解成C++中的模块或函数，把实现特定功能的代码封装在函数中，提供使用接口，在需要的时候我们只要调用模块或函数，不用再把同样的代码重写一遍，有效地提高了程序设计的效率，降低了难度。

还可以使用别人写好的函数，不必知道函数是如何写的，只要知道函数如何调用，C++的标准库函数就是实例。在第3章中介绍了运用C风格字符串处理函数，如返回字符串长度的strlen（）函数，只要按照C++语言规则对函数进行声明和调用即可，不必关心其内部实现的细节。

说明

不仅有标准库函数，许多软件厂商还开发出商业化的代码库，供用户调用以实现特定的功能，模块和函数的使用在一定程度上也刺激了开发过程的商业化。

函数的本质有以下两点。

❑函数由能完成特定任务的独立程序代码块组成，如有必要，也可调用其他函数。

❑函数内部工作对程序的其余部分是不可见的。

6.1.3　实现一个函数

实现一个函数有3个步骤即定义、声明与调用。拿电影来作比喻，定义等价于电影的拍摄，声明等价于电影院得到放映许可，调用是电影院放电影，电影院可以自行拍摄，也可以拿别的单位拍的电影来放。

在程序中，这意味着可以自己定义函数，也可以使用诸如标准库或第三方库提供的函数。但在使用前都要进行声明，通知编译器函数的存在，以获得函数的使用许可，才能进行调用，声明后程序可以多次调用函数，等同于电影院在获得放映许可后，可以多次放映影片。见示例代码6.1。

代码6.1　实现函数的3个步骤FunctionSample

＜------------------------文件名：example601.cpp------------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　int add（int x,int y）；//函数声明

06　int num1=1，num2=2；

07　int numTotal=add（num1，num2）；//函数调用

08　cout＜＜"两数相加结果为："＜＜numTotal＜＜endl；

09　return 0；

10　}

11　int add（int x,int y）//函数定义

12　{

13　int z=x+y；

14　return z；//返回值

15　}

输出结果如下所示。

两数相加结果为：3

【代码解析】代码第11行的add（）函数实现了两数相加的功能。从函数头可以看出，add（）函数接收传来的两个整数值，将两数相加，作为结果返回。代码第5行，在main（）函数中通过语句“int add（int x,int y）；”声明add（）函数，通知编译器add（）函数的存在，获得函数的使用权，才能调用函数，实现对变量num1和num2的相加，关于程序定义、声明和调用的详细介绍将在本章稍后的内容中给出。

技巧在代码6.1中，如果add（）函数定义在main（）函数前面，那么函数声明语句可以省略。

6.2　函数定义

函数定义由函数头和函数体两部分组成，其基本形式如下所示。

返回类型函数名（参数列表）

{

函数体

}

函数定义通过这一结构告诉编译器要进行的操作。

6.2.1　函数头

第一行“返回值类型函数名（参数列表）”称为函数头，定义了函数和调用它的函数之间的接口。

1.函数名

上级函数通过函数名实现对函数的调用，函数名是一个符合C++语法要求的标识符。定义函数名与定义变量名的规则是一样的，但应尽量避免用下划线开头，因为编译器会常常定义一些以下划线开头的变量或函数。函数名应尽可能反映函数的功能，做到“望文知义”。

2.参数列表

0个或多个变量，用于向函数传送数值或从函数带回数值，每个参数都应采取“类型变量名”形式，参数列表中的参数称为形式参数，简称形参。编译器并不会在函数定义时为这些参数分配内存空间，只有在函数调用时，向函数传递了实参后，这些参数才称为程序实体，形参相当于剧本中的角色，而实参是演员，在代码6.1中，函数定义中的x和y是剧本角色，而变量num1和num2是演员，num1扮演了x的角色，num2扮演了y的角色。

如果参数列表中参数个数为0，我们称之为无参函数，无参函数可以定义为如下所示。

返回类型函数名（）{……}或

返回类型函数名（void）{……}

3.返回类型

指定函数用return返回的函数值的类型，如果函数没有返回值，返回类型应为void。C++对返回值的类型有一定的限制，它不能是数组，但可以是其他任何类型，如整型、浮点型及指针，甚至是结构和共用体等。

注意

C++不允许返回数组，但却可以把数组当做结构的组成部分返回。

6.2.2　函数体

花括号中的语句称为函数体，一个函数的功能，通过函数体中的语句来完成，函数体指明了函数要进行的操作及操作顺序。

程序执行到函数体中的return语句返回，在函数体中可以有多个return语句，但函数只能有一个出口，换句话说，只执行一条返回语句，返回语句的基本形式如下所示。

return表达式；

表达式的类型应当与函数头中指定的返回类型一致，否则，编译器会根据函数头中指定的返回类型对表达式进行转换。

返回主要起以下3个作用。

❑撤销函数调用时为参数和变量分配的栈内存空间；

❑向调用函数（上级）返回最多一个值（表达式的值）；

❑将程序流程从当前函数返回上级函数。

代码6.1　中，add（）函数的定义如下所示。

int add（int x,int y）

{

int z=x+y；

return z；

}

其函数头为“int add（int x,int y）”，指明函数名为add，有两个int型形参x和y，函数返回值也为int型。在函数体中声明了int型变量z，并对其初始化为x+y，完成了加法操作，最后将z的值返回给上级函数。

6.2.3　函数定义补充说明

当函数的返回类型是void时，表明函数不向上级函数返回任何值，这时可以用一个空的“return；”语句，将程序流程返回，撤销函数调用时为参数和变量分配的栈内存空间，空的“return；”语句位于函数末尾时，该语句可以省略，用函数体的后花括号实现函数的返回即可。

通常用返回类型为void的函数执行某些操作，见代码6.2。

代码6.2　void型函数VoidFunction

＜---------------------------文件名：example602.cpp---------------------------＞

01　#include＜iostream＞

02　int main（）

03　{

04　using namespace std；

05　void print（）；//函数声明

06　print（）；//函数调用

07　return 0；

08　}

09　void print（）//函数定义，void表示没有返回值

10　{

11　using namespace std；//要使用cout，同样要使用using编译语句

12　int n；

13　cout＜＜"请输入一个整数n："＜＜endl；

14　cin＞＞n；

15　cout＜＜"你输入的数是："＜＜n＜＜endl；

16　}

输出结果如下所示。

请输入一个整数n：

12　（注：键盘输入）

你输入的数是：12

函数print（）返回值是void类型，只完成接收用户输入并将其输出这个操作，不向main（）函数返回任何值，print（）函数最后的return空语句已省略。

【代码解析】在main（）函数内的“using namespace std；”语句只能使std名称空间中所有的名称在main（）函数内使用，要想在函数print中应用cout和cin，必须在print（）函数内部，即代码第11行使用“using namespace std；”语句，一个替代方案是在main（）函数外部使用“usingnamespace std；”语句，这样便可在文件中任何地方使用std名称空间中所有的名称，代码6.2与下列代码是等价的。

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　void print（）；//函数声明

06　print（）；//函数调用

07　return 0；

08　}

09　void print（）//函数定义

10　{

11　int n；

12　cout＜＜"请输入一个整数n："＜＜endl；

13　cin＞＞n；

14　cout＜＜"你输入的数是："＜＜n＜＜endl；

15　}

另外，一个函数必须定义在其他函数的外部，并且在一个程序中只允许定义一次，否则编译器会给出重复定义的错误提示。

6.3　函数声明

函数声明也称函数原型。函数声明，用以通知编译器函数的存在，以获得函数的使用许可，只有这样才能在程序中对函数进行调用。

6.3.1　为什么要进行函数声明

函数声明描述了函数和编译器间的接口，想要调用一个函数，必须在调用函数中对被调用函数进行说明。

在代码6.1中，main（）函数中的“int add（int x,int y）；”用于在main（）函数内声明add（）函数，使其在main（）函数内可用，同时告诉编译器，add（）函数接收两个int型的输入参数，如果程序没有提供这样的参数，编译器便会指出错误，或对传入的其他类型参数进行隐式转换。

在add（）函数完成计算后，将把返回值放置到指定位置（可能是CPU寄存器，也可能是某个内存单元），然后上级函数（代码6.1中为main（）函数）从这个位置取得返回值，add（）函数的声明指出了返回值类型为int，编译器借此知道应检索多少内存字节并对这些字节作出解释。图6.2形象化地说明了代码6.1中函数声明的作用和返回值机制。

函数声明也可以放在main（）函数外部以使其在本程序中可见，这样便可以在程序的任何地方使用该函数，关于这部分的内容参见本章稍后的介绍。

有的读者可能会问：“编译器不能在程序代码中自行寻找函数的定义来决定如何对函数进行调用吗？”这样做会降低编译的效率，使得编译器在编译main（）函数时要经常停下来，并且C++允许将程序代码放在多个文件中，编译器对每个文件单独进行编译，通过链接将编译后的文件组合成可执行的程序，也就是说，调用函数和被调用函数的定义可能不在同一个文件中，在对调用函数进行编译时，编译器可能无权访问被调用函数所在的文件（代码），因此，声明语句的存在就显得十分必要。

图　6.2　代码6.1对应的函数声明与返回值机制

在调用函数之前对函数进行定义可以免去函数声明这一步骤，但在很多情况下，函数的调用是相互交织的，这种方法并不可行。除了在调用函数中对被调用函数进行声明使其可用外，还可以在函数外部（不属于任何一个函数定义范围）对被调用函数进行声明，这样便可以在本程序的任何地方使用该函数。但是，如果程序由许多文件组成，在别的文件中使用该函数同样要进行声明，换言之，外部声明并不具有全局可见性，参见本章稍后关于作用域和可见性的内容介绍。

6.3.2　如何声明一个函数

函数声明类似于函数定义，只不过没有实现代码，函数声明的一般形式如下所示。

返回类型函数名（参数列表）；

函数声明是一个语句，所以要以分号结束，在书写函数声明时，只要把函数头复制下来，并在末尾添加分号即可。

注意

语句结尾处有无分号常常可用来区分是函数声明还是函数定义。

如代码6.1中的“int add（int x,int y）；”，函数声明只要与函数定义一致，能提供给编译器足够的信息即可，因此，C++中的函数声明不要求提供变量名，add（）函数的声明可以写成如下形式。

int add（int,int）；

从上述声明中编译器可知函数名为add，接收两个int型的参数，返回值类型为int，这些信息已经足够明确了。“int add（int x,int y）；”中的变量名x和y仅仅起到增强程序可读性的作用，其中的变量名可以与函数定义中的形参不同，也就是说，将声明语句写成下列形式丝毫不会影响程序的编译和运行。

int add（int A,int B）；

具体来说，函数声明有以下3个作用。

❑使编译器正确处理返回值；

❑使编译器可以检查输入参数的数目；

❑使编译器检查输入参数的类型，如果类型不正确，则对类型进行隐式转换。关于输入参数类型转换的内容，稍后会对其进行详细的介绍。

6.3.3　分割程序文件

一些程序经常会由许多文件组成，为了方便管理，常将函数定义在cpp文件中，而将函数的声明语句放在与cpp文件同名的头文件（h文件）中，这样就可以通过编译预处理“#include＜xxx.h＞”或“#include"xxx.h"”实现函数的声明，这种方法在大型程序文件的组织中十分有用，见代码6.3。

说明

“#include＜xxx. h＞”用于C++系统提供的头文件，这些头文件一般位于C++系统目录中的include子目录下，而“#include"xxx.h"”适用于用户自己建立的头文件，预处理器接收到该指令后，会首先在当前文件所在的目录中进行搜寻，如果找不到，再到C++系统头文件中寻找。

代码6.3　多个文件构成一个程序MultiFile

＜---------------------------文件名：example603.cpp---------------------------＞

01　#include"func.h"//包含自定义的头文件，声明需要的函数add（）、print（）和minus（）

02　int main（）

03　{

04　int num1=2，num2=1；

05　int z=add（num1，num2）；//add（）函数调用

06　print（z）；//print（）函数调用

07　z=minus（num1，num2）；//minus（）函数调用

08　print（z）；//print（）函数调用

09　return 0；

10　}

＜---------------------------------文件名：func.cpp---------------------------＞

11　#include＜iostream＞

12　using namespace std；

13　int add（int x,int y）//add（）函数定义

14　{

15　int z=x+y；

16　return z；//返回值

17　}

18　int minus（int x,int y）//minus（）函数定义

19　{

20　int z=x-y；

21　return z；//返回值

22　}

23　void print（int x）//print（）函数定义，无返回值

24　{

25　cout＜＜x＜＜endl；

26　}

＜-------------------------------文件名：func.h-------------------------------＞

27　int add（int,int）；//add（）函数原型声明

28　int minus（int,int）；//minus（）函数原型声明

29　void print（int）；//print（）函数原型声明

输出结果如下所示。

3

1

【代码解析】函数add（）、minus（）和print（）都定义在func.cpp中。代码第27～29行，是这几个函数的声明（即函数原型），其都放在func.h中。因此，只要在example603.cpp中使用编译预处理语句“#include"func.h"”便可实现3个函数的声明，且这种声明方法使3个函数具有文件可见性，可以在example603.cpp中任何地方对这3个函数进行调用。在example603.cpp中，输出操作都由print（）函数来完成，因此，不必再使用条件预处理语句“#include＜iostream＞”和“using namespace std；”语句，当然，如果要在example603.cpp中使用cin和cout进行输入输出操作，仍旧要使用条件预处理语句“#include＜iostream＞”和“using namespace std；”语句。

函数定义文件（如func.cpp）和函数声明文件（如func.h）使用同一文件名纯粹是为了方便管理，理论上讲程序的文件名可以是任意的。

注意

头文件不会被编译，只起辅助编译的作用。

6.4　函数调用

在前面的代码中，读者已经知道了如何调用一个函数，函数定义和函数声明的目的都是为了函数调用，只有函数调用才能利用函数实现某个功能，函数调用的基本形式如下所示。

函数名（实参列表）；对于无参函数，其调用形式如下所示。

函数名（）；

函数调用由函数名和函数调用运算符（）组成，（）内有0个或多个逗号分隔的参数（称为实参）。每一个参数是一个表达式，且参数的个数与参数的类型要与被调函数定义的参数（称为形参）个数和类型匹配，首先计算参数表达式的值，并将此值传递给形参。如果函数调用后有返回值，调用表达式可以用在表达式中，如代码6.1中的“int numTotal=add（num1，num2）；”，而无参函数的调用必须是一个单独的语句，如“print（）；”。

函数调用的主要作用有以下3个。

❑用实参向形参传递数据；

❑为获得数据的参数和函数中声明的变量分配临时存储空间；

❑中断当前正在运行的上级调用函数，将程序流程转到被调用函数的入口处。

6.4.1　形参和实参

形参和实参，类似于剧本角色和演员的关系，同一个角色可以由不同的演员来扮演，只有在演员扮演的过程中，角色才是鲜活、有意义的。前面的章节已经提到，在函数定义时，并不会为参数列表中的参数分配内存空间，只有在函数调用时，才为形参分配内存空间，并用实参的值为其赋值，在执行到函数结束时，程序会撤销调用过程中为参数和中间变量分配的内存空间。

代码6.4　演示了形参和实参的关系。

代码6.4　形参和实参ParameterAndArgument

＜-------------------------文件名：example604.cpp-----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　int n=2；

06　cout＜＜"地址："＜＜＆n＜＜endl；//输出实参变量n的地址

07　cout＜＜"数值："＜＜n＜＜endl；

08　void print（int）；//函数声明

09　print（n）；//函数调用

10　return 0；

11　}

12　void print（int n）//函数定义

13　{

14　cout＜＜"地址："＜＜＆n＜＜endl；//输出形参n的地址

15　cout＜＜"数值："＜＜n＜＜endl；

16　}

输出结果如下所示。

地址：0013FF7C

数值：2

地址：0013FF2C

数值：2

【代码解析】main（）函数中的变量n和代码第9行print（）函数中的参数n占据不同的内存地址，这说明在调用print（）函数时，编译器为形参n分配了内存空间，并将main（）函数中变量n的值赋给这块内存区域，在结束后这块内存区域将被释放。

main（）函数中的变量n和print（）函数中的形参n（甚至是在print（）函数内部声明的变量）是完全不同、毫无关系的。在函数内声明的变量是该函数私有的，外部不可见的，因此，对函数中变量n的操作不会影响main（）函数中n的数据，因为形参只是实参的复制，而不是原来的数据。

6.4.2　参数类型转换

调用函数时，如果实参类型与形参类型不匹配，编译器会对实参自动进行类型转换（隐式转换），形参的类型，取决于函数的声明语句，见代码6.5。

代码6.5　参数类型转换AutoConversion

＜--------------------------文件名：example605.cpp----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　double m=12；//声明一个double型变量m

06　void print（int）；//函数声明

07　print（m）；//函数调用时，实参m隐式转换为int型传递给形参

08　return 0；

09　}

10　void print（int n）//函数定义

11　{

12　cout＜＜"数值："＜＜n＜＜endl；

13　}

输出结果如下所示。

数值：12

【代码解析】编译器通过函数的声明语句得知，代码第6行，函数print（）的形式参数是int型，但用double型实参m为其赋值时，编译器会对m进行自动类型转换（隐式转换），这在一定程度上简化了程序的书写。

注意

除了编译器提供的自动类型转换以外，还可以采用显式强制转换的方式对参数进行转换。

6.4.3　值传递

很多C++教科书在讲述函数值传递调用时都会举下面这个例子，见代码6.6。

代码6.6　传值调用CallByValue

＜--------------------------文件名：example606.cpp----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　void change（int,int）；//函数声明

06　int x=2，y=3；

07　cout＜＜"交换前：x="＜＜x＜＜"，y="＜＜y＜＜endl；

08　change（x,y）；//传值调用

09　cout＜＜"交换后：x="＜＜x＜＜"，y="＜＜y＜＜endl；

10　return 0；

11　}

12　void change（int n,int m）//函数定义

13　{

14　int temp；

15　temp=n；

16　n=m；

17　m=temp；

18　}

输出结果如下所示。

交换前：x=2，y=3

交换后：x=2，y=3

【代码解析】函数change（）并没有像我们预想的那样将x和y的值进行调换，这是为什么呢？这是因为代码第8行的change（）函数采用的是传值调用，只有在调用change（）函数时，编译器才会为其中的形式参数m和n以及变量temp分配内存区域，并用实参x和y对形参n和m赋值，change（）函数中进行的数值交换是对n和m进行的操作，并不会影响main（）函数中的x和y。而且，m、n及temp在change（）函数执行完毕后便会被撤销，所占的内存区域被收回。调用函数和被调用函数发生的唯一数据关联就是由实参向形参赋值，此后，被调函数中的变量（含形参）与调用函数中的变量便各有各的内存空间，互不干涉，即使变量名相同，也会被编译器认为是不同的变量。

换句话说，被调函数中执行的任何操作都只作用在被调函数内的变量上，而不作用在调用函数的变量上，通过传值调用时被调函数无法改变调用函数中的变量值。

那有没有可能被调函数改变了调用函数中的变量值呢？答案是肯定的，请看随后的内容。

6.4.4　指针传递

要想被调函数改变调用函数中的变量值，应使函数中的操作直接作用在调用函数的变量上，要达到此目的，一个有效的途径就是使用指针传递，见代码6.7。

代码6.7　指针传递CallByPointer

＜--------------------------文件名：example607.cpp----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　void change（int*，int*）；//change（）函数声明，形参为指针

06　int x=2，y=3；

07　cout＜＜"交换前：x="＜＜x＜＜"，y="＜＜y＜＜endl；

08　change（＆x，＆y）；//change（）函数调用，传递的是地址

09　cout＜＜"交换后：x="＜＜x＜＜"，y="＜＜y＜＜endl；

10　return 0；

11　}

12　void change（int*n,int*m）//change（）函数定义

13　{

14　int temp；

15　temp=*n；

16　*n=*m；

17　*m=temp；

18　}

输出结果如下所示。

交换前：x=2，y=3

交换后：x=3，y=2

【代码解析】代码第8行中的change（）函数成功地实现了x和y之间的数据交换，将代码6.7与代码6.6比较可以发现，函数change（）的形参是两个int型的指针，将调用函数（此处为main（）函数）中的变量地址作为实参，赋值给形参，完成对调用函数中变量的处理。

在调用change（）函数时，int型变量temp和形参（指针变量n和m）被创建，并用调用函数中的变量x和y的地址为n和m赋值，这时，间接引用*n和*m实际上等价于main（）函数中的x和y，对*n和*m的操作实质上是对x和y的操作。这样，虽然在函数change（）执行完毕后，变量temp和int型指针n和m都会被撤销，对应的内存区域被收回，但在函数执行过程中已经通过指针间接引用并改变了调用函数（此处为main（）函数）中的变量。

6.4.5　引用传递

在第4章中已经提及，对变量的引用相当于变量的别名，以“int＆m=n；”为例，对m的操作实质上就是对n的操作，例如湖南省的简称是湘，湘就可以被认为是湖南省的引用，新闻标题上说“湘发生……”实质上等价于“湖南省发生……”。所以说，m既不是n的副本，也不是指向n的指针，m就是n本身。

C++允许函数通过引用实现参数传递。在调用函数时，不会再为形参分配内存空间，因为此时的形参就是实参本身，在函数中对形参进行的任何操作，本质上都是对调用函数中的实参进行的操作，见代码6.8。

代码6.8　引用传递CallByRef

＜---------------------------文件名：example608.cpp---------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　void change（int＆，int＆）；//change（）函数声明，传递的是引用

06　int x=2，y=3；

07　cout＜＜"变量x的地址是："＜＜＆x＜＜endl；

08　cout＜＜"交换前：x="＜＜x＜＜"，y="＜＜y＜＜endl；

09　change（x,y）；//change（）函数调用

10　cout＜＜"交换后：x="＜＜x＜＜"，y="＜＜y＜＜endl；

11　return 0；

12　}

13　void change（int＆n,int＆m）//change（）函数定义

14　{

15　int temp；

16　temp=n；

17　n=m；

18　m=temp；

19　cout＜＜"形参n的地址是："＜＜＆n＜＜endl；

20　}

输出结果如下所示。

变量x的地址是：0013FF7C

交换前：x=2，y=3

形参n的地址是：0013FF7C

交换后：x=3，y=2

【代码解析】代码第9行，通过change（）函数的引用传递实现了x和y数值的互换，从输出的实参x和形参n的地址可以看出，两者对应同一片内存区域，是一回事。这说明在调用函数change（）时，并没有给形参m和n分配内存空间，而直接是对实参进行操作的。

不论是传递参数还是作其他用，如果仅仅需要使用某个程序实体的“别名”，推荐用引用，而不是指针，虽然引用能做的事，指针完全也可以做到，但是指针的功能太过强大，能毫无约束地对内存进行任何操作，这是十分危险的。引用的使用虽有很多的限制，但这也大大降低了程序出错的可能性。如果把指针比作一把刀，这把刀既能砍瓜切菜，又能理发剪指甲，若不怕危险，或许还能剔牙，这种万能刀用起来是会经常出问题的，而引用可能就是一把指甲剪，不能拿来切菜，但指甲剪是可以放心使用的，不会带来这样或那样的危险。

注意

用适当的工具做恰如其分的事。

6.4.6　对3种传递的补充

3种传递方式不只是参数传递，函数返回某个值时也有值传递、指针传递和引用传递3种方式，代码6.9是使用范例。

代码6.9　函数返回某个值时的3种传递方式ReturnMethods

＜--------------------------文件名：example609.cpp----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　int add（int,int）；//add（）函数声明

06　int*minus（int,int）；//minus（）函数声明

07　long＆multiply（int,int）；//multiply（）函数声明

08　int x=2，y=3；

09　int addRes=add（x,y）；//add（）函数调用，传值返回

10　int*minusRes=minus（x,y）；//minus（）函数调用，传指针返回

11　long*mul=＆multiply（x,y）；//multiply（）函数调用，传引用返回

12　cout＜＜"2+3="＜＜addRes＜＜endl；

13　cout＜＜"2-3="＜＜（*minusRes）＜＜endl；

14　cout＜＜"2*3="＜＜*mul＜＜endl；

15　delete minusRes；//释放堆内存

16　delete mul；//释放堆内存

17　return 0；

18

19　}

20　int add（int m,int n）//add（）函数定义，返回值

21　{

22　int z=m+n；

23　return z；

24　}

25　int*minus（int m,int n）//minus（）函数定义，返回指针

26　{

27　int*z=new int；//动态堆内存申请

28　*z=m-n；

29　return z；

30　}

31　long＆multiply（int m,int n）//multiply（）函数定义，返回引用

32　{

33　long*z=new long；//动态堆内存申请

34　*z=m*n；

35　return*z；

36　}

输出结果如下所示。

2+3=5

2-3=-1

2*3=6

【代码解析】代码中的3个函数add（）、minus（）和multiply（）分别引用了值传递返回、指针传递返回和引用传递返回，为了便于说明问题，在参数传递上都采用了值传递这种方式。

对于值传递和指针传递来说，就如图6.2所示，被调用函数将返回的值或指针存储在CPU寄存器或某块内存区域中，然后调用函数访问这块内存区域，进行下一步的处理。根据前面的介绍，知道在函数内创建的变量（包括形参）在函数执行完毕后会被撤销，这些变量对应的内存区域会被收回，此时若返回指向函数中变量的指针是错误的，因为在函数执行完毕后，该内存将变成不可用的垃圾内存。换言之，如果将minus（）函数定义如下。

int*minus（int m,int n）

{

int k=0；

int*z=＆k；

*z=m-n；

return z；

}

程序可能会崩溃，函数minus（）执行完毕后，指针z和函数中的变量被撤销，虽然指针z的值（对应内存中的某个地址）已经存储在CPU寄存器或某块内存中，但指针所指的这块内存地址已经被释放掉，访问已经释放的内存会给程序带来致命的问题。

代码第27和33行，使用动态申请内存能有效解决这一问题，在函数执行完毕后，在函数中创建的变量（包括形参）占用的内存（栈内存）会被释放，但动态申请的堆内存不会被释放，关于栈内存和堆内存的内容请参考本章稍后的介绍。

引用传递同样涉及这一问题，同样可以通过动态申请内存来解决。在本例中，似乎运用指针传递和引用传递并没有带来多大的好处，但如果要传递的是很大的结构或对象，指针传递或引用传递能节省内存的消耗，提高程序的编译效率。

6.4.7　默认参数调用

默认参数调用的基本思想是在声明语句中预先初始化一些参数的值，在调用语句中相应的参数可以默认，请看代码6.10的示例。

代码6.10　默认参数调用DefaultParameters

＜--------------------------文件名：example610.cpp----------------------------＞

01　#include＜iostream＞

02　#include＜iomanip＞

03　#include＜string＞

04　using namespace std；

05　int main（）

06　{

07　void ShowString（char*，int n=2，int m=5）；//原型声明时指定默认值

08　ShowString（"I Love C++"）；//默认两个参数调用

09　cout＜＜"------------------------"＜＜endl；

10　ShowString（"Hello"，1）；//默认最后一个参数

11　cout＜＜"------------------------"＜＜endl；

12　ShowString（"World"，3，2）；//全部参数都设置

13　return 0；

14　}

15　void ShowString（char*ptext,int n,int m）

16　{//函数定义，输出n遍字符串，每遍缩进m个字符

17　int len=strlen（ptext）；

18　for（int i=0；i＜n；i++）

19　{

20　cout＜＜setw（len）＜＜ptext＜＜endl；

21　len=len+m；

22　}

23　}

输出结果如下所示。

I Love C++

I Love C++

------------------------

Hello

------------------------

World

World

World

【代码解析】定义了ShowString（）函数用于字符串的输出，该函数有3个参数，分别是字符串ptext（采用指针传递），输出次数n（int型，值传递），每行缩进字数m（int型，值传递），ShowString（）函数用于将字符串ptext输出n遍，每遍首字母依次缩进m个空格。

代码第7行ShowString（）函数声明采用了默认参数调用方式：“void ShowString（char*，int n=2，int m=5）；”，这意味着在函数调用时，可以仅指定第一个参数，后两个参数可以省略，此时，默认将字符串输出两遍，每行首字母缩进5个字符。当然在程序中，也可以指定参数m和n的值，此时参数的默认值不再起作用。

关于默认参数调用要遵守下面4条规则。

（1）参数默认必须按从后向前的顺序，下列函数声明是不合法的。

void ShowString（char*，int n=2，int）；

换句话说，在函数声明时，如果对第n个参数进行初始化，那么其后面所有的参数都应被进行初始化。所以在函数定义时，应合理安排形参列表顺序。

（2）和函数声明一样，在函数调用时省略某个参数，必须省略其后所有后续参数，在代码6.10中，这样的函数调用是不合法的。

ShowString（"World"，2）；

（3）除非必要，否则请不要在函数内（尤其是函数的第一行）对参数进行初始化，否则，函数调用时实参向形参的传递就没有了意义。

（4）在函数调用时，也可对实参变量初始化，但此时不涉及默认参数调用的问题。实际上，每个实参都可以看做一个表达式，首先计算参数表达式的值，并将此值传递给形参，下式中的“int i=3；”的值是3，等价于“ShowString（"World"，3，2）；”。

ShowString（"World"，int i=3，2）；

6.4.8　inline函数

函数的引入可以减少程序的代码量，使得函数程序可以在代码间共享。但函数调用需要一些时空开销。在调用函数时，先要中断调用函数，将执行流程转移到被调函数中，待被调函数执行完毕后，返回调用函数。因此，在调用函数前，应保护被调函数现场，记录程序当前位置，以方便从被调函数中返回，恢复现场，并从原来的位置处继续执行。

对于较长的函数，这种开销可以忽略不计，但对于一些很短的、频繁调用的函数体，这种开销就不能忽视。举例来说，代码6.3中的print（）函数定义如下所示。

void print（int x）

{

cout＜＜x＜＜endl；

}

可能有的读者会问：“定义这么短的函数有意义吗？它有哪些好处呢？”

具体来说，使用函数可以提高程序的可读性，方便阅读和修改。另外，函数程序可方便地用于共享，可以重复使用，但也如前面所述，当这种简短函数被频繁地调用时，调用函数的开销会降低应用程序的性能和执行效率。

inline函数的提出正是为了解决这个问题，提高程序的运行效率。

定义inline函数的方法很简单，只要在普通的函数定义前加修饰符inline即可，如下所示。

inline void print（int x）

{

cout＜＜x＜＜endl；

}

在编译时，编译器会将程序中的inline函数都用其函数体来代替，执行的计算完全相同，虽然目标程序的代码量会增加，但节省了非内联函数调用时的栈内存的创建和释放开销，这是一种以空间（文件大小）来换时间（提高运行效率和性能）的方法。

关于inline函数有以下4点注意事项。

（1）在一个文件中定义的inline函数不能在另一个文件中使用。

（2）inline函数应简洁，只用几个语句，如果语句较多，不适合于定义为inline函数。

（3）inline函数体中，不能有循环语句、if语句或switch语句，否则，即使函数定义时使用了inline关键字，编译器也会将其当成普通函数来处理。

（4）inline函数应在调用和声明前进行定义，否则，有的编译器会将其当成普通函数，不能起到预期的效果，无法提高程序的运行效率，而有的编译器可能会报错（如VC6），见代码6.11。

代码6.11　inline函数的引用InlineFunction

＜-------------------------文件名：example611.cpp-----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　inline void print（int n）//inline函数print（）定义

04　{

05　cout＜＜n＜＜endl；

06　cout＜＜"n的地址是："＜＜＆n＜＜endl；

07　}

08　int main（）

09　{

10　void print（int）；//函数声明

11　int m=6；

12　cout＜＜"m的地址是："＜＜＆m＜＜endl；

13　print（m）；//函数调用

14　return 0；

15　}

输出结果如下所示。

m的地址是：0013FF7C

6

n的地址是：0013FF2C

【代码解析】在代码第3行，如果inline函数print（）定义在main（）函数之后，VC6编译器会给出错误信息，提示无法找到print（）函数。

6.5　递归

除了main（）函数外，C++函数可以调用自身，这称为递归，是一种通用的编程技术，可以有效降低问题的复杂度，为解决某些问题提供了极大的方便。首先来看一个用以求阶乘的函数，对于一个整数n，其阶乘n！定义为如下所示。

n！=n*（n-1）*（n-2）*……*3*2*10的阶乘为1，读者可以从阶乘定义中总结出以下规律，对n＞0而言有如下递归。

n！=n*（n-1）！直观上，可以用循环语句计算n的阶乘，如下列代码所示。

int Calc（int n）

{

int res=1；

for（int i=1；i＜=n；i++）

res*=I；

return res；

}

但是，使用递归可以让函数代码更为清晰，代码6.12使用递归构造Calc（）函数，用以计算用户输入的整数的阶乘。

代码6.12　计算某个整数的阶乘Factorial

＜--------------------------文件名：example612.cpp----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　int Calc（int）；//Calc（）函数声明

06　int x；

07　cout＜＜"请输入一个大于1的整数："＜＜endl；

08　cin＞＞x；//接收用户键盘输入

09　int y=Calc（x）；//函数调用，递归计算

10　cout＜＜x＜＜"的阶乘是"＜＜y＜＜endl；

11　return 0；

12　}

13　int Calc（int n）//定义计算阶乘的函数Calc（）

14　{

15　if（n==1）//如果输入参数n为1，返回1

16　return 1；

17　else

18　return n*Calc（n-1）；//否则，递归返回n*Calc（n-1）

19　}

输出结果如下所示。

请输入一个大于1的整数：

4（注：键盘输入）

4的阶乘是24

【代码解析】Calc（）函数用于计算参数n的阶乘，当n大于1，函数的返回值是“n*Calc（n-1）；”，在代码第18行，Calc（）函数又一次被调用（递归Calc（n-1）），参数为n-1，当n-1大于1时，Calc（n-1）返回“（n-1）*Calc（n-2）”，Calc（n）此时返回值是n*（n-1）*Calc（n-2），如此反复，一直到Calc函数的参数为1，返回值为1，Calc（n）返回n*（n-1）*……*3*2*1，函数递归的调用过程如图6.3所示。

每次调用Calc（）函数，都会为其中的变量（包括形参）开辟栈内存空间，关于函数递归有以下3点需要注意。

（1）一定要有递归调用终止的条件，且每次调用都向调用终止靠近一步，这能保证递归调用正常结束，不至于出现无限循环，导致系统内存耗尽而崩溃。通常用参数对调用过程进行判断，以便在合适的时候切断调用链，如代码6.12中的“if（n==1）{……}”结构。

（2）每次调用函数时，都有一定的时空开销，因此，递归函数的效率总比功能相同的循环结构略低，任何用递归编写的函数都可用循环代替，以提高效率。但是，递归带来的好处也是显而易见的：一是程序的可读性比较好，易于修改和维护，二是在函数不断的调用中，函数的规模在减小，在求解阶乘的Calc（）函数例子中，不断用复杂度为n-1的问题来描述复杂度为n的问题，直到问题可以直接求解为止（参数为1）。

（3）递归函数不能定义为inline函数。

递归是一种通用的程序设计技术，当一个问题蕴涵递归形式，且关系比较复杂时，采用递归算法往往会自然、简洁，更容易理解（虽然从某种程序上来说会降低效率）。

6.6　函数的重载

自然语言中，一个词可以有不同的含义，即该词被重载了，人们可以根据上下文的意思判断出该词的意义。在C++程序中，可以将语义及功能相似的几个函数用同一个名字来表示，这样便于记忆，提高了函数的易用性，称为函数重载。函数重载的示例见代码6.13。

图　6.3　函数递归过程图解

代码6.13　函数重载范例FunctionOverload1

＜--------------------------文件名：example613.cpp----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

01　{

02　void print（int）；//函数声明

03　void print（char*）；//函数声明

04　int m=1；

05　print（m）；//函数调用，根据参数自动选择

06　char str[]="Hello"；

07　print（str）；//函数调用，根据参数自动选择

08　return 0；

09　}

10　void print（int n）//print函数定义，参数为int型

11　{

12　cout＜＜"整数："＜＜n＜＜endl；

13　}

14　void print（char*p）//print函数重载版本，参数为字符串

15　{

16　cout＜＜"字符串："＜＜p＜＜endl；

17　}

输出结果如下所示。

整数：1

字符串：Hello

【代码解析】代码第10和第14行，分别定义了两个函数，函数名同为print，但函数的形参有所不同，一个是整型变量，另一个是字符指针，在main（）函数对两个print函数进行调用时，编译器根据对print（）函数传递的形参决定具体应调用哪个函数。

6.6.1　何时使用函数重载

将不相关的函数都用同样的名字来命名不是个明智的选择，会降低程序的可读性，使程序难以理解，因此，函数重载必须用在合适的场合。

首先，函数名应相同，最重要的一点是函数要完成的任务一样（至少是类似）。比如，都用来输出信息；或都用来和某个硬件打交道等，要避免函数名字相同，但功能完全不同的情形。

其次，形式参数的类型应不同，对于形参类型相同，只有形参个数不同的场合，就无须定义两个函数，采用前面提及的默认参数调用机制，只要定义一个函数即可。

但如果形参类型不同，默认参数调用便不再适用，此时，应使用函数重载。

6.6.2　如何实现函数重载

当调用多个同名的重载函数时，要求能够唯一确定应执行哪一个函数，这是通过函数参数的个数和类型来区分的。所以，重载的函数要求参数个数或者参数类型上不同，否则会出现编译错误。

注意

C++中不允许几个函数名相同、形参个数和类型也相同，仅仅是返回值不同的情形，否则，程序编译时会出现函数重复定义的错误，此时，编译器无法根据返回值的不同决定具体要调用的函数。

6.6.3　陷阱：隐式转换导致重载函数出现二义性

调用重载的函数时，如果实参类型与形参类型不匹配，编译器会对实参自动进行类型转换。如果转换后仍然不能匹配到重载的函数，则会产生一个编译错误，见代码6.14。

代码6.14　无法编译通过的函数重载FunctionOverload2

＜-------------------------文件名：example614.cpp-----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　void print（int）；//函数声明

06　void print（float）；//函数声明

07　print（0. 5）；

08　//print（int（0. 5））；正确，显式样转换

09　//print（float（0. 5））；正确，显式样转换

10　return 0；

11　}

12　void print（int n）//print函数定义，参数为int型

13　{

14　cout＜＜"整数："＜＜n＜＜endl；

15　}

16　void print（float p）//print函数定义，参数为float型

17　{

18　cout＜＜"浮点数："＜＜p＜＜endl；

19　}

VC6给出的错误提示如下所示。

error C2668：'print'：ambiguous call to overloaded function

【代码解析】代码第12行，print（）函数的参数是int型，代码第16行print（）函数的参数是float型。由于数字本身并没有类型，所以在执行到代码第7行“print（0.5）；”一句时，将数字0.5当做函数参数时，编译器会对其自动进行类型转换（称做隐式类型转换），但对此例来说，编译器无法决定将0.5转换成int型（0）还是long型（0.5），调用产生歧义，编译器无法决定执行哪个print函数。

隐式转换虽然能简化程序的书写，但有时也会带来意想不到的问题，推荐对参数采用显式强制转换的方式，避免歧义的产生，对代码6.14而言，将“print（0.5）；”一句更改为如下所示。

print（int（0.5））；

print（float（0.5））；编译正确通过，

输出结果如下所示。

整数：0

浮点数：0.5

6.7　C++如何使用内存

C++有3种管理数据内存的方式即自动存储（栈存储）、静态存储和动态存储（堆存储）。在不同的方式下，内存的分配形式和存在时间的长短都不同，其中，动态存储方式已经在第4章中进行了介绍，下面对自动存储和静态存储进行说明。

6.7.1　自动存储（栈存储）

对于函数的形参、内部声明的变量及结构变量等，编译器将在函数执行时为形参自动分配存储空间，在执行到变量和结构变量等的声明语句时为其自动分配存储空间，因此称其为自动变量（Automatic Variable），有的教科书也称其为局部变量，在函数执行完毕返回时，这些变量将被撤销，对应的内存空间将被释放。

事实上，自动变量的生存期只局限于它所在的代码块。所谓代码块，是包含在花括号对中的一段代码，函数只是代码块的一种，比较下面两段代码。

代码段1

int add（int m,int n）

{//z的生存期包括整个函数

int z=m+n；

return z；

}代码段2

int add（int m,int n）

{

if（m！=0）

{

//z的生存期包括在这个代码块中

int z=m+n；

}

return z；//错误

}

在代码段1中，当函数返回时，变量z被撤销，对应内存空间被释放，但在代码段2中，在if代码块中声明的变量z，其生存期仅限于if结构的两个花括号之间，当程序执行到if结构的后花括号时，变量z已被撤销，其对应的内存空间被释放，此时，再执行“return z；”语句便会出错。

注意

理解代码块的含义十分重要，花括号不是判断代码块的唯一标准，把代码段2中if结构的花括号去掉，代码仍然是错误的，将“return z；”放在if结构中是正确的用法。

自动变量的生存期是局部的，这一特性使得程序员可以在不同的块内使用相同的变量名，用不着为使用不同的变量名绞尽脑汁。

1.什么是“栈”

栈（Stack）是一块存储区，而且是C++程序使用最频繁的存储区，其存储机理为“FILO”，即先进后出（First In,Last Out）。可以将其想象成一个装盘子的桶，最早放入的盘子在桶的底部，最晚放入的盘子在最顶部，取盘子时必须先从后放进去的盘子开始取，这就是所谓的先进后出原则。当一个代码块（包括函数，视为一种特殊的代码块）声明一个自动变量时，系统便为其在栈中开辟内存空间（常称“压入”push），该代码块结束后便将自动变量撤销，释放内存空间（常称“弹出”pop）。

注意

采用“栈”这种机制，C++程序能有效地节省所用内存空间。

2.auto关键字

auto是C++提供的存储类声明符，用于声明自动变量，除了auto声明符外，C++还提供了另外3个存储类声明符，分别是register（寄存器存储）、extern（外部存储）和static（静态存储）。在声明创建变量时，存储类声明符应放在数据类型声明符之前，如下所示。

存储类声明符数据类型变量名[=初始化表达式]其中，初始化表达式是可选的，如下列代码声明创建了int型自动变量A，其只在函数demo（）执行期间存在，demo（）函数执行完毕后，变量A被撤销，对应内存被释放。

void demo（）

{

……

auto int A；

……

}

细心的读者可能会发现，在前面给出的示例代码在声明自动变量时并没有加auto修饰符，实际上，auto常常可以默认，凡是在函数内部（不论是main（）函数还是其他函数）的，没有用其他显式的存储类型声明符，编译器都认为是auto型自动变量。

3.register关键字

除了auto外，还可以通过存储类声明符register来声明自动变量，与auto唯一的不同在于：关键字register通知编译器，用户希望通过CPU寄存器，而不是“栈”来处理某个变量，从而可以在一定程度上加快该变量的访问速度。

提示　一般来说，CPU对寄存器的访问要快过对内存的访问。

用register声明的变量常称为寄存器变量，举例来说，下列代码声明了int型寄存器变量sum，并将其初始化为9，如下所示。

register int sum=9；

需要注意的是，即使用register声明了某个变量，编译器也不一定会满足它的要求。因为，CPU寄存器可能被占用或者无法存储指定类型的数据等，而且，现在的编译器一般可以自动决定应把哪些变量放在CPU寄存器中，因此，在C++程序中，register关键字很少使用。

使用register关键字会带来一定的负面效果，不管是否能满足要求，编译器认为register型自动变量是存储在CPU寄存器中的，而寄存器是没有内存地址的，所以，不能对register型自动变量进行取地址操作，下列代码是错误的。

void demo（）

{

……

register int sum=0；

int*pSum=＆sum；

……

}

注意

事实上，用auto和register声明的变量除了存储位置不同（一个是“栈”，而另一个可能是“栈”也可能是CPU寄存器）外，并无其他差异，我们可以将其统称为自动变量来考虑。

4.自动变量的初始化

可以在声明自动变量时对其进行初始化，也可以使用任何具有确定值的表达式为自动变量赋值，下列语句都是合法的（假定n为int型自动变量）。

n=2；

n=5*m；//m的值确定

n=add（4，6）；

需要特别注意的是，如果没有在自动变量声明的同时对其初始化，其初始值是随机、不可预料的，为避免随机的初始值给程序带来麻烦，推荐在声明自动变量的同时对其显式初始化。

6.7.2　静态存储（编译器预分配）

每个C++程序对应着一块静态数据区（也称全局数据区）。在源程序编译时，编译器就为某些程序实体（某些变量及所有的常量）预分配存储地址和内存空间，程序一开始执行，这些程序实体就被创建，一直到程序结束才被撤销，并释放对应的内存空间，因此称其为“永久存储”。静态存储的程序实体的数目在程序运行过程中是不变的，因此无须使用特殊的机制（如堆、栈）来管理它们，编译器将分配固定的内存块来存储所有的静态存储实体。

常量已经在第2章进行了详细的介绍，下面对静态存储的变量进行讨论，根据用法不同，静态存储的变量可分为全局变量和静态变量。

1.extern关键字

前面已经提及，extern是C++提供的存储类声明符，用extern声明的变量称为全局变量。从字面上看，这意味着该变量可以在程序的任意位置使用，全局变量是在函数和类外定义的，所以也称为外部变量。

与自动变量不同的是，全局变量的声明有两种形式，即定义性声明和引用性声明。定义性声明用于创建变量，基本形式如下所示。

extern类型变量名=初始化表达式；此时，初始化表达式不可省略，此指令通知编译器在静态数据区中开辟一块指定类型大小的内存区域，用于存储该变量，下列语句创建了一个初始值为10的double型全局变量total。

extern double total=10；存储类extern说明符也可以省略，只要是在外部（不属于任何一个函数或类）定义的变量，编译器就将其当做全局变量。在外部定义时上述代码等价于下面的代码。

double total=10；

注意

只有当在定义性声明中省略了extern时，初始化表达式才可省略，系统默认将其初始化为0，对于定义的全局数组或结构，编译器将其中的每个元素或成员的所有位都初始化为0。

在使用全局变量之前，需要对其进行引用性声明，这和函数的用法有一点类似，引用性声明只有一种格式，如下所示。

extern类型变量名；

请注意，这里没有初始化语句，引用性声明用于告诉编译器变量是全局的这一特征，引用性声明不只局限在外部，只要在使用全局变量之前对其进行声明即可。

注意

同函数一样，如果定义在前，而使用在后，且定义和使用在同一文件（注意程序和文件的差别）中，引用性声明可以省略。

定义性声明和引用性声明主要有以下3点区别。

❑定义性声明只能有一次，而引用性声明可以有多次。

❑定义性声明一定是在外部，而引用性声明的位置没有限制，只要在使用前对全局变量声明使其可见即可，关于可见性的内容，请参考本章稍后的介绍。

❑定义性声明有两种形式：带extern的形式（不可缺少初始化语句）和省略extern的形式（可省略初始化语句，编译器默认初始化），而引用性声明只有带extern但不带初始化表达式一种形式。

代码6.15演示了全局变量的定义性声明和引用性声明。

代码6.15　全局变量的定义性声明和引用性声明GlobalVariable

＜-------------------------文件名：example615.cpp-----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　extern int num1；//引用性声明

06　cout＜＜"num1："＜＜num1＜＜endl；

07　void change（）；//函数声明

08　change（）；//函数使用

09　cout＜＜"num1："＜＜num1＜＜endl；

10　return 0；

11　}

12　int num1；//定义性声明

＜-------------------------文件名：func.cpp----------------------------------＞

13　extern int num1；//引用性声明

14　void change（）//函数定义

15　{

16　num1+=3；

17　}

输出结果如下所示。

num1：0

num1：3

【代码解析】代码由两个程序文件组成，代码第12行，定义全局变量num1，其放在example615.cpp中，func.cpp中函数change（）对num1进行了加3处理，在使用num1前，func.cpp的第一句便外部引用声明了全局变量num1，这样，便可以在func.cpp内的任何位置使用num1。

num1的定义性声明省略了extern和初始化语句，编译器自动将其初始化为0，在main（）函数内，访问num1之前同样要对其进行引用声明，可以看出，没有传递任何参数，change（）函数便实现了对num1的修改和访问，这也提供了一种在程序文件间进行数据通信的途径。注意，对全局变量引用声明的时候，不能给初值。例如，在代码6.15中，如果将func.cpp中的引用性声明改为如下所示：

extern int num1=10；

这便不再是引用性声明语句，而成了int型全局变量的定义，编译器会为其在静态数据区分配内存。实际上，example615.cpp中已经定义了全局变量num1，编译器会给出变量重复定义的错误。

注意

首先，全局变量虽然方便了数据的共享，但其破坏了程序的封装型，使程序读起来比较困难。其次，全局变量占据的空间无法释放，一定程度上浪费了内存资源。最后，滥用全局变量容易造成名字冲突和程序错误。因此，应尽量少用全局变量。

2.static关键字

static也是C++提供的存储类声明符，使用static声明的变量称为静态变量，同全局变量一样，静态变量也是永久存储的。

静态变量与全局变量的区别在于以下两点。

❑没有定义性声明和引用性声明之分，只有声明语句。静态变量应在使用前进行声明，编译器会根据声明语句，在静态数据区为静态变量分配内存空间。

❑既可以在外部声明，又可以在内部声明。当在外部声明时（外部静态变量），所声明的标识符只限于一个文件中的函数共享；当在内部声明时（内部静态变量），所声明的标识符仅限于所在的代码块内使用，内部静态变量与自动变量的唯一区别在于内部静态变量具有永久生存期。

静态变量声明的基本形式如下所示。

static类型变量名=[初始化表达式]；

注意

初始化表达式可以省略，此时系统默认将其初始化为0；对于定义的全局数组或结构，编译器将其中的每个元素或成员的所有位都初始化为0。

先来看一个例子，见代码6.16。

代码6.16　静态变量的应用StaticVariable1

＜--------------------------文件名：example616.cpp----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　static int num1=12；//声明

04　int main（）

05　{

06　cout＜＜"num1："＜＜num1＜＜endl；

07　void change（）；//函数声明

08　change（）；//函数使用

09　cout＜＜"num1："＜＜num1＜＜endl；

10　return 0；

11　}

＜---------------------------文件名：hs.cpp-----------------------------------＞

12　#include＜iostream＞

13　using namespace std；

14　void change（）//函数定义

15　{

16　static int num1；//声明

17　num1+=3；

18　cout＜＜"num1："＜＜num1＜＜endl；

19　}

输出结果如下所示。

num1：12

num1：3

num1：12

【代码解析】在example616.cpp第3行，声明的静态变量num1的作用范围仅限于本文件，代码第16行，即hs.cpp中的“static int num1；”相当于声明了另一个静态变量num1，其作用范围仅限于change函数内。两个静态变量都位于静态存储区，编译器通过绝对地址的不同对其加以区分。

试试看将change函数定义在main（）函数后，编译输出会是什么结果呢？

可以发现，将change（）函数定义在main（）函数后，输出结果并没有发生变化。这是因为change（）函数声明的内部静态变量屏蔽了外部静态变量，这是由变量的可见性决定的，在稍后的章节中将介绍可见性的相关内容。

对内部静态变量而言，在函数两次调用之间，静态变量的值是能保持的（保存的），代码6.17是内部静态变量的使用范例。

代码6.17　内部静态变量的应用StaticVariable2

＜-----------------------文件名：example617.cpp-------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　int num1=5；

06　void add（int）；

07　add（num1）；

08　add（num1）；

09　add（num1）；

10　return 0；

11　}

12　void add（int n）

13　{

14　static int m=1；//add（）函数内部定义的静态变量m，在函数两次调用的间隔保持其值

15　m+=n；

16　cout＜＜"m："＜＜m＜＜endl；

17　}

输出结果如下所示。

m：6

m：11

m：16

【代码解析】在代码第14行，用static修饰的内部变量m在函数add（）调用的间隔里保持着数据，换句话说，对静态变量的初始化在程序刚开始执行的时候已然完成，程序中对该变量的写入和访问都会一直存储在静态存储区中。

静态变量不像全局变量那样有定义性声明和应用性声明之分，凡是声明语句都将在静态数据区为其分配空间。因此，应避免同一可见域的重复声明。

注意

对于外部静态变量，推荐声明在文件前面（#include语句后），而对内部静态变量，推荐声明在代码块的最前面。

静态变量的名字不影响其他文件中的同名变量，其提供了隐藏程序实体的一种手段，程序员在设计不同的文件时，不必担心它会和其他文件中的标识符发生冲突。

3.全局变量和静态变量的初始化

对于全局变量和静态变量，允许在其创建时对它进行显式初始化，系统默认将其初始化为0。对于定义的全局数组或结构，编译器将其中的每个元素或成员的所有位都初始化为0。

需要注意的是，只能使用常量表达式来初始化全局变量和静态变量。常量表达式包括第2章中介绍的直接常量、const常量、枚举常量和sizeof（）运算符。下面的初始化代码都是合法的。

int num；//编译器自动将num初始化为0

int num1=20；//直接常量

const int x=10；

int num2=x；//const常量

int num3=sizeof（double）；//sizeof运算符

不能使用变量来初始化全局变量和静态变量，因为全局变量和静态变量的内存空间是在程序刚开始执行就开辟的，初始化也是在这时完成的，此时，变量的值是未知的，变量的内存空间甚至还没有被分配。

试试看能否用一个全局变量（或静态变量）对另一个全局变量（或静态变量）赋值呢？

C++允许全局变量和静态变量间的相互赋值，前提是该变量可见。

6.8　作用域与可见域

在函数一节，讨论形参变量时曾经提过形参变量只在被调用期间才分配内存单元，调用结束则立即释放。这表明形参变量只存活在函数内，只有在函数内才有效，只有在函数内才能使用该变量。程序流程离开该函数后，该变量便不复存在、不再有效、不能再使用了。这3条规则分别对应着变量的生存期、作用域和可见域。

通俗地讲，生存期指的是在程序运行过程中，变量从创建到撤销的一段时间。生存期的长短取决于前面所讲的存储方式，对于自动分配（栈分配），变量与其所在的代码块共存亡；对于静态分配（编译器预分配），变量与程序共存亡，程序开始执行时即已存在，一直到程序运行完毕退出后才撤销。对于动态存储的内存块（不是指向该内存块的指针），由程序员决定其生存期。

在程序代码中，变量有效的范围（源程序区域）称为作用域，能对变量、标识符进行合法的访问的范围（源代码区域）称为可见域。变量有效的前提是变量存在于内存中，实际上，变量的作用域和生存期一样，都取决于存储方式，可见域是作用域的子集。

6.8.1　作用域

严格地说，作用域是程序正文代码（不包括注释等）中的一片区域，在这块区域内，标识符理论上（排除下面要介绍的屏蔽情况）指向同一内存单元，可以将C++作用域分为以下4类。

1.块作用域

自动变量（auto、register）和内部静态变量（static）具有块作用域，在一个块内声明的变量，其作用域从声明点开始，到该块结束为止。函数定义中声明的形参，其作用域限定在该函数体内，与其他函数中声明的同名变量不是一回事，允许在不同的函数中使用相同的变量名，编译器将为这些变量分配不同的存储单元，不会混淆。

注意

强调goto语句，在本函数内给出的标号，不管在任何位置，都可使用goto语句将程序流程转到标号处，但是C++不允许使用goto语句将流程转到别的函数中。

2.文件作用域

外部静态变量（static）具有文件作用域，从声明点开始到文件末尾，此处所指的文件是编译基本单位—cpp文件。

3.全局（程序）作用域

全局变量（extern）具有全局作用域，只要在使用前对其进行声明，便可在程序（由若干个文件组成）的任意位置使用全局变量。

4.类作用域

这将在介绍了类的概念后详细介绍。

6.8.2　可见域

可见域是作用域的子集，我们可以这样理解，作用域指的是变量理论上有效的源代码区域，而可见域指的是该变量实际有效的内存区域，如果没有屏蔽发生，可见域和作用域应该是等价的。

1.屏蔽

先来看下面一个例子，见代码6.18。

代码6.18　内层标识符屏蔽外层同名标识符IDShield

＜----------------------文件名：example618.cpp--------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　extern int A,B，C；//引用性声明，外部int变量A、B和C

04　int main（）

05　{

06　cout＜＜"A："＜＜A＜＜"，B："＜＜B＜＜"，C："＜＜C＜＜endl；//此时，访问的是外部全局变量

07　int B=1；//内部变量B屏蔽掉外部变量B

08　cout＜＜"A："＜＜A＜＜"，B："＜＜B＜＜"，C："＜＜C＜＜endl；//输出验证

09　{

10　int B=2，C=3；//花括号（代码块）内又对代码块外的同名标识符构成屏蔽

11　cout＜＜"A："＜＜A＜＜"，B："＜＜B＜＜"，C："＜＜C＜＜endl；

12　}

13　return 0；

14　}

15　int A=8，B=7，C=6；//定义性声明，外部int变量A、B和C

输出结果如下所示。

A：8，B：7，C：6

A：8，B：1，C：6

A：8，B：2，C：3

【代码解析】在代码第15行为全局变量A、B和C的定义性声明，这3个变量具备全局作用域，经过第3行的引用性声明后，理论上便可以使用在程序的任何地方（如A），但是，第7行声明的自动变量B具有块作用域，从第7行开始到第14行main（）函数结束，这块区域是自动变量B的作用域，也是其可见域。但是，这会导致内层变量B屏蔽了外层变量B，使得全局变量B在第7～14行之间不可见。

因此就本例而言，应将第7～14行从全局变量B的可见域中除去，同理，第9～12行组成的块中，第10行声明的B和C屏蔽了第7行声明的B和全局变量C，全局变量C的可见域为除10、11和12行之外的代码区。

可以看出，一个内层的标识符可以屏蔽外层的同名标识符（包括同名的全局标识符），使其不可见，这时，应从外部标识符的可见域中刨除内部标识符的可见区域。

内部静态变量的作用域、可见域及屏蔽法则和自动变量一样，但对外部静态变量而言，需要强调的是，在一个文件中声明的外部静态变量，将屏蔽其他文件中定义的同名全局变量。但是，不允许在同一文件中定义同名的全局变量和外部静态变量。文件可以看成内层，而程序则是外层，这是屏蔽法则的一种特殊情况。

注意

只有在可见域内才能对变量进行合法的访问。

使用作用域分辨符：可使被屏蔽的全局变量在局部可见，如果将代码6.18中的第11行改为如下所示。

cout＜＜"A："＜＜：A＜＜"，B："＜＜：B＜＜"，C："＜＜：C＜＜endl；输出结果将变为如下所示。

A：8，B：7，C：6

A：8，B：1，C：6

A：8，B：7，C：6

但是，不能使用：使被屏蔽的自动变量（如代码6.18中的第7行的B）在第10行和第12行之间可见。

2.全局变量引用声明位置与其可见域

全局变量只能有一个定义性声明，但允许有多个引用性声明，实际上，引用性声明的位置与其可见域有很大关系。

在块内对全局变量进行引用声明时，其可见性仅局限于该块，在该块外部访问全局变量时，编译器会给出变量未定义的错误提示，如代码6.19。

代码6.19　在块内对全局变量进行引用声明GlobalVariableAccess

＜--------------------------文件名：example619.cpp----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　{

06　extern int A；//全局变量A的引用性声明

07　cout＜＜"A："＜＜A＜＜endl；//输出A

08　}

09　cout＜＜"A："＜＜A＜＜endl；//此时，A不可见

10　return 0；

11　}

12　int A=7；//全局变量A的定义性声明编译链接，VC6给出的错误提示如下所示。

error C2065：'A'：undeclared identifier

在外部（函数外部、类外部）对全局变量进行引用声明具有“文件可见性”，文件中，从声明处开始，到文件结束的任何位置都可合法访问该全局变量。

技巧

推荐将全局变量的引用性声明放在文件的头部。当然，如果全局变量和访问代码在同一文件中，且变量定义在前而访问在后，那么在本文件中，该变量的可见域为从变量定义到文件结束。

引用声明不能提供全局可见性（不具备跨文件性质），这意味着，在使用全局变量之前，必须在当前文件或当前块中进行引用声明。

同一个全局变量的引用声明可以有很多次，变量的可见域是所有引用声明提供的可见域的总和。

3.重复定义

除了屏蔽外，在同一个作用域内，变量不能同名，否则程序编译时，编译器会给出变量重复定义的错误。实际上，在源程序中用变量名指明变量，而在程序执行过程中，变量对应这一块内存空间，所以，编译器维护着变量名和内存地址的映射表，在特定的代码行中，变量名只能对应一块内存空间，否则，编译器无法决定该使用哪块地址空间，便给出变量重复定义的提示，这就是全局变量只能定义声明一次的原因。

对于变量屏蔽这种情况，编译器能根据所在块决定变量名对应的内存地址，因此不会报错。在代码6.19中，如果在第5行和第8行组成的块中声明自动变量A，会导致A重定义的错误。“extern int A；”语句通知编译器A代表的是外部全局变量，此时再声明一个自动变量A，编译器无法判断后续代码中用到的A对应哪块地址（静态存储区或栈），VC6会给出如下错误提示。

error C2086：'A'：redefinition

6.8.3　函数的作用域和可见域

C++不允许在一个函数内部定义另一个函数，因此，在默认情况下，函数是全局的，可以在不同的文件间共享，比较后会发现，这和全局变量的用法有些类似，在使用一个函数前必须要对其进行声明，为了强调函数的外部存储特性，甚至可以在函数声明时使用关键字extern（经常省略）。

还可使用关键字static将函数声明为内部的，这样，只能在本文件中使用该函数，在函数定义和函数声明中都要使用static关键字，如下所示。

static int add（int,int）；

……

static int add（int m,int n）

{

……

}

static函数将屏蔽其他文件中外部定义的同名函数，即使外部定义了同名的函数，编译器仍会采用static函数，但是，不允许在同一文件中定义同名函数（重载除外）。

和全局变量一样，函数的声明位置与其可见域有很大关系，在块内对函数进行声明时，其可见性仅局限于该块，只能在该块内调用该函数；在外部（函数外部、类外部）对函数进行声明，具有“文件可见性”，从声明处开始，到文件结束的任何位置都可调用该函数；函数声明不能提供全局可见性，在调用函数之前，必须在当前文件或当前块中进行引用声明。

注意

如果函数定义和函数调用在同一文件中，且定义在前、调用在后，那么在本文件中，该函数的可见域为从变量定义到文件结束，如果函数定义在头部，那无须声明就可以在本文件的任何位置调用该函数。

函数的声明也可以有很多次，其可见域是所有声明提供的可见域的总和。

除了函数的作用域外，在第8章中会讲到，类的定义、声明和实例化，与其作用域和可见性有很大关系。同时，第5章中提及的结构也可以看成一种特殊的类。和类相关的作用域与可见域说明请参考第8章的相关内容。

6.9　小结

本章首先讲述了函数的作用及模块化带给程序设计的好处，说明了函数的定义和调用方法。一个函数必须定义在其他函数的外部，并且在一个程序中只允许定义一次。在调用函数之前，一定要对被调用函数进行声明。使用static关键字修饰函数的定义和声明，可将其作用域限定在本文件之中。正确地使用inline函数可以提高程序的运行效率。

函数参数有3种传递方法：值传递、指针传递和引用传递。在前两种传递方式下，实参和形参占用不同的存储单元，形参值的变化不会影响到实参的值，这与引用传递不同。函数重载方便了程序设计。重载函数是指函数名相同，但参数个数或类型不能相同，如果参数个数和类型都相同，仅仅是返回类型不同，是不行的，而且，如果只是参数个数相同，推荐用默认参数调用机制。递归是一种解决问题的通用编程方法，编写递归函数时，不要忘记递归调用终止条件，且每调用一次应向调用终止靠近一步，以确保递归调用能够正常结束，不至于导致系统内存耗尽而崩溃。

理解C++的内存管理机制能帮助理解作用域、可见域和生存期等概念。对变量和函数等的访问实际上是对内存单元的访问，这是编程的实质。应掌握全局变量、自动变量、静态变量及使用new和delete动态申请释放内存的用法。在同一个作用域内，变量不能同名，否则，程序编译时，编译器会给出变量重复定义的错误信息。不同的作用域内，变量同名不会出现语法错误，但会出现屏蔽的情况，应特别注意。

尽管没有变量那么复杂的机制，函数也是存在作用域和可见域的，同全局变量一样，声明的位置和可见域有较大的关系。

6.10　上机实践习题

1.实现一个求差函数，能够实现2个整型数据的求差运算，最后输出结果。

【提示】上述题目主要是要求读者熟悉函数的相关知识，重点是掌握函数的声明、定义和使用。

【关键代码】

01　int sub（int x,int y）

02　{

03　return x-y；

04　}

2.定义一个重载求和函数，根据用户输入的数据不同，得到不同的和值输出。

【提示】上述题目主要是要求读者熟悉函数重载的相关知识，重点是掌握如何进行函数重载。

【关键代码】

01　int add（int x,int y）

02　{

03　return x+y；

04　}

05　float add（float x,float y）

06　{

07　return x+y；

08　}

第7章　关于函数的高级专题

合理使用函数能将程序模块化，大大降低了问题的规模，提高了编码效率，方便以后复用代码。函数的参数传递有传值、传指针和传引用3种方式，从类型的角度上看，参数不仅仅可以是系统内建的数据类型，还可以是数组、结构以及后面要介绍的类对象等。另外，内存使用注意事项、函数与指针的关系也是学习C++必须迈过的一道槛。

本章主要涉及以下知识点。

❑内存使用剖析：介绍常见内存使用中的错误。

❑函数的参数与返回：再次重点介绍函数的参数与返回。

❑函数与指针：介绍函数指针的定义及使用。

❑函数与数组：介绍数组与函数之间的调用关系。

❑函数与复合数据：介绍函数与结构体、共用体及类对象如何调用与返回。

❑函数的编写：介绍如何编写一个优质的函数。

7.1　内存使用错误剖析

编写代码时，少不了和内存打交道，很多程序员对此提心吊胆，称内存为“雷区”或“bug集中营”似乎并不过分。即使是久经沙场的老手，有时也难免会落入内存错误的陷阱。本节帮助读者了解这些常见的错误，在编程时加以注意，把出错的概率降到最低。

7.1.1　内存泄露

使用new或malloc（）动态申请的内存，如果不再使用，应该把它释放掉，为程序节省内存空间，方便后面的使用。在C/C++中，内存管理器不会自动回收不再使用的内存。如果忘记释放不再使用的内存，在程序的运行过程中，这些内存就不能再被使用，也就造成了所谓的“内存泄露”。

内存泄露是最为常见的错误，现在的计算机配置比较高，内存容量很大，一两处内存泄露通常不至于让程序崩溃，也不会出现逻辑上的错误。当进程退出时，系统会自动释放该进程所有相关的内存，所以内存泄露的后果相对来说并不是灾难性的。但这并不意味着完全没有危险，如果在程序规模较大、长时间运行，或者是内存资源相对紧张的情况下，内存泄露过多会导致内存耗尽，系统没有后继内存可以使用，程序可能会崩溃。

第6章讲过，代码块中声明的局部变量在代码块执行完毕后会自动消亡，那如果是在代码块中申请的动态内存，系统会自动回收吗？答案是否定的，如下所示。

if（condition）

{

int*p=new int[8]；

}

p是代码块中声明的局部指针变量，在代码块执行完毕退出时，p自动消亡，但p指向的大小为8、类型为int型的内存空间并不会释放掉。代码块退出后，程序无法再通过指针p释放所申请的动态内存，这块内存已经“泄露”。

分配与释放配对使用，适时释放所申请的动态内存，能有效防止内存泄露。

注意

malloc（）要和free（）配对使用，new要和delete/delete[]配对使用。

7.1.2　野指针

前面提到指针消亡，并不意味着其指向的内存会被自动释放，同样释放动态内存，并不意味着指针会消亡，也不意味着指针的值会改变，如下所示。

int*p=new int[8]；

……

delete[]p；

指行完delete[]p后，指针p是不是就自动消亡了呢？错，不论是使用delete/delete[]还是使用free（），指针p非但不会消亡，其值也保持不变，并不会变为null。这时，使用“if（p！=null）”进行处理也无法起到防错作用。

为此，指针被free或delete/delete[]后，一定要置为null，没有置为null的指针常称为“野指针”，释放掉的堆内存会被内存管理器重新分配，野指针指向的内存已经被赋予新的意义。如果使用野指针释放或再次访问这块内存，会给程序带来灾难性的后果。

此外，下列两种情况也可以称为“野指针”。

1.未初始化指针

第4章中已经提到，任何指针变量刚创建时，其内容是随机的，在内存中乱指一通。因此，使用指针前，一定要对其初始化，使其指向合法的内存，对于无处可指的指针，也要将其赋值为null。

2.指向临时变量的指针

当前代码块执行完毕后，代码块中声明的临时变量（包括函数调用时的参数）都会自动消亡，其占用的内存空间（栈内存）也被内存管理器收回。此时，指向这些临时变量的指针便没有了意义，使用这些指针会给程序造成不可预料的后果，见代码7.1。

代码7.1　返回指向临时变量的指针PointerToTemp

＜------------------------文件名：example701.cpp------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　char*GetStr（）

04　{

05　char sz[]="Hello,C++"；//字符数组sz，在函数内部创建，函数执行结束后，

06　//其对应的内存区域会被撤销

07　return sz；

08　}

10　int main（）

11　{

12　char*p=GetStr（）；//返回字符数组的首地址给p

13　cout＜＜p＜＜endl；//用于p指向的内存被撤销，程序运行会出错

14　return 0；

15　}编译链接，编译器会给出如下警告。

warning C4172：returning address of local variable or temporary

输出结果如下所示。

↔

代码7.1　返回指向临时变量的指针，从中可以看出，程序并没有输出我们想要的“Hello,C++”，而是输出了乱码，这是由于指针p是野指针。在函数GetStr（）内，声明了字符数组sz，编译器根据初始化字符串“Hello,C++”的长度为其分配了栈内存，并将数组名指针sz返回，但是，在函数GetStr（）执行结束后，sz指向的内存空间已经撤销，因此，p的初始化实际是没有意义的，p并没有指向合法的内存空间，所以输出错误。

7.1.3　试图修改常量

程序中出现的字符串常量和其他全局常量（如全局const常量），是存放在.rodata里面的，.rodata内存页面是不能修改的，试图对常量进行修改，会引发内存错误，如代码7.2所示。

代码7.2　试图修改常量引发内存错误ModifyConstant

＜------------------------文件名：example702.cpp------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　char*p="Hello,C++"；//p指向一个常量字符串

06　cout＜＜p[1]＜＜endl；//输出字符串的第2个字符

07　p[1]='G'；//试图修改常量字符串，程序出错

08　cout＜＜p＜＜endl；

09　return 0；

10　}

【代码解析】代码第7行，由于试图修改常量字符串，所以会导致程序出错。

编译运行时，会出现如图7.1所示对话框。

图　7.1　试图修改常量引发内存错误

注意

此处的常量不包括局部const常量，从某种意义上说，在函数参数前加上const修饰符，只是给编译器做类型检查用的，编译器禁止修改这样的变量。但这并不是强制的，你完全可以用强制类型转换绕过去，一般也不会出错。

7.1.4　用错sizeof

运算符sizeof（）可以计算数组的大小（字节数），但对指针来说，sizeof仅仅得到指针变量的字节数。当数组作为函数的参数进行传递时，数组退化为同类型的指针，用sizeof是无法取得数组的大小的。代码7.3演示了sizeof的用法。

代码7.3　sizeof用法AboutSizeof

＜-------------------------文件名：example703.cpp-----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　void test（char str[20]）

04　{

05　cout＜＜"sizeof（str）："＜＜sizeof（str）＜＜endl；

06　}

07　int main（）

08　{

09　char str1[20]="1234567890123456789"；

10　cout＜＜"sizeof（str1）："＜＜sizeof（str1）＜＜endl；//字符数组的大小

11　test（str1）；//字符数组作为参数传递时，退化为指针

12　char*pChar=str1；

13　cout＜＜"sizeof（pChar）："＜＜sizeof（pChar）＜＜endl；//指针pChar占据的内存大小

14　return 0；

15　}

输出结果如下所示。

sizeof（str1）：20

sizeof（str）：4

sizeof（pChar）：4

【代码解析】在大小为20的char型数组作为函数参数进行传递时，在函数test（）内，代码第5行，对数组的sizeof（）操作返回的只是指针大小，无法返回数组的大小。

7.1.5　内存越界访问

使用指针和数组访问某块内存区域时，编译器并不会对数组下标是否越界、指针是否有效进行检查，如果不注意，很容易造成内存越界访问的错误，内存越界访问有两种：一种是读越界，一种是写越界，常称做缓冲区溢出。

读越界，即读了不属于自己的数据，如果所读的内存地址是无效的，程序会立刻崩溃，但如果所读内存地址是有效的，在读的时候不会出问题，但读到的数据是随机的，会产生不可预料的后果。

写越界，即往不该写的内存地址空间中写了东西，这往往会给程序带来很多匪夷所思的错误和BUG，有些症状是随机的，时有时无，给问题分析带来很多的困难。

一些辅助工具可以帮忙检查内存越界，但从根本上说，在编程时应十分小心，特别是对于外部传入的参数要仔细检查，另外，要做好程序的防错处理。

7.1.6　变量的初始化

不论是指针变量，还是普通变量，一定要时刻牢记“初始化”，虽然编译器会对有的变量自动初始化为0，但在声明变量时就对它进行初始化，是一个编程的好习惯，还要重视编译器的警告信息，发现有引用未初始化的变量，立即修改过来。

7.2　重申：函数参数传递和返回机制

前面已经提及，函数的参数传递包括有值传递、指针传递和引用传递，函数返回也可是返回值、返回指针或返回引用。抛开引用传递，对值传递和指针传递而言，不论是参数传递还是函数返回，理解“副本”的概念十分重要。

7.2.1　参数传递时的“副本”

先来看一下函数调用的过程，不论是值传递还是指针传递，编译器都要为每个参数制作临时副本，函数体中对参数的修改都是对副本的修改，对值传递调用来说，对副本的操作不会对传入的参数对象有任何的影响，这很好理解，但对指针传递调用来说，情况稍显复杂，很多人对此存在误解。指针参数传递的示例代码见代码7.4。

代码7.4　指针传递同样依赖“副本”ByPointer

＜--------------------文件名：example704.cpp----------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　void GetMem（char*p,int num）//定义一个函数用来申请一块动态内存，传递指针p

04　{

05　p=new char[num]；//为形参p分配一块动态内存

06　cout＜＜"p在内存中的地址："＜＜＆p＜＜endl；

07　}

08　int main（）

09　{

10　char*pChar=NULL；//外部char型指针pChar

11　cout＜＜"pChar在内存中的地址："＜＜＆pChar＜＜endl；

12　GetMem（pChar，10）；//复制的过程

13　if（pChar！=NULL）//判断pChar是否指向一块合法内存

14　{

15　cout＜＜"内存申请成功"＜＜endl；

16　delete[]pChar；//释放资源

17　}

18　else

19　cout＜＜"内存申请失败"＜＜endl；

20　return 0；

21　}

输出结果如下所示。

pChar在内存中的地址：0012FF7C

p在内存中的地址：0012FF28

内存申请失败

【代码解析】函数GetMem（）并没有完成内存申请的功能。在生成代码时，编译器为GetMem（）函数的参数生成副本，从输出的地址看，pChar和函数中的指针变量是两个不同的指针。“GetMem（pChar，10）；”将pChar作为参数传递给GetMem（）函数，实际上是用pChar的值为副本赋值，因此，GetMem（）函数内动态申请的内存是指定给副本p的，对pChar没有任何的影响，实际上，每执行一次GetMem（）函数都会造成一块内存泄露，因为没有用delete[]语句释放所申请的内存。

但是，在将指针参数传递给函数后，此时，副本和外部指针指向的是同一块内存，在函数内使用副本指针（间接访问，*指针）可访问其指向的内存区域，这块区域也是外部指针指向的区域，这样，在函数中便可完成对该区域的读写操作。对大型的结构或变量来说，传值调用需要赋值整个变量（整块内存），而传指针调用仅仅需要复制一个指向该变量的指针，大大提高了程序的运行效率。

从上述角度出发，可以将代码7.4修改为代码7.5。

代码7.5　使用指向指针的指针申请内存PointerToPointer

＜---------------------------文件名：example705.cpp---------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　void GetMem（char**p,int num）//参数更改为指向指针的指针

04　{

05　*p=new char[num]；

06　}

07　int main（）

08　{

09　char*pChar=NULL；

10　GetMem（＆pChar，10）；//函数调用，因为定义在前，函数声明可省略

11　if（pChar！=NULL）//如果分配成功

12　{

13　cout＜＜"内存申请成功"＜＜endl；

14　delete[]pChar；//释放资源

15　}

16　else

17　cout＜＜"内存申请失败"＜＜endl；

18　return 0；

19　}

输出结果如下所示。

内存申请成功

【代码解析】代码第5行，通过“指向指针的指针”作函数参数，对＆pChar所指向的区域，也就是pChar进行修改，达到了内存申请的目的。

7.2.2　函数返回时的“副本”

函数执行完毕后，函数内部声明的局部变量会自动消亡，对应的内存被释放，由内存管理器收回，但返回值会被放置（复制）到指定位置（可能是CPU寄存器，也可能是某个内存单元），然后上级函数从这个位置取得返回值。这个位置，可以看成是函数返回值的“副本”，这解释了为什么可以用“return局部变量；”来返回一个值，示例见代码7.6。

代码7.6　函数返回时的“副本”AboutReturn

＜------------------------文件名：example706.cpp------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　char*GetMem（int num）//函数定义，返回char型指针

04　{

05　char*p=new char[num]；

06　return p；//将指向动态内存的指针p返回，复制到外部，p会被撤销

07　}

08　int main（）

09　{

10　char*pChar=NULL；

11　pChar=GetMem（10）；//函数调用，pChar指向函数GetMem中申请的动态内存

12　if（pChar！=NULL）//如果申请成功

13　{

14　cout＜＜"内存申请成功"＜＜endl；

15　delete[]pChar；//释放内存资源

16　}

17　else

18　cout＜＜"内存申请失败"＜＜endl；

19　return 0；

20　}

输出结果如下所示。

内存申请成功

【代码解析】在GetMem（）函数内声明的char型指针p虽然是局部变量，但第6行，通过“return p；”语句，将该指针的值复制到了外部副本中，通过外部副本传递给了pChar，这样，pChar便指向了在GetMem（）函数中申请的动态内存。

前面“野指针”一节也已经讲到，不能返回指向栈内存的指针，那样的话，即使上级函数从函数返回的“副本”处取得了指针值，但该指针指向的内存已被撤销，会给系统带来意想不到的错误，代码7.1就是个很好的例子。

如果函数返回的指针指向的是静态存储区，情况会是什么样呢？见代码7.7。

代码7.7　返回指向静态存储区的指针SpecialPointer

＜-----------------------文件名：example707.cpp-------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　char*GetMem（int num）

04　{

05　char*p="Hello,C++"；//指针p指向的是常量字符串，位于静态存储区

06　return p；

07　}

08　int main（）

09　{

10　char*pChar=NULL；

11　pChar=GetMem（10）；//调用GetMem函数返回指针pChar，不可对pChar

12　//指向的内存进行改写

13　if（pChar！=NULL）

14　cout＜＜"内存申请成功"＜＜endl；

15　else

16　cout＜＜"内存申请失败"＜＜endl；

17　return 0；

18　}

输出结果如下所示。

内存申请成功

【代码解析】该代码运行没有问题，但因为代码第5行的“Hello,C++”是常量字符串，位于静态存储区.rodata，函数GetMem（）每次返回的始终是同一块“只读”内存，无法通过指针对该块内存进行修改，否则会引发修改常量的错误。

注意

对比代码7.1中“char sz[]="Hello,C++"；”和代码7.7中“char*p="Hello,C++"；”体会数组和指针的异同。

7.3　函数与指针

在前面的章节中已经介绍了函数的传指针参数调用，返回指针，根据数组名和指针的等价性，函数的参数也可以是数组，在这些基本概念的基础上，本节讨论指向函数的指针以及带参主函数的相关内容。

7.3.1　指向函数的指针

函数是一组代码的封装体，这组代码在内存中占有一片存储空间，该空间的起始地址存放在以函数名为名的单元中，换言之，函数名就是指向函数的常指针，这有点类似于数组名是指向数组内存空间的常指针。

1.函数指针的声明和初始化

可以声明一个指针变量指向一个函数，称为指向函数的指针（简称“函数指针”），形式如下所示。

返回类型（*指针名）（参数列表）；这样，便可用该指针间接调用函数，如下所示。

int（*Compare）（const char*，const char*）；上述代码声明了一个名为Compare的函数指针，用以保存某些函数的地址，例如，可使用Compare指向C++标准函数库中的C风格字符串比较函数strcmp。

Compare=＆strcmp；

Compare=strcmp；

两种对Compare赋值（初始化）的方式都是正确的，函数名strcmp就可以看做指向函数的常指针。

此外，还可以在函数指针声明的同时对其初始化，如下所示。

int（*Compare）（const char*，const char*）=strcmp；

和普通指针变量一样，使用函数指针前，一定要对该指针进行初始化，使它指向某一可执行的函数块。给函数指针初始化或赋值时，指针和被指向的函数需要匹配函数参数的个数、类型及函数的返回值。

注意

“int（*Compare）（const char*，const char*）；”和“int*Compare（const char*，const char*）；”有何不同？前者声明了一个函数指针，后者是函数原型声明，返回值是int型指针。

2.函数指针使用举例

函数指针的使用范例如代码7.8所示。

代码7.8　函数指针的使用PointerToFunction

＜-----------------------文件名：example708.cpp-------------------------------＞

01　#include＜iostream＞

02　#include＜cstring＞//strcmp用到的头文件

03　using namespace std；

04　int main（）

05　{

06　char sz1[]="hello,C++"；//C风格字符串

07　char sz2[]="hello"；//C风格字符串

08　int（*pF）（const char*，const char*）；//函数指针的声明

09　pF=strcmp；//也可写作pF=＆strcmp；

10　int result=pF（sz1，sz2）；//也可写作int result=（*pF）（sz1，sz2）；

11　if（result==0）

12　cout＜＜"字符串相等"＜＜endl；

13　else

14　cout＜＜"字符串不等"＜＜endl；

15　return 0；

16　}

输出结果如下所示。

字符串不等

【代码解析】从代码第8～10行，可以看出，使用函数指针调用函数有以下3个步骤。

❑声明函数指针；

❑对函数指针赋值（初始化）；

❑用指针形式调用函数，调用格式如下所示。

指针名（参数表）；//或者是（*指针名）（参数表）

注意

函数指针变量不支持算术运算，对函数指针的加减操作是没有意义的。

7.3.2　typedef

为了避免读者对typedef的用法产生误解，前面一直没有介绍typedef的相关内容，介绍完函数指针，现在介绍typedef的相关内容。

typedef用来给一个已经存在的类型声明一个别名，举一个最简单的例子。

typedef int*int_p；//不要忘记分号typedef为int*引入了一个新的助记符int_p，可以在程序中使用int_p声明指向int型变量的指针，如下所示。

int_p pA,pB；

上述代码声明了两个int型指针变量pA和pB，应当注意typedef不同于编译预处理命令#define，这种不同主要体现在以下两个方面。

（1）#define是预处理指令，在编译预处理时进行简单的替换，不做正确性检查，不管含义，只是简单的替换，如下所示。

#define PI 3.14

程序中，所有出现PI的地方都被替换为3.14，如果错把3.14写为3.i4，编译预处理并不会指出任何错误，所有出现PI的地方都会被替换成3.i4，但是编译器会指出错误。

而typedef是在编译时进行处理的，可以理解为“为某个类型寻找另外一种书写方式”，请看下述代码。

#define int_p int*

int_p a,b；//相当于int*a,b；a是int型指针变量，b是int型变量，简单的宏替换对比

typedef int*int_p；

int_p a,b；//a,b都为指向int型指针变量，typedef为int*引入了一个新的助记符结合指针的相关内容可以解释下述代码。

typedef int*int_ptypedef；

#define int_pdefine int*

那么

const int_ptypedef p1；//p1不可更改，但其指向的变量可更改

const int_pdefine p2；//p2可更改，但其指向的内容不可更改

int_ptypedef是一种指针类型，“const int_ptypedef p1；”声明了const指针p1，相当于“int*const p1”，p1的值不可修改，但其指向的变量可以修改，“const int_pdefine p2；”等价于“const int*p2”，指针p2可以修改，但无法通过p2的间接引用修改其指向的变量。

注意

typedef语句后的分号不要忘记，#define不是语句，后面不能加分号，如果#define结构后出现分号，会一起被替换。

注意

typedef结构不能出现在函数中，因此，typedef结构具有文件作用域（编译单元作用域）。

（2）#define结构可以抽象为如下所示。

#define A B程序中，所有出现A的地方都被B替换，A和B是分开的两个部分，而typedef后面是一条声明语句（一个整体），如下所示。

typedef int*int_p；

把typedef去掉后的部分“int*int_p；”是条完整的声明语句，声明了int型指针变量int_p，使用typedef后，int_p相当于int*的别名，或者是“助记符”。这样，便可以将一个复杂的声明用一个简单的助记符代替，对代码7.8进行改写，如代码7.9所示。

代码7.9　使用typedef简化函数指针声明UseTypedef

＜------------------------文件名：example709.cpp------------------------------＞

01　#include＜iostream＞

02　#include＜cstring＞//strcmp用到的头文件

03　typedef int（*Func）（const char*，const char*）；//typedef后，Func可以当成一种类型来用

04　using namespace std；

05　int main（）

06　{

07　char sz1[]="hello,C++"；

08　char sz2[]="hello"；

09　Func pF；//创建函数指针pF

10　pF=strcmp；//也可写作pF=＆strcmp；

11　int result=pF（sz1，sz2）；//也可写作int result=（*pF）（sz1，sz2）；

12　if（result==0）

13　cout＜＜"字符串相等"＜＜endl；

14　else

15　cout＜＜"字符串不等"＜＜endl；

16　return 0；

17　}

输出结果如下所示。

字符串不等

【代码解析】代码第3行，“typedef int（*Func）（const char*，const char*）；”的返回值为int型，有两个char*参数的函数指针声明了一个助记符（别名）Func，因此，在程序中便可以使用“Func pF；”像声明普通的int、double类型一样声明一个Func型的函数指针。

注意

typedef后面是条完整的声明语句，普通的变量名Func因为typedef的存在成了类型的别名。

7.3.3　通过函数指针将函数作为另一个函数的参数

函数指针的一个功用是把函数地址作为参数传送，以提高函数的通用性和灵活性，如代码7.10所示。

代码7.10　函数指针作参数PointerToFuncSample

＜------------------------文件名：example710.cpp------------------------------＞

01　#include＜iostream＞

02　typedef void（*hs）（int）；//typedef用法，hs可以当成类型来用，声明函数指针

03　using namespace std；

04

05　void Just_Print（int i）//函数定义，输出参数i

06　{

07　cout＜＜i＜＜""；

08　}

09　void Call（const char*name,hs func）//Call函数定义，函数指针func作为参数

10　{

11　int array[9]={1，2，3，4，5，6，7，8，9}；

12　cout＜＜name＜＜endl；

13　for（int i=0；i＜9；i++）

14　func（array[i]）；//通过函数指针调用函数

15　}

16　int main（）

17　{

18　char name[]="输出"；//字符数组name

19　hs pFun=Just_Print；//声明一个hs型函数指针，并用Just_Print为其初始化

20　Call（name,pFun）；//调用Call（）函数，其内部调用pFun，即Just_Print（）函数

21　cout＜＜endl；

22　return 0；

23　}

输出结果如下所示。

输出

1 2 3 4 5 6 7 8 9

【代码解析】代码第2行，“typedef void（*hs）（int）；”将指向“一个int型参数，无返回值的函数”的指针用别名hs代替，这样，可在程序中声明hs型的函数指针。函数call的形式为“void Call（const char*name,hs func）”，第一个参数是字符串，第二个参数是hs型的函数指针，在Call函数内通过该函数指针参数调用该指针所指向的函数Just_Print（），这样，Just_Print（）函数的地址作为实参传递给Call（）函数中的形参func。从本质上讲，这属于传指针调用。

7.3.4　函数指针数组

指向函数的指针还可以组成指针数组，称为函数指针数组。函数指针数组的使用范例见代码7.11。

代码7.11　函数指针数组ArrayofFuncPointers

＜-------------------------文件名：example711.cpp-----------------------------＞

01　#include＜iostream＞

02　typedef void（*hs）（int）；

03　using namespace std；

04　void Sub_Print（int i）//Sub_Print（）函数定义，输出参数减1

05　{

06　cout＜＜（i-1）＜＜""；

07　}

08　void Add_Print（int i）//Add_Print（）函数定义，输出参数加1

09　{

10　cout＜＜（i+1）＜＜""；

11　}

12　void Just_Print（int i）//Just_Print（）函数定义，输出参数，不加不减

13　{

14　cout＜＜i＜＜""；

15　}

16　void Call（const char*name,hs func）//Call函数定义，hs型函数指针func作为参数

17　{

18　int array[9]={1，2，3，4，5，6，7，8，9}；

19　cout＜＜name＜＜endl；

20　for（int i=0；i＜9；i++）

21　func（array[i]）；//对func（）函数调用

22　}

23　int main（）

24　{

25　char*names[]={"输出"，"减1输出"，"加1输出"}；//创建一字符串数组

26　hs fun_sz[]={Just_Print,Sub_Print,Add_Print}；//创建函数指针数组fun_sz

27　for（int i=0；i＜3；i++）

28　{

29　Call（names[i]，fun_sz[i]）；//将函数指针数组中的元素传递给Call（）函数

30　cout＜＜endl；

31　}

32　return 0；

33　}

输出结果如下所示。

输出

1 2 3 4 5 6 7 8 9

减1输出

0 1 2 3 4 5 6 7 8

加1输出

2 3 4 5 6 7 8 9 10

【代码解析】该代码是对代码7.10的扩展，Sub_Print、Add_Print和Just_Print同是“一个int型参数，无返回值的函数”，语句“hs fun_sz[]={Just_Print,Sub_Print,Add_Print}；”声明了大小为3的hs型一维函数指针数组fun_sz，并用已经定义的3个函数（函数名等价于指向函数的常指针）为数组元素初始化。通过数组合理组织函数，使程序结构条理清晰，可读性强。

和普通数组一样，声明多维形式的函数指针数组也十分方便，此处不再赘述。

函数指针数组中的每个元素必须是同类型的指针，即每个元素指向的函数必须具有相同的返回值和参数列表，如果是不同类型的函数指针，可以考虑使用链表。

注意

函数指针和普通指针本质是相同的，都指向内存中特定的程序实体。和普通指针一样，函数指针同样可以作为结构和类的成员（类的相关内容请参考第8章）。

7.3.5　返回函数指针的函数

和普通指针一样，函数指针也可以作为另一个函数的返回值，如代码7.12所示。

代码7.12　返回函数指针的函数ReturnFuncPointer

＜-------------------------文件名：example712.cpp-----------------------------＞

01　#include＜iostream＞

02　typedef int（*hs）（int,int）；

03　using namespace std；

04　int Add（int m,int n）//Add函数定义，两数相加

05　{

06　return m+n；

07　}

08　int multiply（int m,int n）//Multiply函数定义，两数相乘

09　{

10　return m*n；

11　}

12　hs lookup（int choice）//函数返回hs型，即指向函数的指针

13　{

14　if（choice==0）//如果参数choice为0，返回加和函数

15　return Add；

16　else//否则，返回乘积函数

17　return multiply；

18　}

19　int main（）

20　{

21　int num1，num2，xz,res；

22　cout＜＜"请输入两个整数以选择要进行的操作："＜＜endl；

23　cin＞＞num1＞＞num2；//输入两个操作数

24　cout＜＜"相加请输入0，否则为相乘"＜＜endl；

25　cin＞＞xz；//输入要进行的操作

26　hs fun=lookup（xz）；//fun函数将根据xz决定取Add还是Multiply

27　res=fun（num1，num2）；//调用fun函数

28　cout＜＜"结果是"＜＜res＜＜endl；//输出结果

29　return 0；

30　}

输出结果如下所示。

请输入两个整数以选择要进行的操作：

5 8

相加请输入0，否则为相乘

0

结果是13

【代码解析】代码第12行，lookup函数的返回值是hs型函数指针，查询具体要对用户输入的两个整数进行什么操作，将应调用函数的地址通过hs型指针返回，并赋值给fun。

7.3.6　带参主函数

前面介绍过的main函数都是不带参数的，实际上，main函数也可以带参数。main函数的参数是由谁传来的呢？答案是操作系统，C++规定main函数的参数只能有两个，即argc和argv，带参main函数的形式如下所示。

int main（int argc,char*argv[]）

{

……

}

第一个参数argc必须是整型变量，称做参数计数器，其值是包括命令名在内的参数个数；第二个参数argv必须是指向字符指针数组，存放命令名和参数字符串的地址。

要调用带参主函数必须在操作系统环境下进行，参见如下示例代码。

#include＜iostream＞

using namespace std；

int main（int argc,char*argv[]）//两个参数

{

for（int i=argc-1；i＞=0；i--）//倒着输出，0到argc-1，防止越界

{

cout＜＜argv[i]＜＜endl；

}

return 0；

}编译链接上述代码生成可执行文件，假设其名为example.exe，复制到C盘根目录下，在DOS环境下输入下列命令行。

C：\＞example world hello

输出结果如下所示。

hello

world

example

文件名example本身也算个参数，所以，上述命令行调用example.exe共用3个参数，DOS系统向main函数传递的参数argc为3，字符指针数组argv中的元素argv[0]、argv[1]、argv[2]分别存储着字符串example、world、hello的地址，按照main函数的定义，将3个字符串倒序输出。

7.4　函数与数组

前面已经讨论过数组和指针的关系，知道数组名实际上是指向数组所占内存单元的常指针，由此可知，同指针一样，数组既可以作为函数的参数，也可以作为函数的返回值。

7.4.1　数组名作为函数参数

数组名用作函数参数时，用以向程序传递数组所占内存单元的首地址，数组名参数与指针参数的用法几乎一致。有数组参数的函数原型的一般形式如下所示。

返回类型函数名（数组名[]，其他参数）

习惯上在参数列表中要指明数组元素的个数，在本章前面已经说明，在函数内部使用sizeof（数组名）返回的只是指针大小，而不是数组大小，因此，在参数列表中显式注明数组元素的个数是个好的编程习惯，当然，如果能保证不会出现越界访问的情况，这个参数可以省略。理论上，“数组名[]”中[]是空的，没有数字，如果在其中写明元素大小，编译器也不予理会，不会进行错误检验。

代码7.13　演示了如何使用数组名作为函数参数。

代码7.13　数组名作为函数参数CallByArray

＜------------------------文件名：example713.cpp------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int search（int v[]，int n,int val）//search函数定义，数组v作为参数

04　{

05　int i；

06　for（i=0；i＜n；i++）

07　if（val==v[i]）

08　return（i+1）；

09　return-1；

10　}

11　int main（）

12　{

13　int sz[6]={1，6，3，5，4，2}；//创建了数组sz，大小为6

14　int*pInt=sz；//数组名等价于指针，为int型指针pInt赋值

15　int num；//声明int型变量num

16　cout＜＜"请输入你要检索的数字（1～6）："＜＜endl；

17　cin＞＞num；

18　int res=search（sz,sizeof（sz）/sizeof（int），num）；//在数组sz中查num的索引号，传递数组

19　//名常指针

20　cout＜＜num＜＜"是数组中的第"＜＜res＜＜"个元素"＜＜endl；

21　cout＜＜"请输入另一个你要检索的数字（1～6）："＜＜endl；

22　cin＞＞num；

23　res=search（pInt,sizeof（sz）/sizeof（int），num）；//传递指针变量与传递数组名常指针等价

24　cout＜＜num＜＜"是数组中的第"＜＜res＜＜"个元素"＜＜endl；

25　return 0；

26　}

输出结果如下所示。

请输入你要检索的数字（1～6）：

2（注：键盘输入）

2是数组中的第6个元素

请输入另一个你要检索的数字（1～6）：

5（注：键盘输入）

5是数组中的第4个元素

【代码解析】代码第3行，search函数参数列表中，“int v[]”说明v是int型数组，在main（）函数中可以用数组名sz做实参，也可用int型指针pInt做实参，再次验证了数组名和指针的等价性。

用指针形式向函数传送参数还有一个优点，那就是调用参数不一定是数组的起始地址，还可以使用指向数组中某元素的指针，换言之，允许将数组的一部分传递给函数，如代码7.13中，可以将main（）函数写为如下形式。

int main（）

{

int sz[6]={1，6，3，5，4，2}；

int*pInt=＆sz[2]；

int num；

cout＜＜"请输入你要检索的数字（1～6）："＜＜endl；

cin＞＞num；

int res=search（pInt,sizeof（sz）/sizeof（int）-2，num）；

cout＜＜num＜＜"是数组中的第"＜＜res＜＜"个元素"＜＜endl；

return 0；

}代码7.13中其他部分不变，编译运行后，

输出结果如下所示。

请输入你要检索的数字（1～6）：

5（注：键盘输入）

5是数组中的第2个元素或者

请输入你要检索的数字（1～6）：

6（注：键盘输入）

6是数组中的第-1个元素

将sz数组中第3个元素（sz[2]）的地址作为参数传递给函数search后，函数search只检索从sz[2]开始的部分，当然，要指定子数组的大小为“sizeof（sz）/sizeof（int）-2”，看出原来数组中第4个元素5在输入的子数组中排在第2个，而原来第2个元素6不在输入的子数组中，所以程序返回-1。

注意

以数组作为函数参数，通过指针进行通信，是上级调用函数向被调函数传送大量数据的一种方法。深入理解数组名和指针的等价性，有助于写出高质量的代码。

多维数组也可作为函数参数，第一维的大小可以省略，其他维的大小不能省略，如代码7.14所示。

代码7.14　多维数组作为函数参数CallByMultiDArray

＜------------------------文件名：example714.cpp------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int add（int x[][3][4]，int n）//多维数组x作为形参

04　{

05　int res=0；

06　for（int i=0；i＜n；i++）

07　for（int j=0；j＜3；j++）

08　for（int k=0；k＜4；k++）

09　res+=x[i][j][k]；

10　return res；

11　}

12　int main（）

13　{

14　//创建多维数组sz，并初始化，作为add函数调用的实参

15　int sz[2][3][4]={{{1，2，3，4}，{5，6，7，8}，{9，10，11，12}}，

16　{{1，2，3，4}，{5，6，7，8}，{9，10，11，12}}}；

17　int res=add（sz,sizeof（sz）/sizeof（int[3][4]））；

18　cout＜＜"加和："＜＜res＜＜endl；

19　return 0；

20　}

输出结果如下所示。

加和：156

【代码解析】代码第3行，在add函数定义时，形参3维数组x必须指明后面二维的大小，否则，编译器会报错。程序使用语句“sizeof（sz）/sizeof（int[3][4]）”来计算数组sz第一维大小。

7.4.2　通过指针得到多于1个的回传值

理论上说，C++函数最多只能有1个返回值（return返回值），因此，由函数返回数组似乎是不可能的，实际上，使用指针可以使函数得到多于1个回传值。

1.返回指针变量

返回指针变量，便可以对指针指向的一片内存区域进行读写，需要注意的是不可返回指向栈内存的指针，否则会出现“野指针”内存错误。

2.通过指针参数修改多个变量的值，见代码7.15

代码7.15　通过指针参数修改多个变量的值CallByPointerSample

＜------------------------文件名：example715.cpp------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　void Calc（float a,float b,float*s,float*l）//传递指针参数可实现对多个变量的修改

04　{

05　*s=a*b；

06　*l=2*（a+b）；

07　}

08　int main（）

09　{

10　float chang,kuan,mj,zc；//4个float型变量，分别代表长、宽、面积和周长

11　cout＜＜"请输入矩形的长和宽："＜＜endl；

12　cin＞＞chang＞＞kuan；//接收用户输入的长和宽

13　Calc（chang,kuan，＆mj，＆zc）；//计算面积和周长

14　cout＜＜"面积："＜＜mj＜＜endl；

15　cout＜＜"周长："＜＜zc＜＜endl；

16　return 0；

17　}

输出结果如下所示。

请输入矩形的长和宽：

3 7

面积：21

周长：20

【代码解析】代码第3行，通过传递指针调用实现了函数中对多个参数的操作，这样，便可不受“返回值至多有一个”的限制。

说明

每个链表都维护着一个指向第一个元素的指针，该指针可用作函数的参数或返回，以方便对链表中元素的处理，这在本质上和数组指针是相同的。

7.5　函数与结构体、共用体及类对象

前面已经讲过结构体和共用体的概念，结构体和共用体将数据整合为一个单独的实体。结构体变量、共用体变量以及后面要介绍的类对象，用法都接近于普通的变量，对结构体变量、共用体变量和类对象来说，函数支持其传值、传指针和传引用调用，同时，函数可返回结构体变量、共用体变量和类对象，也可返回指向这些变量的指针和引用。

下面以结构体来讨论使用方式，共用体和类对象与结构体在函数调用和返回的机制上是一致的。

7.5.1　3种参数调用

同其他变量一样，结构变量也可以作为函数的参数，请看代码7.16。

代码7.16　结构变量作函数的参数CallByStructVariable

＜-----------------------文件名：example716.cpp-------------------------------＞

01　#include＜iostream＞

02　#include＜cmath＞//使用计算平方根的函数sqrt需要此头文件

03　using namespace std；

04　struct Complex_Int//结构Complex_Int（整型复数）定义

05　{

06　int x；

07　int y；

08　}；

09　void print_CX（Complex_Int cx）；//输出函数，值传递

10　double Calc_Abs（const Complex_Int＆cx）；//计算结构变量的模，引用传递

11　void Expand（Complex_Int*pCX,int n）；//将pCX指向的结构变量扩大n倍，指针传递

12　int main（）

13　{

14　Complex_Int cNum1={1，2}；//声明Complex_Int型结构变量cNum1并对其初始化

15　int n=4；

16　cout＜＜"cNum1："＜＜endl；

17　print_CX（cNum1）；//调用输出函数

18　cout＜＜"cNum1的模是："＜＜Calc_Abs（cNum1）＜＜endl；//调用求模函数

19　Expand（＆cNum1，n）；//调用放大函数

20　cout＜＜"放大4倍后："＜＜endl；

21　print_CX（cNum1）；//放大4倍后输出

22　return 0；

23　}

24　void print_CX（Complex_Int cx）

25　{

26　cout＜＜cx. x＜＜"+i*"＜＜cx.y＜＜endl；

27　}

28　double Calc_Abs（const Complex_Int＆cx）

29　{

30　double res=sqrt（（cx. x*cx.x+cx.y*cx.y））；//平方和开根号

31　return res；

32　}

33　void Expand（Complex_Int*pCX,int n）

34　{

35　pCX-＞x*=n；

36　pCX-＞y*=n；

37　}

输出结果如下所示。

cNum1：

1+i*2

cNum1的模是：2.23607

放大4倍后：

4+i*8

【代码解析】分别演示了传值调用，传指针调用和传引用调用。首先声明了一个复数结构Complex_Int，在print_CX函数中采用了值传递，在函数内将生成实参的“复制品”，在以前写的函数中，参数多是像int,char之类的简单变量，这些变量占用的内存并不多，复制也快。

但结构、共用体以及后面要讲的类对象往往由多个成员变量组成，占用内存大，如果复制一份，会造成时间和空间双重浪费。采用指针传递和引用传递可有效解决这一问题，代码第33行，函数Expand用于将参数pCX指向的结构变量的x和y都扩大为原来的n倍，函数Calc_Abs则采用了引用传递的方式，同时，为了防止在Calc_Abs函数内对参数进行修改，采用了const修饰符，指明参数将被当做常量对待，编译器将不允许在当前函数内修改这个参数。

7.5.2　3种返回机制

函数的返回机制同样有返回结构变量、返回指向结构变量的指针和返回引用3种方式。同参数传递一样，返回结构变量需要复制“结果”，浪费时间和空间，传递指针和引用能有效提高效率，详细的用法与第6章中讲述的普通变量的3种返回机制完全一致。

7.6　函数编写的建议

本节简单介绍如何写出高效，不易出错的代码，当然，这些建议只是最基本的几条，在网络上或者专门讨论C++编程技巧的教材中，讨论函数编写原则和建议的篇幅是这里的几十倍，这里权当是抛砖引玉，有效与否还靠读者的理解和检验。

7.6.1　合理使用const

在指针传递或引用传递时，如果参数仅仅是输入用，则应在类型前加const，以防止指针在函数体内被意外修改，若输入参数采用“值传递”方式，函数将自动产生临时变量用于复制该参数，该参数本就不需要保护，不用const修饰。

对于非内部数据类型的输入参数，尤其是占内存字节较多的参数，应该将“值传递”改为“const引用传递”，以提高效率。但对内部数据类型的输入参数而言，不要将“值传递”的方式改为“const引用传递”。否则既达不到提高效率的目的，又降低了函数的可理解性。

C++中，返回值也可用const修饰，这样，在返回引用或指针时，不允许使用如下述代码的形式对返回值进行改写。

函数名（参数表）=表达式；

示例代码如7.17所示。

代码7.17　const与函数返回值AboutConst

＜-----------------------文件名：example717.cpp-------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int＆addInt（int num1，int num2，int＆sum）//使用const修饰返回值可防意外改写

04　{

05　sum=num1+num2；

06　return sum；

07　}

08　int main（）

09　{

10　int x=1，y=2，sum=0；

11　addInt（x,y，sum）=9；//如果addInt函数返回值使用const修饰，本句将出错

12　//sum=10；这种修改形式是合法的

13　cout＜＜"sum："＜＜sum＜＜endl；

14　return 0；

15　}

输出结果如下所示。

sum：9

【代码解析】代码第11行，通过“addInt（x,y，sum）=9；”完成了对sum的改写，这种写法多少有些费解，常给程序带来一些隐藏的问题。为了防止上述形式对函数返回值的改写，可以用const来修饰函数，函数头定义为如下所示。

const nt＆addInt（int num1，int num2，int＆sum）

这样，编译器将指出语句“addInt（x,y，sum）=9；”存在错误，需要注意的是，const只修饰函数返回值不能进行修改，并不改变sum的读写属性，在程序中仍可通过如“sum=10；”的形式改写sum。

7.6.2　检查输入参数的有效性

很多函数代码本身并没有太大问题，常常是输入参数出错或出现了没有考虑到的情况，推荐的检查方式是采用assert宏，关于assert宏的详细介绍请参考第20章。此外，还要检查一些全局变量、指针等是否有效。

7.6.3　函数返回类型的判断

返回值是传值、传指针还是传引用，在函数设计和编写过程中要规划好。这不仅涉及函数返回的效率，而且还要保证返回的指针和引用指向的不是栈内存，否则，极容易形成“野指针”。

7.7　小结

本章讨论了一些与函数相关的相对高阶的内容，以前面几章的内容为基础，首先介绍了C++程序中经常出现的内存错误，这往往是很多初学者容易忽略的地方，结果让程序到处是漏洞，无从运行。对函数参数传递和返回机制进行了“重申”，理解“副本”的概念，不论是传值还是传指针调用，都存在“复制品”，不同的是传指针仅复制指针变量占据的4个字节（某些系统是两个字节），效率相比传值调用要高，尤其体现在大对象的参数传递时。函数指针的概念是对变量指针概念的推广，借此可以将函数名当做变量名看待，方便将函数作为其他函数的参数，组成函数数组，编写返回函数指针的函数，为了更清晰地理解函数指针的本质，本章介绍了typedef的用法。关于typedef，首先要体会其和#define的不同之处，typedef后面跟着一条完整的声明语句，声明语句中原来的变量名或函数名因为typedef的存在成为了该类型的“助记符”，相当于一种新的类型，借助typedef这一工具，函数指针的体系变得条理清晰，使用方便。

后面讨论了函数与数组、共用体、结构体（还有马上就要介绍的类对象）的关系，数组名等价于指向数组所占内存区域的常指针，因此，数组与指针是完全等价的，数组名也可以作函数参数。数组名作形参，指针作实参是种常见的用法。通过指针参数可以使函数返回多个值，对多个变量进行修改。不只是数组，通过头指针还可以在函数中实现对链表的管理和操作。对结构变量、共用体变量和类对象来说，函数存在值传递、指针传递和引用传递3种参数调用和函数返回机制，这与基本类型变量的参数调用和函数返回机制是完全一致的。

最后，本章简要介绍了一些编写函数的建议，随着类对象的引入，这方面的内容还会不断补充，不断完善。

7.8　上机实践习题

1.定义一个函数指针，能够实现两个整型数据的求差运算，最后输出结果。

【提示】上述题目主要是要求读者熟悉函数指针的相关知识，重点是掌握函数指针的声明、定义和使用。

【关键代码】

01　int（*pf）（int,int）；

02

03　int sub（int x,int y）

04　{

05　return x-y；

06　}

07　//main函数中的代码

08　int x,y；

09　pf=＆sub；

10　cin＞＞x＞＞y；

11　cout＜＜x＜＜"-"＜＜y＜＜"="＜＜pf（x,y）＜＜endl；

2.定义一个函数指针，根据用户输入的数据不同，求菲波拉其数列的前20个数据，并将结果保存到数组中，最后输出。

【提示】上述题目主要是要求读者了解数组作为函数参数的相关知识，重点是掌握如何进行使用。

【关键代码】

01　void（*pf）（int[]，int）；

02

03　void fb（int array[]，int size）

04　{

05　int i=0；

06　array[0]=1；

07　array[1]=1；

08　for（i=2；i＜size；i++）

09　{

10　array[i]=array[i-1]+array[i-2]；

11　}

12　}

13　//main函数中的代码

14　pf=＆fb；

15　pf（array,sizeof（array）/sizeof（int））；

16　for（i=0；i＜sizeof（array）/sizeof（int）；i++）

17　{

18　cout＜＜array[i]＜＜""；

19　}

第三篇　面向对象的C++

第8章　面向对象技术基础

欢迎开启面向对象设计的大门，在第1章中已经简要介绍过面向对象设计的基本理念，本章将具体讲述类和对象的概念。从前面的介绍可知，程序是由一个个函数组成的，是结构化的编程方法。从本章开始，编写的程序是由对象组成的，将要学习用C++语言进行面向对象的程序设计，当然，面向对象设计也离不开函数等前面讲述的基础知识。

本章主要涉及以下知识点。

❑面向对象的概念：介绍什么是面向对象和类。

❑C++中类的定义：介绍类的定义及其和结构体的差别。

❑C++类的实现：介绍类的成员函数的定义。

❑C++类的使用：介绍如何使用类来定义对象及其作用、可见域和生存期。

❑对象的创建和撤销：介绍构造及析构函数。

❑复制构造函数：介绍复制构造函数调用机制带来的问题及解决方法。

❑类中的特殊数据成员：介绍类中各种特殊数据成员。

❑特殊函数成员：介绍类中的静态成员函数。

❑对象的组织：介绍const、对象指针、this指针、对象数组及对象链表。

❑为对象分配内存：介绍new和delete进行对象的分配和释放。

8.1　面向对象基本概念

“对象”（object）是个抽象的概念，现实世界中的任何事物都可以看成是对象，动物、植物、摩托车和汽车等都是对象，对象之间有很大的差异，如人和汽车，但有的对象间却有相似之处，比如摩托车和自行车，它们有共同的特征—有轮子，同样的功能—人的交通工具，也有不同的特征，如“轮子个数”及“车子重量”等，基于此，可将“有轮子”、“可更换轮胎”及“能作为人的交通工具”抽象成一个类别（class），可称为“车”类，摩托车和自行车是该类别的对象。

类的提取往往是从两个方面来考虑的，一是特征（C++常称为“属性”）、另一个是功能（C++中常称为“行为”），具备类中定义的“属性”和“行为”的对象都是该类的对象，因此，我们可以说，电动车也是“车”类的对象。

第二篇讨论的是结构化的程序设计，要解决某一个问题，就要确定这个问题能够分解为哪些函数，数据能够分解为哪些基本的类型。结构化的思考方式是面向机器结构的，不是面向问题结构的，需要程序员在问题结构和机器结构之间建立联系。而面向对象的程序设计针对的是问题的结构，解决某个问题，要确定该问题由哪些对象组成，以及对象间的相互关系是什么？这样，思维方式更贴合现实，程序的组织也更清晰。

8.1.1　类的概念

类和对象的关系与“结构”和“结构体变量”的关系相似。

C++用类来描述对象，类是对现实世界中相似事物的抽象，同是“双轮车”的摩托车和自行车，有共同点，也有许多不同点。“车”类是对摩托车、自行车及汽车等相同点和不同点的提取与抽象，如图8.1所示。

图　8.1　“车”类示意图

类的定义分为两个部分：数据（相当于属性）和对数据的操作（相当于行为）。从程序设计的观点来说，类就是数据类型，是用户定义的数据类型，对象可以看成某个类的实例（某个类的变量），类和对象的关系与前面介绍的“结构”和“结构体变量”的关系相似，但又有不同，在本章稍后类的定义一节中会具体说明这一问题。

8.1.2　类是分层的

每一大类中可分成若干小类，也就是说类是分层的，如图8.2所示。可将所有的图形抽象成“图形”类，该类中共同的属性有很多，这里只取“颜色”这个属性，对所有图形而言，都可定义“显示”操作。同时，“图形”类可进一步分为“一维图形”类、“二维图形”类和其他类，根据形状的不同，“一维图形”类可进一步分为“直线”类和“折线”类，“二维图形”类又可分为“正方形”类和“圆”类。下层的类除了“继承”了上层类中定义的属性和行为外，还可增加新的属性和行为（如与“圆”类相比“二维图形”类增加了“圆心”和“半径”属性，增加了“求面积”这一行为），甚至可以在下层类中重新定义上层类已定义的属性和行为（如“直线”类、“折线”类、“正方形”类和“圆”类中都重新定义了“图形”类中已定义的“显示”操作）。

图　8.2　类是分层次的

8.1.3　类和对象的关系

对象需要从属性和行为两个方面进行描述，类是对象的封装。类的使用主要有以下几个步骤。

❑定义一个类，C++中分别用数据成员和函数成员来表现对象的属性和行为。类的定义强调“信息隐藏”，将实现细节和不允许外部随意访问的部分屏蔽起来。因此，在类定义中，需要用public或private将类成员区分开（此外，还有protected型的数据成员，稍后详细介绍），外界不能访问程序的private成员，只能访问public数据成员，对象间的信息传送也只能通过public成员函数，从而保证对象的数据安全。

❑类的实现，即进一步定义类的成员函数，使各个成员函数相互配合以实现接口对外提供的功能，类的定义和实现是由类设计者完成的。

❑通过该类声明一个属于该类的变量（即对象），并调用其接口（即public型的数据成员或函数成员），这是使用者的工作。

由此可以看出，类的设计者和使用者可能并非同一个人，换言之，在解决某一问题时，既可以自己定义并实现某个类，也可以使用别人定义和已经实现了的类。使用者在乎的只是该类提供了什么接口，能完成什么样的功能，而对类内的细节并不关心。这大大促进了代码的复用，先前设计好的类，可以不用修改或只做少量修改便可移植到新的程序中。

这很好理解，举个例子，对“电视机”类来说，类的定义相当于设计师决定电视机的属性，画出“蓝图”或者说“模型”，指明电视机应提供什么功能，例如是否可接数字信号等，而类的实现相当于电子工程师根据“蓝图”设计板卡，使电视能实现“蓝图”中提供的功能，这样，刚开始提出的“蓝图”就进化丰富成了可用于生产的“技术图纸（电路图）”，通过“电路图”便可生产电视。声明一个对象的过程相当于某个电视机的生产过程，生产完毕后，用户便可以使用电视机，调用其提供的接口实现特定的功能。对用户来讲，不关心电视机的内部工作原理，只关心其功能。

注意

类并不是对象，却相当于“图纸”，必须对类进行实例化，生成对象，才能调用对象的接口，实现想要的功能。

8.2　C++类的定义

先来看一下类是如何定义的，对一些通用的问题，前人已经定义好了很多的类，例如微软的MFC类库，程序员不必关心其内部细节，只要抱着“拿来主义”的态度就好，但对某些特殊问题来说，必须由自己提炼模型，进行类的定义。

8.2.1　类定义的基本形式

C++中使用关键字class定义一个类，其基本形式如下。

class类名

{

public：

公共成员函数

private：

私有成员函数

私有的数据成员定义

}；

注意

class和struct的定义一样，末尾的分号不要省略。

一般而言，类的数据成员都应设置为private，但这并非强制规定。外部只能访问类的公有数据成员和公有函数成员，常称公有函数成员为“类的接口”。private成员与public成员的先后次序无关紧要，推荐将public成员放在前面，因为对使用者而言，更关心的是类提供了哪些可访问的“接口”。

8.2.2　类定义示例

对一台计算机来说，它有如下特征。

❑属性：品牌、价格。

❑方法：输出计算机的属性。

代码8.1　实现了computer类的定义。

代码8.1　computer类的定义DefineAClass

＜-------------------------文件名：example801.h-------------------------------＞

01　class computer

02　{

03　private：//私有成员列表

04　char brand[20]；

05　float price；

06　public：//公共成员列表（接口）

07　void print（）；

08　void SetBrand（char*sz）；

09　void SetPrice（float pr）；

10　}；

【代码解析】根据对计算机类的分析，代码第1行定义了computer类，根据“信息隐藏”原则，数据成员一般不能由外部字节访问，只能通过public成员函数访问，因此，把成员brand（字符串）和price（浮点型）定义为private成员，而把print（）、SetBrand（）、SetPrice（）函数定义为public成员（computer类的接口），这样，用户便可以调用print（）函数输出brand和price信息，调用SetBrand（）函数和SetPrice（）函数修改brand和price的值。

类定义时，有以下几点需要特别注意。

（1）数据成员的类型符前不可使用auto、extern和register等，也不可在类定义时对数据成员初始化，如将“float price；”写成“float price=0；”，否则编译器会指出错误。

（2）private数据成员只能由类内的函数访问，而public可以由类外部的函数访问。在类定义时，关键字private和public出现的顺序和次数可以是任意的，代码8.1也可以是如下形式。

class computer

{

private：

char*brand；

public：

void print（）；

private：

float price；

public：

void SetBrand（char*sz）；

void SetPrice（float pr）；

}；

C++规定，类成员的访问权限默认是private，不加声明的成员默认是private的，因此，上述代码的第一个private完全可以省略。

（3）类的定义中提供的成员函数是函数的原型声明。

8.2.3　class和struct

class的定义看上去很像struct定义的扩展，事实上，类定义时的关键字class完全可以替换成struct，也就是说，第5章中介绍的结构体变量也可以有成员函数。class和struct的唯一区别在于：struct的默认访问方式是public，而class为private。

注意

通常使用class来定义类，而把struct用于定义只有数据对象，而没有成员函数的类。

8.3　C++类的实现

类的实现就是定义其成员函数的过程。类的实现有两种方式：一种是在类定义时同时完成成员函数的定义，另一种是在类定义的外部定义其成员函数。

8.3.1　在类定义时定义成员函数

成员函数的实现可以在类定义时同时完成，如代码8.2所示。

代码8.2　在类定义时实现成员函数DefineAndImplement1

＜------------------------文件名：example802.h--------------------------------＞

01　#include＜iostream＞

02　#include＜cstring＞

03　using namespace std；

04　class computer

05　{

06　private：

07　char brand[20]；

08　float price；

09　public：

10　void print（）//在类定义的同时实现了3个成员函数

11　{

12　cout＜＜"品牌："＜＜brand＜＜endl；

13　cout＜＜"价格："＜＜price＜＜endl；

14　}

15　void SetBrand（char*sz）

16　{

17　strcpy（brand,sz）；//字符串复制

18　}

19　void SetPrice（float pr）

20　{

21　price=pr；

22　}

23　}；

＜----------------------文件名：example802.cpp--------------------------------＞

24　#include"example802.h"//包含了computer类的定义

25　int main（）

26　{

27　computer com1；//声明创建一个类对象

28　com1. SetPrice（5000）；//调用public成员函数SetPrice（）设置price

29　com1. SetBrand（"Lenovo"）；//调用public成员函数SetBrand（）设置Brand

30　com1. print（）；//调用print（）函数输出信息

31　return 0；

32　}

输出结果如下所示。

品牌：Lenovo

价格：5000

【代码解析】从代码10～22行可以看出，此时，类computer定义中的成员函数print（）、SetBrand（）和SetPrice（）不再只是原型声明，而是一个完整的函数定义，有函数头、函数体和返回值，当然，成员函数也可以有参数，但和普通的函数相比，类中的成员函数可以访问同类中的private数据成员。

在类定义的同时定义的成员函数，编译器会自动将其定义为inline函数。而且除特殊指明外，成员函数操作的是同一对象中的数据成员，如代码8.2中的brand和price。

8.3.2　在类定义的外部定义成员函数

在类定义的外部定义成员函数时，应使用作用域操作符（：）来标识函数所属的类，即有如下形式。

返回类型类名：成员函数名（参数列表）

{

函数体

}

其中，返回类型、成员函数名和参数列表必须与类定义时的函数原型一致，这样，代码8.2可改写为代码8.3。

代码8.3　在类定义之外定义成员函数DefineAndImplement2

＜-------------------------文件名：example803.h-------------------------------＞

01　class computer//类定义，起到接口作用

02　{

03　private：

04　char brand[20]；

05　float price；

06　public：

07　void print（）；//3个public成员函数的原型声明

08　void SetBrand（char*sz）；

09　void SetPrice（float pr）；

10　}；

＜-----------------------文件名：example803.cpp-------------------------------＞

11　#include"example803.h"//包含computer类定义

12　#include＜iostream＞

13　#include＜cstring＞

14　using namespace std；

15　void computer：print（）//成员函数的实现，注意作用域操作符的使用

16　{

17　cout＜＜"品牌："＜＜brand＜＜endl；

18　cout＜＜"价格："＜＜price＜＜endl；

19　}

20　void computer：SetBrand（char*sz）

21　{

22　strcpy（brand,sz）；//字符串复制

23　}

24　void computer：SetPrice（float pr）

25　{

26　price=pr；

27　}

【代码解析】由代码第15～27行可以看出，为方便项目的组织管理，常将类的定义放在头文件（h文件）中，而将类的实现放在同名的cpp文件中，这样，只要使用#include命令将类定义的头文件包含进来即可使用定义好的类，但这并非是强制性的，应确保其在本编译单元内，在使用某个类前，该类已经定义。关于编译单元的相关内容，请参考第20章的内容。

将成员函数定义在类定义之外时，在返回值类型前使用关键字inline，同样可使成员函数称为内联函数。需要注意的是，此时类定义和inline函数必须在同一文件中。

8.4　C++类的使用

定义了一个类之后，便可以如同用int和double等类型符声明简单变量一样，创建该类的对象，称为类的实例化。由此看来，类的定义实际上是定义了一种类型，类不接收或存储具体的值，只作为生成具体对象的“蓝图”，只有将类实例化，创建对象（声明类的变量）后，系统才会为对象分配存储空间。

8.4.1　声明一个对象

代码8.4　使用类定义声明了一个对象，并利用对象名实现了public成员函数的调用。

代码8.4　使用类声明一个对象ClassVariable

＜---------------------------文件名：computer.h-------------------------------＞

01　class computer//类定义

02　{

03　private：

04　char brand[20]；

05　float price；

06　public：

07　void print（）；

08　void SetBrand（char*sz）；

09　void SetPrice（float pr）；

10　}；

＜---------------------------文件名：computer.cpp-----------------------------＞

11　#include"computer.h"//包含类定义

12　#include＜iostream＞

13　using namespace std；

14　void computer：print（）//成员函数的实现

15　{

16　cout＜＜"品牌："＜＜brand＜＜endl；

17　cout＜＜"价格："＜＜price＜＜endl；

18　}

19　void computer：SetBrand（char*sz）

20　{

21　strcpy（brand,sz）；//字符串复制

22　}

23　void computer：SetPrice（float pr）

24　{

25　price=pr；

26　}

＜-------------------------文件名：example804.cpp-----------------------------＞

27　#include"computer.h"//定义了类computer

28　int main（）//主函数

29　{

30　computer com1；//声明了computer类对象（或说类变量）com1

31　com1. SetBrand（"Lenovo"）；//调用public成员函数SetBrand设置品牌brand

32　com1. SetPrice（8000）；//调用public成员函数SetPrice设置品牌price

33　com1. print（）；//信息输出

34　return 0；

35　}

输出结果如下所示。

品牌：Lenovo

价格：8000

【代码解析】该代码由3个文件组成，分别是c o m p u t e r.h（c o m p u t e r类的定义）、computer.cpp（computer类的实现）和example804.cpp（main（）函数），在example804.cpp中，语句“#include"computer.h"”引入computer类的定义，使computer类名在example804.cpp中可见，只有这样，才能在example804.cpp代码第30行使用computer类名声明该类型的对象（变量）com1，并调用com1的公用成员函数，实现对其private数据成员的读写。

通过“对象名.公共成员函数（参数表）”的形式就可以调用对象成员函数，通过“对象名.公共数据成员”就可引用对象的数据成员。

8.4.2　对象的作用域、可见域和生存期

对象的作用域、可见域和生存期与普通变量，如int型变量的作用域、可见域和生存期并无不同，对象同样有局部、全局和类内（稍后就将对对象成员进行介绍）之分，对于在代码块中声明的局部对象，在代码块执行结束退出时，对象会被自动撤销，对应的内存会自动释放（当然，如果对象的成员函数中使用new或malloc申请了动态内存，却没有使用delete或free命令释放，那么在对象撤销时，这部分动态内存不会自动释放，会造成内存泄露）。

8.5　对象的创建和撤销

代码8.4　中，通过自定义的公共成员函数SetBrand是SetPrice实现对对象数据成员的初始化。实际上，C++为类提供了两种特殊的成员函数，一是构造函数，在对象创建时自动调用，用以完成对象成员变量等的初始化及其他操作（如为指针成员动态申请内存空间等）；另一个是析构函数，在对象撤销时自动调用，用以执行一些清理任务，如释放成员函数中动态申请的内存等。

8.5.1　构造函数的作用

当创建对象时，自动调用构造函数。构造函数有其独特的地方，如函数的名字与类名相同，以及没有返回类型和返回值。其主要工作有以下3个。

❑给对象一个标识符。

❑为对象数据成员开辟内存空间。

❑完成对象数据成员的初始化（函数体内的工作，由程序员完成）。

上述3点也说明了构造函数的执行顺序，在执行函数体之前，构造函数已经为对象的数据成员开辟了内存空间，这时，在函数体内对数据成员的初始化便顺理成章了。

如果用户没有显式地定义构造函数，编译器将为类生成“默认构造函数”，默认构造函数不能完成对象数据成员的初始化，只是给对象标识符，同时为对象数据成员开辟内存空间，代码8.4中采用的是默认构造函数，所以，需要另外定义成员函数以改写数据成员brand和price。默认的构造函数是无参的，下述代码中，point类定义了显式的无参构造函数。

class point

{

private：

int xPos；

int yPos；

public：

point（）；

}；

point：point（）

{

xPos=0；

yPos=0；

}

当然，作为一种成员函数，构造函数是在类定义的同时进行定义还是在类定义的外部进行定义都可以，关于构造函数的补充内容请参见后续的讲解。

8.5.2　构造函数可以有参数

编译器自动生成的默认构造函数是无参的，实际上，构造函数可以接收参数，在对象创建时提供更大的自由度，如代码8.5所示。

代码8.5　有参构造函数Constructor1

＜------------------------------文件名：point.h------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class point//point类定义，在定义同时实现其成员函数

04　{

05　private：//私有成员，分析该代表x轴和y轴坐标

06　int xPos；

07　int yPos；

08　public：

09　point（int x,int y）//有参构造函数

10　{

11　cout＜＜"对象创建时构造函数被自动调用"＜＜endl；

12　xPos=x；

13　yPos=y；

14　}

15　void print（）//输出信息

16　{

17　cout＜＜"xPos："＜＜xPos＜＜"，yPos："＜＜yPos＜＜endl；

18　}

19　}；

＜---------------------------文件名：example805.cpp---------------------------＞

20　#include"point.h"

21　int main（）

22　{

23　point pt1（3，4）；//调用有参构造函数声明point类变量（类对象）pt1

24　pt1. print（）；//输出pt1的信息

25　return 0；

26　}

输出结果如下所示。

对象创建时构造函数被自动调用

xPos：3，yPos：4

【代码解析】代码第9行显式定义了构造函数“point（int x,int y）”，这样，编译器便不会自动生成默认构造函数，这时，必须使用如“point pt1（3，4）；”创建point类的对象pt1，并对其中的数据成员xPos和yPos进行初始化，如果仍采用“point pt1；”的形式，编译器会报错，提示信息如下所示。

error C2512：'point'：no appropriate default constructor available

8.5.3　构造函数支持重载

前面说，一旦程序员为一个类定义了构造函数，编译器便不会为该类自动生成默认构造函数，因此，如果还想使用无参的构造函数，如“point pt1；”的形式必须在类定义中显式定义一个无参构造函数。这样，构造函数就会出现两个，会不会有问题呢？不会，构造函数支持重载，在创建对象时，会根据传递的具体参数决定采用哪个构造函数。

来看示例代码8.6。

代码8.6　构造函数重载与无参构造函数Constructor2

＜-------------------------------文件名：point.h-----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class point//point类定义，在定义的同时实现其成员函数

04　{

05　private：//私有成员，分析该代表x轴和y轴坐标

06　int xPos；

07　int yPos；

08　public：

09　point（int x,int y）//有参构造函数

10　{

11　cout＜＜"有参构造函数的调用"＜＜endl；

12　xPos=x；

13　yPos=y；

14　}

15　point（）//无参构造函数

16　{

17　cout＜＜"无参构造函数的调用"＜＜endl；

18　xPos=0；

19　yPos=0；

20　}

21　void print（）//输出信息

22　{

23　cout＜＜"xPos："＜＜xPos＜＜"，yPos："＜＜yPos＜＜endl；

24　}

25　}；

＜-----------------------文件名：example806.cpp-------------------------------＞

26　#include"point.h"

27　int main（）

28　{

29　point pt1（3，4）；//调用有参构造函数声明point类变量（类对象）pt1

30　pt1. print（）；//输出pt1的信息

31　point pt2；//调用无参构造函数声明point类变量（类对象）pt2

32　pt2. print（）；//输出pt2的信息

33　return 0；

34　}

输出结果如下所示。

有参构造函数的调用

xPos：3，yPos：4

无参构造函数的调用

xPos：0，yPos：0

【代码解析】代码8.6在代码8.5的基础上重载了构造函数point（）（代码第15行），显式给出了无参构造函数，这样，便可在程序中使用如“point pt2；”的形式声明point的对象pt2。

8.5.4　构造函数允许按参数默认方式调用

代码8.5中的构造函数可以作如下定义。

point（int x=0，int y=0）

{

cout＜＜"对象创建时构造函数被自动调用"＜＜endl；

xPos=x；

yPos=y；

}此时，允许在创建对象时默认参数，下列声明语句都是合法的。

point pt；

point pt（3）；

point pt（3，4）；

正如在第6章函数重载一节中的讲述，重载和参数默认的关系应处理好，重载经常用在参数类型不一致时，如果仅仅是参数个数不一致，推荐使用参数默认方式。如此看来，代码8.6似乎并不科学，但该代码的目的是为了说明构造函数可重载。

说明

避免重载或参数默认调用不当带来的问题，否则编译器无法判断究竟应当调用哪个构造函数，提示出错。

8.5.5　初始化表达式

除了在构造函数体内初始化数据成员外，还可以通过成员初始化表达式来完成。成员初始化表达式可用于初始化类的任意数据成员（后面要介绍的static数据成员除外），该表达式由逗号分隔的数据成员组成，初值放在一对圆括号中。只要将成员初始化表达式放在构造函数的头和体之间，并用冒号将其与构造函数的头分隔开，便可实现数据成员表达式中元素的初始化，对代码8-6而言有下述等价代码。

point（int x,int y）

{

cout＜＜"有参构造函数的调用"＜＜endl；

xPos=x；

yPos=y；

}等价于

point（int x,int y）：xPos（x），yPos（y）

{

cout＜＜"有参构造函数的调用"＜＜endl；

}

“xPos（x），yPos（y）”即是成员初始化表达式，用成员初始化表达式初始化类成员与用构造函数初始化类成员的区别在于：前者的初始化是在构造函数体被执行以前进行的。对普通类型的变量来说，采用构造函数体内赋值初始化和成员初始化表达式进行初始化几乎是等价的，但对一些特殊的数据成员，两种初始化方式会有一定的差异。

注意

由成员初始化表达式可以看，C++内建的数据类型，如int和double等也可以看做一种类，通过该类声明的变量即是该类型的对象。

每个成员在成员初始化表中只能出现一次，初始化的顺序不是由名字在初始化表中的顺序来决定的，而是由成员在类中被声明的顺序决定的。理解这一问题有助于避免意想不到的错误发生，如代码8.7所示。

代码8.7　成员初始化表顺序Constructor3

＜---------------------------文件名：point.h----------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class point

04　{

05　private：

06　int yPos；

07　int xPos；

08　public：

09　point（int x）：xPos（x），yPos（xPos）//初始化表取决于成员声明的顺序

10　{

11　}

12　void print（）

13　{

14　cout＜＜"xPos："＜＜xPos＜＜"，yPos："＜＜yPos＜＜endl；

15　}

16　}；

＜------------------------文件名：example807.cpp------------------------------＞

17　#include"point.h"

18　int main（）

19　{

20　point pt1（3）；//调用有参构造函数声明变量pt1

21　pt1. print（）；

22　return 0；

23　}

输出结果如下所示。

xPos：3，yPos：-858993460

【代码解析】从代码8.7的输出结果看，yPos并没有像预想的那样被初始化为3，问题出在代码第9行的语句上，从成员初始化表的字面顺序看，xPos用x初始化，yPos用xPos初始化，似乎并没有问题。实际上，成员初始化的顺序取决于成员在类定义中的顺序，yPos排在xPos前，因此，“yPos（xPos）”先被执行，xPos（x）后被执行，这意味着yPos的值取决于xPos内存开辟成功后的值，并不等于x。

技巧如果可能（一些变量只能在初始化表中进行初始化，稍后会有介绍），将类似于代码8.7中yPos这样的成员放在构造函数体内初始化。

8.5.6　析构函数

构造函数在创建对象时被系统调用，而析构函数在撤销对象时被自动调用，相比构造函数，析构函数要简单得多。析构函数有如下特点。

❑与类同名，之前冠以波浪号，以区别于构造函数。

❑析构函数没有返回类型，也不能指定参数，因此，析构函数只能有一个，不能被重载。

❑对象超出其作用域被销毁时，析构函数会被自动调用。与构造函数不同的是，程序员可根据需要显式调用析构函数，以撤销对象，释放对象所占据的内存空间。

如果用户没有显式地定义构造函数，编译器将为类生成“默认析构函数”，默认析构函数是个空的函数体，只清除类的数据成员所占据的空间，但对类的函数成员通过new和malloc动态申请的内存无能为力，因此，对于动态申请的内存，应在类的析构函数中通过delete或free进行释放，这样能有效避免对象撤销造成的内存泄露，见代码8.8。

代码8.8　用析构函数实现对象动态内存的释放Destructor

＜---------------------------文件名：computer.h-------------------------------＞

01　#include＜iostream＞

02　#include＜cstring＞

03　using namespace std；

04　class computer

05　{

06　private：

07　char*brand；//指针成员

08　float price；

09　public：

10　computer（const char*sz,float p）

11　{

12　brand=new char[strlen（sz）+1]；//对象创建时为brand分配动态内存

13　strcpy（brand,sz）；//字符串复制

14　price=p；

15　}

16　～computer（）

17　{

18　delete[]brand；//对象撤销时，释放内存，避免泄露

19　cout＜＜"清理现场"＜＜endl；

20　}

21　void print（）//信息输出

22　{

23　cout＜＜"品牌："＜＜brand＜＜endl；

24　cout＜＜"价格："＜＜price＜＜endl；

25　}

26　}；

＜------------------------文件名：example808.cpp------------------------------＞

27　#include"computer.h"

28　int main（）

29　{

30　computer comp（"Dell"，7000）；//调用构造函数声明computer变量

31　comp. print（）；//信息输出

32　return 0；

33　}

输出结果如下所示。

品牌：Dell

价格：7000

清理现场

【代码解析】computer类的数据成员brand是字符型指针，在创建computer对象时，虽然该指针变量的内存被分配，但该指针指向的内存单元并没有分配，换句话说，该指针并没有被合法初始化，所以，在代码第12行构造函数中采用了“brand=new char[strlen（sz）+1]；”为该指针初始化，开辟了一块动态内存用以复制字符串sz。

内存空间的大小之所以是“strlen（sz）+1”，是因为C风格字符串处理函数strlen返回的长度是字符串的实际字符数，并不包括结尾的'\0'，因此，需要动态申请的内存个数应做加1处理。

本例中，在main（）函数执行完毕后，computer类对象comp被撤销，系统自动调用其析构函数，输出提示信息“清理现场”，并释放析构函数中动态申请的内存。

注意

析构函数在对象销毁前被自动调用，对象何时销毁取决于其生存期。例如，全局对象是在程序运行结束时销毁，自动对象是在离开其作用域时销毁，而动态对象则是在使用delete运算符时销毁。析构函数的调用顺序与构造函数的调用顺序相反。

8.5.7　显式调用析构函数

程序员不能显式调用构造函数，但却可以调用析构函数控制对象的撤销，以更高效地利用内存，如下所示。

#include"computer.h"

int main（）

{

computer comp（"Dell"，7000）；

comp.print（）；

comp.～computer（）；//显式调用析构函数，合法，comp被撤销

return 0；

}

8.6　复制构造函数

C++中经常使用一个常量或变量初始化另一个变量，如下所示。

double x=5.0；

double y=x；

使用类创建对象时，构造函数被自动调用以完成对象的初始化，那么能否像简单变量的初始化一样，直接用一个对象来初始化另一个对象呢？答案是肯定的，以代码8.5中定义的point类为例。

point pt1（2，3）；

point pt2=pt1；后一个语句也可写成如下形式。

point pt2（pt1）；上述语句用pt1初始化pt2，相当于将pt1中每个数据成员的值复制到pt2中，这是表面现象，实际上，系统调用了一个复制构造函数。如果类定义中没有显式定义该复制构造函数时，编译器会隐式定义一个默认的复制构造函数，它是一个inline或public的成员函数，其原型形式如下所示。

point：point（const point＆）；

复制构造函数与构造函数的不同之处在于形参，复制构造函数是本类对象的引用，其功能是将一个对象的每一个成员复制到另一个对象对应的成员当中，默认的构造函数相当于内存块的复制。需要注意的是形参中的const并非是必须的，但是推荐使用它，这样可避免对参数对象的修改。

当然，也可以显式定义复制构造函数以完成其他的操作。在有些情况下，系统提供的默认复制构造函数足以满足需要，自行定义该函数似乎没有必要，比如代码8.5中定义的point类。但在另外一些时候，则必须要显式定义构造函数，比如代码8.8中的computer类，这两个类有什么不同呢？答案马上揭晓。

注意

一旦程序员显式定义了复制构造函数，编译器便不再会自动提供默认的复制构造函数。

8.6.1　复制构造函数调用机制

复制构造函数的调用示例如代码8.9所示。

代码8.9　复制构造函数调用机制CopyConstructor

＜-----------------------------文件名：point.h--------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class point

04　{

05　private：

06　int xPos；

07　int yPos；

08　public：

09　point（int x=0，int y=0）

10　{

11　cout＜＜"调用构造函数"＜＜endl；

12　xPos=x；

13　yPos=y；

14　}

15　point（const point＆pt）//复制构造函数的定义及实现

16　{

17　cout＜＜"调用复制构造函数"＜＜endl；

18　xPos=pt. xPos；

19　yPos=pt. yPos；

20　}

21　void print（）

22　{

23　cout＜＜"xPos："＜＜xPos＜＜"，yPos："＜＜yPos＜＜endl；

24　}

25　}；

＜--------------------------文件名example809.cpp------------------------------＞

26　#include"point.h"

27　int main（）

28　{

29　point pt1（3，4）；

30　pt1. print（）；

31　point pt2=pt1；//等价于point pt2（pt1），调用复制构造函数

32　pt2. print（）；

33　point pt3；

34　pt3. print（）；

35　point pt4（pt3）；//等价于point pt4=pt3，调用复制构造函数

36　pt4. print（）；

37　return 0；

38　}

输出结果如下所示。

调用构造函数

xPos：3，yPos：4

调用复制构造函数

xPos：3，yPos：4

调用构造函数

xPos：0，yPos：0

调用复制构造函数

xPos：0，yPos：0

【代码解析】即使去掉复制构造函数的定义，编译器也会自动添加默认构造函数，该代码仍能正确运行，但为了更清晰地展现复制构造函数的调用机制，在代码第15行，point类中显式定义了复制构造函数，为了区分带默认参数的构造函数和复制构造函数，在其中分别添加了输出语句“cout＜＜"调用构造函数"＜＜endl；”和“cout＜＜"调用复制构造函数"＜＜endl；”，从输出结果来看，“point pt1（3，4）；”和“point pt3；”都是由构造函数完成初始化的，而“point pt2=pt1；”和“point pt4（pt3）；”并没有执行构造函数，而是执行了复制构造函数，用对象pt1和pt3分别为pt2和pt4初始化。

提示“point pt2=pt1；”等价于“point pt2（pt1）；”。

8.6.2　默认复制构造函数带来的问题

默认的复制构造函数并非万金油，在一些情况下，必须由程序员显式定义默认复制构造函数，先来看一段错误的代码示例，如代码8.10所示。

代码8.10　默认的复制构造函数带来的问题ProblemOfCopyConstructor

＜--------------------------文件名：computer.h--------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class computer

04　{

05　private：

06　char*brand；

07　float price；

08　public：

09　computer（const char*sz,float p）

10　{

11　brand=new char[strlen（sz）+1]；//构造函数中为brand指针动态分配内存

12　strcpy（brand,sz）；

13　price=p；

14　}

15　～computer（）

16　{

17　delete[]brand；//析构函数中释放申请到的动态内存

18　cout＜＜"清理现场"＜＜endl；

19　}

20　void print（）

21　{

22　cout＜＜"品牌："＜＜brand＜＜endl；

23　cout＜＜"价格："＜＜price＜＜endl；

24　}

25　}；

＜--------------------------文件名：example810.cpp----------------------------＞

26　#include"computer.h"

27　int main（）

28　{

29　computer comp1（"Dell"，7000）；//声明computer类对象comp并初始化

30　comp1. print（）；

31　computer comp2（comp1）；//调用默认的复制构造函数

32　comp2. print（）；

33　return 0；

34　}

【代码解析】编译链接没有错误，输出时却出现了如图8.3所示的错误提示框。

按默认复制构造函数的语义，代码第31行语句“computer comp2（comp1）；”等价于：

comp2.brand=comp1.brand；

comp2.price=comp1.price；

图　8.3　默认的构造函数带来的问题

其中，后一句“comp2.price=comp1.price；”是没有问题的，但“comp2.brand=comp1.brand”却给程序带来了致命的问题，经过赋值操作后，两个对象中的指针指向的是同一片动态内存，当comp1和comp2撤销时，其释放函数都要释放同一块动态内存，可是，两个对象撤销有先有后，一旦一个对象被撤销，另一个对象的brand指针便是“野指针”，使用该指针再次释放同一块动态内存会引发内存错误。不仅仅是重复释放内存的问题，默认复制构造函数有可能给程序带来不易发觉的错误，试分析下述代码。

#include"computer.h"

int main（）

{

computer comp1（"Dell"，7000）；

if（true）

{

computer comp2（comp1）；

comp2.print（）；

}

comp1.print（）；//comp1.brand指向的动态内存此时已经被释放

return 0；

}

由于comp2是在if结构中定义的局部对象，因此，在if结构退出时，comp2被撤销，系统自动调用其析构函数，释放了comp2.brand所指向的动态内存，由于comp2.brand和comp1.brand的值相同，此时comp1.brand已无所指，成了“野指针”，此时对该指针的读写操作都会引发无法预料的错误。

8.6.3　解决方案—显式定义复制构造函数

如果类中含有指针型的数据成员、需要使用动态内存，程序员最好显式定义自己的复制构造函数，避免各种可能出现的内存错误，见代码8.11。

代码8.11　显式定义复制构造函数DefineOwnCopyConstructor

＜--------------------------文件名：computer.h-------------------------------＞

01　#include＜iostream＞

02　#include＜cstring＞

03　using namespace std；

04　class computer

05　{

06　private：

07　char*brand；

08　float price；

09　public：

10　computer（const char*sz,float p）

11　{

12　brand=new char[strlen（sz）+1]；

13　strcpy（brand,sz）；

14　price=p；

15　}

16　computer（const computer＆cp）//自定义复制构造函数

17　{

18　brand=new char[strlen（cp. brand）+1]；//重新为brand开辟于cp.brand

19　//同等大小的动态内存

20　strcpy（brand,cp. brand）；//字符串复制

21　price=cp. price；

22　}

23　～computer（）

24　{

25　delete[]brand；

26　cout＜＜"清理现场"＜＜endl；

27　}

28　void print（）

29　{

30　cout＜＜"品牌："＜＜brand＜＜endl；

31　cout＜＜"价格："＜＜price＜＜endl；

32　}

33　}；

＜----------------------文件名：example811.cpp--------------------------------＞

34　#include"computer.h"

35　int main（）

36　{

37　computer comp1（"Dell"，7000）；//调用有参构造函数声明computer类

38　//对象comp1，并初始化

39　comp1. print（）；

40　//调用复制构造函数声明computer类对象comp2，并用comp1为其初始化

41　computer comp2（comp1）；comp2. print（）；

42　return 0；

43　}

输出结果如下所示。

品牌：Dell

价格：7000

品牌：Dell

价格：7000

清理现场

清理现场

【代码解析】在代码第16行，显式定义了computer类的复制构造函数，没有采用“brand=cp.brand”这种直接指针赋值，而是重新申请了一块动态内存，使用库函数strcpy（）实现了字符串的复制，这样，comp1.brand和comp2.brand指向不同的两块动态内存，避免了可能出现的错误。

8.6.4　关于构造函数和复制构造函数

复制构造函数可以看成是一种特殊的构造函数，这里暂时区分为“复制构造函数”和“普通构造函数”，因此，它们也支持初始化表达式。

创建对象时，只有一个构造函数会被系统自动调用，具体调用哪个取决于创建对象时的参数。C++对编译器何时提供默认构造函数和默认复制构造函数有着独特的规定，如表8.1所示。

不管是普通构造函数还是复制构造函数，构造函数是由系统自动调用的，程序中不能直接调用构造函数。

下面来看一个具有实质性的问题：普通构造函数和复制构造函数到底做了什么？主要体现在以下几个步骤中。

（1）在创建对象时，C++根据传递的参数选择一个合适的构造函数，如果没有匹配的构造函数，编译器会报错，否则执行该构造函数。

（2）进入构造函数后，首先就是为成员变量开辟内存空间，对普通变量（如前面介绍的int、double、char、指针等）来说，能够直接完成空间的开辟，假如数据成员也是一个类的对象，情况稍显复杂，此时要检查成员初始化表，根据其中传递给对象成员的参数检查是否有可用的构造函数（在对象成员的类中定义的构造函数），由对象成员的构造函数为其开辟内存空间，如果成员初始化表中没有为该对象成员传递参数，则该对象的无参构造函数或所有参数都由默认值的构造函数构成，也可能是系统为对象所在类提供的默认构造函数被调用，用以开辟对象空间。

（3）按照类中定义的成员变量顺序，执行成员初始化表。应注意的是并非按成员初始化表的语句先后执行成员初始化，其只取决于类中定义的成员顺序。如果没有成员初始化表，本步骤跳过，直接到步骤4。

（4）执行构造函数体，完成其他操作。

由此看来，默认的构造函数只完成了其中一个功能，那就是步骤2，为成员变量开辟空间，需要说明的是如果类中有对象成员，那么该对象所在的类中必须定义了无参构造函数或所有参数都有默认值的构造参数，或者干脆由系统提供的构造参数，其他情况下，均不合法，对象成员所占内存空间无法开辟，编译器报错。

缺省的复制构造函数似乎没有这么麻烦，编译器根据源对象为目的对象开辟空间，并将源对象中的成员按“内存单元复制”的方式复制到目的对象中，经过这个操作后，源对象和目的对象除了地址不同外，各个成员的取值都是相同的。

8.7　特殊数据成员

有几类特殊的数据成员，其初始化及使用方式与前面介绍的普通数据成员有所不同，下面展开后具体讨论。

8.7.1　const数据成员

数据成员可以由const修饰，这样，一经初始化，该数据成员便具有“只读属性”，在程序中无法对其值进行修改。

事实上，在构造函数体内或复制构造函数体内初始化const数据成员是非法的，如代码8.12所示。

代码8.12　在构造函数体内无法初始化const数据成员ConstMember1

＜-------------------------------文件名：point.h------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class point

04　{

05　private：

06　const int xPos；//符号常量成员xPos和yPos

07　const int yPos；

08　public：

09　point（int x,int y）

10　{

11　xPos=x；//错误，无法直接赋值

12　yPos=y；

13　}

14　point（const point＆pt）

15　{

16　xPos=pt. xPos；

17　yPos=pt. yPos；

18　}

19　void print（）

20　{

21　cout＜＜"xPos："＜＜xPos＜＜"，yPos："＜＜yPos＜＜endl；

22　}

23　}；

＜-------------------------------文件名：example812.cpp-----------------------＞

24　#include"point.h"

25　int main（）

26　{

27　point pt1（3，4）；//调用有参构造函数

28　pt1. print（）；

29　point pt2（pt1）；//调用复制构造函数

30　pt2. print（）；

31　return 0；

32　}编译连接，编译器报错如下所示。

error C2758：'xPos'：must be initialized in constructor base/member initializer list

error C2758：'yPos'：must be initialized in constructor base/member initializer list

error C2166：l-value specifies const object

【代码解析】这是由于代码第11～12行造成的，const型数据成员只能通过成员初始化表达式进行初始化，所以报错。修改成如代码8.13所示。

代码8.13　使用成员初始化表达式初始化const数据成员ConstMember2

＜----------------------------文件名：point.h---------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class point

04　{

05　private：

06　const int xPos；

07　const int yPos；

08　public：

09　point（int x,int y）：xPos（x），yPos（y）//const数据成员只能在初始化表中初始化

10　{

11　}

12　//const数据成员只能在初始化表中进行初始化，对复制构造函数来说同样如此

13　point（const point＆pt）：xPos（pt. xPos），yPos（pt.yPos）

14　{

15　}

16　void print（）

17　{

18　cout＜＜"xPos："＜＜xPos＜＜"，yPos："＜＜yPos＜＜endl；

19　}

20　}；

＜----------------------------文件名：example813.cpp--------------------------＞

21　#include"point.h"

22　int main（）

23　{

24　point pt1（3，4）；//调用有参构造函数

25　pt1. print（）；

26　point pt2（pt1）；//调用复制构造函数

27　pt2. print（）；

28　return 0；

29　}

输出结果如下所示。

xPos：3，yPos：4

xPos：3，yPos：4

【代码解析】代码第9行，在成员初始化表达式中对point类的数据成员xPos和yPos进行了初始化，该操作发生在构在函数体执行之前，这样，类中的其他成员函数只能对xPos和yPos进行只读访问，无法修改其值。

如果类定义中含有const数据成员，程序员必须显式定义构造函数，使用编译器提供的默认构造函数无法完成对const成员的初始化。而缺省的复制构造函数却可以完成const成员的初始化，因此，代码8.13中定义的复制构造函数显得有些多余。

注意

在第3章曾提到，声明一个数组时，可使用诸如5、10或const等整型常量指明数组的大小，那么类中的const整型数据成员能否用于定义数据成员呢？不能，这是因为编译时，const数据成员还没有确定的值，构造函数要到程序运行以后才会被调用。

8.7.2　引用成员

对于引用类型的数据成员，同样只能通过成员初始化表达式进行初始化，见代码8.14。

代码8.14　引用数据成员的初始化RefMember

＜--------------------------------文件名：point.h-----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class point

04　{

05　private：

06　int xPos；

07　int yPos；

08　int＆ref1；

09　double＆ref2；

10　public：

11　//引用成员的初始化同样要放在初始化表中

12　point（int x,int y,double＆z）：ref1（xPos），ref2（z）

13　{

14　xPos=x；

15　yPos=y；

16　}

17　//复制构造函数与此一致：引用成员的初始化同样要放在初始化表中

18　point（const point＆pt）：ref1（pt. ref1），ref2（pt.ref2）

19　{

20　xPos=pt. xPos；

21　yPos=pt. yPos；

22　}

23　void print（）

24　{

25　cout＜＜"xPos："＜＜xPos＜＜"，yPos："＜＜yPos＜＜endl；

26　cout＜＜"ref1："＜＜ref1＜＜"，ref2："＜＜ref2＜＜endl；

27　}

28　void SetX（int x）

29　{

30　xPos=x；

31　}

32　}；

＜--------------------文件名：example814.cpp----------------------------------＞

33　#include"point.h"

34　int main（）

35　{

36　double outInt=5. 0；

37　point pt1（3，4，outInt）；//有参构造函数

38　pt1. print（）；

39　point pt2（pt1）；//复制构造函数

40　pt2. print（）；

41　cout＜＜"改变pt1中的x后"＜＜endl；

42　pt1. SetX（7）；

43　pt1. print（）；

44　pt2. print（）；

45　outInt=6；

46　cout＜＜"outInt变化后："＜＜endl；

47　pt1. print（）；

48　pt2. print（）；

49　return 0；

50　}

输出结果如下所示。

xPos：3，yPos：4

ref1：3，ref2：5

xPos：3，yPos：4

ref1：3，ref2：5

改变pt1中的x后

xPos：7，yPos：4

ref1：7，ref2：5

xPos：3，yPos：4

ref1：7，ref2：5

outInt变化后：

xPos：7，yPos：4

ref1：7，ref2：6

xPos：3，yPos：4

ref1：7，ref2：6

【代码解析】得到了看似奇怪的结果，前面讲过，引用可以看做变量的别名。对类中的引用成员来说，其既可以是类内同类型成员的别名，也可以是外部某个同类型变量的别名。

代码第12行的构造函数“point（int x,int y,double＆z）：ref1（xPos），ref2（z）”指明：ref1是本对象中成员xPos的别名，而ref2是外部变量的引用，但对复制构造函数来说，情况有所变化，使用“point pt2（pt1）；”创建pt2后，pt2的ref1不是pt2.xPos的别名，而是pt1.xPos的别名，pt2.ref2和pt1.ref2一样，都是外部变量outInt的别名，这才有了如上的输出结果。

实际上，C++将类中的引用成员当做一个该类型的指针来对待。在复制构造函数中简单地用“ref1（pt.ref1）”相当于指针的简单赋值，导致两个引用“指向”同一个变量。

将代码8.14中的复制构造函数头改为如下形式，编译运行，体会其中的差异。

point（const point＆pt）：ref1（xPos），ref2（pt.ref2）

上述代码真正使pt2.ref2成为了pt2.xPos的别名，当然，代码8.14给出的复制构造函数定义等价于默认的复制构造函数，因此，如果类中含有引用类型的数据成员（尤其是引用本对象内的成员时），不要使用默认的复制构造函数，应当显式地给出复制构造函数，避免程序出现无法预料的错误。

8.7.3　类对象成员

类数据成员也可以是另一个类的对象，比如，一个直线类对象中包含两个point类对象，在直线类对象创建时需要初始化两个point对象，代码8.15中是对直线类和point类的实现。

代码8.15　类对象成员的初始化ClassMember1

＜------------------------文件名：PointAndLine.h-----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class point//点类的定义

04　{

05　private：

06　int xPos；

07　int yPos；

08　public：

09　point（int x=0，int y=0）//带默认调用的构造函数

10　{

11　cout＜＜"点的构造函数被执行"＜＜endl；

12　xPos=x；

13　yPos=y；

14　}

15　point（const point＆pt）//复制构造函数

16　{

17　cout＜＜"点的复制构造函数被执行"＜＜endl；

18　xPos=pt. xPos；

19　yPos=pt. yPos；

20　}

21　void print（）

22　{

23　cout＜＜"（"＜＜xPos＜＜"，"＜＜yPos＜＜"）"；

24　}

25　}；

26　class line//line类的定义

27　{

28　private：

29　point pt1；//point类对象作为line类成员，此处若写成point pt1（3，4），错

30　point pt2；

31　public：

32　line（int x1，int y1，int x2，int y2）：pt1（x1，y1），pt2（x2，y2）//line对象的有参构造函数

33　{

34　cout＜＜"线的构造函数被执行"＜＜endl；

35　}

36　line（const line＆ll）：pt1（ll. pt1），pt2（ll.pt2）//line对象的复制构造函数

37　{

38　cout＜＜"线的复制构造函数被执行"＜＜endl；

39　}

40　void draw（）

41　{

42　pt1. print（）；

43　cout＜＜"to"；

44　pt2. print（）；

45　cout＜＜endl；

46　}

47　}；

＜---------------------文件名：example815.cpp---------------------------------＞

48　#include"PointAndLine.h"

49　int main（）

50　{

51　line l1（1，2，3，4）；//调用有参构造函数

52　l1. draw（）；

53　line l2（l1）；//调用复制构造函数

54　l2. draw（）；

55　return 0；

56　}

输出结果如下所示。

点的构造函数被执行

点的构造函数被执行

线的构造函数被执行

（1，2）to（3，4）

点的复制构造函数被执行

点的复制构造函数被执行

线的复制构造函数被执行

（1，2）to（3，4）

【代码解析】在line类定义时，和类中的普通变量不能用如“int xPos=0；”初始化一样，在代码第29行，不能使用如“point pt1（3，4）；”的形式完成pt1的初始化。point类中的成员xPos和yPos都是private成员。因此，在line类内无法采用诸如“pt1.xPos”的形式对这些成员进行访问，所以，对pt1和pt2的初始化必须放在初始化表达式中进行。

“line l1（1，2，3，4）；”创建了对象l1，其初始化的顺序如下，首先，pt1的构造函数被调用，接着调用pt2的构造函数，最后调用line类的构造函数体。pt1初始化在pt2之前不是因为在成员初始化表中，pt1位于pt2之前，而是因为在line类的定义中，pt1位于pt2之前。初始化表达式中元素初始化的顺序只取决于类定义中各个数据成员的前后关系。复制构造函数同样如此，首先pt1的复制构造函数被调用，接着调用pt2的复制构造函数，最后调用line类的复制构造函数体。

鉴于point类的一个构造函数为下述形式。

point（int x=0，int y=0）所以，在line类中重载如下构造函数，都是合法的。

line（int x1，int y1，int x2，int y2）：pt1（x1，y1），pt2（x2，y2）；

line（int x1，int y1）：pt1（x1，y1）；//pt2无参或全部参数都有默认值的构造函数被隐式调用

line（int x1，int y1）：pt2（x1，y1）//pt1无参或全部参数都有默认值的构造函数被隐式调用

line（）/pt1和/pt2无参或全部参数都有默认值的构造函数被隐式调用

如果point类中的数据成员是public（不推荐将数据成员设置为putlic，这违反了信息隐藏原则），可不使用成员初始化表，在l i n e类的构造函数体和复制构造函数体内采用诸如“pt1.xPos=5”的形式初始化pt1，但此时，编译器仍会隐式调用point类的无参构造函数或所有参数都有默认的构造函数，见代码8.16，用以开辟point对象所需要的内存，因此，必须在point类中显式定义一个无参构造函数或所有参数都有默认的构造函数。

代码8.16　类对象的构造函数隐式调用ClassMember2

＜-----------------------文件名：PointAndLine.h------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class point

04　{

05　public：//数据成员xPos和yPos也声明为public，这违法了信息隐藏原则

06　int xPos；

07　int yPos；

08　point（int x=0，int y=0）

09　{

10　cout＜＜"点的构造函数被调用"＜＜endl；

11　xPos=x；

12　yPos=y；

13　}

14　void print（）

15　{

16　cout＜＜"（"＜＜xPos＜＜"，"＜＜yPos＜＜"）"；

17　}

18　}；

19　class line

20　{

21　private：

22　point pt1；

23　point pt2；

24　public：

25　line（int x1，int y1，int x2，int y2）

26　{

27　pt1. xPos=x1；//可直接访问pt1和pt2中的公共数据成员xPos和yPos

28　pt1. yPos=y1；

29　pt2. xPos=x2；

30　pt2. yPos=y2；

31　}

32　line（const line＆ln）//复制构造函数

33　{

34　pt1. xPos=ln.pt1.xPos；

35　pt1. yPos=ln.pt1.yPos；

36　pt2. xPos=ln.pt2.xPos；

37　pt2. yPos=ln.pt2.yPos；

38　}

39　void draw（）

40　{

41　pt1. print（）；

42　cout＜＜"to"；

43　pt2. print（）；

44　cout＜＜endl；

45　}

46　}；

＜-----------------------文件名：example816.cpp-------------------------------＞

47　#include"point.h"

48　int main（）

49　{

50　line l1（1，2，3，4）；//调用有参构造函数

51　l1. draw（）；

52　line l2（l1）；//调用复制构造函数

53　l2. draw（）；

54　return 0；

55　}

输出结果如下所示。

点的构造函数被调用

点的构造函数被调用

（1，2）to（3，4）

点的构造函数被调用

点的构造函数被调用

（1，2）to（3，4）

【代码解析】因为point中的数据成员是public的，因此可以在line类的构造函数和复制构造函数中直接对pt1.xPos等赋值，此时，虽然将“line（int x1，int y1，int x2，int y2）”和“line（const line＆ln）”之后的数据成员初始化表达式省略，但编译器仍会隐式调用point类的无参构造函数，或者是所有参数都有默认值的构造函数（即代码第8行），用以分配对象成员的内存。

代码8.16　中的复制构造函数。

line（const line＆ln）

{

pt1.xPos=ln.pt1.xPos；

pt1.yPos=ln.pt1.yPos；

pt2.xPos=ln.pt2.xPos；

pt2.yPos=ln.pt2.yPos；

}

还可以写成如下形式。

line（const line＆ln）：pt1（ln.pt1.xPos,ln.pt1.yPos），pt2（ln.pt2.xPos,ln.pt2.yPos）

{

}

8.7.4　特别说明

对复制构造函数来说，一旦给出了自己定义的形式，编译器便不会提供默认的复制构造函数，所以确保自定义的复制构造函数的有效性很重要。因此，在一些必须使用自定义复制构造函数的场合，掌握特殊成员的用法很必要。在所举的例子中，尽管有些复制构造函数纯属“画蛇添足”，用系统提供的默认复制构造函数足以实现想要的功能，但还是给出了完整的书写形式，这就是原因所在。

8.7.5　static数据成员

C++允许使用static（静态存储）修饰数据成员，这样的成员在编译时就被创建并初始化了（与之相比，对象是在运行时才被创建的），且其实例只有一个，被所有该类的对象共享，就像住在同一宿舍里的同学共享一个房间号一样。静态数据成员和第6章中介绍的静态变量一样，程序执行时，该成员已经存在，一直到程序结束，任何对象都可对其进行访问。static数据成员的用法见代码8.17。

代码8.17　static数据成员的用法StaticMember

＜------------------------------文件名：computer.h----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class computer

04　{

05　private：

06　float price；//float型数据成员price，表示价格

07　static float total_price；//static成员，总价，不依附于某个对象

08　public：

09　computer（const float p）//构造函数，模拟买电脑的操作，对total_price进行累加

10　{

11　price=p；

12　total_price+=p；

13　}

14　～computer（）//析构函数，模拟退还电脑的操作，从total_price中减去

15　//所退电脑的price

16　{

17　total_price-=price；

18　}

19　void print（）//输出函数

20　{

21　cout＜＜"总价："＜＜total_price＜＜endl；

22　}

23　}；

＜------------------------------文件名：example817.cpp------------------------＞

24　#include"computer.h"

25　float computer：total_price=0；//初始化

26　int main（）

27　{

28　computer comp1（7000）；//买入电脑1

29　cout＜＜"购买电脑1后"＜＜endl；

30　comp1. print（）；

31　computer comp2（4999）；//买入电脑2

32　cout＜＜"购买电脑2后"＜＜endl；

33　comp2. print（）；

34　computer comp3（2500）；//买入电脑3

35　cout＜＜"购买电脑3后"＜＜endl；

36　comp1. print（）；//此处调用comp1.print（）、comp2.print（）和comp3.print（）

37　//输出结果相同

38　comp2.～computer（）；//退掉电脑2

39　cout＜＜"退掉电脑2后"＜＜endl；

40　comp3. print（）；

41　return 0；

42　}

输出结果如下所示。

购买电脑1后

总价：7000

购买电脑2后

总价：11999

购买电脑3后

总价：14499

退掉电脑2后

总价：9500

【代码解析】在代码第7行，computer.h文件中“static float total_price；”定义了float型静态数据成员total_price，则该成员为所有computer类的对象所共有，其对应的内存空间在程序编译时已被开辟，请注意example817.cpp中的语句“float computer：total_price=0；”，这条语句将total_price初始化为0，一定不能在类定义时对total_price初始化，事实上，类中的声明语句是向编译器描述应如何为数据成员分配内存，真正的内存分配是由example817.cpp中的定义性声明语句“float computer：total_price=0；”完成的。

对于total_price这样的静态数据成员，应在类声明之外使用单独的定义性声明语句完成其初始化，该语句中不能再使用static，初始化的基本格式如下所示。

类型类名：变量名=初始化表达式；//普通类型变量

类型类名：对象名（构造参数）；//对象成员

注意

将静态数据成员的初始化放在cpp文件中，不要放在h文件中，因为程序中可能有多个cpp文件包含该h文件，这样会出现初始化的复本，引发错误。

类中的任何成员函数都可访问类对象的静态成员，代码8.17用创建对象模拟买入电脑这一操作，用撤销对象，显式调用析构函数模拟退货的操作，并在类的构造函数和析构函数中对total_price进行加减处理，实时计算了当前总价。

当然，如果将静态数据成员的访问权限声明为public，在外部可直接访问该成员，下列格式都是合法的。

cout＜＜comp1.total_price；

cout＜＜comp2.total_price；

cout＜＜comp3.total_price；

cout＜＜computer：total_price；//推荐形式，不依赖于对象

推荐用不与特定对象相联系的方式访问静态数据成员，C++还提供了静态成员函数机制，这方面的相关内容稍后会有介绍，使用静态成员函数访问静态数据成员似乎是顺理成章的事。

要强调的是，如果静态数据成员使用const修饰（const与static没有前后之分），而且是整型、浮点型、布尔型或枚举型，但不能是类对象，数组（包括C风格字符串）、引用和指针，C++允许该成员在类定义中初始化，这样，便不能在外部再次对该成员进行定义性声明，但对该成员的引用性声明是允许的。

说明

VC 6并没有严格地实现最新的C++标准，即使是用const static修饰的数据成员，在VC 6中仍然要在类定义外部初始化，在类定义内初始化时VC 6的编译器会报错，而一些较新的编译器不会。

8.8　特殊函数成员

除了构造函数、复制构造函数和析构函数外，其他成员函数被用来提供特定的功能，一般来说，提供给外部访问的函数称为接口，访问权限为public，而一些不供外部访问，仅仅作为内部功能实现的函数，访问权限设为private。本节主要讨论函数成员的一些特殊用法。

8.8.1　静态成员函数

成员函数也可以定义成静态的，与静态数据一样，系统对每个类只建立一个函数实体，该实体为该类的所有对象共享。静态成员函数用法示例见代码8.18。

代码8.18　静态成员函数的用法StaticFuncMember

＜----------------------------文件名：computer.h-----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class computer

04　{

05　private：

06　char*name；

07　float price；

08　static float total_price；//静态数据成员

09　public：

10　computer（const char*chr,const float p）//构造函数，模拟买电脑操作

11　{

12　name=new char[strlen（chr）+1]；

13　strcpy（name,chr）；

14　price=p；

15　total_price+=p；

16　}

17　～computer（）//析构函数，模拟退掉电脑的操作

18　{

19　delete[]name；

20　total_price-=price；

21　}

22　static void print_total（）//static函数原则上只能访问静态数据成员

23

24　{

25　cout＜＜"总价："＜＜total_price＜＜endl；

26　}

27　static void print（computer＆com）；//静态成员函数print（）原型，如果要访问

28　//某具体对象，必须传递参数

29　}；

30　void computer：print（computer＆com）//静态成员函数print（）实现

31　{

32　cout＜＜"名称"＜＜com.name＜＜endl；

33　cout＜＜"价格"＜＜com.price＜＜endl；

34　}

＜-----------------------文件名：example818.cpp-------------------------------＞

35　#include"computer.h"

36　float computer：total_price=0；//初始化

37　int main（）

38　{

39　computer comp1（"IBM"，7000）；//声明类对象comp1，初始化，买入

40　//类名加作用域限定符访问static成员函数，传参comp1

41　computer：print（comp1）；

42　computer：print_total（）；//类名加作用域限定符访问static成员函数

43　computer comp2（"ASUS"，4999）；//声明类对象comp2，初始化，买入

44　computer：print（comp2）；//传递参数comp2

45　computer：print_total（）；

46　comp2.～computer（）；//析构函数调用，退还电脑

47　computer：print_total（）；

48　return 0；

49　}

输出结果如下所示。

名称IBM

价格7000

总价：7000

名称ASUS

价格4999

总价：11999

总价：7000

注意

使用VC 6编译上述代码时，必须选择Release模式，选择Debug模式时，程序运行会报错，这可能是VC 6编译器设计上的小Bug，关于两种编译模式的介绍请参考第20章。

【代码解析】从代码8.18可以看出，因为静态成员函数是被该类所有对象共享的，因此，如果需要在静态函数成员内访问类的非静态成员，需要将对象的引用或指向对象的指针作为参数，所以在代码第27行的类定义中采取了“static void print（computer＆com）；”的形式，在静态函数内访问静态数据成员显得比较自然。

结合代码8.18，强调一下静态成员函数的用法。

（1）静态成员函数既可以定义为inline的，如“static void print_total（）”，又可定义在函数外部，如“void computer：print（computer＆com）”，在类定义之外时，不使用static。

（2）静态成员函数不与特定的对象联系，基本的调用格式如下所示。

类名：静态成员函数名（参数表）；

如代码8.18中的“computer：print_total（）；”和“computer：print（comp1）；”，当然，将其当做是某个对象的成员也没有错，比如，只要comp1和comp2有效，将“computer：print_total（）；”写成“comp1.print_total（）；”和“comp2.print_total（）；”都是合法的，但不如使用类名来调用，意义更加直观。

8.8.2　const与成员函数

第7章已经介绍了const在函数中的应用，实际上，const在类成员函数中还有种特殊的用法，把const关键字放在函数的参数表和函数体之间（与第7章介绍的const放在函数前修饰返回值不同），称为const成员函数，其基本定义格式如下所示。

（1）类内定义时如下所示。

类型函数名（参数列表）const

{

函数体

}

（2）在类定义之外定义时，共分两步：

❑类内声明时如下所示。

类型函数名（参数列表）const；❑类外定义时如下所示。

类型类名：函数名（参数列表）const

{

函数体

}

注意

类外定义的实现同样需要关键字const，否则编译器会把其看成一个不同的函数。

const成员函数表示该成员函数只能读类数据成员，而不能修改类成员数据。如果const成员函数试图以任何方式改变类的数据成员或调用另一个非const成员函数，编译器将给出错误信息，如代码8.19所示。

代码8.19　const成员函数ConstFuncMember

＜--------------------------------文件名：point.h----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class point

04　{

05　int x；//默认为private的数据成员x和y

06　int y；

07　public：

08　point（int xp=0，int yp=0）//构造函数

09　{

10　x=xp；

11　y=yp；

12　}

13　void print（）const//const成员函数内无法修改数据成员，否则编译器报错

14　{

15　x=5；//试图修改x将引发编译器报错

16　cout＜＜"x："＜＜x＜＜"，y："＜＜y＜＜endl；

17　}

18　}；

＜-----------------------文件名：example819.cpp-------------------------------＞

19　#include"point.h"

20　int main（）

21　{

22　point pt；//声明类对象，以默认参数形式调用构造函数

23　pt. print（）；//调用const成员函数

24　return 0；

25　}

【代码解析】将代码8.19编译并链接，系统提示代码第15行语句“x=5；”错误，在const成员函数print（）内不能对类中的数据成员进行修改。

注意

任何不修改成员数据的函数都应该声明为const函数，这样有助于提高程序的可读性和可靠性。

8.9　对象的组织

有了自己定义的类，或者使用别人定义好的类创建对象，其机制与使用int等创建普通变量几乎一致，同样可以const对象来创建指向对象的指针和创建对象数组，还可使用new和delete等创建动态对象。

8.9.1　const对象

类对象也可以声明为const对象，一般来说，能作用于const对象的成员函数除了构造函数和析构函数外，便只有const成员函数了，因为const对象只能被创建、撤销以及只读访问，改写是不允许的。const对象的使用见代码8.20。

代码8.20　const对象ConstObject

＜-----------------------文件名：point.h--------------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class point//类定义

04　{

05　int x；//默认private型成员变量x和y

06　int y；

07　public：

08　point（int xp=0，int yp=0）//构造函数

09　{

10　x=xp；

11　y=yp；

12　}

13　void Setx（int xp）//非const成员函数Setx，设置x

14　{

15　x=xp；

16　}

17　void Sety（int yp）//非const成员函数Sety，设置y

18　{

19　y=yp；

20　}

21　void print（）const//const成员函数print，不能修改x和y

22　{

23　cout＜＜"x："＜＜x＜＜"，y："＜＜y＜＜endl；

24　}

25　}；

＜-----------------------文件名：example820.cpp-------------------------------＞

26　#include"point.h"

27　using namespace std；

28　int main（）

29　{

30　point pt（3，4）；//声明一个普通类变量pt

31　pt. Setx（5）；//使用pt可调用非const成员函数

32　pt. Sety（6）；

33　pt. print（）；//pt也可调用const成员函数

34　const point ptc（1，2）；//声明一个const对象（类变量）

35　//ptc. Setx（8）；//错误，ptc是const对象，只能调用const成员函数

36　//ptc. Sety（9）；//错误，ptc是const对象，只能调用const成员函数

37　ptc. print（）；//正确，const对象只能调用const成员函数

38　return 0；

39　}

输出结果如下所示。

x：5，y：6

x：1，y：2

【代码解析】代码8.20中，pt是point类的普通对象，通过pt可调用成员函数Setx和Sety函数改写数据成员x和y的值，但ptc是const对象，只能调用const成员函数print，而不能调用非const成员函数（代码第35～36行，即便该成员函数并不修改数据成员的值）。一旦const成员调用了非const成员函数，编译器将会指明错误。

以对象作为函数参数时，为避免对象复制引入的开销，多将参数对象声明为引用类型，这时，为避免对对象参数的偶然修改，常在函数的形参列表中用const修饰，这样，在函数体内只能使用该对象的const成员函数。

说明

对构造函数（含复制构造函数）和析构函数而言，其不能被定义成const成员，但却能被const对象调用。

8.9.2　指向对象的指针

对象占据一定的内存空间，和普通变量一致，C++程序中采用如下形式声明指向对象的指针。

类名*指针名[=初始化表达式]；

初始化表达式是可选的，既可以通过取地址（＆对象名）给指针初始化，也可以通过申请动态内存给指针初始化，或者干脆不初始化（比如设置为NULL），在程序中再对该指针赋值。

提示　指针中存储的是对象所占内存空间的首地址。

定义point类如下所示。

class point

{

int x；

int y；

public：

point（int xp=0，int yp=0）

{

x=xp；

y=yp；

}

void print（）

{

cout＜＜"x："＜＜x＜＜"，y："＜＜y＜＜endl；

}

}；以上述定义的point类为例，下列形式都是合法的。

point pt；//默认参数构造

point*ptr=NULL；//空指针

point*ptr=＆pt；//取某个对象地址

point*ptr=new point（1，2）；//动态分配内存

point*ptr=new point[5]；//动态分配一块对象数组空间使用对象指针引用对象成员要使用成员访问符号“-＞”，这和第5章中struct的相关内容很相近，对于point类，可使用如下形式引用print函数。

point pt（1，2）；

point*ptr=＆pt；

ptr-＞print（）；

注意

如果数据成员x和y的访问权限是public，通过ptr-＞x、ptr-＞y的形式访问数据成员也是合法的。

指向对象的指针满足所有第4章中介绍的指针运算规则。

8.9.3　对象的大小

对象占据一定大小的内存空间，如这块空间有多大？以及对象在内存中是如何存储的？这些看似和代码编写无关的概念其实很重要，有助于理解很多深层的内容。

总的来说，对象在内存中是以结构形式（只包括非static数据成员）存储在数据段或堆中，类对象的大小（sizeof）一般是类中所有非static成员的大小之和。在程序编译期间，就已经为static变量在静态存储区域分配了内存空间，并且这块内存在程序的整个运行期间都存在。而类中的成员函数存在于代码段中，不管多少个对象都只有一个附本。

对象的大小同样遵循第5章中介绍的3条准则，但以下的一些特殊之处需要强调。

❑C++将类中的引用成员当成“指针”来维护，占4个内存字节。

❑在后面章节中将会介绍到的虚函数（virtual function），如果类中有虚函数时，虚析构函数除外，还会额外分配一个指针用来指向虚函数表（vtable），因此，这个时候对象的大小还要加4。

❑指针成员和引用成员属于“最宽基本数据类型”的考虑范畴。

用示列代码8.21来加以说明。

代码8.21　对象的大小SizeofObject

＜--------------------------文件名：example821.cpp----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class cex{

04　private：

05　int a；//int型，在一般系统上占4个内存字节

06　char b；//char型，占1个内存字节

07　float c；//单精度浮点型，占4个内存字节

08　double d；//double型，占8个内存字节

09　short e[5]；//short型数组，每个元素占两个内存字节

10　char＆f；//引用，当成指针维护

11　double＆g；//引用，当成指针维护

12　static int h；//static成员，公共内存，不影响单个对象的大小

13　public：

14　cex（）：f（b），g（d）//和构造函数，引用成员必须在初始化表中初始化

15　{

16　}

17　void print（）//成员函数的定义，普通成员函数不影响对象大小

18　{

19　cout＜＜"Hello"＜＜endl；

20　}

21　}；

22　int cex：h=0；//static成员的初始化

23　int main（）

24　{

25　cout＜＜"sizeof（cex）："＜＜sizeof（cex）＜＜endl；//输出类对象的大小sizeof（cex）

26　return 0；

27　}

输出结果如下所示。

sizeof（cex）：48

【代码解析】代码第3行的cex类，其对象占据48个内存字节，这完全可以由第5章中介绍的3条准则推算出来，计算公式如下所示。

4（a的大小）+1（b的大小）+3（为使c的偏移量是c大小整数倍而填充的字节）+4（c的大小）+4（为使d的偏移量是d大小整数倍而填充的字节）+10（e的大小，虽然是数组，但在考虑偏移量方面按short计算）+2（f在类内当做指针来维护，大小为4，为使f的偏移量是其大小的整数倍而添加的字节）+4（f的大小）+4（g的大小）+0（static成员位于静态存储区，不占据内存空间）+0（成员函数都位于代码段，不占据对象内存空间，所有对象共用函数代码段）+4（最宽基本类型为double，占8个字节，所以对象的大小必须是8的整数倍，增添4个填充字节）=48

注意

类中的const成员不影响类对象的大小。特殊情况下如果类中不含有数据成员，编译器也不会允许类对象的大小为0，通常会填充1个字节，用以标记对象的内存地址。

8.9.4　this指针

前面提到，一个类的所有对象共用成员函数代码段，不管有多少个对象，每个成员函数在内存中只有一个版本，那编译器是如何知道是哪个对象在执行操作呢？答案就是“this指针”。

this指针是隐含在成员函数内的一种指针，称为指向本对象的指针，可以采用诸如“this-＞数据成员”的方式来存取类数据成员。

举例说明如下所示。

class Ex

{

private：

int x；

int y；

public：

void Set（）

{

x=1；

y=2；

}

}；set（）位于函数代码段，是个全局函数，并不在类的对象内，在程序中使用“Ex a,b”创建了两个Ex类对象，当执行a.Set（）和b.Set（）时，编译器如何知道是对哪个对象执行操作的呢？编译器隐含在非static成员函数里面插入了一个参数（即this指针），所以，类中的void Set（）函数实际上等价于如下所示。

void Set（Ex*this）

{

this-＞x=1；

this-＞y=2；

}

调用a.Set（）实际上是Set（＆a），调用b.Set（）实际上是Set（＆b），每个对象都有一个指向自己的this指针，该指针在对象创建时由系统自动填充，该指针和对象“同生共亡”，具有类作用域，关于类作用域的相关内容将在第9章介绍。

编译器不会向静态成员函数传递this指针，这即是“当静态函数成员内访问类的非静态成员时，需要将对象的引用或指向对象的指针作为参数”的原因。

this指针主要有以下两个作用。

（1）显式指明类中数据成员，尤其是和形参以及全局变量相区分。

在第6章中介绍了变量的生存期、作用域和可见域，局部变量对外部变量有屏蔽作用，如果要在代码块内摆脱局部变量的屏蔽，要使用全局作用符：，关于类作用域的相关介绍请参考第9章。

（2）返回本对象的指针或引用。

在函数成员返回时，如果需要返回本对象的指针，只须使用“return this；”，同样，在需要返回本对象引用的时候，使用“return*this；”可使代码清晰明了。

某对象的this指针恒指向该对象，不可修改，对const对象来说，该对象传递的this指针的类型如下所示。

const类名*const this；

在程序中既无法修改this指针，也无法通过this指针修改该对象。

8.9.5　对象数组

对象数组和标准类型数组的使用方法并没有什么不同，也有声明、初始化和使用3个步骤。

1.对象数组的声明

对象数组的声明格式如下所示。

类名数组名[对象个数]；

使用上述格式声明对象数组时，要求类定义中要么未显式定义任何构造函数（包括复制构造函数，这样，编译器将提供默认的默认构造函数），要么有且只能有1个参数都有默认值的构造函数（包括无参构造函数），数组所需要的内存空间才能被开辟。

2.对象数组的初始化

对象数组开辟后，其中的每个成员都是对象，可以调用对象的成员函数对对象数据成员进行访问，此外，还可在数组声明时对数组进行初始化，初始化常用无名对象完成。

Example类定义如下所示。

class Example

{

int x；

int y；

public：

Example（int xp=0，int yp=0）

{

x=xp；

y=yp；

}

}；在数组声明的同时初始化对象数组，必须对每个元素初始化，如下所示。

Example sz[3]={Example（1，2），Example（），Example（7，8）}；如果类中重载了多个构造函数，可以对不同的元素使用不同的构造函数。C++还允许只对对象数组的部分元素进行初始化，如下所示。

Example sz[12]={Example（1，2），Example（），Example（7，8）}；

数组sz包含12个对象，上述语句在数组声明时只对前3个进行了初始化，后面9个对象将调用编译器提供的默认构造函数或用户显式定义的所有参数都有默认值的构造函数（包括无参构造函数）。和普通数组一样，如果编译器可以判断出数组的元素个数，数组的大小可省略。

还可使用某个具体对象为数组中的对象初始化，此时会调用对象的复制构造函数，如下所示。

Example ex1（1，5）；

Example ex2（4，6）；

Example sz[]={ex1，ex2，ex1}；

注意

对象同样可以组织成多维数组，其用法和普通多维数组并没有区别。

8.9.6　对象链表

对象链表中，节点的初始化需要构造函数来完成，除此之外，对象链表和第5章中介绍的链表并无不同。

8.10　为对象动态分配内存

同把一个简单变量创建在动态存储区一样，可以用new和delete为对象分配动态存储区，在复制构造函数一节中已经介绍了为类内的指针成员分配动态内存的相关范例，本节主要讨论如何为对象和对象数组动态分配内存。

8.10.1　使用new和delete为单个对象分配/释放动态内存

代码8.22演示了如何为单个对象分配动态内存。

代码8.22　单个对象动态内存分配NewforObject

＜----------------------------文件名：point.h---------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class point

04　{

05　private：//private数据成员列表

06　int x；

07　int y；

08　public：

09　point（int xp=0，int yp=0）//构造函数，带默认参数值

10　{

11　x=xp；

12　y=yp；

13　cout＜＜"构造函数被调用"＜＜endl；

14　}

15　～point（）//析构函数

16　{

17　cout＜＜"析构函数被调用"＜＜endl；

18　}

19　void print（）//成员函数，类内部实现

20　{

21　cout＜＜"x："＜＜x＜＜"，y："＜＜y＜＜endl；

22　}

23　}；

＜---------------------文件名：example822.cpp--------------------------------＞

24　#include"point.h"

25　using namespace std；

26　int main（）

27　{

28　//动态申请一块内存，存储point类对象，并将地址赋值给point型指针p

29　point*p=new point（4，5）；

30　p-＞print（）；//使用指针加-＞调用成员函数

31　delete p；//释放动态申请的内存，防止内存泄露

32　p=NULL；//养成良好习惯，防止野指针

33　return 0；

34　}

输出结果如下所示。

构造函数被调用

x：4，y：5

析构函数被调用

【代码解析】在代码8.22第28行，用new为对象分配动态内存，对象的构造函数被激活，这时，new先分配一个足够存放该类对象的存储区（大小为sizeof类的存储区），然后调用程序中指明的构造函数初始化该内存区域，并返回该对象的地址给指针p，在用delete释放该动态内存时，将激活对象所在类的析构函数。

使用“delete p；”释放所申请的动态内存非但不会销毁指针p,p的值也不会更改，因此，在执行了delete操作后，及时将指针置为NULL，杜绝野指针的出现，是个很好的编程习惯。

8.10.2　使用new和delete为对象数组分配/释放动态空间

使用new为对象数组分配动态空间时，不能显式调用对象的构造函数，因此，对象要么没有定义任何形式的构造函数（由编译器默认提供），要么显式定义了一个（且只能由一个）所有参数都有默认值的构造函数（包括无参构造函数）。代码8.23演示了如何为动态对象数组申请内存空间。

代码8.23　对象数组动态空间管理NewforObjectArray

＜------------------------------文件名：point.h-------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class point

04　{

05　private：//private数据成员列表

06　int x；

07　int y；

08　public：

09　point（int xp=0，int yp=0）//构造函数，带默认参数值

10　{

11　x=xp；

12　y=yp；

13　cout＜＜"构造函数被调用"＜＜endl；

14　}

15　～point（）//析构函数

16　{

17　cout＜＜"析构函数被调用"＜＜endl；

18　}

19　void print（）//成员函数，类内部实现

20　{

21　cout＜＜"x："＜＜x＜＜"，y："＜＜y＜＜endl；

22　}

23　void Set（int xp,int yp）//成员函数，类内部实现，用来修改成员x和y

24　{

25　x=xp；

26　y=yp；

27　}

28　}；

＜------------------------------文件名：example823.cpp------------------------＞

29　#include"point.h"

30　using namespace std；

31　int main（）

32　{

33　//申请一块动态内存，连续存放连个point对象，将首地址赋值给point指针p

34　point*p=new point[2]；

35　p[0]. print（）；//可以将指针当成数组名，使用下标运算符访问对应对象，等价性

36　p[1]. Set（4，5）；//调用数据元素（对象）的成员函数Set

37　p[1]. print（）；

38　delete[]p；//释放申请的动态内存

39　p=NULL；//养成良好习惯，防止野指针

40　return 0；

41　}

输出结果如下所示。

构造函数被调用

构造函数被调用

x：0，y：0

x：4，y：5

析构函数被调用

析构函数被调用

【代码解析】代码第34行，开辟了一大小为2的对象数组，point类中定义的所有参数都有默认值的构造函数被调用，根据指针和数组名的等价性，通过诸如p[0]和p[1]的形式对数组中的对象进行访问。“delete[]p；”释放了数组所占的内存空间，new和delete激活了数组中每个对象的构造函数和析构函数。

注意

用数组名的形式（如p[0]、p[1]）对其中的对象访问，如果使用“p-＞print（）；”“p++；”等指针形式，一旦p的值不同于内存申请时返回的指针，delete的操作会引发错误。

8.10.3　malloc和free能否为对象动态申请内存

malloc/free无法满足动态对象的要求，因为malloc和free无法像new/delete及new/delete[]那样自动调用对象的构造函数和析构函数。

8.11　小结

本章讲述了C++语言中面向对象编程的基本概念和方法。

C++通过class关键字可以定义类，类的成员包括数据成员和函数成员两种。关于类的使用，大体分为类的定义、类的实现和类对象的创建3个步骤，其中，类的定义指明了类的结构，相当于“蓝图”，而类的实现相当于“技术图纸”，根据定义和实现便可以声明一个类的对象。

类中有几个特殊的成员函数，构造函数、复制构造函数和析构函数。构造函数和复制构造函数用于为类对象开辟所需内存空间，而析构函数则是在撤销对象时，释放其内存空间，但需要注意的是，用户通过new申请的动态内存并不会在对象撤销时被自动释放，所以，应合理搭配new和delete，及时释放无用的动态内存。构造函数不能由用户调用，但析构函数可以显式调用。复制构造函数的形参是本类对象的引用，它是用一个对象来初始化另一个对象。如果编程者没有显式定义构造函数（包括复制构造函数），C++编译器就会隐式定义默认的构造函数。

一些特殊数据成员和函数成员的用法要特别注意，在C++中，static数据成员和static函数成员用以实现类的所有对象对一个或多个类成员的共享，一个类的静态数据成员仅创建和初始化一次，在程序开始执行的时候创建，被该类的所有对象共享；而非静态的数据成员则随着对象的创建而多次创建和初始化。与静态数据成员类似，静态成员函数也是属于类的。静态成员函数能直接访问静态的数据成员，如果需要访问非静态数据成员和非静态函数成员，需要传递对象的引用作参数。

C++引入了const函数的概念。const函数不改变对象的数据成员，也不能调用非const函数。常量对象只能调用const函数；但构造函数和析构函数对这个规则例外，它们从不定义为常量成员，但可被常量对象调用（被自动调用）。

与普通变量一样，对象也可以组织成对象数组和链表。C++中还定义了一个this指针，它仅能在类的成员函数中访问，恒指向当前对象。

不仅可以使用new和delete在类的函数成员内申请动态空间，还可以为单个对象和对象数组申请动态空间，关键字new可自动计算对象的大小，而且，new和delete会自动激活对象的构造函数和析构函数，这都是malloc和free无法做到的，因此，不推荐用malloc和free为对象和对象数组分配动态内存空间。

8.12　上机实践习题

1.定义一个学生类，包括数据成员为年龄、性别、身高、成绩及显示成员函数，最后声明一个该类的对象，调用相应的显示函数输出结果。

【提示】上述题目主要是要求读者熟悉类和对象的相关知识，重点是掌握类中各种成员的声明、定义和使用。

【关键代码】

01　class CStudent

02　{

03　public：

04　CStudent（）

05　{

06　m_sex='m'；

07　m_age=18；

08　m_score=0；

09　m_height=150；

10　}；

11　CStudent（int age,int height,int score,char sex）

12　{

13　m_age=age；

14　m_height=height；

15　m_score=score；

16　m_sex=sex；

17　}；

18　void display（）

19　{

20　cout＜＜"age height score sex"＜＜endl；

21　cout＜＜m_age＜＜""＜＜m_height＜＜""＜＜m_score＜＜""＜＜m_sex＜＜endl；

22　}；

23　private：

24　intm_age；

25　intm_height；

26　float m_score；

27　charm_sex；

28　}；

2.定义一个包含复制构造函数的学生类，先定义一个该类对象，然后用该对象初始化另外一个对象，最后输出结果。

【提示】上述题目主要是要求读者掌握复制构造函数的相关知识，重点是掌握显式定义复制构造函数。

【关键代码】

01　class CStudent

02　{

03　public：

04　CStudent（）

05　{

06　m_sex='m'；

07　m_age=18；

08　m_score=0；

09　m_height=150；

10　m_name=NULL；

11　}；

12　CStudent（int age,int height,int score,char*name,char sex）

13　{

14　m_age=age；

15　m_height=height；

16　m_score=score；

17　m_sex=sex；

18　m_name=new char（strlen（name）+1）；

19　strcpy（m_name,name）；

20　}；

21

22　CStudent（const CStudent＆student）

23　{

24　m_age=student. m_age；

25　m_height=student. m_height；

26　m_score=student. m_score；

27　m_sex=student. m_sex；

28　m_name=new char（strlen（student. m_name）+1）；

29　strcpy（m_name,student. m_name）；

30

31　}

32

33　void display（）

34　{

35　cout＜＜"name age height score sex"＜＜endl；

36　cout＜＜m_name＜＜m_age＜＜m_height＜＜m_score＜＜m_sex＜＜endl；

37　}；

38　private：

39　char*m_name；

40　intm_age；

41　intm_height；

42　floatm_score；

43　charm_sex；

44　}；

第9章　关于对象的高级专题

第8章中介绍了C++中面向对象编程的基本概念，讨论了类的定义和实现、对象的创建和组织、特殊数据成员和函数成员的用法等，本章将在前文介绍的基础上，探讨关于对象的一些深层次内容。

本章主要涉及以下知识点。

❑类的作用域：介绍类的作用域的概念。

❑类的作用域和可见域：介绍类的作用域和可见域的相关知识。

❑对象的生存期、作用和可见域：介绍类对象实例化及释放等相关内容。

❑友元：介绍友元的概念、其与成员和非成员函数的关系，如何重载及友元类。

❑运算符重载：介绍如何进行运算符的重载及其各种形式。

❑运算符示例：介绍常用的几种运算符如何重载。

❑类型转换：介绍其他类型和自定义类型之间的转换。

❑重载函数：介绍如何选择重载函数。

9.1　类的作用域

第6章已经介绍了作用域和可见域的概念，知道可见域包含于作用域中，了解了局部作用域和全局作用域，本节讨论的是与类相关的作用域，大体分为类作用域、类名的作用域以及对象的作用域3部分内容。

在类中定义的数据成员和函数成员的作用域是整个类，这些名称只有在类中（包含类的定义部分和类外函数的实现部分）是可见的，在类外是不可见的，因此，可以在不同类中使用相同的成员名。另外，类作用域意味着不能从外部直接访问类的任何成员，即使该成员的访问权限是public，也要通过对象名来调用，对于static成员，要指定类名来调用。

如果发生“屏蔽”现象，类成员的可见域将小于作用域，但此时可借助this指针或“类名：”形式指明所访问的是类成员，这有些类似于使用“：”访问全局变量。如示例代码9.1所示。

代码9.1　数据成员、形参和全局变量VariableAccess

＜----------------------文件名：example901.cpp--------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　extern int x=100；//定义性声明，全局int型变量x

04　extern int z=200；//定义性声明，全局int型变量z

05　class Example//Example类定义

06　{

07　int x；//默认为private的成员列表

08　int y；

09　public：

10　Example（int xp=0，int yp=0）//构造函数

11　{

12　x=xp；

13　y=yp；

14　}

15　void print（int x）//成员函数print（），形参为x

16　{

17　cout＜＜"传递来的参数："＜＜x＜＜endl；//形参x覆盖掉了成员x和全局变量x

18　//此处的y指的是成员y，如果要访问成员x，可使用this指针

19　cout＜＜"成员x："＜＜（this-＞x）＜＜"，成员y："＜＜y＜＜endl；

20　cout＜＜"除了this指针外，还可以使用类名：的形式："＜＜endl；

21　//或使用类名加作用域限定符的形式指明要访问成员x

22　cout＜＜"成员x："＜＜Example：x＜＜"，成员y："＜＜y＜＜endl；

23　cout＜＜"全局x："＜＜（：x）＜＜endl；//访问全局变量x

24　//没有形参、数据成员对全局变量z构成屏蔽，直接访问z即可

25　cout＜＜"全局z："＜＜z＜＜endl；

26　}

27　}；

28

29　int main（）

30　{

31　Example ex1；//声明一个Example类的对象ex1

32　ex1. print（5）；//调用成员函数print（）

33　return 0；

34　}

输出结果如下所示。

传递来的参数：5

成员x：0，成员y：0

除了this指针外，还可以使用类名：的形式。

成员x：0，成员y：0

全局x：100

全局z：200

【代码解析】在Example类中，其中的数据成员和函数成员都具有类作用域，对成员x和y来说，如果没有“屏蔽”，x和y的可见域等于其作用域，可以在类定义和类实现的任何地方使用x和y来访问数据成员，在代码第17行，由于在print函数中，形参x屏蔽了类的数据成员x，此时数据成员x在print函数内不可见，如何在print函数中访问类的数据成员x呢？有两种途径：一是使用“类名：数据成员名”，二是使用this指针。

注意

从代码9.1可以看到，在print函数内，数据成员y并没有被屏蔽，因此使用y可以直接访问类的数据成员y。

从另一种意义上说，数据成员x在整个类内屏蔽了全局变量x，此时，使用“：x”可以在程序的任何地方访问全局变量，当然，如果类中没有和全局变量同名的数据成员，全局变量在类中便是可见的，如代码9.1中的z。

注意

类内的成员函数同样可能屏蔽外部的全局函数，如果要在类内调用外部的全局函数，仍然可以使用“：全局函数名”的形式。

9.2　类定义的作用域与可见域

同函数一样，类的定义没有生存期的概念，但类的定义有作用域和可见域。

使用类名创建类的对象时，首要的前提是类名可见，类名是否可见取决于类定义的可见域，该可见域同样包含在其作用域中，类本身可被定义在3种作用域内，这也是类定义的作用域。

1.全局作用域

在函数和其他类定义的外部所定义的类称为全局类，绝大多数的C++类是定义在该作用域中，我们在前面定义的所有类都是在全局作用域中，全局类具有全局作用域。

2.类作用域

一个类可以定义在另一类的定义中，这是所谓的嵌套类。举例来说，如果类A定义在类B中，A的访问权限是public，则A的作用域可认为和B的作用域相同，不同之处在于必须使用B：A的形式访问A的类名。当然，如果A的访问权限是private，则只能在类内使用类名来创建该类的对象，无法在外部创建A类的对象。代码9.2展示了嵌套类的应用。

代码9.2　public嵌套类PublicClassSample

＜--------------------------文件名：line.h------------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class line//line类定义

04　{

05　public：

06　class point//point类定义在line类内，public属性，外部可访问

07　{

08　private：//point类内私有成员列表

09　int x；

10　int y；

11　public：

12　point（int xp=0，int yp=0）//point类构造函数，带默认参数值

13　{

14　x=xp；

15　y=yp；

16　}

17　void printpoint（）；//point类成员函数原型，外部实现

18　}；

19　private：

20　point p1，p2；//line内两个point对象成员

21　public：

22　line（int x1，int y1，int x2，int y2）：p1（x1，y1），p2（x2，y2）//构造函数，初始化表

23　{

24　}

25　void printline（）//输出提示信息

26　{

27　p1. printpoint（）；//调用对象成员的公共成员

28　cout＜＜"-------＞"；

29　p2. printpoint（）；//调用对象成员的公共成员

30　cout＜＜endl；

31　}

32　}；

＜------------------------文件名：example902.cpp------------------------------＞

33　#include"line.h"//包含line类和point类定义的头文件

34　void line：point：printpoint（）//point类中函数printpoint（）的实现，注意双重作用域限定符

35　{

36　cout＜＜"（"＜＜x＜＜"，"＜＜y＜＜"）"；

37　}

38　int main（）

39　{

40　line line1（1，2，3，4）；//调用line类构造函数，声明一个line类的对象line1

41　line1. printline（）；//输出提示信息

42　line：point pt（1，3）；//以line：point访问point类定义，声明一个point类的对象pt

43　pt. printpoint（）；//输出提示信息

44　cout＜＜endl；//为整齐美观，换行

45　return 0；

46　}

输出结果如下所示。

（1，2）-------＞（3，4）

（1，3）

【代码解析】代码第6行的point类定义在line类里，嵌套类point的成员函数既可以定义成内联的，也可以在类line外定义，但要合理使用作用域限定符“：”。

注意

嵌套类的成员函数不能内联在外部类中。

point类定义的访问权限是public，因此，不仅可以在line类内通过诸如“point pt1；”的形式创建point类对象，还可以在line类外部使用诸如“line：point pt（1，3）；”的形式创建point类对象，此时，point类既可在line内可见，也可在line外可见，其作用域与全局类line相同。

如果point类定义的访问权限是private，则只能在line类内创建point类对象，在line类外部无法再通过“line：point”的形式访问point类名，此时，point类名只在嵌套类line内可见。

3.块作用域

类的定义在代码块中，这是所谓的局部类，该类完全被块包含，其作用域仅仅限于定义所在块，不能在块外使用类名声明该类的对象，如代码9.3所示。

代码9.3　块作用域类ClassInBlock

＜------------------------文件名：example903.cpp------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05

void Work（int,int）；

//Work函数原型声明

06

Work（5，6）；

//Work函数调用

07

return 0；

08　}

09　void Work（int a,int b）

10　{

11

class point

//类定义在函数内，在函数外无法使用point创建对象

12

{

13

private：

14

int x,y；

15

public：

16

point（int xp=0，int yp=0）

17

{

18

x=xp；

19

y=yp；

20

}

21

void print（）

22

{

23

cout＜＜x＜＜"，"＜＜y＜＜endl；

24

}

25

}；

26

point pt（a,b）；

//函数内，创建point类的对象pt

27

pt.print（）；

//输出提示信息

28　}

输出结果如下所示。

5，6

【代码解析】代码第11行的point类是在函数Work中定义的局部类，因此，只能在Work函数访问类名，通过类名声明该类对象，在Work函数外（如main函数中）不能访问point类，如果在main函数中使用了诸如“point pt1；”的语句，编译器将会指出“point类未定义”。

注意

局部类必须完全定义在局部作用域内。所以，它的所有成员函数必须是内联的，这就决定了局部类的成员函数都是很简单的。

4.类名也存在覆盖

和普通变量的覆盖原则一样，类名也存在“屏蔽”和“覆盖”，不过，依旧可使用作用域声明符“：”指定具体使用的类名，如“：类名”访问的是全局类，使用“外部类：嵌套类”访问嵌套类。

注意

在类成员函数内可用“this-＞成员名”的形式访问类成员，但不能用“this-＞嵌套类名”的形式访问嵌套类。

9.3　对象的生存期、作用域和可见域

前面介绍过类名和函数名一样，只有作用域和可见域，没有生存期。对象有生存期，对象的生存期也是对象中所有非静态数据成员的生存期，对象的所有非静态数据成员（包括const数据成员）都随着对象的创建而创建和初始化，随着对象的撤销而撤销。

对象的生存期、作用域和可见域取决于对象的创建位置，同样有全局、局部和类内之分，同前面关于普通变量的介绍并无区别，这里便不再赘述。

关于对象创建有几点问题需要强调。

9.3.1　先定义，后实例化

类的定义一定要在类对象声明之前，因为编译器要知道需要为类分配多大的内存空间，仅仅对类进行声明是不够的，如下所示。

class B；//声明

B objectB；/创建B类的对象

class B

{

……

}；//B类定义但是，如果不创建B类的对象，而仅仅是声明一个指向类型B对象的指针（或引用），如下所示。

class B；//声明

B*pB=NULL；//创建B类的对象

class B

{

……

}；//B类定义

上述代码是可行的，不论什么类型的指针，其内存大小都是相同的，编译器知道分配多大空间。但如果要创建类的对象，必须先定义类，再创建对象。

9.3.2　对象内存的释放与堆内存

一种普遍的误解是“如果对象被撤销，其占据的内存空间被释放，那么对象创建时和函数执行中通过new和malloc申请的动态内存也会被自动释放”，实际上，除非显式地用delete或free释放，申请的动态内存不会随着对象的撤销而撤销，相反，撤销了对象，却没有释放动态内存反而会引起内存泄露。

当然，在程序结束时，操作系统会回收程序所开辟的所有内存。尽管如此，还是要养成new/delete、malloc/free配对使用的编程习惯，及时释放已经无用的内存。

9.4　友元

一般来说，类的公有成员能够在类外访问，私有的成员只能被类的其他成员函数访问。在C++中，可以定义友元，如果某一个函数定义为类的友元，则该函数就可以访问该类的私有成员，同时，也可以把一个类定义为另一个类的友元，本节将讨论友元函数和友元类的概念。

9.4.1　友元的非成员函数

如果在某个类的定义中用friend声明了一个外部函数（或者是其他类的函数成员，既可以是public型，也可以是private型）后，这个外部函数称为类的友元函数。

友元函数声明的基本格式如下所示。

friend函数原型；

注意

用下面的比喻形容友元函数可能比较恰当，将类比作一个家庭，类的private成员相当于家庭的秘密，一般的外人是不允许探听这些秘密的，只有friend（朋友）才有能力探听这些秘密。

非成员形式的友元函数如代码9.4所示。

代码9.4　友元之非成员函数NonMemberFriend

＜-------------------------------文件名：point.h------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class point//point类定义

04　{

05　private：

06　int x,y；

07　//友元函数的声明，声明位置没有关系，可以是public，也可是private

08　friend float dis（point＆p1，point＆p2）；

09　public：

10　point（int i=0，int j=0）//构造函数，带默认参数值

11　{

12　x=i；

13　y=j；

14　}

15　void disp（）//成员函数

16　{

17　cout＜＜"（"＜＜x＜＜"，"＜＜y＜＜"）"；

18　}

19　}；

＜-------------------------文件名：example904.cpp-----------------------------＞

20　#include"point.h"

21　#include＜cmath＞//使用计算平方根的函数sqrt要用到的头文件

22　int main（）

23　{

24　point p1（1，2），p2（4，5）；//声明两个point类的对象p1和p2

25　p1. disp（）；//显示点p1的信息

26　cout＜＜"与"；

27　p2. disp（）；//显示点p2的信息

28　cout＜＜"距离="＜＜dis（p1，p2）＜＜endl；//利用友元函数计算两点举例

29　return 0；

30　}

31　float dis（point＆p1，point＆p2）//友元函数的实现

32　{

33　float d；

34　d=sqrt（（p1. x-p2.x）*（p1.x-p2.x）+（p1.y-p2.y）*（p1.y-p2.y））；

35　//友元函数中可访问类的private成员

36　return d；

37　}

输出结果如下所示。

（1，2）与（4，5）距离=4.24264

【代码解析】代码第31行的dis函数是外部函数，不属于任何一个类，此时，只要在Point类内，即代码第8行使用语句“friend float dis（point＆p1，point＆p2）；”对其进行声明即可，这样，dis函数便称为point类的友元函数，可以在dis函数中访问类的private成员。

非成员函数的友元函数有如下特点。

（1）在类内只需对函数进行声明，声明位置没有要求，可以出现在类的任何地方，包括在private和public部分。

（2）函数定义要放在类外，具体定义位置没有要求。

（3）友元函数不能直接访问类的成员，必须通过访问对象来实现，为此，必须传递类对象作为函数参数，采用传值、传指针或传引用方式均可，出于对程序执行效率的考虑，建议使用类对象的引用作为参数。

（4）友元函数不是类的成员函数，所以友元函数的实现和普通函数一样，在实现时不用“：”指示属于哪个类，只有成员函数才使用“：”作用域符号。

（5）一个函数可以同时作为多个类的友元函数。

细心的读者可能会发现，在代码9.4的example904.cpp中，并没有在使用dis函数前对其声明，这似乎违反了前面讲过的规则，实际上，在类定义中已经声明了dis函数，编译器已经知道了dis函数的存在，example904.cpp中的dis函数声明因此可以省略。

9.4.2　友元的成员函数

当A类的成员函数作为B类的友元函数时，必须先定义A类，而不仅仅是声明它。成员形式的友元函数如代码9.5所示。

代码9.5　友元的成员函数MemberFriend

＜-----------------------------文件名：point.h--------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class point；//声明point类

04　class line//定义line类

05　{

06　public：

07　float dis（point＆p1，point＆p2）；//友元函数的原型，作为line类的成员函数

08　}；

09　class point//定义point类

10　{

11　private：

12　int x,y；//private型数据成员x和y

13　friend float line：dis（point＆p1，point＆p2）；//友元的声明

14　public：

15　point（int i=0，int j=0）//point类的构造函数，带默认参数

16　{

17　x=i；

18　y=j；

19　}

20　void disp（）//成员函数disp，用来输出点的信息

21　{

22　cout＜＜"（"＜＜x＜＜"，"＜＜y＜＜"）"；

23　}

24　}；

＜-----------------------文件名：example905.cpp-------------------------------＞

25　#include"point.h"

26　#include＜cmath＞//使用计算平方根的函数sqrt要用到的头文件

27　int main（）

28　{

29　line line1；//声明一个line类的对象line1

30　point p1（1，2），p2（4，5）；//声明两个point类的对象p1和p2

31　p1. disp（）；//输出点p1的信息

32　cout＜＜"与"；

33　p2. disp（）；//输出点p2的信息

34　cout＜＜"距离="＜＜line1.dis（p1，p2）＜＜endl；//调用line1的成员函数dis计算两点间的距离

35　return 0；

36　}

37　//line类内成员函数dis的实现，作为point类的友元函数

38　float line：dis（point＆p1，point＆p2）

39　{

40　float d；

41　d=sqrt（（p1. x-p2.x）*（p1.x-p2.x）+（p1.y-p2.y）*（p1.y-p2.y））；

42　//可访问point类对象的private成员

43　return d；

44　}

输出结果如下所示。

（1，2）与（4，5）距离=4.24264

【代码解析】为了方便说明问题，line类仅仅包含一个成员函数dis，若要声明dis为point类的友元函数，必须使用“line：”指明dis是line类的成员函数，如代码第13行，结合代码9.5说明几点注意事项。

（1）友元函数所在的类必须先定义，即代码9.5中line类必须定义在point类前，对类实现的位置没有具体要求。

（2）根据前面“对象的生存期、作用域和可见域”一节的介绍可知，在line类的dis函数中需要传递对象参数，因此，必须在line类前对point类进行声明，且由于point类的定义在line类的定义后面，编译器无法决定dis函数中point对象的大小和成员组成，因此，只能对point对象采取传引用或传指针处理，而且，dis函数不能定义为内联格式，必须定义在point类定义之后。

9.4.3　友元函数的重载

要想使得一组重载函数全部成为类的友元，必须一一声明，否则只有匹配的那个函数会成为类的友元，编译器仍将其他的当成普通函数来处理。

class Exp

{

public：

friend void test（int）；

}；

void test（）；

void test（int）；

void test（double）；

上述代码中，只有“void test（int）”函数是Exp类的友元函数，“void test（）”和“void test（double）”函数都只是普通函数。

9.4.4　友元类

类A作为类B的友元时，类A称为友元类。A中的所有成员函数都是B的友元函数，都可以访问B中的隐藏信息。

A可以在B的public部分或private部分进行声明，方法如下。

friend＜类名＞；//友元类类名

见代码9.6。

代码9.6　友元类FriendClass

＜---------------------------文件名：point.h----------------------------------＞

01　#include＜iostream＞

02　#include＜cmath＞

03　using namespace std；

04　class cz；//声明类cz

05　class point//定义point类

06　{

07　private：

08　int x,y；

09　friend cz；//友元类的声明，位置同样不受限制

10　public：

11　point（int i=0，int j=0）//构造函数，带默认参数值

12　{

13　x=i；

14　y=j；

15　}

16　void disp（）//成员函数，输出点的信息

17　{

18　cout＜＜"（"＜＜x＜＜"，"＜＜y＜＜"）"；

19　}

20　}；

21　class cz//类cz的定义，其中所有的函数都是point类的友元函数

22　{

23　public：

24　float dis（point＆p1，point＆p2）//可访问p1和p2的private成员

25　{

26　float d；

27　d=sqrt（（p1. x-p2.x）*（p1.x-p2.x）+（p1.y-p2.y）*（p1.y-p2.y））；

28　return d；

29　}

30　void Set（point*p1，int a,int b）//可访问p1和p2的private成员

31　{

32　p1-＞x=a；

33　p1-＞y=b；

34　}

35　}；

＜------------------------文件名：example906.cpp------------------------------＞

36　#include"point.h"

37　int main（）

38　{

39　cz cz1；//声明一个cz类的对象cz1

40　point p1（1，2），p2（4，5）；//声明两个point类对象p1和p2

41　p1. disp（）；//输出点p1的信息

42　cout＜＜"与"；

43　p2. disp（）；//输出点p2的信息

44　cout＜＜"距离="＜＜cz1.dis（p1，p2）＜＜endl；//调用cz1的成员函数dis计算两点间距

45

46　cz1. Set（＆p1，3，4）；//调用cz1的成员函数Set改写p1中的private成员x和y

47　p1. disp（）；//修改后的点p1信息输出

48　cout＜＜endl；//换行

49　return 0；

50　}

输出结果如下所示。

（1，2）与（4，5）距离=4.24264

（3，4）

【代码解析】在代码第9行，类cz被声明为类point的友元类，则cz中的所有成员函数都称为point的友元函数，对比代码9.6和代码9.5可知，当把cz类声明为point类的友元类时，并不要求先定义cz，只要先对其进行声明即可，关于友元类有以下几个问题需要注意。

❑友元关系是单向的，不具有交换性。若类X是类Y的友元，类Y不一定是类X的友元，要看在类中是否有相应的声明。

❑友元关系不具有传递性。若类X是类Y的友元，类Y是Z的友元，类X不一定是类Z的友元，同样要看在类中是否有相应的声明。

❑友元关系不能被继承，关于继承的相关介绍请参考第10章。

9.4.5　友元是否破坏了封装性

不可否认，友元在一定程度上将类的私有成员暴露出来，破坏了信息隐藏机制，似乎是种“副作用很大的药”，但俗话说“良药苦口”，好工具总是要付出点代价的，拿把锋利的刀砍瓜切菜，总是需注意不要割到手指的。

友元的存在，使得类的接口扩展更为灵活，使用友元进行运算符重载从概念上也更容易理解一些，而且，C++规则已经极力地将友元的使用限制在了一定的范围内，它是单向的、不具备传递性、不能被继承，所以，应尽量合理使用友元。

9.5　运算符重载

前文已经介绍过函数重载的相关内容，简单地说，函数重载就是赋给同一个函数名多个含义。具体地讲，C++中允许在相同的作用域内以相同的名字定义几个不同的实现函数，这些函数的参数的类型或个数有所不同，而对于返回值的类型没有要求，可以相同，也可以不同。那种参数个数和类型都相同，仅仅返回值不同的重载函数是非法的。因为编译程序在选择相同名字的重载函数时仅考虑函数表，也就是说要靠函数在参数表中参数个数或参数类型的差异进行选择。

重载函数的意义在于用相同的名字访问一组相互关联的函数，由编译程序来进行选择，这有助于解决程序复杂性问题。如构造函数重载给类对象初始化带来了多种方式，为用户提供了更大的灵活性和自由度。

在C++中，操作符和函数是等价的、统一的，因此，运算符也可以重载，虽然系统已经预定义了一些操作符的功能，但毕竟应用有所限制，不能灵活地解决各种问题，而运算符重载可以赋予已有的运算符多重含义。通过重新定义运算符，使它能够用于特定类的对象执行特定的功能，这使得C++具有很强的可扩展性。

9.5.1　运算符重载规则

运算符是一种通俗、直观的函数，如下所示。

int x=2+3；上述语句中的“+”操作符，系统本身就提供了很多个重载版本。

int operator+（int,int）；

double operator+（double,double）；

注意

operator是C++关键字，用以标识后面所跟的为操作符。

对某个类来说，如果要计算该类两个对象的加和，有两种方式，一种是构造一个add函数，另一种是重载操作符“+”，推荐采用操作符重载，这会使得程序更加直观、易读易写，不易出错。

除了表9.1列出的5个运算符外，几乎所有的C++运算符都可重载，具体包括以下几种。

❑算术运算符：+、-、*、/、%、++、--

❑位操作运算符：＆、～、^、＜＜、＞＞

❑逻辑运算符：！、＆＆、||

❑比较运算符：＜、＞、＞=、＜=、==、！=

❑赋值运算符：=、+=、-=、*=、/=、%=、＆=、^=、＜＜=、＞＞=

❑其他运算符：[]、（）、-＞、（逗号运算符）、new、delete、new[]、delete[]、-＞、*

注意

诸如“#”等非C++运算符是不能被重载的。

此外，还有一条重要原则是不能臆造C++中不存在的运算符，如@和$等。

运算符的重载不改变其优先级和结合性，也不改变运算符的语法结构，即单目运算符只能重载为单目运算符，双目运算符只能重载为双目运算符。

用户自定义的重载运算符，要能访问对象的private成员，因此，对一个类来说，运算符重载的方式有成员函数和友元函数两种形式，下面结合实例分别对两种形式进行介绍。

9.5.2　以成员函数形式重载运算符

成员函数形式的运算符声明和实现与成员函数类似，首先应当在类定义中声明该运算符，声明的具体形式如下所示。

返回类型operator运算符（参数列表）；既可以在类定义的同时定义运算符函数使其称为inline型，也可以在类定义之外定义运算符函数，但要使用作用域限定符“：”，类外定义的基本格式如下所示。

返回类型类名：operator运算符（参数列表）

{

……

}

代码9.7　定义了复数类，并重载了其四则运算。

代码9.7　以成员函数形式重载运算符OperatorOverload1

＜---------------------------文件名：complex.h--------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class complex//定义复数类complex

04　{

05　private：

06　double real,imag；//private成员，分别代表实部和虚部

07　public：

08　complex（double r=0. 0，double i=0.0）//构造函数，带默认参数值

09　{

10　real=r；

11　imag=i；

12　}

13　complex operator+（const complex＆）；//成员函数形式重载加

14　complex operator-（const complex＆）；//成员函数形式重载减

15　complex operator-（）；//成员函数形式重载一元（取反）

16　complex operator*（const complex＆）；//成员函数形式重载乘

17　complex operator/（const complex＆）；//成员函数形式重载除

18　complex＆operator++（）；//成员函数形式重载前置++

19　complex operator++（int）；//成员函数形式重载后置++

20　void disp（）//成员函数，输出复数

21　{

22　cout＜＜real＜＜"+"＜＜"i*"＜＜imag＜＜endl；

23　}

24　}；

＜-----------------------------文件名：complex.cpp----------------------------＞

25　#include"complex.h"//包含了类complex的定义

26　complex complex：operator+（const complex＆CC）//加的实现

27　{

28　return complex（real+CC. real,imag+CC.imag）；

29　}

30　complex complex：operator-（const complex＆CC）//减的实现

31　{

32　return complex（real-CC. real,imag-CC.imag）；

33　}

34　complex complex：operator-（）//单目-，即取反的实现

35　{

36　return complex（-real，-imag）；

37　}

38　complex complex：operator*（const complex＆CC）//乘的实现

39　{

40　return complex（real*CC. real-imag*CC.imag,real*CC.imag+imag*CC.real）；

41　}

42　complex complex：operator/（const complex＆CC）//除的实现

43　{

44　return complex（（real*CC. real+imag+CC.imag）/（CC.real*CC.real+CC.imag*CC.imag），

45　（imag*CC. real-real*CC.imag）/（CC.real*CC.real+CC.imag*CC.imag））；

46　}

47　complex＆complex：operator++（）//前置++的实现

48　{

49　cout＜＜"前置++"＜＜endl；

50　real+=1；

51　imag+=1；

52　return（*this）；

53　}

54　complex complex：operator++（int）//后置++的实现，体会和前置++的区别

55　{

56　cout＜＜"后置++"＜＜endl；

57　complex ctemp=*this；

58　++（*this）；

59　return ctemp；

60　}

＜-----------------------文件名：example907.cpp-------------------------------＞

61　#include"complex.h"

62　int main（）

63　{

64　complex cx1（1. 0，2.0），cx2（3.0，4.0），cxRes；

65　cxRes=cx1-cx2；//相当于cx1.operator-（cx2）

66　cxRes. disp（）；

67　cxRes=-cx1；//相当于cx1.operator-（）

68　cxRes. disp（）；

69　cxRes=cx1+cx2；//相当于cx1.operator+（cx2）

70　cxRes. disp（）；

71　cxRes=cx1*cx2；//相当于cx1.operator*（cx2）

72　cxRes. disp（）；

73　cxRes=cx1/cx2；//相当于cx1.operator/（cx2）

74　cxRes. disp（）；

75　complex cx3（1. 0，1.0），cx4（5.0，5.0）；

76　cxRes=++cx3；//相当于cx3.operator++（）

77　cxRes. disp（）；

78　cx3. disp（）；

79　cxRes=cx4++；//相当于cx4.operator++（0）

80　cxRes. disp（）；

81　cx4. disp（）；

82　return 0；

83　}

输出结果如下所示。

-2+i*-2

-1+i*-2

4+i*6

-5+i*10

0.36+i*0.08

前置++

2+i*2

2+i*2

后置++

前置++

5+i*5

6+i*6

【代码解析】代码13～19行，重载了复数类的四则运算、取反及自增操作，每个复数由实部和虚部组成，重载的+、-、*、/、取反（单目-）以及++符合复数的运算规则，在定义四则运算符和取反运算符时，采用了返回无名对象的方式，如下所示。

complex complex：operator+（const complex＆CC）

{

return complex（real+CC.real,imag+CC.imag）；

}相比下列形式。

complex complex：operator+（const complex＆CC）

{

complex c（real+CC.real,imag+CC.imag）；

return c；

}

在两种方式中，似乎第2种更容易理解，可读性强，但第1种的效率要高于第2种。第2种定义方式做了3项工作，首先对象c被创建，同时完成初始化，然后，复制构造函数将c复制到保存返回值的外部存储单元中，最后，函数执行结束，c的析构函数被调用，c被撤销，其对应的非堆内存被释放。而对第1种定义方式而言，编译器直接在外部存储单元中创建了对象，并初始化，省去了复制和析构的开销。

需要强调的是，无论如何定义重载的运算符，也无法改变该运算符在C++中内置的结合性。例如，不能将单目取反操作放在cx1的后面，写成“cx1-”是不合法的。

代码9.7　中，对双目运算符+、-、*、/来说，编译器将左边对象解释为调用对象，将右边对象解释为传递给运算符的参数。例如cx1+cx2等价于cx1.operator+（cx2）。对单目运算的调用有两种形式：“对象+运算符”或者“运算符+对象”。在符合该运算符结合性顺序的前提下，编译器分别将两种形式解释为：“对象.operator运算符（0）”和“对象.operator运算符”，代码9.7中，分别重载了前置自增和后置自增运算符，两者的实现形式也与四则运算和取反运算有所不同。

前置自增的定义形式如下所示。

complex＆complex：operator++（）

{

cout＜＜"前置++"＜＜endl；

real+=1；

imag+=1；

return（*this）；

}

返回自身引用（*this）使得“++对象”可以作为左值（能放在等号的左边），而且函数的实现符合“先增1，再引用”的原则。编译器将“++对象”解释为“对象.operator++（）”。其他前置单目运算符与此类似。

后置自增的定义形式如下所示。

complex complex：operator++（int）

{

cout＜＜"后置++"＜＜endl；

complex ctemp=*this；

++（*this）；

return ctemp；

}

同前置自增不同的是，该函数以传值形式返回，这样，“对象++”不能放在等号的左边（不能对直接函数返回值区域赋值），也就不能成为左值，函数内部实现也符合“先引用，后增1”的原则。编译器将“对象++”解释为“对象.operator++（0）”，其他后置单目运算符与此类似。

说明

由此可以理解为什么“++++变量”是左值，而“++变量++”不是左值。

9.5.3　以友元函数形式重载运算符

用成员函数重载双目运算符时，左操作数无须用参数输入，而是通过隐含的this指针传入，这种做法的效率比较高，此外，操作符还可重载为友元函数形式，这将没有隐含的参数this指针。对双目运算符，友元函数有两个参数，对单目运算符，友元函数有一个参数。

重载为友元函数的运算符重载函数的声明格式如下所示。

friend返回类型operator运算符（参数表）；

使用友元函数形式重写代码9.7后如代码9.8所示。

代码9.8　以友元函数形式重载运算符OperatorOverload2

＜--------------------------文件名：complex.h---------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class complex//定义复数类complex

04　{

05　private：

06　double real,imag；//private成员，分别代表实部和虚部

07

08　public：

09　complex（double r=0. 0，double i=0.0）//构造函数，带默认参数值

10　{

11　real=r；

12　imag=i；

13　}

14　friend complex operator+（const complex＆，const complex＆）；//友元函数形式重载加

15　friend complex operator-（const complex＆，const complex＆）；//友元函数形式重载减

16

17　friend complex operator-（const complex＆）；//友元函数形式重载一元-（取反）

18　friend complex operator*（const complex＆，const complex＆）；//友元函数形式重载乘

19　friend complex operator/（const complex＆，const complex＆）；//友元函数形式重载除

20　friend complex＆operator++（complex＆）；//友元函数形式重载前置

21　friend complex operator++（complex＆，int）；//友元函数形式重载后置

22　void disp（）//成员函数，输出复数

23　{

24　cout＜＜real＜＜"+"＜＜"i*"＜＜imag＜＜endl；

25　}

26　}；

＜-----------------------------文件名：complex.cpp----------------------------＞

27　#include"complex.h"//包含了类complex的定义

28　complex operator+（const complex＆C1，const complex＆C2）//加的实现

29　{

30　return complex（C1. real+C2.real,C1.imag+C2.imag）；

31　}

32　complex operator-（const complex＆C1，const complex＆C2）//减的实现

33　{

34　return complex（C1. real-C2.real,C1.imag-C2.imag）；

35　}

36　complex operator-（const complex＆C1）//单目-，即取反的实现

37　{

38　return complex（-C1. real，-C1.imag）；

39　}

40　complex operator*（const complex＆C1，const complex＆C2）//乘的实现

41　{

42　return complex（C1. real*C2.real-C1.imag*C2.imag,C1.real*C2.imag+C1.imag*C2.real）；

43　}

44　complex operator/（const complex＆C1，const complex＆C2）//除的实现

45　{

46　return

47　complex（（C1.real*C2.real+C1.imag+C2.imag）/（C2.real*C2.real+C2.imag*C2.imag），

48　（C1. imag*C2.real-C1.real*C2.imag）/（C2.real*C2.real+C2.imag*C2.imag））；

49　}

50　complex＆operator++（complex＆C1）//前置++的实现

51　{

52　cout＜＜"前置++"＜＜endl；

53　C1. real+=1；

54　C1. imag+=1；

55　return C1；

56　}

57　complex operator++（complex＆C1，int）//后置++的实现，体会和前置++的区别

58　{

59　cout＜＜"后置++"＜＜endl；

60　complex ctemp=C1；

61　++C1；

62　return ctemp；

63　}

＜-----------------------文件名：example908.cpp-------------------------------＞

64　#include"complex.h"

65　int main（）

66　{

67　complex cx1（1. 0，2.0），cx2（3.0，4.0），cxRes；

68　cxRes=cx1-cx2；//相当于cx1.operator-（cx2）

69　cxRes. disp（）；

70　cxRes=-cx1；//相当于cx1.operator-（）

71　cxRes. disp（）；

72　cxRes=cx1+cx2；//相当于cx1.operator+（cx2）

73　cxRes. disp（）；

74　cxRes=cx1*cx2；//相当于cx1.operator*（cx2）

75　cxRes. disp（）；

76　cxRes=cx1/cx2；//相当于cx1.operator/（cx2）

77　cxRes. disp（）；

78　complex cx3（1. 0，1.0），cx4（5.0，5.0）；

79　cxRes=++cx3；//相当于cx3.operator++（）

80　cxRes. disp（）；

81　cx3. disp（）；

82　cxRes=cx4++；//相当于cx4.operator++（0）

83　cxRes. disp（）；

84　cx4. disp（）；

85　return 0；

86　}

输出结果如下所示。

-2+i*-2

-1+i*-2

4+i*6

-5+i*10

0.36+i*0.08

前置++

2+i*2

2+i*2

后置++

前置++

5+i*5

6+i*6

【代码解析】代码9.8和代码9.7的输出结果完全一致，以友元函数形式重载运算符与以成员函数形式重载运算符相比，最大的不同在于参数的个数（见代码第28～63行）。对双目运算符而言“对象1、运算符、对象2”解释为“operator运算符（对象1，对象2）”，而对单目运算符来说，前置运算符“运算符、对象”解释为“operator运算符（对象）”，而后置运算符解释为“operator、运算符、（对象，0）”，但不论怎样重载运算符，运算符的结合性和优先级并不会改变。

9.5.4　友元函数形式和成员函数形式的比较

对于绝大多数可重载操作符来说，两种重载形式都是允许的，但对下述运算符，只能使用成员函数形式，如表9.2所示。

关于两种重载方式的不同，还有一点要特别强调，涉及类型转换的问题。第2章中已经就C++内置类型的相互转换和转换发生的场合进行了介绍，本章稍后会详细讨论用户定义类的类型转换。

代码9.7　和代码9.8分别以成员函数形式和友元函数形式重载了complex类对象的四则运算符，仅举加法操作为例，如下所示。

complex operator+（const complex＆）；//成员函数形式

friend complex operator+（const complex＆，const complex＆）；//友元函数形式对两种形式来说，下述代码都是合法的。

complex c1（1.0，2.0），c2（3.0，4.0），cRes；

cRes=c1+c2；代码9.7和代码9.8中，complex类的构造函数如下所示。

complex（double r=0.0，double i=0.0）

{

real=r；

imag=i；

}

据此可知两种重载的加法操作符都可这样使用。

complex c1（1.0，2.0），cRes；

cRes=c1+5；这里，编译器会根据加法操作符的参数要求对5进行隐式转换，转换的依据就是complex类是否有合适的构造函数，代码9.7和代码9.8中complex类都提供了一个所有参数都有默认值的构造函数，因此，上述代码实际上相当于如下所示。

complex c1（1.0，2.0），cRes；

cRes=c1+complex（5，0）；可在下述代码的处理上，因为重载方式的不同，编译器会进行不同的处理。

complex c1（1.0，2.0），cRes；

cRes=5+c1；

仅仅是改变了两个操作数5和c1的位置，意义却发生了较大的变化，如果加操作符是以友元函数方式定义的，该代码没有问题，可如果是以成员函数方式定义的，编译器就会报错，问题仍然出在对5的类型转换上。

在友元函数方式下，编译器将上述代码解释为如下所示。

cRes=operator+（5，c1）；

这时，编译器会尝试根据合适的构造函数将5转换为complex类临时对象，完成参数匹配，实现加法操作。

在成员函数方式下，编译器将上述代码解释为如下所示。

cRes=5.operator+（c1）

此时，编译器不会试图将5转换成complex类的临时对象，因此，5不能调用complex类的成员函数，编译器报错。

可见，将操作符定义为友元函数形式可让程序更容易实现类型的自动转换，使两个操作符都被当成函数的参数。

注意

类型的自动转换仅发生在第2章中介绍的几种场合下。

如果程序中大量用到了complex类对象和int、double等的操作，最好的方式是重载几个complex类和内置数据类型（如int、doule等）相加的版本，编译器根据操作数选择具体调用哪个版本，这和函数重载本质上是相同的，关于编译器如何选择合适的版本调用的机制将在本章稍后的内容中进行详细介绍。

注意

重载、类型自动转换以及从多个函数中选择最优，这些问题交织在一起，在看完本章稍后的介绍后做总体考虑，会有新的收获。

9.5.5　对运算符重载的补充说明

运算符重载可以改变运算符内置的语义，如以友元函数形式定义的加操作符。

complex operator+（const complex＆C1，const complex＆C2）

{

return complex（C1.real-C2.real,C1.imag-C2.imag）；

}

明明是加操作符，但函数内进行的却是减法运算，这是合乎语法规则的，不过却有悖于人们的常规思维，会引起不必要的混乱。因此，除非有特别的理由，尽量使重载的运算符与其内置的、广为接受的语义保持一致。

此外，还要注意各运算符之间的关联，例如与下列几个指针相关的操作符。

[]、*、＆、-＞

编译器对这些操作符的解释为一种“等价”关系，因此，如果对其中一个进行了重载，其他对应的操作符也应被重载，使等价操作符完成等价的功能。

9.6　运算符重载范例

本节重点演示几种特殊运算符的重载示例，运算符的重载是很灵活的工具，使用得当，会产生意想不到的效果。

9.6.1　赋值运算符

赋值运算是一种很常见的运算，如果不重载赋值运算符，编译器会自动为每个类生成一个默认的赋值运算符重载函数，如下所示。

对象1=对象2；

实际上是完成了由对象2各个成员到对象1相应成员的复制，其中包括指针成员，这和第8章中复制构造函数和默认复制构造函数有些类似，如果对象1中含指针成员，并且涉及类内指针成员动态申请内存时，就会出现问题。

注意

下述两个代码的不同之处。

类名对象1=对象2；和

类名对象1；

对象1=对象2；

第一个代码是调用类的复制构造函数，完成对象1的创建并初始化，第二个代码是先调用对象1的无参构造函数（或所有参数都由默认值的构造函数）完成对象1的创建，而后调用赋值运算符将对象2的所有成员的值复制到对象1中。

来看一个第8章用过的例子。

代码9.9　赋值运算符重载AssignmentOverload

＜---------------------------文件名：example909.h-----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class computer

04　{

05　private：

06　char*brand；//字符指针brand

07　float price；

08　public：

09　computer（const char*sz,float p）

10　{

11　brand=new char[strlen（sz）+1]；//构造函数中为brand分配一块动态内存

12　strcpy（brand,sz）；//字符串复制

13　price=p；

14　cout＜＜"带参构造函数被调用"＜＜endl；

15　}

16　computer（）//无参构造函数

17　{

18　brand=NULL；//brand初始化为NULL

19　price=0；

20　cout＜＜"无参构造函数被调用"＜＜endl；

21　}

22　computer（const computer＆cp）//复制构造函数

23　{

24　brand=new char[strlen（cp. brand）+1]；//为brand分配动态内存

25　strcpy（brand,cp. brand）；//字符串复制

26　price=cp. price；

27　cout＜＜"复制构造函数被调用"＜＜endl；

28　}

29　～computer（）//析构函数，释放动态内存，delete[]NULL不会出错

30　{

31　delete[]brand；

32　cout＜＜"析构函数被调用"＜＜endl；

33　}

34　void print（）//成员函数，输出信息

35　{

36　cout＜＜"品牌："＜＜brand＜＜endl；

37　cout＜＜"价格："＜＜price＜＜endl；

38　}

39　}；

＜-------------------------文件名：example909.cpp-----------------------------＞

40　#include"example909.h"

41　int main（）

42　{

43　computer com1（"Dell"，2000）；//调用含参构造函数声明对象com1

44　computer com2=com1；//赋值运算符调用

45　if（false）

46　{

47　computer com3；

48　com3=com1；

49　}

50　return 0；

51　}

输出结果如下所示。

带参构造函数被调用

复制构造函数被调用

析构函数被调用

析构函数被调用

【代码解析】代码第45～49行，if（false）块中的代码并没有被执行，程序没有任何问题，复制构造函数很好地解决了使用诸如“computer com2=com1；”或“computer com2（com1）；”等形式的指针赋值，为每个指针成员开辟了单独的动态内存，不会出现多个指针指向同一个内存的情况，避免了多重释放一块内存或使用已释放动态内存的错误，这在第8章中都已经提及，这里仅仅作为回顾与复习。

如果将代码9.9中example909.cpp中的if（false）结构更改为if（true）结构，编译链接并运行，会出现如图9.1所示的内存错误。

问题出现在if（true）结构的代码块内，“computer com3；”调用computer类的无参构造函数创建了com3，并进行了初始化，而后，代码第48行，语句“com3=com1；”将com1中所有成员的值复制给了com3，包括指针brand，这样，com3和com1中的brand都指向同一块内存，随着if（true）代码块结束，com3被撤销，com3.brand对应的动态内存被释放，此时，com1.brand便无所指，成了野指针，程序出错。

图　9.1　对象赋值引发内存错误

注意

复制构造函数中的特殊操作应同等定义到赋值运算符重载中。

使用赋值运算符重载可解决上述问题，需要特别注意的是，赋值运算符只能重载为类成员方式，如下所示。

computer＆computer：operator=（const computer＆cp）//成员函数赋值运算符重载的实现

{

if（this==＆cp）//首先判断是否为自赋值，若是则返回当前对象

return（*this）；

price=cp.price；//如果不是自赋值，先对price赋值

delete[]brand；//防止内存泄露，先释放brand指向的内容

brand=new char[strlen（cp.brand）+1]；//为brand重新开辟一块内存空间

if（brand！=NULL）//如果开辟成功

{

strcpy（brand,cp.brand）；//复制字符串

}

return（*this）；//返回当前对象的引用，为的是实现链式赋值

}

在以上短短的几行程序中，有很多初学者容易忽视的地方，如下所述。

❑判断是否为自赋值，自己给自己赋值是没有意义的，不仅如此，如果不加判断就为指针重新申请内存，原来所指的动态内存块就泄露了。

❑释放brand所指的内存，delete一个NULL指针是不会出问题的。为了有效防错（防野指针），delete后立即将brand置为NULL。

❑为brand重新申请动态内存，该内存的大小由源对象决定。

❑出错处理，判断新的动态内存是否申请成功，若申请成功（brand不为NULL），实施字符串的复制，否则，进行出错处理，本函数什么也不做，返回时brand=NULL，还可以进行异常处理，关于这方面的内容将在第16章进行介绍。

❑返回引用（*this），为实现链式赋值，如“com1=com2=com3；”，根据赋值运算符的结合性，编译器会首先用com3为com2赋值，然后将修改后的com2为com1赋值。

如果将赋值操作符重载为友元形式，非左值（比如说一些常量）会被编译器隐式转换成一个临时对象，非左值出现在等号左边而编译器却认为合理，这破坏了赋值操作符的语义，因此，赋值操作符只能重载为成员形式。

9.6.2　函数调用运算符

函数调用运算符同样只能重载为成员函数形式，C++中，函数调用格式如下。

function（arg1，arg2，……）编译器将其解释为如下形式。

function.operator（）（arg1，arg2，……）

其作用是将函数调用运算符（）作用在对象function上，不过其参数并没有个数的限制。

注意

一个类如果重载了函数调用operator（），就可以将该类对象作为一个函数使用，这样的类对象也被称为函数对象。函数也是一种对象，这是泛型思考问题的方式。

函数对象的使用范例如代码9.10所示。

代码9.10　函数调用运算符重载FuncCallOverload

＜-------------------------文件名：example910.cpp-----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class Demo//Demo类定义

04　{

05　public：

06

07　double operator（）（double x,double y）；//重载函数调用符（），两个double型参数

08

09　double operator（）（double x,double y,double z）；//重载函数调用符（），3个double型参数

10　}；

11

12　double Demo：operator（）（double x,double y）//两个double参数的函数调用操作符的实现

13　{

14　return x＞y?x：y；//返回两个参数中较大的一个

15　}

16

17　double Demo：operator（）（double x,double y,double z）//3个double参数的函数调用操作符的实现

18　{

19

20　return（x+y）*z；//将前两个相加，与第3个参数相乘，返回最后的结果

21　}

22　void main（）

23　{

24　Demo de；//声明一个类对象

25　cout＜＜de（2. 5，0.2）＜＜endl；//可以将对象像函数一样使用

26　cout＜＜de（1. 2，1.5，7.0）＜＜endl；

27　}

输出结果如下所示。

2.5

18　.9

【代码解析】Demo类提供了函数调用运算符（）的两个重载版本，即代码第12行和第17行，这样，对象就可当成函数来使用，在某些场合下，这给程序设计带来了极大的方便。

注意

运算符重载十分灵活，如何应用得益于你的聪明才智，本书只提供给你一把打开大门的钥匙，只有充分发挥你的想象力，才能用好这些工具。

9.6.3　下标运算符

下标运算符是个二元运算符，C++编译器将表达式写成如下形式。

sz[x]；解释为如下所示。

sz.operator[]（z）；一般情况下，下标运算符的重载函数原型如下所示。

返回类型＆operator[]（参数类型）；下标运算符的重载函数只能有一个参数，不过该参数并没有类型限制，使用何种类型都可以，如果类中未重载下标运算符，编译器将会给出下标运算符的默认定义，此时参数必须是int型，并且要声明数组名才能使用下标变量，如下所示。

computer com[3]；

则com[1]等价于com.operator[]（1），如果[]中的参数类型非int型，或者非对象数组要使用下标运算符时，需要重载下标运算符[]。

注意

返回类型的引用很关键，这使得返回值可以作为左值。

下标运算符的范例如代码9.11所示。

代码9.11　下标运算符重载SubscriptOverload

＜-------------------------文件名：example911.cpp-----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class CharSZ//类CharSZ的定义

04　{

05　private：//private成员列表

06　int Len；

07　char*pBuf；

08　public：

09　CharSZ（int l）//构造函数

10　{

11　Len=l；

12　pBuf=new char[Len]；//开辟一块动态内存，字符数组

13　}

14　～CharSZ（）//析构函数

15　{

16　delete pBuf；//释放申请的动态内存

17　}

18　int GetLen（）//读取private成员Len的值

19　{

20　return Len；

21　}

22　char＆operator[]（int i）；//以成员函数形式重载下标运算符

23　}；

24　char＆CharSZ：operator[]（int i）//下标运算符重载的实现

25　{

26　static char def='\0'；//局部静态变量，def，其值为空字符

27　if（i＜Len＆＆i＞=0）//如果参数i在有效范围内

28　return pBuf[i]；//返回字符数组的第i+1个元素

29　else

30　{

31　cout＜＜"下标越界"＜＜endl；//参数i不合法

32　return def；//输出空字符

33　}

34　}

35　int main（）

36　{

37　int cnt=0；

38

39　CharSZ de（7）；//对象de中申请的动态内存大小为7，可存放6个有效字符（除开'\0'）

40　char*sz="Helloo"；

41　for（；cnt＜（strlen（sz）+1）；cnt++）//从0～6，循环执行7次

42　de[cnt]=sz[cnt]；

43　for（cnt=0；cnt＜de. GetLen（）；cnt++）//从0～6，循环执行7次

44　cout＜＜de[cnt]；

45　cout＜＜endl；

46　return 0；

47　}

输出结果如下所示。

Helloo

【代码解析】代码9.11第24行对CharSZ类的下标运算符进行了重载，参数类型仍选为int，重载了[]后便可对普通CharSZ对象（而非CharSZ对象数组）使用[]运算符。

9.7　类型转换

前面已经对普通变量的类型转换进行了介绍，本节来讨论类对象和其他类型对象的转换，这些转换发生的场合同第2章中介绍的一样，主要有以下几种。

❑赋值转换

❑表达式中的转换

❑显式转换

❑函数调用，传递参数时的转换

除了转换场合外，另一个问题就是转换方向，有“由其他类型向定义类的转换”和“由定义类向其他类型的转换”两种，下面分别展开讨论。

9.7.1　由其他类型向定义类的转换

由其他类型（如int和double）等向自定义类的转换是由构造函数来实现的，只有当类的定义和实现中提供了合适的构造函数时，转换才能通过。什么样的构造函数才是合适的构造函数呢？主要有以下几种情况，为便于说明，假设由A类型向自定义的B转换，操作如下所示。

❑类定义和实现中给出了仅包括只有一个A类型参数的构造函数。

❑类定义和实现中给出了包含一个A类型参数，且其他参数都是有默认值的构造函数。

❑类定义和实现中虽然不包含A类型参数，但包含一个C类型参数，此外没有其他参数或者其他参数都有默认值，且A类型参数可隐式转换为C类型参数。

用文字解释似乎很枯燥，来看示例代码9.12。

代码9.12　由其他类型转换为自定义类型Conversion1

＜------------------------文件名：example912.cpp------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class anotherpoint//anotherpoint类定义

04　{

05　private：//private成员列表

06　double x；

07　double y；

08　public：

09　anotherpoint（double xx=1，double yy=1）//构造函数，带默认参数值

10　{

11　x=xx；

12　y=yy；

13　}

14　double getX（）//成员函数，访问private成员x

15　{

16　return x；

17　}

18　double getY（）//成员函数，访问private成员y

19　{

20　return y；

21　}

22　}；

23　class point//point类定义

24　{

25　private：//private成员列表

26　int xPos；

27　int yPos；

28　public：

29　point（int x=0，int y=0）//构造函数，带默认参数，两个int型变量

30　{

31　xPos=x；

32　yPos=y；

33　}

34　point（anotherpoint aP）//构造函数，参数为anotherpoint类对象

35　{

36　xPos=aP. getX（）；

37　yPos=aP. getY（）；

38　}

39　void print（）//输出函数，点的信息

40　{

41　cout＜＜"（"＜＜xPos＜＜"，"＜＜yPos＜＜"）"＜＜endl；

42　}

43　}；

44

45　int main（）

46　{

47

48　point p1；//创建point类对象p1，采用带默认参数的构造函数，即x=0、y=0

49　p1=5；//等价于p1=point（5，0）；

50　p1. print（）；//输出点p1的信息

51　double dX=1. 2；//声明一个double变量dX

52　p1=dX；//等价于p1=point（int（dX），0）

53　p1. print（）；//输出点p1的信息

54

55　anotherpoint p2；//创建anotherpoint类的对象p2，带默认参数的构造函数，xx=yy=1

56　p1=p2；//等价于p1=point（p2）；

57　p1. print（）；//输出点p1的信息

58　return 0；

59　}

输出结果如下所示。

（5，0）

（1，0）

（1，1）

【代码解析】代码中分别进行了int型、double型和anotherpoint型向point类的转换，由于point类中提供了构造函数“point（int x=0，int y=0）”，这样，编译器将“p1=5”解释为“p1=point（5，0）”，先创建一个point类临时对象，再调用赋值运算符对p1进行赋值。对于语句“p1=dX；”，程序首先将double型变量dx转换为int型，再调用构造函数生成临时对象，最后用赋值运算符对p1进行赋值。

代码第56行，语句“p1=p2；”被编译器解释为“p1=point（p2）；”，这时，构造函数“point（anotherpoint ap）；”被调用，同样先生成一个point类临时对象，再调用赋值运算符完成对p1的赋值。

注意

能接受一个参数的构造函数才能称为转换函数，多出来的参数必须有默认值。

不仅仅在赋值运算时，在对象初始化、函数参数传递和函数返回时，都会发生隐式转换。除了隐式调用外，程序中还可以用诸如以下的形式来实现显式的类型转换。

p1=point（5）；

p1=point（dX）；

p1=point（p2）；隐式转换往往会出现一些意想不到的问题，使用关键字explicit可有效地阻止隐式类型的转换，也就是说，如果代码9.12中的构造函数，如下所示。

point（anotherpoint ap）改为如下形式。

explicit point（anotherpoint ap）

那么，包括“p1=p2；”在内的由anotherpoint类对象向point类对象的隐式转换都会被编译器认定为非法，必须使用显式转换“p1=point（p2）；”的形式才能完成转换。

注意

“p1=（point）p2；”的形式也是合法的，这是C语言的用法，推荐采用C++模式。

9.7.2　由自定义类向其他类型的转换

上节说明了由其他类型向自定义类型的转换，那能否反过来，由自定义类型转换成其他类型呢？答案是肯定的，通过转换函数，便可实现自定义类型向其他类型的转换。

自定义类型是用户定义的强制类型转换函数，如何创建强制类型转换函数呢？需要在类中定义如下形式的转换函数。

operator目标类型名（）；

需要注意以下几个使用要点。

❑转换函数必须是成员函数形式，不能是友元函数形式。

❑转换函数不能指定返回类型，但在函数体内必须用return语句以传值方式返回一个目标类型的变量。

❑转换函数不能有参数。

如示例代码9.13所示。

代码9.13　由自定义类型转换为其他类型Conversion2

＜------------------------文件名：example913.cpp------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class anotherpoint//anotherpoint类定义

04　{

05　private：//private成员列表

06　double x；

07　double y；

08　public：

09　anotherpoint（double xx=1，double yy=1）//构造函数，带默认参数值

10　{

11　x=xx；

12　y=yy；

13　}

14　void print（）//成员函数，输出点的信息

15　{

16　cout＜＜"（"＜＜x＜＜"，"＜＜y＜＜"）"＜＜endl；

17　}

18　}；

19　class point//point类定义

20　{

21　private：//private成员列表

22　int xPos；

23　int yPos；

24　public：

25　point（int x=0，int y=0）//构造函数，带默认参数值

26　{

27　xPos=x；

28　yPos=y；

29　}

30　operator int（）//定义point向int型的转换函数int

31　{

32　return xPos；

33　}

34　operator double（）//定义point向double型的转换函数double

35　{

36　return xPos*yPos；

37　}

38　operator anotherpoint（）//定义point向anotherpoint型的转换函数anotherpoint

39　{

40　return anotherpoint（xPos,yPos）；

41　}

42

43　}；

44

45　int main（）

46　{

47　point p1（4，5）；//声明一个point类变量p1

48

49　int x1=p1；//p1赋值给一个int型变量，point中的转换函数int被隐式调用

50　cout＜＜x1＜＜endl；

51

52　double dX=p1；//p1赋值给一个double型变量，point中的转换函数double被隐式调用

53　cout＜＜dX＜＜endl；

54　anotherpoint p2；//声明anotherpoint类对象p2，构造函数采用默认值

55　//p1赋值给p2，point中的转换函数anotherpoint被隐式调用

56　p2=p1；//等价于p2=another（p1.xPos,p1.yPos）

57　p2. print（）；//看p2是否修改成功

58　return 0；

59　}

【代码解析】在代码的point类中定义了int型、double型和anotherpoint型的转换函数，这使得由point类对象p1可以向int型变量x1、double型变量dX,anotherpoint型变量p2转换，在代码9.13中，这些转换都是隐式完成的。

当然，也可以通过显式调用实现由自定义类向其他类的转换，调用格式如下所示。

int x1=int（p1）；

double dX=double（p2）；

p2=anotherpoint（p1）；

注意

类似于“int x1=（int）p1；”的C语言用法也是合法的，但并不推荐这样做。

9.7.3　隐式转换带来的二义性

如果在代码9.13的main函数中加入一条数据语句“cout＜＜p1；”，编译器将会给出如下错误提示。

error C2593：'operator＜＜'is ambiguous

此时，编译器无法确定应该将p1隐式转换成int型还是double型，这称为转换的二义性，解决的方法如下。

❑显式转换。

❑在point类中重载操作符＜＜，这样，便不会发生任何的隐式转换。关于操作符＜＜的重载将在第14章中进行介绍，同时，还会涉及多个重载函数的选择机制，在稍后会进行介绍。

就代码9.13而言，如果在point类中只定义了int型转换函数或double型转换函数，编译器不会报错，因为此时只有一个可选转换，不会产生二义性错误。

9.8　重载函数选择规则

上一节讨论了隐式转换的二义性，实际上，隐式转换和重载函数的选择规则也有着紧密的联系，以代码9.7和代码9.8为例，编译器将显示如下代码。

complex c1（1.0，2.0），cRes；

cRes=c1+5；解释为以下代码。

complex c1（1.0，2.0），cRes；

cRes=c1+complex（5，0）；

可如果在complex类中定义了double型的转换函数，问题的答案似乎变得有些模糊。

❑方案1：cRes=complex（double（c1）+5，0）；

❑方案2：cRes=c1+complex（5，0）；

实际上，编译器会选择第1种方案执行，下面将讨论重载函数的选择规则。

9.8.1　重载解析

在函数重载的情况下，C++编译器需要一套合理的规则来决定函数调用时使用哪一个函数定义，这个过程称为重载解析（overloading resolution），该过程大致有以下几个步骤。

1.筛选出参数数目正确的函数定义

因参数默认值的存在，“数目正确”似乎不太好定义，可解释为“参数数目小于等于函数参数列表中的参数个数，且大于等于函数参数列表中没有默认值的参数个数”。

2.进一步根据参数类型的对应筛选合适的函数定义

对应参数的类型应一致，或存在一个隐式转换。这一步筛选后的函数定义都是可执行的版本，下一步是选择最优的过程。

3.选择最优，确定最佳方案

通常，从最优到最差的排列顺序如下所示。

❑完全匹配，其中常规函数优于模板，关于模板的介绍请参考第12章。

❑提升转换，即第2章的整体提升。

❑其他转换，除整体提升外的转换。

❑用户定义的类型转换，如类定义中定义的转换。

对运算符重载来说，系统内置类型定义的运算符调用具有更高的优先级，这即是前面举例编译器选择“cRes=complex（double（c1）+5，0）；”的原因。

9.8.2　什么是完全匹配

除了参数的类型和个数均一致外，C++默认下列看似不一致的转换也属于“完全匹配”，如表9.3所示。

需要进行特殊说明的是表9.3中第4项和第5项“返回类型函数名（参数列表）”与“返回类型（*指针）（参数列表）”说明“只要返回类型和参数列表一致，函数名和函数指针之间的转换可以认为是完全匹配的”。

如果有多个完全匹配的函数定义，编译器将会指出函数调用出现了二义性。在某些情况下，即使有两个以上的函数完全匹配，编译器仍旧能从中选择最优的版本，如下所述。

❑指向非const变量或对象的指针或引用优先于非const的指针和引用。

❑非模板函数优于模板函数。

本节只是对C++编译器重载函数选择规则的粗略介绍，详细的介绍需要花费很大的篇幅，并且需要对C++有着深层的了解，在此不再进行赘述。

9.9　小结

本章继续讨论了面向对象编程的一些概念，之所以称为“高级专题”，是因为讲述的内容都是基于前面对类和对象的介绍。首先，结合第6章中讲述的作用域、可见域和生存期的知识对类作用域、类定义的作用域和可见域以及对象的生存期、作用域和可见性进行了介绍。

C++引入了友元函数来对类的接口进行扩展，大大提高了外部访问的灵活性，但这在一定程度上也破坏了类的封装性。违反了信息隐藏的原则，因此，对友元的使用要合理，好钢用在刀刃上。

运算符重载是很重要的内容，合理重载运算符能够使程序编写简便灵活而且高效，除了极个别的运算符外，绝大多数的运算符都可被重载。运算符重载有成员函数形式和友元函数形式两种，两种方式各有优缺点，对一些运算符来说，只能采用成员函数的形式。

运算符重载不改变其优先级和结合性，而且用户只能重载C++定义的运算符，不可自己创新臆造，虽然C++允许改变运算符的语义，但不推荐这样做，它会让程序员和阅读程序的人思维混乱，随后结合范例讲解了运算符重载的用法，由于篇幅有限，这里的范例只能起到抛砖引玉的功能，如何用好运算符重载得益于知识的积累，以及程序员的感悟，还有更重要的一点便是想象力和创造力。

类对象和其他类型的对象之间也可以相互转化，发生转化的场合和第2章中所介绍的完全一致，不同之处在于“由其他类型对象向自定义类的转换是通过构造函数来完成的，而由自定义类向其他类型对象的转换是由转换函数来完成的”。

最后，本文简要讨论了编译器是如何从众多重载函数版本中选择最优的，如果编译器无法从给出的函数定义中选择最合适的版本，将会指出程序存在二义性（Ambiguous）错误。

9.10　上机实践习题

1.定义一个学生类，数据成员包括姓名、年龄、身高和成绩并定义一个显示结果的友元函数，最后声明一个该类的对象，调用相应的显式函数输出结果。

【提示】上述题目主要是要求读者熟悉类和对象的相关知识，重点是掌握友元的概念、声明、定义和使用。

【关键代码】

01　class CStudent

02　{

03　public：

04　CStudent（）

05　{

06　m_name=NULL；

07　m_age=18；

08　m_score=0；

09　m_height=150；

10　}；

11　CStudent（char*name,int age,int height,int score）

12　{

13　m_age=age；

14　m_height=height；

15　m_score=score；

16　m_name=new char[strlen（name）+1]；

17　strcpy（m_name,name）；

18　}；

19　～CStudent（）

20　{

21　if（m_name！=NULL）

22　delete[]m_name；

23　m_name=NULL；

24　}

25　friend void display（CStudent＆stud）；

26　private：

27　intm_age；

28　intm_height；

29　float m_score；

30　char*m_name；

31　}；

32

33　void display（CStudent＆stud）

34　{

35　cout＜＜"name age height score"＜＜endl；

36　cout＜＜stud. m_name＜＜""＜＜stud.m_age＜＜""＜＜stud.m_height＜＜"

37　"＜＜stud. m_score＜＜""＜＜endl；

38　}

2.自定义一个四则运算类，其对象可以对两个数进行四则运算，最后输出结果。

【提示】上述题目主要是要求读者掌握运算符重载的相关知识，重点是掌握运算符重载如何实现。

【关键代码】

01　class CCalc

02　{

03　private：

04　int n；

05　public：

06　CCalc（int x）//构造函数，带默认参数值

07　{

08　n=x；

09　}

10　CCalc operator+（const CCalc＆）；//成员函数形式重载加

11　CCalc operator-（const CCalc＆）；//成员函数形式重载减

12　CCalc operator*（const CCalc＆）；//成员函数形式重载乘

13　CCalc operator/（const CCalc＆）；//成员函数形式重载除

14　void disp（）//成员函数，输出复数

15　{

16　cout＜＜"result="＜＜n＜＜endl；

17　}

18　}；

19

20　CCalc CCalc：operator+（const CCalc＆CC）//加的实现

21　{

22　return CCalc（n+CC. n）；

23　}

24　CCalc CCalc：operator-（const CCalc＆CC）//减的实现

25　{

26　return CCalc（n-CC. n）；

27　}

28

29　CCalc CCalc：operator*（const CCalc＆CC）//乘的实现

30　{

31　return CCalc（n*CC. n）；

32　}

33　CCalc CCalc：operator/（const CCalc＆CC）//除的实现

34　{

35　if（CC. n！=0）

36　{

37　return CCalc（n/CC. n）；

38　}

39　cout＜＜"不能进行0值除法"＜＜endl；

40　return CCalc（0）；

41　}

42　//主函数中的调用

43　CCalc cc1（188）；

44　CCalc cc2（8）；

45　CCalc cc3（0）；

46　cc3=cc1-cc2；

47　cc3. disp（）；

48　cc3=cc1+cc2；

49　cc3. disp（）；

50　cc3=cc1*cc2；

51　cc3. disp（）；

52　cc3=cc1/cc2；

53　cc3. disp（）；

第10章　继承

第8章和第9章的内容主要围绕面向对象编程的“抽象性”（如何将抽象的事物构建成类，这是个建模的过程）和“封装性”（保证数据的私密性，只提供外部访问的接口），第10章和第11章将探讨“继承性”和“多态性”。继承的概念不难理解，多少有点“不劳而获”的意思，实际也是如此，面向对象程序设计的一个重要特点就是可以在现有的类的基础上定义新的类，而不必将现有的类的内容重新书写一遍，这称为“继承”（Inheritance），现有类称为“父类”或“基类”，在它的基础上建立的类称为“派生类”、“导出类”或“子类”，在本章的描述中，统一使用“基类”和“派生类”的名称。

本章主要涉及以下知识。

❑类的继承：介绍类的继承的概念及其示例。

❑派生类：介绍类与类之间的各种派生关系。

❑派生：介绍一个类如何声明和定义多个基类。

❑虚基类：介绍虚基类的概念及其优点。

❑派生类的构造和析构：介绍各种派生类的构造和析构函数的实现。

❑组合：介绍组合的概念及其特点。

❑基类与派生类对象之间的转换：介绍基类与派生类之间如何进行转换。

10.1　什么是继承

继承的概念广泛存在于现实世界中，对面向对象的程序设计而言，继承性的引入意义巨大。首先，程序员可以按现实生活中自然的方式去思考和解决问题，组织信息以提高效率；其次，可以重复使用基类的代码，并可以在继承类中增加新代码或者覆盖基类的成员函数，为基类成员函数赋予新的意义，实现最大限度的代码复用。

10.1.1　简单示例

通过例子来理解什么是继承，如代码10.1所示。

代码10.1　什么是继承InheritanceSample

＜----------------------文件名：example1001.cpp------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class point//point类定义

04　{

05　private：//private成员列表，表示点的坐标信息

06　int xPos；

07　int yPos；

08　public：

09　point（int x=0，int y=0）//构造函数，带默认参数

10　{

11　xPos=x；

12　yPos=y；

13　}

14　void disp（）//成员disp函数，用来输出点的信息

15　{

16　cout＜＜"（"＜＜xPos＜＜"，"＜＜yPos＜＜"）"＜＜endl；

17　}

18　int GetX（）//读取private成员xPos

19　{

20　return xPos；

21　}

22　int GetY（）//读取private成员yPos

23　{

24　return yPos；

25　}

26　}；

27　class point3D：public point//三维点类point3D，从point类继承而来

28　{

29　private：

30　int zPos；//在point类基础上增加了zPos坐标信息

31　public：

32　point3D（int x,int y,int z）：point（x,y）//派生类构造函数，初始化表中调用基类构造函数

33　{

34　zPos=z；

35　}

36　void disp（）//隐藏了基类中的同名disp函数

37　{

38　cout＜＜"（"＜＜GetX（）＜＜"，"＜＜GetY（）＜＜"，"＜＜zPos＜＜"）"＜＜endl；

39　}

40　int calcSum（）//增添了计算3个数据成员和的函数

41　{

42　return GetX（）+GetY（）+zPos；

43　}

44　}；

45　int main（）

46　{

47　point pt1（7，8）；//建立point类对象pt1

48　pt1. disp（）；//显示pt1的信息

49　point3D pt2（3，4，5）；//建立point3D类对象pt2

50　pt2. disp（）；//显示pt2的信息

51　int res=pt2. calcSum（）；//计算pt2中3个坐标信息的加和

52　cout＜＜res＜＜endl；//输出结果

53　return 0；

54　}

输出结果如下所示。

（7，8）

（3，4，5）

12

【代码解析】代码10.1中，point类是二维点类，现在要构建一个三维点类point3D，则point3D类可以从point类继承而来（见代码第27行），point类称为“基类”，point3D类称为“派生类”。在point3D类内不用再对xPos和yPos进行定义性声明，只要增加一个private成员zPos即可，还可在point3D类内定义与point类某个成员函数同名的函数，以实现功能覆盖，如point3D中的disp函数实现了point类中disp函数不同的功能。根据需要可在point3D类增加其他一些成员函数和数据成员，如calcSum函数。

在任何情况下，派生类内都无法访问基类的私有成员，因此，基类数据的初始化要通过基类的构造函数，而且，它要在派生类数据之前初始化，所以基类构造函数在派生类构造函数的初始化表中调用，也就是说，对基类（point类）中xPos和yPos的初始化必须放在point3D类的初始化列表中，采用诸如“point3D（int x,int y,int z）：point（x,y）”的形式完成。

由此可见，派生类生成过程包含以下3个步骤。

❑吸收基类的成员。

❑改造基类的成员。

❑添加新的成员。

10.1.2　继承的层次性

任何一个类都可以派生出新类，派生类还可以再派生出新的类，因此，基类和派生类是相对而言的。一个基类可以是另一个基类的派生类，这样便构建了层次性的类结构，如图10.1所示，类B是类A的派生类，同时又派生了新类C，类B又可以看做是类C的基类。

一般来说，派生类是基类的具体化，这符合人们按层次划分问题的习惯。例如从普通的车类，可以派生出自行车类、三轮车类和机动车类等。基类抽取了派生类中的共同特征，而派生类则是对基类添加约束，使之更为具体，面向更专的领域。

图　10.1　继承的层次性

注意

在派生类中可以对基类中的某些成员进行访问，这是由派生方式和成员在基类中的访问权限决定的，随后会有关于派生方式和访问权限的介绍。

10.2　派生类

派生有多种方式，在不同的派生方式下，派生类对基类成员的访问权限以及外部对基类成员的访问权限有所不同，本节详细讨论不同的派生方式。

10.2.1　public派生与private派生

C++中，利用基类派生其子类（派生类）的基本格式如下所示。

class派生类名：派生方式基类名

{

private：

新增私有成员列表；

public：

新增公开成员列表；

}；

如有需要，派生类可以从多个基类继承，也就是多重继承，这将在后面进行介绍。通过继承，派生类自动得到了除基类私有成员以外的其他所有数据成员和成员函数，在派生类中可以直接访问，从而实现了代码的复用。派生方式是指public派生和private派生，两种派生方式的不同点如表10.1所示。

可以看出，private派生和public派生的不同之处如下所述。

❑public派生时，基类中的public成员相当于派生类中的public成员。

❑private派生时，基类中的public成员相当于派生类中的private成员。

注意

不论采用哪种派生方式，基类中的private成员在派生类中和在外部都是不可见的，换言之，基类中的private成员不允许外部函数或派生类中的任何成员访问。

10.2.2　protected成员与protected派生

在第8章和第9章给出的示例中，数据成员多设定为private成员，也就是私有成员，私有成员只能被本类的成员函数所访问，不能通过“对象名.成员”的形式来访问。如果想做到基类的某些成员只能在派生类中访问，而不被外部的函数或对象访问，用private派生和public派生是无法实现的。首先，基类中的私有成员无论采用public派生还是private派生，在外部或其他类（包括派生类）中都是不可见的。其次，类中的public成员在public派生时，不仅可以在派生类中访问，也可以在外部或其他类中访问。再次，类中的public成员在private派生时，虽然只能在派生类内访问，在外部或其他类中无法访问，但用派生类再派生下一级的类时，基类中的所有成员都无法在“派生类的派生类”中访问。

为解决这一问题，C++引入了protected成员，protected类型的成员是一种区分血缘关系内外有别的成员，在派生类中可以访问protected成员，但在外部或其他类中，protected成员和private成员一样，无法被访问。

C++还允许使用protected派生方式，丰富后的访问权限如表10.2所示。

从表10.2可以归纳出如下几条规则。

❑基类的private成员在外部和其他类（包括派生类）中都是不可见的。

❑private派生使得基类中的非private成员都成为派生类中的private成员，在外部和其他类中无法访问。

❑protected派生使得基类中的非private成员都降一级：基类中的protected成员成为派生类中private成员，基类中public成员成为派生类中的protected成员。

❑public派生时，基类中的非private成员在派生类中的访问属性保持不变。

10.3　多基派生

派生类只有一个基类时，称为单基派生，但在实际运用中，我们经常需要派生类同时具有多个基类，这种方法称为多基派生或多重继承，图10.2是双基继承的示意，在实际应用中，还允许使用三基甚至是更多基继承。

图　10.2　双基继承

10.3.1　多基派生的声明和定义

在C++中，声明和定义具有两个以上基类的派生类与声明单基派生类的形式类似，只需将要继承的多个基类用逗号分开即可，如下所示。

派生类名（参数表）：基类名1（参数表1），基类名2（参数表2），……，基类名n（参数表n）

{

private：

新增私有成员列表；

public：

新增公开成员列表；

}；将图10.2表示成代码形式，如下所示（假设都采用public派生）。

class A

{

//类定义

}；

class B

{

//类定义

}；

class C：public A,public B

{

//在A和B基础上添加的成员列表

}；

上述代码中，派生类C有两个基类（类A和类B），按继承的规则，类C中包含了基类A中的成员、基类B中的成员以及该类本身的成员。

注意

多基继承时，在外部或派生类中对基类成员的访问权限与单基派生一致，如表10.2所示。

10.3.2　二义性问题

一般来说，在派生类中对基类成员的访问应当具有唯一性，但在多基继承时，如果多个基类中存在同名成员的情况，造成编译器无法判断具体要访问的是哪个基类中的成员，则称为对基类成员访问的二义性问题，如代码10.2所示。

代码10.2　对基类成员访问的二义性MultiBaseProblem

＜-----------------------文件名：example1002.cpp------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class A//类A的定义

04　{

05　public：

06　void print（）//类A中定义了print函数

07　{

08　cout＜＜"Hello,this is A"＜＜endl；

09　}

10　}；

11　class B//类B的定义

12　{

13　public：

14　void print（）//类B中同样定义了print函数

15　{

16　cout＜＜"Hello,this is B"＜＜endl；

17　}

18　}；

19　class C：public A,public B//类C由类A和类B共同派生而来

20　{

21　public：

22　void disp（）

23　{

24　print（）；//编译器无法决定采用类A中定义的版本还是类B中的版本

25　}

26　}；

27　int main（）

28　{

29　C exC；

30　exC. disp（）；//派生类内访问对象成员的二义性

31　exC. print（）；//外部通过派生类对象访问基类成员的二义性

32　return 0；

33　}编译运行，编译器给出如下错误信息。

error C2385：'C：print'is ambiguous

【代码解析】代码第19行的类C是由类A和类B派生而来的，且派生方式都为public，这样，在类C中访问print函数或外部通过C的对象访问print函数时，编译器无法判断到底应调用类A中的print函数还是类B中的print函数，出现二义性错误。

在多基派生时，为了能清楚地表示出各个类之间的关系，明确派生类中可见成员的归属，常采用一种无回路有向图（Directed Acyclic Graph）表示法，代码10.2可直观地体现为图10.3所示。

从图10.3可以看出，类A和类B是类C的两个基类，且类A中的print函数和类B中的print函数在类C中均可见，因此，简单地在派生类C中调用print函数会出现二义性错误。

图　10.3　DAG图表示法

注意

即使多个基类中存在某个同名成员，只要有一个基类的成员在派生类中是可见的，就不会出现二义性问题。

10.3.3　解决方案

若两个基类中具有同名的数据成员或成员函数，应使用成员名限定来消除二义性，如下所示。

void disp（）

{

A：print（）；

}

上述代码明确指明要在disp函数内调用的是从类A继承来的print函数，虽然做了修改，但是代码10.2仍无法通过编译，问题出在语句“exC.print（）；”上，虽然也可以通过添加作用域限定符，诸如“exC.A：print（）；”来解决，但是最好在类C中也定义一个同名print函数，根据需要来选择调用A：print（）还是B：print（），以实现对基类同名函数的隐藏。关于成员函数重载、覆盖和隐藏的区别将在本章稍后的内容中进行介绍。

10.4　虚基类

多基派生中，如果在多条继承路径上有一个共同的基类，如图10.4所示，从中不难看出，在D类对象中，会有来自两条不同路径的共同基类（类A）的双重复制。

10.4.1　共同基类带来的二义性

共同基类和多基派生的共同作用，使得在派生类中会出现多个共同基类的复制，这很容易带来二义性问题，如代码10.3所示。

图　10.4　多基继承时的共同基类问题

代码10.3　多基继承时的共同基类CommonBase

＜----------------------文件名：example1003.cpp-------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class A//公共基类

04　{

05　protected：//protected成员列表

06　int x；

07　public：//public成员列表

08　A（int xp=0）//构造函数

09　{

10　x=xp；

11　}

12　void SetX（int xp）//设置protected成员x的值

13　{

14　x=xp；

15　}

16　void print（）

17　{

18　cout＜＜"this is x in A："＜＜x＜＜endl；

19　}

20　}；

21　class B：public A//类B由类A派生而来

22　{

23　}；

24　class C：public A//类C由类A派生而来

25　{

26　}；

27　class D：public B,public C//类D由类B和类C派生而来

28　{

29　}；

30　int main（）

31　{

32　D exD；//声明一个D类对象exD

33　exD. SetX（5）；

34　exD. print（）；

35　return 0；

36　}编译运行，编译器提示错误信息如下所示。

error C2385：'D：SetX'is ambiguous

error C2385：'D：print'is ambiguous

【代码解析】代码10.3中，由于类B和类C都是从类A派生而来的，因此，类B和类C的对象中都保存有一份类A的复制，因此，当代码第27行使用类B和类C派生类D时，编译器无法判断所调用的函数继承自B还是C，出现二义性错误，代码10.3的DAG图表示如图10.5所示。

图　10.5　共同基类引发二义性的DAG图表示

10.4.2　解决方案

使用关键字virtual将共同基类A声明为虚基类，可有效解决上述问题。在定义由共同基类直接派生的类（代码10.3中的类B和类C）时，使用下列格式进行定义。

class派生类名：virtual派生方式基类名

{

//类定义

}；

这称为由基类“虚拟派生”派生类，某个基类的所有虚拟派生类只维护基类成员的一个版本，据此对代码10.3进行修改，如代码10.4所示。

代码10.4　虚基派生VirtualBase

＜---------------------文件名：example1004.cpp--------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class A//公共虚基类A

04　{

05　protected：//protected成员列表

06　int x；

07　public：

08　A（int xp=0）//构造函数，带默认构造参数

09　{

10　x=xp；

11　}

12　void SetX（int xp）//SetX函数用以设置protected成员x

13　{

14　x=xp；

15　}

16　void print（）//print函数输出信息

17　{

18　cout＜＜"this is x in A："＜＜x＜＜endl；

19　}

20　}；

21　class B：virtual public A//类B由类A虚基派生而来

22　{

23　}；

24　class C：virtual public A//类C由类A虚基派生而来

25　{

26　}；

27　class D：public B,public C//类D由类B和类C共同派生

28　{

29　}；

30　int main（）

31　{

32　D exD；//声明一个类D对象exD

33　exD. SetX（5）；//SetX函数，由virtual派生，在类D中只有一个版本，不会二义

34　exD. print（）；//print函数，由virtual派生，在类D中只有一个版本，不会二义

35　exD. B：print（）；//还可用类名显式说明调用函数的版本

36　exD. C：print（）；

37　exD. A：print（）；

38　return 0；

39　}

输出结果如下所示。

this is x in A：5

this is x in A：5

this is x in A：5

this is x in A：5

【代码解析】代码第21和24行，通过对类B和类C进行虚基派生，成功解决了二义性问题，虚基派生时，即使类D是由类B和类C多基派生而来的，类D中也只有一份A对象的复制，如图10.6所示。

图　10.6　虚基派生DAG示意图

说明

代码10.4　中，使用exD.print（）、exD.B：print（）、exD.C：print（）和exD.A：print（）是完全等价的，而且不必考虑访问权限和可见性，exD.x、exD.B.x、exD.C.x和exD.A.x对应着同一块内存单元。

关于虚基派生有以下两点需要特别说明。

❑关键字virtual和派生方式（public、private以及protected）的先后顺序无关。

❑为保证共同基类的成员在派生类中只有一个备份，必须将共同基类的直接派生类（代码10.4中为类B和类C）都定义为virtual方式，否则，派生类从所有virtual派生的路径中只能得到共同基类成员的一个备份，此外，还会从其他每条非virtual派生路径中得到一个备份。

10.4.3　虚基派生二义性与多基派生二义性不同

尽管看起来很相似，但虚基派生和多基派生带来的二义性有些细微的差别，多基派生的二义性主要是成员名的二义性，通过加作用域限定符来解决，而虚基派生二义性则是共同基类成员的多重复制带来的存储二义性，使用virtual派生来解决。

10.5　派生类的构造函数和析构函数

派生时，构造函数和释放函数是不能继承的，对派生类必须重新定义构造函数和析构函数，由于派生类对象中包含了基类数据成员的值，因此，创建派生类对象时，系统首先通过派生类的构造函数来调用基类的构造函数，完成基类成员的初始化，而后对派生类中新增的成员进行初始化。

10.5.1　派生类的构造函数

派生类构造函数的一般格式如下所示。

派生类名（派生类构造函数参数表）：基类构造函数（基类构造函数参数表）

{

//函数体

}；

必须将基类的构造函数放在派生类的初始化表达式中，以调用基类构造函数完成基类数据成员的初始化，派生类构造函数实现的功能，或者说调用顺序如下所述。

❑完成对象所占整块内存的开辟，由系统在调用构造函数时自动完成。

❑调用基类的构造函数完成基类成员的初始化。

❑若派生类中含对象成员、const成员或引用成员，则必须在初始化表中完成其初始化。

❑派生类构造函数体执行。

代码10.5　派生类构造函数的调用顺序CallSequence

＜--------------------文件名：example1005.cpp---------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class A//类A的定义

04　{

05　private：//private成员列表

06　int x；

07　public：

08　A（int xp=0）//构造函数，带默认参数

09　{

10　x=xp；

11　cout＜＜"A的构造函数被执行"＜＜endl；

12　}

13　}；

14　class B//类B定义

15　{

16　public：

17　B（）//无参构造函数

18　{

19　cout＜＜"B的构造函数被执行"＜＜endl；

20　}

21　}；

22　class C：public A//类C由类A派生而来

23　{

24　private：

25　int y；

26　B b；

27　public：

28　C（int xp,int yp）：A（xp），b（）//构造函数，基类构造函数在初始化表中调用

29　{

30　y=yp；

31　cout＜＜"C的构造函数被执行"＜＜endl；

32　}

33　}；

34　int main（）

35　{

36　C expC（1，2）；//创建类C对象expC

37　return 0；

38　}

输出结果如下所示。

A的构造函数被执行

B的构造函数被执行

C的构造函数被执行

【代码解析】代码10.5先定义了类A和类B，代码第22行，在A的基础上派生了类C，同时，C中还添加了一个B类型的对象成员，类C的构造函数如下所示。

C（int x,int y）：A（x），b（）

其中，C为派生类名，总参数表中共用2个参数，参数x用来初始化基类的数据成员，参数y用来初始化类C中新增数据成员y。除了要在初始化表达式中调用类A的构造函数外，类C对象成员b的初始化也要放在初始化表达式中进行。

程序的输出结果印证了前面关于构造函数步骤执行顺序的论述，特别要说明的是，如果基类中采用的是系统提供的默认构造函数，或定义了无参构造函数（包括所有参数都有默认值的构造函数），基类的构造函数调用可省略，此时，系统调用默认的基类构造函数完成基类成员的初始化。

也就是说，对代码10.5来说，由于类A提供了如下构造函数。

A（int xp=0）类B也提供了无参构造函数，因此，此处类C的构造函数可写为如下形式。

C（int y）

{

y=yp；

}

但此时程序的输出结果不变，也就是说，系统仍会调用基类和对象成员的构造函数。

注意

派生类的构造函数调用的是基类的构造函数，初始化表达式中是构造函数表达式，而对象成员在初始化时，初始化表达式中是对象名及初始化参数。

10.5.2　派生类的析构函数

当对象被删除时，派生类的析构函数被执行。析构函数同样不能继承，因此，在执行派生类析构函数时，基类析构函数会被自动调用。

执行顺序是先执行派生类的析构函数，再执行基类的析构函数，这和执行构造函数时的顺序正好相反，代码10.6对代码10.5进行了扩充。

代码10.6　派生类的析构函数DerivedDestructor

＜----------------------文件名：example1006.cpp-------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class A//类A的定义

04　{

05　private：//private成员列表

06　int x；

07　public：//public成员列表

08　A（int xp=0）//构造函数，带默认参数

09　{

10　x=xp；

11　cout＜＜"A的构造函数被执行"＜＜endl；

12　}

13　～A（）//析构函数

14　{

15　cout＜＜"A的析构函数被执行"＜＜endl；

16　}

17　}；

18　class B//类B的定义

19　{

20　public：//public成员列表

21　B（）//无参构造函数

22　{

23　cout＜＜"B的构造函数被执行"＜＜endl；

24　}

25　～B（）//析构函数

26　{

27　cout＜＜"B的析构函数被执行"＜＜endl；

28　}

29　}；

30　class C：public A//类C由类A派生而来

31　{

32　private：

33　int y；

34　B b；//对象成员

35　public：

36

37　C（int xp,int yp）：A（xp），b（）//派生类的构造函数，基类和对象成员都在初始化表中完成初始化

38　{

39　y=yp；

40　cout＜＜"C的构造函数被执行"＜＜endl；

41　}

42　～C（）//析构函数

43　{

44　cout＜＜"C的析构函数被执行"＜＜endl；

45　}

46　}；

47　int main（）

48　{

49　C expC（1，2）；//声明一个类C对象expC

50　return 0；//main函数执行完毕，expC撤销，析构函数触发执行

51　}

输出结果如下所示。

A的构造函数被执行

B的构造函数被执行

C的构造函数被执行

C的析构函数被执行

B的析构函数被执行

A的析构函数被执行

【代码解析】代码第30行，说明类C是由类A派生而来的，而整个代码的输出结果体现了派生类析构函数的执行顺序，代码直观简洁，没有需要特别说明的地方。

10.5.3　多基派生类的构造函数和析构函数

多基派生时，派生类的构造函数格式如下（假设有N个基类）。

派生类名（总参数表）：基类名1（参数表1），基类名2（参数表2），……，基类名N（参数表N）

{

//函数体

}

同前面所讲的单基派生类似，总参数表中包含了后面各个基类构造函数需要的参数。

多基派生和单基派生构造函数完成的任务和执行顺序并没有本质不同，唯一区别在于首先要执行所有基类的构造函数，再执行派生类构造函数中初始化表达式的其他内容和构造函数体，各基类构造函数的执行顺序与其在初始化表中的顺序无关，而是由定义派生类时指定的派生类的顺序决定的。

析构函数的执行顺序同样是与构造函数的执行顺序相反。

10.5.4　虚基派生类的构造函数和析构函数

前文提及，为了对包含在派生类对象中的基类对象进行复制和初始化，派生类的构造函数要调用基类的构造函数，虚基派生构造函数的调用规则同前面单基派生和多基派生的情况略有不同，为直观起见，结合图10.6来进行说明。

首先引入“最派生类”的概念，实际应用中，继承结构的层次可能很多，为了便于说明问题，规定在建立对象时指定的类为“最派生类”。对普通的多层继承而言，构造函数的调用多少有点嵌套的意味，如由C1类派生C2类，C2类又派生C3类时，表示为以下形式。

C2（总参数表）：C1（参数表）

C3（总参数表）：C2（参数表）

即对C3中C1类成员的初始化是通过调用C2类构造函数，进而在C2类构造函数中嵌套调用C1类构造函数来实现的。

对虚基派生来说，这有些行不通，类B和类C也可以创建对象，因此，必须在类B和类C的构造函数中调用虚基类A的构造函数，如果只是简单地利用类层次构造函数的嵌套调用，则在最派生类D时，虚基类A的构造函数会被执行两次。

根据虚基派生的性质，最派生类D中只有一份虚基类（共同基类）A的备份，因此，虚基类A的构造函数在D的构造函数中只能被调用一次。

所以，从虚基类A中直接派生（类B和类C）或间接派生（类D）的类中，其构造函数的初始化列表中都要列出对虚基类A构造函数的调用，但只有用于创建对象的那个派生类的构造函数才能够真正调用虚基类A的构造函数，而该派生类的基类构造函数的初始化表中列出的对虚基类A的构造函数调用在执行时被忽略，这样便保证了对虚基类的子对象只初始化一次。

举例说明其正确的用法，如代码10.7所示。

代码10.7　虚基派生类的构造函数和析构函数VirtualConstructorDestructor

＜---------------------文件名：example1007.cpp--------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class A//类A定义

04　{

05　private：//private成员列表

06　int x；

07　public：//public成员列表

08　A（int xp=0）//构造函数，带默认参数

09　{

10　x=xp；

11　cout＜＜"A的构造函数被执行"＜＜endl；

12　}

13　～A（）//析构函数

14　{

15　cout＜＜"A的析构函数被执行"＜＜endl；

16　}

17　}；

18　class B：virtual public A//类B由类A虚基派生而来

19　{

20　public：

21　B（int xp）：A（xp）//在初始化表中调用基类构造函数

22　{

23　cout＜＜"B的构造函数被执行"＜＜endl；

24　}

25　～B（）//析构函数

26　{

27　cout＜＜"B的析构函数被执行"＜＜endl；

28　}

29　}；

30　class C：virtual public A//类C由类A虚基派生而来

31　{

32　public：

33　C（int xp）：A（xp）//在初始化表中调用基类构造函数

34　{

35　cout＜＜"C的构造函数被执行"＜＜endl；

36　}

37　～C（）//析构函数

38　{

39　cout＜＜"C的析构函数被执行"＜＜endl；

40　}

41　}；

42　class D：public B,public C//类D由类B和类C共同派生而来

43　{

44　public：

45　D（int xp）：B（xp），C（xp），A（xp）

46　//初始化表中不仅要调用类B和类C的构造函数，还应调用共同虚基类的构造函数

47　{

48　cout＜＜"D的构造函数被执行"＜＜endl；

49　}

50　～D（）//析构函数

51　{

52　cout＜＜"D的析构函数被执行"＜＜endl；

53　}

54　}；

55　int main（）

56　{

57　D expD（2）；//声明D类对象expD

58　return 0；//main函数执行完毕退出后，expD撤销，析构函数触发执行

59　}

输出结果如下所示。

A的构造函数被执行

B的构造函数被执行

C的构造函数被执行

D的构造函数被执行

D的析构函数被执行

C的析构函数被执行

B的析构函数被执行

A的析构函数被执行

【代码解析】代码10.7中，代码第57行，在创建D类对象expD时，D类的两个基类B和C的构造函数都被调用，但代码第21行的类B和代码第33行的类C构造函数初始化表中的对虚基类A构造函数的调用被忽略了，执行的是类D构造函数初始化表中列出的虚基类A的构造函数。

如果在程序中使用诸如“B expB（1）；”的语句创建类B对象时，则在类B构造函数初始化表中列出的对虚基类A构造函数的调用便会被执行，这解释了为什么要在虚基类派生路径上的每个类构造函数的初始化表都要列出对虚基类构造函数的调用，这样便很好地保证了不管路径多深，虚基类的构造函数必须且只能被调用一次。

注意

C++规定，虚基类构造函数的执行要先于非虚基类构造函数的执行，同类构造函数的执行顺序仍遵循类定义时的派生顺序，而不是出现在初始化表中的顺序。

析构函数的执行顺序与构造函数的执行顺序相反。

特别要说明的是一类特殊情况，即如图10.7表示的多虚基类的情况。

将图10.7各个类的定义简写如下所示（假设采用public派生方式）。

图　10.7　多虚基类的情况

class C：virtual public A,virtual public B

class D：virtual public A,virtual public B

class E：public C,public D各个类的构造函数简写如下所示。

C（）：A（），B（）

D（）：A（），B（）

E（）：C（），D（），A（），B（）；

注意，定义时在前的类在图的左边，在创建E类对象时，按从左到右深度优先遍历算法来调用各个类的构造函数，如下所述。

（1）初始化A类复制

（2）初始化B类复制

（3）初始化C类复制

（4）初始化D类复制

（5）初始化E类复制

10.6　分清继承还是组合

面向对象设计的难点在于类的设计，而不是对象的创建，就像工业生产中图纸是关键，有了图纸产品可以很容易地被制造出来。在程序设计中，如何处理类派生和类组合一直是个很让人觉得困惑的问题。

前面已提及继承的重要性，它能使代码结构清晰，大大提高了程序的可复用性，因此，很多初学者容易犯的错误就是把继承当成灵丹妙药，不管适合与否便使用继承，其实，在面向对象的组织方面，不只有继承，还有对象组合以及聚合等，但就C++的实用性而言，本节先讨论继承与组合的关系。

10.6.1　继承不是万金油

如果两个类没有关联，仅仅是为了使一个类的功能更多而让其去继承另一个类，这种方法是不可行的。继承不是万金油，毫无意义的继承就像乱拉关系，会让条理有序的关系变得一团糟。从逻辑上说，继承是一种a kind of（AKO）或者说IS-A（“是一个”）的关系。汽车是车，因此，汽车类可以从普通的车类继承而来，轮子类就不能从汽车类继承来，轮子是汽车的一个部件，轮子可以作为汽车类的对象成员，这就是“组合”（Composition）。

10.6.2　组合

某类以另一个类对象作为数据成员，称为组合，在逻辑上，如果类A是类B的一部分（a part of）或者说HAS-A（“有一个”），不要从类A派生出类B，而应当采用组合的方式，《高质量C++编程指南》中的这个范例很好地解释了组合的本质，如下所示。

class Eye//眼睛类

{

public：

void Look（void）；//功能：看

}；

class Nose//鼻子类

{

public：

void Smell（void）；//功能：闻

}；

class Mouth//嘴巴类

{

public：

void Eat（void）；//功能：吃

}；

class Ear//耳朵类

{

public：

void Listen（void）；//功能：听

}；

class Head//头类

{

public：

void Look（）

{

m_eye.Look（）；

}

void Smell（）

{

m_nose.Smell（）；

}

void Eat（）

{

m_mouth.Eat（）；

}

void Listen（）

{

m_ear.Listen（）；

}

private：//组合

Eye m_eye；

Nose m_nose；

Mouth m_mouth；

Ear m_ear；

}；虽然代码很冗长，但这是一种正确的做法和设计，如果允许Head类从Eye、Nose、Mouth及Ear派生而来，那Head将自动具有Look、Smell、Eat及Listen的功能，如下所示。

class Head：public Eye,public Nose,public Mouth,public Ear

{

//

}；

派生方式看似简洁、强大，但却会给后续设计带来很多问题，比如要添加一个割双眼皮的函数，对于组合来说，操作对象很明确，要操作的是成员m_eye，但如果按派生方式来操作，割双眼皮函数就会遇到问题，搞不好割的是头皮。

10.7　基类与派生类对象间的相互转换

private派生时，外部无法访问通过派生类对象直接访问从基类继承来的任何成员，因此，private派生的使用较少，本节主要讨论public派生时基类与派生类对象间的相互转换。

注意

只考虑public派生，这样可以保证在派生类中对基类的public成员进行访问，没有加以特别的说明时，本节所说的基类和派生类均系public派生。

10.7.1　类型适应

“类型适应”是指两种类型之间的关系，说类A适应类B是指类A的对象能直接用于类B对象所能应用的场合，从这种意义上讲，派生类适应于基类，派生类的对象适应于基类对象，派生类对象的指针和引用也适应于基类对象的指针和引用。

类型适应很大程度上减轻了程序编写的工作量，因为若一个函数可用于某类型的对象，则它也可作用于该类的所有派生类对象，不必再为处理派生类对象重载该函数。

也就是说，形参要求为基类对象时，可以直接用派生类对象做实参，除此之外，类型适应还体现在以下两个方面。

1.赋值转换

可以用派生类对象为基类对象赋值，用派生类对象初始化基类对象的指针。派生类对象可以理解为包含了一个基类对象和一个附加子对象，因此，上述两种转换不会出现错误，但由于基类不适应派生类，因此，基类不能赋值给派生类，也不能用基类对象初始化派生类的引用。

2.指针转换

可以用派生类对象的地址初始化基类对象的指针，这时因为派生类对象与其基类分量具有相同的地址，如图10.8所示，将派生类的指针赋值给指向基类的指针时，便可通过此指向基类的指针访问派生类对象中从基类继承下来的成员。但对派生类中新加成员，除非采用强制类型转换，否则不可以用指向基类的指针来访问。

同样的道理，把基类指针直接赋值给派生类指针是不允许的，必须通过强制转换的形式来完成，综合示例如代码10.8所示。

图　10.8　派生类的内存模型简图

代码10.8　类型适应示例TypeSample

＜--------------------文件名：example1008.cpp---------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class A//类A定义

04　{

05　private：

06　int x；

07　public：

08　A（int xp=0）//构造函数

09　{

10　x=xp；

11　}

12　void dispA（）//成员函数dispA的定义

13　{

14　cout＜＜"Hello,this is A and x："＜＜x＜＜endl；

15　}

16　int GetX（）//成员函数用以读取private成员x的值

17　{

18　return x；

19　}

20　}；

21　class B：public A//类B由类A派生而来

22　{

23　private：//在基类A的基础上增加的数据成员

24　int y；

25　public：

26　B（int xp=1，int yp=1）：A（xp）//构造函数，在初始化表中调用基类的构造函数

27　{

28　y=yp；

29　}

30　void dispB（）//成员函数dispB

31　{

32　cout＜＜"Hello,this is B and x："＜＜GetX（）＜＜"，y："＜＜y＜＜endl；

33　}

34　}；

35　int main（）//主函数

36　{

37　A pt1（1）；//声明一个类A对象pt1

38　pt1. dispA（）；//通过pt1调用dispA函数

39　B pt2（2，3）；//声明一个派生类对象pt2

40　pt2. dispB（）；//通过pt2调用dispB函数

41　pt1=pt2；//派生类对象为基类对象赋值

42　pt1. dispA（）；

43　A＆rpt1=pt2；//派生类对象初始化基类对象引用

44　rpt1. dispA（）；

45　A*ppt1=＆pt2；//派生类对象地址为基类指针赋值

46　ppt1-＞dispA（）；

47　（（B*）ppt1）-＞dispB（）；//基类指针向派生类指针的强制转换

48　return 0；

49　}

输出结果如下所示。

Hello,this is A and x：1

Hello,this is B and x：2，y：3

Hello,this is A and x：2

Hello,this is A and x：2

Hello,this is A and x：2

Hello,this is B and x：2，y：3

【代码解析】代码10.8中，在基类A的基础上派生了类B，代码第41行是由派生类B的对象向基类A对象赋值，用派生类B的对象初始化基类A的引用，用派生类地址为基类指针赋值都是合法的，如果要使用基类指针访问派生类中添加的成员，必须采用诸如“（（B*）ppt1）”的强制转换形式，而且必须保证强制转换后的指针所指内存有效，见代码第47行。

10.7.2　多基继承时的情况

相比单基派生的情况，多基派生略显复杂，不过基本原理是一样的，所有的派生类都适应于基类，举例来说，如图10.9所示的继承结构，类层次的定义如下所示。

图　10.9　多基派生时的类型适应

class A

class B

class C：public A

class D：public B

class E：public C,public D

派生类E中各成员在内存中的分布如图10.10所示，对本例来说，下列用法都是合法的，a、b、c、d和e分别代表类A、B、C、D和E的对象，pA、pB、pC、pD和pE分别代表5个类的指针。

1.对象赋值和引用初始化

a=c；

a=e；

b=d；

b=e；

c=e；

d=e；

A＆rA=e；//其他情况省略，与赋值完全一致2.指针转换

pA=pC；//或pA=＆c；下同

pA=pE；

pB=pD；

pB=pE；

pC=pE；

pD=pE；

以pB=pE为例，赋值后类B指针pB指向pE所指的类E对象中类B复制的首地址，这样，便可以通过pB对pE所指的类E对象中类B复制部分进行访问。

图　10.10　派生类对象成员的内存模型

10.7.3　公共基类

如果多基派生时出现公共基类，并且没有对该基类使用virtual虚拟派生，在对象赋值和指针转换时，可能会出现二义性错误。举例来说，如图10.11所示的派生结构，其类定义简写为如下形式。

图　10.11　共同基类时的情况

class A

class B：public A

class C：public A

class D：public B,public C

此时，汇聚处类D对象的成员在内存中的分布如图10.12，此时，在类D对象中会出现两份类A对象的副本。

此时，虽然下述语句是合法的，如下所示。

a=b；

a=c；

b=d；

c=d；

pA=pB；

pA=pC；

pB=pD；

pC=pD；但在执行下述语句时，会导致二义性错误。

a=d；

pA=pD；

图　10.12　共同基类多基派生对象的内存分布图

在类D对象d中有两份类A对象的副本，所以C++编译器不知道究竟应该用哪一个来对类A对象赋值，究竟用哪一个的地址来对类A指针pA赋值，会导致二义性错误。

克服这种二义性的方法如下所述。

1.对指针显式指明路径

pA=（A*）（B*）pD；

这样，编译器便可以明确地判断出应使用类D中类B复制中的类A副本的首地址来为pA赋值。

2.先将指针转到不会产生二义性的类型

pA=（B*）pD；//隐式转换

或

B*pTemp=pD；

pA=pTemp；3.对象赋值采用强制类型转换

a=B（d）；//C语言的写法也可以，即a=（B）d；当使用强制转换时，使用pA指向类D对象d中的其他成员，也要显式指明路径，如下列语句是错误的。

D*ppD=（D*）pA；正确的写法应该是如下形式。

D*ppD=（D*）（B*）pA；

10.7.4　虚基类的情况

对图10.11表示的继承结构来说，如果使用类A虚拟派生出类B和类C，则在类D的对象中只有一份类A对象的副本，类D对象的内存分布与图10.12有所不同。此时，派生类对象d的地址可以直接赋给类A指针，不需要指明路径和强制转换，如下所示。

pA=＆d；由类D对象对类A对象的赋值和初始化引用也是合法的，如下所示。

a=d；

A＆rA=d；需要特别注意的是，相反的转换是不允许的，即使添加了全部路径和强制转换也不可以，如下所示。

D*ppD=（D*）（B*）pA；//非法

上述语句会产生编译错误，这是因为系统在为类D对象分配内存时间，虚基类中的数据成员在派生类对象中的布局和非虚基派生时有所不同，所以，不能将指向虚基类的指针（或引用）置回指向派生类。

10.8　小结

本章主要讨论了C++面向对象设计中的继承问题。继承的重要性是支持程序代码复用，它能够从已存在的类中派生出新类，继承基类的成员，而且可以通过覆盖基类成员函数，产生新的行为。派生可以从一个基类派生，称为单基继承；也可以从多个基类派生，称为多基继承；派生类也可以再派生出新类，构成多级继承结构。

本章介绍并引入了一种新的数据成员访问权限protected，对象中的protected成员无法在外部访问，但可以被有血缘关系的类访问。派生有3种方式，各方式的不同在于派生后基类成员在派生类的访问权限不同。

派生类的构造函数和析构函数与普通类略有不同，必须将对基类的构造函数调用放在初始化表中。多基派生时，各基类构造函数的调用顺序取决于类定义时基类的顺序，同初始化表列出的构造函数的调用顺序无关。

继承可以在基类的基础上扩展功能，促进代码复用，虽然很有用但继承不是万金油，在类设计时，应具体分析是继承还是组合，关键看对象间的关系是IS-A还是HAS-A。

为了说明基类对象、指针和派生类对象以及指针的相互转化，引入了“类型适应”的概念，原则上说，派生类适应基类，即应该使用基类对象（或指针）的场合，可以无条件地使用派生类对象（或指针），此外，应用最多的还是基类对象和派生类对象的赋值，以及基类指针和派生类指针的赋值等，在多基继承时，情况会稍复杂些，但只要理解了派生类对象的内存模型，便能很容易掌握转化的规律。

10.9　上机实践习题

1.定义一个类point、然后将这个类作为基类，再定义一个类point3d让其继承于类point，然后定义类point3d的对象，最后调用显示输出的数据。

【提示】上述题目主要是要求读者熟悉类继承的相关知识，重点是掌握类继承的概念及其作用。

【关键代码】

01　class point

02　{

03　public：//public成员列表

04　int m_x；

05　int m_y；

06

07　point（）

08　{

09　m_x=0；

10　m_y=0；

11　}

12

13　point（int x,int y）

14　{

15　m_x=x；

16　m_y=y；

17　}

18

19　void display（）

20　{

21　cout＜＜"point x="＜＜m_x＜＜"point y="＜＜m_y＜＜endl；

22　}

23　}；

24

25　class point3d：public point

26　{

27　private：

28　int m_z；

29　public：

30　point3d（）：point（）

31　{

32　m_z=0；

33　}

34　point3d（int x,int y,int z）：point（x,y）

35　{

36　m_z=z；

37　}

38　void display（）

39　{

40　cout＜＜"point3d x y z"＜＜m_x＜＜""＜＜m_y＜＜""＜＜m_z＜＜endl；

41　}

42　}；

2.用模拟代码来练习虚基派生的使用。

【提示】上述题目主要是要求读者掌握虚基类的相关知识，重点是掌握虚基派生如何实现。

【关键代码】

01　class A//公共虚基类A

02　{

03　protected：//protected成员列表

04　int x；

05　public：

06　A（int xp=0）//构造函数，带默认构造参数

07　{

08　x=xp；

09　}

10　void SetX（int xp）//SetX函数用以设置protected成员x

11　{

12　x=xp；

13　}

14　void print（）//print函数输出信息

15　{

16　cout＜＜"this is x in A："＜＜x＜＜endl；

17　}

18　}；

19　class B：virtual public A//类B由类A虚基派生而来

20　{

21　}；

22　class C：virtual public A//类C由类A虚基派生而来

23　{

24　}；

25　class D：public B,public C//类D由类B和类C共同派生

26　{

27　}；

第11章　多态

多态性是面向对象程序设计语言的基本特征。仅将数据和函数捆绑在一起，进行类的封装，使用一些简单的继承，还不能算是真正应用了面向对象的程序设计思想。多态性是面向对象的精髓。多态性可以简单地概括为“一个接口，多种方法”，前面讲过的函数重载就是一种简单的多态，一个函数名（调用接口）对应着几个不同的函数原型（方法）。

本章主要涉及以下知识点。

❑多态与虚函数：介绍虚函数的概念、声明及其定义。

❑虚函数的访问：介绍虚函数的各种使用方法。

❑纯虚函数与抽象类：介绍纯虚函数和抽象类的概念、声明和使用。

❑虚函数引用的二义性：介绍虚函数的优点和缺点。

❑重载、覆盖与隐藏：介绍重载、覆盖和隐藏的概念及示例。

11.1　多态与虚函数

通俗地说，多态性是指同一个操作作用于不同的对象就会产生不同的响应；多态性分为静态多态性和动态多态性，其中函数重载和运算符重载属于静态多态性，虚函数属于动态多态性。函数重载和运算符重载在前面已经讲过，虚函数是本章的重点，C++是依靠虚函数来实现动态和多态的。在进一步讲述多态前，先要了解几个概念。

11.1.1　静态联编

程序调用函数时，具体应使用哪个代码块是由编译器决定的。举函数重载为例，C++编译器根据传递给函数的参数和函数名来决定具体要使用哪一个函数，称为联编（Binding）。编译器可以在编译过程中完成这种联编，在编译过程中进行的联编叫静态联编（Static Binding）或早期联编（Early Binding）。

11.1.2　动态联编

在一些场合下，编译器无法在编译过程中完成联编，必须在程序运行时完成选择，因此编译器必须提供一套称为动态联编（Dynamic Binding）的机制，也叫晚期联编（Late Binding），C++通过虚函数来实现动态联编。

11.1.3　为什么需要虚函数

结合一段示例代码11.1来分析虚函数的作用，以帮助读者理解使用多态的意义。

代码11.1　没有使用虚函数带来的问题VirtualProblem

＜---------------------------文件名：example1101.cpp--------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class base//基类base定义

04　{

05　public：

06　void disp（）//基类base中的普通成员函数disp

07　{

08　cout＜＜"hello,base"＜＜endl；

09　}

10　}；

11　class child1：public base//派生类child1从base派生而来

12　{

13　public：

14　void disp（）//派生类child1中定义的disp函数将base类中定义的disp函数隐藏

15　{

16　cout＜＜"hello,child1"＜＜endl；

17　}

18　}；

19　class child2：public base//派生类child2从base派生而来

20　{

21　public：

22　void disp（）//派生类child2中定义的disp函数同样会隐藏base类中定义的disp函数

23　{

24　cout＜＜"hello,child2"＜＜endl；

25　}

26　}；

27　void display（base*pb）//display函数，以base指针为参数

28　{

29　pb-＞disp（）；

30　}

31　int main（）

32　{

33

34　base*pBase=NULL,obj_base；//创建一个基类指针pBase，初始化为NULL，创建一个base类对象obj_base

35　obj_base. disp（）；//通过对象名调用disp函数

36　pBase=＆obj_base；//使用obj_base的地址为pBase赋值

37　pBase-＞disp（）；//通过指针调用disp函数

38　child1*pChild1=NULL,obj_child1；

39　//创建一个child1类指针pChild1，初始化为NULL，创建一child1类对象obj_child1

40　obj_child1. disp（）；//通过对象名调用disp函数

41　pChild1=＆obj_child1；//使用obj_child1的地址为pChild1赋值

42　pChild1-＞disp（）；//通过指针调用disp函数

43　child2*pChild2=NULL,obj_child2；

44　//创建一个child2类指针pChild2，初始化为NULL，创建一个child2类对象obj_child2

45　obj_child2. disp（）；//通过对象名调用disp函数

46　pChild2=＆obj_child2；//使用obj_child2的地址为pChild2赋值

47　pChild2-＞disp（）；//通过指针调用disp函数

48　pBase=＆obj_child1；//使用obj_child1的地址为pBase赋值

49　pBase-＞disp（）；//通过指针pBase调用disp函数

50　display（＆obj_base）；//函数调用

51　display（＆obj_child1）；

52　display（＆obj_child2）；

53　return 0；

54　}

输出结果如下所示。

hello,base

hello,base

hello,child1

hello,child1

hello,child2

hello,child2

hello,base

hello,base

hello,base

hello,base

【代码解析】代码的输出结果多少让人有些出乎意料，前6个输出操作没有问题，分别根据对象名和本类指针访问类中的成员，但第7个输出操作，代码第48～49行如下所示。

pBase=＆obj_child1；

pBase-＞disp（）；

仍旧输出了“hello,base”，而不是“hello,child1”，这说明，通过指针访问对象成员时，具体要访问哪个对象只取决于指针的类型，而与用什么对象给这个指针赋值无关。这也解释了代码11.1中3次调用display函数，全都输出了“hello,base”的原因。

是时候引入虚函数了，其定义和相关内容将在稍后章节介绍，这里只对代码11.1进行简单的修改，在base类的disp函数前加上关键字virtual进行修饰，如下所示。

virtual void disp（）

{

cout＜＜"hello,base"＜＜endl；

}其他保持不变，编译链接并运行后

输出结果如下所示。

hello,base

hello,base

hello,child1

hello,child1

hello,child2

hello,child2

hello,child1

hello,base

hello,child1

hello,child2

通过对象名和本类指针进行的前6个输出结果没有变化，第7个输出变为“hello,child1”，3次display函数调用也根据输入对象的不同有了不同的结果。

从输出的结果可知，当通过指针调用virtual修饰的虚函数时，具体调用哪个版本不再取决于指针的类型，而是取决于指针所指的对象类型。

这样做的好处在于以下两点。

❑在程序中只要定义了诸如代码11.1中display这样的函数，只要以基类指针为参数，就可以将派生类对象的地址作为参数，根据参数对象类型的不同，执行的操作也会不同，真正体现了“一种接口，多种方法”，省去了重复定义同功能函数的麻烦。

❑通过基类指针能处理所有派生类中的情况，相当于现在的代码可以服务于未来要添加的类（派生类）。

11.1.4　虚函数的声明和定义

虚函数的定义很简单，只要在成员函数原型前加一个关键字virtual即可。如果一个基类的成员函数定义为虚函数，那么，其所有派生类中也将保持为虚函数，即便是在派生类中省略了virtual关键字，结果也是一样的。换言之，一个虚函数是属于其所在的类的层次的，不光属于其定义所在类，只不过是其在该类层次结构中的不同类中有不同的形态。一旦一个函数被声明为虚函数，不论其经历多少次派生，都会保持其虚函数的特性，即使在派生类中没有使用virtual关键字，其仍然是虚函数。

派生类中可根据需要对虚函数进行重定义，重定义的格式有以下要求。

❑与基类的虚函数有相同的参数个数。

❑其参数的类型与基类的虚函数的对应参数类型相同。

❑其返回值要么与基类虚函数相同，要么都返回指针（或引用），并且派生类虚函数所返回的指针（或引用）类型是基类中被替换的虚函数所返回的指针（或引用）类型的子类型（派生类型）。

一般要求在基类中说明了虚函数后，在派生类说明的虚函数应该与基类中虚函数的参数个数相等，对应参数的类型相同，如果不相同，则将派生类虚函数的参数的类型强制转换为基类中虚函数的参数类型。

需要特别强调以下几点。

（1）virtual应用于修饰public或protected成员函数。private成员既无法从外部访问，也无法在派生类中访问，使用virtual修饰private函数尽管编译器不会报错，但没有意义。

（2）如果派生类中没有对基类中虚函数进行重定义，则它将继承基类中的虚函数。

（3）友元不能是虚函数，因为友元不是类成员，只有成员才能是虚函数，但可以在友元内调用虚函数来解决问题。反之，虚函数可以是另一个类的友元函数。

11.2　虚函数的访问

对虚函数的访问方式不同，程序具体调用哪个函数可能也会有所不同。为了便于说明，本节统一采用类定义如下所示。

＜------------------------文件名：classdef.h---------------------------------＞

#include＜iostream＞

using namespace std；

class base//基类定义

{

public：

virtual void disp（）//虚函数

{

cout＜＜"hello,base"＜＜endl；

}

}；

class child：public base//派生类定义

{

public：

void disp（）//虚函数的覆盖（对普通函数来说是隐藏）

{

cout＜＜"hello,child"＜＜endl；

}

}；

从上述类定义可以看出，base类和child类中的disp函数都是虚函数（尽管在child类没有用virtual显式修饰disp函数，只要在基类中进行了说明，在所有派生类中，该函数都是虚函数）。

11.2.1　对象名访问

同普通函数一样，虚函数一样可以通过对象名来调用，如代码11.2所示。

代码11.2　使用对象名访问虚函数CallVirtualFuncByName

＜-------------------------文件名：example1102.cpp----------------------------＞

01　#include"classdef.h"

02　int main（）

03　{

04　base obj_base；//创建基类对象obj_base

05　child obj_child；//创建派生类对象obj_child

06　obj_base. disp（）；//通过对象名调用虚函数

07　obj_child. disp（）；//通过对象名调用虚函数

08　obj_child. base：disp（）；//通过类名加作用域限定符指明要调用的版本

09　obj_child. child：disp（）；//通过类名加作用域限定符指明要调用的版本

10　return 0；

11　}

输出结果如下所示。

hello,base

hello,child

hello,base

hello,child

【代码解析】代码第6～7行，通过对象名来访问虚函数，此时编译器采用的是静态联编，在child类中还可以使用作用域运算符来指定使用的是哪个类的函数。使用对象名调用虚函数和前面所讲的使用对象名调用非虚函数没有差别。

11.2.2　指针访问

使用指针访问非虚函数时，编译器根据指针的类型来决定要调用哪个函数，而不是根据指针指向的对象类型，而使用指针访问虚函数时，编译器根据指针所指对象的类型来实现动态联编，具体采用哪个函数与指针的类型无关。使用指针访问是虚函数调用的最主要形式，如示例代码11.3所示。

代码11.3　使用指针访问虚函数CallVirtualFuncByPointer

＜-------------------------文件名：example1103.cpp----------------------------＞

01　#include"classdef.h"

02　int main（）

03　{

04

base obj_base；

//创建一个基类对象

05

base*pBase=＆obj_base；

//使用基类对象地址为基类指针赋值

06

child obj_child；

//创建一个派生类对象

07

child*pChild=＆obj_child；

//使用派生类对象地址为派生类指针赋值

08

pBase-＞disp（）；

//使用基类指针调用虚函数

09

pChild-＞disp（）；

//使用派生类指针调用虚函数

10

pBase=pChild；

//将派生类指针赋值给基类指针

11

pBase-＞disp（）；

//使用基类指针调用虚函数

12

pChild=（child*）＆obj_base；

//使用基类对象地址为派生类指针赋值

13

pChild-＞disp（）；

//使用派生类指针调用虚函数，只取决于赋值对象

14

pChild-＞base：disp（）；

//使用类名加作用域限定符指明要调用的版本

15

return 0；

16　}

输出结果如下所示。

hello,base

hello,child

hello,child

hello,base

hello,base

【代码解析】指针的类型不是函数调用的决定因素，决定权在于对象属于哪个类，这更符合现实问题的特点。如代码第11行，这时使用基类指针调用虚函数，只会调用子类的虚函数。还要注意的是在派生类中也可以通过“类名：函数名”的形式访问基类中的虚函数，不过使用了作用域运算符后，编译器会将其作静态联编处理。

11.2.3　引用访问

使用引用访问虚函数，与使用指针访问虚函数类似，不同的是引用已经声明，不能修改，因此在使用上有一定限制，但这在一定程度上提高了代码的安全性，特别体现在函数参数传递等场合中，可以将引用理解成一种“受限制的指针”，如代码11.4所示。

代码11.4　使用引用访问虚函数CallVirtualFuncByRef

＜--------------------------文件名：example1104.cpp---------------------------＞

01　#include"classdef.h"

02　int main（）

03　{

04　base obj_base；//创建基类对象

05　child obj_child；//创建派生类对象

06　base＆rBase1=obj_base；//声明基类引用，用基类对象初始化

07　rBase1. disp（）；//基类引用调用虚函数，基类中的disp版本

08　base＆rBase2=obj_child；//声明基类引用，用派生类对象初始化

09　rBase2. disp（）；//基类引用调用虚函数，派生类中的disp版本

10　return 0；

11　}

输出结果如下所示。

hello,base

hello,child

【代码解析】从代码11.4可以看出，使用引用访问虚函数同样与引用类型无关，只取决于引用初始化的对象，如代码第9行，是基类引用调用虚函数，派生类中的disp版本。因此，通过引用访问虚函数时，编译器会进行动态联编处理。

11.2.4　类内访问

在类内的成员函数中访问该类层次中的虚函数，要使用this指针，见代码11.5所示。

代码11.5　类内成员函数访问虚函数CallVirtualFuncInsideClass

＜-------------------------文件名：example1105.cpp----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class base//基类定义

04　{

05　public：

06　virtual void disp（）//虚函数disp

07　{

08　cout＜＜"hello,base"＜＜endl；

09　}

10　void call_base_1（）

11　{

12　this-＞disp（）；//直接用disp

13　}

14　void call_base_2（）

15　{

16　base：disp（）；

17　}

18　}；

19　class child：public base//派生类定义

20　{

21　public：

22　void disp（）//对虚函数进行了覆盖定义

23　{

24　cout＜＜"hello,child"＜＜endl；

25　}

26　void call_child_1（）

27　{

28　disp（）；

29　}

30　}；

31　int main（）

32　{

33　base obj_Base；//声明一个基类对象

34　child obj_Child；//声明一个派生类对象

35　obj_Base. call_base_1（）；//基类对象调用非虚函数call_base_1

36　obj_Child. call_child_1（）；//派生类对象调用非虚函数call_child_1

37

38　base*pBase=＆obj_Base；//声明一个基类指针，并用基类对象地址为其初始化

39　pBase-＞call_base_1（）；//使用基类指针在成员函数内调用虚函数

40　pBase-＞call_base_2（）；

41

42　pBase=＆obj_Child；//用派生类对象地址为基类指针初始化

43　pBase-＞call_base_1（）；//使用基类指针在成员函数内调用虚函数

44　pBase-＞call_base_2（）；

45　return 0；

46　}

输出结果如下所示。

hello,base

hello,child

hello,base

hello,base

hello,child

hello,base

【代码解析】编译器对使用对象名进行的虚函数调用使用静态联编，而且对使用诸如“类名：虚函数”等使用了作用域运算符的调用静态联编，此时，具体调用哪个函数取决于指针的类型。

函数“void call_base_1（）”和“void call_child_1（）”中对虚函数的调用保持了虚函数的动态联编，关键在于this指针的应用，见代码第12行。

11.2.5　在构造函数或析构函数中进行访问

构造函数和析构函数是特殊的成员函数，在其中访问虚函数时，C++采用静态联编，即在构造函数或系统函数内，使用“this-＞虚函数名”的形式来调用，编译器仍将其解释为静态联编的“本类名：虚函数名”。

如下例所示。

class base

{

public：

virtual void disp（）

{

cout＜＜"hello,base"＜＜endl；

}

}；

class child：public base

{

public：

child（）

{

disp（）；

}

void disp（）

{

cout＜＜"hello,child"＜＜endl；

}

}；

使用上述代码定义的类创建child类对象时，输出信息总是“hello,child”，换言之，在child的构造函数中，不论是用disp（）还是this-＞disp（）来调用，编译器都会将其解释为child：disp（），此时，若想在child构造函数中调用base类的disp函数，必须使用作用域运算符，即base：disp（）的形式。

注意

在构造函数和析构函数中调用虚函数并未体现虚函数真正的意义，不推荐这种用法，在类设计时应尽量避免。

11.3　纯虚函数与抽象类

当在基类中无法为虚函数提供任何有实际意义的定义时，可以将该虚函数声明为纯虚函数，它的实现留给该基类的派生类去做。

11.3.1　纯虚函数的声明和定义

纯虚函数是一种特殊的虚函数，它的一般格式如下所示。

class类名

{

virtual类型函数名（参数表）=0；

……

}；

纯虚函数不能被直接调用，仅提供一个与派生类一致的接口，如代码11.6所示。

代码11.6　纯虚函数使用举例PureVirtualFuncSample

＜-------------------------文件名：example1106.cpp----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class A//类A定义

04　{

05　public：

06　virtual void disp（）=0；//纯虚函数，类A作为抽象类

07　}；

08　class B：public A//类B由抽象类A派生而来

09　{

10　public：

11　virtual void disp（）//此处virtual可省略，继承

12　{

13　cout＜＜"This is from B"＜＜endl；

14　}

15　}；

16　class C：public B//类C从类B派生而来

17　{

18　public：

19　virtual void disp（）

20　{

21　cout＜＜"This is from C"＜＜endl；

22　}

23　}；

24　void display（A*a）//display函数，以类A指针对参数

25　{

26　a-＞disp（）；

27　}

28　int main（）

29　{

30　B*pB=new B；//动态内存申请

31　C*pC=new C；

32　display（pB）；//取决于为指针赋值的数据类型

33　display（pC）；

34　return 0；

35　}

输出结果如下所示。

This is from B

This is from C

【代码解析】代码第6行的，类A虚函数disp仅起到为派生类提供一个一致接口的作用，派生类中重定义的disp（）用来决定具体要完成的动作，通过这个例子，读者应仔细体会一致接口的意义所在。

11.3.2　抽象类

一个类可以包含多个纯虚函数。只要类中含有一个纯虚函数，该类便为抽象类。一个抽象类只能作为基类来派生新类，不能创建抽象类的对象，如代码11.6中的类A便是抽象类，创建类A的对象是非法的，如下所示。

A a；//错误：A为抽象类但可声明一个指向抽象类的指针，如下所示。

A*a=NULL；

A*a=new B；

注意

“A*a=new A；”非法，因为该语句试图创建A的对象。

同普通的虚函数不同，纯虚函数不能被自动继承，在派生类中必须对基类中的虚函数进行重定义，或者在派生类中再次将该虚函数声明为纯虚函数，否则编译器将提示错误信息。这说明，抽象类的派生类也可以是抽象类，只有在派生类中给出了基类中所有纯虚函数的实现时，该派生类便不再是抽象类。同纯虚函数一样，抽象类只起到提供统一接口的作用。

注意

所谓不能继承，指的是纯虚函数的“纯”不会被自动继承，virtual属性还是会保持下来的，也就是说，在代码11.6中，即使类B的disp函数在定义时去掉virtual，该函数仍旧是虚函数。

在成员函数内可以调用纯虚函数，但在构造函数或者析构函数内调用一个纯虚函数将导致程序运行错误，因为在函数中没有纯虚函数定义代码。此外，还要注意区分纯虚函数和空的虚函数的区别，如下所示。

virtual void disp（）=0；//纯虚函数

virtual void disp（）//空的虚函数

{

}；

空的虚函数指的是函数体为空，不执行任何操作，而纯虚函数是未定义任何操作。

先来看一个抽象类的使用范例，如代码11.7所示。

代码11.7　抽象类的应用AbstractClass

＜-------------------------------文件名：shape.h------------------------------＞

01　#include＜iostream＞

02　#include＜cmath＞

03　using namespace std；

04　#define PI 3.1415926//宏定义

05　class Figure//图形基类定义

06　{

07　public：

08　virtual float Area（）=0；//纯虚函数，因此Figure类是抽象类，无法声明其对象

09　virtual void DispName（）=0；

10　}；

11　class Circle：public Figure//在抽象类Figure的基础上派生Circle圆类

12　{

13　private：//private成员列表

14　float radius；//半径

15　public：

16　Circle（float r=0）//构造函数

17　{

18　radius=r；

19　}

20　virtual void DispName（）//覆盖实现了虚函数DispName，此处去掉virtual没有影响

21　{

22　cout＜＜"圆："＜＜endl；

23　}

24

25　virtual float Area（）//覆盖实现了虚函数Area，用来计算圆的面积，去掉virtual同样没有影响

26　{

27　return PI*radius*radius；

28　}

29　}；

30　class Rectangle：public Figure//在抽象类Figure的基础上派生Rectangle矩形类

31　{

32　private：

33　float x；//两个边长x和y

34　float y；

35　public：

36　Rectangle（float xp=0，float yp=0）//构造函数

37　{

38　x=xp；

39　y=yp；

40　}

41

42　virtual void DispName（）//覆盖实现了虚函数DispName，此处去掉virtual没有影响

43　{

44　cout＜＜"矩形："＜＜endl；

45　}

46　//覆盖实现了虚函数Area，用来计算矩形面积，此处去掉virtual同样没有影响

47　virtual float Area（）

48　{

49　return x*y；

50　}

51

52　}；

53　class Triangle：public Figure//在抽象类Figure的基础上派生Triangle三角形类

54　{

55　private：//三角形的三个边长

56　float x；

57　float y；

58　float z；

59　public：

60　Triangle（float xp=0，float yp=0，float zp=0）//构造函数

61　{

62　x=xp；

63　y=yp；

64　z=zp；

65　}

66

67　virtual void DispName（）{//覆盖实现了虚函数DispName，此处去掉virtual没有影响

68　cout＜＜"三角形："＜＜endl；

69　}

70

71　virtual float Area（）//覆盖实现了虚函数Area，用来计算三角形的面积，此处去掉virtual没有影响

72　{

73　float p=（x+y+z）/2；

74　return sqrt（p*（p-x）*（p-y）*（p-z））；

75　}

76　}；

＜-------------------------文件名：example1107.cpp----------------------------＞

77　#include"shape.h"

78　int main（）

79　{

80　Figure*pF=NULL；//虽然不能创建Figure类对象，但可声明Figure型的指针

81　Circle c（3）；//声明一个圆对象，半径为3

82　Rectangle r（1. 2f，3.6f）；//声明一个矩形对象，其边长分别为1.2和3.6

83　Triangle t（6，7，8）；//声明一个三角形对象，其边长分别为6、7和8

84　pF=＆c；//用圆对象c的地址为pF赋值

85　pF-＞DispName（）；//调用函数DispName时，对应着Circle类中的版本

86　cout＜＜pF-＞Area（）＜＜endl；//调用函数Area时，对应着Circle类中的版本

87　pF=＆r；//用矩形对象r的地址为pF赋值

88　pF-＞DispName（）；//调用函数DispName时，对应着Rectangle类中的版本

89　cout＜＜pF-＞Area（）＜＜endl；//调用函数Area时，对应着Rectangle类中的版本

90　pF=＆t；//用三角形对象t的地址为pF赋值

91　pF-＞DispName（）；//调用函数DispName时，对应着Triangle类中的版本

92　cout＜＜pF-＞Area（）＜＜endl；//调用函数Area时，对应着Triangle类中的版本

93　return 0；

94　}

输出结果如下所示。

圆：

28　.2743

矩形：

4.32

三角形：

20　.3332

【代码解析】shape.h文件中定义了抽象基类Figure，并由其派生出3个新类如下所示。

Circle、Rectangle、Triangle

代码第8行，在抽象类Figure中定义的纯虚函数Area用于求图形的面积，代码第9行的DispName（）用于输出类名，在Figure类中，求面积和输出类名的操作没有实际意义，所以将两个函数定义为纯虚函数，为派生类中对应的虚函数提供了访问接口。

在main函数中只定义了指向抽象基类Figure的指针pF，使用不同的派生类对象为该指针赋值时，“pF-＞Area（）”和“pF-＞DispName（）”执行不同的操作，真正实现了“一个接口，多种方法”，体现了多态性的程序设计理念。

11.3.3　另一种抽象类：类中只定义了protected型的构造函数

对一个类来说，如果只定义了protected型的构造函数而没有提供public构造函数，无论是在外部还是在派生类中都不能创建该类的对象，但可以由其派生出新的类，这种能派生出新类，却不能创建自己对象的类是另一种形式的抽象类，代码11.8中便定义了一个构造函数为protected型的类。

代码11.8　protected型的构造函数ProtectedConstructor

＜-------------------------文件名：example1108.cpp----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class Base//基类定义

04　{

05　private：

06　int x；

07　protected：

08　Base（int xp=0）//构造函数声明为protected型

09　{

10　x=xp；

11　}

12　public：

13　void disp（）

14　{

15　cout＜＜"x is"＜＜x＜＜endl；

16　}

17　}；

18　class Child：public Base

19　{

20　public：

21　//Base b；错误，在派生类中不能创建Base类对象

22　Child（int xpp）：Base（xpp）//但派生类中可调用protected构造函数

23　{

24　}

25　}；

26　int main（）

27　{

28　Child c（1）；

29　//Base b；错误，在外部不能创建Base类对象

30　c. disp（）；

31　return 0；

32　}

输出结果如下所示。

x is 1

【代码解析】代码第8行的Base类中只定义了一个protected型的构造函数，此时，无论在外部函数（如main函数）中还是在派生类中创建Base类的对象都是非法的，但是，由于protected型的构造函数可以在派生类中进行访问，因此，Base类可以派生出Child类。Base类是另一种形式的抽象类。

11.3.4　延伸：构造函数能否为private型

前面讲了构造函数可以为public型，也可以为protected型，但构造函数是否可以是private型呢？答案是肯定的。

此时，尽管不能直接在外部函数和派生类中使用“类名+对象名”的形式来创建该类对象，但可以通过类的static函数成员来创建类的实例，如代码11.9所示。

代码11.9　private型构造函数PrivateConstructor

＜-------------------------文件名：example1109.cpp----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class Example//类定义

04　{

05　private：

06　int x；

07　Example（int xp）//类的构造函数定义为private型

08　{

09　x=xp；

10　}

11　public：

12　static Example*CreateObject（int xp）//static函数，不用创建对象便可调用

13　{

14　return new Example（xp）；//申请动态内存

15　}

16　static void DeleteObject（Example*pE）//static函数，不用创建对象便可调用

17　{

18　delete pE；//释放申请到的内存资源

19　}

20　void disp（）//成员函数

21　{

22　cout＜＜"x is"＜＜x＜＜endl；

23　}

24　}；

25　int main（）

26　{

27　Example*pExample=Example：CreateObject（5）；

28　//使用类名访问static函数创建对象，返回指向动态内存的指针

29　pExample-＞disp（）；//功能访问

30　Example：DeleteObject（pExample）；//释放资源

31　return 0；

32　}

输出结果如下所示。

x is 5

【代码解析】代码演示了private构造函数的用法，正因为无法创建对象，所以，代码第12行，必须使用static成员函数来创建实例。通过对上述代码的分析可知，static函数同样可以应用在protected型构造函数中。

11.3.5　虚析构函数

虽然构造函数不能被定义成虚函数，但析构函数可以定义为虚函数。通常情况下如果类中定义了虚函数，析构函数也应被定义为虚析构函数，尤其是类内有申请的动态内存，需要清理和释放的时候。先来看个简单的例子，如代码11.10所示。

代码11.10　析构函数调用不当带来的内存泄露ImproperDestructor

＜-------------------------文件名：example1110.cpp----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class Base//基类定义

04　{

05　private：//字符指针

06　char*data；

07　public：

08　Base（）//无参构造函数

09　{

10　data=new char[64]；//动态内存申请

11　cout＜＜"Base类构造函数被调用"＜＜endl；

12　}；

13　～Base（）//析构函数

14　{

15　delete[]data；//data指向的内存被释放

16　cout＜＜"Base类析构函数被调用"＜＜endl；

17　}；

18　}；

19　class Child：public Base//Child类由基类Base派生而来

20　{

21　private：

22　char*m_data；//增添的字符指针成员

23　public：

24　Child（）：Base（）//构造函数，初始化表中执行基类的构造函数

25　{

26　m_data=new char[64]；//动态申请内存，并将首地址赋给m_data

27　cout＜＜"Child类构造函数被调用"＜＜endl；

28　}；

29　～Child（）//析构函数

30　{

31　delete[]m_data；//内存资源释放

32　cout＜＜"Child类析构函数被调用"＜＜endl；

33　}；

34　}；

35　int main（）

36　{

37　Base*pB=new Child；//动态申请了一块Child大小的内存，赋给Base基类指针

38　delete pB；//执行基类析构函数

39　return 0；

40　}

输出结果如下所示。

Base类构造函数被调用

Child类构造函数被调用

Base类析构函数被调用

【代码解析】编译链接，程序没有任何错误，但细心的读者可能早已发现，代码11.10的语句存在内存泄露问题，main函数中语句“delete pB；”根据指针pB的类型决定应调用哪个类的析构函数，即编译器会调用Base类的析构函数，这意味着Child类的析构函数没有被调用，Child类中char型指针m_data指向的动态内存泄露。

将基类中的析构函数定义为虚析构函数可很好地解决这一问题，为了便于说明，我们用代码11.10的Child类为基础再派生一个GrandChild类，如代码11.11所示。

代码11.11　使用虚析构函数解决内存泄露问题VirtualDestructor

＜-------------------------文件名：example1111.cpp----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class Base//基类定义

04　{

05　private：

06　char*data；//字符指针

07　public：

08　Base（）//无参构造函数

09　{

10　data=new char[64]；//动态内存申请

11　cout＜＜"Base类构造函数被调用"＜＜endl；

12　}；

13　virtual～Base（）//虚析构函数

14　{

15　delete[]data；//data指向的内存被释放

16　cout＜＜"Base类析构函数被调用"＜＜endl；

17　}；

18　}；

19　class Child：public Base//Child类由基类Base派生而来

20　{

21　private：

22　char*m_data；//增添的字符指针成员

23　public：

24　Child（）：Base（）//构造函数，初始化表中执行基类的构造函数

25　{

26　m_data=new char[64]；//动态申请内存，并将首地址赋给m_data

27　cout＜＜"Child类构造函数被调用"＜＜endl；

28　}；

29　～Child（）//析构函数，继承虚拟virtual

30　{

31　delete[]m_data；//内存资源释放

32　cout＜＜"Child类析构函数被调用"＜＜endl；

33　}；

34　}；

35　class GrandChild：public Child//GrandChild类由Child类派生而来

36　{

37　private：

38　char*mm_data；//在Child类基础上增加的字符指针成员mm_data

39　public：

40　GrandChild（）//构造函数

41　{

42　mm_data=new char[64]；//动态内存申请

43　cout＜＜"GrandChild类构造函数被调用"＜＜endl；

44　}；

45　～GrandChild（）//虚析构函数，virtual从继承结构中得来

46　{

47　delete[]mm_data；//内存释放

48　cout＜＜"GrandChild类析构函数被调用"＜＜endl；

49　}；

50　}；

51　int main（）

52　{

53　Base*pB=new Child；//动态申请了一块Child大小的内存，赋给Base基类指针

54　delete pB；//Child类的析构函数执行，释放内存，不会泄露

55

56　Child*pC=new GrandChild；//动态申请了一块GrandChild大小的内存，赋给Child类指针

57　delete pC；//执行GrandChild类的析构函数，释放内存，不会泄露

58　return 0；

59　}

输出结果如下所示。

Base类构造函数被调用

Child类构造函数被调用

Child类析构函数被调用

Base类析构函数被调用

Base类构造函数被调用

Child类构造函数被调用

GrandChild类构造函数被调用

GrandChild类析构函数被调用

Child类析构函数被调用

Base类析构函数被调用

【代码解析】从代码11.11的输出结果可以看出，代码第54行“delete pB；”和代码第57行“delete pC；”的操作很好地调用了类层次结构上的所有析构函数，避免了内存泄露，关键是在Base类中析构函数用virtual修饰，定义为了虚析构函数，这意味着从该基类的所有派生类的析构函数都继承为虚函数，比如代码11.11中Child类和GrandChild类的析构函数，通过“delete指针”撤销指针所指的对象时，不再根据指针的类型，而是根据指针指向对象的类型来调用析构函数，很好地完成了内存清理的任务。

注意

虚析构函数的继承与普通虚函数的继承其函数名不同，除了“～”外，析构函数和类同名，所以只要基类的析构函数定义为虚函数，派生类中的析构函数便继承为虚函数。

11.4　虚函数引入的二义性

第10章中讨论了继承时的二义性问题，同样虚函数也存在二义性问题。本节分别从多基派生、共同基类（非虚基派生）和共同基类（虚基派生）3个方面进行讨论。

11.4.1　多基派生

分析如图11.1所示的DAG图，各类的定义如下所示。

＜-------------------------------文件名：ABCdef.h-----------------------------＞

#include＜iostream＞

using namespace std；

class A

{

public：

virtual void a（）//虚函数

{

cout＜＜"a（）in A"＜＜endl；

}

virtual void b（）//虚函数

{

cout＜＜"b（）in A"＜＜endl；

}

virtual void c（）//虚函数

{

cout＜＜"c（）in A"＜＜endl；

}

}；

class B

{

public：

virtual void a（）//虚函数

{

cout＜＜"a（）in B"＜＜endl；

}

virtual void b（）//虚函数

{

cout＜＜"b（）in B"＜＜endl；

}

void c（）//非虚函数

{

cout＜＜"c（）in B"＜＜endl；

}

void d（）//非虚函数

{

cout＜＜"d（）in B"＜＜endl；

}

}；

class C：public A,public B

{

public：

virtual void a（）//虚函数，覆盖

{

cout＜＜"a（）in C"＜＜endl；

}

void c（）//特殊

{

cout＜＜"c（）in C"＜＜endl；

}

void d（）//非虚函数，隐藏

{

cout＜＜"d（）in C"＜＜endl；

}

}；第11章已经将过，在使用类C对象名（或类C对象指针）调用成员函数b时（静态联编），编译器无法知道应该执行从类A继承来的函数版本还是应执行从类B继承而来的函数版本，会出现二义性错误，如下所示。

C obc；

C*pC=＆obc；

obc.b（）；//错误，二义性

pC-＞b（）；//错误，二义性

图　11.1　多基派生

虚函数访问方式如代码11.12所示，即用基类指针作为访问接口。

代码11.12　虚函数访问方式VirtualFuncAccess

＜-------------------------文件名：example1112.cpp----------------------------＞

01　#include"ABCdef.h"

02　int main（）

03　{

04　C obc；//声明一个派生类对象obc

05　A*pA=＆obc；//用派生类对象obc的地址为A类指针赋值

06　pA-＞a（）；

07　pA-＞b（）；

08　pA-＞c（）；

09　B*pB=＆obc；//用派生类对象obc的地址为B类指针赋值

10　pB-＞a（）；

11　pB-＞b（）；

12　pB-＞c（）；

13　pB-＞d（）；

14　return 0；

15　}

输出结果如下所示。

a（）in C

b（）in A

c（）in C

a（）in C

b（）in B

c（）in B

d（）in B

【代码解析】从代码11.12可以看出，对上述类定义而言，可以使用类A指针或类B指针指向类C对象，3个类（A、B、C）中的a函数都为虚函数，对虚函数来说，派生类中的定义会覆盖基类中的定义。因此代码第5行和第10行的“pA-＞a（）；”和“pB-＞a（）；”都输出了“a（）in C”。

但对“pA-＞b（）；”和“pB-＞b（）；”操作来说，编译器在类A和类B定义中b（）都为虚函数，但由于在类C中没有对b（）进行覆盖，所以编译器无法决定执行哪个函数版本，采用静态联编调用方式，输出结果分别为“b（）in A”和“b（）in B”。

3个类中函数c稍微特殊一点，在类A中c（）为虚函数，而在类B中c（）为普通函数，虽然在类C中重定义（不是覆盖）了c函数，此时“pA-＞c（）；”和“pB-＞c（）；”操作的输出结果取决于指针所在类定义中c函数是否为虚函数，因此，输出结果分别为“c（）in C”和“c（）in B”。

注意

类B和类C中都定义了非虚函数d，这种情况下，派生类C中的d（）会隐藏基类B中的d（），此时，使用类C对象或指针调用的将是类C中定义的d（），关于覆盖和隐藏的内容稍后会进行详细的介绍。

11.4.2　共同基类和虚继承

第10章中介绍过，对非虚基派生方式，无法用汇聚处的派生类对象为共同基类的对象或指针赋值，即无法使用共同基类的指针指向汇聚处的派生类对象，也无法发挥出多态的威力，采用虚基派生的方式只保留基类的一个副本，这时可以用汇聚处的派生类对象为共同基类的对象或指针赋值。图11.2对应的类定义如下所示。

图　11.2　虚基派生

＜-------------------------------文件名：ABCDdef.h----------------------------＞

#include＜iostream＞

using namespace std；

class A//公共基类

{

public：

virtual void a（）//虚函数

{

cout＜＜"a（）in A"＜＜endl；

}

virtual void b（）//虚函数

{

cout＜＜"b（）in A"＜＜endl；

}

virtual void c（）//虚函数

{

cout＜＜"c（）in A"＜＜endl；

}

virtual void d（）//虚函数

{

cout＜＜"d（）in A"＜＜endl；

}

}；

class B：virtual public A//虚基派生类B

{

public：

void a（）//虚函数

{

cout＜＜"a（）in B"＜＜endl；

}

void b（）//虚函数

{

cout＜＜"b（）in B"＜＜endl；

}

}；

class C：virtual public A//虚基派生类C

{

public：

void a（）//虚函数

{

cout＜＜"a（）in C"＜＜endl；

}

void b（）//虚函数

{

cout＜＜"b（）in C"＜＜endl；

}

void c（）//虚函数

{

cout＜＜"c（）in C"＜＜end1；

}

}；

class D：public B,public C//多基派生

{

public：

void a（）//虚函数

{

cout＜＜"a（）in D"＜＜endl；

}

void d（）//虚函数

{

cout＜＜"d（）in D"＜＜endl；

}

}；在上述类定义下，编写代码如下所示。

A*pA=new D；

pA-＞a（）；//输出：a（）in D，在D中进行了定义

pA-＞b（）；//错误，二义性

pA-＞c（）；//输出：c（）in A，从基类A中继承而来

pA-＞d（）；//输出：d（）in D，在D中进行了定义，与pA-＞a（）；类似

此时，上述代码中对b函数的调用会产生二义性，因为b函数没有在类D中重定义，那么在类D中继承而来的b函数是来自类B还是类C呢？编译器无法判断。

11.5　重载、覆盖与隐藏

对类层次中的同名成员函数来说，有3种关系：重载（Overload）、覆盖（Override）和隐藏（Hide、Oversee），理清3种关系，有助于写出高质量的代码。

11.5.1　重载

重载的概念相对简单，只有在同一类定义中的同名成员函数才存在重载关系，主要特点是函数的参数类型和数目有所不同，但不能出现函数参数的个数和类型均相同，仅依靠返回值类型不同来区分的函数，这和普通函数的重载是完全一致的。另外，重载和成员函数是否是虚函数无关，举例如下所示。

class A

{

……

virtual int fun（）；

void fun（int）；

void fun（double,double）；

……

}；

上述类A定义中的3个fun函数便是重载关系。

11.5.2　覆盖

覆盖指的是派生类的成员函数覆盖基类中的同名函数，要求两个函数的参数个数和类型相同，且基类函数必须是虚函数，这样，在派生类中便覆盖了该虚函数，以该派生类做基类再进行派生时，后续派生类继承的便是覆盖后的虚函数版本，除非在后续派生类中对该虚函数进行重定义。举例如下所示。

class A

{

……

virtual void fun1（int,int）；

virtual int fun2（char*）；

……

}；

class B：public A

{

……

void fun1（int,int）；

……

}；

class C：public B

{

……

int fun2（char*）；

……

}；

上述代码中，类B中fun1函数覆盖了类A中的fun1函数，同时，类B继承了类A中的fun2函数，类C继承了类B中的fun1函数，却重定义了fun2函数对类B从类A继承来的fun2函数进行了覆盖。

此时，若声明基类A型指针“A*pA=new C；”，根据虚函数的动态联编原则，“pA-＞fun1；”将执行类B中定义的fun1函数，而“pA-＞fun2；”将执行类C中定义的fun2函数。

11.5.3　隐藏

隐藏指的是在某些情况下，派生类中的函数屏蔽了基类中的同名函数，如下所述。

❑派生类中的函数与基类中的函数参数相同，但基类函数不是虚函数，同覆盖的区别在于基类函数是否是虚函数。

❑派生类中的函数与基类中的函数参数不同，不管基类函数是否是虚函数，基类函数都会被屏蔽，同重载的区别在于两个函数不在同一类中。

“屏蔽”指的是使用派生类对象调用其成员函数（静态联编）时，基类中的函数将不会被执行，执行的是派生类中的函数，即使传递的参数符合基类中的函数参数列表，基类函数也不会执行，编译器会给出错误提示，这相当于把基类的成员函数“隐藏”了起来，即命名的由来。如示例代码11.13所示。

代码11.13　类层次结构中的函数隐藏Oversee

＜-------------------------文件名：example1113.cpp----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class A//类A的定义

04　{

05　public：

06　void fun（int xp）//非虚成员函数fun，参数为int型

07　{

08　cout＜＜xp＜＜endl；

09　}

10　}；

11　class B：public A//类B由类A派生而来

12　{

13　public：

14　void fun（char*s）//覆盖，oversee，参数为字符串

15　{

16　cout＜＜s＜＜endl；

17　}

18　}；

19　int main（）

20　{

21　B obB；//声明一个类B对象

22　obB. fun（"hello"）；//字符串参数版本

23　obB. fun（2）；//错误，发生覆盖，找不到fun（int）

24　return 0；

25　}编译链接，系统给出错误信息如下所示。

cannot convert parameter 1 from'const int'to'char*'

【代码解析】代码11.13说明，基类A中定义的fun函数已经被代码第14行类B中定义的fun函数覆盖，使其不可见，在类B中重载fun函数可有效地解决这一问题，将类B的定义修改为如下所示。

class B：public A

{

public：

void fun（char*s）

{

cout＜＜s＜＜endl；

}

void fun（int xp）//重载

{

A：fun（xp）；

}

}；重新编译运行，

输出结果如下所示。

hello

2

读者通过对类B的修改过程可以更进一步地理解覆盖、重载以及隐藏的区别。

11.6　小结

多态性是面向对象程序设计的又一个重要特征，在C++中多态性是通过虚函数实现的。通过虚函数，可以用同一个指针（尤其是指向基类的指针）访问不同对象的成员函数。多态配合第10章中讲解的类型适应，可以为类层次中的函数调用提供统一接口，具有十分重要的意义。

本章首先介绍了静态联编和动态联编的含义，随后引出了虚函数的定义方式，不同方式的访问，虚函数的响应形式有所不同。在某些情况下，在基类中可能无法定义虚函数，这时可以将其设置为纯虚函数，此时的类称为抽象类。纯虚函数和抽象类只能提供接口的作用，抽象类无法创建对象。

同普通函数一样，多基派生时，虚拟函数也会存在二义性问题，在类设计时要合理规划，避免出现编译器无法判断的情况。从整个类层次的角度来分析，其中的同名成员函数存在3种关系，即重载、覆盖和隐藏，理解同名函数间的关系有助于代码的阅读和编写，也能够提高程序设计的效率。

11.7　上机实践习题

1.定义一个抽象类，然后将这个类作为基类，再派生两个子类，最后调用其中的虚函数显示输出数据。

【提示】上述题目主要是要求读者熟悉类继承的相关知识，重点是掌握继承的概念和作用。

【关键代码】

01　class base//基类定义

02　{

03　public：

04　virtual void disp（）//虚函数

05　{

06　cout＜＜"hello,base"＜＜endl；

07　}

08　}；

09　class child1：public base//派生类定义

10　{

11　public：

12　void disp（）

13　{

14　cout＜＜"hello,child1"＜＜endl；

15　}

16　}；

17

18　class child2：public base

19　{

20　public：

21　void disp（）

22　{

23　cout＜＜"hello,child2"＜＜endl；

24　}

25　}；

26

27　void disp（base*p）

28　{

29　p-＞disp（）；

30　}

2.定义一个抽象类Point，其中包括一个纯虚函数getArea（），然后派生两个子类CRect和CTrigon，最后在主函数中初始化并调用该虚函数输出结果。

【提示】上述题目主要是要求读者掌握纯虚函数的相关知识，重点是掌握纯虚函数的作用及其实现方法。

【关键代码】

01　class CPoint

02　{

03　public：

04　CPoint（float w,float h）；//构造函数

05　virtual float getArea（）=0；//求几何面积的纯虚函数

06　private：

07　float m_w,m_h；

08　}；

09　CPoint：CPoint（float w,float h）

10　{

11　m_w=w；

12　m_h=h；

13　}

14

15　class CRect：public CPoint

16　{

17　public：

18　CRect（float w,float h）；

19　float getArea（）；//纯虚函数在子类中的声明

20　private：

21　float m_w,m_h；

22　}；

23　CRect：CRect（float w,float h）

24　：CPoint（w,h）

25　{

26　m_w=w；

27　m_h=h；

28　}

29　float CRect：getArea（）//纯虚函数在子类中的实现

30　{

31　cout＜＜"调用CRect类的getArea成员函数"＜＜endl；

32　return m_w*m_h；

33　}

34

35　class CTrigon：public CPoint

36　{

37　public：

38　CTrigon（float f,float h）；

39　float getArea（）；//纯虚函数在子类中的声明

40　private：

41　float m_f,m_h；

42　}；

43　CTrigon：CTrigon（float f,float h）

44　：CPoint（f,h）

45　{

46　m_f=f；

47　m_h=h；

48　}

49　float CTrigon：getArea（）//纯虚函数在子类中的实现

50　{

51　cout＜＜"调用CTrigon类的getArea成员函数"＜＜endl；

52　return m_f*m_h/2；

53　}

54

55　void disp（CPoint*p）

56　{

57　cout＜＜"面积为："＜＜p-＞getArea（）＜＜endl；

58　}

第四篇　泛型编程

第12章　模板

现在的C++编译器实现了一项新的特性：模板（Template）。简单地说，模板是一种通用的描述机制，当使用模板时允许使用通用类型来定义函数或类等。通用类型可被具体的类型，如int,double甚至是用户自定义的类型来代替。模板引入了一种全新的编程思维方式，称为“泛型编程”或“通用编程”。

本章主要涉及以下知识点。

❑模板的用处：介绍了为什么要定义模板及其使用它的好处。

❑函数模板：介绍函数模板的定义及其使用。

❑类模板：介绍类模板的定义及其使用。

❑模板的嵌套：介绍如何在模板中再嵌套模板。

❑模板的参数：介绍模板中使用参数的注意事项。

12.1　为什么要定义模板

形象地说，把函数比喻为一个游戏过程，函数的流程就相当于游戏规则，在以往的函数定义中，总是指明参数是int型还是double型等，这就像是为张三（好比int型）和李四（好比double型）的比赛制定规则。可如果王五（char*型）和赵六（bool型）要比赛，还得提供一套函数的定义，这相当于又要制定一次规则，显然这是很麻烦的。模板的引入解决了这个问题，不管是谁和谁比赛，都把他们定义成A与B比赛，制定好了A与B比赛的规则（定义了关于A和B的函数）后，比赛时只要把A替换成张三，把B替换成李四就可以了，大大简化了程序代码量，保持了结构的清晰，提高了程序设计的效率。该过程称为“类型参数化”。

12.1.1　类型参数化

在讲解类型参数化之前，先来看一个示例代码12.1，如下所示。

代码12.1　函数重载FuncOverload

＜----------------------文件名：example1201.cpp-------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int add（const int x,const int y）//定义两个int类型相加的函数

04　{

05　return x+y；

06　}

07　double add（const double x,const double y）//重载两个double类型相加的函数

08　{

09　return x+y；

10　}

11　char*add（char*px,const char*py）//重载两个字符数组相加的函数

12　{

13　return strcat（px,py）；//调用库函数strcat

14　}

15　int main（）

16　{

17　cout＜＜add（1，2）＜＜endl；//调用add（const int,const int）

18　cout＜＜add（1. 0，2.0）＜＜endl；//调用add（const double,const double）

19　char x[20]="Hello"；//创建字符数组，注意要留够大小

20　char y[]="C++"；//

21　cout＜＜add（x,y）＜＜endl；//调用add（char*，char*）

22　return 0；

23　}

输出结果如下所示。

3

3

Hello C++

【代码解析】代码12.1分别重载了3个add函数，这时，编译器将根据传递的参数决定应调用的函数，如下所示。

const int x=add（3，2）；//调用int add（const int,const int）

const double y=add（3.0，2.0）；//调用double add（const double,const double）

如果需要处理其他类型，程序中必须提供相应的函数重载，这看起来会很麻烦，有没有其他更高效的方法呢？也许有的读者会认为可以通过宏定义#define add（a,b）（（a）+（b））来实现，但是指针（C风格字符串）不能直接相加，而且调用宏时，编译器并不对其中的参数进行类型检查，这给程序带来了很大的安全隐患。

C++引入了模板来解决这个问题，如代码12.2所示。

代码12.2　函数模板FuncTemplate

＜----------------------文件名：example1202.cpp-------------------------------＞

01　#include＜iostream＞

02　#include＜string＞//使用标准类库中提供的string类时必须

03　//包含此头文件

04　using namespace std；

05　template＜class T＞

06　T add（const T＆t1，const T＆t2）

07　{

08　return t1+t2；

09　}

10　int main（）

11　{

12　cout＜＜add（1，2）＜＜endl；//调用add（const int,const int）

13　cout＜＜add（1. 0，2.0）＜＜endl；//调用add（const double,const double）

14　string x（"Hello，"），y（"world"）；

15　cout＜＜add（x,y）＜＜endl；//调用add（string,string）

16　return 0；

17　}

输出结果如下所示。

3

3

Hello,world

【代码解析】代码12.2仅仅采用如下形式定义了一个函数。

template＜class T＞

T add（const T＆t1，const T＆t2）

{

return t1+t2；

}

与之前一直讨论的函数定义相比，最大的不同在于代码第6行使用的add函数并没有指定参数的类型，而是用T来代替，现在，C++编译器可以根据我们调用sum函数的参数类型“现场”生成一个适当的函数，然后对其进行调用，这便是类型参数化。

对代码12.2定义的add函数而言，两个参数的类型必须一致，否则，编译器会报错，如下所示。

float a；

double b；

add（a,b）；//错误，参数类型不一致当然，这并不是说使用模板只能用一种参数类型，关键在于函数是如何定义的，将add函数修改为如下所示。

template＜class T1＞

T1 add（const T1＆t1，const T2＆t2）

{

return t1+t2；

}

上述函数定义允许两个参数的类型不同，而且，返回类型与第一个参数一致。

下面学习模板的定义和使用。

12.1.2　模板的定义

模板的引入使得函数定义摆脱了类型的束缚，代码更为高效灵活。在C++中定义一个模板如下所示。

template＜class T＞或

template＜typename T＞

早期模板定义使用的是class，关键字typename是最近才加入到标准中的，相比class,typename更容易体现“类型”的特点，虽然两个关键字在模板定义时是等价的，但从代码兼容的角度讲，使用class较好一些。

模板有函数模板和类模板之分，本章将分别进行介绍。

12.2　函数模板

代码12.2　中的add函数便是一个函数模板，编译器根据函数模板的定义，检查传入的参数类型，生成相应的函数，并进行调用。函数模板的定义形式如下所示。

template＜模板参数表＞

返回类型函数名（参数列表）

{

//函数体

}

关键字template放在模板的定义与声明的最前面，其后是用逗号分隔的模板参数表，用尖括号＜＞括起来。模板参数表不能为空，模板参数有以下两种类型。

❑class或typename修饰的类型参数，代表一种类型。

❑非类型参数，由已知类型符，代表一个常量表达式。

返回类型和函数的参数列表中可以包含类型参数，在函数中可以使用模板参数表中的常量表达式，如下所示。

template＜class Any,class Another,int number＞

double fun（Any a,int b,Another c）

{

//函数体，其中number可以作为一个int型常量来使用

}

12.2.1　函数模板的使用

函数模板的使用规则和普通函数是相同的，在使用函数模板之前，必须对函数模板进行声明，此说明必须在外部进行，也就是说不能在任何一个函数（包括main函数）中声明，声明的格式如下所示。

template＜class T1[，class T2，……]＞//或template＜class T1[，class T2，……]＞

函数原型；

注意

同普通函数一样，如果在使用函数模板前对函数模板进行了定义，函数模板的声明可以省略，如代码12.2所示。

12.2.2　隐式实例化

函数模板实际上不是个完整的函数定义，因为其中的类型参数还不确定，只是定义了某些类型的角色（或变量）在函数中的操作形式，因此，必须将模板参数实例化后才能使用函数，模板参数实例化后的函数也称为模板函数，最简单的实例化称为“隐式实例化”（Implicit Instantiation）。下面通过示例代码12.3进行说明。

代码12.3　隐式实例化ImplicitInstantiation

＜----------------------文件名：example1203.cpp-------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　template＜class Ex＞

04　Ex Greater（Ex x,Ex y）；//函数模板的声明

05　int main（）

06　{

07　int intX=1，intY=2；

08　double dblX=3. 0，dblY=2.9；

09　cout＜＜Greater（intX,intY）＜＜endl；//实参为int型，生成int型模板函数，并对第2个

10　//参数进行检查

11　cout＜＜Greater（dblX,dblY）＜＜endl；//实参为double型，生成double型模板函数，并

12　//对第2个参数进行检查

13　return 0；

14　}

15　template＜class Ex＞//函数模板的定义

16　Ex Greater（Ex x,Ex y）

17　{

18　return x＞y?x：y；

19　}

输出结果如下所示。

2

3

【代码解析】之所以叫“隐式实例化”，这是因为函数模板中的类型参数直到该函数真正地被调用时才能决定，并且由编译器根据传递给函数的参数类型自动完成。编译器根据最先遇到的（从左到右）实参隐式生成一个模板函数。如代码第16行定义的一个函数模板Greater用以返回两个同类型值中较大的一个，如下代码所示。

Greater（intX,intY）

编译器根据上述语句第一个参数intX的类型int隐式生成一个模板函数，其原型如下所示。int Greater（int,int）：

也就是将Ex替换成int，此时，对函数参数列表中其余的Ex型参数进行类型检查，如果后续的Ex型实参可直接解释为int型，则编译通过，否则，编译器报错。注意，此处的直接解释不包括类型转换的情况，这意味着下述调用的代码是错误的。

Greater（2.5，3）；原因在于编译器首先根据2.5将Ex解释为double型参数，其次用double型参数去检查剩余的Ex型参数，而3是int型数据，因此编译器报错。通过显式转换可解决这个问题，如下所示。

Greater（2.5，double（3））；

语句“Greater（dblX,dblY）”的执行过程与上述相似。

注意

当存在多个类型参数时，如Ex1，Ex2等，编译器根据这些类型参数对应的第一个实参为其解释类型，并用此类型检查后续参数。

12.2.3　显式实例化

早期的编译器只支持隐式实例化，新的C++标准允许显式实例化（Explicit Instantiation），由程序员直接命令编译器创建满足条件的模板函数，用以解决因重载等引入的二义性问题。

显式实例化的标准格式如下所示。

template返回类型函数名＜类型实参表＞（函数参数表）；

最前面的关键字template说明这是对函数模板的实例化，类型实参被显式指定在逗号分隔的列表中，用尖括号＜＞括起来，紧跟在函数模板实例的名字后面。如示例代码12.4所示。

代码12.4　显式实例化ExplicitInstantiation

＜----------------------文件名：example1204.cpp-------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　template＜class Ex＞

04　Ex Greater（Ex x,Ex y）；

05　template int Greater＜int＞（int,int）；//显式实例化Greater函数模板

06　int main（）

07　{

08　int intX=1，intY=2；

09　double dblX=3. 0，dblY=2.9；

10

11　cout＜＜Greater（intX,intY）＜＜endl；//调用实例化的Greater（int,int）

12　cout＜＜Greater（dblX,dblY）＜＜endl；//实参为double型，生成double型模板函数

13　//并对第2个参数进行检查

14　return 0；

15　}

16　template＜class Ex＞

17　Ex Greater（Ex x,Ex y）

18　{

19　return x＞y?x：y；

20　}

输出结果如下所示。

2

3

【代码解析】代码第5行使用了显式实例化语句“template int Greater＜int＞（int,int）；”，要求用模板实参int实例化模板Greater。这样，编译器看到调用“Greater（intX,intY）”时，将使用显式实例化时生成的模板函数，但在调用“Greater（dblX,dblY）”时，编译器会生成Greater的double版本，因为两个参数的类型都是double。

在程序中需要注意对于给定的函数模板实例，显式实例化声明在一个程序中只能出现一次。也就是说，可以使用实例化语句“template int Greater＜int＞（int,int）；”显式生成Greater函数模板的int版本，也可以使用实例化语句“template double Greater＜double＞（double,double）；”来显式生成Greater函数模板的double版本，但在程序中，每个版本只能显式实例化一次，否则编译器会指明重复定义的错误。

12.2.4　特化

C++引入了特化（Explicit Specialization）来解决某些类型在函数中的特殊操作，当编译器寻找到函数调用的特化后，使用特化的定义，不再使用模板函数。

特化的基本格式如下所示。

template＜＞返回类型函数名[＜类型实参表＞]（函数参数表）

{

//函数体定义

}

类型实参表可以省略，由后续的函数参数表来指定。

显式实例化是指使用模板生成某些类型参数的模板函数，而特化是指不使用模板生成函数定义，而是单独为某些类型参数生成函数定义，这也是为什么特化有函数体的原因。

在使用特化函数时，须完成其定义在前面或者在后面定义均可，但在使用前要对其进行声明，先来看一个简单的例子，如代码12.5所示。

代码12.5　特化ExplicitSpecialization

＜-----------------------文件名：example1205.cpp------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　template＜class Ex＞

04　Ex Greater（Ex x,Ex y）；

05　template＜＞double Greater＜double＞（double,double）；//特化声明

06　int main（）

07　{

08　int intX=1，intY=2；

09　double dblX=3. 0，dblY=2.9；

10

11　cout＜＜Greater（intX,intY）＜＜endl；//隐式实例化

12　cout＜＜Greater（dblX,dblY）＜＜endl；//优先调用特化函数

13　return 0；

14　}

15　template＜class Ex＞

16　Ex Greater（Ex x,Ex y）

17　{

18　return x＞y?x：y；

19　}

20　template＜＞double Greater（double x,double y）//特化定义

21　{

22　return x+y；

23　}

输出结果如下所示。

2

5.9

【代码解析】代码第20行定义了一个特化函数，将比大小的操作改成了加和操作，在调用“Greater（dblX,dblY）”时，特化函数优先于模板函数。

12.2.5　重载

函数模板支持重载，既可以在模板之间重载（同名模板），又可以实现模板和普通函数间的重载，但模板的重载相比普通函数的重载要复杂一些，如下所示。

template＜class T1，class T2＞

T1 Greater（T1 a,T2，b）{……}与

template＜class T3，class T4＞

T3 Greater（T3 c,T4，d）{……}

看似不同的两个模板，仔细分析后发现其本质是一样的，如果调用“Greater（2，3.5）；”，都实例化为“Greater（int,double）；”，会出现重复定义的错误。

仅仅依靠返回值不同的模板重载也是不合法的，如下所示。

template＜class T1，class T2＞

T1 Greater（T1 a,T2，b）{……}与

template＜class T3，class T4＞

T3*Greater（T3 c,T4，d）{……}

总体来说，模板的重载应做到不引起“二义性”，是否会出现二义性不仅仅取决于定义的形式和传递的参数类型，还取决于编译器对函数执行顺序的选择，这在稍后会进行介绍。

12.2.6　优先级与执行顺序

总体来说，一般函数的执行优先与模板的特化函数相比，模板的特化函数优先于实例化函数，如示例代码12.6所示。

代码12.6　重载与优先级OverloadAndPRI

＜------------------------文件名：example1206.cpp-----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　template＜class Ex＞

04　Ex Greater（Ex x,Ex y）；

05　template＜＞double Greater（double,double）；//特化声明

06　int main（）

07　{

08　int intX=1，intY=2；

09　double dblX=3. 0，dblY=2.9；

10

11　cout＜＜Greater（intX,intY）＜＜endl；//实例化

12　cout＜＜Greater（dblX,dblY）＜＜endl；//优先调用特化函数

13　cout＜＜Greater（"world"，"hello"）＜＜endl；

14　return 0；

15　}

16　template＜class Ex＞

17　Ex Greater（Ex x,Ex y）

18　{

19　return x＞y?x：y；

20　}

21　template＜＞double Greater（double x,double y）//特化定义

22　{

23　return x+y；

24　}

输出结果如下所示。

2

5.9

Hello

【代码解析】前两个结果都是正确的，但字符串的比较出了问题，这是为什么呢？原来在代码第13行调用语句“Greater（"world"，"hello"）”时，编译器将类型参数Ex实例化为char*，由此根据模板Greater生成的模板函数如下所示。

char*Greater（char*x,char*y）

{

return x＞y?x：y；

}

比较的结果实际上是x和y两个字符串的地址大小，而不是字符串内容的比较，为char*构造特化函数是一个解决思路，此外还可以编写一个一般函数，同样会具有比实例化函数更高的优先级。

重载的普通函数如下所示。

char*Greater（char*x,char*y）//一般函数

{

cout＜＜"一般函数被执行"＜＜endl；

return（strcmp（x,y）＞0）?x：y；//比较两个字符串的大小

}输出结果变为如下所示。

2

5.9

一般函数被执行

world

输出结果正确，普通函数优先被执行。

在某些情况下，对于一个函数调用，即使两个不同的函数模板都可以实例化，但是该函数调用仍然可能是二义的，这涉及编译器选择的问题。一般来说，更特化的模板函数优先，这是为什么显式实例化优先于隐式实例化的原因。

此外，更特化的含义体现在“编译器做决定时执行的转换最少”，C++遵循部分排序规则（Partial Ordering）来选择最优的模板函数。如果模板X匹配的参数集同样匹配于模板Y，但反之不成立，就说明模板X比模板Y更特化，如下例所示。

template＜class T＞void f（T）{}#1

template＜class T＞void f（T*）{}#2

template＜class T＞void f（const T*）{}#3

template＜class T＞void g（T）{}#4

template＜class T＞void g（T＆）{}#5

int main（）{

const int*p；

f（p）；

int q；

//g（q）；

}

模板#2比模板#1更特化，模板#3比模板#2更特化，因此，f（p）调用的是“template＜class T＞void f（const T*）{}”，对模板#4和模板#5来说，既不能说模板#4比模板#5更特化，也不能说模板#5比模板#4更特化，因此，g（q）的调用会出现二义性错误。

结合第9章中对重载函数选择规则的内容，可以看出重载解析用于寻找最匹配的函数，如果只有一个可选函数，则毫无疑义地调用它；如果有多个可选函数，首先选择普通函数，其次是模板函数；如果都是模板函数，则选择最特化的那个；如果有多个函数，并且编译器不能决定究竟要调用哪一个，编译器将会指出二义性错误，当然如果没有可选函数，也是错误。

12.3　类模板

模板同样可以用在类场合中，提供通用型的类定义。C++标准库中提供了很多模板类，下一章中要讨论的C++标准模板库STL，其基础便是模板，本节先介绍类模板的基础知识。

12.3.1　定义类模板

理解了函数模板的应用，类模板的提出便是水到渠成的事，如示例代码12.7所示。

代码12.7　类模板的定义ClassTemplateDefinition

＜-----------------------------文件名：Stack.h--------------------------------＞

01　template＜class T,int num＞//类型参数表

02　class Stack//Stack类定义

03　{

04　private：

05　T sz[num]；//存储空间，用数组表示

06　int point；//指针，表示存储位置（即元素个数）

07　public：

08　Stack（）；//构造函数

09　bool isEmpty（）；//判断栈是否为空

10　bool isFull（）；//判断栈是否已满

11　bool push（const T＆）；//将一个元素压入栈

12　bool pop（T＆）；//从栈中弹出一个元素

13　}；

14　template＜class T,int num＞

15　Stack＜T,num＞：Stack（）

16　{

17　point=0；//初始位置为0，栈底

18　}

19　template＜class T1，int num1＞//参数列表不要求每个字都相同，但形式要相同

20　bool Stack＜T1，num1＞：isEmpty（）

21　{

22　return point==0；//point为0，说明当前无元素

23　}

24　template＜class T,int num＞

25　bool Stack＜T,num＞：isFull（）

26　{

27　return point==num；//point为num，说明数组已满

28　}

29　template＜class T,int num＞

30　bool Stack＜T,num＞：push（const T＆obt）

31　{

32　if（isFull（））

33　return false；//如果栈已满，压入不成功，返回false

34　else

35　{

36　sz[point]=obt；//将传入的元素存储在point指向的当前位置

37　point++；//point加1，向栈顶移动

38　return true；//压入成功，返回true

39　}

40　}

41　template＜class T,int num＞

42　bool Stack＜T,num＞：pop（T＆obt）

43　{

44　if（isEmpty（））

45　return false；//如果栈已空，无法弹出，返回false

46　else

47　{

48　point--；//point减1，向栈底移动，指向存储的最上面一个元素

49　obt=sz[point]；//将point指向的当前位置元素复制给传入参数

50　return true；//弹出成功，返回true

51　}

52　}

＜-----------------------文件名：example1207.cpp------------------------------＞

53　#include＜iostream＞

54　#include"Stack.h"

55　using namespace std；

56　int main（）

57　{

58　Stack＜int，10＞st；

59　cout＜＜"开始时st是否为空？"＜＜st.isEmpty（）＜＜endl；

60　st. push（5）；//压入元素5

61　cout＜＜"此时st是否为空？"＜＜st.isEmpty（）＜＜endl；

62　for（int i=1；i＜10；i++）

63　{

64　st. push（i）；//压入9个元素

65　}

66　cout＜＜"此时st是否已满？"＜＜st.isFull（）＜＜endl；

67　int rec=0；

68　while（st. pop（rec））

69　cout＜＜rec＜＜""；

70　cout＜＜endl；

71　return 0；

72　}

输出结果如下所示。

开始时st是否为空？1

此时st是否为空？0

此时st是否已满？1

9 8 7 6 5 4 3 2 1 5

【代码解析】代码第2～52行，相对完整地实现了一个栈类Stack，列出了类模板和成员函数模板。模板的引入使得类的定义更为灵活，可以在类创建时指明其中的元素类型T，以及非类型常量num的大小，需要注意的是不管是类定义还是成员函数定义，都要遵守模板的定义形式。

注意

类模板和成员函数模板不是传统意义上的类定义和成员函数定义，它们是C++编译指令，用以向编译器说明如何生成类定义和成员函数定义。example1207中调用“Stack＜int，10＞st；”生成对象的操作称为实例化（Instantiation）或具体化（Specialization）。

原则上，类模板和成员函数模板要放在同一个文件中，不能像普通的类一样将类定义放在一个文件中，成员函数的实现可以放在另一个文件中，这是因为模板不是函数，不能被单独编译。最简单的方法是像代码12.7中那样将所有的模板信息放在头文件中，只要在使用这些模板的文件中包含该头文件即可。

当然，如果在类模板和函数模板定义时使用export关键字进行修饰，类模板和成员函数模板可以分开来，这种功能在当前的VC编译器（包括VC 2005）中尚不支持。

12.3.2　隐式实例化

代码12.7　中使用下述语句创建了Stack类模板的对象。

Stack＜int，10＞st；这是隐式实例化（Implicit Instantiation），编译器可以根据要求，隐式地生成类定义，但应当注意，下述语句不需要创建对象，编译器不会隐式生成类定义。

Stack＜int，10＞*pS；

12.3.3　显式实例化

同根据函数模板生成模板函数的过程类似，显式实例化（Explicit Instantiation）的基本格式如下所示。

template class类名＜类型参数表＞；

上述代码将“类名＜类型参数表＞”声明为一个类，此时，虽未创建类对象，但编译器已经显式生成了类定义。

12.3.4　显式特化

不论是隐式实例化还是显式实例化，都是使用模板来生成类定义，而特化是特定类型（用于替换模板中的通用类型）的定义，抛开了模板而使用独立的专门的类定义。

显式特化（Explicit Specialization）也称完全特化，其基本格式如下所示。

template＜＞class类名＜特殊类型＞

{

//类定义

}

如果对某个特殊类型进行了显式特化处理，显式特化定义的类优先于通用模板类，类似于覆盖或隐藏的操作。

如下代码12.8中的Stack.h与代码12.7相同，也就是说，两个代码中通用类模板的定义相同。

代码12.8　类模板的显式特化ClassTemplateExplicitSpecialization

＜----------------------文件名：example1208.cpp-------------------------------＞

01　#include＜iostream＞

02　#include"Stack.h"

03　using namespace std；

04　template＜＞class Stack＜double，5＞

05　{

06　private：

07　double sz[5]；//此处的5个元素与显式特化中的5无关

08　int point；//指针，表示存储位置（即元素个数）

09　public：

10　Stack（）；//构造函数

11　bool isNotEmpty（）；//判断栈是否为空

12　}；

13　Stack＜double，5＞：Stack（）

14　{

15　point=0；//初始位置为0，栈底

16　}

17　bool Stack＜double，5＞：isNotEmpty（）

18　{

19　return point！=0；//point为0，说明当前无元素

20　}

21　int main（）

22　{

23　Stack＜double，5＞st；//调用显式具体化生成的类定义

24　cout＜＜st. isNotEmpty（）＜＜endl；

25　return 0；

26　}

输出结果如下所示。

0

【代码解析】代码第4行，显式特化了类Stack＜double，5＞，不再沿用通用模板类的定义，而是单独定义了一个类，上述代码为了简单起见，只定义了一个成员函数isNotEmpty，此时，再用创建的st调用isEmpty函数和isFull函数，编译器会报错，因为这些函数并没有在类Stack＜double，5＞中进行定义。

12.3.5　部分特化

C++引入了部分特化（Partial Specialization）来部分地限制类模板的通用性，例如代码12.8中可使用下述方式对类定义进行特化。

template＜＞class Stack＜double,int num＞只限制了前一个类型参数为double，对第2个int型常量没有限制，这个例子有些特殊，来看一个通用模板类的例子，如下所示。

template＜class T1，class T2＞class Example

{

//类定义

}；部分特化定义如下所示。

template＜class T2＞class Example＜int,T2＞

{

//类定义

}

如果所有类型都已指定，那么template后面的＜＞内为空，这就是显式特化，从这个角度看，部分特化和显式特化是相通的。

12.3.6　重载和优先级

如果有多个模板可供选择，编译器将选择特化程度最高的那个，如下所示。

template＜class T1，class T2＞class Example//通用类模板

{

//类定义

}；

template＜class T2＞class Example＜int,T2＞//部分特化

{

//类定义

}

template＜＞class Example＜int,int＞//显式特化

{

//类定义

}在创建不同类的对象时，会有不同的模板被调用，如下所示。

Example＜double,double＞E1；//调用通用类模板

Example＜int,float＞E2；//调用部分特化

Example＜int,int＞E3；//调用显式特化

部分特化有两个重要的应用，如下所述。

（1）为指针提供特殊版本的模板。

template＜class TYPE＞//模板#1

class Example{……}

template＜class TYPE*＞//模板#2

class Example{……}

如果提供的类型参数是指针，模板#2会被调用，否则，模板#1会被调用。

（2）为类模板的调用设置各种限制，如下所示。

template＜class T1，class T2＞//模板#1

class Example{……}

template＜class T1＞//模板#2

class Example＜T1，T1＞{……}

当提供的两个类型参数相同时，模板#2会被调用，否则，模板#1会被调用。

12.4　模板的嵌套

模板的套嵌可以理解为在另外一个模板里面定义一个模板。以模板（类或者函数）作为另一个模板（类或者函数）的成员，也称成员模板。

提示　成员模板是不能声明为virtual的。

12.4.1　函数成员模板

可以将函数模板作为另一个类（必须是模板类）的成员，称为函数成员模板，其用法和普通成员函数类似，如示例代码12.9所示。

代码12.9　成员模板示例MemberTemplate

＜----------------------文件名：example1209.cpp-------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　template＜class A＞

04　class Test//Test类定义

05　{

06　public：

07　template＜class B＞//成员模板

08　A f（B）；

09　}；

10　template＜class A＞

11　template＜class B＞//成员模板的定义

12　A Test＜A＞：f（B）

13　{

14　return A（B）；

15　}

16　int main（）

17　{

18　Test＜int＞t；

19　cout＜＜t. f（3.14）＜＜endl；

20　return 0；

21　}在VC 2005下编译，

输出结果如下所示。

3

注意

在VC 6下，上述代码无法编译通过，主要因为VC 6的编译器对模板的支持相对较差，所以推荐采用较新的编译器，如VC 2005。

【代码解析】代码第8行定义了函数成员模板“A f（B）；”，程序很简单，关键是掌握函数成员模板的定义方法。

12.4.2　对象成员模板

类模板的定义可以放在另一个类中，实例化后的模板类对象可以作为另一个类的成员，如示例代码12.10所示。

代码12.10　对象成员模板ObjectMemberTemplate

＜-----------------------文件名：example1210.cpp------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　template＜class T＞

04　class Outside//外部Outside类定义

05　{

06

07　public：

08　template＜class R＞

09　class Inside//嵌套类模板定义

10　{

11　private：

12　R r；

13　public：

14　Inside（R x）//模板类的成员函数可以在定义时实现

15　{

16　r=x；

17　}

18　void disp（）；

19　}；

20　Outside（T x）：t（x）

21　{

22　}

23　void disp（）；

24　private：

25　Inside＜T＞t；

26　}；

27

28　template＜class T＞

29　template＜class R＞

30　void Outside＜T＞：Inside＜R＞：disp（）//模板类的成员函数也可以在定义外实现

31　{

32　cout＜＜"Inside："＜＜Outside＜T＞：Inside＜R＞：r＜＜endl；

33　}

34　template＜class T＞

35　void Outside＜T＞：disp（）

36　{

37　cout＜＜"Outside："；

38　t. disp（）；

39　}

40　int main（）

41　{

42　Outside＜int＞：Inside＜double＞obin（3. 5）；//声明Inside类对象obin

43　obin. disp（）；

44　Outside＜int＞obout（2）；//创建Outside类对象obout

45　obout. disp（）；

46　return 0；

47　}

输出结果如下所示。

Inside：3.5

Outside：Inside：2

【代码解析】理解的难点在于“类模板不等于类定义，需要实例化或特化来生成类实例”。代码第9行的Inside类模板的访问权限为public，因此，可以调用下述语句。

“Outside＜int＞：Inside＜double＞obin（3.5）；”

在Outside类内使用“Inside＜T＞t；”语句声明了Inside＜T＞类的对象，在Outside模板类对象创建时，首先采用隐式实例化生成Inside＜T＞类的定义，其次再根据此定义创建对象成员t。

12.5　模板参数

模板包含类型参数（如class Type）和非类型参数（如int NUM,NUM是常量），实际上，模板的参数可以是另一个模板，也就是说，下述形式是合法的。

template＜template＜class T1＞class T2，class T3，int Num＞；

上述简单示例将原来简单的“class T2”或“Typename T2”扩充为“template＜class T1＞class T2”，来看以下示例代码12.11。

代码12.11　模板参数ParameterTemplate

＜-----------------------------文件名：Stack.h--------------------------------＞

01　template＜class T,int num＞//类型参数表

02　class Stack//Stack类定义

03　{

04　private：

05　T sz[num]；//存储空间，用数组表示

06　public：

07　int ReturnNum（）；//判断栈是否为空

08　}；

09　template＜class T1，int num1＞//参数列表不要求每个字都相同，但形式要相同

10　int Stack＜T1，num1＞：ReturnNum（）

11　{

12　return num1；//返回数组大小

13　}

＜--------------------文件名：example1211.cpp---------------------------------＞

14　#include＜iostream＞

15　#include"Stack.h"

16　using namespace std；

17

18　template＜template＜class Type,int NUM＞class TypeClass,class T1，int N＞

19　void disp（）//函数模板，其类型参数表中包含一个类模板

20　{

21　TypeClass＜T1，N＞ob；//类模板的隐式实例化，创建对象ob

22　cout＜＜ob. ReturnNum（）＜＜endl；//调用ob的public成员函数

23　}

24

25　int main（）

26　{

27　disp＜Stack,int，8＞（）；//函数模板的隐式实例化，并调用

28　return 0；

29　}

输出结果如下所示。

8

【代码解析】代码第18～19行定义了函数模板disp，该模板的类型参数表中又包含了一个类模板TypeClass，在函数模板disp内可以对类TypeClass进行实例化处理。

12.6　小结

模板是C++引入的新特性，也是第13章要介绍的标准模板库STL的基础，模板有函数模板和类模板之分，两种应用有很多相似之处。学习模板，最重要的是理解模板定义（函数模板定义和类模板定义）与具体定义（函数定义和类定义）的不同，模板不是定义，要通过实例化（通过模板）或特化（避开模板）来生成具体的函数或类定义，再调用函数或创建类的对象。

模板支持嵌套，也就是说可以在一个模板里面定义另一个模板。以模板（类或者函数）作为另一个模板（类或者函数）的成员，也称成员模板。同时，模板也可以作为另一个模板的参数，出现在类型参数表中。

12.7　上机实践习题

1.定义两个函数模板，能够进行各种类型数据的加法算法及除字符串数据类型外的减法算法。

【提示】上述题目主要是要求读者熟悉函数模板的相关知识，重点是掌握模板和函数模板的概念及作用。

【关键代码】

01　template＜class Ex＞

02　Ex Add（Ex x,Ex y）//函数模板的声明

03　{

04　return x+y；

05　}

06　template＜class Ex＞

07　Ex Sub（Ex x,Ex y）

08　{

09　return x-y；

10　}

11

12　template＜＞char*Add＜char*＞（char*px,char*py）//特化定义

13　{

14　strcat（px,py）；

15　return px；

16　}

2.定义一个队列类模板，其中包括置空队、判队空、入队、出队及判断队满的成员函数。

【提示】上述题目主要是要求读者熟悉类模板的相关知识，重点是掌握类模板的概念、定义和使用。

队列的规则如下所述。

❑nHead直接指向了队头的数据位置。

❑nTail直接指向了队尾要插入的位置。

❑如果nTail=nHead，队列为空。

❑nTail比nHead位置小1（就是在它前面一个），代表队满。

❑所以数组中能存放的数据要比数组的空间小1，是为了判断队列满的情况。

【关键代码】

01　template＜class T,int num＞//类型参数表

02　class MyQueue//Queue类定义

03　{

04　private：

05　T sz[num]；//存储空间，用数组表示

06　int nTail；//队列尾

07　int nHead；//队列头

08　public：

09　MyQueue（）；//构造函数

10　bool QueueEmpty（）；//判断队列是否为空

11　bool QueueFull（）；//判断队列是否已满

12　bool InQueue（const T＆）；//将1个元素入队

13　bool OutQueue（T＆）；//从队列中弹出1个元素

14　}；

15　template＜class T,int num＞

16　MyQueue＜T,num＞：MyQueue（）

17　{

18　nTail=0；//初始位置为0

19　nHead=0；

20　}

21　template＜class T1，int num1＞//参数列表不要求每个字都相同，但形式要相同

22　bool MyQueue＜T1，num1＞：QueueEmpty（）

23　{

24　return（nTail==nHead）；//队头等于队尾说明当前无元素

25　}

26　template＜class T,int num＞

27　bool MyQueue＜T,num＞：QueueFull（）

28　{

29　return（nTail+1）%num==nHead；//如果nTail比nHead小1，则队列满

30　}

31　template＜class T,int num＞

32　bool MyQueue＜T,num＞：InQueue（const T＆obt）

33　{

34　if（QueueFull（））

35　return false；//如果队列已满，入队不成功，返回false

36　else

37　{

38　sz[nTail]=obt；//将传入的元素存储在nTail指向的当前位置

39　nTail=（nTail+1）%num；//加1，向队列后面移动

40　return true；//入队成功，返回true

41　}

42　}

43　template＜class T,int num＞

44　bool MyQueue＜T,num＞：OutQueue（T＆obt）

45　{

46　if（QueueEmpty（））

47　return false；//如果队列已空，无法弹出，返回false

48　else

49　{

50　obt=sz[nHead]；

51　nHead=（nHead+1）%num；

52　return true；//出队成功，返回true

53　}

54　}

第13章　标准模板库

标准模板库（Standard Template Library,STL）不是面向对象的编程，而是一种新的编程模式，即泛型编程（Generic Programming）。STL是C++标准库的组成部分，它是个庞大且复杂的系统，仅STL就可以写出厚达千页的技术书籍，所以本章不可能面面俱到地进行介绍，重点在于介绍泛型编程的思想和本质，以及一些常用的方法，为初学者学习STL提供一些感性认识，起到抛砖引玉的作用。

注意

由于STL是一项比较新的技术，而VC6又是微软公司开发得比较早的一款编译器，其对STL的支持并不是太好，因此在学习本章时，推荐使用较新的VC 2005或VC 2008编译器。

本章主要涉及以下知识点。

❑STL：介绍STL的概念、容器、适配器、迭代器和算法。

❑序列式容器及其操作：介绍几种常用的序列式容器的使用。

❑关联式容器及其操作：介绍几种常用的关联式容器的使用。

❑迭代器：介绍迭代器的概念、类型及其使用。

❑泛型算法：介绍函数对象及常用算法分类。

❑适配器：介绍容器、迭代器和函数适配器的使用。

❑基类与派生类对象之间的转换：介绍基类与派生类对象之间如何进行转换。

13.1　理解STL

STL库是用模板（Template）写出来的，在第12章中已经介绍过模板是STL库的基础所在。大致来说，STL是由以下4部分组成的。

❑容器（Container）

❑迭代器（Iterator）

❑容器适配器（Adapter）

❑算法（Algorithm）

容器、容器适配器和迭代器都是用类模板实现的，迭代器用于遍历容器中的每一个元素，算法用于操作数据。

13.1.1　容器

如果没有STL的支持，在处理一些复杂问题时，要自行设计存储模式，如数组管理和插入删除操作等，不但很烦琐，而且错误频出，是程序出现问题最多的地方。STL运用模板类库机制，为数据存储、查找和其他操作提供了一整套方案，大大提高了程序的正确性，不仅如此，类库还对常用的许多操作进行了优化处理，大大提高了程序的运行效率。

容器即是可容纳一些数据的模板类，STL中包含vector、list、deque、set、map、multimap和multiset等容器。

13.1.2　适配器

适配器就是接口（Interface），对容器、迭代器和算法进行包装，但其实质还是容器、迭代器和算法，只是不依赖于具体的标准容器、迭代器和算法类型。容器适配器可以理解为容器的模板，迭代器和适配器可理解为迭代器的模板，算法适配器可理解为算法的模板。

常见的容器适配器有stack、queue和priority_queue。

13.1.3　迭代器

在某些专业书籍中，迭代器也称游标，可以将迭代器初步理解为广义指针，迭代器和指针功能很像，它是通过重载一元的"*"和"-＞"来从容器中间接地返回一个值。

迭代器包括随机访问迭代器（Random Access Iterator）、双向迭代器（Bidirectional Iterator）、前向迭代器（Forward Iterator）、输入迭代器（Input Iterator）和输出迭代器（Output Iterator）5种，稍后会详细介绍。

13.1.4　算法

STL包含了很多对容器进行处理的函数，其处理思路大体相同，即使用迭代器来标识要处理的数据或数据段以及结果的存放位置，有的函数还作为对象参数传递给另一个函数，实现数据的处理。

13.2　使用序列式容器

容器是STL的基础，容器有序列式容器（Sequential Container）和关联式容器（Associative Container）之分。总体来说，序列式容器会强调元素的次序，依次维护第一个元素到最后一个元素，面向序列式容器的操作主要是迭代操作，本节讨论序列式容器vector、list和deque的用法，以及序列式容器的共同操作。

13.2.1　序列式容器的创建和元素的访问

要使用序列式容器，必须包含相关的头文件，vector、list以及deque分别对应于如下头文件。

#include＜vector＞

#include＜list＞

#include＜deque＞

创建序列式容器的对象，大体有以下几种方式。

（1）创建空的容器，此时容器中的元素个数为0。

vector＜int＞obV；

list＜float＞obL；

deque＜double＞obD；（2）产生特定大小的容器，此时容器中的元素被创建，但未被显式初始化，编译器使用默认值为元素隐式初始化，像int、float和double等内置的数据类型会被初始化为0。对于类对象元素，将调用其无参构造函数（用户定义的或编译器默认提供的）或每个参数都有默认值的构造函数。如下所示。

vector＜double＞obV（50）；//vector型对象obV中含50个double型的元素，初始化为0

list＜int＞obL（1000）；//list型对象obL中含1000个int型元素，初始化为0

deque＜float＞obD（2）；//deque型对象obD中含2个float型元素，初始化为0

（3）在（2）的基础上，创建特定大小的容器，并且为其中的每个元素指定初始值，此时在元素的参数后增加一个参数，如下所示。

vector＜int＞obV（10，8）；//10个int型元素，每个都初始化为8

list＜double＞obL（5，3.2）；//5个double型元素，每个都初始化为3.2

deque＜string＞obD（100，"Hello"）；//100个string型元素，每个都初始化为“Hello”

（4）根据已有同类型的容器创建新容器，并将其中的元素完全复制过来，设obV1、obL1和obD1都是现成的容器，里面存储的数据均为int型，则可用下述命令创建新容器。

vector＜int＞obV2（obV1）；//或vector＜int＞obV2=obV1；

list＜int＞obL2（obL1）；//或list＜int＞obL2=obL1；

deque＜int＞obD2（obD1）；//或deque＜int＞obD2=obD1；

（5）通过一对迭代器（可暂时理解为指针），使编译器决定元素的个数和初始值，这对迭代器用以标识一组元素区间。

int sz[5]={1，2，3，4，5}；

vector＜int＞obV（sz,sz+5）；

list＜int＞obL（sz,sz+5）；

deque＜int＞obD（sz,sz+5）；

容器创建完毕后，便可以通过“容器名[下标]”或“容器名.at（序号）”的形式对vector和deque中的元素进行随机访问，这是因为在vector和deque类模板中已经对下标运算符进行了重载，返回类型便是其中存储的数据类型。同数组一样，元素的初始下标为0，而且编程时同样要注意下标越界问题。此外，还可以通过迭代器对vector和deque中的元素进行间接访问，在迭代器一节中会进行具体的介绍，但对list来说，其不支持下标运算符访问方式（数组表示法），也就是说无法对list中的元素进行随机访问，必须通过迭代器来进行。如示例代码13.1所示。

代码13.1　容器的创建与其中元素的访问CreateAndContainer

＜------------------------文件名：example1301.cpp-----------------------------＞

01　#include＜iostream＞

02　#include＜vector＞

03　#include＜list＞

04　#include＜deque＞

05　using namespace std；

06　int main（）

07　{

08　vector＜int＞obv；//创建一个空的vector

09　cout＜＜"obv的元素个数为："＜＜obv.size（）＜＜endl；//size（）用以返回元素的个数

10　double sz[5]={1，2，3，4，5}；//创建double型数组sz

11　deque＜double＞obD（sz,sz+5）；//创建deque型容器obD，用sz的

12　//首地址和（末地址+1）初始化，方式5

13　for（unsigned int i=0；i＜obD. size（）；i++）//对obD中的元素进行随机访问

14　{

15　cout＜＜obD[i]＜＜""；//下标表示法或obD.at（i）

16　}

17　cout＜＜endl；//换行

18　list＜float＞obL（3，5）；//创建一个大小为3的list型容器obL，

19　//其中每个元素都初始化为5

20　list＜float＞：iterator iter=obL. begin（）；//创建list＜float＞型迭代器，类似指针的

21　//概念，并使其指向obL的第1个元素

22　while（iter！=obL. end（））//while结构，直到iter指向obL的尾部

23　{

24　cout＜＜（*iter）＜＜""；//通过迭代器间接访问容器中的元素

25　iter++；//指向下一个元素

26　}

27　cout＜＜endl；//换行

28　return 0；

29　}

输出结果如下所示。

obv的元素个数为：0

1 2 3 4 5

5 5 5

【代码解析】代码中演示了3种容器的创建及其元素的访问过程，值得注意的是对代码第18行的list型容器对象obL，不能使用诸如“obL[1]”之类的下标运算符和诸如“obL.at（1）”的形式访问其中的元素，只能借助于迭代器的间接访问。

代码13.1　初步演示了迭代器的用法，从表面上看迭代器类似于指针，迭代器的声明格式如下所示。

容器类型＜class type＞：iterator迭代器对象名；

这只是迭代器的一种形式，稍后会有更详细的介绍。

13.2.2　所有容器都支持的特征

代码13.1　中，“obL.begin（）”返回的是指向容器第1个元素的迭代器，这是所有容器（容器和容器适配器）都支持的基本特征，此外，还有如表13.1所示的所有容器都支持的基本特征，其中ob、ob1和ob2是容器对象名。

需要特别说明的是ob.end（），其返回的是末尾元素的下一个迭代器，在有的教材中称做超尾值迭代器，初学者往往会把ob.end（）理解为指向容器中末尾元素的迭代器，这也是为什么在代码13.1中对obL进行遍历输出时使用“while（iter！=obL.end（））”的原因。

注意

size函数返回的类型是size_type，这个类型的定义出发点和size_t类似，是为了提高可移植性，关于size_type的介绍请参考13.4.2节的介绍。

复杂度是操作和算法性能的衡量手段，固定复杂度代表操作所用的时间与对象中的元素数目无关，而线性复杂度代表操作所用时间正比于对象中的元素数目。

13.2.3　序列式容器中元素的插入和删除

在普通数组中，元素的插入和删除是件很烦琐的事情，但在序列式容器中，只要调用操作函数，所有的事情都由STL类库自动完成，而且容器对象都能随着元素的插入和删除自动增大或缩小。

注意

在创建数组时，需要指定元素的个数以帮助编译器开辟所需内存区域，而在创建容器对象时不必指明容器对象的最大容量，因为由STL类库管理的容器对象内存是动态的。

1.push_back（t）和pop_back（void）

push_back（t）和pop_back（void）允许在容器对象的末尾进行安插和删除操作，从字面上不难看出push_back（t）是在最末端安插一个元素t，容器对象的容量自动增大，pop_back（void）是删除最末端的元素，容器对象的容量自动减小。以vector为例如以下示例代码13.2所示。

代码13.2　push_back（t）和pop_back（void）函数InsertAndDelete1

＜------------------------文件名：example1302.cpp-----------------------------＞

01　#include＜iostream＞

02　#include＜vector＞

03　using namespace std；

04　int main（）

05　{

06　vector＜int＞obV（3，1）；//创建一个vector类对象，3个int型元素，全为1

07　obV. push_back（2）；//将int型数据2安插在容器对象obV末尾

08　for（unsigned int i=0；i＜obV. size（）；i++）

09　{

10　cout＜＜obV[i]＜＜""；//输出处理，看是否安插成功

11　}

12　cout＜＜endl；

13　obV. pop_back（）；//将最后一个元素弹出

14　for（unsigned int i=0；i＜obV. size（）；i++）

15　{

16　cout＜＜obV[i]＜＜""；//输出处理，看是否弹出成功

17　}

18　cout＜＜endl；

19　return 0；

20　}

输出结果如下所示。

1 1 1 2

1 1 1

【代码解析】代码第7行将数据插入到容器最后，代码第13行将最后一个元素弹出。最后的输出结果验证了容器对象大小的动态变化。

2.push_front（t）和pop_front（void）

list和deque还提供了push_front（t）以及pop_front（void），分别用于在容器对象的头部安插或删除一个元素，不过，这两个操作函数对vector并不适用。以list为例，如以下示例代码13.3所示。

代码13.3　push_front（t）和pop_front（void）函数InsertAndDelete2

＜------------------------文件名：example1303.cpp-----------------------------＞

01　#include＜iostream＞

02　#include＜list＞

03　using namespace std；

04　int main（）

05　{

06　int sz[5]={1，2，3，4，5}；

07　list＜int＞obL（sz,sz+5）；//创建一个list对象，包含5个int型元素

08　obL. push_front（0）；//将int型数据0安插在容器对象obL头部

09　list＜int＞：iterator iter=obL. begin（）；

10　while（iter！=obL. end（））

11　{

12　cout＜＜（*iter）＜＜""；//输出处理，看是否安插成功

13　iter++；

14　}

15　cout＜＜endl；

16　obL. pop_front（）；//将第1个元素弹出

17　iter=obL. begin（）；

18　while（iter！=obL. end（））

19　{

20　cout＜＜（*iter）＜＜""；//输出处理，看是否弹出成功

21　iter++；

22　}

23　cout＜＜endl；

24　return 0；

}

输出结果如下所示。

0 1 2 3 4 5

1 2 3 4 5

【代码解析】代码第8行和第16行使用简单的两个函数（push_front和pop_front）完成了从头部安插和删除一个元素的操作。

3.front（void）和back（void）

前面介绍的push_back（t）和push_front（t），以及pop_back（void）和pop_front（void）返回值类型都是void，也就是说，无法从pop_back（void）和pop_front（void）得到被删除的元素值。为此，STL提供了front（void）和back（void）来读取序列式容器对象的最前端元素和最末端元素，这两个函数方法对vector、list和deque都适用。以deque为例，如以下示例代码13.4所示。

代码13.4　front（void）和back（void）函数InsertAndDelete3

＜------------------------文件名：example1304.cpp-----------------------------＞

01　#include＜iostream＞

02　#include＜deque＞//使用deque必须包含的头文件

03　using namespace std；

04　int main（）

05　{

06　double sz[6]={0，1，2，3，4，5}；

07　deque＜double＞obD（sz,sz+6）；

08　cout＜＜obD. front（）＜＜endl；//读取最前端元素的值

09　cout＜＜obD. back（）＜＜endl；//读取最末端元素的值

10　return 0；

11　}

输出结果如下所示。

0

5

【代码解析】需要注意的是代码第8行的front（void）和第9行的back（void）只能用于元素的读取，而不能用于改写元素的值，如“obD.front（）=1；”是非法的。

说明

前面介绍的6个函数操作：push_back（t）、push_front（t）、pop_back（void）、pop_front（void）、front（void）和back（void），其复杂度都是固定的，换言之，操作所用的时间与容器对象元素的个数无关。

4.insert插入操作

不仅可以在最前端和最末端安插元素，序列式容器还可在中间特定位置安插元素，insert成员函数有如下几种重载形式，编译器根据传递的参数决定调用哪个版本。

❑iterator insert（iterator p,elemType t）；

将元素t安插于迭代器p之前，返回的迭代器指向被安插的元素。

❑void insert（iterator p,int num,elemType t）；

在迭代器p之前安插num个元素，这些元素的值都为t，无返回值。

❑void insert（iterator p,iterator first,iterator last）；

在迭代器p之前安插[first,last）之间的所有元素。

从以下示例代码中体会3种insert不同的操作，如代码13.5所示。

代码13.5　序列式容器通用的3种insert操作InsertMethods

＜-----------------------文件名：example1305.cpp------------------------------＞

01　#include＜iostream＞

02　#include＜vector＞

03　using namespace std；

04　void disp（vector＜int＞＆x）//定义disp函数用以输出容器对象所有元素

05　{

06　unsigned int i=0；

07　for（；i＜x. size（）；i++）

08　{

09　cout＜＜x[i]＜＜""；

10　}

11　}

12　int main（）

13　{

14　vector＜int＞obD（5，0）；//创建一个vector＜int＞容器对象

15　vector＜int＞：iterator pD=obD. end（）；//创建迭代器pD

16　pD=obD. insert（pD，1）；//在尾部插入元素1，并使迭代器指向新插入的1

17　disp（obD）；

18　cout＜＜endl；

19　obD. insert（pD，2，3）；//在新插入的元素1之前插入两个元素3

20　disp（obD）；

21　cout＜＜endl；

22　pD=obD. begin（）；//很重要，插入后，原来的迭代器可能失效

23　int sz[3]={7，8，9}；

24　obD. insert（pD,sz,sz+3）；//将两个指针（相当于迭代器）插入到头部

25　disp（obD）；

26　cout＜＜endl；

27　return 0；

28　}

输出结果如下所示。

0 0 0 0 0 1

0 0 0 0 0 3 3 1

7 8 9 0 0 0 0 0 3 3 1

【代码解析】代码第16行、第19行和第24行演示了3种insert操作的用法，初学者容易忽略的是3种操作的返回值，代码第16行的语句“pD=obD.insert（pD，1）；”在迭代器前插入元素1，并用指向新插入的元素1的迭代器为pD赋值，这有效避免发生“迭代器失效”现象。

“迭代器失效”（Iterator Invalid）将在迭代器一节中进行详细介绍，从字面意义上理解这意味着迭代器不再指向原来的位置，这是由于系统为容器的扩充或删除等操作造成的，因此在继续使用迭代器前，必须对迭代器重新定位。所以语句“pD=obD.begin（）；”在此十分重要，将pD指向最前端元素，则语句“obD.insert（pD,sz,sz+3）；”成功地将7、8和9插入vector的头部。

5.erase删除操作

除了使用pop_front（void）和pop_back（void）外，序列式容器可以通过erase成员函数抹除容器中指定位置处的元素，erase函数有如下两种重载形式。

❑iterator erase（iterator p）；

抹除迭代器p指定的元素，返回指向所删除元素的下一个元素的迭代器。

❑iterator erase（iterator first,iterator last）；

抹除[first,last）之间的所有元素，返回指向被删除的一片元素的下一个元素的迭代器。

以list容器为例，体会元素的删除操作，如代码13.6所示。

代码13.6　序列式容器通用的两种erase操作EraseMethods

＜-----------------------文件名：example1306.cpp------------------------------＞

01　#include＜iostream＞

02　#include＜list＞

03　using namespace std；

04　void disp（list＜int＞＆x）//定义disp函数用以输出容器对象所有元素

05　{

06　list＜int＞：iterator it=x. begin（）；

07　for（；it！=x. end（）；it++）//list不支持下标随机访问

08　{

09　cout＜＜（*it）＜＜""；

10　}

11　}

12　int main（）

13　{

14　int sz[9]={1，2，3，4，5，6，7，8，9}；

15　list＜int＞obL（sz,sz+9）；//使用两个迭代器（指针）创建list＜int＞对象

16　disp（obL）；

17　cout＜＜endl；

18　list＜int＞：iterator iter=obL. begin（）；//创建迭代器iter，指向最前端元素

19　iter++；//指向第2个元素

20　iter=obL. erase（iter）；//抹掉第2个元素，iter指向第3个元素

21　disp（obL）；

22　cout＜＜endl；

23　obL. erase（iter,obL.end（））；//将第3个元素直到最后1个元素都抹掉

24　disp（obL）；

25　cout＜＜endl；

26　return 0；

27　}

输出结果如下所示。

1 2 3 4 5 6 7 8 9

1 3 4 5 6 7 8 9

1

【代码解析】代码中在创建list＜int＞型迭代器时用“obL.begin（）”为其初始化，并用“iter++”使其指向list＜int＞容器对象obL的第2个元素2，代码第20行语句“iter=obL.erase（iter）；”将iter指向的第2个元素从obL中抹去，并返回指向第3个元素的迭代器，赋值给iter，因此，代码第23行语句“obL.erase（iter,obL.end（））；”将从iter指向的元素到最后1个元素全部从obL中抹去，obL中只剩下了第1个元素1。

因为vector和deque都支持下标运算符随机访问，所以使用诸如“容器对象名.erase（迭代器p，迭代器p+2）；”是合法的，但list不能这样用，换言之，在代码13.6中如果使用语句“obL.erase（iter,iter+3）；”，编译器会指出错误。

6.clear操作

成员函数clear用于将容器对象清空，clear成员函数只有一个版本，如下所示。

void clear（void）；

除了返回值外，上述代码等价于“erase（begin（），end（））”。

13.2.4　vector容器

介绍完vector、list以及deque的通用用法，下面分别讨论它们的特别之处。首先是vector，字面翻译为向量，其用法类似于前面介绍的数组，但其功能比数组更强大。简单地说，vector是数组的类表示，其提供了自动管理内存的功能，可以动态改变vector对象的长度，并随着元素的增删而增大或缩小，提供对元素的随机访问。同数组一样，在vector尾部添加和删除元素（push_back和pop_back）的时间是固定的，但在vector中间或头部增删元素（insert,erase）的时间和复杂度线性正比于vector容器对象中元素的个数。

13.2.5　deque容器

deque表示双端队列（Double-ended Queue），deque容器对象支持下标随机访问，在deque头部和尾部添加或删除元素的时间都是固定的，因此，如果有很多操作都是针对序列的头部位置，建议使用deque容器。但是，如果是在deque的中间进行元素的增删处理，操作的复杂度和时间正比于deque对象中元素的个数。

13.2.6　list容器

list类模板表示双向链表，除了首尾元素外，list容器对象中的每个元素都和前后元素相链接。list不支持下标随机访问，只能通过迭代器双向遍历。

和vector和deque不同的是，在list的任何位置增删元素的时间都是固定的。

说明

除了上述介绍的基本操作外，序列式容器还有其他用于特定场合的成员函数操作，由于篇幅有限本章只对STL作入门式介绍，了解更详细的内容可查阅相关资料。

13.3　使用关联式容器

关联式容器（Associative）又称“联合容器”，将值（value）和关键字（keyword）成对关联。举例来说，在学生管理系统设计时，可以将学号作为关键字起到索引的目的，而将学生姓名、性别和籍贯等信息作为值与学号配对。

标准STL提供了4种联合容器类模板，即set、map、multiset和multimap。总体来说set中仅仅包含关键字，而没有值的概念；map中存储的是“关键字-值”对，map和set中不会出现多个相同的关键字，即multiset和multimap可以分别看做对set和map的扩展，在multiset和multimap中允许相同关键字的存在。4种关联式容器都会根据指定的或默认的顺序排列函数，以关键字为索引对其中的元素进行排序。

13.3.1　set容器

要使用set，必须使用头文件包含命令“#include＜set＞”，同前面介绍的序列式容器vector、deque和list相似，set也要使用模板参数来提供要存储的值的类型，如下所示。

set＜存储类型[，排序函数或对象]＞容器对象名；

第1个模板类型参数用以指定存储类型，而可选的第2个模板类型参数用来指定对关键字进行排序的函数或函数对象，关于函数对象在本章稍后的内容中会有介绍。在默认情况下，将使用less＜＞模板，字面意义上可理解为按从小到大进行排列。

根据set的特点，STL提供了3种创建set的方式，如下所述。

❑创建空set容器对象，如下所示。

set＜int＞obS；❑将迭代器的区间作为参数的构造函数，如下所示。

int sz[9]={1，2，3，4，5，6，3，5，6}；

set＜int＞A（sz,sz+9）；❑根据已有同类型的容器创建新容器，如下所示。

set＜int＞B（A）；

set不支持随机访问方式，必须通过迭代器方式对set容器对象中的元素进行访问，如示例代码13.7所示。

代码13.7　set容器对象的创建和元素访问CreateAndSetContainer

＜-----------------------文件名：example1307.cpp------------------------------＞

01　#include＜iostream＞

02　#include＜set＞//使用set必须包括此头文件

03　using namespace std；

04　int main（）

05　{

06　int sz[9]={2，1，3，5，4，6，3，5，6}；//定义int型数组，数组名相当于指针（迭代器）

07　set＜int＞A（sz,sz+9）；//将迭代器区间作为参数创建容器对象A

08　cout＜＜A. size（）＜＜endl；//输出A中元素个数

09　set＜int＞：iterator it=A. begin（）；//创建set＜int＞：iterator迭代器it，指向A头部

10　while（it！=A. end（））//输出全部元素

11　{

12　cout＜＜（*it）＜＜""；

13　it++；

14　}

15　cout＜＜endl；

16　return 0；

17　}

输出结果如下所示。

6

1 2 3 4 5 6

【代码解析】代码第6行语句“set＜int＞A（sz,sz+9）”采用了9个元素的int型数组创建set＜int＞型容器对象A，可通过输出“A.size（）”发现A中的元素的确只有6个，这符合set的定义，即其中的关键字都是唯一的，与前面相同的元素都会被抛弃，通过从前到后遍历输出A中元素，输出结果为“1 2 3 4 5 6”，可以看出，set自动对关键字进行了排列，采用的是默认的由小到大的顺序，当然，在创建set型对象时，可以自行指定排序方式（第2个模板类型参数）。

13.3.2　multiset容器

使用multiset同样需要包含头文件＜set＞，multiset的创建方式与set相同，有3种方式。multiset与set不同之处在于其允许出现相同的元素，以下所示代码13.8是代码13.7的修改版本。

代码13.8　multiset的创建和元素访问CreateAndMultisetContainer

＜------------------------文件名：example1308.cpp-----------------------------＞

01　#include＜iostream＞

02　#include＜set＞//使用multiset必须包括此头文件

03　using namespace std；

04　int main（）

05　{

06　int sz[9]={2，1，3，5，4，6，3，5，6}；//定义int型数组，数组名相当于指针（迭代器）

07　multiset＜int＞A（sz,sz+9）；//将迭代器区间作为参数创建容器对象A

08　cout＜＜A. size（）＜＜endl；//输出A中元素个数

09　multiset＜int＞：iterator it=A. begin（）；//创建multiset＜int＞：iterator迭代器it，指向A头部

10　while（it！=A. end（））//输出全部元素

11　{

12　cout＜＜（*it）＜＜""；

13　it++；

14　}

15　cout＜＜endl；

16　return 0；

17　}

输出结果如下所示。

9

1 2 3 3 4 5 5 6 6

【代码解析】代码的输出结果很明显地指出了multiset和set的不同，代码第7行的语句“multiset＜int＞A（sz,sz+9）”创建multiset＜int＞型容器对象A。multiset允许有相同的关键字，因此A中的元素个数为9，但是在创建A时，还是对9个元素进行了排序。

13.3.3　map容器

使用map必须包括头文件＜map＞，map是一对对“关键字-值”的组合，“关键字”用于搜寻，而“值”用来表示要存储和取出的数据，在map容器对象中，每个关键字只能出现一次，或者说特定的关键字只能和一个值相关联。

为了方便对map和multimap进行增删处理，STL使用pair＜class Type1，class Type2＞类模板将两个类型Type1和Type2存储到一个对象中，如果keyword是关键字类型，value是被存储的数据类型，那么可以用下述指令生成map容器对象的一个元素t。

pair＜const keyword,value＞t（keydata,valuedata）；也可以创建pair＜const keyword,value＞对象的匿名对象，调用格式如下所示。

pair＜const keyword,value＞（keydata,valuedata）；

注意

const表明关键字是只读的，不可修改。

创建一个map容器对象的基本格式如下所示。

map＜关键字类型，值类型[，排序函数或对象]＞容器对象名；

排序函数或对象仍然是可选的，用以指定对关键字进行排序的函数或函数对象。同set及multiset一样，默认使用less＜＞模板，字面意义上可理解为从小到大进行排列。

由此可知map的创建方式同样有以下3种。

❑创建空map容器对象，如下所示。

map＜int,string＞obM；//关键字为int型，值为C++字符串string型❑将迭代器的区间作为参数的构造函数，如下所示。

pair＜int,string＞sz[4]={pair＜int,string＞（1，"Asia"），pair＜int,string＞（4，"Africa"），

pair＜int,string＞（9，"Euro"），pair＜int,string＞（4，"America"）}；

map＜int,string＞obM（sz,sz+4）；❑根据已有同类型的容器创建新容器，如下所示。

map＜int,string＞B（A）；

map同样不支持下标随机访问的方式，必须通过类内迭代器对元素进行访问，如示例代码13.9所示。

代码13.9　map容器对象的创建和元素访问CreateAndMapContainer

＜------------------------文件名：example1309.cpp-----------------------------＞

01　#include＜iostream＞

02　#include＜map＞//使用map容器要包含的头文件

03　#include＜string＞//使用string类要包含的头文件

04　using namespace std；

05　int main（）

06　{

07　//创建part＜int,string＞型数组

08　pair＜int,string＞sz[4]={pair＜int,string＞（9，"Asia"），pair＜int,string＞（4，"Africa"），

09　pair＜int,string＞（1，"Euro"），pair＜int,string＞（4，"America"）}；

10　map＜int,string＞obM（sz,sz+4）；//用迭代器区间构造obM

11　cout＜＜obM. size（）＜＜endl；//输出obM中的元素个数

12　map＜int,string＞：iterator it=obM. begin（）；//创建map＜int,string＞模板类的

13　//迭代器，指向obM的头部

14　while（it！=obM. end（））//按顺序逐个输出obM中的元素

15　{

16　cout＜＜（*it）. first＜＜"："＜＜（*it）.second＜＜endl；

17　it++；

18　}

19　return 0；

20　}

输出结果如下所示。

3

1：Euro

4：Africa

9：Asia

【代码解析】代码第8行的pair＜int,string＞类型的数组sz有4个元素，可map＜int,string＞容器对象中只有3个元素，这是因为pair＜int,string＞（4，"Africa"）和pair＜int,string＞（4，"America"）的关键字同为4，而map中不允许出现相同的关键字，所以后面一个pair被抛弃。

通过map＜int,string＞：iterator声明的类内迭代器的间接访问返回的pair＜int,string＞类型，在pair类模板中并没有对输出操作（＜＜）进行重载，所以要分别使用（*it）.first和（*it）.second形式输出pair模板类对象的第1个元素和第2个元素。

从输出结果可以看出，在obM创建时，对其中的元素自动进行了排列，由于程序中并没有显式指定要使用的比较函数或对象，所以采用了默认的less＜＞方式，按从小到大的顺序进行排列。

13.3.4　multimap容器

multimap与map的关系，类似于multiset与set的关系，使用multimap同样需要包含头文件＜map＞，multimap的创建方式与map相同，有3种方式。multimap与map不同之处在于其允许出现相同的元素，代码13.10和代码13.9几乎一样，不同的是将map换成了multimap，以帮助读者理解两者的区别。

代码13.10　multimap容器对象的创建和元素访问CreateAndMultimap

＜------------------------文件名：example1310.cpp-----------------------------＞

01　#include＜iostream＞

02　#include＜map＞//使用multimap容器要包含的头文件

03　#include＜string＞//使用string类要包含的头文件

04　using namespace std；

05　int main（）

06　{

07　//创建part＜int,string＞型数组

08　pair＜int,string＞sz[4]={pair＜int,string＞（9，"Asia"），pair＜int,string＞（4，"Africa"），

09　pair＜int,string＞（1，"Euro"），pair＜int,string＞（4，"America"）}；

10　multimap＜int,string＞obM（sz,sz+4）；//用迭代器区间构造obM

11　cout＜＜obM. size（）＜＜endl；//输出obM中的元素个数

12　multimap＜int,string＞：iterator it=obM. begin（）；//创建multimap＜int,string＞模板类的

13　//迭代器，指向obM的头部

14　while（it！=obM. end（））//按顺序逐个输出obM中的元素

15　{

16　cout＜＜（*it）. first＜＜"："＜＜（*it）.second＜＜endl；

17　it++；

18　}

19　return 0；

20　}

输出结果如下所示。

4

1：Euro

4：Africa

4：America

9：Asia.

【代码解析】输出结果体现了multimap的特点，即允许特定的关键字多次出现，也就是说允许发生一个关键字对应多个值的情况，代码第10行的multimap容器对象在创建时同样会对其中的元素进行排序处理。

13.4　关联式容器支持的成员函数操作

同序列式容器一样，关联式容器同样支持插入、删除以及元素的查找和访问等通用操作，本节将针对上述3个方面分别进行介绍。

13.4.1　元素的插入

几种insert函数的描述如表13.2所示，其中ob代表关联式容器对象名，t对于set和multiset是个关键字，而对map和multimap是个pair结构。

下面对表13.2中所列函数进行简要的介绍。

1.pair＜iterator,bool＞ob.insert（t）

此函数适用于map和set容器，返回一个pair＜iterator,bool＞值，当ob中不包含与t有相同关键字的值时，将t插入ob中，bool的值为true；如果ob中已包含与t有相同关键字的值，不进行插入操作，bool的值为false，不论是否进行了插入操作，iteraotr都指向关键字与t相同的元素。

2.iterator ob.insert（t）

此函数适用于multimap和multiset容器，将t插入到ob中并返回指向其位置的迭代器。编号1和2的两个insert成员函数的用法如代码13.11所示。

代码13.11　将一个元素插入到关联式容器中AssociativeInsert1

＜-------------------------文件名：example1311.cpp----------------------------＞

01　#include＜iostream＞

02　#include＜map＞//使用map必须要使用的头文件

03　#include＜string＞

04　using namespace std；

05　int main（）

06　{

07　//创建一个pair＜int,string＞数组sz

08　pair＜int,string＞sz[2]={pair＜int,string＞（1，"A"），pair＜int,string＞（2，"B"）}；

09　pair＜int,string＞t（2，"X"）；//待插入的pair对象

10　map＜int,string＞obM（sz,sz+2）；//基于sz创建map对象obM

11　map＜int,string＞：iterator itM=obM. begin（）；创建map＜int,string＞类内迭代器itM

12　//将t插入obM中，返回结果保存在pair＜map＜int,string＞：iterator,bool＞对象res中

13　pair＜map＜int,string＞：iterator,bool＞res=obM. insert（t）；

14　if（res. second）//判断是否插入成功

15　cout＜＜"插入成功"＜＜endl；

16　else

17　cout＜＜"已包含关键字与t相同的元素："＜＜（*res.first）.second＜＜endl；

18　multimap＜int,string＞obDM（sz,sz+2）；//基于sz创建multimap对象obDM

19　//创建multimap＜int,string＞类内迭代器

20　multimap＜int,string＞：iterator itDM=obDM. begin（）；

21　itDM=obDM. insert（t）；//执行插入操作

22　cout＜＜"插入的元素为："＜＜（*itDM）.second＜＜endl；

23　return 0；

24　}

输出结果如下所示。

已包含关键字与t相同的元素：B

插入的元素为：X

【代码解析】代码第13行是执行map容器的插入操作，而代码第21行是执行multimap容器的插入操作。

3.iterator ob.insert（p,t）

从迭代器p开始搜索应将t插入到ob中的位置，如下所示。

❑如果ob是map和set容器对象，只有当ob中不包含与t相同的关键字时，才进行插入，否则不进行插入。不论插入与否，返回的iterator都指向另外一个关键字与t相同的元素。

❑如果ob是multimap和multiset对象，将t插入到ob中并返回指向其位置的迭代器。p的作用是加快查找的速度。

4.void ob.insert（i,j）

i和j是一对输入迭代器，对map和set来说，i和j指向其关键字类型；对multimap和multiset来说，i和j指向pair＜keyword,value＞结构。此函数用以将区间[i,j]的元素插入到ob中，如示例代码13.12所示。

代码13.12　关联式容器插入一段区间AssociativeInsert2

＜-----------------------文件名：example1312.cpp------------------------------＞

01　#include＜iostream＞

02　#include＜map＞//使用map必须要使用的头文件

03　#include＜string＞

04　using namespace std；

05　int main（）

06　{

07　//创建一个pair＜int,string＞数组sz

08　pair＜int,string＞sz[4]={pair＜int,string＞（7，"G"），pair＜int,string＞（3，"C"），

09　pair＜int,string＞（5，"E"），pair＜int,string＞（1，"A"）}；

10　//创建一个pair＜int,string＞数组t

11　pair＜int,string＞t[4]={pair＜int,string＞（2，"B"），pair＜int,string＞（4，"D"），

12　pair＜int,string＞（6，"F"），pair＜int,string＞（1，"A"）}；

13　map＜int,string＞obM（sz,sz+4）；//基于sz创建map对象obM

14　obM. insert（t,t+4）；//执行插入操作

15　for（map＜int,string＞：iterator itM=obM. begin（）；itM！=obM.end（）；itM++）

16　{

17　cout＜＜（*itM）. first＜＜""＜＜（*itM）.second＜＜endl；

18　}

19　cout＜＜"下面是multimap的插入操作："＜＜endl；

20　multimap＜int,string＞obDM（sz,sz+4）；//基于sz创建multimap对象obDM

21　obDM. insert（t,t+4）；//执行插入操作

22　for（multimap＜int,string＞：iterator itDM=obDM. begin（）；itDM！=obDM.end（）；itDM++）

23　{

24　cout＜＜（*itDM）. first＜＜""＜＜（*itDM）.second＜＜endl；

25　}

26　return 0；

27　}

输出结果如下所示。

1 A

2 B

3 C

4 D

5 E

6 F

7 G下面是multimap的插入操作：

1 A

1 A

2 B

3 C

4 D

5 E

6 F

7 G

【代码解析】代码第14行演示了如何将一段区间插入到map，而代码第21行是将一段区间插入到multimap中，容器对象会根据插入元素的关键字对元素进行排序。

注意

相比map和multimap的用法，set和multiset要简单得多。不过4种关联式容器都会根据关键字对插入的元素进行排序处理。

13.4.2　元素的删除

关联式容器支持以下4种删除元素的方式。

1.ob.erase（keyword）

删除容器对象ob中所有关键字为keyword的元素，并返回删除元素的个数。以较复杂的map和multimap为例，演示这一应用如代码13.13所示。

代码13.13　删除具有特定关键字的所有元素ElementsDelete

＜--------------------文件名：example1313.cpp---------------------------------＞

01　#include＜iostream＞

02　#include＜map＞

03　#include＜string＞

04　using namespace std；

05　typedef map＜int,string＞CM；//typedef简化定义

06　typedef CM：value_type CMV；//CMV实际上等价于pair＜int,string＞

07　typedef CM：iterator CMI；

08　typedef multimap＜int,string＞MCM；

09　typedef MCM：iterator MCMI；

10　void disp（CM＆x）//定义CM类型元素遍历输出函数disp

11　{

12　for（CMI it=x. begin（）；it！=x.end（）；it++）

13　{

14　cout＜＜（*it）. first＜＜""＜＜（*it）.second＜＜endl；

15　}

16　}

17　void disp（MCM＆x）//重载MCM类型元素遍历输出函数

18　{

19　for（MCMI it=x. begin（）；it！=x.end（）；it++）

20　{

21　cout＜＜（*it）. first＜＜""＜＜（*it）.second＜＜endl；

22　}

23　}

24　int main（）

25　{

26　//相当于定义了一个pair＜int,string＞数组

27　CMV sz[3]={CMV（1，"A"），CMV（2，"B"），CMV（1，"C"）}；

28　CM obM（sz,sz+3）；//创建map＜int,string＞对象

29　disp（obM）；//元素遍历输出

30　CM：size_type num1=obM. erase（1）；

31　cout＜＜"删除所有关键字为1的元素"＜＜endl；

32　disp（obM）；

33　cout＜＜"共删除了"＜＜num1＜＜"个元素"＜＜endl；

34　MCM obDM（sz,sz+3）；//创建multimap＜int,string＞对象

35　disp（obDM）；//元素遍历输出

36　MCM：size_type num=obDM. erase（1）；

37　cout＜＜"删除所有关键字为1的元素"＜＜endl；

38　disp（obDM）；

39　cout＜＜"共删除了"＜＜num＜＜"个元素"＜＜endl；

40　return 0；

41　}

输出结果如下所示。

1 A

2 B

删除所有关键字为1的元素

2 B

共删除了1个元素

1 A

1 C

2 B

删除所有关键字为1的元素

2 B

共删除了2个元素

【代码解析】代码中引入了一些惯常用法，细心的读者可能早已注意到，在前面给出的示例中使用的诸如map＜int,string＞：iterator使得代码十分冗长，可读性变差。解决的思路是引入typedef重命名机制，用一个简单的借记符来代替，如代码第5行所示。

typedef map＜int,string＞CM

便指定使用CM来代替map＜int,string＞。

另一个问题是容器中定义的类型的引入，原来指定map容器对象的元素结构时，一律是直接采用pair结构来指定，实际上STL为每个容器，不仅仅是关联式容器，也包括顺序容器都定义了一些类型，用以为不同的容器提供统一的接口，而一直使用的类内迭代器，如map＜int,string＞：iterator便是一种“容器中定义的类型”。为所有容器定义的类型如表13.3所示，其中Type代表容器类型，如list＜int＞和multimap＜int,string＞等。

介绍完容器中定义的类型后，读者可以再看看代码13.1，其中在使用下标随机访问obD中的元素时，使用的下标i是unsigned int型，其实更为合理的用法应将i声明为如下形式。

deque＜double＞：sizetype i=0；

同理，对map＜int,string＞类模板来说，使用map＜int,string＞：value_type比使用pair＜int,string＞更能增加程序的可读性，虽然两者本质上是等价的。

STL还特别对关联式容器定义了一些类型，如表13.4所示。

关于函数对象的概念在算法一节中将会仔细介绍。

2.void ob.erase（p）

从ob中删除迭代器p所指向的元素，p必须指向ob中确实存在的元素，而且不能等于ob.end（），因为ob.end（）称为“超尾值”，它是最后一个元素的下一个位置，所以ob.end（）指向的元素并非“确实存在”于对象中。

3.void ob.erase[q1，q2）

从ob中删除半开半闭区间[q1，q2）之间的元素。

4.void clear（void）

删除ob中所有元素，等价于void ob.erase（ob.begin（），ob.end（））。

13.4.3　元素的查找与访问

STL为关联式容器提供了一些元素的查找与访问成员函数操作，以方便对元素的管理，如表13.5所示。

注意

除了上述介绍的成员函数操作外，STL中还有其他定义的成员函数，但无法一一列举，详细资料可查阅STL的专用工具书。

13.5　迭代器

在本章介绍过的示例代码中，迭代器起了很重要的作用，从介绍过的用法看，迭代器类似于指针，用以指示容器中的某个元素。实际上，我们使用的类内迭代器只是迭代器的一种形式，本节将详细介绍迭代器的相关知识。

13.5.1　理解迭代器本质

模板的引入使得函数和类定义脱离了存储类型的限制，在需要时指定它们即可，这是一种泛化的思维观念，这使得算法独立于类型，迭代器是一种更高层次的抽象，其使得算法独立于容器。

迭代器是一种类型，在程序中使用的是其对象。从迭代器的层面上看，对所有类型容器元素的访问应该是等价的，因此，迭代器对象应具备以下功能。

❑间接访问（*p）即deference。在迭代器类中必须对一元*操作符定义，还需定义运算符-＞以访问容器元素。

❑迭代器对象之间的赋值，如p=q。在迭代器类中必须定义赋值操作符。

❑迭代器对象间的比较即比较两个迭代器是否相等。因此，在迭代器类内必须对关系运算符==和！=进行定义，原则上讲，不需要对迭代器进行大小比较（＜、＞）等，就像比较指针实际上是比较其中存储的地址大小一样，这毫无意义。

❑能使用迭代器遍历容器中所有的元素。在本章已给出的示例代码中已经大量应用了诸如“p++”之类的操作，因此，在迭代器类中必须对前缀增1和后缀增1进行定义。

根据上述要求，STL为每个容器类定义了相应的迭代器类，即“Type：iterator”结构，其中，Type是诸如“vector＜int＞”、“multiset＜double＞”等形式的模板类，对于某些容器来说，迭代器可能就是指针，如set容器，而对另外一些容器，迭代器可能是对象，如map容器，但不管其内部是如何实现的，类内定义的迭代器都提供了上述基本功能，有的容器根据需要还扩展了一些其他功能。

每个容器类都定义了begin（）和end（）操作，分别返回指向容器第一个元素和超尾位置的迭代器，每个容器类都定义了++操作，逐个遍历容器中的每个元素。

注意

STL是标准库，因此除非出于研究的需要，一般不需要知道容器类是如何实现的，以及类内迭代器类又是如何实现的，只要会使用即可。

理解迭代器是下一步理解STL算法的基础，正因为有了迭代器，算法才脱离了容器类型的束缚，真正做到了“泛型”和“通用”。

13.5.2　迭代器类型

不同的算法对迭代器的要求不同，为此STL定义了5种迭代器，即随机访问迭代器（RandomAccessIterator）、双向迭代器（BidirectionalIterator）、前向迭代器（ForwardIterator）、输入迭代器（InputIterator）和输出迭代器（OutputIterator），其层次结构如图13.1所示。

按图13.1所示的箭头方向，迭代器实现的功能越来越少，对其使用限制越来越多，下面分别对5种类型进行介绍。

1.输入迭代器

可以被用来读取容器中的元素但是不保证支持向容器的写入操作，“输入”是相对于程序而言的，换言之不能使用输入迭代器修改容器对象中元素的值。

图　13.1　迭代器层次结构图

输入迭代器实现了下述必备功能。

❑两个iterator的相等和不相等测试。

❑前置++和后置++递增iterator指向下一个元素，以实现容器中元素的遍历。

❑通过解引用操作符operator*读取一个元素。

2.输出迭代器

输出迭代器可被认为是与输入迭代器功能相反的迭代器，可用其向容器中写入元素（修改元素的值），但不能读取容器中的元素。

3.前向迭代器

与输入迭代器和输出迭代器一样，前向迭代器只使用++来遍历容器，这也是命名“前向”的由来。前向迭代器既可以读取数据，也可以修改数据。如果使用const来修饰前向迭代器对象，则该迭代器对象只能用来读取数据。

前向迭代器可以看成是输入迭代器和输出迭代器双向功能的集成。

4.双向迭代器

双向迭代器可以从两个方向对一个容器进行读写。除了同前向迭代器能够实现的功能外，双向迭代器还支持前后缀的递减操作。

5.随机访问迭代器

只有随机访问迭代器能够支持随机访问，能够直接跳到容器中的任何一个元素处，对其进行读写操作。除了能够实现双向迭代器的功能外，随机访问迭代器还添加了支持随机访问的操作（重载了[]操作符）和用于对元素进行排序的关系操作符，如表13.6所示，其中，o1和o2是迭代器变量，v1和v2是o1和o2的值，n为整数。

注意

迭代器增减时，只有当表达式仍在容器区间内时才合法。

13.5.3　为什么要定义这么多迭代器

不同的算法要求的迭代器类型不同，之所以定义了5种迭代器，是为了使用“最合适”的工具，编写算法时在满足要求的基础上尽可能地使用功能少的迭代器，减少迭代器引入的副作用，假设要编写一个查找函数find的程序，只要能读取容器中的元素即可，最理想的方案是使用输入迭代器，这样，有效防止了在find函数内对元素的修改，真正实现“物尽其用”，正如指针一章中曾经介绍的，一把刀既能削铁如泥，又能砍瓜切菜，还能理发，但是真正用其理发是很危险的，不如剃头刀来的安全方便。

对5种迭代器初步了解后，重新看一下图13.1，实际上，依照箭头的方向，迭代器实现的功能越来越少，除了输出迭代器和输入迭代器是功能相反的并列关系外，箭头左侧的迭代器不仅能够实现右侧迭代器所有的功能，还在其基础上增加了一些功能。所以说，箭头左侧的迭代器“适应”于箭头右侧的迭代器，因此，如果某个算法的形参为前向迭代器，则实参可以是双向迭代器和随机访问迭代器，但不能是输出迭代器或输入迭代器。

根据特定迭代器类型编写的算法可以使用该种迭代器，还可以使用具有其所有功能的迭代器，从这个意义上说，随机访问迭代器最强大，几乎可以适用于任何的算法。

迭代器实现的功能汇总如表13.7所示，■代表此迭代器有此功能，而□代表没有，其中p为迭代器变量。

13.5.4　容器中定义的迭代器类型与5种类型的对应

在前面介绍的7种容器中，类内定义的迭代器对应的类型如表13.8所示，明确这一点可以为STL算法一节的理解打下基础。

13.5.5　流迭代器

流迭代器是一种特殊的迭代器，它包括istream_iterator和ostream_iterator，需要理解的要点是将输入/输出流看做容器。因此，任何接受迭代器参数的算法都可以和流一起工作。使用流迭代器必须要包含头文件＜iterator＞。

1.输出流迭代器

对输出流来说，STL提供了ostream_iterator模板，其定义的基本格式如下所示。

ostream_iterator＜class typename1，class typename2=char＞迭代器名（ostream_type＆ost,const typename2*p=0）；例如下述代码所示。

ostream_iterator＜int,char＞it（cout，"\n"）；

it现在是个输出流迭代器，通过it可使用cout输出信息，typename1（这里为int）指出发送给输出流的数据类型，typename2（这里是char）指出输出流要使用的字符类型，char是默认值，构造函数第1个参数ost指出了要使用的输出流，其可以是文件输出流，见第14章的内容。因此，流迭代器可用于文件的读写，最后1个参数，即typename2类型指针p指向发送给输出流的每个typename数据后显示的分隔符。

输出流迭代器的示例如代码13.14所示。

代码13.14　输出流迭代器的用法OstreamIterator

＜-----------------------文件名：example1314.cpp------------------------------＞

01　#include＜iostream＞

02　#include＜iterator＞

03　#include＜algorithm＞

04　#include＜vector＞

05　using namespace std；

06　int main（）

07　{

08　int sz[6]={1，2，3，4，5，6}；

09　vector＜int＞ob（sz,sz+6）；//创建vector＜int＞容器ob

10　ostream_iterator＜int,char＞osi（cout，""）；//创建输出流迭代器osi

11　copy（ob. begin（），ob.end（），osi）；//将ob复制到流中

12　cout＜＜endl；

13　return 0；

14　}

输出结果如下所示。

1 2 3 4 5 6

【代码解析】代码第11行使用了泛型算法copy，关于泛型算法的介绍请参考下一节的内容，copy共有3个迭代器参数，前两个表示要复制的范围，第3个表示将第1个元素复制到什么位置，本例理解的关键在于将输出流理解为一个容器，与普通容器的不同之处在于其中的元素都会被自动输出，且输出的元素间有特定的分割符。

2.输入流迭代器

对输入流来说，STL提供了istream_iterator模板，其定义格式如下所示。

istream_iterator＜class typename1，class typename2=char＞迭代器名（istream_type＆ist）；

输入流迭代器同样有两个模板参数，typename1指出要读取的数据类型，typename2指出输入流使用的字符类型，默认为char，构造函数只有一个参数，ist指明了要使用的输入流，既可以是常用的cin，又可以是文件输入流。

输入流迭代器的具体用法，如代码13.15所示

代码13.15　输入流迭代器的用法IstreamIterator

＜--------------------文件名：example1315.cpp---------------------------------＞

01　#include＜iostream＞

02　#include＜iterator＞

03　#include＜algorithm＞

04　#include＜vector＞

05　using namespace std；

06　int main（）

07　{

08　vector＜int＞ob；//创建vector＜int＞容器ob

09　istream_iterator＜int,char＞isi（cin）；//创建输入流迭代器osi

10　copy（isi,istream_iterator＜int,char＞（），back_inserter（ob））；//将输入流插入到ob中

11　for（vector＜int＞：iterator it=ob. begin（）；it！=ob.end（）；it++）//遍历输出ob中的元素

12　{

13　cout＜＜（*it）＜＜""；

14　}

15　cout＜＜endl；

16　return 0；

17　}

输出结果如下所示。

1 5 9 7 8（用户输入）

2 6 4（用户输入）

h（用户输入）

1 5 9 7 8 2 6 4

【代码解析】代码第10行的copy函数的起始位置分别是输入流迭代器isi和省略了构造函数的匿名输入迭代器istream_iterator＜int,char＞（），代表输入失败，因此，copy语句用于从输入流中提取int型数据并将其插入ob的后部，直到类型不匹配或出现其他故障，所以，当用户输入h时，copy操作停止，程序对ob进行遍历输出。

back_inserter是另外一种迭代器适配器，迭代器适配器的相关内容稍后会详细介绍。

13.5.6　迭代器失效

可以将迭代器理解为广义的指针，如果对容器的操作影响了元素的存放位置，那么原来的指针指向的区域便不再有意义，这称为迭代器失效。

迭代器失效的情况相对较复杂，需要在实践中不断地进行总结，下面是一些常见的迭代器失效情况。

1.vector

❑插入（push_back）一个元素后，end操作返回的迭代器肯定失效。

❑插入（push_back）一个元素后，capacity返回值与没有插入元素之前相比有改变，则需要重新加载整个容器，此时first和end操作返回的迭代器都会失效。

❑进行删除操作（erase,pop_back）后，指向删除点的迭代器全部失效；指向删除点后面的元素的迭代器也将全部失效。

2.deque

❑在deque容器首部或者尾部插入元素不会使得任何迭代器失效。

❑在其首部或尾部删除元素则只会使指向被删除元素的迭代器失效。

❑在deque容器的任何位置插入和删除操作将会使指向该容器元素的所有迭代器失效。

3.List/set/map/multiset/multimap

删除元素时，指向该删除节点的迭代器失效。

13.6　泛型算法

本节讨论的算法不是指容器的成员函数，而是STL类库中提供的一些通用的非成员函数操作。算法是STL的重要组成部分，是处理容器中数据的方法和操作。这些算法可应用于多种容器类型中，因此称为“泛型”。泛型算法不是针对容器编写的，而只是单独依赖迭代器和迭代器操作来实现的。

使用泛型算法必须先包含algorithm头文件，使用泛化的算术算法则必须包含numeric头文件，如下所示。

#include＜algorithm＞

#include＜numeric＞

在介绍算法前，先讲解函数对象的概念。

13.6.1　什么是函数对象

在第7章中介绍了指向函数的指针这个概念，使得函数可以作为参数传递给另一个函数，在第9章中又介绍了函数调用操作符的重载，它在程序中可以使用诸如“对象名（参数表）”的形式，实际上上述两个概念都属于函数对象。

函数对象是可以以函数方式与（）结合使用的任意对象，包括以下内容。

❑函数名。

❑指向函数的指针。

❑重载了（）操作符的类对象，即定义了函数operator的类。

STL定义了一组函数对象，分为算术运算（Arithmetic）、关系元素（Relational）和逻辑运算（Logical）3大类，在使用这些函数对象前，必须包含相应的头文件＜functional＞，如下所述。

❑6个算术运算：plus＜type＞、minus＜type＞、negate＜type＞、multiplies＜type＞、divides＜type＞和modules＜type＞。

❑6个关系运算：less＜type＞、less_equal＜type＞、greater＜type＞、greater_equal＜type＞、equal_to＜type＞和not_equal_to＜type＞。

❑3个逻辑运算，分别对应于＆＆、！和！：logical_and＜type＞、logical_or＜type＞和logical_not＜type＞。

常用下列规则对函数对象进行分类。

1.生成器（Generator）

不用参数就可以调用的函数对象。

2.一元函数（Unary Function）

用一个参数就可以调用的函数对象。

3.二元函数（Binary Function）

用两个参数就可以调用的函数对象。

需注意将返回bool值的一元函数称为一元断言（Predicate），简称断言；将返回bool值的二元函数是二元断言（Binary Predicate）。

13.6.2　算法分类

STL将算法库分为4组，前3个在algorithm头文件中进行描述，而第4个在numeric头文件中进行描述，如下所述。

❑非修改式序列操作，不可以改变容器的内容，如find（）和for_each（）等。

❑修改式序列操作，可以修改容器中的内容，如transform（）、random_shuffle（）和copy等。

❑排序和相关操作，包括各种排序函数等，如sort（）等。

❑通用数字运算，计算两个容器的内部乘积等。

STL中算法函数太多，无法一一进行介绍，本节先以简表的形式列举出常用的算法。

1.非修改式序列操作

非修改式序列操作函数如表13.9所示，因为参数的形式相对复杂，表中只列举了函数模板名，具体的应用方式可查阅STL的相关资料。

以for_each函数为例，其原型如下所示。

template＜class InputInterator,class Function＞

Function for_each（InputIterator First,InputIterator Last,Function f）；

将一个非修改式函数对象f用于这个区间[First,Last）中的每个成员，其中函数对象f为一元函数，参数为容器对象元素或元素的引用，for_each函数的使用范例如代码13.16所示。

代码13.16　for_each函数的用法ForEach

＜------------------------文件名：example1316.cpp-----------------------------＞

01　#include＜iostream＞

02　#include＜algorithm＞//使用for_each函数要包含的头文件

03　#include＜vector＞

04　using namespace std；

05　typedef vector＜int＞VI；

06　typedef VI：iterator VIP；

07　void func（VI：value_type v）//定义1个原函数func，以元素类型为参数

08　{

09　cout＜＜v＜＜""；

10　}

11　int main（）

12　{

13　int sz[]={1，2，3，4，5，6}；

14　VI ob（sz,sz+6）；

15　for_each（ob. begin（），ob.end（），func）；//对ob中所有元素执行func操作

16　cout＜＜endl；

17　return 0；

18　}

输出结果如下所示。

1 2 3 4 5 6

【代码解析】代码第7行的func函数是一个函数对象，定义了对ob中元素的操作，for_each函数用于对ob中所有元素执行func操作。

2.修改式序列操作

STL中提供了很多序列操作函数，表13.10列举了函数模板名，具体的应用方式可查阅STL的相关资料。

3.排序和相关操作

表13.11列举排序及其相关操作的函数模板名，具体的应用方式可查阅STL的相关资料。

以replace_if（）为例，其原形如下所示。

template＜class InputIterator,class Predict,class T＞

void replace_if（Forward Iterator First,ForwardIterator Last,Predicate pred,const T＆new）；

如果区间[First,Last）中的元素使得元素pred返回true，则将该元素用new替换，否则不予替换，如代码13.17所示。

代码13.17　replace_if函数使用方法ReplaceIf

＜----------------------文件名：example1317.cpp-------------------------------＞

01　#include＜iostream＞

02　#include＜algorithm＞//使用for_each函数要包含的头文件

03　#include＜functional＞//使用less＜＞类模板要用的头文件

04　#include＜vector＞

05　using namespace std；

06　typedef vector＜int＞VI；

07　typedef VI：iterator VIP；

08　int main（）

09　{

10　int sz[]={1，2，3，4，5，6}；

11　VI ob（sz,sz+6）；

12　less＜int＞lt；//使用类模板less＜int＞创建函数对象

13　replace_if（ob. begin（），ob.end（），bind2nd（lt，3），7）；

14　for（VIP v=ob. begin（）；v！=ob.end（）；v++）

15　{

16　cout＜＜（*v）＜＜""；

17　}

18　cout＜＜endl；

19　return 0；

20　}

输出结果如下所示。

7 7 3 4 5 6

【代码解析】代码第12行使用类模板less＜int＞创建的函数对象it是二元断言，而replace_if函数中要使用的是一元断言，因此必须为it提供一个接口，使其可以用在replace_if函数中，为了将it转化成一元断言，代码13.17使用了“将it的第2个数据绑定为用户指定的数值3”的方法，即如果ob中的元素小于3，则返回true，该元素替换为7，否则不执行替换。

这种转换的思路便是适配器（Adapter）思想，稍后详细介绍适配器。

4.数字操作

表13.12列举了的是STL提供的数字和计算函数模板。

13.7　适配器

前面简要提到了适配器的概念，适配器相当于提供了一个接口，使得某些不适用于特定对象的方法可以被该对象所用，适配器形象的功能图解如图13.2所示。图中容器或函数对象无法直接应用于算法，因此，必须有一种中间过渡机制来实现两者的匹配，这就是适配器。从本质上而言适配器是使一种事物的行为类似于另一种事物的行为的一种机制。

STL定义了3种形式的适配器，即容器适配器、迭代器适配器和函数适配器。

图　13.2　适配器作用图

❑容器适配器：包括栈（stack）、队列（queue）及优先（priority_queue）。可以把stack看做是某种特殊的vector、deque或者list容器，只是其操作仍然受到stack本身属性的限制。queue和priority_queue与之类似。容器适配器的接口更为简单，只是受限比一般容器要多。

❑迭代器适配器：修改为某些基本容器定义的迭代器的接口的一种STL组件。反向迭代器和插入迭代器都属于迭代器适配器，迭代器适配器扩展了迭代器的功能。

❑函数适配器：通过转换或者修改其他函数对象使其功能得到扩展。这一类适配器有否定器（Negator，相当于“非”操作）、绑定器（Binder）和成员函数适配器。

13.7.1　容器适配器

容器适配器可以看做对容器的封装，使其具有某些特殊的功能。容器适配器以一种已存在的容器类型采用另一种不同的抽象类型的工作方式实现，只是发生了接口转换而已。标准库提供了3种序列容器适配器，即queue、priority_queue和stack。

所有适配器都定义了两个构造函数：默认构造函数用于创建空对象；带一个容器参数的构造函数将参数容器的副本作为其基础值。

默认的stack和queue都基于deque容器实现，而priority_queue则在vector容器上实现。在创建适配器时，通过将一个顺序容器指定为适配器的第2个类型参数，可覆盖其关联的基础容器类型。例如，下述代码创建的ob栈是基于vector实现的。

stack＜int,vector＜int＞＞ob；

对于给定的适配器，其关联的容器必须满足一定的约束条件。stack适配器所关联的基本容器可以是任意一种序列容器类型，因为这些容器类型都提供了push_back、pop_back和back操作；queue适配器要求其关联的基础容器必须提供pop_front操作，因此其不能建立在vector容器上；priority_queue适配器要求提供随机访问功能，因此不能建立在list容器上。

1.stack

比起普通的序列式容器stack的限制更多，其不允许随机访问栈内元素，甚至不允许对栈内元素进行遍历访问，其使用机制遵循FILO原则，支持的操作为压元素入栈、弹元素出栈、查看栈顶、查看元素数目和测试栈是否为空等，如表13.13所示。

2.queue

使用queue和priority_queue必须包含头文件＜queue＞，与stack不同的是，queue使用了先进先出（FIFO）的存储和检索策略，进入队列的元素放置在尾部，下一个被取出的元素则取自队列的首部。

queue模板的限制比deque或list更多，同stack一样，queue不允许随机访问队列元素，也不允许遍历访问队列内的元素，支持的操作为压元素入队尾、从队首弹出元素、参看队首和队尾的值、查看元素数目和测试队列是否为空等，表13.14列出了这些操作。

3.priority_queue

priority_queue支持的操作与queue相同，默认使用元素类型的＜操作符来确定其之间的优先级关系，用户也可以定义自己的优先级关系。在优先级队列中，新元素被放置在比其优先级低的元素的前面。

priority_queue默认的底层容器是vector，但用户也可以用deque对其进行覆盖，并指定优先级排列方式，如下所示。

priority_queue＜int,deque＜int＞＞pq（less＜int＞）；

在上述定义的pq中，较小的元素具有高优先级。

相比queue,priority_queue增加了一个top操作，返回具有最高优先级的元素，但不删除该元素，如表13.14所示。

注意

对3种容器迭代器来说，pop方法只会将队首或栈顶的元素从队列或栈中删除，并不返回该值，要检索该值，应使用front（）或top（）操作。

13.7.2　迭代器适配器

从某种意义上讲，前面介绍的流迭代器也是迭代器适配器，此外还包括反向迭代器和插入迭代器。同使用流迭代器一样，使用反向和插入迭代器都必须包含头文件＜iterator＞。

1.反向迭代器

reverse_iterator的用法很有趣，对其执行递增操作会导致其递减。只要将reverse_iterator使用于原来正向访问的函数中，便可实现对容器的逆向操作。对常用容器来说（3种序列式容器和4种关联式容器），其内部都定义了reverse_iterator类，例如代码13.18用于反向输出容器中的元素。

代码13.18　反向迭代器的使用ReverseIterator

＜------------------------文件名：example1318.cpp-----------------------------＞

01　#include＜iostream＞

02　#include＜iterator＞//使用reverse_iterator和ostream_iterator包含的头文件

03　#include＜vector＞

04　using namespace std；

05　int main（）

06　{

07　int sz[5]={1，2，3，4，5}；

08　vector＜int＞ob（sz,sz+5）；//创建vector＜int＞对象

09　ostream_iterator＜int,char＞osi（cout，""）；//创建输出流迭代器

10　vector＜int＞：reverse_iterator ri=ob. rbegin（）；//创建反向迭代器

11　copy（ri,ob. rend（），osi）；//输出操作

12　cout＜＜endl；

13　return 0；

14　}

输出结果如下所示。

5 4 3 2 1

【代码解析】代码第11行在不改变copy接口的情况下，使用copy函数将ob中的元素逆序复制到输出流中输出。

2.插入迭代器适配器

STL中提供了3种插入迭代器适配器，即back_insert_iterator、front_insert_iterator和insert_iterator。使用这3种迭代器适配器时必须包括头文件＜iterator＞，这3种迭代器适配器将容器类型作为模板参数，换言之，插入迭代器适配器是依托于容器的，在创建迭代器时有两种格式，如下例所述。

❑back_insert_iterator＜vector＜int＞＞back_it（obV）；

front_insert_iterator＜queue＜int＞＞front_it（obQ）；

insert_iterator＜list＜int＞＞insert_it（obL,p）；

❑back_inserter（ob1）；

front_inserter（ob2）；

inserter（ob3，p）；

其中，第1种格式是根据类模板来创建插入迭代器适配器对象的，第2种格式是调用STL提供的函数并根据容器对象来返回相应的插入迭代器适配器对象。两者的不同之处在于第1种方式必须指明容器类型，之所以要这么做是因为插入迭代器必须要使用合适的容器方法。例如，back_insert_iterator的构造函数认为传递给其的容器对象类型有push_back（）成员方法。

代码13.15　中使用第2种格式创建了匿名的back_insert_iterator对象。下面来看为何要使用插入迭代器对象，首先请读者尝试对代码13.15进行更改，将back_inserter（ob）替换成ob.begin（），编译时没有错误，可运行时程序会崩溃，原因在于copy是个独立的函数，没有重新调增容器大小的权限，而ob在建立时是个空容器，此时无法通过copy向ob中写入任何数据。

3种插入迭代器适配器通过复制转换和自动内存分配确保新信息的容纳，避免了对原有信息的覆盖，back_insert_iterator将元素插入到容器的尾部，front_insert_iterator将元素插入到容器的头部，insert_iterator将元素插入到其构造函数中迭代器参数p指定位置的前面，这3种迭代器属于输出迭代器。

注意

因为vector容器不能满足front_insert_iterator的要求，所以不能基于vector创建front_insert_iterator对象。

技巧通过插入迭代器适配器可以将复制算法转换成插入算法。

13.7.3　函数适配器

在前面介绍的代码13.17中已经使用过函数适配器“bind2nd（lt，3）”，这是绑定适配器的一种，此外，还有否定器和函数指针适配器。

1.绑定器

诸如“bind2nd（lt，3）”之类的绑定器可以对函数对象it进行修改操作，将it的参数绑定在某个特定值上，如代码13.17中便是把it函数对象的第2个参数绑定为3，这使得二元函数对象可转换成一元函数对象，使得在某些场合下不能用的函数变得能用。STL提供了以下两个绑定器。

❑bind1st（函数对象，指定值）

将指定值绑定在函数对象的第1个参数上。

❑bind2nd（函数对象，指定值）

将指定值绑定在函数对象的第2个参数上。

2.否定器

STL中提供了否定器用于逆转函数对象的真伪值，因此要求函数对象是断言，STL提供了以下两个否定器。

❑not1

用以逆转一元断言。

❑not2

用以逆转二元断言。

例如代码13.19所示，它是在代码13.17的基础上修改而来的。

代码13.19　否定器的使用Negator

＜------------------------文件名：example1319.cpp-----------------------------＞

01　#include＜iostream＞

02　#include＜algorithm＞//使用for_each函数要包含的头文件

03　#include＜functional＞//使用less＜＞类模板要用的头文件

04　#include＜vector＞

05　using namespace std；

06　typedef vector＜int＞VI；

07　typedef VI：iterator VIP；

08　int main（）

09　{

10　int sz[]={1，2，3，4，5，6}；

11　VI ob（sz,sz+6）；

12　less＜int＞lt；//创建less＜int＞类函数对象

13　replace_if（ob. begin（），ob.end（），not1（bind2nd（lt，3）），7）；

14　for（VIP v=ob. begin（）；v！=ob.end（）；v++）

15　{

16　cout＜＜（*v）＜＜""；

17　}

18　cout＜＜endl；

19　return 0；

20　}

输出结果如下所示。

1 2 7 7 7 7

【代码解析】对比代码13.19和代码13.17的输出结果可以很容易地体会到否定器的用法，在代码第13行中使用“replace_if”语句，将大于等于3（less＜int＞，即小于3的反）的元素都被替换为7。

3.成员函数适配器

如果某个容器中存储的元素是某个类的对象，该如何调用对象的函数呢？有以下的解决方法，Ex为类名，vector＜Ex＞ob为容器对象，假设void do（）是Ex类中定义的成员函数，如下所示。

for（vector＜Ex＞：size_type i=0；i＜ob.size（）；i++）

ob[i]-＞Do（）；当然，也可以用iterator，如下所示。

for（vector＜Ex＞：iteratorit=ob.begin（）；it！=ob.end（）；it++）

（*it）-＞Do（）；实际上，STL中不推荐自己书写循环结构，因此一种替代性的思路便是使用for_each（）结构，为此必须书写一个函数，如下所示。

void DoInterface（Ex＆ex）

{

ex.do（）；

}此时，只要使用如下代码即可实现每个元素都调用该成员函数。

for_each（ob.begin（），ob.end（），DoInterface）；实际上，借助STL提供的成员函数适配器（mem_fun_ref和mem_fun）即可实现对元素对象内成员函数的直接调用，如下所示。

for_each（ob.begin（），ob.end（），mem_fun_ref（Ex：do（）））；

mem_fun_ref和mem_fun的区别在于当容器中存放的是对象实体的时候用mem_fun_ref，当容器中存放的是对象的指针的时候用mem_fun。

13.8　小结

本章介绍了STL的基础知识，掌握STL可以使你的程序结构更合理，数据存储和管理更科学，因此，学好STL是件很必要的事情，由于篇幅有限，本章只能对STL作简要介绍。力图通过简单的示例让读者掌握STL的精华所在，能够真正理解泛型编程的意义。

模板使得数据结构和类型分离，而STL算法只通过某个数据结构的几个基本的语义属性，屏蔽了数据结构的特定实现，成功的将算法与数据结构分离，在没有损失效率的前提下，得到了极大的发挥空间。

STL可以看做是由不同的组件组成的系统，理解容器、迭代器和适配器的概念十分重要，在此基础上才能真正理解STL算法的本质。

有些算法被表示为容器类方法，但大量STL算法都是通用的、非成员函数的，这时可将迭代器作为容器和算法间的接口来使用，另外，STL算法通过迭代器和指针的概念融合，可用于非STL容器，如常规数组以及后面要介绍的string对象等。

13.9　上机实践习题

1.使用vector容器，进行数据动态增加和删除，并输出中间结果。

【提示】上述题目主要是要求读者熟悉STL中的vector和迭代器的相关知识，重点是掌握迭代器的使用。

【关键代码】

01　void disp（vector＜int＞＆x）//定义disp函数用以输出容器对象所有元素

02　{

03　unsigned int i=0；

04　for（；i＜x. size（）；i++）

05　{

06　cout＜＜x[i]＜＜""；

07　}

08　}

09　//主函数代码

10　vector＜int＞obD（5，0）；//创建一个vector＜int＞容器对象

11　vector＜int＞：iterator pD=obD. end（）；//创建迭代器pD

12　pD=obD. insert（pD，1）；//在尾部插入元素1，并使迭代器指向新插入的1

13　disp（obD）；

14　cout＜＜endl；

15　obD. insert（pD，2，3）；//在新插入的元素1之前插入两个元素3

16　disp（obD）；

17　iter=obL. erase（iter）；//抹掉第2个元素，iter指向第3个元素

18　disp（obL）；

19　cout＜＜endl；

20　obL. erase（iter,obL.end（））；

21　disp（obD）；

22　cout＜＜endl；

2.定义map和multimap容器，并插入数据和输出结果。

【提示】上述题目主要是要求读者熟悉STL中map和multimap容器的相关知识，重点是掌握map和multimap容器的使用。

【关键代码】

01　pair＜int,string＞sz[2]={pair＜int,string＞（1，"A"），pair＜int,string＞（2，"B"）}；

02　pair＜int,string＞t（2，"X"）；

03　map＜int,string＞obM（sz,sz+2）；

04　map＜int,string＞：iterator itM=obM. begin（）；

05　pair＜map＜int,string＞：iterator,bool＞res=obM. insert（t）；

06　if（res. second）//判断是否插入成功

07　cout＜＜"插入成功"＜＜endl；

08　else

09　cout＜＜"已包含关键字与t相同的元素："＜＜（*res.first）.second＜＜endl；

10　multimap＜int,string＞obDM（sz,sz+2）；//基于sz创建multimap对象obDM

11　//创建multimap＜int,string＞类内迭代器

12　multimap＜int,string＞：iterator itDM=obDM. begin（）；

13　itDM=obDM. insert（t）；//执行插入操作

14　cout＜＜"插入的元素为："＜＜（*itDM）.second＜＜endl；

第五篇　输入输出处理和编程规范

第14章　输入输出和文件

输入和输出是人和计算机交互的手段，也是计算机的作用所在。试想，如果一台机器既不接受任何形式的指令，也不给出任何形式的结果，这台机器的存在就没有了意义。几乎每个程序都要用到输入和输出，本书给出的示例代码中，显示结果是依靠cout，输入信息是依靠cin，这两个都是C++标准库中ostream类和istream类中提供的对象，此外，C++标准库还提供了其他类和对象，不仅仅可实现向屏幕输出和从键盘读入，还可以写入文件以及从文件读入，本章重点讨论输入输出和与文件相关的内容。

本章主要涉及以下知识点。

❑输出与输出：介绍输入与输出的相关知识。

❑高层I/O：介绍使用函数的输入与输出。

❑流类库：介绍C++中的流处理类。

❑输出流：介绍输出流中各种设置、填充和控制。

❑输入流：介绍输入流中各种格式、设置、填充和控制。

❑重载＞＞和＜＜：介绍如何对插入和抽取符进行重载。

❑文件操作：介绍C++中的文件操作的方法和类型。

❑字符串流：介绍strstream和sstream两个类族的使用。

14.1　输入输出概述

C++语言不具备内部输入输出能力，需要借助外部I/O包来完成，将输入输出具体要完成的操作留给编译器来做，这样做的目的是为了最大限度地保证语言与平台的无关性。因为输入输出功能都是与操作系统相关的，如果C++为某种操作系统实现内部输入输出功能，那它也就被限制在这个操作系统中了。举例来说，如果C++为Windows内部实现了输入输出命令，那么该命令就只能用在Windows环境下，在其他系统，如Linux中将完全不起作用，这大大限制了程序的可移植性。

首先，先让我们一起来了解几个概念。

14.1.1　什么是文件

使用电脑时，经常会遇到“文件”这个词，通常理解为存放在存储介质上的一组信息，但从程序设计的角度来理解，文件的概念要宽泛得多。

文件的准确定义为存放在外部介质上的以文件名为标识的数据的集合，此处的外部介质不仅仅包括磁盘还包括设备，例如键盘、显示器以及打印机等。凡是能够起到输入输出作用，与CPU直接或间接打交道的一组信息集合都是文件。

每个文件都以文件名为标识，I/O设备的文件名是系统定义的，如下所示。

COM1或AUX—第一串行口，附加设备

COM2—第二串行口，此外还可能有COM3和COM4等

CON—控制台（Console），键盘（输入用）或显示器（输出用）

LPT1或PRN—第一并行口或打印机

LPT2—第二并行口，还可能有LPT3等

NUL—空设备

磁盘文件可以由用户自己命名，但上述被系统（Windows和Dos下均是如此）保留的设备名字不能当做文件名，如不能把一个文件命名为CON（不带扩展名）或CON.TXT（即使加了扩展名也不可以）。

本章所讨论的输入输出操作既包括对磁盘文件的读写操作，又包括对设备的输入输出操作，常常将对设备的输入输出操作称为底层输入输出。

14.1.2　流

关于“流”，可以学院派地解释为“流是（表达）读写数据的一种可移植的方法，其为一般的I/O操作提供了灵活有效的手段。一个流是一个由指针操作的文件或者是一个物理设备，而这个指针正是指向了这个流”。

就C++程序而言，I/O操作可以简单地看做是从程序移进或移出字节，这种搬运的过程便称为流（Stream）。程序只需要关心是否正确地输出了字节数据，以及是否正确地输入了要读取的字节数据，特定I/O设备的细节对程序员是隐藏的。

在C++中所有的信息都是以0、1来进行编码的，所以输出的字节（也就是0、1序列）代表的内容，以及输入的源头或输出的目的都不是流所关心的内容，流的实质在于是否正确完成了运输任务，形象的表述如图14.1所示，左侧是输入流而右侧是输出流。

图　14.1　C++输入输出流

流是一个动态的概念，可以将一个字节形象地比喻成一滴水，字节在设备、文件和程序之间的传输就是流，类似于水在管道中的传输，引用Sergio C.Carbone所著的《Learning C For Real》一书中的一段话来进行说明，如下所述。

流的概念意味着什么呢？

流是独立于设备之外而操纵外设的一种逻辑手段。

大多数外设都是互异的，所以（操纵）它们需要专门的编程技术。

流对程序员隐藏这些不同点，而准许他们以同样的方式来处理大多数外设。

考虑到一连串的字符需要一次读一个，流（相当于）是具有缓冲作用的接口。

个人计算机都是基于流架构的。

从中可以看出，流是对输入输出源的一种抽象，也是对传输信息的一种抽象。通过对输入输出源的抽象，屏蔽了设备之间的差异，使程序员能以一种通用的方式进行存储操作；并且通过对传输信息的抽象，使得所有信息都转化为字节流的形式传输，信息解读的过程与传输过程分离。

14.1.3　缓冲区

读者可能已对硬件缓冲区比较熟悉了，为了协调计算机快速设备和慢速设备间的通信步伐，计算机中大量使用了硬件缓冲区，如CPU中的Cache，内存也可以看成是一种缓冲区，用于CPU高速处理和相对瓶颈化的硬盘存储速度之间的矛盾。

流是传输信息的一种逻辑表示，是具有缓冲作用的接口，同样有缓冲区的需求，但这里的缓冲区是一种逻辑概念，同物理设备中的缓冲区有所不同。通常流的缓冲区是作为中介的内存块，它是从设备传递给程序或者由程度传递给设备的临时存储池（Pool），下面从文件输入和文件输出的角度来具体分析缓冲区的作用。

对于输入操作，程序通常一次只能处理一个或几个字节的信息，如果没有缓冲区，从磁盘文件中读取一个字符都要进行大量的操作，速度很慢，跟不上CPU的节奏，所以C++采取的策略是一次性从磁盘文件中读取大量数据存放在缓冲区（一块内存）中，CPU只要从该内存中提取字节进行操作即可，由于CPU访问内存的速度比访问硬盘的速度快很多，这在一定程度上提高了输入的效率，当缓冲区中的数据读取完毕后，再一次性地读入大量数据，进行重复操作。

输出操作也是同样的道理，如果每输出一个字节都要进行一次磁盘硬件操作，会导致费时费力，效率低下，同样可开辟一块缓冲区内存，当要写入磁盘的字节信息达到一定数量时，将积累的信息一次性写出。

理解缓冲区的另一个重点在于“刷新”（Flush），不论缓冲区是否已满，刷新操作可以将缓冲区中存储的字节信息立即输入（传递给程序）或输出（写入磁盘或输出到屏幕上）。

14.1.4　重定向

标准的输出和输入设备通常指的是显示器和键盘，但一些操作系统支持重定向，这使得标准输入和输出能被替换。例如，一个程序的信息原本是要输出到屏幕上的，可通过操作系统的支持，在不改变程序的前提下，便可让信息输出到其他位置，以DOS系统为例来进行说明。举例如下所示。

#include＜iostream＞

using namespace std；

int main（）

{

cout＜＜"Hello,txt will be shown on screen"；

return 0；

}编译运行，找到生成的exe文件（假设名称为example.exe），将其复制到E盘下，进入DOS系统或MS-DOS，通过以下步骤运行该程序。

E：＞example（注：键盘输入）

Hello,txt will be shown on screen

E：＞example＞ex.txt（注：键盘输入）

E：＞

打开E盘会发现其根目录下多了一个ex.txt文件，其中内容为“Hello,txt will be shown on screen”，并没有对程序进行任何的修改，仅仅依靠操作系统提供的重定向输出符号＞便把本应显示在标准设备上的内容显示到了文件中。

说明

输入同样可以重定向，DOS系统提供了重定向输入符号＜供使用。

14.1.5　3种输入输出机制

C++语言的输入输出机制包含3层，前两种是从传统的C语言继承而来的，分别是底层I/O和高层I/O，第3种是C++中增添的流类库，这是本章讨论的重点。

1.底层I/O

底层I/O依赖于操作系统来实现，调用操作系统的功能对文件进行输入输出处理，具有较高的速度。底层I/O将外部设备和磁盘文件都等同于逻辑文件，采用相同的方法进行处理，一般过程为打开文件、读写文件和关闭文件，这些是通过一组底层I/O函数来完成的，这些函数定义在头文件io.h中。

注意

如果要对串口进行读写操作，可以用底层I/O，也可以用即将介绍的高层I/O。

因为底层I/O涉及很多硬件知识，需要程序员对要操作的设备有一定的了解，因此，本章不对底层I/O进行具体的讨论，感兴趣的读者可查阅相关资料。

2.高层I/O

高层I/O是在底层I/O的基础上扩展而来的，仍旧将外部设备和磁盘文件进行统一的处理，但处理的方式更为灵活，提供的一组处理函数定义在头文件stdio.h中，新的C++标准头文件为＜cstdio＞，提供的这些函数大体可分为两类，即一般文件函数（外部设备和磁盘文件）和标准I/O函数。

一般文件函数的操作过程和底层I/O函数类似，分为打开文件、读写文件和关闭文件3个步骤，但标准I/O函数面向的是标准设备，如stdin（标准输出设备，一般是键盘）和stdout（标准输出设备，一般是显示器），不需要对这些设备进行打开和关闭操作，标准I/O文件函数有以下几类。

❑字符处理函数：“int getchar（void）；”用于从标准输入设备（默认键盘）读取一个字符，并返回所读字符值，以及“int pubchar（int c）；”用于向标准输出设备（默认显示器）输出一个字符c。

❑C风格字符串处理函数：“char*gets（char*s）；”从标准输入设备读取字符，存储在指针s指向的字符数组中，直到读入换行符或文件末尾标记EOF，写入空字符'\0'，并将换行符抛弃，如果写入了字符则返回s，否则返回NULL，以及“int puts（const char*s）；”将以空字符结尾的串s输出到标准设备中，调用成功则返回非负值，否则返回EOF。

❑格式化处理函数：“int scanf（const char*ctl_string，……）”用于格式化输入，以及“int printf（const char*ctl_string，……）”用于格式化输出。

底层I/O和高层I/O不具备扩展机制，不能输出自定义的类型，稍后会有关于高层I/O的详细介绍。

3.流类库

除了从C语言中继承了上述两种I/O机制外，C++还有一种输出机制，即流类库（iostream类库），这是C++所特有的，iostream类库为内置类型对象提供了输入输出支持，也支持文件的输入输出，另外类的设计者可以通过运算符重载机制对iostream库进行扩展，来支持自定义类型的输入输出操作。

14.2　高层I/O

C++中的高层I/O是从C继承而来的，使用头文件＜cstdio＞。本节先从其标准的输入输出讲起，再来学习高层I/O中文件的操作。原则上流类库已经能胜任所有的输入输出处理，但高层I/O的介绍很有必要，这有助于读者阅读源代码。

14.2.1　标准输出函数printf

printf函数用于将字节流复制到标准输出设备，其使用方式十分灵活，有两个突出的优点，一是其参数表的长度是可变的；二是其转换说明和格式控制十分简单。printf函数的参数表分为两部分，即控制字符串和参数表，基本格式如下所示。

int printf（const char*，para1，para2……）；

注意

这里使用的概念是函数而不是对象，注意printf和cout的不同，这也许就是C和C++思维方式的不同。

printf函数用于将字符流复制到标准输出设备，在进行深入讲解前，先看以下示例代码14.1。

代码14.1　printf函数使用范例PrintfSample

＜------------------------文件名：example1401.cpp------------------------------＞

01　#include＜cstdio＞//使用printf要包含的头文件

02　int main（）

03　{

04　printf（"%s is%d years old\n"，"Deco"，24）；'\n'是换行符

05　return 0；

06　}

输出结果如下所示。

Deco is 24 years old

【代码解析】代码第4行演示了printf函数最基本的用法，注意下述代码。

控制字符串：最前面的一个字符串，即"%s is%d years old\n"，其中%s和%d是格式要求

参数表：两个参数"Deco"和24。

细心的读者可能会发现，代码14.1并没有引入名称空间std，这是因为头文件＜cstdio＞中实体的定义并没有采用名称空间的形式，也就是说，其中的函数和实体都定义在全局空间中，所以在包含了＜cstdio＞头文件后，直接使用函数名printf调用即可。

printf函数对参数表中的表达式进行求值，并按照控制字符串中的格式要求进行转换，把结果放在标准输出流中，控制字符串中符号%标记格式说明符的开始，而由转换字符结尾（诸如代码14.1中跟在%后的s或d）标记格式说明符的结束，也就是说，“%s”整体是一个格式说明符，格式说明符将被参数表中对应位置的参数值替换，控制字符串中不是格式说明符的部分将字节放入标准输出流中，如代码14.1中的“is”和“years”以及单词之间的空格等，printf返回当前已成功写入标准输出流的字符数。

printf函数支持的转换字符如表14.1所示，%标记着某个格式转换符的开始，而转换字符标记着其所在格式转换符的结束，它是必不可少的，且在单个格式转换符中只能有一个转换字符。

特别说明，%g或者%G表示系统决定对应浮点数采取常规或科学记数法中最短的方式输出该数据，且不输出无意义的0，如下所示。

printf（"%g"，123.456）；//显示123.456；

printf（"%g"，0.0000123456）；//显示1.23e-008

当浮点数%f（%F）或%e（%E）形式的小数位数小于6，补足0使其达到6位，若大于6位，不予截断，而是保留，再比较两种形式下的数据长度，选择合适的一种，进行6位有效数字的截断，如123.456的%f形式为：123.456000，占10个字符空间，%e形式为：1.234560e-002，占12位字符空间，因此，%g取%f的形式进行输出，且不输出没有意义的0，结果为123.456。0.0000123456的%f形式为：0.0000123456（不截断），占12个字符空间，%e形式为1.234560e-005，同样占据12个字符空间，在这种情况下，优先使用%e的形式。

除了开头和结尾的标记，printf函数还提供了其他一些可选的控制标记，使输出形式更为灵活，充分满足程序的需求，在%与转换字符之间依次可以有下列标记。

1.域宽（Field Width）M

单个字符占用的显示空间称为一格，域宽M是用以指明被转换参数要使用的最少格数，用一个正整数来表示，如果被转换参数的字符数小于M，那么需要对该参数对应的显示区进行填充，使满足域宽要求。如果参数的字符数大于M，那么域宽的设定便失效，按参数的实际字符数显示。

域宽是可选的，默认情况下等同于参数的实际字符数。

如何填充取决于调整方式（左调整还是右调整），是用0还是空格填充，这可以由用户根据其他标记设定，稍后会进行介绍。默认情况下是右调整并以空格填充，如下所示。

printf（"%5d"，10）；//输出"□□□10"，右调整，左边添加空格以满足域宽要求

printf（"%5f"，12.123456789）；//原来的字符数已经超过域宽5，限制失效，但f转换默认为

//6位小数，所以输出为"12.123457"

注意

为了说明更形象直观，书中使用□表示空格符，这是一种人为替换而不是真实输出。

2.精度（Precision）.N

用一个小数点加一个整数N来表示，对e、E和f转换用以指明非整型参数转换后的小数位数目，如果参数的小数位数多于N，对参数进行近似处理，若参数位数小于N，用0进行填充使其小数位数为N，对d、i、o、u、x和X等整型转换来说，N描述了转换参数被显示数字的最小个数。举例如下所述。

printf（"%.3f"，1.23456789）；//小数位数多于精度3，近似处理，输出"1.235"

printf（"%.6f"，1.23）；//小数位数不足精度6，用0填充，输出结果为"1.230000"

printf（"%.6d"，567）；//d型转换，精度用于指定被显示数字的最小个数，至少应有

//6个数字被显示，输出结果为"000567"对s转换来说，精度N描述了要显示的最大字符数，如果参数字符数大于N，字符串会被截断，当字符数小于N时，限制失效，如下所示。

printf（"%.4s"，"Computer"）；//参数字符数大于4，输出结果为"Comp"

printf（"%.10s"，"Hello"）；//参数字符数小于10，输出结果为"Hello"，限制失效

对g和G转换来说，精度M描述了有效数字的最大位数，默认为6。

精度可以和域宽结合使用，如下例所示。

printf（"%7.3f"，1.23456789）；//输出结果为"□□1.235"，注意小数点也占1格

printf（"%6.4d"，567）；//输出结果为"□□0567"

printf（"%10.4s"，"Hello"）；//输出结果为"□□□□□□Hell"

3.可选项h

h是short修饰符，如果将h放在转换字符d、i、o、u、x和X的前面，那么转换描述适用于short int或unsignedt short int参数，如果h后跟转换字符n，那么相应的参数是指向short int或unsigned short int的指针。

4.可选项l

l是long修饰符，如果将l放在转换字符d、i、o、u、x和X的前面，那么转换描述适用于long int或unsignedt long int参数，如果将l放在转换字符e、E、f、g或G前，那么转换描述适应于double型参数，如果l后跟转换字符n，那么相应的参数是指向long int或unsigned long int的指针。

5.可选项L

L也是一个long修饰符，如果将L放在转换字符e、E、f、g或G前，那么转换描述适用于long double参数。

说明

对于单精度数，使用%f格式符输出时，仅前7位是有效数字，小数为6位。对于双精度数，使用%lf格式符输出时，前16位是有效数字，小数为6位。

6.减号-

减号的放置位置有一定要求，必须在%和域宽M之间，用以指定参数是左对齐（左调整），如果没有减号，参数右对齐（右调整），如以下域宽中的例子所示。

printf（"%-5d"，10）；//输出"10□□□"，左调整，右边添加空格以满足域宽要求

7.加号+

加号要放置在%和域宽M（如果已设定了M，否则应放在精度前）之间，但和减号并没有先后顺序要求，加号只能用来修饰d、i、e、E、f、g和G等有符号类型，如果没有指明加号，负数输出以减号开头，非负前没有符号，通过指明加号非负数输出以正号（加号）开头，如下所示。

printf（"%+d"，10）；//输出结果为"+10"

8.空格

空格的位置要求和加号一样，所起作用也相似。如果输出为非负数，则在其前面加一个空格，如果用加号和空格同时指定，空格被忽略，空格同样只能用于修饰d、i、e、E、f、g和G等有符号类型，如下所示。

printf（"%.3f"，1.23）；//输出"□1.230"

9.#

#的位置要求同加号以及空格一样，用于指定其他输出格式，对于o格式，第一个数字必须为零；对于x/X格式，指定在输出的非0值前加0x或0X；对于e/E/f/g/G格式，指定输出总有一个小数点，即便精度设置为0；对于g/G格式，还指定输出值后面无意义的0将被保留，如下所示。

printf（"%o"，127）；//输出177

printf（"%#o"，127）；//输出0177

printf（"%x"，127）；//输出7f

printf（"%#x"，127）；//输出0x7f

printf（"%.0f"，3.1415）；//输出3

printf（"%#.0f"，3.1415）；//输出3.（小数点也被显示）

10.0

0的位置要求和#一样，如果设定了0标记，则当输出长度小于域宽M时，用0进行填充，而不是用空格来填充，如下所示。

printf（"%010d"，256）；//输出0000000256在一个格式中，可以用*代表整数来说明域宽或精度，这需要从参数表中获取值，下面是个简单的示例。

char ch[20]；

printf（"%*.*s\n"，m,n，ch）；

上述代码输出对应域宽为m，精度为n，此时，如果m为负数，将该参数看成是减号后跟着域宽；如果n为负数，没有意义，将其抛弃。

说明

printf函数的标记符系统比较繁杂，需要在使用中仔细体会。

14.2.2　标准输入函数scanf

scanf函数用于从标准输入设备（通常是键盘）读取流数据到内存中，同printf函数的使用方法几乎一致，其参数表也分为两部分，即控制字符串和参数表，基本格式如下所示。

int scanf（const char*，＆para1，＆para2……）；

scanf函数中用于保存读入值的变元必须都是变量指针，即相应变量的地址。scanf函数的参数表长度也是非固定的，而且转换说明符也相对简单，同printf函数中的转换说明符完全一样。因此scanf函数的使用也十分灵活，如以下简单的示例代码14.2所示。

代码14.2　标准输入函数scanf的基本用法ScanfSample

＜------------------------文件名：example1402.cpp------------------------------＞

01　#include＜cstdio＞

02　int main（）

03　{

04　char a,s[20]；//声明一个char型变量a，一个字符数组s

05　double b；//double型变量b

06　int i；//int型变量i

07　scanf（"%c%s%d%Lf"，＆a,s，＆i，＆b）；//依次从标准输入流中读取字节信息放入a、

08　//s、i和b对应的内存空间中

09　printf（"a is%c,s is%s,i is%d,b is%Lf"，a,s，i,b）；//输出处理

10　return 0；

11　}

输出结果如下所示。

w（用户输入w后回车）

hello（用户输入hello后回车）

20　（用户输入20后回车）

3.1415（用户输入3.1415后回车）

a is w,s is hello,i is 20，b is 3.141500

【代码解析】代码第7行的scanf函数按照控制字符串中的转换说明把标准输入流中的字符转换成值存储到参数表达中相应指针指向的内存区域，这个过程常称“扫描”。先对扫描进行形象的说明，输入扫描的过程是一个从左至右逐个匹配的过程，“回车”起到将缓冲区中的数据交付给scanf函数处理和分割输入项的作用，以便更好地完成输入与变量的匹配，关于匹配的内容稍后会有介绍。

注意

代码14.2　中字符数组名s本身即是指针，不能再使用＆s的形式。

控制字符串有以下3部分组成。

❑格式说明符

❑空白符

❑非空白符

1.格式说明符

首先来看一下格式说明符，scanf函数的格式说明符与printf函数大致相同，同样是由%和转换字符组成，scanf的转换字符如表14.2所示。

此外同printf函数一样，scanf函数也支持一些可选的标记，如可以将h放在转换字符d、i、o、u和x前面，表示被转换的值以short int和unsigned short int型存储，可以将l放在转换字符d、i、o、u、x和X的前面，表示被转换的值以long int或unsignedt long int型存储，如果将l放在转换字符e、E、f、g或G前，表示被转换的值以double型参数存储，如果将l放在n前，那么相应的参数是指向long int或unsigned long int的指针。如果将L放在转换字符e、E、f、g或G前，表示转换后的值以long double型存储。

%与转换字符间的星号（*）表示读指定类型的数据但不保存。因此有如下代码所示。

scanf（"%d%*c%d"，＆x，＆y）；

输入“10/20”，10存入变量x，20存入变量y，字符'/'虽被读取，但并不保存。

此外，还可指定扫描宽度，即扫描的字符数，默认是输入流的长度，例如，%s跳过空白字符，然后读入非空白字符，直到遇见空白字符或文件结束标记，与此对照，%8s跳过前导空白符，读入非空白字符，直到读入了5个字符、遇到空白字符或文件结束标记位置。可以用%nc读入n个字符，其中包括空白字符。

2.空白符

在控制字符串中但不在转换说明中的空白字符称为空白符，其可以和输入流中的空白字符匹配，也可以不匹配，换言之，控制字符串中的一个空白符可以和标准输入流中的任意个连续空白（包括0个）匹配。

3.普通字符

控制字符串中除了空白符和转换说明符以外的字符称为普通字符，普通字符必须与输入流中的字符相匹配，否则当前扫描结束，scanf函数退出，如下例所示。

int num；

scanf（"H%d"，＆num）；

通过键盘输入时，必须先输入字符H以完成匹配，才能进一步输入一个整数值，存储到num中，如果一开始输入的不是字符H，匹配失败，scanf函数退出，并不会将数据存储到num中，反而会在当前缓冲区中留下垃圾。

除了%c外，诸如%d和%f等读数操作和%s字符串读入都会自动跳过前导空格，如下所示。

scanf（"%d"，＆num）；//输入"□□□□2"，则num=2，前面的空格被自动跳过

scanf（"%s"，str）；//输入"□hello China"，则str为"Hello"，遇到空格停止

scanf（"%c"，＆chr）；//输入"□x"，则chr内容为空白，不会自动跳过前导空白

要想使%c同样可以跳过前导空白，可以使用空白符的形式，如下所示。

scanf（"%c"，＆chr）；//输入"□□□□x"，chr中的内容为'x'调用scanf函数时，可能会因为不匹配发生输入失败，如果输入流中没有字符，返回值为EOF（根据不同的系统取不同的值，一般是-1），若发生匹配失败，非法的字符被留在输入流（缓冲区）中，返回已成功转换和存储的字符数，如果没有进行任何成功的转换，返回值就是0，如果全体匹配成功，即scanf成功，返回成功转换的字符数，如下所示。

scanf（"%d"，＆num）；

如果输入“w”，无法将其转换为十进制整数，匹配失败，返回已成功转换的字符数0，但需要注意的是此时w仍滞留在输出流（缓冲区）中，如果不加妥善处理，会给后续程序带来问题，这称为“缓冲区垃圾”，如示例代码14.3所示。

代码14.3　缓冲区垃圾BufferSample

＜----------------------文件名：example1403.cpp--------------------------------＞

01　#include＜cstdio＞

02　int main（）

03　{

04　int num；

05　char chr；

06　scanf（"%d"，＆num）；//读取十进制整数到num中

07　fflush（stdin）；//清空输入缓冲区

08　scanf（"%c"，＆chr）；//读取一个字符到chr中

09　printf（"chr is%c\n"，chr）；

10　return 0；

11　}

输出结果如下所示。

w（用户输入）

chr is w

【代码解析】代码第6行的本意是先输入int型变量num的值，再输入char型变量chr，可输入num值时出现了错误，系统无法将字符w解释为int型数据，匹配失败，但已经输入的字符w并没有从缓冲区中清除出去，这样代码第8行检查到缓冲区中有字符w，便自动完成了对chr的赋值，而不是由用户完成输入的，这就是常见的缓冲区垃圾现象，稍不留意便会给程序带来意想不到的后果。

常规的解决思路是清除输入缓冲区函数“fflush（stdin）”，将这行语句添加到代码第7行，问题得到了解决，该函数能将缓冲区中当前内容清空，另一个方法是不断读出数据直到缓冲区变空，while（（c=getchar（））！='\n'＆＆c！=EOF），getchar函数的原形如下所示。

int getchar（void）；

用于从当前标准输入流中读取并返回一个字符。

注意

在输入流中，数据项必须由空格、制表符和换行符分割。逗号和分号等不是分隔符，如以下代码所示。

scanf（"%d%d"，＆r，＆c）；

程序接受输入“10 20”或“10回车20”，但遇到“10，20”则运行失败。

14.2.3　扫描集

对scanf函数而言，扫描集（Scanset）是个十分重要的概念。扫描集定义一个字符集合，可由scanf函数读入其中允许的字符并赋给对应字符数组。扫描集合由一对方括号中的一串字符定义，左方括号前必须加百分号。例如，以下的扫描集使scanf函数读入字符A、B和C组成字符串。

%[ABC]

使用扫描集时，scanf函数连续将当前输入流中属于扫描集合中的字符放入对应的字符数组，直到发现不在扫描集中的字符为止。返回时数组中放置以'\0'结尾并由读入字符组成的字符串。

对于许多实现来说，用连字符可以说明一个范围。例如，以下扫描集使scanf函数接受字母A～Z。

%[A-Z]

注意

扫描集是区分大小写的。因此，希望扫描大、小写字符时，应该分别说明大、小写字母。

如果扫描集中第一个字符是^，那么输入的字符串由除了扫描集中的元素组成，scanf函数连续将当前输入流中不属于扫描集合中的字符放入对应的字符数组，直到发现扫描集中的字符为止。比如%[^EOF]就是直到有EOF输入，字符串才会被终止。

一定要记住scanf输入是基于缓冲区的，如下扫描集所示。

%[^X]，

在输入回车之前，输入多少个X都是不可能结束的。

14.2.4　sprintf函数和sscanf函数

函数sprintf和sscanf分别是函数printf和scanf的变形版本（在原函数的基础上增加了s前缀而得名），它们的原形分别如下所示。

int sprintf（char*s，控制字符串，参数表）；

int sscanf（const char*s，控制字符串，参数表）；

参数表中参数的个数同样是任意的，与printf和scanf函数相比，变形版本不再和标准输入输出流相关联，而是从C风格字符串（字符数组）中读取值存储到变量中（sscanf），向C风格字符串（字符数组）写入字符（sprintf），如示例代码14.4所示。

代码14.4　sprintf函数和sscanf函数基本用法SerialFunc

＜------------------------文件名：example1404.cpp------------------------------＞

01　#include＜cstdio＞

02　int main（）

03　{

04　char inputS[]="10 3. 14 hello"；

05　char outputS[50]，x[10]；

06　int a；

07　double b；

08

09　sscanf（inputS，"%d%Lf%s"，＆a，＆b,x）；//从字符串inputS中读取信息为a、b和x赋值

10　printf（"a is%d,b is%Lf,x is%s\n"，a,b，x）；//调用printf函数看赋值是否成功

11　sprintf（outputS，"x is%s,b is%Lf,a is%d"，x,b，a）；//将控制字符串转换后写入outputS

12　printf（"%s\n"，outputS）；//调用printf函数看字符串是否写入成功

13　return 0；

14　}

输出结果如下所示。

a is 10，b is 3.140000，x is hello

x is hello,b is 3.140000，a is 10

【代码解析】代码中没有用到键盘输入为a、b和x赋值，而是通过代码第9行的sscanf函数读取C风格字符串inputS中的信息为a、b和x赋值，这相当于将键盘替换成了C风格字符串，同理，sprintf函数相当于将显示器替换成了C风格字符串，这在某种程度上是重定向。

注意

为了支持wchar_t类型，＜cstdio＞头文件中提供了wsprintf函数和wsscanf函数版本。

14.2.5　fprintf函数和fscanf函数

f前缀代表file，这里的输入输出设备不再是键盘和显示器，也不是函数sscanf和sprintf中的C风格字符串，而是文件。

首先来看一下高层I/O中的文件机制，头文件＜cstdio＞中包含了文件结构FILE的定义，该结构用以描述文件当前的状态，但从编程的层面看，不必了解其中的实现细节，会应用即可。前面提及任何设备包括输入输出设备都可以看成是文件，高层I/O也是如此，在＜cstdio＞中定义了3个文件指针，即stdin、stdout和stderr，分别代表标准输入（与键盘相关）、标准输出（与显示器相关）和标准错误输出（与显示器相关），这和＜iostream＞流类库头文件中预定义的cout、cin和cerr有些类似。

fprintf和fscanf的原型如下所示。

int fprintf（FILE*ofp，控制字符串，参数表）；

int fscanf（FILE*ifp，控制字符串，参数表）；

参数表中参数的个数同样是任意的，fprintf函数用于将转换后的控制字符串写入到ofp指向的文件中，fscanf函数用于从ifp指向的文件中读取字节信息为参数表中的参数赋值。

实际上，函数printf和scanf分别等价于下述代码。

fprintf（stdout，控制字符串，参数表）

fscanf（stdin，控制字符串，参数表）

14.2.6　文件访问机制

前面已经提及，文件访问分为打开文件、读写文件和关闭文件3个步骤，I/O库中提供的文件访问函数很多，本书不可能逐个详细介绍，以下将常用函数简要列出。

（1）fclose函数：关闭一个流的函数。

原形：int fclose（FILE*fp）；。

使用方式：fclose（文件指针名）；，其中文件指针名=fopen（）；。

功能：关闭指定的流fp，断开与其所有连接，并清除所有与之相联的缓冲区，释放系统分配的缓冲区，但由setbuf设置的缓冲区不能自动释放。

返回值：0（成功）；，EOF（失败）。

（2）fcloseall函数：关闭打开的流的函数。

原形：int fcloseall（void）；。

功能：关闭所有打开的流，由stdin、stdout、stdprn、stderr和stdaux设置的流除外。

返回值：关闭流的总数。如果发现错误则返回EOF。

（3）fgets函数：从流中读取若干字符到C风格字符串中的函数。

原形：char*fgets（char s[]，int n,FILE*stream）；。

使用格式：fgets（字符串指针，字符个数，文件指针）。

功能：从输入流stream中读入字符存到s串中。当读了n-1个字符或遇到换行符时，函数停止读过程，读入的最后一个字符后面加一个空字符。

返回值：成功时返回字符串参数s，出错或遇到文件结束时，返回NULL。

（4）fopen函数：打开一个流函数。

原形：FILE*fopen（const char*filename,const char*mode）；。

功能：打开用filename指定的文件，并使其与一个流相联，可用mode如下表14.3所示。

注意

“a”和“a+”在对待文件尾标记EOF时有细微不同，a模式不会将原文件的EOF移除，而a+模式会移除原文件中的EOF。

还可以使用b（代表binary，二进制）或t（代表text，文本）指定文件的打开方式是文本方式（ASCII）还是二进制形式，b和t一定要放在r、w和a之后，+号之前，否则，如果将b和t作为前缀，fopen函数同样会运行失败，返回NULL。

返回值：指明流的指针（成功时）或NULL（失败时）。

注意

需先定义FILE*文件指针名，“文件名”若用argv[1]代替，则可使用命令行形式指定文件名。

（5）fprintf函数：传送输出到一个流中的函数。

原形：int fprintf（FILE*stream,const char*format[，argument，……]）；。

功能：向文件指针指向的文件输出ASCII代码，以及向显示器输出错误信息。

使用形式：fprintf（文件指针，控制字符串，参数表）；和fprintf（stderr，"错误信息"）；。

返回值：成功则返回输出的字节数，失败则返回EOF。

（6）fscanf函数：格式化输入函数。

原形：int fscanf（FILE*stream,const char*format[，address，……]）；。

功能：从一个流中扫描输入字段，一次扫描一个字符，每个输入字段根据format所指格式指示符格式化后，把输入字段存在format后面由地址参数给出的位置上。

（7）fseek函数：移动文件指针函数。

原形：int fseek（FILE*stream,long offset,int whence）；。

功能：强制一个文件的位置指针指向某个特定的位置（甚至超出文件的尾部）。定义FILE*文件指针名；起点whence取值有以下3种情况。

❑0或SEEK_SET（表示文件开头）

❑1或SEEK_CUR（表示当前位置）

❑2或SEEK_END（表示文件尾端）

返回值：0表示成功，非0值表示失败。

（8）fputs函数：向文件输出C风格字符串的函数。

原形：int fputs（const char*s,FILE*fp）；。

功能：把以空字符结尾的串s复制到fp相关的文件中，但空字符除外（注意与puts函数的区别）。

返回值：成功调用返回非负值，否则返回EOF。

（9）getc函数：从流中取一个字符的函数。

原形：int getc（FILE*stream）；。

功能：返回输入流stream中一个字符，移动文件指针使之指向下一个字符。

返回值：正常情况下能够读取到代码值，读到文件尾或出错时返回EOF。

注意

需定义char字符变量和FILE*文件指针。

（10）getchar函数：从stdin流中读取一个字符的函数。

原形：int getchar（void）；。

功能：将键盘上输入的单个字符的值（ASC　Ⅱ码）回车后赋给字符型变量。

注意

getchar函数相当于getc（stdin）。

（11）gets函数：读取到一个C风格字符串的函数。

原形：char*gets（char*s）；。

功能：从标准输入流stdin中读取一C风格字符串的函数。

返回值：如果写入了字符，返回s，否则返回NULL。

（12）fputc函数：输出一个字符到文件的函数。

原形：int fputc（int c,FILE*fp）；。

功能：将参数c转换成unsigned char，写入fp相关的文件中。

返回值：调用成功，返回（int）（unsigned char）c，否则返回EOF。

（13）putchar函数：输出一个字符到stdout（标准输出）的函数。

原形：int putchar（int c）；。

功能：在stdout上输出字符c。

返回值：成功返回字符c，失败返回EOF。

注意

putchar（c）等价于fput（c,stdout）。

（14）puts函数：输出一C风格字符串到stdout（标准输出）的函数。

原形：int puts（const char*s）；。

功能：把以空字符结尾的串s赋值到标准输出流中，但不复制空字符，而是写入一个换行符。

返回值：如果调用成功，函数返回一个非负值，否则返回EOF。

（15）remove函数：删除一个文件的函数。

原形：int remove（const char*filename）；。

功能：删除一个文件。

返回值：0（成功）；-1（失败）。

简要地对一些基本函数进行了介绍后，来看以下示例代码14.5所示。

代码14.5　高层I/O文件操作FileIO

＜------------------------文件名：example1405.cpp------------------------------＞

01　#include＜cstdio＞

02　#include＜cstdlib＞

03　int main（）

04　{

05　FILE*fp1=NULL，*fp2=NULL；//创建指向FILE结构的指针

06　char sz[100]；

07　printf（"请输入一段字符：\n"）；

08　scanf（"%s"，sz）；//等价于sscanf（sz，"%z"）；

09　fp1=fopen（"out. txt"，"w+"）；//生成out.txt用于读写

10　if（fp1==NULL）

11　exit（1）；//如果流创建失败，退出

12　fprintf（fp1，"%s"，sz）；//将sz的内容写入fp1指向的文件中

13　fseek（fp1，0，SEEK_SET）；//移动到fp1指向的文件头部

14　fp2=fopen（"trans. txt"，"wt"）；//生成trans.txt，用于写

15　if（fp2==NULL）

16　exit（1）；

17　int c=0；

18　while（（c=getc（fp1））！=EOF）//从文件开始到结束EOF

19　{

20　fputc（toupper（c），fp2）；//转换成大写形式写出

21　fputc（''，fp2）；//写出一个空格

22　}

23　fclose（fp2）；//关闭流

24　fclose（fp1）；

25　return 0；

26　}

输出结果如下所示。

请输入一段字符：

Hello,China（用户输入）此时，在文件夹下多了两个txt文件，out.txt和trans.txt，其中的内容分别如下所示。

out.txt：Hello,China

trans.txt：H E L L O,C H I N A

【代码解析】代码第5行首先声明了两个指向FILE结构的指针fp1和fp2，利用fopen函数使fp1和fp2分别指向两个文件流，首先提示用户输入一个字符串，存储在char型数组sz中，而后将sz存储到fp1指向的文件out.txt中，最后将out.txt中的字符逐个大写后复制到fp2指向的文件trans.txt中。

需要说明第一toupper（char）函数，定义在头文件＜cstdlib＞中，是用于返回一个字符的大写形式，第二exit函数，在第2章中已经进行过介绍在此不再赘述。

14.3　流类库

前面对高层I/O进行了简单的介绍，实际C++引入的流类库比高层I/O更为安全高效，流类库对输入、输出和文件等的操作进行了合理的封装，使得程序的逻辑结构更为清晰。因为绝大多数的流类库是模板形式，可示例化为char版本和wchar_t版本，如无特别说明，本节讨论的是char版本的情况，wchar_t版本与char版本完全一致。

14.3.1　流类库更安全、更高效

理解这个标题的关键在于“更”字，不可否认，从C标准库继承来的高层I/O也十分安全和高效，但为什么说流类库比高层I/O更安全高效呢？这要从流类库和高层I/O的机制区别说起。

高层I/O库函数主要用来处理基本数据类型（字符、整型和浮点数等），它使用参数表进行数据传输，并且使用控制字符串指定数据类型和输入输出格式。它在运行时对格式字符串进行语法分析，并据此对变量进行解释。

虽然printf函数和scanf族函数（包括串行版本和文件版本）已经被很好地进行过优化，在运行期间进行解释却严重依赖于用户的输入，稍不留意便可能出现问题。如果能在编译期间分析格式字符串里的变量，根据不同的类型调用各自的函数来进行处理，会加快运行速度，而且编译期间的类型检查有助于发现错误。

另外对高层I/O来说，不能通过重载对printf函数和scanf族函数进行扩展，用于输出自定义的结构和类型，因为重载函数要有不同类型的参数，而printf函数和scanf族函数的参数表本身都是可见的，因此高层I/O无法满足自定义结构变量或类对象的输入输出要求。

流类库是通过类的继承，类成员函数的重载来实现的。继承性和多态性使得流类库可以用统一的函数接口操作标准I/O、文件及存储块等输入输出设备。不仅通过函数重载，为每种内部数据类型定义了流输入输出函数，还使得用户可以用相同的格式对各种数据类型进行操作，编译程序根据数据的类型自动选择相应的输入输出函数，具备了很好的扩展性，用户通过重载还可以对自定义的对象进行流的操作。因此，与高层I/O相比，流类库更简单、更安全、更有效。

14.3.2　流类库层次

iostream库主要包括如表14.4所示的几个头文件，在前面所有示例代码中使用的头文件＜iostream＞便是其中之一。

前面提及的输入输出操作分别是由输入流（istream）和输出流（ostream）这两个类提供的，ios类是istream和ostream类的公共基类，为了允许双向的输入/输出，由istream和ostream派生出了iostream类。

新旧C++标准中，iostream类库的组织方式稍有不同，不仅体现在头文件是否有后缀上，事实上C++的I/O库被完全改写以适应国际化的需求，旧的C++标准只支持8位char型字符，为了使C++支持各种语言，新的标准扩展了wchar_t字符集。

旧标准下的I/O库中，诸如iostream等类是确切的定义，其类层次如图14.2所示，其中ios类和streambuf类是类库中的两个基类，其他类均是在此基础上派生而来的，两个类之间没有继承关系，它们是组合使用的，ios类中维护着一个指向streambuf类对象的指针。事实上，ios类仅提供编程界面和格式特征，而streambuf则是做实质性的工作。

图　14.2　旧标准中I/O库类层次结构略图

iostream类库中，streambuf、ios、istream、ostream、iostream、istream_withassign和ostream_withassign这些基本I/O流类和预定义的cin、cout、cerr和clog在iostream.h文件（及iostream.h中包含的头文件）中进行说明。filebuf、ifstream、ofstream和fstream在fstream.h中进行说明。strstream、istrstream、ostrstream和strstreambuf在strstream.h中说明。

注意

fstream. h和strstrea.h中都包含了iostream.h，所以如果使用标准输入输出（控制台I/O），只要包含iostream.h头文件即可，如果使用fstream或者strstream，只要包含相应的fstream.h和strstrea.h即可。

每个类的具体作用如表14.5所示。

新标准中根据wchar_t对I/O库进行了修改，使其模板化，其中包括basic_ios＜class T,traits＜T＞＞、basic_istream＜class T,traits＜T＞＞等（basic修饰），而原来的ios、istream等只是其在T为char时的实例，如下所示。

typedef basic_ios＜char,char_traits＜char＞＞ios

typedef basic_istream＜char,char_traits＜char＞＞istream

其中，traits＜T＞是一个模板类，为字符类型定义了具体特性，例如，如何比较字符是否相等以及字符的EOF值等，这涉及一些深层次的内容，不是本书讨论的范畴。

当T为wchar_t时，模板类实例化为wios和wistream形式（w版本），如下所示。

typedef basic_ios＜wchar_t,char_traits＜wchar_t＞＞wios

typedef basic_istream＜wchar_t,char_traits＜wchar_t＞＞wistream

抛开实例化来看新标准下的类层次结构，如图14.3所示，其中，斜体加粗、背景为灰色的类为非模板类，其余均为模板类，在诸如basic_iostream类附近的斜体“iostream”是指模板类“basic_iostream”中当T为char时的实例，意思是说，strstream是从iostream派生而来，只对应着char型，没有wchar_t下的strstream版本。

图　14.3　新标准下的I/O类层次图

I/O类库中的一些独立于类型的信息被移植到新类ios_base（非模板类）中，包括一些原来在ios中的格式常量，而且，用以初始化的Init类也嵌套在了ios_base中进行定义，访问格式为“ios_base：Init”。

仍旧可以把basic_ios类和basic_streambuf类看做是类库中的两个基类，其他类均是在此基础上派生而来的，两个类之间没有继承关系，它们是组合使用的，basic_ios类（而不是ios_base类）中维护着一个指向basic_streambuf类对象的指针。

iostream类库中，streambuf、ios、istream、ostream、iostream这些基本I/O流类的char型特化实现（及其模板类即basic_XXX形式）和预定义的cin、cout、cerr和clog在iostream文件（及iostream中包含的其他头文件）中说明。filebuf、ifstream、ofstream和fstream（及其模板类即basic_XXX形式）在fstream中说明。非模板类strstream、istrstream、ostrstream和strstreambuf在strstream中说明，用以执行C风格的串，也就是字符数组的操作。stringstream、istringstream、ostringstream和stringbuf这些char型特化版本（及其模板类即basic_XXX形式）在头文件＜sstream＞中实现，用以执行std：string型的操作，关于string的介绍请参考稍后的内容。

上述如ios等指的是char型实例化后的类，对应的wchar_t版本的类只要在前面加上w即可，如wios和wiostream，如无特别说明，本章所讨论的是char型实例化的情况。

注意

＜strstream＞无法对wchar_t提供有效的支持，其中的内容已经被新的C++标准明确标明为“不要再使用”，推荐使用string版本的＜sstream＞来替代。

流可以看做是输入输出类库的对象，＜iostream＞文件中便预定义了如下4个用于char型操作的流对象。

❑cin：标准输入，istream类对象，从标准设备（键盘）中读入数据。

❑cout：标准输出，ostream类对象，向标准设备（显示器）输出或者写数据。

❑cerr：标准错误输出，ostream类对象，默认情况下，cerr不能重定向，即只能允许向屏幕设备写数据。这个流是没有缓冲区的，意味着信息会自动发送到设备，不会等到缓冲区填满或遇到换行符号。

❑clog：标准输出错误，从字面上看有记录的意思，在默认情况下同样不能重定向，只能输出到屏幕上，但这个流有缓冲区。

新的＜iostream＞中还定义了流对象wcin、wcout、wcerr和wclog，用以处理wchar_t型信息。

前面示例中经常使用的cout和cin是对象不是函数，也许这让你有些难以理解。实际上操作符＜＜和＞＞就是以友元方式重载在cout和cin类中，以实现内置类型的输入和输出的。举例来说，“cout＜＜x”相当于执行了“operator＜＜（cout,x）”。通过对操作符＜＜和＞＞重载，就能对自定义的类型进行输入或输出，这就是C++类库的可扩展性。

14.4　输出流

C++将输入输出都抽象为字节流，输出流ostream类的任务便是将数值类型转换为以字符字节为单位的输出，这些是通过ostream类的公共方法（接口）来实现的，本节以标准输出流对象cout为例来进行讨论。

14.4.1　操作符＜＜

在流类库中，＜＜称为插入操作符（Insertion Operator），ostream类中已预先以成员函数的形式重载了C++基本类型（如char、int、long和double等）的＜＜操作符处理函数，其基本类型如下所示。

ostream＆operator＜＜（基本类型）；在前面很多的示例中都使用了＜＜进行输出，如下例所示。

cout＜＜5；上述输出整数5的操作实际上等价以下示例代码。

cout.operator＜＜（5）；//对应的原型为cout.operator＜＜（int）＜＜操作符的返回类型都为ostream＆，这是为了实现链式输出，如下所示。

cout＜＜5＜＜"，"＜＜6.2；＜＜的结合顺序为从左到右，上述代码等价于下述代码。

（（cout.operatoer＜＜（5））.operator＜＜（"."））.operator＜＜（6.2）；

即对cout对象调用成员函数operator＜＜（int），返回ostream类对象的引用，查询ostream类的定义可知，“return this”语句说明返回对象仍旧为cout，再调用operator＜＜（char*）用于"，"的输出，依旧返回cout，再次调用operator＜＜（double）输出6.2，这称为链式输出，有的教材中也称为“拼接输出”。通过返回ostream类对象的引用，使得不同的输出类型可以直观地拼接起来，按从左到右的顺序复制到输出流中。

特别强调下字符串的输出，用指向字符串的指针来表示字符串，关于字符串的输出在第3章中已经进行了介绍，在此不再赘述，看以下示例代码14.6所示。

代码14.6　C风格字符串输出C-StringOutput

＜-----------------------文件名：example1406.cpp-------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　char*pC="Hello"；//指向只读字符串的指针pC

06　char sz[]="China"；//字符数组sz

07　cout＜＜pC＜＜endl；//输出char型指针指向的字符串

08　cout＜＜＆pC＜＜endl；//输出指针在内存中的地址

09　cout＜＜（void*）pC＜＜endl；//输出指针指向的字符串在内存中的地址

10　cout＜＜sz＜＜endl；//输出字符数组

11　cout＜＜＆sz＜＜endl；//输出字符数组名在内存的地址

12　cout＜＜（void*）sz＜＜endl；//输出字符数组在内存中的首地址

13　cout＜＜"China"＜＜endl；//输出只读字符串

14　cout＜＜（void*）"China"＜＜endl；//输出只读字符串在内存中的地址

15　return 0；

16　}

输出结果如下所示。

Hello

0012FF7C//栈区

0046E024//只读数据区

China

0012FF74//栈区

0012FF74//栈区

China

0046E01C//只读数据区

【代码解析】代码的输出结果直观体现了3种用法的不同，对代码第5行的“char*pC="Hello"；”而言，字符串"Hello"将被存储在只读数据区，pC只是个char型指针，指向这块不能被修改的内存，对pC的输出操作“cout＜＜pC＜＜end1；”实际上是输出pC指针指向的字符串，而“cout＜＜＆pC＜＜end1”用以输出pC指针这个变量在内存中的地址，“cout＜＜（void*）pC＜＜end1；”用以输出pC指向的字符串在内存中的首地址，这两个地址是不同的。

对代码第6行的字符数组来说，情况略有不同，“char sz[]="China"；”，编译时，字符串"China"仍将被放置到只读数据区，但程序执行时会在栈区也开辟了和"China"相同大小的内存空间，用sz来标记，并用存储在只读数据区的字符串"China"为其初始化，此处的数组名sz可以看成是指向在栈区开辟的这块内存的指针，因此，“cout＜＜sz＜＜end1；”实际上是对sz指向的字符串（位于栈区的字符串）进行输出，“cout＜＜＆sz＜＜endl；”和“cout＜＜（void*）sz＜＜endl；”等价，都是输出栈区字符串在内存中的首地址。

对于代码第13行的“cout＜＜"China"＜＜endl；”这样的操作，实际上是直接对存储在只读数据区的字符串进行输出，而“cout＜＜（void*）"China"＜＜end1；”则是输出只读字符串"China"在内存中的首地址，此外，程序中出现的3个“China”是当成不同的实体存储在只读数据区的。

14.4.2　其他ostream方法

除了＜＜操作符外，ostream类中还定义了成员函数put和write，分别用于显示字符和字符串，其函数原形分别如下所示。

ostream＆put（char）；

ostream＆write（const char*，int n）；

put函数的参数指明了要显示的字符，write函数的第1个参数提供了要显示的字符串的地址，第2个参数指明了要显示多少个字符。

因为put函数和write函数都返回ostream类对象的引用，因此，put函数、write函数和＜＜操作符可组成链式输出，如示例代码14.7所示。

代码14.7　ostream类中的put函数和write函数OstreamSample

＜-------------------------文件名：example1407.cpp-----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　cout. put（'x'）；//输出单个字符

06　cout. put（'\n'）；//输出换行符

07　cout. put（'h'）.put（'e'）.put（'\n'）；//链式输出

08　cout. put（61）.put（'\n'）；//int型参数也被解释为char

09　cout. write（"Hello"，3）；//输出字符串，截断处理

10　cout. put（'\n'）；

11　cout. write（"Nice to"，7）.write（"meet you"，9）；//字符串也可以链式输出

12　cout. put（'\n'）；

13　cout. write（"China"，8）.write（"Beijing"，3）；

14　//当第2个参数大于字符串长度时，不会因为空字符而停止

15　cout. put（'\n'）；

16　cout. write（"Excellent"，3）.put（'z'）＜＜endl；//链式

17　return 0；

18　}

输出结果如下所示。

x

he

=

Hel

Nice to meet you

China Bei

Excz

【代码解析】代码第5～16行演示了如果通过标准输出流对象cout调用put函数和write函数的过程，对put函数来说，既支持字符类型，也支持可转换的int型等数值参数，如代码第8行的“cout.put（61）”输出的便是字符"="，对write函数来说，当第2个参数小于字符串的长度时，将对字符串进行截断输出，但若第2个参数大于字符串的长度，write函数不会因为遇到字符串结束的空字符就停止输出，而是继续从下一个内存单元中读取字符输出，直到达到第2个参数规定的数目，所以，代码第14行的“cout.write（"China"，8）”的输出结果并不确定，取决于编译器对内存的安排，本例中输出了“China”（3个空格）的形式。从代码14.7中还可以看出，put、write和＜＜可以组合成链式输出。

14.4.3　格式状态字

在ios_base类中定义了用于控制输入/输出的格式状态字，以满足不同的I/O需求。由图14.3可知，ostream类是从ios类派生而来的，而ios类是从ios_base类派生而来，在ios_base类中定义了流的格式信息，以及设置和读取这些信息的方法。

格式状态字是一个32位的long型整数，其每一位都代表了特定的含义。理解格式化常量能更好地理解格式状态字，在ios_base类中，维护了下述枚举结构。

enum_Fmtflags

{

skipws=0x0001，//in，跳过输入中的空白

unitbuf=0x0002，//out，插入操作后＜＜立即刷新缓冲区

uppercase=0x0004，//out，十六进制中的A-F以及X大写

showbase=0x0008，//out，输出时使用前缀，八进制为0，十六进制为0x

showpoint=0x0010，//out，强迫显示浮点数末尾的小数点和0

showpos=0x0020，//out，正数前加+号

left=0x0040，//out，输出数据在输出域中左对齐

right=0x0080，//out，输出数据在输出域中右对齐

internal=0x0100，//out，两端对齐，在符号位或基前缀（0或0x）后填充字符

dec=0x0200，//input＆output，转换为十进制

oct=0x0400，//input＆output，转换为八进制

hex=0x0800，//input＆output，转换为十六进制

scientific=0x1000，//out，用科学记数法显示浮点数

fixed=0x2000，//out，使用定点形式显示浮点数

boolalpha=0x4000，//out，输入和输出bool值时，显示true或false

adjustfield=0x01c0，//0000 0001 1100 0000

basefield=0x0e00，//0000 1110 0000 0000

floatfield=0x3000，//0011 0000 0000 0000

_Fmtmask=0x7fff，//0111 1111 1111 1111

_Fmtzero=0//0000 0000 0000 0000

}；

上述枚举常量便是格式化常量，每个格式化常量都是一个32位的整型数，而且前15项（从skipws到boolalpha）按enum中的定义顺序分别对应着32位（bit）格式状态字的不同位，前15个格式化常量可以看成是“格式状态字只有某位为1时的取值”。

格式状态字和格式化常量的关系如图14.4所示，从skipws到boolalpha分别对应着格式状态字的某位，该位为1，允许该格式，否则，禁止该格式。换言之，格式状态字是32个状态位的组合。

可以通过位与、位或或位反操作来实现不同格式化常量的组合，如下所示。

图　14.4　格式控制字位对应形象图

skipws|unibuf；0x0003，即0000 0000 0000 0011，后两位都有效

_Fmtmask=0x7fff，//0111 1111 1111 1111

_Fmtflags结构中定义的上述结构便可以看成是前15个枚举量位或的结果，同理，adjustfield可以看做“left|right|internal”，basefield可以看成“oct|base|hex”，floatfield可以看成“fixed|scientific”，关于这些格式化常量的具体应用稍后会有详细的介绍。

诸如left及fixed等格式化常量是在ios_base类里定义的，外部访问时必须用作用域分辨符来存取其值，即直接使用left和fixed是不行的，必须指定为ios_base：left和ios_base：fixed。

说明

从_Fmtflags结构的定义可以看出，大部分的格式化常量都是针对输出操作的，针对输入操作的仅有skipws、dec、oct和hex 4个。因此，对格式化状态字的介绍也主要是围绕输出而来的。

旧的C++标准中是没有ios_base类的，上述格式化常量和后面要介绍的一些设置函数是定义在ios类的，新的C++标准将流类库模板化，ios类也是对basic_ios进行实例化后生成的模板类，因此，诸如格式化常量和设置函数等一些独立类型的信息被转移到一个非模板新类ios_base中，以方便对输入输出流进行控制。本节中所有的示例基于新的C++标准，读者如果使用旧的标准（比如使用诸如“#include＜iostream.h＞”之类的头文件），则可能需要把诸如“ios_base：left”的格式化常量访问形式改写成“ios：left”。

说明

ios类是从ios_base类派生而来的，实际上，本书中所有对格式状态字的访问完全可以用“ios：”的形式，甚至还可以是“ostream：”或“istream：”的形式。

14.4.4　格式控制值的默认值

默认情况下，I/O流类库的格式控制值如下所示。

0000 0010 0000 0001

第1位为1，即会跳过输入中的空白，第2位为0代表不会在插入操作后立即刷新缓冲区，第3位为0代表十六进制输出时，A-F和前缀X（如果要输出前缀的话）大写，第4位为0代表采用八进制和十六进制时不输出前缀“0”或“0x”，第5位为0代表不输出小数后无意义的0，第6位为0代表正数前不会加正号，第7、8、9位为0，代表默认情况下C++并不对输出进行对齐，C++将数据的正确显示排在首要位置，而把是否对齐等美观性的考虑放在第二位，第10位为1，第11位或第12位为0代表在输出数据或接收输入时，默认采用十进制的形式，而不是八进制或十六进制的形式，第13、14位为0代表系统的默认浮点输出既不一定恒为定点形式，也不一定恒为浮点数形式，而是根据输出浮点数的具体情况进行判断，这在稍后“输出浮点数”一节中会有介绍，第15位为0代表在输出bool型变量时，默认将true输出为1，将false输出为0，而不是直接输出true或false。

14.4.5　flag函数读取和设定格式状态字

ios_base类中提供了成员函数flag来读取和设置格式状态字，flag函数有两种调用形式，如下所示。

❑long flags（）。

❑long flags（long flagNew）。

1.long flags（）

无参的flags函数调用返回当前格式状态字的情况，而不对其做任何的改变，如以下示例代码14.8所示。

代码14.8　无参flags函数的调用FlagsSample1

＜------------------------文件名：example1408.cpp------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　cout＜＜cout. flags（）＜＜endl；

06　return 0；

07　}

输出结果如下所示。

513

【代码解析】代码的用法很简单，第5行的flags函数是ios_base类的公有成员函数，而cout所属的ostream类是从ios_base类间接派生而来的，因此，可以用派生类的对象cout调用flags函数，返回结果为513，这是系统默认的十进制形式，其对应的二进制形式如下所示。

0000 0000 0000 0000 0000 0010 0000 0001

等价于ios_base：skipws|ios_base：dec，这和C++默认的输入输出状态字是一致的。

2.long flags（long flagNew）

这种调用方式将flagNew以位方式复制到格式状态字中，这样实现了对格式状态位的操作，返回修改前的格式状态字。

可以自行计算参数flagNew应设定的值，举例来说，要使得格式化常量showpos、showbase、oct和right对应的位为1，而其他位都设为0，可以将其分别对应的值0x0020、0x0008、0x0400和0x0080加起来，产生程序使用的值0x04A8，如下所示。

cout.flags（0x40A8）；使用这种方法会给程序的可移植性带来问题，因为不同的C++编译工具和实现中格式化常量的具体值可能不同，所以推荐的用法是使用位与或位或命令。举例来说，要在当前格式状态字的基础上使showpos有效（对应位设为1），可使用下列语句。

cout.flags（cout.flags（）|ios_base：showpos）；通过位或指令，将状态控制字中相应位置设为1。同理，如果要使某项无效，可以用位与的方式，比如要使得showbase无效（对应位设为0），如下所示。

cout.flags（cout.flags（）＆～ios_base：showbase）；

对showbase取反后，则除其对应位变为0外，其余位都为1，此时再和cout.flag（）返回的当前格式状态字位与，则格式状态字中showbase对应位被成功设置0。

如示例代码14.9所示。

代码14.9　有参flags函数的用法FlagsSample2

＜-----------------------文件名：example1409.cpp-------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　int x=255；

06　cout＜＜x＜＜endl；

07　cout. flags（cout.flags（）＆～ios_base：dec）；//位与抵消掉某个选项

08　cout. flags（cout.flags（）|ios_base：hex）；//位或添加某个选项

09　cout＜＜x＜＜endl；

10　return 0；

11　}

输出结果如下所示。

255

ff

【代码解析】代码第8行通过“cout.flags（cout.flags（）|ios_base：hex）；”以位或的方式将输出转换为十六进制，但细心的读者可能会注意到此前的第7行语句“cout.flags（cout.flags（）＆～ios_base：dec）；”，其作用是使系统默认的十进制输出失效，这不难理解，在某个时刻，八进制、十进制和十六进制3种形式至多只能有1个有效，否则是会出现问题，像hex、oct和dec等成并列关系的格式化常量还有另外两组，分别是“right、left和internal”以及“scientific和fixed”。

为方便编程，提高效率，ios_base类中同样定义了格式化常量adjustfield、basefield和floatfield，其取值分别如下所示。

adjustfield=0x01c0，//0000 0001 1100 0000

basefield=0x0e00，//0000 1110 0000 0000

floatfield=0x3000，//0011 0000 0000 0000从上述取值中不难看出以下内容。

adjustfield实际上等价于right|left|internal

basefield实际上等价于dec|oct|hex

floatfield实际上等价于scientific|fixed引入这3个常量后，省去了判断当前状态的麻烦，代码14.9比较简单，在设定十六进制前，确切知道系统当前格式状态字中dec有效（即默认为十进制），可如果程序结构相对复杂，或格式状态字已被设置多次，程序员不确定此时的状态控制字中到底是dec有效还是oct有效，因此需要加以判断，使用上述定义的3个格式化常量后，不再需要判断，仍以代码14.9为例，代码如下所示。

cout.flags（cout.flags（）＆～ios_base：basefield）；

一次性将dec、oct和hex对应的位都设置为0，而后再根据需要对hex进行设置即可，其他的两个格式化常量adjustfield（对齐方式相关）和floatfield（浮点数显示相关）的用法与此一致。

14.4.6　使用setf函数和unsetf函数设定格式关键字

flags函数已能够很好地设定格式状态字，但在设定前都要读取当前格式状态字，变化后采用位复制的形式进行修改，对每个位都进行了操作，这似乎有些麻烦，为此，ios_base类中提供了成员函数setf和unsetf来对格式状态字进行设置，与flags函数的本质区别在于setf函数和unsetf函数只对参数中指定的位进行操作，对其他位没有影响，如下所述。

（1）一元setf函数，对应的原形如下所示。

long setf（long ff）；这种调用方式返回当前格式状态字，并根据ff将格式状态字的相应位设置为1，使其有效，但不影响所有其他的标志位，一元setf函数实际上等价于如下代码。

long flags（flags（）|ff）；

通俗地说，一元setf函数将格式状态字中与ff中为1的位相对应的位设定为1。

（2）二元setf函数，对应的原形如下所示。

long setf（long ffadd,long ffremove）；这种调用方式同样返回当前格式状态字，但与一元setf函数不同之处在于其多了一个参数ffremove，实际上，二元setf函数等价于如下代码。

long flags（flags（）＆～ffremove）；

long flags（flags（）|ffadd）；通俗地说，二元函数首先将格式状态字中与第2个参数中为1的位相对应的位设置为0，而后将与第1个参数中为1的位相对应的位设置为1，而其他位不发生变化。这种调用形式如前面所提的dec、hex和oct等意义并列的标志位，以要修改的基数为例，只要将ios_basefield作为第2个参数即可，如下所示。

cout.setf（ios_base：hex,ios_base：basefield）；

类似的并列意义标志位如表14.6所示。

（3）unsetf函数。

unsetf函数只有一种函数调用形式，如下所示。

long unsetf（long ffremove）；

同setf函数意义相反，unsetf根据ffremove将格式控制字的对应位设置为0，使其失效，如示例代码14.10所示。

代码14.10　格式控制字与setf的用法SetfSample

＜----------------------文件名：example1410.cpp--------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　int x=255；

06　cout＜＜x＜＜endl；//默认按十进制输出

07　cout. setf（ios_base：hex,ios_base：basefield）；

08　cout＜＜x＜＜endl；//按十六进制输出

09　cout. setf（ios_base：showbase|ios_base：uppercase）；//设置基指示符输出和

10　//数值中的字母大写输出

11　cout＜＜x＜＜endl；

12　cout. setf（ios_base：dec,ios_base：basefield）；//恢复到十进制输出

13　cout. setf（ios_base：showpos）；//正数前加+号

14　cout＜＜x＜＜endl；

15　bool y=true；

16　cout＜＜y＜＜endl；//bool型默认输出0或1

17　cout. setf（ios：boolalpha）；

18　cout＜＜y＜＜endl；//输出true或false

19　return 0；

20　}

输出结果如下所示。

255

ff

0XFF

+255

1

true

【代码解析】代码演示了如何使用setf函数和unsetf函数设置格式状态字，默认情况下，系统按十进制形式输入输出，所以第1个对x的输出操作，结果为255，而后通过代码第7行的二元setf函数设置16为输入输出的基，所以第2个对x的输出操作结果为ff，通过代码第9行的语句“cout.setf（ios_base：showbase|ios_base：uppercase）；”中showbase有效设置了输出基指示符（八进制为0，十六进制为0x），uppercase有效地将十六进制采用大写的A-F以及0X，所以第3个对x的输出结果为0XFF，第13行语句“cout.setf（ios_base：showpos）；”命令用于在正数前输出正号，需要注意的是输出正号只是针对十进制形式的，对八进制和十六进制而言，系统统一当成无符号数来处理，所以在第5个对x的输出之前，先将输入输出的基改回十进制，结果是+255，对bool型变量y来说，系统默认以0和1分别代表false和true输出，但只要设定了常量boolalpha有效，便可以输出“true”或“false”的形式。

说明

在输入输出基的选择上，编译器一般采取如下规则，只有当oct、dec和hex对应的位分别为1，0，0时，便采用八进制，分别为0，0，1时，便采用十六进制，其他情况下均采用默认的十进制形式。

14.4.7　设置域宽

域宽是用来控制输出的，在ios类中用int型量x_width存放，对域宽进行操作的函数有以下两个。

1.int width函数

此函数是用来返回当前的域宽值，其函数实现如下所示。

int ios_base：width（）

{

return x_width；

}

2.int width（int wid）函数

此函数用来设置域宽，并返回原来的域宽，具体函数的定义如下所示。

int ios_base：width（int wid）

{

int i=x_width；

x_width=wid；

return i；

}

默认时（即不对width进行设置的情况下），输出一个值时，其占据的屏幕宽度大小取决于其字符数。width函数用以指定最小域宽，当值的输出宽度（字符数）小于指定的域宽时，可用当前的填充字符（默认为空格）将其补充到指定的域宽；而当值的长度超出最小域宽时，不会被截断，C++会增宽字段，以容纳该数据。

如示例代码14.11所示。

代码14.11　width函数用法WidthSample

＜-----------------------文件名：example1411.cpp-------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　int i=cout. width（）；//返回当前域宽

06　cout＜＜"默认宽度为："＜＜i＜＜endl；

07　cout. width（20）；//设置域宽为20

08　i=cout. width（）；//返回当前域宽

09　cout＜＜"修改后域宽为："＜＜i＜＜endl；

10　cout＜＜"一次有效："＜＜cout.width（）＜＜endl；//域宽为20，生效

11　cout＜＜5＜＜endl；//域宽设定不再有效

12　return 0；

13　}

输出结果如下所示。

默认宽度为：0

□□□□□□修改后域宽为：20

一次有效：0

5

【代码解析】代码第5行首先输出了系统的默认域宽，结果为0，证明对任何输出都没有设最小域宽的限制，默认情况下值所占据的屏幕格数等于其字符数，代码第7行设定为20后，输出域宽最小为20，因此语句“cout＜＜"修改后域宽为："＜＜i＜＜end1；”的结果为“□□□□□□修改后域宽为：20”，每个汉字相当于两个字符格数，因此，以默认的右对齐方式放置字符串，并以默认的填充字符（空格）使其达到20格。对齐方式可以通过格式状态字进行设置，使用稍后要介绍的ios_base类成员函数fill也可以指定其他填充字符。

注意

width函数仅作用于下一次输出，在语句“cout＜＜"修改后域宽为："＜＜i＜＜endl；”中，i的输出便没有了域宽的要求，因此，如有必要，在每条输出语句前都应重新设定域宽。

14.4.8　填充字符

从代码14.11可以看出，C++默认用空格填充显示时不足域宽的部分，ios_base类还提供了成员函数fill以指定其他的填充符，其格式如下所示。

char ios_base：fill（）；//返回当前填充符

char ios_base：fill（char ch）；//将ch作为新的填充符，并返回原来的填充符

width函数与指定域宽“一次有效”不同的是，新的填充字符将一直有效，采用fill函数指定新的填充字符，如示例代码14.12所示。

代码14.12　使用fill函数指定填充字符FillSample

＜---------------------文件名：example1412.cpp---------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　cout. fill（'$'）；//指定$为填充字符

06　cout. width（20）；//域宽20

07　cout. setf（ios_base：left,ios_base：adjustfield）；//设置左对齐

08　cout＜＜"Hello"＜＜endl；

09　return 0；

10　}

输出结果如下所示。

Hello$$$$$$$$$$$$$$$

【代码解析】代码第5行将填充符指定为$，同时第7行的语句“cout.setf（ios_base：left,ios_base：adjustfield）；”将对齐方式指定为左对齐，因此，输出时在字符串Hello后补足了15个$以满足域宽的要求。

14.4.9　浮点数输出和显示精度

默认情况下，C++采用前面printf函数%g说明符的格式，6位有效数字，不输出无意义的0。此外，ios_base类还定义了fixed（定点计数法）和scientific（科学记数法）两种输出模式，其中fixed意味着不管数字长度如何，使用格式567.89表示浮点值，而scientific意味着恒使用5.6789+e002形式，如果两个标志位均为0，采用默认形式。

如果不指定精度，fixed和scientific都默认为6位小数，但可以使用ios_base类提供的precision函数指定浮点数的小数位数，precision函数同样有以下两种调用形式。

int ios_base：precision（）；//返回当前精度

int ios_base：precision（int n）；//设置当前精度为n，并返回原来的精度

同fill函数一样，precision函数同样一直有效，直到有新的精度值并设定为止，但要特别注意的是precision函数的精度值设定影响的仅是浮点型数据，对整型数据或其他类型没有影响，如示例代码14.13所示。

代码14.13　浮点数数据与精度设置PrecisionSample

＜------------------------文件名：example1413.cpp------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　double pi=3. 1415926535；

06　int num=200；

07　cout＜＜"默认精度为："＜＜cout.precision（）＜＜"位"＜＜endl；

08　cout＜＜"默认表达方式："＜＜num＜＜"，"＜＜pi＜＜endl；

09　//按值大小，相当于printf函数的%g模式，6位有效数字

10　cout. setf（ios：scientific,ios：floatfield）；//设定为科学记数法

11　cout＜＜"科学记数法表达方式："＜＜num＜＜"，"＜＜pi＜＜endl；

12　cout. setf（ios：fixed,ios：floatfield）；//设为定点，取消科学记数法表达方式

13　cout＜＜"定点表达方式："＜＜num＜＜"，"＜＜pi＜＜endl；

14　cout. precision（9）；//精度为9位

15　cout. setf（ios：scientific,ios：floatfield）；//设定为科学记数法

16　cout＜＜"定点9位表达方式："＜＜num＜＜"，"＜＜pi＜＜endl；

17　cout. setf（ios：fixed,ios：floatfield）；//设为定点，取消科学记数法表达方式

18　cout＜＜"科学记数9位表达方式："＜＜num＜＜"，"＜＜pi＜＜endl；

19　cout. unsetf（ios_base：floatfield）；//设置：既非定点，又非科学记数法

20　cout＜＜"默认方式下："＜＜num＜＜"，"＜＜pi＜＜endl；

21　return 0；

22　}

输出结果如下所示。

默认精度为：6位

默认表达方式：200，3.14159

科学记数法表达方式：200，3.141593e+000

定点表达方式：200，3.141593

定点9位科学记数法表达方式：200，3.141592654e+000

科学记数法9位表达方式：200，3.141592654

默认方式下：200，3.14159265

【代码解析】从代码14.13的输出结果可以看出，精度的设置只对浮点数起作用，代码第7行的语句输出默认的精度为6位，对定点显示法，其对应于printf函数的%f，精度指的是小数的位数，科学记数法对应于printf函数的%e，精度同样指的是小数的位数，但如果既非fixed，又非scientific时采用默认输出方式，对应printf函数的%g，精度指的是有效数字的位数。

注意

在新的C++标准中，fixed和scientific输出模式都会输出末尾无意义的0以满足精度的需求，而在旧标准中，末尾的0是不会显示的，除非对showpoint常量进行了设置。

14.4.10　控制符

在使用ios_base类中设定的setf函数时，需要对参数有足够的了解。因此，为了提高函数调用的友好性，C++流类库提供了＜iomanip＞头文件，同时也提供了多个控制符，由控制符完成setf函数的调用，这些控制符如表14.7所示。

对于表14.7中定义的无参控制符不仅在头文件iomanip中有定义，在头文件iostream中也有定义，因此，使用无参控制符可以不使用“#include＜iomanip＞”指令，但如果要使用表中最后4个有参控制符，必须包含头文件iomanip。

无参控制符实际上是函数，其原型如下所示。

ios_base＆控制符（ios_base＆）；如下例所示。

boolalpha（cout）；//等价于cout.setf（ios_bse：boolalpha）

cout＜＜true＜＜endl；实际上，ostream类重载了＜＜操作符，如下述用法与上面的示例等价。

cout＜＜boolalpha＜＜true＜＜endl；

说明

也许读者对endl的用法已经很熟悉，实际上控制符是定义在名称空间std中的，是全局性的函数定义，可以直接用函数名调用，而原来诸如函数width和fill等是定义在ios_base类内的成员函数，必须通过cout等进行调用。

由于操作符都返回了ios_base类对象的引用，这就允许将＜＜拼接起来，实现链式输出，如示例代码14.14所示。

代码14.14　控制符的应用FormatSample

＜-----------------------文件名：example1414.cpp-------------------------------＞

01　#include＜iostream＞

02　#include＜iomanip＞

03　using namespace std；

04　int main（）

05　{

06

07　cout＜＜showbase＜＜hex＜＜uppercase＜＜255＜＜endl；//显示基，十六进制，大写形式输出

08　cout＜＜setw（10）＜＜setfill（'*'）＜＜"hello"＜＜endl；//设定域宽为10，一次输出有效

09　cout＜＜5＜＜endl；

10　return 0；

11　}

输出结果如下所示。

0XFF

*****hello

0X5

【代码解析】代码演示了控制符的用法，从中可以看出不论是无参控制符还是有参控制符，其实现的功能和前面介绍的ios_base类内成员差不多，但使用控制符表示起来相对要方便很多，也更加友好，编程的效率也更高。如代码第7行设置的十六进制大写形式输出。

注意

控制符setw实际输出同样是一次有效，实际输出意味着排除其他操作符的链式拼接，只关心真实复制到输出流中的内容。

14.5　输入流

本节来讨论输入的相关内容，输入是相对程序而言的，即如何从键盘或外部给程序提供数据，头文件iostream中定义了istream流类库对象cin,cin的使用方法相信读者已经很熟悉了，那就首先从操作符＞＞说起。

14.5.1　＞＞操作符

在流类库中，＞＞称为抽取操作符（Extraction Operator），istream类中已预先以成员函数的形式重载了C++基本类型（如char、int、long和double等）的＞＞操作符处理函数，其基本类型如下所示。

istream＆operator＞＞（基本类型＆）；在前面的示例代码中曾使用＞＞通过键盘对基本类型变量赋值，如下例所示。

int x=0；

cin＞＞x；等价于下述代码。

cin.operator＞＞（x）；//对应原型为cout.operator＞＞（int＆）

通过传递引用的方式，保证了抽取操作处理的变量x自身，而不是像传值操作那样处理其副本，因此，cin能直接修改作为参数的变量的值。

＞＞操作符的返回类型都为istream＆，这说明cin可以和cout一样，实现链式输入或称拼接输入，如下所示。

double x；

int y；

char z[10]；

cin＞＞x＞＞y＞＞z；＞＞的结合顺序为从左到右，上述代码等价下述代码。

（（cin.operator＞＞（x））.operator＞＞（y））.operator＞＞（z）；

说明

对C风格字符串的输入操作请参考第3章的相关内容。

14.5.2　输入流与格式状态字

在前面提到的格式状态字中，只有4项是和输入有关的，如下所述。

skipws=0x0001，//in，跳过输入中的空白

dec=0x0200，//input＆output，转换为十进制

oct=0x0400，//input＆output，转换为八进制

hex=0x0800，///input＆output，转换为十六进制

1.skipws

在高层I/O中曾介绍过，除了%c外，诸如%d、%f等读数操作和%s字符串读入都会自动跳过前导空格，直到遇到非空字符才开始进行匹配性检查，对%c读操作，即使是空白，也会将其存入字符变量中。

C++流类库机制与高层I/O基本相同，诸如对int、double、float等读数操作和C风格字符数组的读入同样会自动跳过前导空格，直到遇到非空字符才开始进行匹配性检查，但在对单个字符的读入处理中，两者有些细微的不同，C++对单字符输入（char）同样默认跳过前导空白，直到非空字符开始抽取，并进行匹配性检查。

这是由格式状态字中的枚举常量skipws决定的，默认时skipws有效，其在格式状态字中的对应位为1，如示例代码14.15所示。

代码14.15　cin和skipws格式化常量InputSample

＜-----------------------文件名：example1415.cpp-------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　char x；

06　cin＞＞x；//输入x

07　cout＜＜x＜＜endl；//输出x

08　cin. unsetf（ios_base：skipws）；//设置skipws失效

09　cin＞＞x；

10　cout＜＜x＜＜endl；

11　return 0；

12　}

输出结果如下所示。

5（注：键盘输入）

5

【代码解析】代码的结果看似有些奇怪，用户只输入了一个5，回车后程序就结束了，这和代码14.3所讲的缓冲区垃圾有相似之处。默认状态下，cin对char型变量输入同样跳过前导空格，因此，即使用户输入了“5”，字符'5'还是成功地赋值给了char型变量x，但第8行的语句“cin.unsetf（ios_base：skipws）；”使得skipws失效，cin对char型变量的输入不再跳过前导空白，只要是字符，都会被赋值给x，而提取了字符'5'后，输入缓冲区中尚存有一个回车符，这个回车符便赋值给了x，所以用户还未来得及输入，程序便结束了。

istream类中提供了sync成员函数清空输入缓冲区，在上例中只要在第2个“cin＞＞x”之前插入语句“cin.sync（）；”即可解决问题。

注意

对代码14.15来说，还有一种更简洁的方式便是用skipws和noskipws控制符。

再看一段失败的代码，如下所示。

char c1='A'；

while（c1！='\n'）

{

cin＞＞c1；

cout＜＜c1；

}

表面看来，该代码没有问题，但实际上这是个死循环，因为＞＞操作将会跳过空白符，也就是说c1永远不可能等于换行符'\n'。

2.dec、oct和hex

默认情况下，dec有效而oct和hex无效，即系统采用十进制的形式输入输出，但实际上，可以通过使用诸如“cin.setf（ios_base：hex,ios_base：basefield）；”的指令将输入基制转变为十六进制，这样当用户通过键盘输入11时，编译器解释为十六进制的0x11，实际上等价于十进制下的11。

除了格式化常量外，还可使用iomanip或iostream中定义的控制符，例如，下述语句可将输入进制更改为十六进制。

cin＞＞hex；

如示例代码14.16所示。

代码14.16　改变输入数的基ChangeBase

＜------------------------文件名：example1416.cpp------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　int x；

06　cin＞＞hex＞＞x；//改变输入的基

07　cout＜＜x＜＜endl；

08　return 0；

09　}

输出结果如下所示。

ff（注：键盘输入）

255

【代码解析】代码中第6行语句“cin＞＞hex＞＞x；”将输入的数解释为十六机制，因此，用户输入的ff实际上等价于十进制的255，从“cout＜＜x”的结果255可以看出，输入和输出维护着两套不同的格式状态字，对输入流的设置不会影响输出流，反之亦成立。

14.5.3　输入流与域宽

iostream类中定义的成员函数，如width、fill和precsion，以及iomanip中定义的一些有参控制符，如setfill、setw和setprecision等，都适用于输入流，但实际只有少数几个真正能影响输入流对象，本节介绍的是使用最多的用法，即字符数组与域宽。

在对char型数组进行输入时，一个重要的问题是保证输入字符串的长度小于数组的大小，这在第3章中已经介绍过，流类库提供了width函数和setw控制符，用于限定输入的字符个数，如下所示。

cin.width（n）：设置读入最多n-1个字符。最后1个要放'\0'。

setw（n）：与cin.width（n）等价。

如下述示例代码14.17所示。

代码14.17　字符数组输入与域宽CharArraySample

＜---------------------文件名：example1417.cpp---------------------------------＞

01　#include＜iostream＞

02　#include＜iomanip＞

03　using namespace std；

04　int main（）

05　{

06　char x[5]；//开辟一块字符空间，大小为5

07　cin＞＞setw（5）＞＞x；//只接受4个字符（5-1）

08　cout＜＜x＜＜endl；

09　return 0；

10　}

输出结果如下所示。

123456789

1234

【代码解析】代码中，字符数组x的大小为5，因此只能用来存放4个字符，第7行的语句“cin＞＞setw（5）＞＞x；”会自动对输入的字符串进行截断，只取前4个放入x中，其余字符仍遗留在输入缓冲区中。

注意

同输出流中width函数和setw控制符的用法一样，输入流中域宽的设置也只是“一次有效”，只作用于最近的＞＞操作符上。

14.5.4　使用get函数读取单个字符

第3章中曾介绍过使用get函数和getline函数读取一行字符的操作，这两个函数称为非格式化函数，它们只读取字符输入（包括空格），不进行数据转换。本节讨论如何使用iostream类的get成员函数读取单个字符。

1.istream＆istream：get（char＆）；

此函数调用将待赋值char型变量作为其参数，返回的istream类的引用，所以可以将其拼接起来，即下列用法是合法的。

char c1，c2，c3；

cin.get（c1）.get（c2）.get（c3）；甚至是和＞＞的拼接，如下所示。

cin.get（c1）.get（c2）＞＞c3；

按从左到右的顺序结合，先将第1个输入字符赋值给c1，并返回cin，则代码变为cin.get（c2）＞＞c3，而后将第2个输入字符赋值给c2，返回cin，此时代码变为cin＞＞c3，由于＞＞默认跳过空白字符，因此，下一个非空字符会被赋值给c3，从这个过程也可以体会到get函数与＞＞的不同，get函数并不会判断字符是否为空，而只是直接进行读取。

当istream＆get（char＆）；到达流尾时，不会对参数赋值，还将设置流状态，使得返回的cin测试结果为false，关于流状态的内容稍后会有介绍。

说明

所谓流尾，可能是真正的文件尾，也可能是键盘输入的文件尾，如DOS下的CTRL+Z。

如以下示例代码14.18所示。

代码14.18　使用有参get函数读取单个字符SingleCharSample1

＜------------------------文件名：example1418.cpp------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　char c1；

06　while（cin. get（c1））//cin的判断测试

07　{

08　cout＜＜c1；

09　}

10　return 0；

11　}

输出结果如下所示。

5（注：键盘输入）

5

A（注：键盘输入）

A

^Z（注：键盘输入）

【代码解析】代码第6行的“cin.get（c1）”将用户输入的字符赋值给c1，并在while结构中显示出来，只要“cin.get（c1）”的返回结果测试为真，这个过程会一直进行下去，只有当到达流尾时，程序才会终止，^Z便是用户通过键盘输入的文件尾。

代码14.18　中还有一个关键点需要理解，即程序对'\n'的处理，本节一直在强调，get函数不会跳过任何空白，因此，用户输入5和回车后，输入流中有两个字符'5'和'\n'，此时，'5'首先被赋值给c1并输出，而后'\n'被赋值给c1并输入，实现换行，输入缓冲区变空，程序可以等待下一步的输入。

2.int istream：get（void）；

该函数调用读入一个字符（包括空白字符），并返回该字符的整型值，同有参get版本不同，因返回值并非istream类的对象，因此，无法将多个无参get函数拼接起来，基本的用法如下所示。

char c1=cin.get（）；下述用法是错误的。

c1=cin.get（）.get（）；cin. get（）返回一个int值，int内置类型不能调用get函数，因此语法出现错误。但无参get函数可以放在抽取序列的尾部，如下所示。

char c1，c2；

c2=cin.get（c1）.get（）；

上述代码首先执行cin.get（c1），将输入流中的第1个字符赋值给c1，并返回cin，这时代码变为c2=cin.get（），读取下一个字符，并返回其整型值给c2。

到达流尾后，无参get函数会返回EOF,EOF是iostream类中提供的一个符号常量，多为-1，但可能会随系统的不同而不同，EOF是End Of File的缩写，用以标识文件尾。

下述代码14.19实现了与代码14.18相同的功能。

代码14.19　使用无参get函数读取单个字符SingleCharSample2

＜------------------------文件名：example1419.cpp------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　char c1='A'；

06　while（（c1=cin. get（））！=EOF）//cin的判断测试

07　{

08　cout＜＜c1；

09　}

10　return 0；

11　}

输出结果如下所示。

x（注：键盘输入）

x

6（注：键盘输入）

6

^Z（注：键盘输入）

【代码解析】代码第6行使用无参get函数读取单个字符，到达流尾时产生的EOF同样会赋值给外部接收变量，读者可从中体会两个get函数的细微差别。

14.5.5　使用get函数和getline函数读取C风格字符串

istream类中重载了get函数和getline函数来一次性读取多个字符到C风格字符串中，大致有以下重载版本。

1.istream＆istream：get（char*buffer,int size,char delim='\n'）；

该函数调用形式，用于读连续的字符到buffer中，停止条件有两个，即

第1个是达到最大字符数size-1；第2个是遇到分隔符delim（默认为换行符'\n'），只要两个条件有一个满足，读取便停止，在buffer最后加'\0'。注意分隔符不会写入字符串中，而是留在输入流中。

如以下示例代码14.20所示。

代码14.20　使用get函数读取C风格字符串GetAString1

＜-----------------------文件名：example1420.cpp-------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　const int Len=10；//定义常量Len，用以声明数组大小

04　int main（）

05　{

06　char sz[Len]；

07　cin. get（sz,Len，'A'）；//get函数读取字符串

08　cout＜＜sz＜＜endl；

09　return 0；

10　}.

输出结果如下所示。

Congratulations（注：键盘输入）

Congratul或

CONGRATULATION（注：键盘输入）

CONGR

【代码解析】代码第7行使用get函数读取一串字符到字符数组sz中，输入停止的约束条件有两个，一是9个字符；二是出现字符'A'，当输入“Congratulations”时，输出结果便为“Congratul”，达到了9个字符，但当输入大写形式“CONGRATULATION”时，因为出现了分隔符'A'，所以，只读取了“CONGR”到sz中，分隔字符'A'不会被写出，留在了输入缓冲区中。

使用get函数读取字符串同样不会跳过任何形式的空格。

2.istream＆istream：getline（char*buffer,int size,char delim='\n'）；

getline函数与前面介绍的get函数用法几乎一致，唯一的不同体现在对分隔符的处理，get函数将分隔符留在输入流中，而getline函数抽取并丢弃输入流中的分隔符（分隔符同样不会读取到字符串中），如以下示例代码14.21所示，用以体会两者的区别。

代码14.21　使用getline函数读取C风格字符串GetAString2

＜-----------------------文件名：example1421.cpp-------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　const int Len=10；//定义常量Len，用以声明数组大小

04　int main（）

05　{

06　char sz1[Len]，sz2[Len]；

07　cin. get（sz1，Len，'A'）；

08　cin. get（sz2，Len）；

09　cout＜＜sz1＜＜endl；

10　cout＜＜sz2＜＜endl；

11　cin. sync（）；//清空输入缓冲区

12　cin. getline（sz1，Len，'A'）；

13　cin. getline（sz2，Len）；

14　cout＜＜sz1＜＜endl；

15　cout＜＜sz2＜＜endl；

16　return 0；

17　}

输出结果如下所示。

CONGRATULATION（注：键盘输入）

CONGR

ATULATION

CONGRATULATION（注：键盘输入）

CONGR

TULATION

【代码解析】代码中两者输入结果的不同体现在sz2的值上，get函数将分隔符'A'留在输入缓冲区中，因此，sz2以字符'A'开头，而代码第12行getline函数将分隔符'A'从输入缓冲区中提出，但不写入sz1中，而是抛弃掉，这样，sz2便以字符'T'开头。

还要特别说明的是换行符的处理，在程序中使用了语句“cin.sync（）；”来清空输入缓冲区，实际上语句“cin.get（）”也可以胜任，共同的目的是将get函数读取sz2后残留在输入缓冲区中的换行符提出，否则，在使用getline读取字符到sz1中时，换行符会是sz1的第1个字符。

技巧理解换行符的处理十分重要，否则程序可能会出现一些意想不到的问题。

14.5.6　其他istream方法

除了get函数和getline函数之外，istream类中还定义了ignore、read、peek、gcount和putback等成员函数，本节简要地介绍它们的用法。

1.ignore函数

其基本调用形式如下所示。

istream＆istream：ignore（int n=1，int delim=EOF）；

ignore函数从输入流中抽取一定数目的字符丢弃，停止的条件有两个，一是达到n个字符；二是碰到分隔符delim。两个条件只要有一个满足即停止，分隔符同样从输入流中抽取出来。如以下示例代码14.22所示。

代码14.22　istream类中ignore函数的用法IstreamIgnore

＜-------------------------文件名：example1422.cpp-----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　const int Len=10；//长度为10

04　int main（）

05　{

06　char buf[Len]；

07　cin. ignore（5，'Z'）；//从输入流中提取一段，丢掉

08　cin. getline（buf,Len）；

09　cout＜＜buf＜＜endl；

10　return 0；

11　}

输出结果如下所示。

ABCDEFGHIJKLMNOPQRST（注：键盘输入）

FGHIJKLMN或

XYZABCDEFGHIJKLMN（注：键盘输入）

ABCDEFGHI

【代码解析】代码第7行语句“cin.ignore（5，'Z'）；”用以从输入流中抽取一定数目的字符并抛弃，当用户输入“ABCDEFGHIJKLMNOPQRST”时，因前5个字符中不含'Z'，因此，前5个字符都被抽取出并抛弃，所以b u f为“F G H I J K L M N”，但当用户输入“XYZABCDEFGHIJKLMN”时，第3个字符是‘Z’，因此，ignore函数只会将前3个字符“XYZ”抽取并抛弃，此时的buf为“ABCDEFGHI”。

2.read函数

其基本调用形式如下所示。

istream＆istream：read（char*buf,int n）；

该函数读取n个字符到buf中，与getline和get函数不同的是，read函数不会在输入后加入空字符'\0'，因此不能将buf转换为字符串。

实际上，istream类中read函数是和ostream类中定义的write函数搭配使用的。

3.peek函数

其基本调用格式如下所示。

int istream：peek（）；

该函数返回输入流中下一个字符的int值，但不将其从输入流中抽取出来。

4.gcount函数

其基本调用格式如下所示。

int istream：gcount（）

该函数返回最后一个非格式化抽取方法读取的字符数，这意味着字符是由get函数、getline函数、ignore函数和read函数读取的，而不是使用＞＞读取的，因为＞＞对输入进行了格式化，使之与特定的数据类型相匹配。

5.putback函数

其基本调用格式如下所示。

istream＆istream：putback（char）；

该函数将一个字符插入到输入缓冲区的头部（下一个要取的位置上），并返回istream类对象的引用，实际上peek函数相当于是先用get函数读取一个字符，再将该字符putback回去。

说明

istream类中还定义了其他成员函数，本节不再逐个介绍，感兴趣的读者可查阅相关资料。

14.6　流状态

每个流（istream或osream）都有一个与之相关的状态字，出错和非标准条件都通过适当地设置和检测这个状态字来处理。

14.6.1　什么是流状态

ios_base类中定义了如下枚举结构，用以表示流的状态。

enum Iostate

{

goodbit=0x0，//没有位设置，操作正常

eofbit=0x1，//到达文件末尾

failbit=0x2，//I/O操作失败，如未能取得预期字符，

badbit=0x4，//非法操作

_Statmask=0x7

}；

以上述结构中不难看出，eofbit、failbit和badbit状态常量实际上分别对应着状态字的第1、2、3位，当输入输出流出现各种问题时，会自动置eofbit、failbit和badbit中的一个或多个有效，也就是将其在状态字中的对应位被置为1。

只有当3位都为0，即状态字等于goodbit时，才说明一切正常，程序可以使用ios_base提供的共用成员函数检查和设置流的状态，决定要进行的操作。

14.6.2　读取流状态

ios_base类提供了如下函数读取当前的流状态，如表14.8所示。

以下为流出错的例子，如代码14.23所示。

代码14.23　流出错范例IOState

＜------------------------文件名：example1423.cpp------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　int x,sum=0；

06　while（cin＞＞x）//当“cin＞＞x”成功，即输入一直是数时

07　{

08　sum+=x；//累加和

09　}

10　cout＜＜sum＜＜endl；

11　if（cin. fail（））//如果输入失败

12　{

13　cout＜＜"匹配失败"＜＜endl；

14　}

15　return 0；

16　}

输出结果如下所示。

1 2 3 4 5（注：键盘输入）

6 7（注：键盘输入）

8 A 7 9（注：键盘输入）

36

匹配失败

【代码解析】因为输入缓冲，所以第3行的输入在单击回车键前不会发送给程序，但循环在字符A处停止了匹配处理，cin自动将failbit置1，while结构中失效。

对代码第6行“while（流）”结构来说，只有当流状态为0，即一切正常时，才返回true，否则返回false。除了上述if结构判断用法外，还可以使用switch开关结构对流出错进行处理，以cin为例，如下所示。

switch（cin.rdstate（））

{

case（ios_base：goodbit）：

//一切正常的处理

break；

case（ios_base：eofbit）：

//到达文件尾的处理

break；

case（ios_base：failbit）：

//I/O失败怎么办

break；

case（ios_base：badbit）：

//非法错误怎么办

break：

default：

//其他问题处理

}

14.6.3　管理流状态

对流状态的管理可分为状态字的设置和缓冲区管理两部分，以下分别进行介绍。

1.设置状态字

ios_base类中提供了clear函数和setstate函数用来设置状态字。

clear函数的原型如下所示。

void ios_base：clear（int nState=0）；

如果参数为0，该函数将清除所有错误标志，否则，参数可以设置为goodbit、eofbit、failbit及badbit中的一种或者其共同的组合，clear函数首先将所有的标志清除，然后将参数指定的标志置位。

setstate函数的原型如下所示。

void ios_base：setstate（int nState）；

参数可以设置为eofbit、failbit及badbit中的一种或者其共同的组合，与clear函数不同，setstate函数并不强制覆盖流的原状态，而是将括号内参数所代表的状态叠加到原始状态上。但需要注意的是如果nstate为goodbit，则setstate操作不会对流状态产生任何影响。

具体来说，对状态字的设置可分为复位和置位两种具体操作。

以输入流为例，如果流状态字中eofbit、failbit和badbit对应的位被置为1，说明输入流出了问题。此时，随后所有的输入操作都会被忽略，直到流状态字恢复正常，即为goodbit。使用程序设置流状态字的操作就是复位，使用无参clear函数便可很好地完成复位，如以下示例代码14.24所示。

代码14.24　流状态字复位ResetState

＜------------------------文件名：example1424.cpp------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　int x=0；

06　cin＞＞x；

07　cout＜＜x＜＜endl；

08　if（cin. fail（））//如果匹配失败，输出信息

09　{

10　cout＜＜"匹配失败"＜＜endl；

11　}

12　//cin. clear（）；

13　char y='x'；

14　cin＞＞y；

15　cout＜＜y＜＜endl；

16　return 0；

17　}

输出结果如下所示。

A

0

匹配失败

x

【代码解析】先将代码第12行的语句“cin.clear（）；”注释起来，当用字符'A'给int型变量x赋值时，发生匹配错误，输入流将状态常量failbit对应的位置1，此时如果不加复位，随后的输入操作都被忽略，即便是留在缓冲区中的'A'可以和char型变量y匹配。

为此，在代码14.24中加入语句“cin.clear（）”，输出结果变为如下所示。

A

0

匹配失败

A

尽管对int型变量x的输入同样失败，但留在缓冲区中的字符'A'成功地赋值给了char型变量y。不仅仅需要复位，编程中根据需要可能会用到置位操作，用以通知输入输出流中出现的错误。

2.管理缓冲区

在某些情况下，仅仅将流状态字复位还不够，因为导致匹配失败和错误发生的输入仍残留在缓冲区中，程序必须将其从缓冲区中提取出来，跳过这些输入字符。解决的办法很多，前面介绍的诸如ignore函数和get函数等都可以满足要求。

这里特别推荐一个sync函数，其调用格式如下所示。

int sync（）

当同步成功，即数据被清除后返回0，发生错误时，返回EOF。

C和C++标准输入输出缓冲区的清空方式如表14.9所示。

14.7　重载＞＞和＜＜

前面提及同高层I/O相比，流类库的优势在于可以对输入输出进行扩展，以满足自定义类型和结构的需要，本节给出插入符＜＜和抽取符＞＞的使用范例。

14.7.1　插入符的重载

在istream类中定义的操作符＞＞是作为成员方式重载的，如下例所示。

cout＜＜5；等价于下述代码。

cout.operator＜＜（5）；对自定义类型来说，操作符＞＞必须以友元方式进行使用，将ostream类对象的引用作为第一参数，将本类型的引用作为第二参数，同时为了实现链式输出，还要求返回ostream类的引用，来看一个具体的例子，定义了复数结构如下所示。

class Complex

{

private：

double imag；//虚部

double real；//实部

public：

Complex（double r=0.0，double i=0.0）//构造函数

{

real=r；

imag=i；

}

void disp（）

{

cout＜＜real＜＜"+i*"＜＜imag＜＜endl；

}

}；此时仅仅定义了其内部成员和构造函数，虽然disp函数可以用来输出诸如“2.5+i*3.1”之类的复数形式，但在程序中无法直观地使用流对象cout等来输入，即下列用法是非法的。

Complex c1（2.5，3.1）；

cout＜＜c1；

编译器无法为Complex类对象找到合适的重载版本。所以，必须要在新定义的Complex类中重载＜＜操作符来满足需要，如代码14.25所示。

代码14.25　重载＜＜操作符OperatorOverload1

＜-----------------------文件名：example1425.cpp-------------------------------

01　#include＜iostream＞

02　using namespace std；

03　class Complex

04　{

05　private：

06　double imag；//虚部

07　double real；//实部

08　public：

09　Complex（double r=0. 0，double i=0.0）//构造函数

10　{

11　real=r；

12　imag=i；

13　}

14　friend ostream＆operator＜＜（ostream＆，Complex＆）；//友元函数声明

15　}；

16　ostream＆operator＜＜（ostream＆os,Complex＆C1）//对操作符＜＜的重载

17　{

18　os＜＜C1. real＜＜"+i*"＜＜C1.imag＜＜endl；

19　return os；

20　}

21　int main（）

22　{

23　Complex c1（2. 5，3.1）；

24　cout＜＜c1；

25　return 0；

26　}

输出结果如下所示。

2.5+i*3.1

【代码解析】代码第16行，通过友元函数的形式对Complex类对象的＜＜操作进行了重载，实现了对流类库的扩充，使其满足需要。

14.7.2　抽取符的重载

同插入符一样，可以针对自定义类型重载抽取符＞＞，同样有以下3个要求。

❑以友元函数形式进行。

❑第一个参数是istream类的引用，第二个参数是自定义类型的引用。

❑返回istream类的引用以实现链式抽取，即拼接抽取。

仍以代码14.25中定义的Complex类为例，为其重载抽取符，见代码14.26。

代码14.26　重载＞＞操作符OperatorOverload2

＜----------------------文件名：example1426.cpp--------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class Complex

04　{

05　private：

06　double imag；//虚部

07　double real；//实部

08　public：

09　Complex（double r=0. 0，double i=0.0）//构造函数

10　{

11　real=r；

12　imag=i；

13　}

14　friend ostream＆operator＜＜（ostream＆，Complex＆）；//友元函数声明

15　friend istream＆operator＞＞（istream＆，Complex＆）；

16　}；

17　ostream＆operator＜＜（ostream＆os,Complex＆C1）//对操作符＜＜的重载

18　{

19　os＜＜C1. real＜＜"+i*"＜＜C1.imag＜＜endl；

20　return os；

21　}

22　istream＆operator＞＞（istream＆is,Complex＆C1）//对操作符＞＞的重载

23　{

24　is＞＞C1. real；

25　while（is. get（）！='*'）

26　{

27　}

28　cin＞＞C1. imag；

29　return is；

30　}

31　int main（）

32　{

33　Complex c1（2. 5，3.1）；

34　cin＞＞c1；

35　cout＜＜c1；

36　return 0；

37　}

输出结果如下所示。

4.5+i*5.6（用户输入）

4.5+i*5.6

【代码解析】代码第22行，通过对Complex重载了操作符＞＞，使得可以从用户输入的“4.5+i*5.6”格式串中提炼出实部和虚部，大大提高了程序的可读性和友好性。

14.8　文件操作

第1章中已经提及RAM中存储的内容在掉电后会自动消失，可有些信息在下次通电开机时可能会用到，文件可以有效解决这一问题，文件本身是存储在某种设备（硬盘、光盘、软盘和磁带等）上的一系列字节，流类库处理文件的机制和标准输入输出机制完全一样，文件也被看成是一种“流”，要写入文件，可以通过创建ofstream类对象，并使用其类内方法，如write函数或＜＜操作符来完成，要读取文件可通过创建ifstream类对象，使用其类内方法，如get函数或＞＞操作符来完成。

14.8.1　文件操作基本过程

在C++中，要进行文件的输入/输出，必须首先创建一个流，然后将这个流与文件相关联，即打开文件，此时才能进行读/写操作，完成后再关闭这个文件。这就是C++中进行文件输入/输出的基本过程。

C++中有3种类型的文件流：输入文件流ifstream，输出文件流ofstream，以及输入/输出文件流fstream。这些文件流类都定义在fstream文件中。因此，在使用3种流对象之前，只需要包含头文件＜fstream＞即可。实际上，ifstream是从istream继承而来，ofstream是从ostream继承而来，而fstream是从iostream继承而来的，也就是说，前面所讲的输入输出流的机制，如各种插入符＞＞的定义、格式化方法、控制符和流状态等同样适用于文件操作，同时，为了满足文件操作的特殊要求，在派生类中增加了一些针对文件的处理函数。

提示　实际上，ifstream、ofstream和fstream也分别是模板类basic_ifstream、basic_ofstream和basic_fstream的实例化。

14.8.2　文件的打开

在C++中打开一个文件，就是建立一个文件的输入输出流，并将文件和流关联起来，而关闭一个文件，就是取消这种关联，首先来看以下流的建立过程。

建立流的过程就是定义流类的对象，如下所示。

ifstream in；

ofstream out；

fstream io；

上述代码分别定义了输入流对象in、输出流对象out和输入/输出流对象io。

接下来，需要将流对象和特定的文件关联起来，使用open函数打开文件是一种常用的方法，open函数的原型是在fstream中定义的，在ifstream,ofstream和fstream类中均有实现，其原型如下所示。

void open（const char*，ios_base：openmode）；第1个参数传递的是文件名，openmode是在ios_base类中定义的一个类型，用于表示模式，其定义格式如下所示。

enum openmode

{

in=0x01，//打开文件，以便从文件中读取

out=0x02，//打开文件，以便向文件中输出

ate=0x04，//打开文件，并移到文件尾

app=0x08，//输出追加到文件尾

trunc=0x10，//删除同名文件

binary=0x20//文件以二进制形式打开，无效时默认为文本文件

}；

旧的C++标准中＜fstream.h＞头文件中还定义了常量nocreate和ios：noreplace，但新的＜fstream＞库已经取代了＜fstream.h＞，不再支持这两个标志。

同前面格式状态字和流状态字类似，openmode中定义的这些枚举常量也分别对应着模式字的某个位，使常量有效即是使其对应位为1，因此，你可以用位或操作符“|”组合这些标志，如下所示。

ofstream logfile；

logfile.open（"login.txt"，ios_base：out|ios_base：trunc）；fstream类型对象同时支持读和写操作，可以写成如下形式。

fstream logfile；

logfile.open（"database.txt"，ios_base：in|ios_base：out）；

表14.3中介绍了高层I/O中fopen函数的模式，流类库与高层I/O文件打开模式的对比如表14.10所示。

默认情况下，常量ios_base：binary无效，即其对应位为0，对应着高层I/O中的“t”模式，处理的是文本文件，如果处理的是二进制文件，只要在模式中置ios_base：binary有效即可，对应着“b”模式。

除了使用open函数外，在创建流时可以直接调用构造函数完成其和某个文件的关联，举例如下所示。

ofstream logfile（"login.txt"，ios_base：out|ios_base：trunc）；

fstream logfile（"database.txt"，ios_base：in|ios_base：out）；

其接收的参数同样有两个：const char*和ios_base：openmode。这同open函数中的用法完全一致。

流对象的构造函数和open函数对第2个参数openmode提供了默认值，ifstream的openmode默认为ios_base：in,ofstream默认为ios_base：out|ios_base：trunc,fstream类不提供默认openmode值，因此，在创建fstream类对象时，必须显式指明第2个参数。

新的C++流类库提供了is_open成员函数来判断流和文件是否正确地关联，如下所示。

ofstream logfile（"login.txt"，ios_base：out|ios_base：trunc）；

if（logfile.is_open（））

{

……

}

如果成功关联，is_open函数返回true，否则返回false。

还可以利用流状态的相关信息对文件和流是否关联成功进行判断，如logfile.good（）等，但这种方式容易对某些关联失败产生漏判和误判，因此这里便不再赘述，推荐采用is_open函数进行判断。

14.8.3　取消文件和流的关联

当流对象生命期结束被撤销时，该对象和文件的关联会自动断开。此外，C++还提供了close函数来显式断开文件和流的连接，如下所示。

logfile.close（）；//假定logfile为ostream类对象

注意

close函数的操作仅仅是断开了文件和流的连接，流对象并不会被撤销，流缓冲区中的数据也会被保留，这个流还可以关联到其他的文件中，如示例代码14.27所示。

代码14.27　C++流类库文件操作FileOperation

＜-----------------------文件名：example1427.cpp-------------------------------＞

01　#include＜iostream＞

02　#include＜fstream＞

03　using namespace std；

04　int main（）

05　{

06　ofstream of（"ex1. txt"）；//创建ofstream类对象of，采用默认的openmode

07　of＜＜"Hello,this is example1"＜＜endl；//同使用cout一样使用of将字符串写入关联文件中

08　of. close（）；//断开的只是of和ex1.txt

09　double x；

10　cout＜＜"请输入x的大小："＜＜endl；//正常使用iostream中定义的标准输入输出对象

11　cin＞＞x；

12　of. open（"ex2.txt"）；//of重新关联到ex2.txt中

13　of＜＜"您输入的数据是："＜＜x＜＜endl；//输出到ex2.txt中

14　of. close（）；//断开连接

15　return 0；

16　}

输出结果如下所示。

请输入x的大小：

3.1415（用户输入）此时，在文件夹中创建了ex1.txt和ex2.txt两个文本文件，其中的内容分别如下所示。

Hello,this is example1（注：ex1.txt中的内容）

您输入的数据是：3.1415（注：ex2.txt中的内容）

【代码解析】代码第6行创建了ofstream类对象of，利用构造函数在其创建时便和文件ex1.txt相关联，构造函数的第2个参数采用了默认值的形式，即ios_base：out|ios_base：trunc，如果文件不存在，则创建之，否则，将其删除重新创建（也可理解为改写），而且，of.close（）操作仅起到断开流对象of和文件关联的作用，并不会撤销of，因此，还可以使用open函数令of和另一个文件ex2.txt关联。

可以像使用cout一样应用of对文件进行写操作，这里所讲的输入输出是针对程序来说的，由程序向文件传送数据是写操作，从文件向程序传送数据是读操作。稍后会有读写操作的相关介绍。

说明

前面所讲的输入输出流的机制，如各种插入符＞＞的定义、格式化方法、控制符和流状态等完全适用于文件流对象。

14.8.4　文件的读写

文件分为文本文件和二进制文件，前者以字节（Byte）为单位，每字节对应1个ASCII码，表示1个字符，故又称字符文件。二进制文件以字位（Bit）为单位，实际上是由0和1组成的序列。例如，float型值3.141592以文本形式存储占用8个字节，即每位数字都对应1个字符，这需要将float型数的机内表示转换为字符格式，以二进制形式存储则可能只占用4个字节，即存储的是该float型值的机内表示。

对文件的读写也分为按文本（Text）方式和按二进制（Binary）方式两种，两者的特点对比如表14.11所示。

文本文件不仅以字符为单位，还以常量数字、单词（字符串）和行为单位。它不仅要区分一个整数，一个浮点数，一个字符以及一个字符串，同时还要分行。因此，文本文件适合于需要直观显示的场合，而一般在大量数据及大规模程序的执行代码，以及较大的文本文件的中间进行处理，当需要提高I/O操作速度，以及简化I/O编程时，以二进制方式可显示出其的优越性。

14.8.5　文本文件的读写

文本文件的读写和前面介绍的输入流和输出流用法几乎一致，所不同的是将cout和cin等标准输入输出流替换成了ofstream类和ifstream类对象，面向的对象也是文本文件，如示例代码14.28所示。

代码14.28　文本文件读写TXTReadAndWrite

＜-----------------------文件名：example1428.cpp-------------------------------＞

01　#include＜iostream＞

02　#include＜fstream＞

03　using namespace std；

04　main（）

05　{

06　ofstream out（"test. dat"）；//创建ofstream流对象out

07　if（！out. is_open（））//判断文件关联是否成功

08　{

09　cout＜＜"文件打开失败"；

10　return 0；

11　}

12　out＜＜5＜＜""＜＜3. 14515＜＜""＜＜"string\n"；//将信息写入test.dat中，注意分隔

13　out. close（）；//关闭流

14　int ix；

15　double dy；

16　char sz[10]；

17　ifstream in（"test. dat"）；//创建ifstream流对象in，以便从test.dat中读取信息

18　if（！in. is_open（））//以判断文件关联是否成功

19　{

20　cout＜＜"读取文件失败"；

21　}

22　in＞＞ix＞＞dy＞＞sz；//数据读入

23　cout＜＜ix＜＜endl＜＜dy＜＜endl＜＜sz＜＜endl；//标准输出，屏幕显示

24　in. close（）；//关闭流

25　return 1；

26　}

输出结果如下所示。

5

3.14515

string

【代码解析】代码14.28在文件夹中创建了文件test.dat，其中内容可以通过记事本程序查看。上例首先创建了1个ofstream流对象，将1个整数值、1个浮点数值和1个字符串写入文件test.dat中，而后代码第17行创建了1个ifstream流对象，从test.dat中将信息读出，赋值给变量ix、dy和字符数组sz，并借助标准输出流对象cout输出。

注意

输入输出流中其他方法，如put函数、get函数和getline函数等都适用于文件输入输出流对象。

14.8.6　二进制文件的读写

对二进制文件进行读写主要是使用read函数和write函数，如示例代码14.29所示。

代码14.29　二进制文件读写BinaryReadAndWrite

＜-------------------------文件名：example1429.cpp-----------------------------＞

01　#include＜iostream＞

02　#include＜fstream＞

03　using namespace std；

04　main（）

05　{

06　ofstream out（"test. dat"，ios_base：out|ios_base：binary）；//创建ofstream流对象out

07　if（！out. is_open（））//判断文件关联是否成功

08　{

09　cout＜＜"文件打开失败"；

10　return 0；

11　}

12　float f1=3. 141592f；

13　out. write（（char*）＆f1，sizeof（f1））；//以二进制形式写入test.dat中

14　out. close（）；//关闭流

15　float f2=0；

16　ifstream in（"test. dat"，ios_base：in|ios_base：binary）；

17　//创建ifstream流对象in，以便从test.dat中读取信息

18　if（！in. is_open（））//判断文件关联是否成功

19　{

20　cout＜＜"读取文件失败"；

21　}

22　in. read（（char*）＆f2，sizeof（f2））；//以二进制形式读入数据

23　cout. setf（ios_base：fixed）；

24　cout＜＜f2＜＜endl；//标准输出，屏幕显示

25　in. close（）；//关闭流

26　return 1；

27　}

输出结果如下所示。

3.141592

【代码解析】代码14.29会在文件夹下生成二进制文件test.dat，用记事本程序打开后会发现里面是乱码。使用二进制文件存储一个float型值3.141592仅仅需要4个字节，而使用文本文件模式将使用8个字节。

代码第6和16行，ofstream类对象out和ifstream类对象in创建时，需要指定ios_base：binary模式，同时，在使用read函数和write函数时，必须将地址强制转换成指向char的指针。从这个意义上说get和put函数配对也可以完成对二进制文件的读写，但两个函数每次只能操作1个字符，所以要人为控制读写的字节数目，不如read函数和write函数用得方便。

14.8.7　文件定位指针和随机读取

每个流（不仅仅是文件流，也包括普通流）都隐含着一个指针，对文件来说，C++把每一个文件都看成一个有序的字节流，如图14.5所示，每一个文件或者以文件结束符（End Of File Marker）结束，或者在特定的字节号处结束。

图　14.5　文件字节流示意图

文件是和流关联的，对文件进行读写实际上受流中的文件定位指针（File Position Pointer）的控制，指针标记文件操作的当前位置，该指针在哪里读写操作就在哪里进行。

输入流（istream及其派生类，如ifstream等）的指针称为读指针（Get Pointer），每次抽取操作自动将读指针向流尾移动。输出流（ostream及其派生类，如ofstream等）指针称写指针（Put Pointer），每次插入操作自动将写指针向流尾移动。输入输出流（iostream及其派生类，如fstream等），同时继承了读指针和写指针。

可以通过使用以下成员函数来读出或配置这些指向流中读写位置的流指针。

1.tellg函数和tellp函数

这两个成员函数不用传入参数，返回pos_type类型的值（根据ANSI-C++标准），就是一个long整数，代表当前get流指针的位置（用tellg）或put流指针的位置（用tellp）。

2.seekg函数和seekp函数

这对函数分别用来改变get流指针和put流指针的位置，返回的是流对象的引用，两个函数都被重载为两种不同的原型，如下所述。

❑绝对位置参数

seekg（pos_type position）；

seekp（pos_type position）；

使用这个原型，流指针被改变为指向从流开始计算的一个绝对位置。要求传入的参数类型与tellg函数和tellp函数的返回值类型相同。

❑相对位置参数

seekg（off_type offset,seekdir direction）；

seekp（off_type offset,seekdir direction）；这个原型将put流指针（seekp函数）或get流指针（seekg函数）改变为从“参数direction指定指针”开始计算，位移为offset处的位置。seekdir是在ios_base类中定义的枚举结构，如下所示。

enum seekdir

{

beg=0，//begin，从流开始计算的offset

cur=1，//current，从指针当前位置开始计算的offset

end=2//end，从流末尾开始计算的offset

}；

offset为正值时，代表指针向流尾方向移动，offset也可以为负值，此时，指针向流的头部移动。

对文本模式而言，其中的某些特殊字符（如换行符）可能被修改，不好定位，所以随机访问多用于二进制文件，如示例代码14.30所示，该代码用以获得一个二进制文件的长度。

代码14.30　二进制文件的随机读取RandomAccess

＜------------------------文件名：example1430.cpp------------------------------＞

01　#include＜iostream＞

02　#include＜fstream＞

03　using namespace std；

04　main（）

05　{

06　ofstream out（"test. dat"，ios_base：out|ios_base：binary）；//创建ofstream流对象out

07　if（！out. is_open（））//判断文件关联是否成功

08　{

09　cout＜＜"文件打开失败"；

10　return 0；

11　}

12　float f1=3. 141592f；

13　out. write（（char*）＆f1，sizeof（f1））；//以二进制形式写入test.dat中

14　out. close（）；//关闭流

15　float f2=0；

16　ifstream in（"test. dat"，ios_base：in|ios_base：binary）；

17　//创建ifstream流对象in，以便从test.dat中读取信息

18　if（！in. is_open（））//判断文件关联是否成功

19　{

20　cout＜＜"读取文件失败"；

21　}

22　in. seekg（0，ios_base：end）；//将指针移到流尾

23　long num=in. tellg（）；//输出当前指针

24　cout＜＜num＜＜endl；//标准输出，屏幕显示

25　in. close（）；//关闭流

26　return 1；

27　}

输出结果如下所示。

4

【代码解析】本代码的前半部分和代码14.29相同，将浮点数3.141592写入到二进制文件test.dat中，为了获得test.dat的大小，采取了先移动指针到流尾，即代码第22行，再输出其值的方式，本例很好地演示了如何获取当前指针以及移动指针。

14.9　字符串流

C++库中提供了非模板strstream类族和模板sstream类族用以处理字符串的输入输出，strstream类族用于读取字符数组的格式化信息并将格式化信息写入字符数组，也就是说，strstream类流对象以字符数组为输入输出设备，sstream类族用于读取string对象中的格式化信息并将格式化信息写入string对象，也就是说，sstream类流对象将string对象作为输入输出设备。

注意

新的C++标准已经指出，除非出于和旧式代码兼容的目的，不推荐使用strstream类流对象，但本节还是为读者进行简要的介绍。

14.9.1　strstream类族

strstream类族是针对char型定义的，不是由模板类实例化而来，包括istrstream类（用于从字符数组中读取格式化信息）、ostrstream类（用于向字符数组中写入格式化信息）以及strstream类（即可读取，也可以写入）。

提示使用strstream类族必须包含头文件＜strstream＞。

1.ostrstream类流对象

ostrstream类流对象支持一个字符数组作为数据传输目的地的输出流，可以将某个现成的字符数组和ostrstream类流对象关联起来，也可以使用流对象自动分配的存储空间，这取决于使用的构造函数，如下所示。

ostrstream（char*，int,int=ios_base：out）；

该构造函数第1个参数是字符数组指针；第2个参数是缓冲区的大小；第3个参数是打开模式。如果是默认的模式（ios_base），则从缓冲区头部开始添加新的字符，如果打开模式是ios_base：ate或ios_base：app，则从缓冲区的尾部添加新的字符。

如示例代码14.31所示。

代码14.31　ostrstream与现成字符数组的绑定OstrstreamSample

＜------------------------文件名：example1431.cpp------------------------------＞

01　#include＜iostream＞

02　#include＜strstream＞

03　using namespace std；

04　const int Len=20；

05　int main（）

06　{

07　int num=9；

08　char sz[]="Hello"；

09　char buf[Len]；

10　ostrstream os（buf,Len）；//创建ostrstream对象，并将其和字符数组buf绑定

11　os＜＜"num="＜＜num＜＜endl；//输出格式化信息到流中，实际上是到字符数组中

12　os＜＜"sz="＜＜sz＜＜endl；

13　os＜＜ends；//输出字符串结束符

14　cout＜＜buf；

15　return 0；

16　}

输出结果如下所示。

num=9

sz=Hello

【代码解析】代码第10行在创建ostrstream流对象os时将其和字符数组buf绑定，这样使用os进行＜＜操作时，目的对象就是buf数组，成功地将格式化信息写入了字符数组，由于strstream类族是从iostream类族派生而来的，因此，前面介绍的其他操作函数、控制字和流状态等同样适用于字符串流。例如当插入操作超过申请的存储空间的大小时，流对象的bad函数返回false。

注意

如果想添加更多的字符，通常的做法是新建一个ostrstream，将旧的流灌入新流中，并向新流添加字符。

如果使用自动分配存储空间的方法，则使用无参的构造函数，如下所示。

ostrstream（）；

此时，创建的流对象会在堆中分配一块存储空间，并自己维护。当流中的信息不断增加，原存储块不够用时，可以动态分配更多的存储空间，程序中可调用成员函数rdbuf返回缓冲区指针，以访问缓冲区的内容。

当不知道数据需要多少空间时，这是一种很好的方法。但问题随之出现了，当空间不够，流对象自己移动存储块以分配更多空间时，之前调用rdbuf函数返回的缓冲区指针已失效，而且由于是由流对象管理内存，所以返回的指针何时失效，程序员是不知道的，ostrstream采用“冻结”（freeze）的方法解决这个问题，通过调用str成员函数返回指向缓冲区字符数组的指针，并将流本身“冻结”，冻结之后的输出操作均无效，做抛弃处理，而且ostrstream对象也不再负责存储空间的自动释放，必须由用户自己清理缓冲区，如下所示。

delete os.str（）；

来看一段示例代码14.32。

代码14.32　ostrstream无参构造函数OstrstreamConstructor

＜--------------------------文件名：example1432.cpp----------------------------＞

01　#include＜iostream＞

02　#include＜strstream＞

03　using namespace std；

04　const int Len=20；

05　int main（）

06　{

07　int num=9；

08　char sz[]="Hello"；

09　ostrstream os；//创建ostrstream对象os，由其自己管理缓冲区

10　os＜＜"num="＜＜num＜＜endl；//输出格式化信息到流中，实际上是到字符数组中

11　os＜＜"sz="＜＜sz＜＜endl；

12　os＜＜ends；//输出字符串结束符

13　cout＜＜os. str（）；//返回指向缓冲区中字符数组的指针，冻结

14　os＜＜"C++"＜＜endl；//冻结之后的输出，抛弃

15　cout＜＜os. str（）；

16　delete os. str（）；//用户清理os缓冲区

17　return 0；

18　}

输出结果如下所示。

num=9

sz=Hello

num=9

sz=Hello

【代码解析】代码第9行采用无参构造函数的形式创建了ostrstream流对象os，则os自动开辟缓冲区并维护其大小，采用str成员函数可以返回指向缓冲区中字符数组的指针，使用标准输出流对象cout对该指针输出，实现了对缓冲区中内容的访问，同时，str函数将os冻结，此后的“os＜＜"C++"＜＜endl；”操作被抛弃，不再写入流中。由于流对象已不再对缓冲区进行自动维护，因此需要用户在流对象被撤销前执行清理缓冲区内存，以免造成内存泄露。

注意

ostrstream都不会为operator＜＜调用自动添加'\0'，所以要记得在所输出的信息最后加上＜＜ends，标识流的结尾。

2.istrstream类流对象

istrstream类流对象将字符数组当做信息传入源，将格式化的信息读出赋值给某些变量，因此，istrstream类流对象必须和某字符数组关联。

istrstream类流对象的构造函数原形如下所示。

istrstream（const char*str,int size=0）；

参数1表示字符数组，参数2表示缓冲区大小，当其为0时，表示istrstream类流对象关联到由第1个参数str指向的一片以空字符'\0'结尾的内存空间。如示例代码14.33所示。

代码14.33　istrstream类流对象的使用IstrstreamSample

＜-----------------------文件名：example1433.cpp-------------------------------＞

01　#include＜iostream＞

02　#include＜strstream＞

03　using namespace std；

04　const int Len=20；

05　int main（）

06　{

07　char sz[]="H156 3. 1415"；//注意：H前有个空格

08　istrstream is（sz）；//创建istrstream对象is，与字符数组sz绑定

09　char c；

10　int i；

11　double d；

12　is. setf（ios_base：skipws）；//设置格式状态字，对char型通过跳过前导空白

13　is＞＞c＞＞i＞＞d；//输入，从字符串（源头）到变量

14　cout＜＜"c is"＜＜c＜＜"，i is"＜＜i＜＜"，d is"＜＜d＜＜endl；//标准输出验证结果

15　return 0；

16　}

输出结果如下所示。

c is H,i is 156，d is 3.1415

【代码解析】代码第8行创建的istrstream流对象is和字符数组sz关联，则sz就成了is操作的信息源，缓冲区大小采用了默认值0，意味着缓冲区的大小为strlen（sz）+1。前面介绍的格式状态字和流状态等对istrstream类对象同样有效，因此，语句“is.setf（ios_base：skipws）；”用以设定在char型输入时跳过空白（默认不跳），这样char型变量c才能读到正确的字符‘H’，否则空白被赋值给c，后续的字符“H”无法赋值给int型变量i，输入流出错，流状态位failbit有效。

14.9.2　sstream类族

sstream类族包含一个从ostream类中派生而来的ostringstream类（是basic_ostringstream类的char型示例化，对应着wchar_t宽类型还有wostringstream，本节针对的都是char型实例化后的sstream类族），从istream类中派生来的istringstream类，还有从iostream类派生而来的stringstream类。相比strstream类族，sstream类族将C++风格字符串，即string类型变量当做输入输出设备。

注意

使用sstream类族必须包含头文件＜sstream＞。

1.ostringstream类流对象

ostringstream类流对象用于向string类变量中插入字符，ostringstream类流对象的构造函数有如下两种重载形式。

ostringstream（const string＆str,openmode which=ios_base：out）；

ostringstream（openmode which=ios_base：out）；

第1种重载形式用string对象str初始化缓冲区，从传递的参数str为const引用来看，流对象的输出操作并不会对原来的string对象str产生影响，输出的目的地是流缓冲区。第2种形式不对缓冲区进行初始化。

不论采用哪种方式，都是由流对象本身动态管理内存，当同样可以使用str函数返回一个string类临时对象时，该临时对象使用缓冲区中的内容初始化，其原型如下所示。

string str（）const；

注意

返回临时string对象的同时，流对象不会被冻结。

此外，ostringstream类中还重载了另一种str函数形式，如下所示。

void str（const string＆s）；

这种调用形式首先将流缓冲区清空（Deallocate），而后重新开辟缓冲区，用string对象s初始化，此时put指针位于ostringstream流对象的头部，如示例代码14.34所示。

代码14.34　ostringstream类流对象的用法OstringstreamSample

＜-----------------------文件名：example1434.cpp-------------------------------＞

01　#include＜iostream＞

02　#include＜sstream＞

03　#include＜string＞//使用string必须包括此头文件

04　using namespace std；

05　int main（）

06　{

07　string str（"Hello"）；

08　ostringstream os（str）；//用str为os缓冲区初始化

09　os＜＜"I Love China"＜＜endl；//输出字符串，从流指针（当前指向头部）开始

10　cout＜＜os. str（）；

11　os. str（"123456789"）；

12　cout＜＜os. str（）；//输出缓冲区中的string对象值

13　cout＜＜endl；//换行

14　os＜＜"NUDT"；//也可使用seekp函数移动put指针，调节位置

15　cout＜＜os. str（）＜＜endl；//不会冻结，后面的输出仍采纳

16　return 0；

17　}

输出结果如下所示。

I Love China

123456789

NUDT56789

注意

上述代码在VC6下编译通过，但运行时会出现内存错误，而在其他编译器上没有这种问题，这可能是VC6的bug，建议使用VC 2005编译运行本例。

【代码解析】创建了ostreamstring对象os，并用string类对象os初始化其缓冲区，则可以像使用cout一样使用os将格式化信息写入os的缓冲区中，通过“os.str（）”形式即可返回一个由os缓冲区数据初始化的string临时对象，完成对缓冲区的访问。代码第11行语句“os.str（"123456789"）；”首先清空了os的缓冲区，而后重新用字符串“123456789”初始化新的缓冲区，此时put指针重新回到流的头部，因此，后面“os＜＜NUDT”操作将覆盖缓冲区中的前4个字符，输出结果为“NUDT56789”。

注意

ostringstream类流对象同样涉及结尾标识ends，即'\0'的添加问题。

2.istringstream类流对象

istringstream类流对象将string类对象作为信息输入源，istringstream构造函数同样有两种重载形式，如下所示。

istringstream（openmode which=ios_base：in）；

istringstream（const string＆str,openmode which=ios_base：in）；

常用的是第2种形式。如示例代码14.35所示。

代码14.35　istringstream类流对象的使用IstringstreamSample

＜-------------------------文件名：example1435.cpp-----------------------------＞

01　#include＜iostream＞

02　#include＜sstream＞

03　using namespace std；

04　int main（）

05　{

06　string str（"123H3. 1415"）；//创建string类对象

07　istringstream is（str）；//创建istringstream类流对象is

08　int i；

09　char c；

10　double d；

11　is＞＞i＞＞c＞＞d；//格式化输入

12　cout＜＜"i is"＜＜i＜＜"，c is"＜＜c＜＜"，d is"＜＜d＜＜endl；

13　return 0；

14　}

输出结果如下所示。

i is 123，c is H,d is 3.1415

【代码解析】代码第7行创建了istringstream类的对象is，并使用string类对象str对is的缓冲区进行了初始化，可以像使用cin一样使用is，从其缓冲区中格式化输入信息到变量i、c和d中，使用cout对3个变量的标准输出来验证程序的正确性。

对第1种形式的构造函数来说，istringstream类流对象的缓冲区为空，因此在输入操作前需要使用“void str（const string＆s）”成员函数来对其缓冲区赋值，以便格式化输入，如代码14.36所示。

代码14.36　使用str函数改写istringstream类流对象缓冲区FuncStr

＜-----------------------文件名：example1436.cpp-------------------------------＞

01　#include＜iostream＞

02　#include＜sstream＞

03　using namespace std；

04　int main（）

05　{

06　istringstream is；//创建istringstream类流对象is

07　is. str（"123A9.2"）；

08　int i；

09　char c；

10　double d；

11　is＞＞i＞＞c＞＞d；//格式化输入

12　cout＜＜"i is"＜＜i＜＜"，c is"＜＜c＜＜"，d is"＜＜d＜＜endl；

13　return 0；

14　}

输出结果如下所示。

i is 123，c is A,d is 9.2

【代码解析】代码第6行虽然调用无参构造函数创建了istringstream类流对象is，但随后通过str函数改写了is的缓冲区，从而实现了信息的格式化输入。

14.10　小结

本章详细地讨论了C++的文件存储机制，在介绍C++独有的流类库之前，讲述了从C语言继承而来的底层I/O和高层I/O机制。尤其是高层I/O机制的一些函数需要重点了解，这有助于读者读懂以前的代码，实现从高层I/O向C++流类库的转换。

从功能上讲，流类库可以分为输入流和输出流两部分。从操作的对象来看，可以分为标准输入输出流（面向控制台）、文件流（面向文件）和字符串流（面向字符串）。可以对流进行格式化，每个流也都维护着一个流状态，通过查询该状态可以知道操作是否成功以及其他相关信息。

98版标准中，所有的流类库都是确切定义的，而新标准的流类库都基于模板改写，以提供char和wchar_t两种实例化，通常所讲的iostream类实际上是basic_iostream模板类的char型实例。iostream头文件中自动定义并打开了8个流：cin、cerr、cout和clog，以及它们的wchar_t版本wcin、wcerr、wcout和wclog。

I/O类库提供了大量的成员方法，对所有的基本类型重载了＜＜（输出为字符或字符串）和＞＞（将字符或字符串转换为某类型）操作符，get函数和getline函数也可用于字符和字符串的输入，put函数和write函数也为字符和字符串的输出提供了支持。

使用ios_base类中提供的方法和头文件iomanip以及iostream中提供的控制符，可以对输入或输出的格式进行设置，同时，每个流维护着一个状态字，通过读取和设置状态字能判断流的好坏，进行合理控制。

fstrstream类族将iostream中的方法扩展到文件的处理上，fstream类族是从iostream类族继承而来的，所以，fstream类族对象可以调用所有的iostream方法来对文件进行操作。但和普通输入输出不同的是，必须将文件流对象和某文件关联起来，并按打开、读写和关闭的顺序进行操作。tellp函数、tellg函数、seekp函数和seekg函数用以对输入输出流指针进行操作，这使得文件和流的随机存取成为可能。

strstream类族和sstream类族可以使用istream和ostream类方法抽取字符串中的信息，并把要放在字符串中的信息格式化，strstream处理的是C风格字符串，而sstream类对象是针对C++风格字符串，也就是针对string类对象而言的。

14.11　上机实践习题

1.根据用户输入的文件名和路径，读取指定的文本文件内容并显示出来。

【提示】上述题目主要是要求读者熟悉C++文件操作流的相关知识，重点是掌握文件操作流的使用。

【关键代码】

01　char filename[FILENAME_MAX]={0}；

02　char str[FILENAME_MAX]={0}；

03　cout＜＜"请输入文件名及路径"＜＜endl；

04　cin＞＞filename；

05　ifstream in（filename）；//创建ifstream流对象in，以便从test.dat中读取信息

06　if（！in. is_open（））//以判断文件关联是否成功

07　{

08　cout＜＜"读取文件失败"；

09　}

10　while（！in. eof（））

11　{

12　in. read（str,FILENAME_MAX-1）；

13　cout＜＜str＜＜endl；

14　memset（str，0，FILENAME_MAX）；

15　}

16　in.close（）；//关闭流

2.根据用户输入的文件名和路径，然后得到指定文件的大小。

【提示】上述题目主要是要求读者熟悉文件操作的相关知识，重点是掌握文件定位指针的使用。

【关键代码】

01　char filename[FILENAME_MAX]={0}；

02　long filesize=0；

03　cout＜＜"请输入文件名及路径"＜＜endl；

04　cin＞＞filename；

05　ifstream in（filename）；//创建ifstream流对象in，以便从test.dat中读取信息

06　if（！in. is_open（））//以判断文件关联是否成功

07　{

08　cout＜＜"读取文件失败"；

09　}

10　in. seekg（0，ios_base：end）；

11　filesize=in. tellg（）；

12　in. close（）；//关闭流

13　cout＜＜filename＜＜"文件大小等于："＜＜filesize＜＜"字节"＜＜endl；

第15章　名称空间

大型程序往往是由团队开发的，即使是个人编写的程序，随着代码量的增多，变量、函数和类等重名及相互冲突的现象时有发生，有些情况下编译器会指明错误所在，但有时候会发生一些察觉不到的覆盖，让程序员对出现的错误摸不着头脑。

很多厂商也提供了快捷的第3方类库，用户不用关心库中的类是如何实现的，只要知道如何调用接口使用即可，但不同厂商定义的变量、函数和类可能会发生冲突，同样是初始化操作，甲公司类库提供了initial函数，乙公司可能也提供了initial函数，如果在程序中同时使用了两个公司的类库，initial函数将对应哪个版本呢？

为了解决这些问题，新的C++标准提供了名称空间机制。旧标准中（ANSI/ISO 1998）并没有该项机制，所以一些特别老的编译器可能并不支持名称空间特性。

本章主要涉及以下知识点。

❑名称空间：介绍名称空间的概念及其定义。

❑实体的作用和可见域：介绍实体的概念、作用和可见域的相关知识。

❑名称空间的作用和可见域：介绍名称空间的定义和使用。

❑名称空间的思考：介绍名称空间使用的基本原则。

15.1　什么是名称空间

旧的标准中，仅依靠名称在程序中的作用域和可见域来区分同名实体，在第6章中已经介绍了实体的作用域和可见域的概念，由于“屏蔽”等原因，可见域一般是作用域的子集。

注意

旧标准中使用诸如“#include＜iostream.h＞”形式的头文件，而新标准中只要使用“#include＜iostream＞”即可，旧标准中不需要using指令指明名称空间，而新标准中需要这样做。

15.1.1　名称空间范例

名称空间通过定义一种新的声明区域来创建命名的名称空间，一个名称空间中的实体不会和另外一个名称空间中的同名实体相冲突，来看一个简单形象的例子如代码15.1所示，其既能说明名称空间是怎么回事，又演示了如何定义全局的名称空间。

代码15.1　使用名称空间UseNamespace

＜---------------------------文件名：example1501.cpp--------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　namespace yaya//创建名称空间yaya

04　{

05　extern int num=1；//yaya中对num的定义性声明

06　class Ex//yaya中对Ex的类定义

07　{

08　public：

09　void hello（）

10　{

11　cout＜＜"This is yaya"＜＜endl；

12　cout＜＜num＜＜endl；

13　}

14　}；

15　}

16　namespace abao//创建名称空间abao

17　{

18　extern int num=2；//abao中对num的定义性声明

19　class Ex//abao中对Ex的类定义

20　{

21　public：

22　void hello（）

23　{

24　cout＜＜"This is abao"＜＜endl；

25　cout＜＜num＜＜endl；

26　}

27　}；

28　}

29　int main（）

30　{

31　yaya：Ex obY；//使用yaya中的类Ex

32　abao：Ex obA；//使用abao中的类Ex

33　obY. hello（）；

34　obA. hello（）；

35　return 0；

36　}

输出结果如下所示。

This is yaya

1

This is abao

2

【代码解析】代码第3和16行定义了两个全局性的名称空间yaya和abao，虽然两个名称空间中都定义了类Ex，也都定义性声明了全局变量num，通过使用限定符，如yaya：Ex和abao：Ex可以有效地对两个名称空间中定义的实体进行区分，如果没有引入名称空间机制，这是不可能实现的。

15.1.2　定义名称空间

C++中定义名称空间的基本格式如下所示。

namespace名称空间名

{

变量类型变量；

函数返回类型函数原型；

}变量可以是简单变量，也可以是复杂变量；函数的具体实现既可以在名称空间内，也可以在名称空间外，但在外部实现时，应指明其所属的名称空间，如代码15.1中两个名称空间的定义等价于如下代码。

namespace yaya//创建名称空间yaya

{

extern int num=1；//yaya中对num的定义性声明

class Ex//yaya中对Ex的类定义

{

public：

void hello（）；

}；

}

namespace abao//创建名称空间abao

{

extern int num=2；//abao中对num的定义性声明

class Ex//abao中对Ex的类定义

{

public：

void hello（）

{

cout＜＜"This is abao"＜＜endl；

cout＜＜num＜＜endl；

}

}；

}

namespace yaya//名称空间是开放的

{

void Ex：hello（）

{

cout＜＜"This is yaya"＜＜endl；

cout＜＜num＜＜endl；

}

}

这说明，名称空间是开放的，可以根据需要随时将实体加入到名称空间中。

15.2　实体的作用域与可见域

名称空间可以如代码15.1中的yaya和abao一样，定义成全局的，也可以将一个名称空间定义在另一个名称空间内，形成名称空间的嵌套，但名称空间的定义不能在代码块内。因此在默认情况下，名称空间中的实体作用域是全局的。

15.2.1　实体可见域

名称空间中实体的作用域是全局的，并不意味着其可见域也是全局的，如果不使用作用域限定符和using机制，抛开名称空间嵌套和内部屏蔽的情况，实体的可见域是从实体创建到该名称空间结束（如果有名称空间的嵌套，则内部名称空间中的实体可能会屏蔽外部名称空间中的实体，这在稍后会讲到），在名称空间外，该实体是不可见的。如示例代码15.2所示。

代码15.2　实体可见域VisibleArea

＜-----------------------------文件名：example1502.cpp------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　namespace A//创建名称空间A

04　{

05　void dispA（）

06　{

07　cout＜＜"This is A"＜＜endl；

08　}

09　}

10　namespace B//创建名称空间B

11　{

12　void dispB（）

13　{

14　dispA（）；

15　cout＜＜"This is B"＜＜endl；

16　}

17　}

18　int main（）

19　{

20　dispB（）；

21　return 0；

22　}编译运行，编译器给出出错信息如下所示。

error C2065：'dispA'：undeclared identifier

error C2065：'dispB'：undeclared identifier

【代码解析】代码第5和第12行的dispA函数和dispB函数的作用域是全局的，但这并不是说这两个函数的可见域就是全局的，事实上直接使用函数名“dispA（）”和“dispB（）”形式的调用只能用在该函数的名称空间里，A中定义的dispA函数在名称空间B中是不可见的，同理在外部函数main中，名称空间B中定义的dispB函数也是不可见的。

15.2.2　可见域的扩展

在某个名称空间中定义或创建的程序实体，如果要在其他名称空间中或外部函数中访问，必须使用作用域限定符：或使用using声明机制来使实体可见，这可看做是作用域的扩展。对代码15.2进行修改后如代码15.3所示。

代码15.3　使用作用域限定符使实体可见AccessMethods

＜----------------------------文件名：example1503.cpp-------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　namespace A//创建名称空间A

04　{

05　void dispA（）

06　{

07　cout＜＜"This is A"＜＜endl；

08　}

09　}

10　namespace B//创建名称空间B

11　{

12　void dispB（）

13　{

14　A：dispA（）；//名称空间A中的dispA函数

15　cout＜＜"This is B"＜＜endl；

16　}

17　}

18　int main（）

19　{

20　B：dispB（）；//名称空间B中的dispB函数

21　return 0；

22　}

输出结果如下所示。

This is A

This is B

【代码解析】通过“空间名：实体”的形式，如代码第14行的“A：dispA（）”使得名称空间A中实体dispA可见。除此之外，还可使用using声明机制来扩展某个名称空间中实体的可见域。

15.2.3　using声明机制

如果不希望在每次使用名称空间实体时都使用作用域限定符，可使用using声明机制扩展其可见域，using声明的基本格式如下所示。

using名称空间：实体名；

如“using A：dispA（）”，至于using声明语句将该实体的可见域扩展到什么程度，这取决于using语句的书写位置，换言之，这取决于using语句的可见域。

1.using声明在全局区域

这使得该程序实体等价于一个全局实体，如代码15.4所示。

代码15.4　全局using声明GlobalUsing

＜------------------------------文件名：example1504.cpp-----------------------＞

01　#include＜iostream＞

02　using namespace std；

03　namespace A//创建名称空间A

04　{

05　void dispA（）

06　{

07　cout＜＜"This is A"＜＜endl；

08　}

09　}

10　using A：dispA；//using声明语句，dispA等价于全局变量

11　void dispEx（）//全局函数

12　{

13　cout＜＜"This is outside"＜＜endl；

14　}

15　namespace B//创建名称空间B

16　{

17　void dispB（）

18　{

19　dispEx（）；

20　dispA（）；

21　cout＜＜"This is B"＜＜endl；

22　}

23　}

24　int main（）

25　{

26　B：dispB（）；

27　dispA（）；

28　return 0；

29　}

输出结果如下所示。

This is outside

This is A

This is B

This is A

【代码解析】代码第10行的using声明语句“using A：dispA；”使得dispA函数等价于一个全局函数，因此可以在名称空间B和main函数中直接调用dispA（）。

2.using声明语句在局部

这里的局部包括其他名称空间内，或某个函数内，甚至是某个代码块内。如if结构中，此时可以将所声明实体理解为在using声明处创建的，如代码15.5所示。

代码15.5　局部using声明LocalUsing

＜------------------------文件名：example1505.cpp-----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　namespace A//创建名称空间A

04　{

05　int num=0；

06　}

07　void disp（）//全局函数

08　{

09　using A：num；//num的可见域为从此处到disp函数结束

10　cout＜＜num＜＜endl；//解释为A：num

11　}

12　int main（）

13　{

14　disp（）；

15　return 0；

16　}

输出结果如下所示。

0

【代码解析】在disp函数内第9行使用了using声明“using A：num；”，则从using语句开始到disp函数结束都是num的可见域。

总之，using声明语句使得实体的可见域等价于在using语句处创建的实体，但第6章中提及内部实体会屏蔽外部实体。以代码15.5为例，如果将disp函数更改为如下所示。

void disp（）//全局函数

{

using A：num；//num的可见域为从此处到disp函数结束

cout＜＜num＜＜endl；//解释为A：num

if（true）

{

int num=1；//屏蔽了外部num

cout＜＜num＜＜endl；//此处的num不再解释为A：num

}

}

则using声明语句中的num会被内部if代码块中声明的num屏蔽，关于屏蔽的问题稍后会有介绍。

15.2.4　using声明带来的多重声明问题（二义性）

如果using声明使用不当，很容易引起多重声明错误（Multiple Declaration），例如已经定义了全局函数disp，却还使用全局using声明语句“using A：disp”，假设没有屏蔽发生，那么调用disp函数时，编译器不确定是全局函数版本还是A：disp（），会引发多重声明错误。

变量名同样存在这种问题，假设有两个名称空间A和B中都定义了int型变量num，在程序的某处需要使用num，如下代码所示。

using A：num；

using B：num；

num=5；

则最后一句对num的赋值操作具有二义性，因此应合理使用using声明机制。

15.2.5　空间内的屏蔽

在一个空间内变量可见域的规则，如内部变量对外部变量的屏蔽等是和第6章中介绍的内容一致的。通过以下示例代码15.6来形象地说明这一点。

代码15.6　空间内外同名实体的屏蔽关系ShieldSample

＜---------------------------文件名：example1506.cpp--------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　extern int num=0；//外部变量num的定义性声明

04　namespace B//创建名称空间B

05　{

06　int num=2；//B中声明的num

07　}

08　namespace A//创建名称空间A

09　{

10　int num=1；//A中声明的num

11　void dispA（）

12　{

13　int num=3；

14　cout＜＜"dispA函数中的num："＜＜num＜＜endl；

15　cout＜＜"A中的num："＜＜A：num＜＜endl；

16　cout＜＜"B中的num："＜＜B：num＜＜endl；

17　cout＜＜"外部的num："＜＜：num＜＜endl；

18　}

19　}

20　int main（）

21　{

22　A：dispA（）；

23　return 0；

24　}

【代码解析】代码第13行，在名称空间A的dispA函数中声明的局部变量num屏蔽了A中的num，此时，如果要在dispA函数中的语句“int num=3；”后访问A中的num，必须使用A：num的形式。全局变量num由定义性声明语句“extern int num=0；”创建，如果要访问该全局变量，必须使用全局说明符的形式：num。

这同第6章中介绍的内容是一致的，可以说名称空间多少有些类似于代码块，或者说是种特殊的代码块，而且把整个程序也看成是一种特殊的无名名称空间，全局extern变量就在这个特殊空间里，要访问extern变量，要使用“：extern变量名”的形式。

注意

可以将namespace中的实体声明为extern型，但这并没有实际意义。例如，将代码15.6中namespace B中的num声明改为“extern int num=2”，程序输出并无变化。

15.2.6　先声明，后使用

同普通变量一样，在使用名称空间中的实体前，必须保证其有效。举例来说，在使用一个变量前，必须对该变量进行声明。例如，如果将代码15.6中名称空间A定义修改为如下代码。

namespace A//创建名称空间A

{

void dispA（）

{

int num=3；

cout＜＜"dispA函数中的num："＜＜num＜＜endl；

cout＜＜"A中的num："＜＜A：num＜＜endl；

cout＜＜"B中的num："＜＜B：num＜＜endl；

cout＜＜"外部的num："＜＜：num＜＜endl；

}

int num=1；//A中声明的num

}编译运行代码15.6，编译器将给出如下错误信息。

error C2039：'num'：is not a member of'A'

15.3　名称空间的作用域与可见性

原则上讲名称空间的作用域是全局的，但其可见域却并非如此，而且不论使用限定符还是使用using声明语句，都要求名称空间可见。就代码15.6来说，如果将namespace B的定义放在namespace A定义之后，编译器将会指出错误。using语句同样如此，如果在using声明时，namespace尚未定义，或者说namespace已经定义，但声明的实体尚未包含在此namespace中，编译器同样会指出错误，因此，名称空间同样要先定义后使用。

15.3.1　名称空间的定义策略

以代码15.6为基础，假设想实现如下功能：A：dispA函数中要访问B：num，这要求B定义在A前，同时在B中增加dispB函数，其中调用A：dispA函数，这要求A定义在B之前，如此看来，上述功能似乎不太可能实现。

实则不然，这取决于名称空间的定义策略，在前面讲过名称空间中函数的定义和实现可以分开进行，这是我们解决问题的突破口，如代码15.7所示。

代码15.7　名称空间的定义策略DefineANamespace

＜---------------------------文件名：example1507.cpp--------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　extern int num=0；//外部变量num的定义性声明

04　namespace B//创建名称空间B

05　{

06　int num=2；//B中声明的num

07　void dispB（）；

08　}

09　namespace A//创建名称空间A

10　{

11　int num=1；//A中声明的num

12　void dispA（）

13　{

14　int num=3；

15　cout＜＜"dispA函数中的num："＜＜num＜＜endl；

16　cout＜＜"A中的num："＜＜A：num＜＜endl；

17　cout＜＜"B中的num："＜＜B：num＜＜endl；

18　cout＜＜"外部的num："＜＜：num＜＜endl；

19　}

20　}

21　namespace B//创建名称空间B

22　{

23　void dispB（）

24　{

25　A：dispA（）；

26　}

27　}

28　int main（）

29　{

30　A：dispA（）；

31　B：dispB（）；

32　return 0；

33　}

输出结果如下所示。

dispA函数中的num：3

A中的num：1

B中的num：2

外部的num：0

dispA函数中的num：3

A中的num：1

B中的num：2

外部的num：0

【代码解析】代码第7行，通过将namespace B中dispB函数的原型（定义）和声明分开，使得A：disp函数中，访问B：num的操作位于B的可见域，又使得B：disp函数中，调用A：disp的操作位于A的可见域。

名称空间中的变量等实体只能声明一次，函数实现也只能有一次，但函数原型却可多次说明，常用函数原型列表来扩展名称空间的可见域，如代码15.8所示。

代码15.8　多文件namespace组织MultiFileNamespace

＜-------------------------文件名：example1508.cpp----------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　extern int num=0；//外部变量num的定义性声明

04　namespace B//创建名称空间B

05　{

06　int num=2；//B中声明的num

07　void dispB（）；

08　}

09　namespace A//创建名称空间A

10　{

11　int num=1；//A中声明的num

12　void dispA（）

13　{

14　int num=3；

15　cout＜＜"dispA函数中的num："＜＜num＜＜endl；

16　cout＜＜"A中的num："＜＜A：num＜＜endl；

17　cout＜＜"B中的num："＜＜B：num＜＜endl；

18　cout＜＜"外部的num："＜＜：num＜＜endl；

19　}

20　}

21　int main（）

22　{

23　A：dispA（）；

24　B：dispB（）；

25　return 0；

26　}

＜-------------------------------文件名：nameb.cpp----------------------------＞

27　namespace A

28　{

29　void dispA（）；//原型可以多次说明

30　}

31　namespace B//创建名称空间B

32　{

33　void dispB（）

34　{

35　A：dispA（）；

36　}

37　}

输出结果如下所示。

dispA函数中的num：3

A中的num：1

B中的num：2

外部的num：0

dispA函数中的num：3

A中的num：1

B中的num：2

外部的num：0

【代码解析】代码第33行将B：disp函数的实现放在了另一个文件nameb.cpp中，为了实现对A：dispA的调用，B：disp函数的实现必须位于namespace A的可见域内，为此，使用了下述形式对namespace A进行了“声明”，以扩大其可见域。

namespace A

{

void dispA（）；//原型可以多次说明

}

因为在example1508.cpp中已经对A：dispA函数进行了实现，因此，此处的dispA只能是原型形式，但是，nameb.cpp的namespace A中可以添加其他函数的原型甚至是实现，当然也可以添加其他变量，但不能是num，否则会出现重复定义错误。

注意

一条指导性原则就是“函数实现只能有一次，函数原型可以多次，变量声明只能有一次”，当然，名称空间中允许函数重载。

由于声明并不分配空间，所以const类变量可多次声明，但不管声明多少次，程序只维护该变量定义后的一个版本。

15.3.2　推荐用法

为了在程序的多个文件中合理使用名称空间，需要合理组织名称空间的定义，与类定义相似（但又不同，类定义中成员变量不被创建，而名称空间中的变量在声明时被创建），一种推荐的做法是将名称空间的声明接口（仅包括函数原型）放在头文件中，而将变量声明和函数实现放在单独的.cpp文件中，这样在使用头文件的地方，只要包含相应的头文件即可，如示例代码15.9所示。

代码15.9　名称空间文件组织FileOrgnization

＜--------------------------------文件名：namespaceAB.h-----------------------＞

01　namespace B//创建名称空间B

02　{

03　void dispB（）；//函数原型

04　}

05　namespace A//创建名称空间A

06　{

07　void dispA（）；//函数原型

08　}

＜--------------------------------文件名：namespaceAB.cpp---------------------＞

09　#include＜iostream＞

10　#include"namespaceAB.h"

11　using namespace std；

12　extern int num；//引用性声明外部变量

13　namespace B

14　{

15　int num=2；//B中声明的num

16　void dispB（）//函数实现

17　{

18　A：dispA（）；

19　}

20　}

21　namespace A

22　{

23　int num=1；//A中声明的num

24　void dispA（）//函数实现

25　{

26　int num=3；

27　cout＜＜"dispA函数中的num："＜＜num＜＜endl；

28　cout＜＜"A中的num："＜＜A：num＜＜endl；

29　cout＜＜"B中的num："＜＜B：num＜＜endl；

30　cout＜＜"外部的num："＜＜：num＜＜endl；

31　}

32　}

＜--------------------------------文件名：example1509.cpp---------------------＞

33　#include＜iostream＞

34　#include"namespaceAB.h"

35　using namespace std；

36　extern int num=0；//外部变量num的定义性声明

37　int main（）

38　{

39　A：dispA（）；//调用名称空间A中的dispA函数

40　B：dispB（）；//调用名称空间B中的dispB函数

41　return 0；

42　}

输出结果如下所示。

dispA函数中的num：3

A中的num：1

B中的num：2

外部的num：0

dispA函数中的num：3

A中的num：1

B中的num：2

外部的num：0

【代码解析】代码第10和34行的“namespaceAB.h”起到了接口的作用，使得名称空间A和B中的函数在example1509.cpp中使用了#include"namespaceAB.h"在文件中可见，但由于变量只能声明一次，所以对变量的访问仍要求合理控制名称空间的定义顺序。

注意

经常所说的“库函数”，函数是调用的常用手段。

15.3.3　名称空间嵌套

一个名称空间可以定义在另一个名称空间内，以单层嵌套为例，要访问内部名称空间中的实体，必须采用“外部名称空间：内部名称空间：实体名”的形式，如果是多层嵌套，还要多次使用作用域限定符，如示例代码15.10所示。

代码15.10　名称空间的嵌套MultiLevelNamespace

＜-------------------------文件名：example1510.cpp---------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　extern int num=0；//定义性声明外部变量

04　namespace A//创建名称空间A

05　{

06　int num=1；//A中声明的num

07　namespace B

08　{

09　int num=2；//B中声明的num

10　void disp（）

11　{

12　cout＜＜"嵌套类B中的num："＜＜num＜＜endl；

13　cout＜＜"嵌套类A中的num："＜＜A：num＜＜endl；

14　cout＜＜"外部全局变量num："＜＜：num＜＜endl；

15　}

16　}

17　}

18　int main（）

19　{

20　A：B：disp（）；//双重限定符

21　return 0；

22　}

输出结果如下所示。

嵌套类B中的num：2

嵌套类A中的num：1

外部全局变量num：0

【代码解析】代码第7行的名称空间B定义在名称空间A中，所以B的可见域为从其定义位置开始，到名称空间A结束。因此，如果要在其他名称空间、其他函数或全局空间中访问名称空间B，必须像使用A中实体一样使用诸如“A：B”的作用域限定形式。

从示例代码15.10中可以看出，嵌套名称空间中的创建或声明的实体屏蔽了外部空间中的实体，在B中要访问A类中的num，也要采用A：num的形式。

15.3.4　using编译指令

前面介绍过通过using声明语句使得某个空间中的特定实体可见，using编译指令比using声明更进一步，通过“using namespace名称空间名；”的形式，使得名称空间中的实体都可见，不再需要作用域限定符。

到此为止，几乎所有的示例代码都使用了下述语句。

using namespace std；这是因为所有的C++标准库函数，都定义在名称空间std下，以头文件＜iostream＞为例，可以在其中找到如下代码。

_STD_BEGIN

//OBJECTS

static ios_base：Init_Ios_init；

extern_CRTIMP istream cin；

extern_CRTIMP ostream cout；

extern_CRTIMP ostream cerr,clog；

//CLASS_Winit

class_CRTIMP_Winit{

public：

_Winit（）；

～_Winit（）；

private：

static int_Init_cnt；

}；

//WIDE OBJECTS

static_Winit_Wios_init；

extern_CRTIMP wistream wcin；

extern_CRTIMP wostream wcout,wcerr,wclog；

_STD_END

其中，_STD_BEGIN和_STD_END是两个宏，定义如下所示。

#define_STD_BEGIN namespace std{

#define_STD_END}；

从中不难看出，头文件＜iostream＞中创建了名称空间std，包含了该头文件，便可以通过名称空间限定符在当前文件中使用输入输出流对象cout和cin，形式为“std：cout”和“std：cin”，如果觉得这样书写很麻烦，可以使用using声明机制，如下所示。

using std：cout；

using std：cin；

当然，也可以使用using编译指令，即“using namespace std；”使得名称空间中当前有效的实体都可见，本书前面的示例代码中都采用了在外部使用using编译指令，使得cout和cin可以在程序中自由使用。

为什么说是当前有效的实体，因为不仅是＜iostream＞中定义了namespace std，其他标准头文件中也定义了std，根据名称空间的开放性，添加的头文件越多，std越庞大，有效的实体也越多。

对某个名称空间使用using编译指令时，同样要保证其可见和有效，不能出现先using编译，后来才创建该空间的情况，否则编译器会报错。

注意

只有新式头文件才支持名称空间特性，在旧式头文件中没有这项机制，关于头文件标准的变化请参考第2章中的相关内容介绍。

using编译指令比using声明指令更为强大，付出的代价是更容易出现二义性。因此，使用using编译指令时要合理安排，避免多重声明。同时，名称空间的提出是为了大型程序的管理，使用using编译指令虽然很省力，但并不推荐使用它，using编译后会导致名称空间失效，这不是我们想看到的。

15.3.5　未命名的名称空间

也可以通过省略名称空间的名称来创建未命名的名称空间，此时，该无名空间中的实体的可见性无法扩展（既不能采用“名称空间：实体”的形式，也不能采用using声明机制扩展），因此该实体只能在本空间内使用。

注意

在无名空间中创建的全局变量，具有全局生存期，却只能被本空间内的函数访问，它是static变量的有效替代手段。

15.4　对名称空间的思考

下面引用了当前流行的名称空间使用指导原则。

❑使用在已命名的名称空间中声明的变量，而不是使用外部全局变量。

❑使用已命名的名称空间中声明的变量，而不是静态全局变量。

❑如果开发了一个函数库或者类库，将其放在一个名称空间中。事实上C++提倡将标准函数库放在名称空间std中。

❑仅将编译指令using作为一种将旧代码转换为名称空间的权宜之计。

❑对于using声明，首先将其作用域设置为局部而不是全局。

❑首先，不要在头文件中使用using编译指令，这样会使得可用名称变得模糊，容易出现二义性；其次，包含头文件的顺序可能会影响程序的行为，如果非要使用using编译指令，建议放在所有#include预编译指令之后。

名称空间相当于是在原有名称层次的基础上又扩展了一层，而且是最外面的一层，原始的名称层次进化规则简单归纳为如图15.1所示。

图15.1中并列、代码块和类是3种组织名称实体的方式，相互包含和嵌套，在这3种方式无法满足大型软件项目管理的时候，名称空间被提出，相当于在原有基础上新增一维，给名称实体的管理带来了极大的灵活性。

图　15.1　名称层次图

15.5　小结

本章探讨了名称空间的用法，这是C++新增加的一个特性，其目的是为了减少冲突，这在大型程序组织中十分有效。使用名称空间，关键是掌握空间中实体的作用域与可见域，以及名称空间的作用域与可见域的知识，理解其实质，做到既知其然，又知其所以然。

作用域限定符：、using声明机制和using编译机制是3种常用的扩展实体可见域的方式，使名称空间中的特定实体或全部实体在声明的可见域内可用。名称空间内实体的访问规则和原来介绍的没有名称空间时的情况类似，名称空间还支持嵌套层次结构，在外部使用内层空间时，必须使用多重作用域限定符的形式。

15.6　上机实践习题

定义两个名称空间，包含一个变量和一个函数，然后在主函数中只使用该名称空间的函数和变量，并输出结果。

【提示】上述题目主要是要求读者熟悉类继承的相关知识，重点是掌握继承的概念和作用。

【关键代码】

01　#include＜iostream＞

02　using namespace std；

03　namespace A//创建名称空间A

04　{

05　int n；

06　void dispA（）

07　{

08　cout＜＜"This is A"＜＜endl；

09　}

10　}

11　namespace B//创建名称空间B

12　{

13　void dispB（）

14　{

15　A：dispA（）；//名称空间A中的dispA函数

16　cout＜＜"This is B"＜＜endl；

17　}

18　}

19　//主函数内代码

20　A：n=10；

21　B：dispB（）；//名称空间B中的dispB函数

22　cout＜＜A：n＜＜endl；

第16章　异常和错误

程序运行时可能会出现这样那样的问题，例如文件打开不成功，以及内存耗尽等，通常，通过防错编码可以应付这些问题，但要求在程序编写过程中对所有可能出现的问题进行防错处理显然是不现实的，为此C++提供了异常处理机制来解决运行错误。首先来了解编码时的防错机制。

本章主要涉及以下知识点。

❑编码时的防错：介绍abort函数和exit函数的使用。

❑异常机制：介绍在程序运行出现异常时如何处理。

❑异常发生时的内存管理：介绍发生异常时的内存管理方法。

❑auto_prt类：介绍智能指针的优点及其使用。

16.1　编码时的防错

在文件输入输出一章中已经使用了编码时的防错，举例来说，调用流对象的is_open函数，如果返回true，证明文件打开成功，否则，文件打开失败，程序报警退出，这就是基本的防错处理，比只执行open函数却不检查是否成功的代码更加完善了。

16.1.1　调用abort函数或exit函数

abort函数的原型位于头文件cstdlib中，无形参，abort函数的表现及返回值取决于不同的编译器和系统实现，一个典型实现（DOS下）是向标准错误输出流（cerr）发送“程序异常终止”（Abnormal Program Termination）消息，并终止程序，如示例代码16.1所示。

代码16.1　abort函数的使用FuncAbort

＜--------------------文件名：example1601.cpp---------------------------------＞

01　#include＜iostream＞

02　#include＜cstdlib＞

03　using namespace std；

04　int main（）

05　{

06　int x,y；

07　cout＜＜"请输入两个整数："＜＜endl；

08　cin＞＞x＞＞y；

09　if（y==0）

10　{

11　cout＜＜"错误，y为0"＜＜endl；

12　abort（）；//如果y等于0，调用abort函数

13　}

14　else

15　cout＜＜"x/y is"＜＜（x/y）＜＜endl；

16　return 0；

17　}

输出结果如下所示。

请输入两个整数：

4 2（用户输入）

x/y is 2或

请输入两个整数：

4 0（用户输入）

错误，y为0

Abnormal Program Termination

注意

代码16.1　应采用VC的release模式编译。

【代码解析】将用户输入的两个int型的值x和y作除法处理，为防止被0除的现象发生，对y的值进行判断，如果y不为0，则进行正常输出，否则在第12行调用abort函数，通知操作系统，处理失败，Abnormal Program Termination消息发送到标准错误流cerr，显示在屏幕上。

此外，还可以使用exit（int）函数，该函数并不发送错误消息，只是结束程序，并将参数表中的int值传递给操作系统或上级调用函数。

16.1.2　返回错误标志

当某些错误发生时，使用abort函数或exit函数来结束程序似乎有些过激，实际上可通过返回错误标志，让上级调用函数作出判断，如代码16.2所示。

代码16.2　返回错误标志ReturnErrorCode

＜-----------------------文件名：example1602.cpp------------------------------＞

01　#include＜iostream＞

02　#include＜cstdlib＞

03　using namespace std；

04　bool pd（int x,int y）

05　{

06　if（y==0）

07　return false；

08　return true；

09　}

10　int main（）

11　{

12　int x=0，y=0；

13　while（！pd（x,y））

14　{

15　cout＜＜"请输入两个整数："＜＜endl；

16　cin＞＞x＞＞y；

17　}

18　cout＜＜"x/y is"＜＜（x/y）＜＜endl；

19　return 0；

20　}

输出结果如下所示。

请输入两个整数：

2 0

请输入两个整数：

5 0

请输入两个整数：

4 2

x/y is 2

【代码解析】代码第4～9行，编写了pd函数对用户输入的两个整数x和y进行判断，符合x/y条件（y不等于0）时pd函数返回true，否则返回false，而后只要在main函数中判断pd返回值的真假，便可知道是否合法，提示用户不断输入，直到输入的y非0，输出x/y，程序退出。如此来看，代码16.2并没有因一次输入失败便结束整个程序，而是采取了更为友好的方式提醒用户重新输入。

16.2　异常机制

除了人为防错外，还可以使用异常机制来处理错误，异常机制提供了将控制权从程序的一部分转移到另一部分的方法，对异常处理主要由以下3部分组成。

❑使用try块。

❑异常发生时，使用throw抛出。

❑使用catch块捕获异常。

如示例代码16.3所示。

代码16.3　异常处理范例ExceptionHandling

＜-----------------------文件名：example1603.cpp------------------------------＞

01　#include＜iostream＞

02　#include＜cmath＞//使用sqrt函数要包含的头文件

03　using namespace std；

04　double calc（）

05　{

06　cout＜＜"请输入1个不小于0的数："＜＜endl；

07　double num=-1；

08　cin＞＞num；

09　if（！cin. good（））//输入失败

10　{

11　cin. clear（）；//清除流状态字

12　cin. sync（）；//清空流缓冲区

13　throw"错误，输入的不是数字"；//抛出字符串型异常

14　}

15　if（num＜0）//如果num＜0，或输入不成功，则抛出字符串类型s

16　throw"错误，请输入1个不小于0的数字"；//抛出字符串型异常

17　return sqrt（num）；

18　}

19　int main（）

20　{

21　double res=0；

22　while（true）

23　{

24　try//使用try块标记可能发生异常的代码块

25　{

26　res=calc（）；//正常的函数调用，如果异常发生，将被抛出

27　}

28　catch（const char*s）//捕获try块中抛出的异常

29　{

30　cout＜＜s＜＜endl；

31　continue；//重新循环

32　}

33　cout＜＜res＜＜endl；//没有异常发生，输出结果

34　break；//跳出循环

35　}

36　return 0；

37　}

输出结果如下所示。

请输入1个不小于0的数：

G（用户输入）

错误，输入的不是数字

请输入1个不小于0的数：

-3（用户输入）

错误，请输入1个不小于0的数字

请输入1个不小于0的数：

5（用户输入）

2.23607

【代码解析】代码提示用户输入一个非负数，计算其平方根并输出。在用户输入时，输入的字符可能不是数字，也有可能输入一个负数，而求平方根要求操作数非负，因此，在calc函数中对可能出现的错误进行了throw处理，代码第13和16行的语句将标记错误信息的字符串“错误，输入的不是数字”和“错误，请输入1个不小于0的数字”抛出。当异常处理程序（即catch块）捕获异常后，对字符型异常进行显式处理。

try块表示了发生异常的代码块，try块可以和一个或多个catch块（异常处理程序，Exception Handler）搭配使用，catch语句也有参数，其参数即是throw语句抛出异常的类型，代码16.3中抛出的两个异常都是字符串，实际上异常可以是其他类型，如int和double等内置类型，设置还可以是自定义的类对象，这在稍后会有所介绍，举抛出int型异常为例，catch语句头如下所示。

catch（int num）

{

……

}

如果try块调用的函数中有可能抛出多种类型的异常，便需要多个catch块，每个catch块捕获一种类型的异常，进行相应的处理。注意，只要在try块中抛出的异常才会被catch块捕获，代码16.3中，如果在try块外部调用calc函数，catch块无法对异常进行处理。

注意

在VC6中，代码16.3应采用release模式编译。

16.2.1　关键字throw

throw关键字表示抛出异常，这实际上是跳转语句，当程序执行到throw语句抛出异常时，直接跳出当前try块，执行对应的catch块，如果未定义捕获该异常类型的catch块时，默认的terminate函数将被执行，稍后会有相关介绍。

说明

如此看来，throw操作实际上是将本函数中无法处理的错误或异常抛出到更高级别的代码域中进行处理。

C++提供了异常规范说明机制以显式说明函数所抛出的异常，方便使用者对程序进行处理，当然，异常规范说明不是必须的，在代码16.3中便没有进行异常规范说明。

异常规范说明使用了关键字throw，函数可能抛出的所有可能异常的类型应该都被写在throw之后的括号内。“throw（类型列表）”形式的异常规范说明应写在函数参数列表的后面，如对代码16.3中定义的calc函数可添加如下异常规范说明。

double calc（）throw（const char*）

{

……

}如果需要抛出多种类型的异常，则需要将抛出的类型都写在参数列表中，如下所示。

double calc（）throw（const char*，int）

{

……

}

上述代码明确告诉函数调用者，calc函数有可能抛出字符串型的异常，也可能抛出int型的异常，如果函数所抛出的异常没有列在异常规范说明中，系统将自动调用函数unexpected，稍后会有对此函数的介绍。

注意

“throw（）”表示函数不会抛出任何异常；当省略异常规范说明时，表示函数可能抛出任何类型的异常。

16.2.2　异常处理程序

异常处理程序由一个或多个catch函数组成，每个catch函数块参数列表只能有一个参数，用于匹配由throw抛出的异常的类型，当try块中调用的函数可能抛出多种类型的异常时，需要多个catch函数形成异常捕获网，捕获所有可能的异常类型。异常处理程序紧接着try块，并且由关键字catch表示，如下所示。

try

{

可能抛出异常的一个或多个函数调用

}

catch（type1 id1）

{

处理type1类型的异常

}

catch（type2 id2）

{

处理type1类型的异常

}

catch（type3 id3）

{

处理type1类型的异常

}

程序继续

异常处理程序（catch函数）必须紧跟在try块之后，当某个异常被抛出，异常处理机制会按照catch块的代码顺序依次寻找匹配的catch块，一旦找到一个相匹配的catch块，则系统会认为该异常已得到处理。

除了后面所讲的有继承关系的异常对象外，异常处理机制并不对异常参数类型进行转换。前面讲过，如果try块中抛出的异常没有找到相应的catch块对其进行处理，将会执行系统默认的terminate函数。

注意

最好是把catch函数块的参数写成引用传递，而不是值传递，稍后会作出解释。

有时候可能需要一种能够捕获所有异常的catch块，用省略号（3个点）代替catch块的参数列表就可实现，如下所示。

catch（……）

{

cout＜＜"发生异常"＜＜endl；

}

省略号catch块能够捕获任何类型的异常，最好将其放在catch块列表的最后，避免架空其后面的异常处理器。

省略号catch块不接受任何参数，无法得到任何有关异常的信息，作为一个“全能捕获者”，省略号catch块经常用于清理资源并重新抛出所捕获的异常。重新抛出异常是指在一个catch块内部，使用不带参数的throw语句将所捕获的异常重新抛出到更高一层的语境中去处理，如下所示。

catch（……）

{

cout＜＜"异常发生"＜＜endl；

//资源释放（稍后会讲到）

throw；//重新抛出异常

}

如以下示例代码16.4所示。

代码16.4　省略号catch块和重新抛出异常CatchAll

＜----------------------文件名：example1604.cpp-------------------------------＞

01　#include＜iostream＞

02　#include＜cmath＞//使用sqrt函数要包含的头文件

03　using namespace std；

04　double calc（）

05　{

06　cout＜＜"请输入一个不小于0的数："＜＜endl；

07　double num=-1；

08　cin＞＞num；

09　if（！cin. good（））//输入失败

10　{

11　cin. clear（）；//清除流状态字

12　cin. sync（）；//清空流缓冲区

13　throw"错误，输入的不是数字"；//抛出字符串型异常

14　}

15　if（num＜0）//如果num＜0或输入不成功，则抛出字符串类型s

16　throw"错误，请输入一个不小于0的数字"；//抛出字符串型异常

17　return sqrt（num）；

18　}

19　void keepInput（）

20　{

21　double res=0；

22　try//使用try块标记可能发生异常的代码块

23　{

24　res=calc（）；//正常的函数调用，如果异常发生，将被抛出

25　}

26　catch（……）//捕获try块中抛出所有异常

27　{

28　cout＜＜"异常重新抛出"＜＜endl；

29　throw；//重新抛出

30　}

31　cout＜＜res＜＜endl；//没有异常发生，输出结果

32　}

33　int main（）

34　{

35　while（true）//无限循环，直到break退出

36　{

37　try//使用try块标记可能抛出异常的代码块

38　{

39　keepInput（）；//函数调用

40　}

41　catch（const char*s）

42　{

43　cout＜＜s＜＜endl；//捕捉keepinput函数中重新抛出的异常

44　continue；//继续循环

45　}

46　break；//运算成功，退出

47　}

48　return 0；

49　}

输出结果如下所示。

请输入一个不小于0的数：

A

异常重新抛出

错误，输入的不是数字

请输入一个不小于0的数字：

-5

异常重新抛出

错误，请输入一个不小于0的数字

请输入一个不小于0的数字：

2

1.41421

【代码解析】在keepinput函数中调用了calc函数，为了处理calc函数中抛出的异常，使用了try+catch块的异常处理机制，在main函数中，对keepinput函数的调用也是通过try+catch异常机制来完成的，代码第26行的keepinput函数中使用了省略号catch块，这将捕获calc函数抛出的所有类型的异常，同时，在keepinput函数中，采用了无参throw将异常重新抛出到更高一层范围内，也就是main函数try块后紧跟的catch块列表，由main函数的catch块来捕捉异常并进行处理。

16.2.3　自定义异常对象

抛出类型可以是自定义的类对象，这样做的优点一方面在于可以使用不同的异常类型来区分不同函数在不同情况下引发的异常，另一方面，自定义的类对象可用来传递信息，如以下示例代码16.5所示。

代码16.5　自定义异常对象DefineAnException

＜----------------------文件名：example1605.cpp-------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class Exception1

04　{

05　public：

06　void disp（）

07　{

08　cout＜＜"Exception1异常被引发"＜＜endl；

09　}

10　}；

11　class Exception2

12　{

13　public：

14　Exception2（const Exception1＆）//以Exception1来复制构造Exception2

15　{

16　}

17　void disp（）

18　{

19　cout＜＜"Exception2异常被引发"＜＜endl；

20　}

21　}；

22　void fun（）

23　{

24　throw Exception1（）；//抛出Exception1类型的异常（默认的构造函数）

25　}

26　int main（）

27　{

28　try

29　{

30　fun（）；

31　}

32　catch（Exception2＆Ex）//引用传递而不是值传递

33　{

34　Ex. disp（）；

35　}

36　catch（Exception1＆Ex）

37　{

38　Ex. disp（）；

39　}

40　return 0；

41　}

输出结果如下所示。

Exception1异常被引发

【代码解析】代码中首先定义了两个类Exception1和Exception2，函数fun不进行任何的操作，仅仅是抛出了Exception1类的对象，在对象创建的时候，使用的是系统默认的无参构造函数，在main函数中有两个catch块来捕获异常，前一个用以匹配Exception2类型，后一个用以匹配Exception1类型，这里有个疑问是在代码第14行Exception2类中定义了一个复制构造函数，其参数是Exception1类对象，那异常处理机制是否会自动将fun函数中抛出的Exception对象转换成一个Except2对象，使得第一个catch块被匹配呢？答案是否定的，异常处理机制在处理异常的过程中不会做任何形式的转换，只寻找和参数最匹配的catch块进行处理，异常将被第二个catch语句捕获，当然有种特殊情况，那就是类继承。

16.2.4　有继承关系的类异常

在有继承关系的类异常捕获处理上，有条原则为对某个catch块来说，如果其参数列表中是基类的类型，则其总是可以捕获基类和其派生类的异常对象，而如果catch块的参数列表中是派生类型，不能捕获基类的异常对象。

因此，为避免基类垄断的局面，推荐将用于捕获派生类的catch块写在上面，用于捕获基类的catch块写在下面，这保证了派生类先找到与其匹配的异常处理catch块，而最后再由基类找到与其匹配的catch块。

如示例代码16.6所示。

代码16.6　有继承关系的类异常处理ExceptionInheritance

＜---------------------文件名：example1606.cpp--------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class Exception1

04　{

05　}；

06　class Exception2：public Exception1//派生类定义

07　{

08　}；

09　void fun（）

10　{

11　cout＜＜"请输入一个整数大于0，基类异常被抛出，

12　在其他情况下，派生类异常被抛出"＜＜endl；

13　int num=0；

14　cin＞＞num；

15　if（num＞0）

16　throw Exception1（）；//抛出Exception1类型的异常（默认的构造函数）

17　else

18　throw Exception2（）；//抛出Exception2类型的异常（默认的构造函数）

19　}

20　int main（）

21　{

22　try

23　{

24　fun（）；

25　}

26　catch（Exception1＆Ex）

27　{

28　cout＜＜"由基类异常处理程序捕获"＜＜endl；

29　}

30　catch（Exception2＆Ex）//引用传递而不是值传递

31　{

32　cout＜＜"由派生类异常处理程序捕获"＜＜endl；

33　}

34　return 0；

35　}

输出结果如下所示。

请输入一个整数大于0，基类异常被抛出，在其他情况下，派生类异常被抛出

-3（用户输入）

基类异常被引发

【代码解析】代码中即使输入了小于0的数-3，仍然是由基类异常处理程序来捕获的，这是因为代码16.6写错了catch块的顺序，因为代码第26行的catch（Exception1＆Ex）不仅可以捕获基类Exception1的对象，同样可以捕获派生类Exception2的对象，后面的仅用于捕获派生类对象的catch（Exception2＆Ex）语句实际上被架空了，解决的办法是将两个catch块的顺序进行调换。

16.2.5　terminate函数和set_terminate函数

如果try块抛出的异常没有任何一个catch块能与之匹配，一个特殊的库函数terminate（在头文件＜exception＞中定义）会被自动调用。默认情况下，terminate函数会调用标准C库函数abort使程序执行异常而退出。

通过使用＜exception＞头文件中提供的set_terminate函数，可以自定义terminate函数，set_terminate函数返回被替换的指向terminate函数的指针（第一次调用set_terminate函数时，返回函数库中默认的terminate函数的指针），这样就可以在需要的时候恢复以前的terminate函数。自定义的terminate函数不能有参数，返回值必须是void，而且，必须无条件服从某个结束语句，如调用exit函数或abort函数结束整个程序。换言之，如果terminate函数被调用，意味着问题已无法解决，情况已不可收拾，必须结束程序。

如示例代码16.7所示。

代码16.7　terminate函数和set_terminate函数的用法FuncTerminate

＜---------------------文件名：example1607.cpp--------------------------------＞

01　#include＜exception＞//必须的头文件

02　#include＜iostream＞

03　using namespace std；

04　void Ownterminate（）//自定义Ownterminate函数

05　{

06　cout＜＜"严重错误，退出"＜＜endl；

07　exit（0）；//无条件退出

08　}

09　void（*old_terminate）（）=set_terminate（Ownterminate）；//设置新的Ownteminate函数

10　//返回之前teminate函数指针

11　void fun（）

12　{

13　throw"error"；//抛出字符串类型的异常

14　}

15　int main（）

16　{

17　try

18　{

19　fun（）；

20　}

21　catch（int x）//只定义了捕获int型异常的函数

22　{

23　cout＜＜x＜＜endl；

24　}

25　return 0；

26　}

输出结果如下所示。

严重错误，退出

【代码解析】代码第4行，自定义了Ownterminate函数，代码第9行使用语句“void（*old_terminate）（）=set_terminate（Ownterminate）；”使用Ownterminate函数代替了之前的terminate函数，使用函数指针old_terminate存储了之前的terminate函数，以备恢复时使用。这样，当try块抛出的异常类型找不到与之匹配的catch块时，Ownterminate函数将被触发，输出告警信息“严重错误，退出”，如前所述如果teminate函数（无论是库函数，还是自定义函数）被触发，意味着程序已无法挽救，必须无条件退出，代码16.7中调用了函数“exit（0）”结束程序。

16.2.6　unexpected函数与set_unexpected函数

当函数头中存在异常规范说明时，如果函数所抛出的异常没有列在异常规范说明中，系统将自动调用库函数unexpected，该函数默认调用terminate函数结束程序。同时，还可以调用set_unexpected库函数自定义unexpected函数，这同前面所讲的terminate函数类似，同样在使用unexpected函数和set_unexpected函数时也须包含头文件＜exception＞。

如示例代码16.8所示。

代码16.8　unexpected函数与set_unexpected函数FuncUnexpected

＜-----------------------文件名：example1608.cpp------------------------------＞

01　#include＜exception＞

02　#include＜iostream＞

03　using namespace std；

04　void Ownunexpected（）//自定义Ownunexpected函数

05　{

06　cout＜＜"抛出了未知异常"＜＜endl；

07　exit（0）；//无条件退出

08　}

09　void（*old_unexpected）（）=set_unexpected（Ownunexpected）；//设置新的Ownunexpected函数

10　//并返回之前unexpected函数指针

11　void fun（）throw（int）//异常规范说明中指出：抛出的异常类型为int

12　{

13　throw"error"；//抛出字符串类型的异常

14　}

15　int main（）

16　{

17　try

18　{

19　fun（）；

20　}

21　catch（const char*s）//捕获字符串型异常的catch块

22　{

23　cout＜＜s＜＜endl；

24　}

25　return 0；

26　}

输出结果如下所示。

抛出了未知异常

【代码解析】代码中在fun函数定义时进行了异常规范说明，fun函数抛出的异常类型为int，如果在fun函数中有其他类型的异常被抛出，默认的unexpecetd函数将被执行，当然，通过set_unexpected函数可以将库函数提供的unexpected函数进行替换，如代码第9行便使用语句“void（*old_unexpected）（）=set_unexpected（Ownunexpected）；”，用自定义的Ownunexpected函数替换了默认的库函数unexpected，并返回了之前的函数指针以备恢复时使用。自定义的unexpected函数格式要求和自定义的terminate函数一样，无参且返回值必须为void。

注意

代码16.8　中的编译应在Eclipse环境中进行，在VC6和VC 2005中都没有对异常规范说明进行支持，VC编译器接受异常规范说明（不报错）但并不实现此规范，唯一的解决办法是等待更高版本的编译器出现。

16.2.7　标准异常

头文件＜exception＞中定义了exception标准异常类，可用它作为其他异常类的基类。头文件＜stdexcept＞中定义了其他几个异常类，主要有logic_error和runtime_error两个，它们都是从exception类public派生而来。

logic_error用于描述程序中出现的逻辑错误，如传递无效的参数等，一般可通过编程修复。runtime_error描述了可能在运行期间发生但难以预料的错误，例如硬件故障或者内存耗尽，这种异常一般无法避免，会导致程序失败。logic_error和runtime_error都提供了一个参数类型为std：string的构造函数，这样就可以把消息保存到这两种类型的异常对象中，通过exception：what成员函数，可以从对象中得到它所保存的信息，what函数的原型如下所示。

virtual const char*what（）；

如果自定义的类从exception类派生而来，通过定义虚成员函数what可返回一个字符串，指示异常信息。

在logic_error和runtime_error两个类的基础上，派生了其他几个标准异常类，供函数抛出，用以细分可能出现的情况，如表16.1所示。

因为exception类是上述所有异常类的基类，所以一个快捷的办法就是使用基类的对象作为catch块的参数，来捕获所有的标准异常类异常，如代码16.9所示。

代码16.9　标准异常类的使用ExceptionSample

＜-----------------------文件名：example1609.cpp------------------------------＞

01　#include＜stdexcept＞//必须的头文件

02　#include＜iostream＞

03　using namespace std；

04　void fun（int x）

05　{

06　if（x＜0）

07　throw invalid_argument（"参数传递错误"）；//抛出了标准异常类对象

08　}

09　int main（）

10　{

11　try

12　{

13　fun（-3）；

14　}

15　catch（exception＆e）//使用基类（exception类）捕获所有派生类异常

16　{

17　cout＜＜e. what（）＜＜endl；

18　}

19　return 0；

20　}

输出结果如下所示。

参数传递错误

【代码解析】代码第7行的fun函数中抛出了invalid_argument标准异常类对象，在main函数中，只使用参数为exception类对象的catch块便可以捕获所有直接派生和间接派生自exception的异常。

从exception中还可以派生由于C++语言特性而抛出的异常，例如new抛出bad_alloc、dynamic_cast抛出bad_cast以及typeid抛出bad_typeid。先来看一下关于new运算符的处理，其他两个将在RTTI一章中进行介绍。

在使用运算符new申请动态内存时，有可能会失败。之前讲过如果内存未申请成功，返回的指针为null空指针，通过判断返回指针是否为空可进行防错处理。实际上，头文件＜new＞中声明了bad_alloc异常类，同样是从exception类public派生而来的，注意bad_alloc类对象是由new运算符自动抛出的，如下所示。

try

{

int*p=new int[1000]；

}

catch（bad_alloc＆ba）

{

//当上述try块中内存申请失败时，异常被捕获

}

若try块中动态内存申请失败，bad_alloc异常类对象将会被自动抛出，由相应的catch函数捕获并进行处理。

16.2.8　对unexpected函数的补充

对unexpected函数来说，除了如代码16.8中Ownunexpected函数定义中采用的“exit（0）”结束整个程序外，自定义的unexpected函数还可以抛出异常，如下所示。

（1）如果新抛出的异常符合原函数的异常规范说明，与之匹配的catch块被执行，如下所示。

void fun（）throw（const char*）

{

throw 0；//抛出了int型，这将导致unexpected函数被执行

}自定义的unexpected处理函数可抛出符合原函数规范说明的异常，如下所示。

void Myunexpected（）

{

throw"error"；//抛出字符串型异常

}

在Myunexpected函数中抛出的异常将被字符串型catch块捕获处理。

（2）如果新抛出的异常不符合原函数的异常规范说明，而且异常规范说明中没有包括std：bad_exception类型，则程序将调用terminate函数，结束程序，terminate函数定义在头文件exception中，它是从exception类派生而来。

（3）如果新抛出的异常不符合原函数的异常规范说明，而且异常规范说明中包括std：bad_exception类型，不匹配的类型将被std：bad_exception异常取代，如代码16.10所示。

代码16.10　unexpected函数抛出异常ThrowByFuncUnexpected

＜-----------------------文件名：example1610.cpp------------------------------＞

01　#include＜exception＞

02　#include＜iostream＞

03　using namespace std；

04　void Ownunexpected（）//自定义Ownunexpected函数

05　{

06　cout＜＜"抛出了未知异常"＜＜endl；

07　throw"error"；//或仅仅用throw也可

08　}

09　void（*old_unexpected）（）=set_unexpected（Ownunexpected）；//设置新的Ownunexpected函数，

10　//并返回之前unexpected函数指针

11　void fun（）throw（int,bad_exception）//异常规范说明中指出：抛出的异常类型

12　//为int型，包含了bad_exception

13　{

14　throw"error"；//抛出字符串类型的异常

15　}

16　int main（）

17　{

18　try

19　{

20　fun（）；

21　}

22　catch（const char*s）//捕获字符串型异常的catch块

23　{

24　cout＜＜s＜＜endl；

25　}

26　catch（exception＆e）//使用exception类捕获派生类异常

27　{

28　cout＜＜e. what（）＜＜endl；

29　}

30　return 0；

31　}

输出结果如下所示。

抛出了未知异常

St13bad_exception

注意

代码16.10　的编译和运行同样基于Eclipse环境，VC编译器未实现异常规范说明。

【代码解析】代码第11行的fun函数的异常规范说明中包含了bad_exception，因此，当Ownunexpected函数中抛出的异常仍然和原来的规范说明不匹配时，Ownunexpected函数中新抛出的异常会被自动转换成bad_exception异常，被“catch（exception＆e）”捕获。

16.3　异常发生时的内存管理

异常的发生使得程序的流程发生改变，原有的一些变量和类对象等的撤销操作会不会受影响，是否会有内存泄露，这是本节要讨论的内容。

16.3.1　堆栈解退

在函数一章中提到，当函数执行结束后，在其中声明的局部变量会被自动撤销，自动类对象的析构函数会被自动执行并释放资源。如果函数因为异常而终止，从throw语句到try语句之间所有的局部变量会被自动撤销，自动类对象的析构函数被自动执行，这种机制叫做堆栈引退。如示例代码16.11所示。

代码16.11　堆栈解退示例StackSample

＜-----------------------文件名：example1611.cpp------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class MyEx//MyEx类定义

04　{

05　int num；

06　public：

07　MyEx（int n）

08　{

09　num=n；

10　}

11　～MyEx（）

12　{

13　cout＜＜num＜＜"，MyExX析构"＜＜endl；

14　}

15　}；

16　void fun（）

17　{

18　int num=8；

19　throw"error"；//抛出异常

20　}

21　void call（）

22　{

23　MyEx m1（5）；//try块外定义的MyEx类对象

24　try

25　{

26　MyEx m2（6）；//try块内定义的MyEx类对象

27　fun（）；

28　}

29　catch（const char*s）

30　{

31　cout＜＜s＜＜endl；

32　}

33　}

34　int main（）

35　{

36　call（）；

37　return 0；

38　}

输出结果如下所示。

6，MyExX析构

error

5，MyExX析构

【代码解析】当fun函数执行到第19行的throw语句抛出异常时，fun函数终止，此时，fun函数内的int型变量num被撤销，但撤销的不只是fun函数内的变量，从fun函数的throw语句到try块之间的局部变量都会被撤销，包括第26行的try块内定义的MyEx类对象m2，所以输出结果的第一行为“6，MyExX”，而后程序转到匹配catch块执行相应操作，输出“error”，在catch块执行结束后，call函数结束，call函数中定义的MyEx对象m1被撤销，其析构函数被调用。

16.3.2　异常处理机制与函数的不同

异常处理机制与函数的不同主要体现在以下3个方面。

1.堆栈引退

代码16.11　直观地体现了两者的不同，对函数来说，执行结束后只有本函数中定义的局部变量和类对象被撤销，但对throw-catch异常处理机制来说，从throw语句到try语句之间所有的局部变量和类对象都会被自动撤销。

2.控制权返回

函数执行结束后，控制权交回给上级调用函数，但throw语句将控制权向上返回到第一个这样的函数，即包含能捕获相应异常的try-catch组合，这可能一下跨越了多个调用层次，如以下代码所示。

void fun1（）

{

throw"error"；//fun1函数中抛出异常error

}

void fun2（）

{

fun1（）；//fun2函数中调用fun1

}

void fun3（）

{

fun2（）；//fun3函数中调用fun2

}

void fun4（）

{

try

{

fun3（）；

}

catch（const char*s）

{

cout＜＜s＜＜endl；

}

}

执行到fun1函数中的throw语句时，控制权将直接返回到fun4函数。

3.catch块的参数

引发异常时，编译器总是创建一个临时副本，即使异常规范和catch块参数中指定的是引用。这是因为抛出的对象在throw语句执行时，控制权返回后就被撤销了。那么既然throw语句将生成副本，为什么还要在catch块参数中使用引用呢？这并非出于效率的考虑，而是通过引用与虚函数机制，使得基类引用可以调用派生类对象的成员函数。

16.3.3　构造函数中抛出异常

对局部类对象来说，如果其构造函数没有完全执行，便由throw语句退出，其析构函数不会被执行，这可能会造成一定程度的内存泄露和资源浪费，如代码16.12所示。

代码16.12　构造函数中抛出异常ThrowByAConstructor

＜---------------------文件名：example1612.cpp--------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class OnlyOne

04　{

05　static bool isOne；//静态成员isOne

06　int num；

07　public：

08　OnlyOne（int n）

09　{

10　num=n；

11　cout＜＜num＜＜"，进入构造函数"＜＜endl；

12　if（isOne）//如果已经定义了一个示例，抛出异常

13　throw"已经有一个实例了"；

14　isOne=true；

15　cout＜＜num＜＜"，构造函数执行完毕"＜＜endl；

16　}

17　～OnlyOne（）

18　{

19　cout＜＜num＜＜"，析构函数被执行"＜＜endl；

20　}

21　}；

22　bool OnlyOne：isOne=false；//初始化为false

23　int main（）

24　{

25　try

26　{

27　OnlyOne x（5）；

28　OnlyOne y（6）；

29　}

30　catch（const char*s）

31　{

32　cout＜＜s＜＜endl；

33　}

34　return 0；

35　}

输出结果如下所示。

5，进入构造函数

5，构造函数执行完毕

6，进入构造函数

5，析构函数被执行

已经有一个实例了

【代码解析】代码第5行OnlyOne类中定义了一个静态成员isOne，初始化为false，如果创建了一个对象后，在代码第14行将其更改为true，在下次再创建对象时，构造函数便会抛出异常“已经有一个实例了”。从输出结果可以看出，在执行catch块之前，类对象x的析构函数被执行，而y由于构造函数执行不完全，其析构函数得不到执行。

16.3.4　内存泄露

尽管堆栈解退机制将调用局部类对象的析构函数并释放了资源，但在某些情况下，仍然会发生内存泄露，如下述代码所示。

void fun（）

{

int*p=new int[1000]；

if（condition）

throw"error"；

delete[]p；

}

一旦condition为真，抛出异常，指针变量p将被删除，但其指向的堆内存却不会被自动释放，造成了内存泄露。解决这个问题的方法有很多种。

（1）在fun函数中捕获该异常，进行一些清理工作，而后将异常转抛，如下所示。

void fun（）

{

int*p=new int[1000]；

try

{

if（condition）

throw"error"；

}

catch（const char*s）

{

delete[]p；

throw；

//重新抛出

}

delete[]p；

}

（2）所有事物都抽象为对象，使得每次对对象的资源分配具有原始性，即避免动态分配直接暴露，将其封装在某个类中，该类的构造函数申请动态内存，而析构函数释放该动态内存。

（3）使用auto_ptr模板，本章稍后会进行介绍。

16.3.5　析构函数中可否抛出异常

对局部类对象来说，如果成员函数中抛出异常，在处理该异常之前，该对象要先进行析构，才开始执行匹配catch块，可如果在析构函数中也抛出了异常，会出现不可预料的错误，如示例代码16.13所示。

代码16.13　析构函数中抛出异常ThrowByADestructor

＜----------------------文件名：example1613.cpp-------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　void Myterminate（）//自定义Myterminate函数

04　{

05　cout＜＜"错误，退出"＜＜endl；

06　exit（0）；

07　}

08　void（*old）（）=set_terminate（Myterminate）；//用自定义的Myterminate函数替代原有库函数

09　class Ex

10　{

11　public：

12　void fun（）//成员函数中抛出异常

13　{

14　throw"Error in fun（）"；

15　}

16　～Ex（）//析构函数中同样抛出异常

17　{

18　throw 0；

19　}

20　}；

21　int main（）

22　{

23　try

24　{

25　Ex e；

26　e. fun（）；

27　}

28　catch（……）//可接收任何类型的异常

29　{

30　cout＜＜"Hello"＜＜endl；

31　}

32　return 0；

33　}

输出结果如下所示。

错误，退出

【代码解析】代码的输出结果有些出人意料，尽管采用了省略号catch块，看起来应该能够捕获所有异常，但结果是terminate函数总是会被调用，这是因为两个异常接连被抛出的缘故，当第26行“e.fun（）；”中抛出异常时，根据堆栈解退机制，自动类对象e将会被撤销，其析构函数被执行，即代码第16行，此时又将抛出一个新的异常，在这种情况下，terminate函数将被执行。

技巧除非特殊需要，不推荐在析构函数中抛出异常，否则，这很有可能成为程序的致命伤。

16.4　auto_ptr类

auto_ptr类是个模板类，auto_ptr又常常被称为智能指针，它巧妙地利用C++退出作用域时会自动释放变量的机制，来清理其维护的对象。根据前面的内容可知，用指针申请动态内存时，如果不先对动态内存进行释放就销毁指针，会造成内存泄露。那有没有可能引入一种机制，当指针被撤销时，其指向的动态内存会被自动释放，这样不仅可以避免程序员遗忘带来的内存泄露，也使得程序的结构更为优雅，而且在某些场合下，程序员很难判断到底在什么位置对内存进行释放较为合适，如下所示。

char*GetMem（int n）

{

char*p=new char[100]；

return p；

}

上述函数用于申请一块动态内存，并返回指向该块内存的指针，当申请不成功时，返回null，申请到的内存块是供外部使用的，何时释放也取决于具体的需要，有时候很难判断，这很容易造成内存泄露，给程序带来安全隐患。

如何做到这一点呢？使用普通的指针显然是不行的，那能否对其进行封装，这样在对象过期时，析构函数被调用，释放其指向的内存呢？这也是auto_ptr类模板的由来。

16.4.1　使用auto_ptr类模板

使用auto_ptr时，需要使用头文件memory，该文件中定义了auto_ptr类模板，在程序中只要对其实例化即可使用，其定义如下所示。

template＜class T＞

class auto_ptr

{

public：

explicit auto_ptr（X*p=0）throw（）；

……

}举例如下所示。

auto_ptr＜int＞pi（new int）；

auto_ptr＜string＞ps（new string）；

这样，可将auto_ptr实例化后创建的类对象pi和ps当成一种“高级指针”或“智能指针”来看，不用再对动态内存进行delete释放，对应内存的清理工作在pi和ps被撤销时自动执行。

auto_ptr中的构造函数是显式（Explicit）的，因此，下列隐式转换都是非法的。

int*p=new int；

auto_ptr＜int＞pi；//pi用空指针初始化

pi=p；//错误，隐式转换

及

auto_ptr＜int＞pi=p；//错误，隐式转换除此之外，完全可以把auto_ptr实例化后的对象当成指针来用，可以对其间接引用（dereference，如*pi），如果类型T是某个类，还可以使用auto_ptr访问其成员（-＞），可将其赋给常规类型的指针，如下所示。

auto_ptr＜int＞pi（new int）；

int*p=pi；

此外，auto_ptr还支持同类型间的赋值，但和普通的赋值有所不同，把一个auto_ptr赋值给另一个auto_ptr，前一个auto_ptr就会变成null值，这在稍后会进行讲解。

16.4.2　关于auto_ptr的若干问题

auto_ptr不是万金油，在使用这种“智能指针”时需要特别注意以下问题。

1.auto_ptr只能用于堆内存的管理

auto_ptr类中使用delete释放其管理的内存区域，这就要求只能用堆内存（也就是new申请的内存）来初始化auto_ptr对象，下述用法显然是不合法的。

string sz（"Hello,C++"）；

auto_ptr＜string＞ap（＆sz）；

2.auto_ptr对象赋值

如下代码所示。

auto_ptr＜string＞ap1（new string（"Hello"））；

auto_ptr＜string＞ap2=ap1；

显然如果仅仅是指针赋值（浅复制），ap1和ap2维护的是同一块动态内存，当ap1和ap2都过期时，会试图删除同一块内存两次，这会引发错误。

为解决这个问题，auto_ptr类模板采用了“所有权”的概念，对同一块堆内存（即同一个对象），只能有一个auto_ptr对象拥有它，赋值操作会转移所有权，如下例所示。

ap2=ap1；

执行该赋值操作后，ap1便不再维护原来的堆内存，而将维护权转让给了ap2，此时ap1的值为null。

3.auto_ptr不能直接用于管理动态数组

auto_ptr中使用的delete，而不是delete[]，因此auto_ptr不能直接用于动态数组的申请，如果需要对动态数组使用auto_ptr智能指针，可对auto_ptr进行扩充，如下所示。

template＜class T,int num＞

class auto_array_ptr

{

auto_ptr＜T＞ap[num]；

//添加对[]操作符的重载

}；

16.5　小结

本章对C++的异常和错误处理机制进行了讨论，很好地处理异常和错误能使程序更为完善，在程序设计时需要对代码可能出现的软硬件问题进行仔细的考虑，做好防错处理和出错补救。C++的throw-catch异常处理机制是个完整的体系，用以将异常抛出到更大的范围内统筹考虑，使得代码的结构更为清晰，异常处理更为方便。

关键字throw用于抛出异常，这将中断函数的执行，使得代码流程发生跳转，抛出的异常将被与try块并列的catch语句列表捕获，进行相应的处理，如果没有与异常匹配的catch块，库函数terminate将自动触发，结束整个程序，通过set_terminate库函数可以自定义terminate函数，同时，在函数定义时，为方便函数的使用者，C++引入了异常规范说明机制，明确限定了函数所抛出异常的类型，如果函数抛出的异常不在异常规范说明中，库函数unexpected将自动触发，同样，使用库函数set_unexpected可以自定义unexpected函数。

最后，本章简要介绍了auto_ptr智能指针的用法，同普通的指针不同，在智能指针对象过期被销毁时，其维护的（或说指向的）动态内存会被自动释放，这有效地解决了内存泄露的问题。

16.6　上机实践习题

1.尝试自定义一个对于除法中除数为0或者不是数字的异常抛出，并在上级函数中进行捕获，最后输出异常结果。

【提示】上述题目主要是要求读者熟悉异常机制处理的相关知识，重点是掌握异常机制的概念及其使用。

【关键代码】

01　#include＜iostream＞

02　#include＜cmath＞//使用sqrt函数要包含的头文件

03　using namespace std；

04　int calc（）

05　{

06　cout＜＜"请输入被除数和除数："＜＜endl；

07　int n=0；

08　int m=0；

09　cin＞＞n＞＞m；

10　if（！cin. good（））//输入失败

11　{

12　cin. clear（）；//清除流状态字

13　cin. sync（）；//清空流缓冲区

14　throw"错误，输入的不是数字"；//抛出字符串型异常

15　}

16　if（m==0）//如果num＜0，或输入不成功，则抛出字符串类型s

17　throw"错误，除数不能为0"；//抛出字符串型异常

18　return n/m；

19　}

20　//主函数处理

21　int res=0；

22　while（true）

23　{

24　try//使用try块标记可能发生异常的代码块

25　{

26　res=calc（）；//正常的函数调用，如果异常发生，将被抛出

27　}

28　catch（const char*s）//捕获try块中抛出的异常

29　{

30　cout＜＜s＜＜endl；

31　continue；//重新循环

32　}

33　cout＜＜res＜＜endl；//没有异常发生，输出结果

34　break；//跳出循环

35　}

2.自定义一个terminate函数，并将其设置为默认异常处理函数，最后输出异常结果。

【提示】上述题目主要是要求读者掌握terminate函数的相关知识，重点是掌握terminate函数的概念及其使用。

【关键代码】

01　#include＜exception＞//必须的头文件

02　#include＜iostream＞

03　using namespace std；

04　void Ownterminate（）//自定义Ownterminate函数

05　{

06　cout＜＜"严重错误，退出"＜＜endl；

07　exit（0）；//无条件退出

08　}

09　void（*old_terminate）（）=set_terminate（Ownterminate）；//设置新的Ownteminate函数

10

11　void fun（）

12　{

13　throw"error"；

14　}

15　//主函数实现

16　try

17　{

18　fun（）；

19　}

20　catch（int x）

21　{

22　cout＜＜x＜＜endl；

23　}

第17章　RTTI和类型转换操作符

运行时类型识别（RunTime Type Identification,RTTI），它是C++中相对较新的特性，一些老式的编译器可能不支持，不同编译器的实现方法也不尽相同。

本章主要涉及以下知识点。

❑RTTI机制：介绍关于程序运行时类型识别的相关知识。

❑类型转换操作符：介绍常用的几种类型转换操作符。

17.1　RTTI机制

C++是一种静态类型语言。其数据类型是在编译期就确定的，不能在运行时更改。在第11章中讲述了多态的概念，虚函数的使用使得动态联编成为可能。例如存在一个类层次结构A-＞B-＞C，结构定义如下所示。

class A{……}；//包含虚函数

class B：public A{……}；

class C：public B{……}；

此时，可以直接用派生类（B类或C类）对象为A类指针赋值，而且，通过该A类指针调用虚成员函数时，调用的版本是为其赋值的派生类对此虚函数的覆盖定义。当然，不可能把所有函数都定义成虚函数，对派生类中定义的普通函数来说，使用A类指针调用该函数是否合法需要具体分析。

先来看如下的强制类型转换机制。

A*pa=new B；

B*pb=（B*）pa；//直接赋值编译器会报错，需要强制转换上述代码是很安全的，因为pa指向的堆空间中存储的恰好为B类对象，此时使用pb调用B类中定义的非虚函数不会出错，另一个转换如下所示。

A*pa=new A；

B*pb=（B*）pa；

虽然编译器不会报错，但上述代码明显不安全并存在问题，此时使用pb调用B类中调用的非虚函数必然会出错。

注意

虚函数调用只取决于是什么类型的变量为指针赋值，而与指针类型无关，因此不涉及类型判断问题。

17.1.1　dynamic_cast操作符

使用派生类对象为基类指针赋值是安全的，可什么时候使用基类指针为派生类指针赋值是安全的呢？C++提供了操作符dynamic_cast，其语法如下所示。

Tson*p1=dynamic_cast＜Tson*＞（pbase）；

其中pbase是基类指针，Tson是派生类型，如果pbase指向的对象是Tson型或Tson的派生类型，指针转换成功，否则p1为null，即空指针，如示例代码17.1所示。

代码17.1　dynamic_cast的使用DynamicCastSample

＜---------------------------文件名：example1701.cpp---------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　class A//基类定义

04　{

05　public：

06　virtual void disp（）//虚函数

07　{

08　cout＜＜"disp（）in A"＜＜endl；

09　}

10　void printA（）//普通成员函数

11　{

12　cout＜＜"print（）in A"＜＜endl；

13　}；

14　}；

15　class B：public A//派生类

16　{

17　public：

18　void disp（）//虚函数覆盖

19　{

20　cout＜＜"disp（）in B"＜＜endl；

21　}

22　void printB（）//普通成员函数

23　{

24　cout＜＜"print（）in B"＜＜endl；

25　}

26　}；

27　int main（）

28　{

29　A*pa=new B；

30　B*pB=dynamic_cast＜B*＞（pa）；//转换是安全的

31　if（pB！=NULL）

32　{

33　pB-＞printB（）；

34　}

35　else

36　cout＜＜"转换不安全，退出"＜＜endl；

37　A*p1=new A；

38　B*p2=dynamic_cast＜B*＞（p1）；//转换并不安全

39　if（p2！=NULL）

40　{

41　p2-＞printB（）；

42　}

43　else

44　cout＜＜"转换不安全，退出"＜＜endl；

45　return 0；

46　}

输出结果如下所示。

print（）in B

转换不安全，退出

注意

虽然VC6编译器支持RTTI，但默认情况下，该特性是关闭的，因此，在编译代码17.1时，需要将该特性打开，否则程序虽可通过编译，但运行中会报错。打开方式为：单击“Project”菜单，执行“Setting”命令或直接按快捷键“Alt+F7”，在弹出的“Project Setting”对话框中，选择C/C++页，单击“Enable Runtime Type Information”前的复选框启动该特性。

【代码解析】代码第30和38行演示了dynamic_cast操作符的使用，在使用基类指针为派生类指针赋值时，编译器会根据基类指针实际指向的对象来决定转换是否安全，如果转换不安全，则停止该转换，将指针置为NULL。相比之下，普通的强制类型转换不会进行安全性检查，容易出现运行错误。

dynamic_cast也可用于引用，由于没有与空指针对应的引用，因此，当转换不安全时，dynamic_cast将抛出bad_cast异常，该异常是从exception类派生而来的，定义在头文件typeinfo中，使用格式如下所示。

#include＜typeinfo＞//包含要用的头文件

……

try//try块用以标记可能抛出异常的区域

{

Tson＆rs=dynamic_cast＜Tson＆＞（rbase）；//引用的转换

……

}

catch（bad_cast＆）//异常捕获机制

{

……

}

其中，Tson是派生类型，rbase是基类引用，当基类引用的对象是Tson类型或Tson类的派生类时转换成功，否则转换失败，抛出异常。

17.1.2　typeinfo类和typeid操作符

在头文件typeinfo中还定义了typeinfo类和typeid操作符，从typeid的字面即可看出，该操作符用以返回类的id，即类型信息，其基本调用格式如下所示。

typeinfo＆typeid（expr）；

参数expr可以是单个对象，也可以是以返回结果为对象的表达式，还可以是类名，typeid返回一个typeinfo对象的引用，如果expr是类对象（或类名）且至少包含有一个虚函数，typeid操作符返回的typeinfo对象需要在运行时计算，否则，返回一个静态对象，在编译时就可以计算得到。

typeinfo类中包含了一个name（）成员，返回一个字符串，通常是类名。例如下述语句返回的均为静态typeinfo对象。

cout＜＜"typeid（5）.name（）"＜＜endl；//输出字符串"int"

cout＜＜"typeid（double）.name（）"＜＜endl；//输出字符串"double"来看一个返回动态typeinfo对象的例子，如下所示。

class A{……}；//包含虚函数

class B：pulic A{……}

A*pa=new B；

cout＜＜typeid（*pa）.name（）＜＜endl；

输出结果为"B"，换言之，编译器无法根据指针pa的类型在编译阶段对typeid（*id）的输出结果进行判断，必须等到运行时才能计算其返回值。

typeinfo类中对==和！=进行了重载，因此，可以使用typeid来判断某个变量是否是某种类型，如下所示。

if（typeid（double）==typeid（x））

上述代码即用以判断变量x是否是double类型。

注意

使用typeid操作符和typeinfo类时，不要忘记包含头文件typeinfo。

17.1.3　补充说明

RTTI只能应用于包含虚函数的类层次中，只有在对虚函数的处理中使用派生类对象给基类指针赋值才有意义。如果类层次中没有虚函数，将派生类赋值给基类指针没有实质意义。RTTI的引入，可检查基类指针向派生类指针的转换是否安全，为类层次中非虚函数和数据成员的调用提供了方法。

17.2　类型转换操作符

C++中添加了4个类型转换符，用以对数据类型的转换进行更严格的限制，它们分别是dynamic_cast、const_cast、static_cast和reinterpret_cast，同前面所讲的类型转换机制相比，新机制让程序员根据需要选择要使用的操作符，明确了转换意图，可读性更强，而且编译器可方便地对转换是否安全进行检查，能排查出很多在传统类型转换中无法找出的问题，动态操作符dynamic_cast已在上节介绍过，本节对其余3个静态类型转换操作符进行讲解。

17.2.1　const_cast操作符

const_cast的基本使用格式如下所示。

const_cast＜T＞（expr）；

expr应该为指针和引用的形式，该操作符不会影响expr，除了const和volatile修饰符等，T应与expr类型相同，返回一个新的T型变量，与expr有相同的值，如示例代码17.2所示。

代码17.2　const_cast用法ConstCastSample

＜-----------------------------文件名：example1702.cpp-------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）{

04　int i=5；

05　const int*ci=＆i；//ci指向的单元不可改写

06　*（const_cast＜int*＞（ci））=8；//去除了const属性

07　cout＜＜i＜＜endl；

08　cout＜＜（*ci）＜＜endl；

09　return 0；

10　}

输出结果如下所示。

8

8

【代码解析】通过代码第5行语句“const int*ci=＆i；”声明的指针ci原则上是不可改写间接引用的，但代码第6行语句“*（const_cast＜int*＞（ci））=8；”消除了其const属性，使得ci指向的值修改为8，语句“*（const_cast＜int*＞（ci））=8；”实际上等价于以下代码。

int*p=const_cast＜int*＞（ci）；

*p=8；

注意

p和ci是两个不同的变量（＆p！=＆ci），两者只是取值相同（p==ci）。

17.2.2　static_cast操作符

static_cast的基本语法如下所示。

static_cast＜T＞（expr）

该运算符把expr转换为T型，仅当T类型可被隐式转换成expr所属类型，或expr所属类型可被隐式转换成T类型时，上述转换才合法，否则会报错，主要有如下几种用法。

❑用于类层次结构中基类和子类之间指针或引用的转换。进行上行转换（把子类的指针或引用转换成基类表示）是安全的；进行下行转换（把基类指针或引用转换成子类表示）时，由于没有动态类型检查，所以是不安全的。

❑用于基本数据类型之间的转换，如把int转换成char，把int转换成enum。这种转换的安全性也要开发人员来保证。

❑把空指针转换成目标类型的空指针。

❑把任何类型的表达式转换成void类型。

注意

static_cast不能转换掉expression的const和volitale等属性。

例如下述转换都是合法的。

int i=0；

double d=static_cast＜double＞（i）；//合法，i可隐式转换成double

int j=static_cast＜int＞（d）；//合法，int可隐式转换成double

17.2.3　reinterpret_cast操作符

reinterpret_cast的调用格式和上述两个操作符一致，如下所示。

reinterpret_cast＜T＞（expr）

reinterpret_cast无法保证转换的安全性，它用来将一种类型指针转变为另一种类型的指针，也用于将整型量转为指针或将指针转为整型量，如示例代码17.3所示。

代码17.3　reinterpret_cast用法ReinterpretCastSample

＜---------------------------文件名：example1703.cpp---------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　double i=2. 5；

06　double*p=＆i；

07　int*pc=NULL；

08　pc=reinterpret_cast＜int*＞（p）；//指针间的相互转换，地址赋值

09　cout＜＜（*pc）＜＜endl；

10　return 0；

11　}

输出结果如下所示。

0

【代码解析】代码第8行完成的仅是指针的赋值，所以（*pc）中存储的仅是8字节double变量i中的前4个字节。因此，这种转换和硬件联系紧密，移植性较差，在不同的硬件系统中，代码17.3的输出结果可能不同。

reinterpret_cast的两个使用限制如下所述。

❑指针类型转换成整型时，必须转换成能存储其表示的整型（在Windows+VC6下必须要4个字节），不能转化成更小的整型数。

❑函数指针和数据指针间不能相互转换。

说明

reinterpret_cast绕开了指针类型检查机制，对内存字节逐个处理。

17.3　小结

RTTI机制特性能让程序在运行时检测对象的类型，不以指针转换是否安全为依据，而是考虑指针指向的对象，其实质考虑的是指针指向的内存块的有效性，因而能保证使用指针安全调用虚函数和普通函数。typeid操作符返回一个typeinfo对象的引用，通过typeinfo类提供的name成员函数可以输出参数所属类名。

本章对C++中新增的4个类型转换操作符进行了说明，同传统的类型转换相比，新增的4个cast操作符很好地保证了类型转换的安全性，直观体现了程序员的意图，编译器可很好地进行查错处理。

17.4　上机实践习题

1.尝试定义一个包含虚函数的基类，然后将这个类作为基类，再派生1个子类，最后用dynamic_cast进行类型安全转换。

【提示】上述题目主要是要求读者熟悉dynamic_cast操作符的相关知识，重点是掌握操作符的作用及其使用。

【关键代码】

01　#include＜iostream＞

02　using namespace std；

03

04　class base//基类定义

05　{

06　public：

07　virtual void disp（）//虚函数

08　{

09　cout＜＜"hello,base"＜＜endl；

10　}

11　}；

12　class child：public base//派生类定义

13　{

14　public：

15　void disp（）

16　{

17　cout＜＜"hello,child1"＜＜endl；

18　}

19　}；

20

21　//main（）函数的实现

22　base*pbase=new child；

23　child*pchild=dynamic_cast＜child*＞（pbase）；

24　if（pchild！=NULL）

25　{

26　pchild-＞disp（）；

27　}

28　else

29　cout＜＜"转换不安全，退出"＜＜endl；

30　base*pbase1=new base；

31　child*pchild1=dynamic_cast＜child*＞（pbase1）；//转换并不安全

32　if（pchild1！=NULL）

33　{

34　pchild1-＞disp（）；

35　}

36　else

37　cout＜＜"转换不安全，退出"＜＜endl；

2.尝试使用const_cast、static_cast和reinterpret_cast操作符，来进行类型转换并输出结果。

【提示】上述题目主要是要求读者掌握类型转换操作符的相关知识，重点是操作符的使用。

【关键代码】

01　int i=5；

02　const int*ci=＆i；

03　*（const_cast＜int*＞（ci））=8；//去除了const属性

04　cout＜＜i＜＜endl；

05　cout＜＜（*ci）＜＜endl；

06

07　double d=static_cast＜double＞（i）；

08　int j=static_cast＜int＞（d）；

09

10　double n=2. 5；

11　double*p=＆n；

12　int*pc=NULL；

13　pc=reinterpret_cast＜int*＞（p）；//指针间的相互转换，地址赋值

14　cout＜＜（*pc）＜＜endl；

第18章　string字符串类

字符串处理在程序中应用广泛，在第3章中介绍了C风格字符串的相关内容，C风格字符串是以'\0'（空字符）来结尾的字符数组，在使用时程序员需要考虑字符数组大小的开辟，以及结尾空字符的处理，使用起来有诸多不便，实际上C++提供了string类用于字符串的处理。string类定义在头文件string中，注意同第3章提到的头文件cstring进行区分，cstring中定义的是一些对C风格字符串的处理函数，本章仅对string类的使用方法进行讨论。

本章主要涉及以下知识点。

❑字符串类：介绍string类的优点及作用。

❑字符串类的对象声明：介绍如何声明一个string类的对象。

❑字符串的输入输出：介绍string对象的输入与输出操作。

❑string类的功能：介绍如何使用string类中最常用的几个功能。

18.1　为什么要使用string类

本节的题目也可写成“string类比C风格字符串要好”，之所以抛弃C风格字符串而选用C++标准程序库中的string类，是因为string类同C风格字符串相比，不必担心内存是否够用、字符串的长度以及结尾的空白符等。string作为一个类出现，其集成的成员操作函数功能强大，几乎能满足所有的需求。从另一个角度讲，完全可以把string当成是C++的内置数据类型，放在和int及double同等的位置上。

说明

如无特别说明，本章中提及的字符串均是指string字符串。

同流类库差不多，string类其实是basic_string类模板关于char型的实例化，对应着wchar_t类型和wstring类，本章只讨论string类的用法，wstring类的用法与它完全一致。

同STL一样，basic_string模板中同样定义了size_type类，用来表示元素个数等与系统相关的无符号整型。此外，还有表示元素类型的value_type，对string类而言，value_type等价于char，但对wstring类来说value_type为wchar_t。

18.2　声明一个字符串

string是字符串类，首先了解如何通过构造函数来声明一个字符串，string类的构造函数如表18.1所示。

如示例代码18.1所示。

输出结果如下所示。

Hello,C++

China Beijing

China B

C+

【代码解析】代码第6行的str1是由字符串来初始化的，而代码第9行的str2是由字符数组（C风格字符串）初始化的，str1和str2的初始化方式是等价的，代码第10行的str3采用的是C风格字符串sz的前7个字符初始化，所以输出为“China B”（注意：空格也占1个字符），而代码第13行的str4是在str1的基础上初始化的，采用的是从第6个字符（'C'）开始，向字符串尾部取不超过两个字符的串为其初始化，所以，输出结果为“C+”。

string类的析构函数只有一种形式，如下所示。

string：～string（）

析构函数可以由string对象显式调用，以销毁所有字符并释放内存。

注意

从代码18.1中不难看出，string类对＜＜进行了重载，不仅如此，操作符+=被重载用于将1个字符串附加到另1个字符串后面（string类对象自动调整所占内存大小），重载的=用于将1个字符串赋给另1个字符串（深度复制），重载的＞＞可用于输入字符串，重载的[]用于访问字符串中的某个字符。

18.3　字符串的输入输出

C风格字符串的输入方式大致有“＞＞”、“cin.getline（）”和“cin.get（）”3种，对string字符串来说，除了重载“＞＞”实现输出外，string头文件还定义了getline函数用以输入string字符串，抛弃了“cin.getline（）”和“cin.get（）”两种输入方式，原因在于getline外部函数能自动调整目标string的大小，使其能恰好存储输入的字符。

注意

＞＞输入同样会自动调整string对象的大小。

外部getline函数第1个参数必须为输入流对象，第2个参数是待输入的string对象，第3个参数是分界符，getline函数将输入流中的字符存储到string变量中，直到满足下列条件之一为止。

❑到达文件尾，此时输入流的eofbit置位有效。

❑遇到分界字符，默认为换行符'\n'。此时分解符会从流中删除，但并不会存储到string对象中。

❑读取的字符达到最大允许值（string类中的常量npos决定了string所能存储的最大字符数，一般都很大，不会对输入产生影响，另一个因素是可用内存字节数，因此，最大允许值便是从npos和可用内存字节数中选取较小的一个），输入流的failbit置位有效。

如示例代码18.2所示。

代码18.2　string字符串输入StringInput

＜---------------------------文件名：example1802.cpp---------------------------＞

01　#include＜iostream＞

02　#include＜string＞

03　using namespace std；

04　int main（）

05　{

06　string str1（"Hello,C++"）；//使用字符串初始化

07　string str2；//建立空字符串

08　cout＜＜"请输入str1"＜＜endl；

09　cin＞＞str1；//输入str1

10　cout＜＜"请输入str2"＜＜endl；

11　getline（cin,str2，'！'）；//输入str2

12　cout＜＜"您输入的是："＜＜endl；

13　cout＜＜str1＜＜endl；

14　cout＜＜str2＜＜endl；

15　return 0；

16　}

输出结果如下所示。

请输入str1

ABC（注：键盘输入）

请输入str2

ILoveChina！Beijing（注：键盘输入）

您输入的是：

ABC

ILoveChina

【代码解析】代码第6～7行首先声明了两个字符串str1和str2，其中str1用C风格字符串进行了初始化，而str2是个空字符串，语句“cin＞＞str1；”对str1进行输入，从结果可以看出，执行输入操作时，str1中原有内容被清空，只保留新输入的内容“ABC”。代码第11行的语句“getline（cin,str2，'！'）”采用外部getline函数对str2进行输入，以感叹号为分界符，所以输入的“ILoveChina！Beijing”只保留了感叹号之前的部分“ILoveChina”。

细心的读者可能注意到，输出结果中，在“ABC”和“ILoveChina”之间有个空行，这个换行符实际上是str2的第1个字符，根据前面的内容可知使用＞＞执行输入操作时，遇到换行符输入结束，但换行符留在输入缓冲区中，因为getline函数设定了分界符为感叹号，所以此换行符读入到str2中，如果getline中采用默认的分界符（即换行符），str2将为空，读取不到任何字符。

18.4　string类功能

在讨论了如何创建字符串，以及对字符串进行输入输出之后，本节将讨论string类的其他一些public成员函数，通过这些函数可以方便地对字符串进行赋值和清空、实现不同字符串间的比较、字符的插入、删除和追加以及搜索与查找等，下面分别进行讲解。

18.4.1　string字符串和C风格字符串的转换

C风格字符串转换为string字符串相对来说比较简单，通过构造函数即可实现。但由于string字符串实际上是类对象，其并不以空字符'\0'结尾，因此string字符串向C风格字符串的转化是通过3个成员函数完成的，如下所示。

const char*data（）；//以字符数组的形式返回字符串内容，但末尾并不添加'\0'

const char*c_str（）；//返回一个以'\0'结尾的字符数组

int copy（char*s,size_type n）；//字符串的内容复制或写入既有的C风格字符串或字符数组内

18.4.2　赋值和清空

对string字符串的赋值有两种途径，即使用操作符=和使用成员函数assign。

1.使用操作符=

等号右侧的操作数可以是string、C风格字符串或单个字符。

2.使用成员函数assign

成员函数assgin有多种重载形式，从本质上说同有参构造函数的参数是相同的，参考表18.1。把字符串清空的方法有两个，以string字符串str为例进行讲解，如下所示。

str=""l//用空字符串为其赋值

str. erase（）；//删除全部元素

此外，erase函数还可用于删除部分元素。

18.4.3　元素删除

erase函数用于从字符串中删除字符，原型如下所示。

1.iterator erase（iterator First,iterator Last）；

删除[First,Last）的字符，返回的迭代器指向最后一个被删除元素的后一个元素。

2.iterator erase（iterator It）；

删除string中It所指的字符，返回指向下一个元素的迭代器，如果后面没有其他元素，则返回end（）。

3.string＆erase（size_type pos=0，size_type n=npos）；

删除string中从pos位置开始的n个字符或删除到末尾，返回删除后的string的引用。

18.4.4　元素追加与相加

提到追加与相加，首先想到的是重载的+=操作符，+=操作符能将另一个string字符串、C风格字符串，甚至是单个字符添加到string字符串后。

string类还定义了更灵活的append函数将另一个string字符串、C风格字符串，甚至是单个字符添加到string字符串后。此外，通过指定初始位置和追加字符数，以及指定区间的方法，可以将另一个string字符串或C风格字符串的一部分追加到字符串中，append函数的各个重载版本定义如下所述。

（1）string＆append（const char*s）；将C风格字符串s追加到string后面。

（2）string＆append（const char*s,size_type n）；将C风格字符串s中从位置n开始的字符追加到string后面。

（3）string＆append（const string＆str）；将另一个string对象str追加到该string后面。

（4）string＆append（const string＆str,size_type pos,size_type n）将另一个string对象str的一部分追加到该string后面。

（5）string＆append（Iterator First,Iterator Last）；将[First,Last）之间的字符追加到该string后面。

（6）string＆append（size_type n,char c）；将n个字符c追加到该string后面。

注意

当追加得到的字符串长度大于最大字符串长度时（取string：npos和可用内存字节中较小的一个），将抛出length_error异常。

此外，string类重载了+运算符，以便拼接字符串，该运算符不修改字符串，而是创建一个新的字符串用于存储结果，+运算符是以友元形式重载的，能用于string对象和string对象、string对象和字符数组、字符数组和string对象、字符和string对象以及string对象和字符的相加。

18.4.5　元素插入

成员函数insert用于将string对象、字符数组或字符插入到string字符串中，这和追加append有些相似，只是元素插入不再局限于尾部，可以插入到中间，因此需要一个指示插入位置的参数，该参数可以是位置，也可以是迭代器，数据将被插入该位置的前面，insert（）函数的重载形式如下所示。

（1）string＆append（size_type pos,const char*s）；

（2）string＆append（size_type pos,const char*s,size_type n）；

（3）string＆append（size_type pos,const string＆str）；

（4）string＆append（size_type pos,const string＆str,size_type pos1，size_type n）；

（5）string＆append（size_type pos,size_type n,char c）；

（6）void insert（Iterator pos,Iterator First,Iterator Last）；

（7）void insert（Iterator pos,size_type n,char c）；

如果插入位置超过了目标字符串长度，或者说pos1超过了要插入的字符串str的结尾，out_of_rang异常将被抛出，如果插入后得到的字符串长度大于最大长度，length_error异常将被抛出。

18.4.6　大小和容量

string类提供了一些与大小和容量相关的函数，如表18.2所示。

18.4.7　元素存取

可以使用下标操作符[]和函数at对字符串中包含的字符进行访问，需要注意的是操作符[]并不检查索引是否有效（有效索引0～str.length（）），如果索引失效，会引起未定义的行为，而at函数会检查，如果使用at函数的时候索引无效，会抛出out_of_range异常。

注意

对const修饰的string常量str来说，操作符[]对索引值str.length（）仍然有效，返回值是'\0'，其他情况下（即str不是const修饰的字符串常量），str.length（）索引都是无效的。

18.4.8　字符串比较

string字符串支持常见的比较操作符（＞、＞=、＜、＜=、==、！=），也支持string与C风格字符串的比较。使用上述比较操作符时，根据字符的字典顺序（字典排序靠前的字符小）从前往后逐一比较，遇到不等的字符就按该位置上的这两个字符的比较结果确定两个字符串的大小，即string（"abcde"）＜string（"ac"）。

此外，string类还提供了成员函数compare用于字符串比较，支持多参数处理，支持用索引值和长度定位子串来进行比较，返回一个整数（0：相等、＞0：大于、＜0：小于）来表示比较结果。

18.4.9　提取子串

string类提供了substr成员函数用于提取子串，以string str（"12345678912345678"）为例，如下所示。

str. substr（）；//返回str的全部内容

str. substr（9）；//从索引9后面的子串，即"12345678"

str. substr（5，6）；//从索引5开始，提取6个字符，即"678912"

18.4.10　搜索与查找

string类提供的搜索与查找函数很多，由于篇幅有限，本章便不再展开讲叙，总体上说，string类提供了6种搜索函数，返回值均为size_type，简要列举如下所示。

1.find（）系列

返回待查找元素或子串在字符串中第一次出现的位置。举例来说，字符串str为“Welcome to C++World”，使用find函数系列查找单个字符'o'在str中第一次出现的位置，返回结果为4，还可查找子串"to"在str中的位置，返回结果为8。

2.rfind（）系列

返回待查找元素或子串在字符串中最后一次出现的位置，如使用rfind函数查找单个字符'o'在str中最后一次出现位置，返回结果为16。

3.find_first_of（）系列

返回待查找元素在字符串中第一次出现的位置，如果查找的子串，不是查找整个字符串的匹配，而是搜索子串中的字符首次出现的位置，同样以字符串“Welcome to C++World”为例，使用find_first_of（）系列函数查找字符串"abc"在其中首次出现的位置，返回结果为3，此时第一次出现了字符串"abc"中的元素'c'。

4.find_last_of（）系列

返回待查找元素在字符串中最后一次出现的位置，如果查找的子串，不是查找整个字符串的匹配，而是搜索子串中的字符最后出现的位置。

5.find_first_not_of（）系列

与find_first_of（）系列的工作方式类似，不过搜索的是第一个不位于字符串中的字符第一次出现的位置。

6.find_last_not_of（）系列

与find_first_of（）系列的工作方式类似，不过搜索的是第一个不位于字符串中的字符最后一次所在位置。

18.5　小结

本章主要对C++标准库中提供的string字符串类进行了简要介绍，同普通的C风格字符串相比，string类提供了自动管理内存的功能以及众多处理字符串的方法和函数。例如字符串的追加和相加、字符的插入、字符串比较以及搜索和查找等，合理使用string类能大大提高程序的运行效率。

string类是由basic_string类模板根据char型实例化而来，而且string的定义和STL联系密切，迭代器等概念都是完全相通的，因此，推荐采用string代替C风格字符串进行编程。

18.6　上机实践习题

定义一个字符串对象，然后将接收用户的输入，最后将输入结果输出。

【提示】上述题目主要是要求读者熟悉字符串类的相关知识，重点是掌握字符串类的概念和使用。

【关键代码】

01　#include＜string＞

02　using namespace std；

03

04　string str；

05　cout＜＜"请输入字符串"＜＜endl；

06　cin＞＞str；

07　cout＜＜"您输入的字符串是："＜＜str＜＜endl；

第19章　编码风格

编码风格不属于技术范畴，但却受到越来越多的重视，随着程序规模的扩大，仅依靠一个人完成所有代码的编写是不可能的，程序员应当培养的是合作和交流的能力，而编码风格决定了程序的可读性，以写文章来作比喻，好的风格包括结构和条理清晰以及字体工整，这会让阅读者感觉十分舒服，也能很快领会文章的意思，而差的风格让人读起来完全摸不着头脑，何来交流可谈。

本章主要从版式、命名规则和项目文件的组织3个方面来讲述怎样为好的编码风格，重申一遍，这不属于技术范畴，而且没有统一的标准，业界也是争论不休，本章提出的仅是建议，仅供参考。

本章主要涉及以下知识点。

❑程序的排版：介绍在程序代码中排版应该注意的问题。

❑命名规则：介绍常用的几种命名规则。

❑项目文件的组织：介绍如何对项目中的文件进行划分。

19.1　程序的排版

排版这个词好像更多用于文字领域，其实代码文件也可以看成是一部作品，好的程序不仅要做到功能正确、效率高，还要书写规范便于阅读。

19.1.1　注释

很多程序员都有这样的体会，看几个月前自己写的代码，完全不知所云，读了半天才弄懂，相当于重新写了一遍。如果添加了注释，阅读和理解会快捷很多，注释往往应用于以下情况。

❑版本、版权声明以及文件介绍。

❑函数接口和功能说明。

❑重要的代码行或代码块。

注释的位置应与被描述的代码相邻，常放在代码的上方或右方，不推荐放在下方，而且，注释一定要清晰，读了让人更迷糊的注释不如不加。如果对代码进行了修改，注释的修改也要同步进行，否则会让读代码和注释的人摸不着头脑。

使用注释的另一个弊端是注释过多，每行都加注释是不必要的，注释过多也会惹人讨厌，关键是控制好一个度。

在结构复杂的代码块中，尤其是多重嵌套时，使用代码缩进固然能使层次更清晰，但在一些段落结束时添加注释能使结构更清晰，如下所示。

if（x＞0）

{

……

if（y＞0）

{

……

}//end of if（y＞0）

……

}//end of if（x＞0）函数的功能和接口注释推荐格式如下所示。

/*

*函数介绍：功能

*输入参数：in

*输出参数：out

*返回类型：

*/

19.1.2　空行的用法

编译器不会对空行进行处理，它只起到分隔段落，区分层次的作用。总体来说，逻辑不相关的代码块间最好都添加一个空行，如每个函数定义后、每个类声明后以及函数体中的一个if结构后等。

19.1.3　代码行

原则上一行代码只做一件事情，不要以为程序中到处是下述形式的代码是件很酷的事情。

y=（x++）+（++z）；将其分解如下所示。

z++；

y=x+z；

x++

更容易阅读，结构更清晰，便于查错排错。

原则上，if、for、while及do之后的执行语句都要加{}，哪怕只有一行，因为以后可能会对if结构进行修改，始终加上{}有助于防止书写失误。

提示　很多教科书中提倡尽可能在变量定义的同时进行初始化，以避免遗忘。

19.1.4　空格

代码行之间也需要插入一定数量的空格，使得变量名、关键字和操作符之间有一定的间隙，既方便阅读，又美观大方，如果乱七八糟地挤在一起，不仅读起来费劲，而且想理顺关系也不容易。

一般来说，有以下5个指导原则。

（1）算术运算符（+、-、*/等）、比较运算符（＜、==、！=等）、逻辑运算符（＆＆和||）、位操作符（＆和|）以及赋值操作符（=和+=等），这些二元操作符前后推荐添加空格。

（2）一元操作符，如！（取反）、～（位反）、++（自增）、--（自减）和＆（取地址）前后不加空格。

（3）if、while及for等关键字后推荐留一个空格再接'（'，以突出关键字，但函数名和'（'之间不加空格，以便区分函数名和关键字。

（4）“（）”内部，'（'后不加空格，'）'前不加空格。

（5）逗号后要加空格，分号如果不是一行的结尾（如for结构中的分号），其后应加空格。'['、']'、'-＞'和'.'前后不加空格。

19.1.5　缩进与对齐

以if结构为例，流行以下两种书写样式。

if（condition）{

//代码

}和

if（condition）

{

//代码

}

其他诸如函数定义、for结构及while结构都存在这两种样式，推荐采用第2种方式，如果出现嵌套的{}，应使用缩进对齐以方便阅读。

19.1.6　＆和*的位置

以声明指针变量为例，*应靠近类型（int*p；）还是靠近变量名（int*p）一直以来都颇有争议，推荐采用后一种写法，如下所示。

int*p1，p2；

便不会把p2错认为是int型的指针。

19.2　命名规则

如何对变量和函数命名，这对程序来说不是结果是否正确，效率是否高的决定性因素，程序员完全可以根据自己的喜好创造变量名（也许这是能让程序员感到自己是上帝的主宰），如果框架来制约程序员应如何对变量进行命名，可能会惹来众多非议，似乎剥夺了程序员权力。

但变量名和函数名对程序的阅读很重要，好的命名能让阅读者望文知意，通过几个字母的组合便知道其意义和功能，不用每个都去查手册。因此，要写出高质量的代码，需要一套合理的命名规则。

Microsoft公司一直倡导匈牙利命名法，其主导思想是在函数名或变量之前加上前缀以说明其含义，增进人们的理解。例如所有指针都以p开头，所有int型变量都以i开头等，本节推荐一套成熟有效的命名规则。

19.2.1　Windows风格和Unix风格

好的名字应做到短小精悍，短小则输入方便，精悍则望文知意，就书写来说，有两种成熟的风格即Windows风格和Unix风格。

Unix风格的标志是“小写字母+下划线”，如delete_file（）函数用以删除一个文件。而Windows风格不使用下划线，而改用混排，使用大写首字母区分单词的方式，如删除一个文件的函数可命名为DeleteFile（）。

19.2.2　如何对程序实体命名

本节推荐Windows编程风格，当然仅是推荐，喜欢使用何种形式完全取决于读者的喜好。根据程序实体的不同，命名规则也有所不同，如下所示。

（1）变量和参数的开头字母要小写，如currentState（当前状态）。

（2）静态变量前加s_（表示static），如s_totalNum（总体数量）。

（3）全局变量前加g_（表示global）。

（4）类内成员变量前加m_（表示member）。

（5）类名和函数名首字母也大写，以和变量区分。

（6）常量全用大写字母，用下划线分隔单词，如NUM_OF_ELEMENTS。

虽然C++语言对大小写敏感（区分大小写），但程序中尽量不要出现仅靠大小写区分的标识符，如num和Num。而且，程序中尽量不要使用名字完全相同，仅仅依靠作用域（屏蔽作用）来区分的标识符，如全局变量num和局部变量num，这可能会给程序带来一些安全隐患。尽量不要出现“num1”、“num2”等数字编号的标识符，这可能是程序员偷懒，不肯动脑想出的名字，但从阅读的角度看，这样的命名对提高程序的可读性没有任何意义。

变量的名字应使用“名词”或“形容词加名词”，而函数命名应尽量采用“动词”或“动词加名词”形式，表明函数是一个动作是操作手段，特殊地，从类的角度看，类的成员函数应为“动词”，省略的名词就是类对象本身（this）。

19.3　项目文件的组织

C++程序文件可分为头文件（.h文件）和定义文件（.cpp文件）两个部分，这些相信读者已不再陌生，本节来简要对项目的组织机制和两种文件的写法进行讲解。

19.3.1　模块划分

“划”是规划的意思，意指怎样合理的将一个很大的软件划分为一系列独立的部分合作完成系统的需求。C++语言中，模块是按类层次结构进行划分的，用于完成特定功能的一个或一组类组成一个模块。

自行编写的模块可以看成是一个cpp文件和一个h文件的结合，头文件中是对该模块接口的声明，对一些二进制代码库来说，可能没有cpp文件。

19.3.2　头文件

系统提供的库函数大多是已编译好的二进制文件，为了能对库提供的功能，如函数等进行调用，必须使用头文件来提供接口声明，不必关心内部代码是如何书写的，编译器会自动从库文件中提取相应的代码。

提示之所以提供的是已编译好的二进制形式，主要是出于版权和保密的需要，不能对外公开。

同样商业代码公司也可以编写并出售实现特定功能的二进制代码库，用户只要包含其头文件，提供接口声明，便可调用库中功能，至于功能如何实现的，不是用户需要关心的重点，这就是常说的第3方库，这有效促进了业界的产业化。

另一方面，对自己编写的cpp文件和h文件，将接口声明都放在h文件中有个好处就是省去了对接口的显式声明，因为h文件是在文件开始包含的，因此其中声明的接口对整个文件都是可见的，方便调用，同时头文件能加强类型安全检查，当调用方式和头文件中的声明不一致时，编译器将指出错误。

对头文件来说，一条重要的原则是只声明，不定义。在头文件中不要出现函数的定义，即使是类的成员函数，虽然可以在声明的同时定义，且自动成为内联形式，但不推荐使用，不论函数体多小，建议将其实现都放在cpp文件中，如果要声明为内联形式，可以使用inline修饰。

全局变量的使用破坏了程序的封装性，不提倡使用全局变量，如非确实需要，尽量不要在头文件中出现如“extern int g_num”这样的声明。永远不要在头文件中定义变量，定义变量和声明变量的区别在于定义会分配内存，属于编译的范畴，而声明则只是告诉包含该声明的模块再从其他模块中寻找外部函数和变量，属于链接的范畴。

单独的头文件不参加编译，只有包含在cpp文件中的头文件才辅助该cpp文件完成编译，这便是编译单元（Compile Unit）的概念，关于编译单元的介绍请参考第20章。

19.3.3　定义文件

cpp文件用以定义变量和实现函数，定义文件是编译的主体。合理的模块划分形式应能很好地控制实现文件和函数体的大小，如果某个函数太大，会给阅读带来困难，一个合理的解决思路是将其功能进一步细分。

19.3.4　目录结构

为了方便项目管理，不同的模块可以放在不同的文件夹中。比如，负责界面显示的类可放在display文件夹下，负责数据库管理的类可放在database文件夹下等。

另一种组织方式是将头文件和cpp文件分开放。比如，将头文件放在include文件夹下，而将cpp文件放在source文件夹下。

cpp文件的#include可以包含路径信息，如下所示。

#include"E：/C++/example.h"//绝对路径

#include"../example.h"//相对路径，../表示上一层目录

#include"../../example.h"//相对路径，可嵌套，上层目录的上层目录

相对路径可嵌套至根目录，但不可跨盘符。

19.4　小结

本章讲述了编码风格的内容，大多不属于技术范畴，但对程序的可读性，对业内的交流和学习有着重要的作用。

好的排版使得代码结构清晰明了，容易阅读，这主要通过合理使用空行、空格、缩进和对齐来实现。好的命名机制能使人望文知意，快速把握代码的内涵，在介绍了Windows和Unix两种风格的命名机制后，重点推荐了Windows命名方式。

随着程序规模的扩大，对项目进行模块化划分就显得十分必要，划分的依据主要是以一个或一组实现特定功能的类组成模块，这可通过cpp和h文件配对实现。同时，为了方便管理，不同的模块及不同的文件可分开放置在不同的目录中。

本章只是对编码风格的简要探讨，相信随着读者经验的增加，对编码风格的体会会更深，希望本章能起到抛砖引玉的作用。

19.5　上机实践习题

1.找出下面代码中的不符合排版规则的编码。

【提示】上述题目主要是要求读者熟悉程序排版的相关规则，重点是掌握程序编码的排版规则。

【关键代码】

01　#include＜iostream＞

02　using namespace std；

03　int main（）

04　{

05　int x=10，z=20；

06　int y=（x++）+（++z）；

07　int*p1，p2；

08　p1=＆x；p2=z；

09　cout＜＜*p1＜＜y＜＜p2＜＜endl；

10　return 0；

11　}

2.找出下面代码中的变量及程序中命名不规范的地方。

【提示】上述题目主要是要求读者掌握命名规则的相关知识，重点是两种不同风格命名方式。

【关键代码】

01　#include＜iostream＞

02　using namespace std；

03　int dd（int a）

04　{

05　return a*a；

06　}

07

08　int main（）

09　{

10　int i=1，n=2，m=3；

11　float a=1. 2，b=3.2，c=3.3；

12　char ddd='b'；

13　cout＜＜dd（n）＜＜endl；

14　return 0；

15　}

第20章　程序编译

编写的cpp文件和h文件是如何组织成二进制可执行文件的，这是本章要讨论的内容。除了使用变量定义、函数定义与调用以及类定义与类实现外，C++还提供了诸如“#inlcude”等预处理机制，用以辅助代码的编写，同时，本章将针对程序的编译和调试进行讲解。

本章主要涉及以下知识点。

❑程序的编译过程：介绍程序编译过程中的各关键环节。

❑预处理：介绍预处理中各个命令。

❑VC6调试入门：介绍如何用VC6进行程序调试。

❑其他调试方法：介绍另外几种常用的程序调试方法。

20.1　程序的编译流程

第1章中介绍了使用VC6开发环境编译运行C++程序的基本步骤，提及“选择‘Build’菜单中的‘Build工程名’命令，或直接按F7键即可实现对整个工程所有源代码文件的编译和链接。编译链接无误即可生成一个后缀为exe的可执行文件”。所有的工作都是由VC6开发环境完成的，对一个程序员来说，了解其内部运作细节有助于对程序编写有个全局性的把握，程序的编译流程大体可分为编辑、预处理、编译和链接4个步骤。

1.编辑

源文件是通过键盘输入计算机的，存储在硬盘上的程序文件，在DOS和Windows环境下其后缀名为cpp（定义文件）或h（头文件），一个程序可能包含很多源文件。将源文件输入计算机并进行修改和保存的过程就称为“编辑”。

2.预处理和编译

编译用于将每个编译单元翻译成二进制代码文件，在DOS和Windows环境下，二进制代码文件的后缀名为obj，在Unix环境下其后缀名为o。

首先来看下什么是编译单元，在很多C++教材中有这样的论述“头文件不参加编译，只有实现文件（cpp文件）才参加编译”，这种简单的表述是不科学的，容易给读者留下错误的印象。事实上，编译器处理的对象是由单个cpp文件和其中递归包含的头文件组成的编译单元。

当一个cpp文件在编译时，预处理器首先递归包含头文件，形成一个含有所有必要信息的单个源文件，这个源文件就是一个编译单元。这个编译单元会被编译成为一个与cpp文件名同名的目标文件（.o或是.obj）。

每个cpp文件对应一个编译单元，而每个编译单元都会生成一个二进制代码文件，所以，每个cpp文件对应着一个二进制代码文件。

预处理器（Preprocessor）是在真正的编译开始之前由编译器调用的独立程序。预处理器可以删除注释、包含文件以及执行宏替代，稍后会详细介绍编译预处理的相关内容。

3.链接

链接程序的作用是将编译得到的零散的二进制代码文件组合成二进制可执行文件，有以下两个意义。

（1）对应于某个编译单元的对象文件包含在其他编译单元中定义的函数引用或其他指定项的引用，而这些函数或项仍没有被解析。

例如，某个程序由两个cpp文件组成，分别为A.cpp和B.cpp，两个cpp文件和其中递归包含的头文件组成两个编译单元，经过预处理和编译生成二进制代码文件A.obj和B.obj，假设函数C是定义在B.cpp中，但在A.cpp中对函数C进行了说明和调用，这样，在A.obj中实际上仅包括对C函数的引用，其二进制定义代码需要从B.obj中提取，插入到A.obj的调用处，这个过程称为函数解析（Resolve），这个组合的过程是由链接器完成的。不仅是函数，变量（诸如有外部链接性的全局变量）也涉及解析的问题。

注意

当B.cpp没有定义函数C时，编译时不会产生错误，但链接时却会提示，有未解析的对象。因此，在代码错误分析时要弄清问题是出在编译阶段还是链接阶段。

（2）建立与库函数的链接。

出于商业考虑或保密需要，C++标准库函数和其他第三方库函数是以二进制代码形式提供的，库文件的后缀名为lib，如果在编译单元中声明并调用了库函数，便需要对库函数进行解析，链接器会从对应的库文件中将该函数的二进制代码抽出插入到调用处，当然，如果库中无此函数或找不到对应的库，也会发生未解析（unresolved）的错误。

在每个编译单元生成的obj文件中，变量和函数并未得到系统分配的绝对地址，因而无法执行。实质上链接是为程序中的变量和函数分配绝对地址，使二进制文件可执行的过程。此外，根据不同的操作系统，链接器会为组合后的二进制程序“添加”一些与操作系统有关的代码，使其可运行，这也解释了为什么在Windows环境下编译生成的可知性程序，如exe格式文件在Unix中无法运行。

20.2　预处理

前面接触到的“#include”和“#define”都属于编译预处理，C++允许在程序中用预处理指令写一些命令行，均以“#”开头，为同普通的语句进行区分，预处理行尾不加分号，原则上预处理行可以写在程序的任何位置，但推荐（或是习惯写法）写在程序文件的头部。编译器在对文件进行实质性的编译之前，先处理这些预处理行，这也是“预”字的含义。

注意

预处理器在编译器之前根据指令更改程序文本。编译器看到的是预处理器修改过的代码文本。

常用的预处理指令如下所示。

❑头文件包含。

❑宏。

❑条件编译。

❑其他，如#pragma等。

20.2.1　头文件包含

头文件包含以下两种格式。

❑#include＜头文件名＞。

❑#include“头文件名”。

两者的区别在于预处理器查找头文件的顺序，尖括号形式是用于系统提供的头文件，而引号方式首先在当前目录下查找，因而适用于自定义的头文件。

预处理器会用包含的头文件插入到源文件中，因为包含嵌套的关系（例如，在某个A.cpp文件中使用了头文件包含#include"B.h"，而B.h文件中使用#include"C.h"，相当于B.h和C.h都会被插入A.cpp文件中，用以替换两个#include指令），可能有多个头文件插入到源文件中。

注意

头文件并不拘泥于h后缀名的形式，只要是文本格式均可包含，比如无后缀文本文件、cpp源文件甚至是txt文件等。

20.2.2　宏

宏定义即是用一个宏名来命名一个字符串，形式如下。

#define宏名字符串

宏名是合法的C++标识符，定义的宏名可以应用在程序中，预编译器将编译单元中出现的宏都使用字符串来替换，不做任何形式的检查。

1.对象宏

不带参数的宏被称为对象宏（Objectlike Macro），#define经常用来定义常量，此时的宏名称一般为大写的字符串，如下所示。

#define NUM 20在程序中便可将NUM当做常量20来用，如下所示。

int sz[NUM]；预编译器只是用宏定义中的字符串对宏名进行替换，并不做任何形式的检查，因此，宏定义后的注释必须采用一对/*和*/的形式，如下所示。

#define NUM 100//个数程序中代码如下所示。

int x=NUM+1；将被替换成下述代码。

int x=100//个数+1

这将导致编译错误。

来看以下示例，从中体会字符串替换的意义。

#define NUM 3+4

int x=NUM*5；将被替换为如下代码。

int x=3+4*5；而

#define NUM（3+4）

int x=NUM*5；将被替换为如下代码。

int x=（3+4）*5；

注意

"#define X"等价于"#define X 0"。

2.函数宏

带参数的宏也被称为函数宏，利用宏可以提高代码的运行效率，函数的调用需要压栈出栈，这一过程如果过于频繁会耗费大量的CPU运算资源，所以一些代码量小但运行频繁的代码如果采用带参数宏来实现会提高代码的运行效率，如下所示。

#define cube（x）（（x）*（x）*（x））在代码中可使用诸如cube（3）形式返回（3*3*3）=27，函数宏的替换同样不进行类型检查。在函数宏定义时，括号的使用要特别注意，如上例中每个括号都不可少。例如将其定义为如下代码所示。

#define cube（x）（x*x*x）代码“int a=cube（3+4）；”将被替换成以下代码形式。

int a=（3+4*3+4*3+4）；

下面来看宏定义的作用域。简单地说宏定义只在本编译单元内有效，如果源程序文件中没有使用命令“#undef”，则会以作用域为定义点到本编译单元的末尾；如果使用了命令“#undef”，则定义域是从定义点到该命令之前。

20.2.3　条件编译

#if、#else和#endif用于条件编译，其基本形式如下所示。

#if判断表达式

语句

#else

语句

#endif或

#if判断表达式

语句

#endif

判断表达式可以是包含宏、算术运算和逻辑运算等的合法C++表达式。如果常量表达式为一个未定义的宏，那么其值被视为0，注意判断表达式中出现的宏必须为对象宏。

使用条件编译可以提升代码的可移植性，针对不同平台执行不同语句，如示例代码20.1所示。

代码20.1　条件编译的使用ConditionalCompilation

＜--------------------文件名：example2001.cpp---------------------------------＞

01　#include＜iostream＞

02　using namespace std；

03　#define WINDOWS/*定义WINDOWS宏*/

04　void disp（）

05　{

06　#if defined（WINDOWS）//条件编译

07　cout＜＜"本程序当前运行在Windows环境下"＜＜endl；

08　#else

09　cout＜＜"本程序当前运行在非Windows环境下"＜＜endl；

10　#endif

11　}

12　int main（）

13　{

14　disp（）；//函数调用

15　return 0；

16　}

输出结果如下所示。

本程序当前运行在Windows环境下

【代码解析】代码第4行的disp函数中采用了条件编译指令编写，因为程序中定义了WINDOWS宏，所以输出“本程序当前运行在Windows环境下”，假若将“#define WINDOWS”语句注释掉，程序将输出“本程序当前运行在非Windows环境下”。

代码第6行“#if defined（WINDOWS）”语句中的“defined（宏名）”的含义是若宏被定义则返回1，否则返回0，defined之间可以进行逻辑运算。

此外，使用条件编译指令也可用于大段代码的注释或调试信息的输出等，如下所示。

#if defined（DEBUG）

{

一大段代码；

}

#endif

如果定义了DEBUG宏，其中的一大段代码会被执行，否则这段代码等同于注释。

20.2.4　#ifdef、#ifndef与重复包含

#ifdef、#ifndef用来测试某个宏是否被定义，“#ifdef”相当于“#if defined（）”，“#ifndef”相当于“#if！defined（）”，经常用于避免头文件的重复引用。

头文件重复引用往往是由于包含嵌套造成的，例如某个cpp文件中包含如下头文件。

#include＜A.h＞

#include＜B.h＞而A.h中包含了B.h，如下所示。

#include＜B.h＞

这意味该cpp文件对应的编译单元中，头文件B.h被引用了两次，这就是重复包含。

重复包含带来的问题便是重复定义，前面讲过对普通变量和普通函数来说，定义只能有一次，而声明可以有多次，如果定义多次，编译器就会报错。头文件中往往定义了大量的宏，根据编译器规则，宏可以重复定义，前提是这些定义必须是相同的，因此，如果头文件只是提供接口作用，不对变量进行定义，不包含函数的具体实现，重复包含似乎不会引发什么问题。但是在编写头文件时，很多程序员并不遵守这些规则，有的干脆把全局变量定义在头文件中，所以防止重复包含十分重要。

在头文件编写时使用如下格式可有效防止本编译单元内的头文件重复包含。

//文件名：headfile.h

#ifndef__HEADFILE_200802241900_

#define__HEADFILE_200802241900_

……//原来headfile.h中的内容

#endif

上述代码中“__HEADFILE_200802241900_”仅是示例，可根据不同的头文件自由设置，只要保证在本编译单元中唯一即可，推荐写法是“两个下划线+头文件名+日期时间”。这样，当在组成某个单元的cpp文件和h文件中第1次“#include＜headfile.h＞”时，由于宏“__HEADFILE_200802241900_”尚未定义，满足“#ifndef__HEADFILE_200802241900_”这个判断，故执行“#define__HEADFILE_200802241900_”，并将原来headfile.h头文件中的内容插入到包含处，如果出现了对headfile.h的重复引用，在本编译单元内第二次“#include＜headfile.h＞”时，判断条件“#ifndef__HEADFILE_200802241900_”不再满足，所以不会将原来headfile.h中的内容插入到包含处，会直接跳到#endif很好地防止了头文件被重复引用。

注意

与条件编译不同的是，#ifdef和#ifndef既可作用于对象宏，又可作用于函数宏。

20.2.5　使用const代替#define定义常量

const和#define都可用于定义常量，但相比#define,const有很多优势。对#define定义的符号常量，预处理器只是进行简单的字符串替换，并不对其进行类型检查，而且会与程序中定义的同名变量相冲突，如下所示。

#define num 10；

void disp（）

{

int num；

cout＜＜num；

}

编译时，预处理器会将disp函数中的语句“int num；”替换成“int 10”，若使用“const int num=10；”便不会出现这种问题。

const标识符遵循变量的作用域规则，可以定义在数据块内、名称空间中或者函数外部，当定义在函数外部时，其作用域是文件作用域，即const定义的外部常量的作用域限定在本编译单元内，从定义点处到结束。

在C++中，#define和const定义的常量都可用作数组长度的参数。总之推荐使用const代替#define来定义常量。

20.2.6　inline与#define的比较

使用#define定义函数宏时，对括号的使用要特别注意。函数宏最大的优势是摆脱了函数执行的压栈和出栈，提高了执行效率，函数宏不对参数进行类型检查，这降低了程序的安全性，从另一个角度看这也成了其优点，提高了灵活性，对函数来说，通过函数模板可实现同样的功能，使函数独立于参数。

前面介绍过inline函数，这是一种空间换效率的做法，同样摆脱了普通函数执行时的压栈和出栈，提高了执行效率，与函数宏不同的是inline函数采用的是真正的参数传递机制，会对参数类型进行检查，这提高了程序的安全性，但灵活性上不如函数宏。

注意

应根据不同的情况选择使用inline还是#define来定义小型函数。

20.3　VC6调试入门

一般来说，程序可能出现的错误分两类，一类是编译链接错误，说明代码书写不符合编译器和链接器的处理规则，另一类是逻辑错误和运行时产生的错误，表现在可通过编译链接，但运行结果与预期不符合，或能正常运行但很不稳定，动辄死机重启等。

从排错修改的角度看，逻辑错误和运行时错误比编译链接错误棘手得多，对应着不同情况，需要具体分析，而编译链接错误相对容易解决，只要遵照C++的语法规则修改即可。本节简要介绍解决逻辑错误和运行时错误的技术手段。

本节对VC6调试环境进行简要介绍，多数开发环境，包括VC 2003、VC 2005及Borland C++等的调试环境与其类似。

20.3.1　断点的设置与去除

当程序运行到断点时，程序中断执行回到调试器。断点是最常用的技巧。

1.设置断点

程序在调试时，只有设置了断点并使程序回到调试器，才能对程序进行在线调试，设置断点的手段有很多种。

❑把光标移动到需要设置断点的代码行上，然后按F9快捷键。

❑选择“Edit”菜单中的“Breakpoints”命令，或按快捷键Ctrl+B或Alt+F9，弹出“Breakpoints”对话框，如图20.1所示。打开后单击“Break at”编辑框右侧的箭头，选择断点位置，一般情况下，直接选择“line×××”，如果当前光标所在不是断点要设置的位置，可以单击Advanced，弹出“Advanced Breakpoint”对话框，然后填写函数、行号和可执行文件的信息，通常只要填写行号（点+数字）和文件名（诸如×××.cpp的形式）即可，如图20.2所示。

2.去除断点

去除断点同样有两种方式，一是把光标移到断点所在行，再次按F9就可以去除断点，二是打开“Breakpoints”对话框，选中要删除的断点，单击“Remove”按钮即可。

图　20.1　Breakpoints对话框

图　20.2　Advanced Breakpoint对话框

说明

只有在debug模式下才能触发断点，单击“Build”菜单，选择“Start Debug”，单击“Go”项或直接按快捷键“F5”开始程序调试。

20.3.2　条件断点

在“Breakpoints”对话框中，选定一个设置好的断点，单击“Conditions”按钮，会弹出“Breakpoint Condition”对话框，如图20.3所示，条件断点大致可提供如下功能（从上到下）。

（1）设置一个表达式，如果该表达式是一个判断表达式，当其值为true时断点触发，如果该表达式是一个值（如变量名），当该值（变量的值）发生变化时，断点触发，这可以方便地检测一个变量何时被修改。

（2）中间的编辑框用以输入“观察数组或者结构的元素个数”，须设置一个非负整数值，当数组或结构的元素个数达到这个值时，断点触发。

图　20.3　Breakpoint Condition对话框

（3）设置一个非负整数值，断点被跳过一定次数后才触发。

20.3.3　数据断点

在“Breakpoints”对话框中选择“Data”标签页，可以在编辑框中输入一个表达式，当表达式的值发生变化时，中断触发。与条件断点不同的是，数据断点不依附于具体哪个代码行，而是自动将程序中断使表达式的值改变那一行。因此，这个表达式应该由运算符和全局变量构成（条件断点中可以用局部变量，因为断点依附于代码行）。

20.3.4　消息断点

VC支持对Windows消息进行截获，提供两种截获方式，即窗口消息处理函数和特定消息中断。在“Breakpoints”对话框中选择“Messages”标签页，就可以设置消息断点。如果在上面那个对话框中输入消息处理函数名，每次消息被此函数处理时断点触发，还可使用下拉列表框选择一个消息，每次这种消息到达时断点触发。

20.3.5　观察视图（Watch）

断点触发只是使程序中断，VC调试器提供了其他有用的工具来辅助程序员查看变量、表达式和内存的值，找出错误所在。所有这些查看操作都必须在断点触发及程序中断的情况下进行。

VC提供了Data Tips功能，程序在调试时把光标移动到某个变量上，一个小的“tooltip”窗口将会在该变量右侧出现并显示其当前值。

VC提供一种被称为Watch的机制来查看变量和表达式的值。程序在调试时，在变量上单击右键，选择“Quick Watch”命令，就会弹出一个对话框显示该变量的值。

单击View菜单下“Debug Windows”选项中的“Watch”命令，或直接按快捷键“Alt+3”，会出现一个Watch视图（Watch1、Watch2、Watch3和Watch4），如图20.4所示，在该视图中输入变量或者表达式，就可以观察变量或者表达式的值。

图　20.4　Watch视图

注意

这个表达式不能有副作用，例如++运算符绝对禁止用于这个表达式中，因为这将修改变量的值，破坏了程序逻辑。

20.3.6　内存视图（Memory）

在程序调试中需要查看一块内存的值，VC调试器提供了Memory视图。单击“View”菜单下“Debug Windows”选项中的“Memory”命令，或直接按快捷键“Alt+6”，将会弹出Memory对话框，在其中输入地址，就可查看该地址所指向的内存中的内容。

20.3.7　变量视图（Variables）

单击“View”菜单下“Debug Windows”选项中的“Variables”命令，或直接按快捷键“Alt+4”，会出现一个Variables视图，显示所有当前执行上下文中可见的变量的值和其他信息，特别是当前指令修改的变量，将以红色显示。

20.3.8　寄存器视图（Registers）

单击“View”菜单下“Debug Windows”选项中的“Registers”命令，或直接按快捷键“Alt+5”，会弹出“Registers”对话框，显示当前所有寄存器的值。

20.3.9　调用堆栈视图（Call Stack）

调用堆栈（Call Stack）反映了当前断点处函数是被那些函数按照什么顺序调用的，单击“View”菜单下“Debug Windows”选项中的“Registers”命令，或直接按快捷键“Alt+7”，弹出“Call Stack”对话框。在“Call Stack”对话框中显示了一个调用系列，最上面的是当前函数，往下依次是调用函数及其上级函数，单击函数名可以跳到对应的函数中去。

20.3.10　反汇编视图（Disassembly）

单击“View”菜单下“Debug Windows”选项中的“Disassembly”命令，或直接按快捷键“Alt+8”，可以方便地查看当前函数的汇编代码。

20.3.11　进程控制

VC允许被中断的程序继续运行、单步运行和运行到指定光标处，分别对应快捷键F5、F10/F11和Ctrl+F10。各快捷键的功能如表20.1所示。

20.4　其他调试手段

介绍完强大的VC6编译器后，本节介绍基本C++内置的两个函数，即assert和verify。合理使用这两个函数能有效辅助程序的编制和调试，首先来了解二进制可执行文件发布的debug模式和release模式。

20.4.1　Debug模式和Release模式

绝大多数的开发环境都提供了Debug编译模式和Release编译模式，简要地说，Debug编译的时候会插入调试信息，方便程序被调试，而Release最终发布的是编译方式会去掉程序中的调试信息，对程序运行基本没有什么影响，但在某些情况下也有一定的区别。Debug会在系统中定义一个_DEBUG的宏，某些宏或库函数会根据是否定义了_DEBUG宏而表现出不同的行为，例如本节要讨论的assert宏。

Debug模式和Release模式是一组编译选项的集合，在VC中可修改这些编译选项，关于编译选项的修改涉及内容太多，本章不再展开讲述，感兴趣的读者可查阅相关资料。在VC开发环境中，可通过单击“Build”菜单，执行“Set Active Configuration”命令，在弹出的对话框中选择采用哪种编译方式。

20.4.2　assert宏

assert宏是基于库函数_assert定义的，其接收一个表达式做参数，若该表达式为true，不执行任何操作，否则中断当前程序执行。当程序中定义了NDEBUG宏时，assert失效，就VC编译器来说，Debug模式默认定义_DEBUG宏而不是NDEBUG宏，而Release模式中定义了NDEBUG宏，因而在Debug模式下，assert宏会对表达式进行计算，并判断其值是否为true，而在Release模式下，不对表达式进行任何计算和判断。

20.4.3　输出字符串

尽管开发环境提供的编译器已经足够强大，但有时候却不方便使用。例如要解决的问题仅出现在程序的Release版本中，在这些情况下最简捷的方法就是字符串输出，通过在函数调用前或函数调用中实时地输出某些变量或表达式的值，来判断程序是否已排除了问题。

20.5　小结

本章对程序的编译流程、调试技巧以及VC6编译器的使用进行了简要的介绍。C++程序的编译分编辑、预处理、编译和链接4个步骤。编译单元（Compile Unit）是编译的基本单位，每个cpp文件对应一个编译单元，此外cpp文件中包含的及嵌套包含的头文件都会被预处理器插入到cpp文件中，组成编译单元。编译器将每个单元编译成二进制代码文件，但此时分散的二进制代码文件中的变量和函数没有分配到具体的内存地址，因而不能执行，需要链接器将这些二进制代码文件、用到的库文件中的相关代码以及系统相关的信息组合起来，形成二进制可执行文件。

预处理指令是由预处理器负责执行的，主要有头文件、宏定义和条件编译，推荐采用const，而不是#define来定义常量。重点介绍了VC6的调试环境，调试必须在Debug模式下进行，默认的Release模式不包含调试信息，通过快捷键F5可以方便地启动调试。

断点是调试的基础，断点触发使得程序中断，才能进一步查看变量、内存或其他程序实体的值或状态，除了普通的和固定位置的行断点外，VC还支持条件断点、数据断点以及消息断点。当程序因断点触发而中断时，可通过VC6提供的视图，如Watch、Memory、Variables、Registers、Call Stack和Disassembly等查看与程序相关的信息，找出问题所在。

20.6　上机实践习题

1.下面给出一段代码，请读者自己进行编译，判断是否有问题，如果有问题，请找出问题并修改，直到编译通过。

【提示】上述题目主要是要求读者熟悉编译过程的相关知识，重点是掌握编译中的排错和修订。

【关键代码】

01　#include＜iostream＞

02　using namespace std；

03

04　class base

05　{

06　public：

07　virtual void disp（）

08　{

09　cout＜＜"hello,base"＜＜endl；

10　}

11　}；

12　class child1：public base

13　{

14　public：

15　void disp（）

16　{

17　cout＜＜"hello,child1"＜＜endl；

18　}

19　}；

20

21　class child2：public base

22　{

23　public：

24　void disp（）

25　{

26　cout＜＜"hello,child2"＜＜endl；

27　}

28　}；

29

30　int main（）

31　{

32　base obj_base；

33　child1 obj_child1；

34　child2 obj_child2；

35　disp（＆obj_base）；

36　disp（＆obj_child1）；

37　disp（＆obj_child2）；

38　return 0；

39

40　}

41

42　void disp（base*p）

43　{

44　p-＞disp（）；

45　}

2.下面给出一段代码，请读者自己进行编译，判断是否有问题，如果有问题，请找出问题并修改，直到编译通过。

【提示】上述题目主要是要求读者熟悉编译过程的相关知识，重点是掌握编译中的排错和修订。

【关键代码】

01　#include＜iostream＞

02　using namespace std；

03　class CStudent

04　{

05　public：

06　CStudent（）

07　{

08　m_name=NULL；

09　m_age=18；

10　m_score=0；

11　m_height=150；

12　}；

13　CStudent（char*name,int age,int height,int score）

14　{

15　m_age=age；

16　m_height=height；

17　m_score=score；

18　m_name=new char[strlen（name）+1]；

19　strcpy（m_name,name）；

20　}；

21　～CStudent（）

22　{

23　if（m_name！=NULL）

24　delete[]m_name；

25　m_name=NULL；

26　}

27　void display（CStudent＆stud）；

28　private：

29　intm_age；

30　intm_height；

31　float m_score；

32　char*m_name；

33　}；

34

35　void display（CStudent＆stud）

36　{

37　cout＜＜"name age height score"＜＜endl；

38　cout＜＜stud. m_name＜＜""＜＜stud.m_age＜＜""＜＜stud.m_height＜＜""

39　＜＜stud. m_score＜＜""＜＜endl；

40　}

41

42　int main（）

43　{

44　CStudent std1（"wanghao"，20，176，90）；

45　CStudent std2（"liangliping"，18，158，80）；

46　display（std1）；

47　display（std2）；

48　return 0；

49　}

第六篇　面试题精选

第21章　常见面试题

笔试是考核应聘者学识水平的重要工具。这种方式可以有效地测评出应聘人员的基本知识、专业知识、管理知识、综合分析能力和文字表达能力等综合素质及能力的差异。一次能够通过十几道乃至上百道试题，对众多面试者进行知识、技能和能力的综合考核，可信度和效率都较高，可以大规模进行人才筛选，同时减小应聘者的心理压力，较易发挥水平，成绩评定比较客观。

现在去应聘工作的时候，很多公司都会有笔试的环节。那么在本章中，笔者将对C++中各种常见的面试题进行介绍，其中包括基本知识问答题、世界500强公司的智力问答题、分析题和编程题等多种类型。希望通过本章的介绍能够帮助大家见识一些笔试题目，从而能够轻松顺利地通过笔试找到自己满意的工作。

21.1　常见基本知识问答题

本节主要对C++中容易混淆的基本知识内容以问答的形式进行介绍，方便读者熟悉掌握。

面试题1：简述面向对象的3个基本特征

答：面向对象有如下3个基本特征。

（1）封装：将客观事物抽象成类，每个类对自身的数据和方法实行保护，其权限分为private、protected和public。

（2）继承：广义的继承有3种实现方式：实现继承（指使用基类的属性和方法而无须额外编码的能力）、可视继承（子窗体使用父窗体的外观和实现代码）以及接口继承（仅使用属性和方法，实现滞后到子类实现）。前两种（类继承）和后一种（对象组合=＞接口继承以及纯虚函数）构成了功能复用的两种方式。

（3）多态：是将父对象设置成为和一个或更多的其子对象相等的技术，赋值之后，父对象就可以根据当前赋值给它的子对象的特性以不同的方式运作。简单地说就是：允许将子类类型的指针赋值给父类类型的指针。

面试题2：局部变量能否和全局变量重名

答：能，局部变量能够屏蔽全局变量。要用全局变量，需要使用“：”。局部变量可以与全局变量同名，在函数内引用这个变量时，会用到同名的局部变量，而不会用到全局变量。对于有些编译器而言，在同一个函数内可以定义多个同名的局部变量，例如在两个循环体内都定义一个同名的局部变量，而那个局部变量的作用域就在那个循环体内。

面试题3：类成员函数的重载、覆盖和隐藏的区别是什么

答：成员函数被重载的特征有以下4个。

（1）相同的范围（在同一个类中）。

（2）函数名字相同。

（3）参数不同。

（4）virtual关键字可有可无。

覆盖是指派生类函数覆盖基类函数，特征如下。

（1）不同的范围（分别位于派生类与基类）。

（2）函数名字相同。

（3）参数相同。

（4）基类函数必须有virtual关键字。

“隐藏”是指派生类的函数屏蔽了与其同名的基类函数，规则如下。

（1）如果派生类的函数与基类的函数同名，但是参数不同。此时，不论有无virtual关键字，基类函数都将被隐藏（注意不要同重载混淆）。

（2）如果派生类的函数与基类的函数同名，并且参数也相同，但是基类函数没有virtual关键字。此时基类的函数被隐藏（注意不要同覆盖混淆）。

面试题4：用变量a给出下面的定义

（1）一个整型数。

（2）一个指向整型数的指针。

（3）一个指向指针的指针，它指向的指针是指向一个整型数。

（4）一个有10个整型数的数组。

（5）一个有10个指针的数组，该指针是指向一个整型数的。

（6）一个指向有10个整型数数组的指针。

（7）一个指向函数的指针，该函数有一个整型参数并返回一个整型数。

（8）一个有10个指针的数组，该指针指向一个函数，该函数有一个整型参数并返回一个整型数。

答：

（1）int a；

（2）int*a；

（3）int**a；

（4）int a[10]；

（5）int*a[10]；

（6）int（*a）[10]；

（7）int（*a）（int）；

（8）int（*a[10]）（int）；

面试题5：在C++中，下面的结构是合法的吗？如果是，其作用是什么

int a=5，b=7，c；

c=a+++b；答：上面的例子是完全合乎语法的。问题是编译器如何处理。水平不高的程序员会对这个问题有争议，根据常规处理原则，编译器应当能处理尽可能所有合法的用法。因此，上面的代码被编译如下。

c=a+++b；

因此，这段代码执行后a=6，b=7，c=12。

注意

这是一个关于代码编写风格、代码的可读性以及代码的可修改性的好试题。

面试题6：在非C++中建类A和B，在哪几种情况下B能隐式转化为A

答：

（1）class B：public A{……}//B公有继承自A，可以是间接继承的

（2）class B{operator A（）；}//B实现了隐式转化为A的转化

（3）class A{A（const B＆）；}//A实现了non-explicit的参数为B（可以有其他带默认

//值的参数）构造函数

（4）A＆operator=（const A＆）；//赋值操作，虽不是正式的隐式类型转换，但也可以勉

//强算一个

面试题7：C++中的空类，默认产生哪些类成员函数

答：

class Empty

{

public：

Empty（）；//默认构造函数

Empty（const Empty＆）；//复制构造函数

～Empty（）；//析构函数

Empty＆operator=（const Empty＆）；//赋值运算符

Empty*operator＆（）；//取址运算符

const Empty*operator＆（）const；//取址运算符const

}；

面试题8：C++有哪些性质（面向对象特点）

答：封装、继承和多态。

在面向对象程序设计语言中，封装是利用可重用成分构造软件系统的特性，它不仅支持系统的可重用性，而且还有利于提高系统的可扩充性；消息传递可以实现发送一个通用的消息而调用不同的方法；封装是实现信息隐蔽的一种技术，其目的是使类的定义和实现分离。

面试题9：子类析构时要调用父类的析构函数吗

答：析构函数调用的次序是先派生类的析构后基类的析构，也就是说在调用基类的析构时，派生类的信息已经全部销毁了。定义一个对象时，先调用基类的构造函数，然后调用派生类的构造函数；析构的时候恰好相反，先调用派生类的析构函数，然后调用基类的析构。

面试题10：什么是引用？声明和使用“引用”要注意哪些问题

答：引用就是某个目标变量的“别名”（Alias），对应用的操作与对变量直接操作效果完全相同。声明一个引用时，切记对其进行初始化。引用声明完毕后，相当于目标变量名有两个名称，即该目标原名称和引用名，不能再把该引用名作为其他变量名的别名。声明一个引用，不是新定义了一个变量，它只表示该引用名是目标变量名的一个别名，它本身不是一种数据类型，因此引用本身不占存储单元，系统也不给引用分配存储单元。不能建立数组的引用。

面试题11：将引用作为函数返回值类型的格式及优点有哪些

答：格式为类型标识符＆函数名（形参列表及类型说明）{//函数体}

优点为在内存中不产生被返回值的副本。注意正是基于这个原因，所以返回一个局部变量的引用是不可取的。因为随着该局部变量生存期的结束，相应的引用也会失效，产生runtime error！

面试题12：引用与指针的区别是什么

答：指针通过某个指针变量指向一个对象后，对其所指向的变量间接操作。程序中使用指针，程序的可读性差；而引用本身就是目标变量的别名，对引用的操作就是对目标变量的操作。

面试题13：重载和重写（覆盖）的区别

答：需要从以下两个方面来讲述。

（1）就定义而言，重载是指允许存在多个同名函数，而这些函数的参数表不同。或许参数个数不同，或许参数类型不同，也或许两者都不同。重写是指子类重新定义父类虚函数的方法。

（2）从实现原理上而言，重载编译器根据函数不同的参数表，对同名函数的名称进行修饰，然后这些同名函数就成了不同的函数（至少对于编译器来说是这样的）。例如有两个同名函数function func（p：integer）：integer；和function func（p：string）：integer；。那么编译器进行了修饰后的函数名称可能是int_func或str_func。对于这两个函数的调用，在编译器间就已经确定了是静态的。也就是说，其地址在编译期就被绑定了（早绑定），因此重载与多态无关！

重写和多态真正相关。当子类重新定义了父类的虚函数后，父类指针根据赋给它的不同的子类指针，动态的调用属于子类的该函数，这样的函数调用在编译期间是无法确定的（调用的子类的虚函数的地址无法给出）。因此，这样的函数地址是在运行期绑定的（晚绑定）。

面试题14：多态的作用是什么

答：主要有以下两方面的作用。

（1）隐藏实现细节，使得代码能够模块化；扩展代码模块，实现代码重用。

（2）接口重用。为了类在继承和派生时，保证使用家族中任一类的实例的某一属性时的正确调用。

面试题15：请指出const与#define相比有何优点

答：const的作用为定义常量、修饰函数参数及修饰函数返回值3个作用。被const修饰的内容都会受到强制保护，可以预防意外的变动，能提高程序的安全性。

（1）const常量有数据类型，而宏常量没有数据类型。编译器可以对前者进行类型安全检查，而对后者只进行字符替换，没有类型安全检查，并且在字符替换中可能会出现意料不到的错误。

（2）有些集成化的调试工具可以对const常量进行调试，但是不能对宏常量进行调试。

面试题16：C语言中static关键字的具体作用有哪些

答：在函数体中一个被声明为静态的变量在这个函数被调用过程中维持其值不变。

在模块内但在函数体外，一个被声明为静态的变量可以被模块内所有函数访问，但不能被模块外其他函数访问。它是一个本地的全局变量。

在模块内一个被声明为静态的函数只可被这一模块内的其他函数调用，即这个函数被限制在声明它的模块的本地范围内使用。

static全局变量与普通的全局变量的区别为static全局变量只初始化一次，防止在其他文件单元中被引用。

static局部变量和普通局部变量的区别为static局部变量只被初始化一次，下一次依据上一次的结果值。

static函数与普通函数的区别为static函数在内存中只有一份，普通函数在每个被调用中维持一份副本。

面试题17：如何判断程序是由C编译程序还是由C++编译程序编译的

答：

#ifdef__cplusplus

cout＜＜"c++"；

#else

cout＜＜"c"；

#endif

面试题18：关键字const的含义是什么

const int a；

int const a；

const int*a；

int*const a；

int const*a const；

答：

前两个的作用是一样，a是一个常整型数。第3个意味着a是一个指向常整型数的指针，也就是整型数是不可修改的，但指针可以修改。第4个意思是a为一个指向整型数的常指针，也就是指针指向的整型数是可以修改的，但指针是不可修改的。第5个意味着a是一个指向常整型数的常指针，也就是指针指向的整型数是不可修改的，同时指针也是不可修改的。

注意

如果应试者能正确回答以上这些问题，那么他就给测试人员留下了一个好印象。另外，即使不用关键字const，也还是能很容易写出功能正确的程序，那么为什么还要如此看重关键字const呢？有如下3个理由。

（1）关键字const的作用是为给读代码的人传达非常有用的信息，实际上，声明一个参数为常量是为了告诉用户这个参数的应用目的。如果你曾花很多时间清理过他人在编程中留下的垃圾，就能体会这点多余的文字的意义（当然，懂得应用const的程序员很少会留下垃圾让别人来清理的）。

（2）通过给优化器一些附加的信息，使用关键字const也许能产生更紧凑的代码。

（3）合理地使用关键字const可以使编译器自然地保护那些不希望被改变的参数，防止其被无意的代码修改。简而言之，这样可以减少错误的出现。

21.2　世界500强公司的智力问答题

智力问答题是测试一个面试者的智力和逻辑思考能力的一种手段。很多的大公司都非常重视这种非专业的能力测试。所以在这里专门为各位读者准备了一些常见的智力问答题及其答案，以开拓思维提高应变能力。

面试题19：工人分金条

问：你让工人工作7天，给工人的回报是1根金条。金条平分成相连的7段，你必须在每天结束时给他们发1段金条，如果只允许你弄断两次金条，该如何给你的工人发金条呢？

答：第1天给1段，第2天让工人把1段归还，然后给2段，第3天给1段，第4天归还前3天发的金条，后给4段。第5天依次类推直到7天后发完1根金条。

注意

回答这个问题主要是能够想到题目中没有提示的，可将金条回收这个环节。

面试题20：分蛋糕

问：请把一盒蛋糕切成8份，分给8个人，但蛋糕盒里还必须留有1份。

答：面对这样的智力题，有些面试者绞尽脑汁也想不出分配办法，而有些面试者却感到此题实际很简单，只需要把切成的8份蛋糕先拿出7份分给7人，剩下的1份连蛋糕盒一起分给第8个人即可。

面试题21：过桥问题

问：小明一家要过一座桥，过桥时是黑夜，所以必须有灯。现在小明过桥要1秒，小明的弟弟要3秒，小明的爸爸要6秒，小明的妈妈要8秒，小明的爷爷要12秒。每次此桥最多可过两人，而过桥的速度依过桥最慢者而定，并且灯在点燃后30秒就会熄灭。请回答小明一家如何过桥？

答：这道题的答案有3种，如下所述。

❑小明与弟弟过桥，小明回来，耗时4秒；小明与爸爸过桥，弟弟回来，耗时9秒；妈妈与爷爷过桥，小明回来，耗时13秒；最后，小明与弟弟过桥，耗时3秒，总共耗时29秒。

❑小明和弟弟过桥，小明回来，耗时4秒；爷爷和妈妈过桥，弟弟回来，耗时15秒；爸爸和小明过桥，小明回来耗时7秒；小明和弟弟过桥，耗时3秒，总共耗时29秒。

❑小明和弟弟过桥，弟弟回来，耗时6秒；爷爷和妈妈过桥，小明回来耗时13秒；爸爸和小明过桥，小明回来，耗时7秒；小明和弟弟过桥，耗时3秒；总共耗时29秒。

注意

这个题目主要的解题思路是考虑把花费时间多的两个人组合在一起过桥。

面试题22：黑白帽子

问：一群人开舞会，每人头上都戴着1顶帽子。帽子只有黑白两种颜色，黑色的至少有1顶。每个人都能看到其他人帽子的颜色，却看不到自己的帽子颜色。主持人先让大家看看别人头上戴的是什么颜色的帽子，然后关上灯，如果有人认为自己戴的是黑帽子，就打自己一个耳光。第1次关灯，没有声音；于是再开灯，大家再看一遍，第二次关灯时仍然鸦雀无声；直到第3次关灯后，才有噼里啪啦打耳光的声音响起。请指出有多少人戴着黑帽子？

答：假如只有1个人戴黑帽子，那他看到所有人都戴白帽子，那么在第1次关灯时就应自打耳光，所以应该不止1个人戴黑帽子。如果有两顶黑帽子，第1次两人都只看到对方头上的黑帽子，不敢确定自己的颜色，但到第2次关灯后，这两人应该明白，如果自己戴着白帽子，那对方早应在上1次关灯时就打耳光了，因此自己戴的也是黑帽子，于是在第2次关灯后也会有耳光声响起。可事实是第3次关灯后才响起了耳光声，说明全场不止两顶黑帽子，依次类推，应该是关了几次灯，就会有几顶黑帽子。

面试题23：电梯与钻石

问：1楼到10楼的每层电梯门口都放着1颗钻石，钻石大小不一。你乘坐电梯从1楼到10楼，每层楼电梯门都会打开一次，若只能拿1次钻石，问怎样才能拿到最大的那颗？

答：前5层楼都不拿，观察各层钻石的大小，做到心中有数；后5层楼再选择，选择大小接近前5层楼出现过的最大钻石的大小。

注意

其实至今也没有人知道这道题的标准答案，因为也许就没有标准答案，仅是考一下你的思路而已。考试只是手段，不是目的，最终需要得到一个结果，但更重要的是考生得出这个结果的分析过程，也就是针对分析方法的考查。

面试题24：手电筒传递

问：u2合唱团在17分钟内要赶到演唱会场，途中必须跨过一座桥，4个人从桥的同一端出发，你必须帮助他们到达另一端，天色很暗，而他们只有1个手电筒。每次最多可同时有两人一起过桥，而过桥的时候必须持有手电筒，所以就要由人传递手电筒。4个人的步行速度各不相同，若两人同行则以较慢者的速度为准。BONO需花1分钟过桥，EDGE需花2分钟过桥，ADAM需花5分钟过桥，LARRY需花10分钟过桥。他们要如何在17分钟内过完桥呢？

答：BONO和EDGE先过EDGE返回送手电，耗时4分钟；ADAM和LARRY过桥，BONO返回给EDGE送手电，耗时11分钟；最后BONO和EDGE同时过桥，耗时2分钟。总时间为4+11+2=17分钟。

面试题25：烧绳问题

问：烧1根不均匀的绳子要用1个小时，如何用绳子来准确判断半个小时？

答：两边一起烧。

注意

这道题考核的是人的变向思维能力。

面试题26：圆的下水道盖子

问：为什么下水道的盖子是圆的？

答：第一个可能是在同等耗材的情况下其面积最大。第二个可能是因为如果是方的、长方的或椭圆的，那无聊的人拿起来就可以直接扔进下水道。但由于圆形的盖子周长一样，怎么放都不会掉入下水道中，所以就可以避免前面这种情况的发生。

面试题27：选建筑师

问：一位国王要修建宫殿，想聘请一位建筑师主持设计。于是，他召集了全国所有著名的建筑师，让他们考虑是否要报名。如果要报名就需要再推荐另一位建筑师作为自己的助手。当看完所有报名候选建筑师的材料后，国王轻而易举地挑出了最后的人选，请问被选出的建筑师是谁？（提示：这些建筑师谁都不愿意主动放弃这次机会）。

答：最后的人选是被提名当助手次数最多的建筑师。每位建筑师都会把自己说成是主设计师最合适的人选，可是在考虑助手时，则会挑选自己认为是最好的建筑师，因此被提名最多的建筑师，自然就是最合适的人选。

面试题28：天平分盐

问：有7克和2克砝码各1个，天平1个，如何只用这些物品3次将140克的盐分成50克与90克各1份？

答：天平一边放7克+2克=9克砝码，另一边放9克盐；天平一边放7克砝码和刚才得到的9克盐，另一边放16克盐；天平一边放刚才得到的16克盐和先前得到的9克盐，另一边放25克盐。最后将称出的盐相加16克+9克+25克=50克盐，剩下的就是90克盐。

面试题29：分不开的钱

问：小王和小李去郊外游玩，两个人都带了饼干当午餐，小王带了15块，小李带了9块。路上他们觉得肚子饿了，便坐下来打算吃饼干。这时，走来1位过路人，说自己没带吃的东西，想花钱购买他们的饼干，小王和小李同意了。于是，这3个人平分了这些饼干。吃完后过路人留下了2.40元。小王收了其中的1.50，留下0.90给小李，小李却认为很不公平，认为自己应该得到一半的钱。最后两人找来1位过路人评理。如果你是那个过路人，会怎么分配这些钱呢？

答：这是个有关分数的问题，3个人共有24块饼干，因为是平均分成3份，所以每人分得8块饼干。小王把自己多出来的7块饼干给了过路人，而小李只多出了1块。过路人吃的8块饼干中，有小王的7块和小李的1块，比例为7:1。2.40元中应该给小王2.10元，给小李0.30元。

面试题30：小鸟追火车

问：有1辆火车以每小时15公里的速度离开洛杉矶直奔纽约，另一辆火车以每小时20公里的速度从纽约开往洛杉矶。如果有1只鸟，以每小时30公里的速度和两辆火车同时出发，它从洛杉矶出发，碰到另1辆车后返回，依次在两辆火车之间来回飞行，直到两辆火车相遇，请回答这只小鸟飞行了多长距离？

答：假设洛杉矶到纽约的距离为s，那小鸟飞行的距离可以用以下公式计算出来，如图21.1所示。

图　21.1　小鸟飞行距离公式

面试题31：给红色最大的机会

问：你有两个罐子、50个红色弹球和50个蓝色弹球，随机选出1个罐子，随机选取出1个弹球放入罐子，怎样给红色弹球最大的选中机会？在你的计划中，得到红球的准确几率是多少？

答：自己睁着眼睛挑一个红色的球啊，这是给红色最大的机会了，除非你是色盲。

注意

这道题考查的其实是看你有没有魄力坚持自己的意见。

面试题32：镜子中的影像

问：想象你站在镜子前，请回答为什么镜子中的影像可以颠倒左右，却不能颠倒上下？

答：因为人的两眼在水平方向上对称，而不是垂直方向上对称。

面试题33：污染的药丸

问：你有4个装药丸的罐子，每个药丸都有一定的重量，被污染的药丸是没被污染的药丸重量+1。只准称量1次该如何判断出哪个罐子的药丸被污染了？

答：分别给4个罐子编上号，即1、2、3和4。然后1号拿1个，2号拿2个，3号拿3个，4号拿4个，称一下，若是都没被污染，应该重10个药丸的重量，若是11个药丸的重量就是1号罐，12个药丸的重量就是2号罐，13个药丸的重量就是3号罐，14个药丸的重量就是4号罐。

注意

这道题有个隐含的前提条件，即计算标准的10颗药丸重量，与现在的10颗药丸重量比较。

面试题34：称出不是标准重量的水

问：如果你有很多水，并有一个3升和一个5升的提桶，如何能准确地称出4升的水？

答：其称出4升水的过程如表21.1所示。

面试题35：多彩的果冻

问：你有一桶果冻，其中有黄色、绿色和红色3种不同颜色的果冻，如果闭上眼睛抓取同样颜色的两个，那么需抓取多少个就可以确定你肯定有两个同样颜色的果冻？

答：4个（假设前3个各不同，第4个肯定与前3个中的一种颜色相同）。

面试题36：电子游戏机

问：汤姆和吉米都十分喜欢玩电子游戏，一天他们各自出了100美元，合买了一台游戏机，准备轮流玩。可是没过两天，他们都对这个游戏机上瘾了，都想据为己有。于是他们商量用拍卖的方法来决定它的归属。拍卖的规则是以1美元为单位，两人各自把自己的拍卖价格写在一张纸上，然后同时打开。哪方的价格高，就能拥有这台游戏机，同时把对方所写的价格作为补偿付给对方。汤姆虽然很想在拍卖中胜出，但他更不愿自己吃亏。他究竟该如何出价呢？

答：汤姆应出101美元。如果吉米的出价是100美元，汤姆得到游戏机，并给吉米100元，加上购买时的100美元，他就等于花了200美元买了这台游戏机，一点儿也没有吃亏。如果吉米的出价低于100美元，汤姆所花的钱将少于200美元，只赚不赔。要是吉米的出价高于101美元，汤姆就能拿回101美元。同样赚到1美元。要是汤姆选择的出价高于101美元，只要吉米的出价比他低（但仍然高于100美元），汤姆就要多花钱了。

面试题37：监狱里的囚犯

问：监狱里有12名囚犯，每天早6：00～晚18：00之间轮流出去放风。一天，有名囚犯病了，看守人员让剩余的11名囚犯平均分配这12个小时，但他们只能看到监狱里钟楼上的大钟。他们该怎么分配这段放风时间才公平呢？

答：看大钟从早6：00开始～晚18：00结束，除最后1次外，一共出现11次时针和分针夹角为180˚的情况，于是就把12个小时等分成11份了。

面试题38：灯的状态

问：对一批编号为1～100全部开关朝上开的灯进行以下操作，凡是1的倍数反方向拨一次开关；2的倍数反方向再拨一次开关；3的倍数反方向再拨一次开关。那最后为关闭状态的灯的编号是多少？

答：根据题目我们可以知道，号码为N的灯，拨开关的次数等于N的约数的个数。

要想使灯关闭，约数的个数应该是奇数。

而一般来说，任何一个数N都至少有两个约数，即1和N本身。

其他任何一个约数都是一一对应的（例如6的约数中2和3对应）。

从理论上来讲，每个数的约数的个数都应该是偶数。

只有一种例外的情况，即某数中两个互相对应的约数相等，例如4的约数中2的对应约数也为2。

这样的数才能有奇数个约数。

即N必须为某数的平方。

100以内最大的平方数为100，即10的平方。

最小的平方数为1，即1的平方。

那么100以内的平方数总共只能有10个，如下所示。

1^2=1

2^2=4

3^2=9

4^2=16

5^2=25

6^2=36

7^2=49

8^2=64

9^2=81

10^2=100

而这些平方数就是所求的编号数。

面试题39：时针和分针

问：假设时钟到了12：00。注意时针和分针重叠在一起。在1天之中，时针和分针共重叠多少次？你知道它们重叠时的具体时间吗？

答：这可以归结为速度差的相遇问题，分钟每走1圈都会和时针相遇，即1天中有几小时就会相遇几次。重点是算相遇的具体时间。12：00过后分针和时针的第一圈，它们的路程是1圈（60分钟），速度差是55分钟，即时间是60/55。也就是说第1圈它们大约在1：05。第2圈，它们的路程是120分钟，速度差还是55分钟，它们相遇的时间大约是2：10。依次类推。总计是24次。

相遇时间如下所示。

1：05；2：10；3：15；4：20；5：25；6：30；7：35；8：40；9：45；10：50；11：55；12：00；13：05；14：10；15：15；16：20；17：25；18：30；19：35；20：40；21：45；22：50；23：55；24：00.

面试题40：3个奇数

问：中间只隔1个数字的两个奇数被称为奇数对，例如17和19。证明奇数对之间的数字总能被6整除（假设这两个奇数都大于6）。如何证明没有由3个奇数组成的奇数对？

答：不能证明，比如13和15之间的数字是14，就不能被6整除。根据奇数的定义，不能被2整除的数叫做奇数，那么奇数和偶数一定是互相交替的，这样就不可能存在连续两个数都是奇数，自然也就不存在由3个奇数组成的奇数对了。

注意

这道题考核的是应聘者的逻辑和数学知识，而不是专业能力。

面试题41：屋内的灯

问：1个屋子有1扇门（门是关闭的）和3盏电灯。屋外有3个开关，分别与这3盏灯相连。你可以随意操纵这些开关，可一旦你将门打开，就不能变换开关位置了。如何确定每个开关具体管哪盏灯？

答：先在门外打开1个开关，等一段时间后关掉它，再打开另1个开关之后进入屋内，热而不亮的那个灯泡便是由第1个开关所控制的，亮的便是由第2个开关控制的，既不亮又不热的灯泡便是由第3个开关所控制的了。

面试题42：找出最重的球

问：假设你有8个球，其中一个略微重一些，但是找出这个球的唯一方法是将两个球放在天平上进行对比。最少要称多少次才能找出这个较重的球？

答：将球分成3组，其中2组有3个球，1组有2个球。先称有3个球的那2组。如果天平平衡，那么质量不同的球就在有2个球的那1组里，再称2个球，便分出来了；如果其中的1组球较重，那么质量不同的球就在较重的那1组里。在较重的这组球中随便拿出2个球称，如果2个球一样重，那么质量不同的就是第3个球；如果不一样重，较重的那1个就是质量不同的球。

面试题43：盲人分袜子

问：有两位盲人，他们都各自买了四双黑袜和四双白袜，八双袜子的质地和大小完全相同，而每双袜子都由1张商标纸连着。两位盲人不小心将八双袜子混在了一起。他们每人怎样才能选出黑袜和白袜各两双呢？

答：将每双袜子拆开后一人一只平分。

面试题44：烧香计时

问：有两根不均匀分布的香，香烧完的时间是1个小时，你怎样来确定15分钟的时间？

答：香a点燃一头，香b点燃两头；等香b烧完时，时间过去了30分钟；再把香a剩下的另一头也点燃；从这时起到香a烧完的时间就是15分钟。

面试题45：平分蜂蜜

问：小明和小华买了10斤蜂蜜，装在一个大瓶子里。他们要把蜂蜜平分，现在只有两个空瓶子，一个正好装7斤，另一个正好装3斤，怎样才能用最简单的方法把蜂蜜分出来？

答：分蜂蜜过程如表21.2所示。

面试题46：猜经理女儿的年龄

问：1位经理有3个女儿，3个女儿的年龄相加等于13，3个女儿的年龄相乘等于经理自己的年龄，有1位下属已知道经理的年龄，但仍不能确定经理3个女儿的年龄，这时经理说只有1个女儿的头发是黑的，然后这1位下属就知道了经理3个女儿的年龄。请问3个女儿的年龄分别是多少？为什么？

答：3个女儿的年龄应该是2岁、2岁和9岁。因为只有1个孩子黑头发，即只有她长大了，其他两个还是幼年时期即小于3岁，头发为淡色。再结合经理的年龄应该至少大于25岁。

面试题47：两个圆环

问：有两个圆环半径分别为1和2，小圆在大圆内部绕大圆圆周一圈，问小圆自身转了几圈？如果在大圆的外部，小圆自身转几圈？

答：无论内外，小圆转两圈。

面试题48：喝啤酒

问：假如每3个空啤酒瓶可以换1瓶啤酒，某人买了10瓶啤酒，那么他最多可以喝到多少瓶啤酒？

答：喝完10瓶后用9个空瓶换来3瓶啤酒（喝完后有4个空瓶），喝完这3瓶又可以换到1瓶啤酒（喝完后有2个空瓶），这时他有2个空酒瓶，如果他能向老板先借1个空酒瓶，就凑够了3个空瓶又可以换到1瓶啤酒，把这瓶啤酒喝完后将空瓶还给老板就可以了。所以他最多可以喝到10瓶+3瓶+1瓶+1瓶=15瓶啤酒。

注意

这道题主要是考虑到最后用两个空瓶子换1瓶啤酒，然后把空瓶子还给老板。考核的是应聘者的创新思维。

面试题49：算24点

问：运用4、4、10和10，只用加减乘除怎么计算出24来？

答：计算公式如图21.2所示。

图　21.224　点计算公式

面试题50：聪明人

问：从前，有甲乙两个相邻的国家，他们的关系很好，货币可以通用，也就是说甲国的100元等于乙国的100元。可是两国的关系最近破裂了，虽然他们的贸易往来仍然继续，货币也能互相兑换，但两国的国王却宣布对方的100元只能兑换本国的货币的90元。有个聪明人借机发了一笔横财，你知道他是怎么做的吗？

答：他用甲国的货币100元在甲国购物10元，商家本来应该找给他90元甲国的货币，而这个人声称自己要到乙国去，要求商家找给乙国的钞票，因为此时甲国的90元等于乙国的100元，所以商家找给他100元的乙国货币。然后，他再用这100元到乙国购物10元，再要求找给他100元的甲国的货币，之后再回到甲国购物。这样这个聪明人手中的钱始终保持不变，买的东西却越来越多了，如此下去，这个人自然能借机捞到一大笔。

面试题51：海盗分金币

问：5个海盗抢到100枚金币后，讨论如何进行公正分配。他们商定的分配原则如下。

（1）抽签确定各人的分配顺序号码1、2、3、4及5。

（2）由抽到1号签的海盗提出分配方案，然后5人进行表决，如果方案得到超过半数的人同意，就按照他的方案进行分配，否则就将抽到1号签的海盗扔进大海喂鲨鱼。

（3）如果抽到1号签的海盗被扔进大海，则由抽到2号签的海盗提出分配方案，再由剩余的4人进行表决，如果方案得到超过半数的人同意时，就按照他的方案进行分配，否则也将其扔入大海。

（4）依次类推。

这里假设每1个海盗都是绝顶聪明而理性的，他们都能够进行严密的逻辑推理，并能很理智地判断自身的得失，即能够在保住性命的前提下得到最多的金币。同时还假设每一轮表决后的结果都能顺利得到执行，那么抽到1号签的海盗应该提出怎样的分配方案才能使自己既不被扔进海里，又可以得到较多的金币呢？

答案：如表21.3所示。

推理方法：如果当对3的方案表决时，4一定会支持3，否则的话他就要被5反对，从而死。因此，如果1，2死了，3的方案肯定是100，0，0，并且一定会得到3和4的支持，此时4，5的收入为0，因此1，2可以贿赂4，5而得到支持。同时3的期望收入为100，他必定会不顾一切地反对1，2。而如果1死了，2的方案肯定是98，0，1，1，并且一定会通过。所以1的最优方案为96，0，0，2，2，并且一定会通过。其实98，0，0，1，1也可以，并且有可能通过（看4，5的心情和残忍程度而定）。

面试题52：到底是什么牌

问：S先生、P先生和Q先生他们知道桌子的抽屉里有16张扑克牌：红桃A、Q和4，黑桃J、8、4、2、7和3，草花K、Q、5、4和6及方块A和5。约翰教授从这16张牌中挑出1张牌来，并把这张牌的点数告诉P先生，把这张牌的花色告诉Q先生。这时，约翰教授问P先生和Q先生，你们能从已知的点数或花色中推知这张牌是什么牌吗？于是S先生听到如下的对话。

P先生说我不知道这张牌。

Q先生说我知道你不知道这张牌。

P先生说现在我知道这张牌了。

Q先生说我也知道了。

听罢以上的对话，S先生想了想之后，就正确地推测出了这张牌。这张牌究竟是什么牌？

答：方块5，其推理过程如下。

由第1句话“P先生说我不知道这张牌。”可知，此牌的点数必出现在两种或两种以上花色中，即可能是A、Q、4和5。如果此牌只有一种花色，P先生已知道这张牌的点数，肯定就能知道这张牌。

由第2句话“Q先生说我知道你不知道这张牌。”可知，此花色牌的点数只能包括A、Q、4、5，符合此条件的只有红桃和方块。Q先生知道此牌花色，只有红桃和方块花色包括A、Q、4、5，Q先生才能作出断言。

由第3句话“P先生说现在我知道这张牌了。”可知，P先生通过“Q先生说我知道你不知道这张牌。”判断出花色为红桃和方块，P先生已知道这张牌的点数，P先生便知道这张牌。据此排除A，此牌可能是Q、4、5。如果此牌点数为A,P先生还是无法判断。

由第4句话“Q先生说我也知道了。”可知，花色只能是方块。如果是红桃，Q先生排除A后，还是无法判断是Q还是4。综上所述，这张牌是方块5。

面试题53：聪明的老板娘

问：据说有人给酒肆的老板娘出了一个难题，此人明知店里只有两个舀酒的勺子，分别能舀7两和11两酒，却硬要老板娘卖给他2两酒。聪明的老板娘毫不含糊，用这两个勺子在酒缸里舀酒，居然量出了2两酒，聪明的你能做到吗？

答：其舀酒的过程如表21.4所示。

面试题54：找出不同重量的球

12个球和1个天平，已知只有1个和其他的球重量不同，问怎样称才能用3次就找到那个球？（注意此题并未说明那个球的重量是轻是重，所以需要仔细考虑）

答案：首先证明，如果有3个球P1、P2和P3满足，要么P1较重，要么P2、P3中有1个较轻，并且有2个标准球，则质量不同的那个可以用天平一次找出。事实上，取P1，P2与标准球比较，如果平衡则P3为较轻，如果P1、P2质量之和大于标准球则P1为较重的球，如果P1、P2质量之和小于标准球则P2为较轻的球。同理可得，P1、P2、P3满足要么P1较轻，要么P2、P3中有一个较重的情况，同样可以一次找出非标准球。

将球先分成3批（标记为A、B、C组），每批4个，取A、B两批称量。如果平衡，则质量不同的球在C组，可以用两次称量找出（先取两个与标准球作比较，如果平衡再在余下的两个中取1个与标准球作比较，如果不平衡，则在其中取一个与标准球作比较）。如果不平衡（不妨假定C组轻于B组），则C组为标准球。将A,B排列如图21.3所示。

图　21.3　A和B组排列

取A1，A2，B1（A'组）与A3，A4，B4（B'组）分别放在天平两边称量。如果A'组轻于B'组，则要么A1，A2中有较轻的，要么B4为较重的，由前面的证明知，第3次称量可以找出质量不同的那个。如果A'组重于B'组，则要么B1为较重的，要么A3，A4中有较轻的，同样可以找出质量不同的那个。如果平衡，则B2，B3中有较重的，分别放在天平两端即可找出较重的。

21.3　分析题

分析题主要是对应聘者阅读程序能力的考核，所以在这一节中主要对一些常见的基本分析题进行介绍。

面试题55：分析下面的程序段，显示最后的结果

#include＜iostream＞

using namespace std；

void fun（int,int,int*）；

void main（）

{

int x,y，z；

fun（2，3，＆x）；

fun（4，x，＆y）；

fun（x,y，＆z）；

cout＜＜x＜＜'，'＜＜y＜＜'，'＜＜y＜＜endl；

}

void fun（int a,int b,int*c）

{

b*=a；

*c=b-a；

}

答：显示结果为4，12，44。

面试题56：分析程序最后的输出结果

#include＜iostream＞

using namespace std；

void main（）

{

float a=1.0f；

cout＜＜（int）a＜＜endl；

cout＜＜（int＆）a＜＜endl；

cout＜＜boolalpha＜＜（（int）a==（int＆）a）＜＜endl；//输出什么

float b=0.0f；

cout＜＜（int）b＜＜endl；

cout＜＜（int＆）b＜＜endl；

cout＜＜boolalpha＜＜（（int）b==（int＆）b）＜＜endl；

}

答：从上到下依次输出的结果如下所示。

01　1//转换成整数输出

02　1065363216//意思是将a代表的内存中的二进制数当成整型数据来看待

03　false//输出比较后的bool型结果

04　0 //转换成整数输出

05　0 //直接得到b在内存单元的值

06　true//输出比较后的bool型结果

面试题57：分析最后输出的结果

01　#include＜iostream＞

02　using namespace std；

03　class B

04　{

05　public：

06　B（）

07　{

08　cout＜＜"default constructor"＜＜endl；

09　}

10　～B（）

11　{

12　cout＜＜"destructed"＜＜endl；

13　}

14　B（int i）：data（i）//B（int）works as a converter（int-＞instance of B）

15　{

16　cout＜＜"constructed by parameter"＜＜data＜＜endl；

17　}

18　private：

19　int data；

20　}；

21

22　B Play（B b）

23　{

24　return b；

25　}

26

27　int main（int argc,char*argv[]）

28　{

29　B t1=Play（5）；

30　B t2=Play（t1）；

31　return 0；

32　}答：上面程序最后

输出结果如下所示。

constructed by parameter 5

destructed

destructed

destructed

destructed

面试题58：求下面函数的返回值

int func（x）

{

int countx=0；

while（x）

{

countx++；

x=x＆（x-1）；

}

return countx；

}

假定x=9999。

答：返回值为8。

技巧将x转化为二进制，看含有的1的个数。

面试题59：分析最后输出的结果

01　#include＜iostream＞

02　using namespace std；

03　class B

04　{

05　public：

06　B（）

07　{

08　cout＜＜"default constructor"＜＜endl；

09　}

10　～B（）

11　{

12　cout＜＜"destructed"＜＜endl；

13　}

14　B（int i）：data（i）//B（int）works as a converter（int-＞instance of B）

15　{

16　cout＜＜"constructed by parameter"＜＜data＜＜endl；

17　}

18　private：

19　int data；

20　}；

21

22　B Play（B b）

23　{

24　return b；

25　}

26

27　int main（int argc,char*argv[]）

28　{

29　B t1=Play（5）；

30　B t2=Play（10）；

31　return 0；

32　}答：最后输出的结果如下所示。

constructed by parameter 5

destructed

constructed by parameter 10

destructed

destructed

destructed

面试题60：分析最后输出的结果

01　#include＜iostream.h＞

02

03　class A

04　{

05　public：

06　virtual void print（void）

07　{

08　cout＜＜"A：print（）"＜＜endl；

09　}

10　}；

11　class B：public A

12　{

13　public：

14　virtual void print（void）

15　{

16　cout＜＜"B：print（）"＜＜endl；

17　}；

18　}；

19　class C：public B

20　{

21　public：

22　virtual void print（void）

23　{

24　cout＜＜"C：print（）"＜＜endl；

25　}

26　}；

27　void print（A a）

28　{

29　a. print（）；

30　}

31　void main（void）

32　{

33　A a，*pa，*pb，*pc；

34　B b；

35　C c；

36　pa=＆a；

37　pb=＆b；

38　pc=＆c；

39　a. print（）；

40　b. print（）；

41　c. print（）；

42　pa-＞print（）；

43　pb-＞print（）；

44　pc-＞print（）；

45　print（a）；

46　print（b）；

47　print（c）；

48　}答：最后输出的结果如下所示。

A：print（）

B：print（）

C：print（）

A：print（）

B：print（）

C：print（）

A：print（）

A：print（）

A：print（）

21.4　编程题

编程题主要是考核应聘者的C++编程的专业知识，这里选取了9个常见的编程示例进行介绍。

面试题61：结果输出到文件

问：文件中有一组整数，要求排序后输出到另一个文件中。

答：编写的程序如下所示。

01　#include＜iostream＞

02　#include＜fstream＞

03　#include＜vector＞

04　using namespace std；

05

06　void Order（vector＜int＞＆data）//bubble sort

07　{

08　int count=data. size（）；

09　int tag=false；//设置是否需要继续冒泡的标志位

10　for（int i=0；i＜count；i++）

11　{

12　for（int j=0；j＜count-i-1；j++）

13　{

14　if（data[j]＞data[j+1]）

15　{

16　tag=true；

17　int temp=data[j]；

18　data[j]=data[j+1]；

19　data[j+1]=temp；

20　}

21　}

22　if（！tag）

23　break；

24　}

25　}

26

27　void main（void）

28　{

29　vector＜int＞data；

30　ifstream in（"c：/data. txt"）；

31　if（！in）

32　{

33　cout＜＜"file error！"；

34　exit（1）；

35　}

36　int temp；

37　while（！in. eof（））

38　{

39　in＞＞temp；

40　data. push_back（temp）；

41　}

42　in. close（）；//关闭输入文件流

43　Order（data）；

44　ofstream out（"c：/result. txt"）；

45　if（！out）

46　{

47　cout＜＜"file error！"；

48　exit（1）；

49　}

50　for（int i=0；i＜data. size（）；i++）

51　out＜＜data[i]＜＜""；

52　out. close（）；//关闭输出文件流

53　}

面试题62：String类的具体实现

问：已知String类的定义如下。

class String

{

public：

String（const char*str=NULL）；//通用构造函数

String（const String＆another）；//复制构造函数

～String（）；//析构函数

String＆operater=（const String＆rhs）；//赋值函数

private：

char*m_data；//用于保存字符串

}；

尝试写出类的成员函数实现。

答：编写的程序如下所示。

01　String：String（const char*str）

02　{

03　if（str==NULL）//strlen在参数为NULL时会抛异常，才会有这步判断

04　{

05　m_data=new char[1]；

06　m_data[0]='/0'；

07　}

08　else

09　{

10　m_data=new char[strlen（str）+1]；

11　strcpy（m_data,str）；

12　}

13

14　}

15

16　String：String（const String＆another）

17　{

18　m_data=new char[strlen（another. m_data）+1]；

19　strcpy（m_data,other. m_data）；

20　}

21

22　String＆String：operator=（const String＆rhs）

23　{

24　if（this==＆rhs）

25　return*this；

26　delete[]m_data；//删除原来的数据，新开一块内存

27　m_data=new char[strlen（rhs. m_data）+1]；

28　strcpy（m_data,rhs. m_data）；

29　return*this；

30　}

31

32　String：～String（）

33　{

34　delete[]m_data；

35　}

面试题63：链表题，一个链表的节点结构

struct Node

{

int data；

Node*next；

}；

typedef struct Node Node；

问：（1）已知链表的头节点head，写一个函数把这个链表逆序。（Intel公司题目）

答：编写的程序如下所示。

01　Node*ReverseList（Node*head）//链表逆序

02　{

03　if（head==NULL||head-＞next==NULL）

04　return head；

05　Node*p1=head；

06　Node*p2=p1-＞next；

07　Node*p3=p2-＞next；

08　p1-＞next=NULL；

09　while（p3！=NULL）

10　{

11　p2-＞next=p1；

12　p1=p2；

13　p2=p3；

14　p3=p3-＞next；

15　}

16　p2-＞next=p1；

17　head=p2；

18　return head；

19　}问：（2）已知两个链表head1和head2各自有序，请把它们合并成一个链表依然有序。（保留所有节点，即便大小相同）。（微软公司题目）答：编写的程序如下所示。

01　Node*Merge（Node*head1，Node*head2）

02　{

03　if（head1==NULL）

04　return head2；

05　if（head2==NULL）

06　return head1；

07　Node*head=NULL；

08　Node*p1=NULL；

09　Node*p2=NULL；

10　if（head1-＞data＜head2-＞data）

11　{

12　head=head1；

13　p1=head1-＞next；

14　p2=head2；

15　}

16　else

17　{

18　head=head2；

19　p2=head2-＞next；

20　p1=head1；

21　}

22　Node*pcurrent=head；

23　while（p1！=NULL＆＆p2！=NULL）

24　{

25　if（p1-＞data＜=p2-＞data）

26　{

27　pcurrent-＞next=p1；

28　pcurrent=p1；

29　p1=p1-＞next；

30　}

31　else

32　{

33　pcurrent-＞next=p2；

34　pcurrent=p2；

35　p2=p2-＞next；

36　}

37　}

38　if（p1！=NULL）

39　pcurrent-＞next=p1；

40　if（p2！=NULL）

41　pcurrent-＞next=p2；

42　return head；

43　}问：（3）已知两个链表head1和head2各自有序，请把它们合并成一个链表依然有序，这次要求用递归方法进行。（Autodesk公司题目）答：编写的程序如下所示。

44　Node*MergeRecursive（Node*head1，Node*head2）

45　{

46　if（head1==NULL）

47　return head2；

48　if（head2==NULL）

49　return head1；

50　Node*head=NULL；

51　if（head1-＞data＜head2-＞data）

52　{

53　head=head1；

54　head-＞next=MergeRecursive（head1-＞next,head2）；

55　}

56　else

57　{

58　head=head2；

59　head-＞next=MergeRecursive（head1，head2-＞next）；

60　}

61　return head；

62　}

面试题64：写一个函数并找出在一个整数数组中第二大的数

答：编写程序如下。（微软公司题目）

01　const int MINNUMBER=-32767；

02　int find_sec_max（int data[]，int count）

03　{

04　int maxnumber=data[0]；

05　int sec_max=MINNUMBER；

06　for（int i=1；i＜count；i++）

07　{

08　if（data＞maxnumber）

09　{

10　sec_max=maxnumber；

11　maxnumber=data；

12　}

13　else

14　{

15　if（data＞sec_max）

16　sec_max=data；

17　}

18　}

19　return sec_max；

20　}

面试题65：字符串的逆序输出

问：如何将一个字符串，在输入后将其逆序输出。

答：编写的程序如下。

01　#include＜iostream＞

02　using namespace std；

03

04　void main（）

05　{

06　char a[50]；memset（a，0，sizeof（a））；

07　int i=0，j；

08　char t；

09　cin. getline（a，50，'/n'）；

10　for（i=0，j=strlen（a）-1；i＜strlen（a）/2；i++，j--）

11　{

12　t=a[i]；

13　a[i]=a[j]；

14　a[j]=t；

15　}

16　cout＜＜a＜＜endl；

17　}

面试题66：判断操作系统的位数

问：编写一段C++程序，判断一个操作系统是16位还是32位，不能用sizeof函数。

答：编写的程序如下。

01　#include＜iostream＞

02　using namespace std；

03

04　void main（）

05　{

06　int ni=65536；//16位的系统下输出为0，32位的系统下输出为65536

07　long li=0；

08　long ln=65536；

09　if（li==ni）

10　{

11　cout＜＜"该系统是16位系统"＜＜endl；

12　}

13　else if（ni==ln）

14　{

15　cout＜＜"该系统是32位系统"＜＜endl；

16　}

17　}

面试题67：实现对数组的降序排序

问：如何将一个指定数组的数据进行降序排列。

答：编写的程序如下。

01　#include＜iostream＞

02　using namespace std；

03　void sort（int*arr,int n）；

04

05　void main（）

06　{

07　int array[]={33，56，76，45，212，1，34，123，2，36，77}；

08　sort（array，11）；

09　for（int i=0；i＜=10；i++）//曾经在这儿出界

10　cout＜＜array[i]＜＜""；

11　cout＜＜endl；

12　system（"pause"）；

13　}

14

15　void sort（int*arr,int n）

16　{

17　int temp；

18　for（int i=1；i＜9；i++）

19　{

20　for（int k=0；k＜9-i；k++）//曾经在这儿出界

21　{

22　if（arr[k]＜arr[k+1]）

23　{

24　temp=arr[k]；

25　arr[k]=arr[k+1]；

26　arr[k+1]=temp；

27　}

28　}

29　}

30　}

面试题68：斐波那契数列1、1、2、3、5等，编写程序求第10项

问：斐波那契数列1、1、2、3、5等，编写程序求第10项，必须使用递归算法。

答：编写的程序如下所示。

01　#include＜iostream＞

02　using namespace std；

03　int Pheponatch（int n）；

04

05　void main（）

06　{

07　int n；

08　cin＞＞n；

09　int ph=Pheponatch（n）；

10　cout＜＜ph＜＜endl；

11　system（"pause"）；

12　}

13

14　int Pheponatch（int n）

15　{

16　if（n＜=0）

17　exit（-1）；

18　else

19　if（n==1||n==2）

20　return 1；

21　else

22　return Pheponatch（n-1）+Pheponatch（n-2）；

23　}