跳过正文

《C++ Primer Plus》第六版笔记01

··39752 字·80 分钟
目录
《C++ Primer Plus》第六版 - 这篇文章属于一个选集。
§ 1: 本文

第一章 预备知识
#

C++总览简介

C++ 是一种静态类型的、编译式的、通用的、大小写敏感的、不规则的编程语言,支持过程化编程、面向对象编程和泛型编程。 C++ 被认为是一种中级语言,它综合了高级语言和低级语言的特点。 C++ 是由 Bjarne Stroustrup 于 1979 年在新泽西州美利山贝尔实验室开始设计开发的。C++ 进一步扩充和完善了 C 语言,最初命名为带类的C,后来在 1983 年更名为 C++。 C++ 是 C 的一个超集,事实上,任何合法的 C 程序都是合法的 C++ 程序。

注意:使用静态类型的编程语言是在编译时执行类型检查,而不是在运行时执行类型检查。

面向对象程序设计

C++最大的亮点就是面向对象程序设计理念的运用。包括面向对象开发的四大特性: 封装 抽象 继承 多态

C++的组成部分

标准的 C++ 由三个重要部分组成: 核心语言,提供了所有构件块,包括变量、数据类型和常量,等等。 C++ 标准库,提供了大量的函数,用于操作文件、字符串等。 标准模板库(STL),提供了大量的方法,用于操作数据结构等。

第二章 开始学习 C++
#

(略)这一章很简单,无笔记。

C++用分号隔开每一个执行语句。

第三章 处理数据
#

面向对象编程(OOP)的本质是设计并扩展自己的数据类型。设计自己的数据类型就是让类型与数据匹配。

内置的C++类型分两组:基本类型和复合类型。本章将介绍基本类型,即整数和浮点数。似乎只有两种类型,但C++知道,没有任何一种整型和浮点型能够满足所有的编程要求,因此对于这两种数据,它提供 了多种变体。

3.1 简单变量
#

把信息存储在计算机中,程序必须记录3个基本属性:

  • 信息将存储在哪里;
  • 要存储什么值;
  • 存储何种类型的信息。

实际上,程序将找到一块能够存储整数的内存,将该内存单元标记为braincount,并将5复制到该内存单元中;然 后,您可在程序中使用braincount来访问该内存单元。

可以使用&运算符来检索braincount的内存地址。

3.1.1 变量名
#

C++ 提倡使用有一定含义的变量名,并遵循几 种简单的 C++ 命名规则:

  • 在名称中只能使用字母字符、数字和下划线 ( _ );
  • 名称的第一个字符不能是数字;
  • 区分大写字符与小写字符;
  • 不能将C++关键字用作名称;
  • 两个下划线下划线+大写字母 打头的名称被保留给实现(编译器及其使用的资源)使用;
  • 一个下划线 开头的名称被保留给实现,用作全局标识符;

C++对于名称的长度没有限制,名称中所有的字符都有意义,但有些平台有长度限制。

倒数第二点与前面几点有些不同,因为使用像 __time_stop 或 _Donut 这样的名称不会导致编译器错误,而会导致行为的不确定性。换句话说,不知道结果将是什么。不出现编译器错误的原因是,这样的名称不 是非法的,但要留给实现使用。

如果想用两个或更多的单词组成一个名称,通常的做法是用下划线字符将单词分开,如 my_onions (用这类);或者从第二个单词开始将每个单词的第一个字母大写,如 myEyeTooth。

在 C++ 所有主观的风格中,一致性和精度是最重要的。请根据自己的需要、喜好和个人风格来使用变量名(或必要时,根据雇主的需要、喜好和个人风格来选择变量名)。

3.1.2 整型
#

不同C++整型使用不同的内存量来存储整数。使用的内存量越大,可以表示的整数值范围也越大。

C++的基本整型(按宽度递增的顺序排列)分别是char、short、int、long和C++11新增的long long,其中每种类型都有符号版本和无符号版本,因此总共有10种类型可供选择。

3.1.3 整型short、int、long和long long
#

C++ 的short、int、long和long long类型通过使用不同数目的位来存储值,最多能够表示4种不同的整数宽度:

  • short至少16位;
  • int至少与short一样长;
  • long 至少32位,且至少与int一样长;
  • long long至少64位,且至少与long一样长。

这意味着可以把16位单元设置成65 536个不同的值,把 32位单元设置成4 294 672 296个不同的值,把64位单元设置为18 446 744 073 709 551 616个不同的值。作为比较,unsigned long存储不了地球上当前的人数和银河系的星星数,而long long 能够。

当前很多系统都使用最小长度,即short为16位,long为32位。这仍然为int提供了多种选择,其宽度可以是16位、24位或32位,同时又符合标准;甚至可以是64位,因为long和long long至少长64位。

类型的宽度随实现而异,这可能在将C++程序从一种环境移到另一种环境(包括在同一个系统中使用不同编译器)时引发问题。

首先,sizeof运算符返回类型或变量的长度;其次,头文件climits(在老式实现中为limits.h)中包含了关于整型限制的信息。具体地说,它定义了表示各种限制的符号名称。例如,INT_MAX为int的最大取值,CHAR_BIT为字节的位数。

在我的 Macbook Air 64位 OS 上,数据类型是这样的:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
int is 4 bytes.
short is 2 bytes.
long is 8 bytes.
long long is 8 bytes.

Maximum values:
int: 2147483647
short: 32767
long: 9223372036854775807
long long: 9223372036854775807

Minimum int value = -2147483648
Bits per byte = 8

**赋值与声明合并在一起叫做初始化。**如果知道变量的初始值应该是什么,则应对它进行初始化。

如果不对函数内部定义的变量进行初始化,该变量的值将是不确定的。这意味着该变量的值 将是它被创建之前,相应内存单元保存的值。

### 3.1.4 无符号类型 要创建无符号版 本的基本整型,只需使用关键字 unsigned 来修改声明即可。注意,unsigned本身是unsigned int的缩写。

3.1.5 选择整型类型
#

通常,int被设置为对 目标计算机而言最为“自然”的长度。自然长度(natural size)指的是计 算机处理起来效率最高的长度。如果没有非常有说服力的理由来选择其 他类型,则应使用int。

如果知道变量可能表示的整数值大于16位整数的最大可能值,则使用long。即使系统上int为32位,也应这样做。这样,将程序移植到16位系统时,就不会突然无法正常工作(参见图3.2)。如果要存储的值超过20亿,可使用long long。

所以对于基因组数据上的位置信息,最好是 long 类型咯,即便 unsigned int 类型已经足够。(不过我可能还是希望用 unsigned int )

如果short比int小,则使用short可以节省内存。通常,仅当有大型整 型数组时,才有必要使用short。如果只需要一个字节,可使用char。

3.1.6 整型字面值
#

整型字面值(常量)是显式地书写的常量,如212或1776。

诸如cout«hex;等代码不会在屏幕上显示任何内容,而只是修改 cout显示整数的方式。因此,控制符hex实际上是一条消息,告诉cout采 取何种行为。另外,由于标识符hex位于名称空间std中,而程序使用了 该名称空间,因此不能将hex用作变量名。然而,如果省略编译指令 using,而使用std::cout、std::endl、std::hex和std::oct,则可以将hex用作 变量名。

3.1.7 C++如何确定常量的类型
#

3.1.8 char类型:字符和小整数
#

char类型是专为存 储字符(如字母和数字)而设计的。

char类型是另一种整型。实际上,很多系统支持的字符都不超过128个,因此用一个字节就可以表示所有的符号。因此,虽然char最常被用来处理字符,但也可以 将它用做比short更小的整型。

C++对字符用单引号,对字符串使用双引号。 cout对象能够处理这两种情况,但正如第4章将讨论的,这两者有天壤之别)。

与int不同的是,char在默认情况下既不是没有符号,也不是有符号。是否有符号由C++实现决定,这样编译器开发人员可以最大限度地将这种类型与硬件属性匹配起来。如果char有某种特定的行为对您来说 非常重要,则可以显式地将类型设置为signed char 或unsigned char。

如果将char用作数值类型,则unsigned char和signed char之间的差异将非常重要。unsigned char类型的表示范围通常为0~255,而signed char 的表示范围为−128到127。

例如,假设要使用一个char变量来存储像200这样大的值,则在某些系统上可以,而在另一些系统上可能不可以。但 使用unsigned char可以在任何系统上达到这种目的。

如果大型字符 集是实现的基本字符集(如中文日文系统),则编译器厂商可以将char定义为一个16位的字节或更长的字节。其次,一种实现可以同时支持一个小型基本字符集和 一个较大的扩展字符集。8位char可以表示基本字符集,另一种类型 wchar_t(宽字符类型)可以表示扩展字符集。wchar_t类型是一种整数类型,它有足够的空间,可以表示系统使用的最大扩展字符集。这种类 型与另一种整型(底层(underlying)类型)的长度和符号属性相同。对底层类型的选择取决于实现,因此在一个系统中,它可能是unsigned short,而在另一个系统中,则可能是int。

3.2 const 限定符
#

C++有一种比 #define 更好的处理符号常量的方法,这种方法就是使用 const 关键字来修改变量声明和初始化。

1
const int Months = 12;

常量(如Months)被初始化后,其值就被固定 了,编译器将不允许再修改该常量的值。如果您这样做,g++将指出程 序试图给一个只读变量赋值。关键字const叫做限定符,因为它限定了声 明的含义。

#define 定义符号常量的方式应抛弃。

一种常见的做法是将名称的首字母大写,以提醒您Months是个常量。这决不是一种通用约定,但在阅读程序时有助于区分常量和变量。另一种约定是将整个名称大写,使用#define创建常量时通常使用这种约定。

注意: 如果在声明常量时没有提供值,则该常量的值将是不确定的,且无 法修改。

3.3 浮点数
#

使用浮点类型可以表示诸如2.5、3.14159和122442.32这样的数字,即带小数部分的数字。计算机将这样的值分成两部分存储。一部分表示值,另一部分用于对值进行放大或缩小。下面打个比方。对于数字34.1245和34124.5,它们除了小数点的位置不同外,其他都是相同的。 可以把第一个数表示为0.341245(基准值)和100(缩放因子),而将第二个数表示为0.341245(基准值相同)和10000(缩放因子更大)。缩放因子的作用是移动小数点的位置,术语浮点因此而得名。C++内部表示浮点数的方法与此相同,只不过它基于的是二进制数,因此缩放因 子是2的幂,不是10的幂。

3.3.1 书写浮点数
#

C++有两种书写浮点数的方式:

  • 第一种是使用常用的标准小数点表示法;
  • 第二种表示浮点值的方法叫做E表示法,其外观是像这样的: 3.45E6,这指的是3.45与1000000相乘的结果;E6指的是10的6次方,即 1后面6个0。因此,3.45E6表示的是3450000,6被称为指数,3.45被称为 尾数。

E表示法确保数字以浮点格式存储,即使没有小数点。注意,既可 以使用E也可以使用e,指数可以是正数也可以是负数,不要有空格。

电子的质量是 9.11e-31 kg 表示 0.000000000000000000000000000000911 kg, 而美国报警电话 911 竟然很巧合地与此相同。

3.3.2 浮点类型
#

C++也有3种浮点类型:float、double 和 long double。

事实上,C和C++对于有效位数的要求是,float至少32位,double至少48位,且不少于float,long double至少和double一样多。这三种类型的有效位数可以一样多。然而,通常,float为32位,double为64位, long double为80、96或128位。另外,这3种类型的指数范围至少是−37到37。可以从头文件cfloat或float.h中找到系统的限制。

3.3.3 浮点常量
#

在默认情况下,像8.24和2.4E8这样的浮点常量都属于double类型。如果希望常量为float类型,请使用f或F后缀。对于long double类型,可使用l或L后缀(由于l看起来像数字1,因此L是更好的选择)。

3.3.4 浮点数的优缺点
#

与整数相比,浮点数有两大优点。首先,可以表示整数之间的 值。其次,由于有缩放因子,它们可以表示的范围大得多。另一方面, 浮点运算的速度通常比整数运算慢,且精度将降低

如: 2.34E+22是一个小数点左边有23位的数字。加上1,就 是在第23位加1。但float类型只能表示数字中的前6位或前7位,因此修改第23位对这个值不会有任何影响。

3.4 C++算术运算符
#

11.17 + 50.25 应等于61.42,但是输出的却是61.419998。这不是运算问题,而是由于float类型表示有效位数的能力有限。记住,对于float,C++只保证6位有效位。如果将 61.419998四舍五入成6位,将得到61.4200,这是在保证精度下的正确值,如果用double 则精度足够,所以直接可以获得 61.42 的值。

通常来说 double 都比 float 更精准,应尽量使用 double。

3.4.1 运算符优先级和结合性
#

算术运算符遵 循通常的代数优先级,先乘除,后加减。

3.4.2 除法分支
#

除法运算符(/)的行为取决于操作数的类型。如果两个操作数都 是整数,则C++将执行整数除法。这意味着结果的小数部分将被丢弃, 使得最后的结果是一个整数。如果其中有一个(或两个)操作数是浮点 值,则小数部分将保留,结果为浮点数

浮点常量在默认情况下为 double类型。

3.4.3 求模运算符
#

求模运算符返回整数除法的余数。它与整数除 法相结合,尤其适用于解决要求将一个量分成不同的整数单元的问题。

3.4.4 类型转换
#

由于有11种整型和3种浮点类型,因此计算机需 要处理大量不同的情况,C++自动执行很多类型转换:

  • 将一种算术类型的值赋给另一种算术类型的变量时,C++将对值进 行转换;
  • 表达式中包含不同的类型时,C++将对值进行转换;
  • 将参数传递给函数时,C++将对值进行转换。

C++允许将一种类型的值赋给另一种类型的变量。这样做时,值将 被转换为接收变量的类型。例如,假设so_long的类型为long,thirty的类 型为short。

将一个值赋给值取值范围更大的类型通常不会导致什么问题。如,将short值赋给long变量并不会改变这个值,只是占用的字节更多而 已。然而,将一个很大的long值(如2111222333)赋给float变量将降低 精度。因为float只有6位有效数字,因此这个值将被四舍五入为 2.11122E9。因此,有些转换是安全的,有些则会带来麻烦。

如下的报错是一个可能:

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

当同一个表达式中包含两种不同的算术类型时,将出现什么情况 呢?在这种情况下,C++将执行两种自动转换:首先,一些类型在出现 时便会自动转换;其次,有些类型在与其他类型同时出现在表达式中时 将被转换。

  • 在计算表达式时,C++将bool、char、unsigned char、signed char和short值转换为int。具体地说,true被转换为1,false 被转换为0。这些转换被称为整型提升(integral promotion)。

int 是一种最自然的类型,运算速度也最快,要主用。

  • 同样,wchar_t被提升成为下列类型中第一个宽度足够存储wchar_t 取值范围的类型:int、unsigned int、long或unsigned long。

  • 将不同类型进行算术运算时,也会进行一些转换,例如将int和float 相加时。当运算涉及两种类型时,较小的类型将被转换为较大的类型。

  • 传递参数时的类型转换通常由C++函数原型控制。
  • C++还允许通过强制类型转换机制显式地进行类型转换。强制类型转换 的格式有两种。如下:

新格式的想法 是,要让强制类型转换就像是函数调用。这样对内置类型的强制类型转换就像是为用户定义的类设计的类型转换。

更安全的转换方式是使用 static_cast<typeName> (value) 函数。

Stroustrup认为,C语言式的强制类型转换由于有过多的可能性而极 其危险,这将在第15章更深入地讨论。运算符 static_cast<> 比传统强制 类型转换更严格。

3.4.5 C++11中的auto声明
#

在初始化声明中,如果 使用关键字auto,而不指定变量的类型,编译器将把变量的类型设置成 与初始值相同。

3.5 总结
#

C++的基本类型分为两组:一组由存储为整数的值组成,另一组由存储为浮点格式的值组成。整型之间通过存储值时使用的内存量及有无符号来区分。整型从最小到最大依次是:bool、char、signed char、 unsigned char、short、unsigned short、int、unsigned int、long、unsigned long以及C++11新增的long long和unsigned long long。还有一种wchar_t 类型,它在这个序列中的位置取决于实现。C++11新增了类型char16_t 和char32_t,它们的宽度足以分别存储16和32位的字符编码。C++确保了char足够大,能够存储系统基本字符集中的任何成员,而wchar_t则可以存储系统扩展字符集中的任意成员,short至少为16位,而int至少与short一样长,long至少为32位,且至少和int一样长。确切的长度取决于 实现。

浮点类型可以表示小数值以及比整型能够表示的值大得多的值。3种浮点类型分别是float、double和long double。C++确保float不比double 长,而double不比long double长。通常,float使用32位内存,double使用 64位,long double使用80到128位。

对变量赋值、在运算中使用不同类型、使用强制类型转换时,C++将把值从一种类型转换为另一种类型。

第四章 复合类型
#

本章很重要(特别是指针部分),需要掌握的内容为:

  • 创建和使用数组;
  • 创建和使用C-风格字符串;
  • 创建和使用 string 类字符串;
  • 使用方法 getline() 和 get() 读取字符串;
  • 混合输入字符串和数字;
  • 创建和使用结构体;
  • 创建和使用共用体;
  • 创建和使用枚举类型;
  • 创建和使用指针;
  • 使用 new 和 delete 管理动态内存;
  • 创建动态数组;
  • 创建动态结构;
  • 自动存储、静态存储和动态存储;
  • vector 和 array 类简介。

4.1 数组
#

数组(array) 是一种数据格式,能够存储多个同类型的值。每个值都存储在一个独立的数组元素中,计算机在内存中依次存储数组的各个元素。

即一个数组所在的内存区域是连续的。

要创建数组,可使用声明语句。数组声明应指出以下三点:

  • 存储在每个元素中的值的类型;
  • 数组名;
  • 数组中的元素数。

在C++中,通过在简单变量后面添加中括号(其中包含 元素数目)来完成数组声明。而数组中的每一个元素都看作是一个简单变量。声明数组的通用格式为 :

typeName arrayName[arraySize]

表达式arraySize指定元素数目,它必须是整型常数(如10)或const 值,也可以是常量表达式(如8 * sizeof(int)),即其中所有的值在编 译时都是已知的。具体说,arraySize不能是变量,变量的值是在程序 运行时设置的。如:

1
int months[12];   // 声明一个包含 12 个整数的数组

C++ 通过使用下标来访问数组中的每一个元素,C++ 数据从0开始编号,数组总长度-1 是最后一个元素,如上:months[0] 为第一个元素,months[11] 是最后一个元素。

4.1.1
#

C++可以在声明语句中初始化数组,只需提供一个用逗号分隔的值列表(初始化列表),并将它们用花 括号括起即可。如 :

1
int yamcosts[3] = {20, 30, 5};

如果没有初始化数组的值,则其元素值将是不确定的,这意味着元素的值为以前驻留在该内 存单元中的值。

将sizeof运算符用于数组名,得到的将是整个数组 中的字节数。

4.1.2 数组的初始化规则
#

只有在定义数组时才能使用初始化,此后就不能使用了,也不能将一个数组赋给另一个数组

这个方式和 Python 完全不同,原因可能是 Python 以引用为基础赋值,C++此处则是值赋值,只能一个一个来(声明时除外)。

但此后,仍然可以通过下标分别给数组中的元素赋值。注意,这里是赋值不再是初始化

初始化数组时,提供的值可以少于数组的元素数目,编译器将把其他元素设置为 0。因此,将数组中所有的元素都初始化为0非常简单—只要显式地将第 一个元素初始化为0,然后编译器会自动将其他元素都初始化为0。

还有一种方法,如果初始化数组时方括号内([ ])为空,C++编译器将计算元素个 数。但这是很糟糕的做法,不提倡,不举例。

4.1.3 C++11数组初始化方法
#

数组以前就可使用列表初始化, 但C++11中的列表初始化新增了一些功能。

  • 首先,初始化数组时,可省略等号(=);
  • 其次,可不在大括号内包含任何东西,这将把所有元素都设置为零;
  • 第三,列表初始化禁止缩窄转换。

在上述代码中,第一条语句不能通过编译,因为将浮点数转换为整 型是缩窄操作,即使浮点数的小数点后面为零。第二条语句也不能通过 编译,因为1122011超出了char变量的取值范围(这里假设char变量的长 度为8位)。第三条语句可通过编译,因为虽然112是一个int值,但它在 char变量的取值范围内。

4.2 字符串
#

字符串是存储在内存的一片连续字节中的一系列字符,这里的连续是重点。 存储在连续字节中的一系列字符意味着可以将字符串存储在char数 组中,其中每个字符都位于自己的数组元素中。字符串提供了一种存储 文本信息的便捷方式。本章介绍两种处理字符串的方法:(1)C-风格字符串;(2)基于 string 类的方法。

C-风格字符串具有 一种特殊的性质:以空字符(null character)结尾,空字符被写作 \0, 其ASCII码为0,用来标记字符串的结尾。

这两个数组都是char数组,但只有第二个数组是字符串,空字符对 C-风格字符串而言至关重要。C++有很多处理字符串的函数,其 中包括cout使用的那些函数。它们都逐个地处理字符串中的字符,直到 到达空字符为止。如果使用cout显示上面的cat这样的字符串,则将显示 前7个字符,发现空字符后停止。使用cout显示上面的dog数 组(它不是字符串),cout将打印出数组中的8个字母,并接着将内存 中随后的各个字节解释为要打印的字符,直到遇到空字符为止(因此不加空字符是C-风格字符串中极其危险的行为)。

有一种更好的、 将字符数组初始化为字符串的方法—只需使用一个用引号括起的字符串 即可,这种字符串被称为字符串常量(string constant)或字符串字面值 (string literal),如下所示:

用引号括起来的字符串隐式地包括结尾的空字符,因此不用显式地包括它。

最需要注意的是,使用C-风格字符串,在确定存储字符串所需的最短数组时,别忘了+1,要将结尾的空字符计算在内

这里的赋值改为:char shirt_size[2] = "S"; 才是正确的。

4.2.1 拼接字符串常量
#

事实上,任何两个由空 白(空格、制表符和换行符)分隔的字符串常量都将自动拼接成一个。 因此,下面所有的输出语句都是等效的:

注意,拼接时不会在被连接的字符串之间添加空格,第二个字符串 的第一个字符将紧跟在第一个字符串的最后一个字符(不考虑\0)后 面。第一个字符串中的\0字符将被第二个字符串的第一个字符取代。

4.2.2 在数组中使用字符串
#

从程序清单4.2中可以学到什么呢? 首先,sizeof运算符指出整个数组的长度:15字节,但 strlen( )函数返回的是存储在数组中的字符串的长度,而不是数组本身的长度。 另外,strlen( ) 只计算可见的字符,而不把空字符计算在内。因此,对于Basicman,返回的值为8,而不是 9。如果cosmic是字符串,则要存储该字符串,数组的长度不能短于strlen(cosmic)+1。 由于name1和name2是数组,所以可以用索引来访问数组中各个字符。例如,该程序使用 name1[0]找到数组的第一个字符。另外,该程序将name2[3]设置为空字符。这使得字符串在第 3个字符后即结束,虽然数组中还有其他的字符(参见图4.3)。

程序清单 4.2 使用 cin 暗含两个问题:

  • 遇到空格结束;
  • 输入字符串长于目标数组。

4.2.3 字符串输入
#

cin是如何确定已完成字符串输入呢?由于不能通过键盘输入空字 符,因此cin需要用别的方法来确定字符串的结尾位置。cin使用空白 (空格、制表符和换行符)来确定字符串的结束位置,这意味着cin在 获取字符数组输入时只读取一个单词。读取该单词后,cin将该字符串 放到数组中,并自动在结尾添加空字符。

这个例子的实际结果是,cin把Alistair作为第一个字符串,并将它 放到name数组中。这把Dreeb留在输入队列中。当cin在输入队列中搜索 用户喜欢的甜点时,它发现了Dreeb,因此cin读取Dreeb,并将它放到 dessert数组中。

另一个问题是,输入字符串可能比目标数组长(运行中没有揭示出 来)。像这个例子一样使用cin,确实不能防止将包含30个字符的字符 串放到20个字符的数组中的情况发生。

文章将这个问题的系统处理留到第 17 章,但下文也能解决。

4.2.4 每次读取一行字符串输入
#

每次读取一个单词通常不是最好的选择。具体地说,需要采用面向行而不是面向单词的方法。istream中的类(如 cin)提供了一些面向行的类成员函数:getline()get()。这两个函数都读取一 行输入,直到到达换行符(回车符)。区别是,getline() 将读入并丢弃换行符,而 get() 则将换行符保留在输入队列中。注意是还保留在输入流里,没有被读走!

1.面向行的输入:getline( )

getline( )函数读取整行,它使用通过回车键输入的换行符来确定输入结尾。通过 cin.getline() 调用。该函数有两个参数,第一个参数是存储输入行的数组名称,第二个参数是要读取的字符数。如果这个参数为20,则函数最多读取19个字符或者碰到换行符为止,并自动在结尾处添加空字符。如:

1
cin.getline(array_name, 20);

例子程序清单 4.4:

2.面向行的输入:get( )

iostream 中的 get() 虽然和 getline() 类似,它们接受的参数相同,解释参数的方式也相同,并且都读取到行尾。但 get 将换行符保留在输入队列中,假设我们连续两次 调用 get( ) 那么要注意了。

第一次调用后,换行符将被留在输入队列中,因此第二次调用时看到的第一个字符便是换行符,此时 get() 以为已经到达行尾了,而没有发现任何可读取的内容。如果不借助于帮助,get() 将不能跨过该换行符

这时要用 get() 的另一种变体——不带任何参数,可读取下一个字符(即使是换行符),因此可以用它来处理换行符,为读取下一行输入做好准备。例子:

由于 cin.get(name, ArSize) 返回的还是 cin 对象,因此还可以合并起来调用:

1
cin.get(name, ArSize).get();  // 这样也能同时将末尾的换行符读走

但风险就是,假如 ArSize 小于一行的字符数,那么调用 get() 后就会发生字符丢失

getline() 成员函数也同样可以合并调用连续读取字符串:

1
cin.getline(name1, ArSize).getline(name2, ArSize);

这语句将把输入中 连续的两行分别读入到数组name1和name2 中,其效果与两次调用 cin.getline() 相同。

为什么要使用 get(),而不是 getline() 呢? 首先,老式实现没有 getline()。其次,get() 使输入更仔细。例如,假设用get( ) 将一行读入数组中。如何知道停止读取的原因是由于已经读取了整行,而不是由于数组已填满呢? (getline,判断不了), 查看下一个输入字符,如果是换行符,说明已读取了整行;否则,说明该行中还有其他输入。

总之, getline() 使用起来简单一些,但 get( ) 使得检查错误更简单些。

3.空行和其他问题

getline()get() 读取空行时,将发生什么情况?最初的做法是, 下一条输入语句将在前一条 getline()get() 结束读取的位置开始读取; 但当前的做法是,当 get()(不是getline)读取空行后将设置失效位 (failbit)。这意味着接下来的输入将被阻断,要用 cin.clear() 的命令来恢复输入。

另一个潜在的问题是,输入字符串可能比分配的空间长。如果输入行包含的字符数比指定的多,则 getline()get() 将把余下的字符留在输入队列中,getline() 还会设置失效位,并关闭后面的输入。

4.2.5 混合输入字符串和数字
#

清单程序4.6 的一个问题是: 当cin读取年份,将回 车键生成的换行符留在了输入队列中。后面的cin.getline( )看到换行符 后,将认为是一个空行,并将一个空字符串赋给address数组。解决之道 是,在读取地址之前先读取并丢弃换行符。这可以通过几种方法来完 成,其中包括使用没有参数的get( )和使用接受一个char参数的get( )。

也可以利用表达式cin»year返回cin对象,将调用拼接起来:

1
(cin >> year).get();  // or (cin >> year).get(ch);

4.3 string 类简介
#

string 类包含于头文件 string 并位于名称空间 std 中,使用起来比字符数组简单,它提供了将字符串作为一种数据类型的表示方法,符合 C++ 的风格。 string 类定义隐藏了字符串的数组性质,让我们能够像处理普通变量那样处理字符串。

在很多方面,使用 string 对象的方式和使用字符数组相同:

  • 可以使用 C-风格字符串来初始化 string 对象;
  • 可以使用 cin 来将键盘输入存储到 string 对象中;
  • 可以使用 cout 来显示 string 对象;
  • 可以使用数组表示法来访问存储在 string 对象中的字符。

string 对象和字符数组的主要区别是,可以将 string 对象声明为简单变量,而不是数组。

类设计让程序能够自动处理string的大小。例如,str1的声明创建一 个长度为0的 string 对象,但程序将输入读取到str1中时,将自动调整 str1 的长度。

这使得与使用数组相比,使用string对象更方便,也更安全。从理论上说,可以将char 数组视为一组用于存储一个字符串的 char 存储单元,而 string 类变量是一个表示字符串的实体。

4.3.1 C++11字符串初始化
#

4.3.2 string 赋值、拼接和附加
#

string 可以如简单变量那般操作。比如,虽然不能将一个数组赋给另一个数组,但却可以将一个 string 对象赋给另一个 string 对象。

可以使用运算符 + 将两个string 对象合并起来,还可以使用运算符 += 将字符串附加到string对象的末尾。

1
2
3
4
5
6
7
string str1 = "aa";
string str2 = "bb";
string str3;

str3 = str1 + str2;  // 正确
str3 += str1;        // 正确
str3 += " cc";       // 一样正确

4.3.3 string类的其他操作
#

处理string对象的语法通常比使用C字符串函数简单,尤其是执行较为复杂的操作时。例如,对于下述操作:

str3 = str1 + str2;

使用C-风格字符串时,需要使用的函数如下:

1
2
strcpy(charr3, charr1);
strcat(charr3, charr2);

strcpy()strcat() 函数包含在头文件 cstring 中。函数 strcpy() 将字符串复制到字符数组中,使用函数 strcat() 将字符串附加到字符数组末尾。 另外,使用字符数组时,总是存在目标数组过小,无法存储指定信息的危险

函数strcat( )试图将全部12个字符复制到数组site中,这将覆盖相邻的内存。这可能导致程序终止,或者程序继续运行,但数据被损坏。string类具有自动调整大小的功能,从而能够避免这种问题发生

4.3.4 string类I/O
#

在用户输入之前,该程序指出数组charr中的字符串长度为27,这比 该数组的长度要大。这里要两点需要说明。首先,为初始化的数组的内 容是未定义的;其次,函数strlen( )从数组的第一个元素开始计算字节 数,直到遇到空字符。在这个例子中,在数组末尾的几个字节后才遇到 空字符。对于未被初始化的数据,第一个空字符的出现位置是随机的, 因此您在运行该程序时,得到的数组长度很可能与此不同。

用户输入之前,str中的字符串长度为0。这是因为未被初始化的string对象的长度被自动设置为0。 这是和字符串数组不同的地方

源代码中是将一行输入读取到string对象中的代码是:getline(cin, str);。这个 getline() 不是类方法,它将 cin 作为参数,指出去哪里查找输入,另外,也没有指出字符串长度 的参数,因为 string 对象将根据字符串的长度自动调整自己的大小。

4.3.5 其他形式的字符串字面值
#

除char类型外,C++还有类型wchar_t;而C++11新 增了类型char16_t和char32_t。可创建这些类型的数组和这些类型的字符 串字面值。对于这些类型的字符串字面值,C++分别使用前缀L、u和U 表示,下面是一个如何使用这些前缀的例子:

C++11还支持Unicode字符编码方案UTF-8。 C++11新增的另一种类型是原始(raw)字符串,并使用前缀 R 来标识,原始字符表示的就是自己,例如,序列 \n 不表示换行符,而表示两个常 规字符—斜杠和n,因此在屏幕上显示时,将显示这两个字符。

4.4 结构体简介
#

**结构体(Struct)**是 一种比数组更灵活的数据格式,因为同一个结构体可以存储多种不同类型的数据,从而将数据的表示合并到一起。

原文称 struct 为 “结构”,我认为这不符合一贯以来的称呼,因此在这个笔记中我一律改为通常在 C/C++ 中所称呼的 “结构体”。

结构体是用户定义的类型,而结构体声明定义了这种类型的数据属性。结构体的定义需要用 关键字 struct 做标识。 定义了类型后,便可以创建这种类型的变量。以下例子是一个结构体的定义,它使用了一个适合用于存储字符串的 char数组、一个float和一个double。列表中的每一项都被称为结构体成员,因此 infatable 结构体有3个成员(参见图4.6)

定义之后就可以将这个结构体当做常规的 C++ 数据类型(如同 int、double、string等)来使用。

在结构体类型中,可以通过使用成员运算符 . 来访问各个成员。

4.4.1 在程序中使用结构体
#

结构声明的位置很重要。

结构体的初始化方式和数组一样,使用逗号分隔值列表,并将这些值用花括号括起。如:

花括号中每个值可以独占一行,也可以将他们都放在同一行中,注意变量之间有逗号隔开就行,其他的随意。

4.4.2 C++11 结构体初始化
#

4.4.3 结构体可以将string类作为成员吗?
#

可以,只要编译器支持 string 类型就没问题,唯一要注意的问题注意添加名字空间 std 。 如:

4.4.4 其他结构属性
#

结构体和 C++ 内置类型的用法类似,它可以作为参数传递给函数,也可以让函数返回一个结构体。另外,还可以使用赋值运算符(=)将结构体赋给另一个同类型的结构体,这样结构体中每个成员都将被设置为另一个结构中相应成员的值,即使成员是数组。这种赋值被称为成员赋值(memberwise assignment)。

结构体类型还可以同时完成定义结构体和创建结构体变量的工作,只需将变量名放在结束括号的后面即可:

但不推荐这样做,将结构体定义和变量声明分开,可以使程序更易于阅读和理解。

4.4.5 结构体数组
#

创建结构体数组的方法和创建 C++ 基本类型的数组完全相同。

要初始化结构体数组,可以使用初始化数组的规则(用逗号分隔每个元素的值,并将这些值用花括号括起)和初始化结构体的规则(用逗号分隔每个成员的值,并将这些值用花括号括起)。由于数组中的每个 元素都是结构,因此可以使用结构初始化的方式来提供它的值。

4.5 共用体
#

共用体(union),也叫联合体是一种数据格式,它能够存储不同的数据类型, 但只能同时存储其中的一种类型。也就是说,结构体可以同时存储 int、 long和double,共用体只能存储int、long或double。共用体的句法与结构相似,但含义不同。如:

共用体每次只能存储一个值,因此它必须有足够的空间来存储最大的成员,所以,共用体的长度为其最大成员的。

共用体的用途之一是,当数据项使用两种或更多种格式(但不会同时使用)时,可节省内存空间,其实对当前内存充裕的计算机来说,并非很有必要使用共用体。

4.6 枚举
#

这也是一个很少用的类型,略。

4.7 指针和自由存储空间
#

计算机在存储数据时必须跟踪的3种基本属性:

  • 信息存储在何处;
  • 存储的值为多少;
  • 存储的信息是什么类型。

C++ 提供了一种策略,可以在程序内部跟踪内存单元,这个策略以指针为基础。C++ 中 指针是一个变量,其存储的是值的地址,而不是值本身

首先,我们如何找到常规变量的地址?在C/C++中只需对变量应用地址运算符 &,就可以获得它的位置;例如,如果 home 是一个变量,则 &home 是它在内存中的地址。

使用常规变量时,值是指定的量,而地址为派生量。接下来看看指针策略,它是C++内存管理编程理念的核心。

指针处理存储数据的新策略刚好相反,指针将地址视为指定的量,而将值视为派生量,因此,指针名表示的是地址。* 运算符被称为间接值(indirect velue)或解除引用 (dereferencing)运算符,应用于指针得到该地址处所存储的值。例如,假设 manly 是一个指针,则 manly 表示的是一个地址,*manly 表示存储在该地址处的值,即 *manly 和常规变量等效。变量的指针和变量的值本身本质只不过是一个硬币的两个面

变量是编译时分配出来的一个有名称的内存,这是变量的实质。而指针是一个可以通过名称直接访问内存的别名,内存里存储的东西就是变量的值。

4.7.1 声明和初始化指针
#

不同数据类型存储值时的内存格式不同,所以指针声明时必须 指定指针指向的数据类型,声明一个指针的通用方式:

typeName *p_name;

* 符用于指明该变量是指针,每个指针变量声明时都需要带 *

使用指针的时候要注意区分地址是地址,值是值。指针本身也是一种数据类型,它的值是地址,这个地址也有它自己的字节长度(由计算机系统内定),它的长度和它这个地址里存储的数值没有任何关系。指向 int 类型和指向 double 类型或者 string 类型的指针类型的字节长度都是一样的,它们都是地址。

4.7.2 指针的危险
#

创建指针时,计算机将分配用来存储指针这个变量本身的内存,但不会分配指针所指向的数据所需的内存。为数据提供空间是一个独立的步骤,如果忽略这一步麻烦就大了。如:

1
2
long *fellow;       // 创建一个指向 long 类型的指针
*fellow = 223323;   // 指针还不知道指向哪个地址,就强制赋值,大错特错

声明时,只是告诉程序 fellow 是一个 long 指针,程序只给 fellow 这个指针变量分配了一个属于 fellow 自己的存储空间,至于它里面要存什么地址值还完全不知道。必须先申请一个存 long 类型的地址空间,然后赋值给 fellow,最后才可以将 223323 赋值过去。

一定要在对指针应用解除引用运算符 * 之前,将指针初始化为一个确定的、适当的地址,这是关于使用指针的铁律。

4.7.3 指针和数字
#

指针不是整型,虽然计算机通常把地址当作整数来处理。在有些平台 中,int 类型是个2字节值,而地址是个4字节值。

4.7.4 使用new来分配内存
#

指针真正的用武之地在于,运行阶段分配未命名的内存用于存储值。C++ 通过 new 运算符为变量分配内存。

程序员要告诉new,需要为哪种数据类型分配内存;new将找到一个长 度正确的内存块,并返回该内存块的地址。程序员的责任是将该地址赋 给一个指针。下面是一个这样的示例:

1
int *pn = new int;

new int 告诉程序,需要适合存储 int 的内存。new 运算符根据类型来确定需要多少字节的内存。然后,它找到这样的内存,并返回其地址。 接下来,将地址赋给 pnpn 是被声明为指向 int 的指针。现在,pn 是地址,而 *pn 是存储在那里的值。

注意:pn 指向的内存不是变量(那是一个地址,是不变的),内存里存储的东西才是可变的。

这引出了一个问题:pn指向的内存没 有名称,如何称呼它呢?我们说pn指向一个数据对象,这里的“对象”不 是“面向对象编程”中的对象,而是一样“东西”。术语“数据对象”比“变 量”更通用,它指的是为数据项分配的内存块。

为一个数据对象(可以是结构,也可以是基本类型)获得并指定分 配内存的通用格式如下:

1
typeName *pointer_name = new typeName;

需要指出的另一点是,new 分配的内存块通常与常规变量声明分配的内存块不同。常规变量存储在被称为栈 (stack)的内存区域中,而 new 从被称为堆(heap)或自由存储区(free store)的内存区域分配内存。

如果内存分配失败,new 运算符将返回空指针(null pointer),其值为 0。

4.7.5 使用 delete 释放内存
#

delete 运算符,用于将不再使用的内存归还给内存池,归还或释放(free)的内存可供程序的其他部分使用。使用 delete 时,后面要加上指向内存块的指针(这些内存块最初是用new分配的)。

1
2
3
int *ps = new int;   // 声明指针并分配一个可以存 int 类型的内存给指针
...
delete ps;  // 归还内存

归还 ps 指向的内存,并不会删除指针 ps 本身,它可以继续用来指向新分配的内存。 newdelete 一定要配对使用,否则会导致程序发生内存泄露(memory leak),即分配出去的内存拿不回来,无法再使用。

另外,不要尝试释放已经释放的内存块,C++标准指出,这样做的结果将是不确定的,这意味着什么情况都可能发生。

一般来说,不要创建两个指向同一个内存块的指针,因为这将增加 错误地删除同一个内存块两次的可能性。

另外,不能使用 delete 释放声明变量所获得的内存,只能用它释放 new 分配的内存,这是关键点。

4.7.6 使用new来创建动态数组
#

对于大型数据(如数组、字符串和结 构),应使用new,这正是new的用武之地。

在编译时给数组分配内存被称为静态联编(static binding),这就是为什么只能分配固定大小的数组。但使用 new 时,在运行阶段需要数组,则创建它;如果不需 要,则不创建,还可以选择数组的长度,这被称为动态联 编(dynamic binding),意味着数组是在程序运行时创建的。

1.使用 new 创建动态数组

创建动态数组很容易;只要将数组的元素类型和元素数 目告诉new即可。必须在类型名后加上方括号,其中包含元素数目。为数组分配内存的通用格式如下:

typeName *pointer_name = new typeName [num_elements];

给个例子如:

1
int *psome = new int [10];  // 分配可以存储 10 个 int 的内存块

new 返回第一个元素的地址,并将该地址被赋给指针 psome

释放这一块内存时要这样做:

1
delete [] psome;

方括号告诉程序,应释放整个数组,而不仅仅是指针指向的元素。 注意delete和指针之间有方括号。对于 ANSI/ISO标准来说,newdelete 的格式要匹配,否则导致的后果是不确定。

总之,使用new和delete时,应遵守以下规则:

  • 不要使用 delete 来释放不是 new 分配的内存;
  • 不要使用 delete 释放同一个内存块两次;
  • 如果使用 new [] 为数组分配内存,则应使用 delete [] 来释放;
  • 如果使用 new [] 为一个实体分配内存,则应使用 delete(没有方括 号)来释放(这一点要找例子来理解);
  • 对空指针应用 delete 是安全的。

2.使用动态数组

创建动态数组后,如何使用它呢?只需要将指针名当做数组名,然后按照数组的访问方式即可,C/C++ 中数组和指针是基本等价的(但也有实质区别,后续再说)。例子:

1
int *psome = new int [10];

对于第1个元素,可以使用 psome[0],而不是 *psome;对于第2个元 素,可以使用 psome[1],依此类推。

程序清单4.18演示了如何使用new来创建动 态数组以及使用数组表示法来访问元素;它还指出了指针和真正的数组名之间的根本差别(注意红框)。

p3 加1导致它指向第2个元素而不是第1个。将它减1后,指针将指向原来的值,这样程序便可以给 delete[] 提供正确的地址。(这叫指针移动)

4.8 指针、数组和指针算术
#

指针算术是有特殊之处的。

指针和数组基本等价的原因在于指针算术(pointer arithmetic)和 C++ 内部处理数组的方式

将整数变量加1 后,其值将增加1;但将指针变量加1后,增加的量等于它指向的类型的 字节数。将指向double的指针加1后,如果系统对double使用8个字节存 储,则数值将增加8;将指向short的指针加1后,如果系统对short使用2 个字节存储,则指针值将增加2。

4.8.1 程序说明
#

将指针变量加1后,其增加的值等于指向的类型占用的字节数。 也就是跳到下一个存储值的地址上。

从该程序的输出可知,*(stacks + 1)stacks[1] 是等价的。同样, *(stacks + 2)stacks[2] 也是等价的。通常,使用数组表示法时,C++ 都执行下面的转换:

arrayname[i] becomes *(arrayname + i)

如果使用的是指针,而不是数组名,则C++也将执行同样的转换:

pointername[i] becomes *(pointername + i)

因此,在很多情况下,可以相同的方式使用指针名和数组名。也就是说,使用 new 来创建数组以及使用指针来访问不同的元素时,只要把指针当作数组名对待即可。

【注意】数组名被解释为其第一个元素的地址,而对数组名应用地址运算符 时,得到的是整个数组的地址,区别很大。

4.8.2 指针小结
#

1.声明指针 要声明指向特定类型的指针,请使用下面的格式:

1
typeName *pointer_name;

2.给指针赋值

应将内存地址赋给指针。可以对变量名应用 & 地址运算符,来获得被命名的内存的地址,或 new 运算符返回未命名的内存的地址。

3.对指针解引用

对指针解引用意味着获得指针指向的值。* 是指针应用解引用或间接值运算符。

4.区分指针和指针所指向的值

如果 pt 是指向int的指针,则 *pt 不是指向 int 的指针,而是完全等同于 一个 int 类型的变量,pt 才是指针。

5.数组名

大多数情况下,C++将数组名视为数组的第一个元素的地址。一种例外情况是,将sizeof运算符用于数组名用时,此时将返回整个数组的长度(单位为字节)。

6.指针算术

C++允许将指针和整数相加。加1的结果等于原来的地址值加上指向的对象占用的总字节数,也就是移动到下一个存储值的地址。还可以将一个指针减去另一个指针,获得两个指针的差。后一种运算将得到一个整数,仅当两个指针指向同一个数 组(也可以指向超出结尾的一个位置)时,这种运算才有意义,这将得到两个元素的间隔。

7.数组的动态联编和静态联编

使用数组声明来创建数组时,将采用静态联编,即数组的长度在编 译时设置。 使用 new[] 运算符创建数组时,将采用动态联编(动态数组),即将在运行时为数组分配空间,其长度也将在运行时设置。使用完这种数组后,应使用 delete [] 释放其占用的内存。

8.数组表示法和指针表示法

使用方括号数组表示法等同于对指针解引用。数组名和指针变量都是如此,因此对于指针和数组名,既可以使用 指针表示法,也可以使用数组表示法。

4.8.3 指针和字符串
#

数组和指针的特殊关系可以扩展到C-风格字符串。

1
2
char flower[10] = "rose";
cout << flower << "s are red.\n";

以上代码中,数组名是第一个元素的地址,因此cout语句中的flower是包含字符r 的 char 元素的地址。cout 对象认为 char 的地址是字符串的地址,因此它打 印该地址处的字符,然后继续打印后面的字符,直到遇到空字符 (\0) 为止。总之,如果给cout提供一个字符的地址,则它将从该字符开始打 印,直到遇到空字符为止。

这意味着可以将指向char的指针变量作为cout的参数,因为它也是 char的地址。 在C++中,用引号括起的字符串像数组名一样,也是第一个元素的地 址。这意味着对于数组中的字符串、用引号括起的字符串常量以及指针 所描述的字符串,处理的方式是一样的,都将传递它们的地址。与逐个 传递字符串中的所有字符相比,这样做的工作量确实要少。

在 cout 和多数 C++ 表达式中,char 数组名、char 指针以及用引号括起的字符串常量都被解释为字符串第一个字符的地址。

请不要使用字符串常量或未被初始化的指针来接收输入。为避免这 些问题,也可以使用std::string对象,而不是数组。

在将字符串读入程序时,应使用已分配的内存地址。该地址可以是数组名,也可以是使用new 初始化过的指针。

注意这段代码及其输出:

一般来说,如果给cout提供一个指针,它将打印地址。但如果指针 的类型为 char *,则 cout将显示指向的字符串。如果要显示的是字符串的 地址,则必须将这种指针强制转换为另一种指针类型,如 int *(上面的 代码就是这样做的)。

经常需要将字符串放到数组中。初始化数组时,请使用=运算符; 否则应使用strcpy( )或strncpy( )。应使用strcpy( )或strncpy( ),而不是赋值运算符来将字符串赋给数组。

这是 C-风格 字符串的操作方式,C++ string 类型则并不需要如此,会简单得多,不用担心字符串会导致数 组越界,并可以使用赋值运算符而不是函数strcpy( )和strncpy( )。

4.8.4 使用 new 创建动态结构体
#

在运行时创建数组优于在编译时创建数组,对于结构也是如此。通过使用new,可以创建动态结构。

将 new 用于结构体由两步组成:创建结构体和访问其成员。要创建结构体,需要同时使用结构体类型和 new。

1
inflatable *ps = new inflatable;

这将把足以存储inflatable结构的一块可用内存的地址赋给ps。这种 句法和C++的内置类型完全相同。

此时要访问结构体成员,需通过箭头成员运算符 −>,它用于指向结构体的指针,就像点运算符可用于结构体名一样,如 ps->price 指向了结构体中的 price 成员。

如果结构标识符是结构名,则使用句点运算符;如果标识符是指向结构 的指针,则使用箭头运算符。

另一种访问结构成员的方法是先对指针解引用。如果 ps 是指向结构的指针,则 *ps 就是被指向的值—结构本身。由于 *ps 是一个结构,因此 (*ps).price 是 该结构的 price 成员。

4.8.5 自动存储、静态存储和动态存储
#

根据用于分配内存的方法,C++有3种管理数据内存的方式:自动存储、静态存储和动态存储(有时也叫作自由存储空间或堆)。在存在时间的长短方面,以这3种方式分配的数据对象各不相同。

  1. 自动存储

在函数内部定义的常规变量使用自动存储空间,被称为自动变量 (automatic variable),只在包含它的代码中有效,这意味着它们在所属的函数被调用时自动产生,在该函数结束时消亡。

自动变量通常存储在栈中。

  1. 静态存储

静态存储是整个程序执行期间都存在的存储方式。使变量成为静态 的方式有两种:一种是在函数外面定义它;另一种是在声明变量时使用 关键字 static

  1. 动态存储

newdelete 运算符提供了一种比自动变量和静态变量更灵活的方法。它们管理了一个内存池,这在C++中被称为自由存储空间(free store)或堆(heap)。该内存池同用于静态变量和自动变量的内存是分开的。

4.9 类型组合
#

指针数组,指向指针的指针,也叫二维指针可以用来创建动态二维数组,类型较为复杂。

4.10 数组的替代品
#

STL 标准库中的模板类 vectorarray 是数组的替代品。

4.10.1 模板类 vector
#

模板类 vector 类似于 string 类,也是一种动态数组,创建时要包含头文件 vector 。可以在运行阶段设置 vector 对象的长度,也可在末尾附加新数据,还可以在中间插入新数据,vector 的长度不要求必须设定,因为它会依据插入的数据量自动增长,容量增长的速度是 2 的 n 次方。

基本上,它是使用 new 创建动态数组的替代品。实际上,vector 类确实使用 newdelete 来管理内存,但这种工作是自动完成的。

vector 的声明方式:

1
2
#include<vector>
vector<typeName> v_name(number);

这样 v_name 就是一个 vector 对象,但 number 不是必须的。

vector 的最完整文档和例子,应参照网站 http://cplusplus.com/reference/vector/vector

4.10.2 模板类array(C++11)
#

vector 类的功能比数组强大,但付出的代价是效率稍低(其实没那么低效)。如果您需 要的是长度固定的数组,使用数组是更佳的选择,但代价是不那么方便 和安全。有鉴于此,C++11 新增了模板类 array,它也位于名称空间 std 中。与数组一样,array 对象的长度也是固定的,也使用栈(静态内存分 配),而不是自由存储区,因此其效率与数组相同,但更方便,更安全。

要创建array对象,需要包含头文件array。array对象的创建语法与 vector稍有不同:

1
2
3
#include<array>

array<typeName, num_element> arr;

与创建 vector 对象不同的是,n_element 不能是变量,必须是固定的常量和普通数组要求一样。

通过 array 模板类创建数组有什么优势?

https://cplusplus.com/reference/array/array

4.10.3 比较数组、vector 对象和 array 对象
#

这三者的异同是什么?

首先,无论是数组、vector 对象还是 array 对象,都可使用标 准数组表示法来访问各个元素;

其次,从地址可知,array 对象和数组存 储在相同的内存区域(即栈)中,而vector 对象存储在另一个区域(自由存储区或堆)中;

第三,可以将一个 array 对象直接赋给另一个 array 对象,而标准数组,必须逐元素复制数据 (使用 array 模板类的两个好处之一,另一个好处是控制数组超界风险)。

例子:

在上面代码中注意一个语句:

1
a1[-2] = 20.2;

这是什么意思?这个语句会被转换为:

1
*(a1 - 2) = 20.2;

含义是:找到a1指向的地方,向前移两个double元素,并将 20.2存储到目的地。也就是说,将信息存储到数组的外面。与C语言一 样,C++也不检查这种超界错误,也就是说数组的这种行为是不安全的,C++ 却不禁止,要小心。

vectorarray 对象能够禁止这种行为吗?如果您让它们禁止,它们 就能禁止。另外,这 些类还让您能够降低意外超界错误的概率。例如,它们包含成员函数 begin()end(),让您能够确定边界,以免无意间超界。

4.11 总结
#

数组、结构体和指针是C++的3种复合类型。

数组可以在一个数据对 象中存储多个同种类型的值。通过使用索引或下标,可以访问数组中各 个元素。

结构体可以将多个不同类型的值存储在同一个数据对象中,可以使用成员关系运算符. 来访问其中的成员。使用结构体的第一步是创建结构体模板,它定义结构存储了哪些成员。模板的名称将成为新类型的标识符,然后就可以声明这种类型的结构变量

指针是被设计用来存储地址的变量,指针指向它存储的地 址。指针声明指出了指针指向的对象的类型。对指针应用解引用运算符 *,将得到指针指向的位置中的值。

字符串是以空字符为结尾的一系列字符。字符串可用引号括起的字 符串常量表示,其中隐式包含了结尾的空字符。可以将字符串存储在 char数组中,可以用被初始化为指向字符串的char指针表示字符串。

string 对象将根据要存储的字符串自动调整其 大小,用户可以使用赋值运算符来复制字符串。

new 运算符允许在程序运行时为数据对象请求内存。该运算符返回 获得内存的地址,可以将这个地址赋给一个指针,程序将只能使用该指 针来访问这块内存。使用解引用运算符 * 获得其值;如果数据对象是数组,则可以像使用数组名那样使用指针来访问元素;如果数据对象是结构体,则可以用指针解引用运算符 -> 访问其成员。

指针和数组紧密相关。如果 ar 是数组名,则表达式 ar[i] 被解释为 *(ar + i)i 可以是负值,表示指针左移),其中数组名被解释为数组第一个元素的地址。这样,数组名的作用和指针相同。反过来,可以使用数组表示法,通过指针名来访 问new分配的数组中的元素。

运算符 newdelete 允许显式控制何时给数据对象分配内存,何时将内存归还给内存池。

第五章 循环和关系表达式
#

本章内容包括:

  • for 循环;
  • 表达式和语句;
  • 递增运算符和递减运算符: ++ 和 –;
  • 组合赋值运算符;
  • 复合语句(语句块);
  • 逗号运算符;
  • 关系运算符:>、>=、==、<=、< 和 !=;
  • while 循环;
  • typedef 工具;
  • do while 循环;
  • 字符输入方法 get() ;
  • 文件尾条件;
  • 嵌套循环和二维数组。

5.1 for 循环
#

5.1.1 for 循环初始化
#

for循环为执行重复的操作提供了循序渐进的步骤。组成部分完成下面这些步骤:

  • 设置初始值;
  • 执行测试,判断循环是否应当继续进行;
  • 执行循环操作;
  • 更新用于测试的值。

初始化、测试和更 新操作构成了控制部分,这些操作由括号括起。其中每部分都是一个表 达式,彼此由分号隔开。控制部分后面的语句叫作循环体,只要测试表 达式为true,它便被执行:

1
2
for (initialization; test-expression; update-expression)
    body;

循环体如果大于一条语句,需要加花括号:

1
2
3
for (initialization; test-expression; update-expression) {
    body;
}

C++语法将整个for看作一条语句—虽然循环体可以包含一条或多条 语句。循环只执行一次初始化。

for 是一个 C++ 关键字,编译器不会将 for 视为一个函 数,这还将防止将函数命名为 for

【编程风格 Tips】C++ 常用的方式是,在 for 和括号之间加上一个空格,但省略函数名与括号之间的空格。

看这个表达式赋值逻辑:

1
x = y = z = 0;

赋值运算符是从右向左结合的,因此首先将0赋给z,然 后将z = 0赋给y,依此类推。

C++在C循环的基础上添加了一项特性,要求对for循环句法做一些 微妙的调整。

这是原来的句法:

1
2
for (expresson; expression; expression)
    statement;

但,C++循环允许像下面这样做:

1
for (int i=0; i<5; ++i)

也就是说,可以在for循环的初始化部分中声明变量。这很方便,但并不适用于原来的句法,因为声明不是表达式。这个变量的生存时间只存在于for语句中,也就是说,当程序离开循环后,i 这个变量将消失。

5.1.2 回到for循环
#

很简单,略。

5.1.3 修改步长
#

循环示例每一轮循环都将循环计数加1或减1。可以通 过修改更新表达式来修改步长,例如改为表达式 i = i + by,其中 by 是用户选择的步长值。

5.1.4 使用for循环访问字符串
#

很简单,略。

5.1.5 递增运算符(++)和递减运算符(−−)
#

这两个运算符执行两种极其常见 的循环操作:将循环计数加1或减1。这两个运算符都有两种变体。前缀(prefix)版本位于操作数前 面,如++x;后缀(postfix)版本位于操作数后面,如x++。两个版本对操作数的影响是一样的,但是影响的时间不同。这就像对于钱包来说, 清理草坪之前付钱和清理草坪之后付钱的最终结果是一样的,但支付钱 的时间不同。

粗略地讲,a++意味着使用a的当前值计算表达式,然后将a的值加 1;而++b的意思是先将b的值加1,然后使用新的值来计算表达式。 递减操作符也同理。

5.1.6 副作用和顺序点
#

C++就递增运算符何时生效的哪些方面做了规 定,哪些方面没有规定。首先,副作用(side effect)指的是在计算表达式时对某些东西(如存储在变量中的值)进行了修改;顺序点 (sequence point)是程序执行过程中的一个点,在这里,进入下一步之 前将确保对所有的副作用都进行了评估。在C++中,语句中的分号就是 一个顺序点,这意味着程序处理下一条语句之前,赋值运算符、递增运算符和递减运算符执行的所有修改都必须完成。

另外,任何完整的表达式末尾都是一个顺序点。

何为完整表达式呢?它是这样一个表达式:不是另一个更大表达式 的子表达式。完整表达式的例子有:表达式语句中的表达式部分以及用 作while循环中检测条件的表达式。

5.1.7 前缀格式和后缀格式
#

如果变量被用于某些目的(如用作函数参数或给变量赋 值),使用前缀格式和后缀格式的结果将不同。

然而,虽然选择使用前缀格式还是后缀格式对程序的行为没有影 响,但执行速度可能有细微的差别。对于内置类型和当代的编译器而 言,这看似不是什么问题。然而,C++允许您针对类定义这些运算符, 在这种情况下,用户这样定义前缀函数:将值加1,然后返回结果;但后缀版本首先复制一个副本,将其加1,然后将复制的副本返回。因 此,对于类而言,前缀版本的效率比后缀版本高。

5.1.8 递增/递减运算符和指针
#

可以将递增运算符用于指针和基本变量。将递增 运算符用于指针时,将把指针的值增加其指向的数据类型占用的字节数,这种规则适用于对指针递增和递减。

前缀递增、前缀递减和解引用运算符的优先级相同,以从右到左的方式进行结合。后缀递增和后缀递减的优先级相同,但比前缀运算符的优先级高,这两个运算符以从左到右的方式进行结合。

前缀运算符的从右到到结合规则意味着 *++pt 的含义如下:现将 ++ 应用于 pt(因为 ++ 位于 * 的右边),然后将 * 应用于被递增后的 pt。 另一方面,++*pt 意味着先取得 pt 指向的值,然后将这个值加 1,pt 依然指向原来的地址;

而这个组合 (*pt)++ 圆括号指出,首先对指针解引用,得到值。然后,运算符++将这个值递增 1,但 pt 仍然指在原来的地址。

5.1.9 组合赋值运算符
#

+= 运算符将两个操作数相加,并将结果赋给左边的操作数。这意味着左边的操作数必须能够被赋值,如变量、数组元素、结构体成员或通过对指针解引用来标识的数据。

5.1.10 复合语句(语句块)
#

只有这个地方要注意:假设对循环体进行了缩进,但省略了花括号,编译器将忽略缩进,因此只有第一条语句位于循环中。

5.1.11 其他语法技巧—逗号运算符
#

语句块允许把两条或更多条语句放到按C++句法 只能放一条语句的地方。逗号运算符对表达式完成同样的任务,允许将 两个表达式放到C++句法只允许放一个表达式的地方。如:

1
++j, --i你  // 没有分号,合法,但不能独立运行,要在另外语句中,如 for 里

但逗号并不总是逗号运算符。 如:

1
int i, j; // 个声明中的逗号将变量列 表中相邻的名称分开

到目前为止,逗号运算符最常见的用途是将两个或更多的表达式放 到一个for循环表达式中。不过C++还为这个运算符提供了另外两个特 性。首先,它确保先计算第一个表达式,然后计算第二个表达式(换句 话说,逗号运算符是一个顺序点)。

1
i = 20, j = 2 * i   // 这是合法的,这里 y 是 40

在所有运算符中,逗号运算符的优先级是最低的。

5.1.12 关系表达式
#

5.1.13 赋值、比较和可能犯的错误
#

很简单,略。

5.1.14 C-风格字符串的比较
#

假设要知道字符数组中的字符串是不是mate。如果word是数组名, 下面的测试可能并不能像我们预想的那样工作:

1
word == "mate"

请记住,数组名是数组的地址。同样,用引号括起的字符串常量也 是其地址。因此,上面的关系表达式不是判断两个字符串是否相同,而 是查看它们是否存储在相同的地址上。两个字符串的地址是否相同呢? 回答是否定的,虽然它们包含相同的字符。

由于C++将C-风格字符串视为地址,因此如果使用关系运算符来比 较它们,将无法得到满意的结果。需要用 C 字符串库中的 strcmp() 函数来比较。 该函数接受两个字符串地址作为参数。这意味着 参数可以是指针、字符串常量或字符数组名。如果两个字符串相同,该 函数将返回零;如果第一个字符串按字母顺序排在第二个字符串之前, 则strcmp( )将返回一个负数值;如果第一个字符串按字母顺序排在第二 个字符串之后,则strcpm( )将返回一个正数值。实际上,“按系统排列顺 序”比“按字母顺序”更准确。这意味着字符是根据字符的系统编码来进 行比较的。例如,使用ASCII码时,所有大写字母的编码都比小写字母 小,所以按排列顺序,大写字母将位于小写字母之前。因此,字符 串“Zoo”在字符串“aviary”之前。

存储在不同长度的数组 中的字符串彼此不相等。但是C-风格字符串是通过结尾的空值字符定义 的,而不是由其所在数组的长度定义的。这意味着两个字符串即使被存 储在长度不同的数组中,也可能是相同的。

5.1.15 比较string类字符串
#

如果使用string类字符串而不是C-风格字符串,比较起来将简单些。直接通过运算符比较:>, <, == 等。

5.2 while 循环
#

while 循环是一个没有初始化和更新部分的 for 循环,它只有测试条件和循环体,与for循环一样,循环体也由一条语句或两个花括号定义的语句块组成。句法如下:

1
2
while (test-condition)
    body

如果循环体有多条语句,需要花括号:

1
2
3
while (test-condition) {
    body
}

首先,程序计算圆括号内的测试条件(test-condition)表达式。如 果该表达式为true,则执行循环体中的语句。执行完循环体后,程序返 回测试条件,对它进行重新评估。如果该条件为非零,则再次执行循环 体。测试和执行将一直进行下去,直到测试条件为false为止。

while 循环和 for 循环一样,也是一 种入口条件循环。因此,如果测试条件一开始便为false,则程序将不会 执行循环体。

5.2.1 for 与 while
#

在C++中,for 和 while 循环本质上是相同的。

但它们之间存在三个差别:

  • for 循环中省略了测试条件时,将认为条件为 true
  • for 循环中,可使用初始化语句声明一个局部变量,但在 while 循环中不能这样做;
  • 如果循环体中包括 continue 语句,情况将稍有不同,continue 语句将在第6章讨论。

通常,程序员使用 for 循环来为循环计数,因为 for 循环格式允许将所有相关的信息—初始值、终止值和更新计数器的方法—放在同一个地方。在无法预先知道循环将执行的次数时,程序员常使用 while 循环。

5.2.2 等待一段时间:编写延时循环
#

函数 clock() 可以返回程序开始执行后所用的系统时间。但,首先,clock( )返回时间的单位不一定是秒;其次,该函数的返回类 型在某些系统上可能是long,在另一些系统上可能是unsigned long或其 他类型。

头文件 ctime 提供了这些问题的解决方案。首先,它定义了一个符号常量—CLOCKS_PER_SEC,该常量等于 每秒钟包含的系统时间单位数。因此,将系统时间除以这个值,可以得 到秒数。或者将秒数乘以 CLOCK_PER_SEC,可以得到以系统时间单位 为单位的时间。其次,ctimeclock_t 作为 clock() 返回类型的别名,这意味着可以将变量声明为 clock_t 类型,编译器将把它转换为long、unsigned int 或适合系统的其他类型。

do while 循环
#

它 不同于另外两种循环,因为它是出口条件(exit condition)循环。这意 味着这种循环将首先执行循环体,然后再判定测试表达式,决定是否应 继续执行循环。如果条件为false,则循环终止;否则,进入新一轮的执 行和测试。这样的循环通常至少执行一次,循环体是一条语句或用括号括起的语句块。句法:

1
2
3
do {
    body
} while (test-condition);

5.4 基于范围的for循环(C++11)
#

C++11新增了一种循环:基于范围(range-based)的for循环。这简化了一种常见的循环任务:对数组(或容器类,如vector和array)的每 个元素执行相同的操作。

5.5 循环和文本输入
#

5.5.1 使用原始的cin进行输入
#

为什么程序在输出时省略了空格呢?原因 在cin。读取char值时,与读取其他基本类型一样,cin将忽略空格和换行 符。因此输入中的空格没有被回显,也没有被包括在计数内。

更为复杂的是,发送给cin的输入被缓冲。这意味着只有在用户按 下回车键后,他输入的内容才会被发送给程序。这就是在运行该程序 时,可以在#后面输入字符的原因。按下回车键后,整个字符序列将被 发送给程序,但程序在遇到#字符后将结束对输入的处理。

5.5.2 使用cin.get(char)进行补救
#

通常,逐个字符读取输入的程序需要检查每个字符,包括空格、制 表符和换行符。cin 所属的 istream 类(在 iostream 中定义)中包含一个能 够满足这种要求的成员函数。具体地说,成员函数cin.get(ch)读取输入 中的下一个字符(即使它是空格),并将其赋给变量ch。使用这个函数 调用替换cin»ch,可以修补程序清单5.16的问题。

在C语言中, 要修改变量的值,必须将变量的地址传递给函数。但程序清单5.17调用 cin.get( )时,传递的是ch,而不是&ch。在C语言中,这样的代码无效, 但在C++中有效,只要函数将参数声明为引用即可。引用是C++在C语 言的基础上新增的一种类型。头文件iostream将cin.get(ch)的参数声明为 引用类型,因此该函数可以修改其参数的值。

5.5.3 使用哪一个cin.get( )
#

函数重载允许创建多个同名函数,条件是它们 的参数列表不同。例如,如果在C++中使用 cin.get(name,ArSize), 则编译器将找到使用 char*int 作为参数的 cin.get() 版本;如果使用 cin.get(ch),则编译器将使用接受一个 char 参数的版本;如果没有提 供参数,则编译器将使用不接受任何参数的cin.get() 版本。函数重载允 许对多个相关的函数使用相同的名称,这些函数以不同方式或针对不同类型执行相同的基本任务。

5.5.4 文件尾条件
#

如果输入来自于文件,则可以使用一种功能更强大的技 术—检测文件尾(EOF)。C++输入工具和操作系统协同工作,来检测文件尾并将这种信息告知程序。

检测到EOF后,cin将两位(eofbit和failbit)都设置为1。可以通过 成员函数eof( )来查看eofbit是否被设置;如果检测到EOF,则cin.eof( )将 返回bool值true,否则返回false。同样,如果eofbit或failbit被设置为1, 则fail( )成员函数返回true,否则返回false。

5.5.5 另一个cin.get( )版本
#

为成功地使用cin.get( ),需要知道其如何处理EOF条件。当该函数 到达EOF时,将没有可返回的字符。相反,cin.get( )将返回一个用符号 常量EOF表示的特殊值。该常量是在头文件iostream中定义的。EOF值 必须不同于任何有效的字符值,以便程序不会将EOF与常规字符混淆。 通常,EOF被定义为值−1,因为没有ASCII码为−1的字符,但并不需要知道实际的值,而只需在程序中使用EOF即可。

另一方面,使用cin.get(ch)(有一个参数)进行输入时,将不会导 致任何类型方面的问题。前面讲过,cin.get(char)函数在到达EOF时,不 会将一个特殊值赋给ch。事实上,在这种情况下,它不会将任何值赋给 ch。ch不会被用来存储非char值。

那么应使用cin.get( )还是cin.get(char)呢?使用字符参数的版本更符 合对象方式,因为其返回值是istream对象。这意味着可以将它们拼接起 来。例如,下面的代码将输入中的下一个字符读入到ch1中,并将接下 来的一个字符读入到ch2中:

1
cin.get(ch1).get(ch2);

5.6 嵌套循环和二维数组
#

C++没有提供二维数组类型,但用户可以创建每个元素本身都是数 组的数组。可以这样声明数组:

1
2

int maxtemps[4][5];

假设要打印数组所有的内容,可以用一个for循环来改变行,用另一 个被嵌套的for循环来改变列:

1
2
3
4
5
6
for (int row = 0; row < 4; ++row) {
    for (int col=0; col < 5; ++col) {
        cout << maxtemps[row][col] << "\t";
    }
    cout << endl;
}

实际上,我更喜欢用 vector 来创建动态二维数据,只要创建 vector 的vector 就行,用起来更简单。

5.7 总结
#

C++提供了3种循环:for循环、while循环和do while循环。如果循环 测试条件为true或非零,则循环将重复执行一组指令;如果测试条件为 false或0,则结束循环。for循环和while循环都是入口条件循环,这意味 着程序将在执行循环体中的语句之前检查测试条件。do while循环是出 口条件循环,这意味着其将在执行循环体中的语句之后检查条件。

关系表达式对两个值进行比较,常被用作循环测试条件。关系表达 式是通过使用6种关系运算符之一构成的:<<===>=>!=。 关系表达式的结果为bool类型,值为true或false。

第六章 分支语句和逻辑运算符
#

本章的内容包括:

  • if 语句;
  • if else 语句;
  • 逻辑运算符:&&||!
  • cctype 字符函数库;
  • 条件运算符:?:
  • switch 语句;
  • continuebreak 语句;
  • 读取数字的循环;
  • 基本文件输入/输出。

6.1 if 语句
#

如果测试条件为 true, 则 if 语句将引导程序执行语句或语句块;如果条件是false,程序将跳过 这条语句或语句块。因此,if 语句让程序能够决定是否应执行特定的语句。

6.1.1 if else 语句
#

if 语句让程序决定是否执行特定的语句或语句块,而if else语句则让程序决定执行两条语句或语句块中的哪一条,这种语句对于选择其中一 种操作很有用。

if else 语句的通用格式如下:

6.1.2 格式化 if else 语句
#

if else 中的两种操作都必须是一条语句。如果需要多条语句,需要 用大括号将它们括起来,组成一个块语句。

6.1.3 if- else if- else 结构
#

计算机程序也可能提供两个以上的 选择。可以将C++if else 语句进行扩展来满足这种需求。正如读者知 道的,else 之后应是一条语句,也可以是语句块。由于 if else 语句本身是 一条语句,所以可以放在 else 的后面。

6.2 逻辑表达式
#

C++ 提供了3种逻辑运算符,来组合或修改已有的表达式。这些运算符分别是逻 辑 OR (||)、逻辑 AND (&&)和逻辑 NOT (!)

6.2.1 逻辑OR运算符:||
#

C++可以采用逻辑OR运算 符 ||,将两个表达式组合在一起。如果原来表达式中的任何一个或 全部都为 true(或非零),则得到的表达式的值为 true;否则,表达式的值为 false

C++ 规定 || 运算符是个顺序点(sequence point)。也是说,先修改左侧的值,再对右侧的值进行判定(C++11的说法是,运算符左边的子表达式先于右边的子表达式)。

6.2.2 逻辑AND运算符:&&
#

逻辑AND运算符 &&,也是将两个表达式组合成一个表达式。 仅当原来的两个表达式都为 true 时,得到的表达式的值才为 true

&& 运算符也是顺序点,因此将首先判定左侧, 并且在右侧被判定之前产生所有的副作用。如果左侧为 false,则整个逻 辑表达式必定为 false,在这种情况下,C++ 将不会再对右侧进行判定。

6.2.3 用 && 来设置取值范围
#

&& 运算符还允许建立一系列if else if else语句,其中每种选择都对 应于一个特定的取值范围。

6.2.4 逻辑NOT运算符:!
#

! 运算符将它后面的表达式的真值取反。也是说,如果 expressiontrue,则 !expressionfalse;如果 expressionfalse,则 !expressiontrue。更准确地说,如果 expressiontrue 或非零,则 !expressionfalse

然而,! 运算符对于返回 true-false 值或可以被解释为 true-false 值的函数来说很有用。例如,如果C-风格字符串 s1s2 不同,则 strcmp(s1,s2) 将返回非零true值,否则返回 0。这意味着如果这两个字符串相同, 则 !strcmp(s1,s2)true

6.2.5 逻辑运算符细节
#

! 运算符的优先级高于所有的关系运算符和算术运算符。因此,要对表达式求反,必须用括号将其括起。

6.2.6 其他表示方式
#

并不是所有的键盘都提供了用作逻辑运算符的符号,因此C++标准 提供了另一种表示方式,如表6.3所示。标识符andornot都是C++保 留字,这意味着不能将它们用作变量名等。它们不是关键字,因为它们 都是已有语言特性的另一种表示方式。另外,它们并不是C语言中的保 留字,但C语言程序可以将它们用作运算符,只要在程序中包含了头文 件 iso646.h。C++不要求使用头文件。

6.3 字符函数库 cctype
#

C++ 从 C 语言继承了一个与字符相关的、非常方便的函数软件包, 它可以简化诸如确定字符是否为大写字母、数字、标点符号等工作,这 些函数的原型是在头文件cctype(老式的风格中为 ctype.h)中定义的。 例如,如果 ch 是一个字母,则 isalpha(ch) 函数返回一个非零值,否则返回 0。同样,如果 ch 是标点符号(如逗号或句号),函数 ispunct(ch) 将返回 true。(注意这些函数的返回类型为 int,而不是 bool, 但通常 bool 转换能够将它们视为bool 类型

isalpha() 不仅更容易使用,而且更通用。例子中用AND/OR格式假设A-Z的字符编码是连续的,其他字符的编码不在这个范围内。这种假设对于ASCII码来说是成立的,但也并非总是如此。

具体地说:

  • isalpha() 用来检查字符是否为字母字符;
  • isdigit() 用来测试字符是否为数字字符(0~9),如 3;
  • isspace() 用来测试字符是否为空白,包括换行符、空格和制表符;
  • ispunct() 用来测试字符是否为标点符号。

总结一下这个函数包:

6.4 ?: 运算符
#

C++有一个常被用来代替 if else 语句的运算符,这个运算符被称为条件运算符 ?:,它是C++中唯一一个需要3个操作数的运算符,也叫三目运算符

1
expression1 ? expression2 : expression3

如果 expression1true,则整个条件表达式的值为 expression2 的值; 否则,整个表达式的值为 expression3 的值。

6.5 switch语句
#

switch 语句与 Pascal 等语言中类似的语句之间存在重大的差别。 C++中的 case 标签只是行标签,而不是选项之间的界线。也是说,程序跳到 switch 中特定代码行后,将依次执行之后的所有语句,除非有明确的其他指示。程序不会在执行到下一个 case 处自动停止,要让程序执行 完一组特定语句后停止,必须使用break 语句。这将导致程序跳到 switch 后面的语句处执行。

6.5.1 将枚举量用作标签
#

通常,cin 无法识别枚举类型(它不知道程序员是如 何定义它们的),因此该程序要求用户选择选项时输入一个整数。当 switch 语句将 int 值和枚举量标签进行比较时,将枚举量提升为 int。另 外,在 while 循环测试条件中,也会将枚举量提升为 int 类型。

6.5.2 switchif else
#

switch语句和if else语句都允许程序从选项中进行选择。相比之下, if else更通用,例如,它可以处理取值范围。

然而,switch并不是为处理取值范围而设计的。switch语句中的每 一个case标签都必须是一个单独的值。另外,这个值必须是整数(包括 char),因此 switch 无法处理浮点测试。另外 case 标签值还必须是常量。 如果选项涉及取值范围、浮点测试或两个变量的比较,则应使用 if else 语句。

6.6 breakcontinue 语句
#

breakcontinue语句都使程序能够跳过部分代码。可以在 switch语句或任何循环中使用 break 语句,使程序跳到 switch 或循环后面的语句处执行。continue 语句用于循环中,让程序跳过循环体中余下的代码,并开始新一轮循环。

6.7 读取数字的循环
#

假设要编写一个将一系列数字读入到数组中的程序,并允许用户在 数组填满之前结束输入,一种方法是利用cin。看下面的代码:

1
2
int n;
cin >> n;

如果用户输入一个单词,而不是一个数字,发生这种类型不匹配的情况时,将发生4种情况:

  • n 的值保持不变;
  • 不匹配的输入将被留在输入队列中;
  • cin 对象中的一个错误标记被设置;
  • cin 方法的调用将返回 false(如果被转换为 bool 类型)。

方法返回 false 意味着可以用非数字输入来结束读取数字的循环。当用户输入的不是数字时,该程序将不再读取输入。非数字输入设置错误标记意味着必须重置该标记,程序才能继续读取输 入clear() 方法重置错误输入标记,同时也重置文件尾(EOF条件,参见第5章)。输入错误和 EOF 都将导致cin返回 false

如果用户输入非数字输入,程序将拒绝,并要求用户继续输入数字。可以看到,可以使用 cin 输入表达式的值来检测输入是不是数字。 程序发现用户输入了错误内容时,应采取3个步骤:

  • 重置 cin 以接受新的输入;
  • 删除错误输入;
  • 提示用户再输入。

请注意,程序必须先重置cin,然后才能删除错误输入。如下程序清单 6.14演示了如何完成这些工作。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
#include <iostream>

const int Max = 5;
int main() {
    using namespace std;
    // get data
    int golf[Max];
    cout << "Please enter your golf scores.\n";
    cout << "You must enter " << Max << " rounds.\n";
    int i;
    for (i = 0; i < Max; i++) {
        cout << "round #" << i+1 << ": ";
        while (!(cin >> golf[i])) {
            cin.clear();     // reset input
            while (cin.get() != '\n')
                continue;    // get rid of bad input
            cout << "Please enter a number: ";
        }
    }
    
    // calculate average
    double total = 0.0;
    for (i = 0; i < Max; i++)
        total += golf[i];
        
    // report results
    cout << total / Max << " = average score "
         << Max << " rounds\n";
    // cin.get();
    // cin.get();
    return 0;
}

如果用户输入 88,则 cin 表达式将为 true,因此将一个值放到数组中;而表达式 !(cin >> golf [i])false,因此结束内部循环。然而,如果用户输入 must i?,则 cin 表达式将为 false,因此不会将任何值放到数组 中;而表达式 !(cin >> golf [i]) 将为 true,因此进入内部的 while 循环。该循环的第一条语句使用 clear() 方法重置输入,如果省略这条语句,程序将拒绝继续读取输入。接下来,程序在 while 循环中使用 cin.get() 来读取 行尾之前的所有输入,从而删除这一行中的错误输入。另一种方法是读取到下一个空白字符,这样将每次删除一个单词,而不是一次删除整 行。最后,程序告诉用户,应输入一个数字。

6.8 简单文件输入/输出
#

6.8.1 文本I/O和文本文件
#

6.8.2 写入到文本文件中
#

文件输出:

  • 必须包含头文件 fstream
  • 头文件 fstream 定义了一个用于处理输出的 ofstream 类;
  • 需要声明一个或多个 ofstream 变量(对象),并以自己喜欢的方式 对其进行命名,条件是遵守常用的命名规则;
  • 必须指明名称空间 std。例如,为引用元素 ofstream,必须使用编译指令using 或前缀 std::
  • 需要将 ofstream 对象与文件关联起来。为此,方法之一是使用 open() 方法;
  • 使用完文件后,应使用方法 close() 将其关闭;
  • 可结合使用 ofstream 对象和运算符 << 来输出各种类型的数据。

虽然头文件 iostream 提供了一个预先定义好的名为 coutostream 对象,但您必须声明自己的 ofstream 对象,为其命名,并将其同文件关联起来。

注意,方法open() 只接受C-风格字符串作为参数,这可以是一个 字面字符串,也可以是存储在数组中的字符串。

重要的是,声明一个ofstream对象并将其同文件关联起来后,便可以像使用cout那样使用它。所有可用于cout的操作和方法(如«、endl 和setf( ))都可用于ofstream对象(如前述示例中的outFile和fout)。

总之,使用文件输出的主要步骤如下:

  • 包含头文件 fstream
  • 创建一个 ofstream 对象;
  • 将该 ofstream 对象同一个文件关联起来;
  • 就像使用 cout 那样使用该 ofstream 对象;
  • 调用 close() 成员函数,关闭文件。

默认情况下,open() 的将首先截断该文件,即将其长度截短到零——丢弃原有的内容,然后将新的输出加入到该文件中。

6.8.3 读取文本文件
#

接下来介绍文本文件输入,它是基于控制台输入的。控制台输入涉 及多个方面,下面首先总结这些方面:

  • 必须包含头文件 fstream
  • 头文件 fstream 定义了一个用于处理输入的 ifstream 类;
  • 需要声明一个或多个 ifstream 变量(对象),并以自己喜欢的方式对其进行命名,条件是遵守常用的命名规则;
  • 必须指明名称空间 std;例如,为引用元素 ifstream,必须使用编译指令using或前缀std::
  • 需要将 ifstream 对象与文件关联起来。为此,方法之一是使用 open() 方法;
  • 使用完文件后,应使用 close() 方法将其关闭;
  • 可结合使用 ifstream 对象和运算符 >> 来读取各种类型的数据;
  • 可以使用 ifstream 对象和 get() 方法来读取一个字符,使用 ifstream 对象和 getline() 来读取一行字符;
  • 可以结合使用 ifstreameof()fail() 等方法来判断输入是否成功;
  • ifstream 对象本身被用作测试条件时,如果最后一个读取操作成 功,它将被转换为布尔值 true,否则被转换为 false

如果试图打开一个不存在的文件用于输入,情况将如何呢?这种错误将导致后面使用ifstream 对象进行输入时失败。检查文件是否被成功打开的首选方法是使用is_open(),为此,可以使用类似于下面的代码:

1
2
3
4
inFile.open("b.txt");
if (!inFile.is_open()) {
    exit(EXIT_FEAILURE);
}

如果文件被成功地打开,方法 is_open() 将返回 true;因此如果文件没有被打开,表达式 !inFile.is_open() 将为 true。函数 exit() 的原型是在头文件 cstdlib 中定义的,在该头文件中,还定义了一个用于同操作系统通信的参数值 EXIT_FAILURE。函数 exit() 终止程序。

方法 is_open() 是C++中相对较新的内容。如果读者的编译器不支持它,可使用较老的方法 good() 来代替。

程序例子:

假设该文件名为scores.txt,包含的内容如下:

1
2
3
18 19 18.5 13.5
16 19.5 18.5
17.5

检查文件是否被成功打开至关重要。

读取文件时,有 几点需要检查。首先,程序读取文件时不应超过 EOF。如果最后一次读取数据时遇到 EOF,方法 eof() 将返回 true。其次,程序可能遇到类型不 匹配的情况。例如,程序清单6.16期望文件中只包含数字。如果最后一次读取操作中发生了类型不匹配的情况,方法 fail() 将返回 true(如果遇到了 EOF,该方法也将返回 true)。最后,可能出现意外的问题,如文 件受损或硬件故障。如果最后一次读取文件时发生了这样的问题,方法 bad() 将返回 true。不要分别检查这些情况,一种更简单的方法是使用 good() 方法,该方法在没有发生任何错误时返回 true

1
2
3
while (inFile.good()) {
    ......
}

方法 good() 指出最后一次读取输入的操作是否成功,这一点至关重要。这意味着应该在执行读取输入的操作后,立刻应用这种测试。为此,一种标准方法是,在循环之前(首次执行循环测试前)放置一条输 入语句,并在循环的末尾(下次执行循环测试之前)放置另一条输入语句:

1
2
3
4
5
6
// standard file-reading loop design
inFile >> value;         // get first value
while (inFile.good()) {  // while uput good and not at EOF
    //loop body goes here
    inFile >> value;     // get next value
}

鉴于以下事实,可以对上述代码进行精简:表达式 inFile >> value 的结果为inFile,而在需要一个 bool 值的情况下,inFile 的结果为 inFile.good(),即 truefalse

因此,可以将两条输入语句用一条用作循环测试的输入语句代替。 也就是说,可以将上述循环结构替换为如下循环结构:

1
2
3
4
5
// omit pre-loop input
while (inFile >> value) {  // read and test for success
    // loop body goes here
    // omit end-of-loop input
}

这些代码紧跟在循环的后面,用于判断循环为何终止。由于 eof() 只能判断是否到达 EOF,而 fail() 可用于检查 EOF 和类型不匹配,因此上述 代码首先判断是否到达 EOF。这样,如果执行到了 else if 测试,便可排除 EOF,因此,如果 fail() 返回 true,便可断定导致循环终止的原因是类型不匹配。

这种设计仍然遵循了在测试之前进行读取的规则,因为要计算表达式 inFile >> value 的值,程序必须首先试图将一个数字读取到 value 中。

以上仅是对文件 I/O 的初步介绍。

6.9 总结
#

C++提供了 if 语句、if-else 语句和 switch 语句来管理选项。 C++还提供了帮助决策的运算符。通过使用逻辑运算符(&&||!),可以组合或修改关系表达式,创建更细致的测试。条件运算符 (?:) 提供了一种选择两个值之一 的简洁方式。

cctype 字符函数库提供了一组方便的、功能强大的工具,可用于分析字符输入。

文件 I/O 与控制台 I/O 极其相似。声明 ifstream 和 ofstream 对象,并将它们同文件关联起来后,便可以像使用 cin 和 cout 那样使用这些对象。

第七章 函数——C++的编程模块
#

本周内容包括:

  • 函数基本知识;
  • 函数原型;
  • 按值传递函数参数;
  • 设计处理数据的函数;
  • 使用 const 指针参数;
  • 设计处理文本字符串的函数;
  • 设计处理结构的函数;
  • 设计处理 string 对象的函数;
  • 调用自身的函数(递归);
  • 指向函数的指数。

7.1 函数基本知识
#

自定义函数三要素:

  • 函数定义;
  • 函数原型;
  • 调用函数。

7.1.1 定义函数
#

函数有两类:没有返回值的函数和有返回值的函数。

函数定义通用格式:

1
2
3
4
void functionName(parameterList) {
    statement(s);
    return;
}

C++ 对于返回值的类型有一定的限制:不能是数组,但可以是其他任何类型——整数、浮点数、指针,甚至可以是结构体和对象。

C++ 函数返回值的原理是什么? 首先,函数将返回值复制到指定的CPU寄存器或内存单元中将值返回;随后,调用程序将查看该内存单元。返回函数和调用函数必须就该内存单元中存储的数据的类型达成一致。函数原型将返回值类型告知调用程序,而函数定义命令被调用函数应返回什么类型的数据。

7.1.2 函数原型和函数调用
#

  1. 为什么需要函数原型

原型描述了函数到编译器的接口,也就是说,它将函数返回值的类型(如果有的话)以及参数的类型和数量告诉编译器。举个例子:

1
double volume = cube(side);

首先,原型告诉编译器,cube() 有一个 double 参数。如果程序没有 提供这样的参数,原型将让编译器能够捕获这种错误。其次,cube() 函数完成计算后,将把返回值放置在指定的位置——可能是 CPU 寄存器, 也可能是内存中。然后调用函数(这里为 main())将从这个位置取得返回值。由于原型指出了 cube() 的类型为 double,因此编译器知道应检索多少个字节以及如何解释它们。如果没有这些信息,编译器将只能进行猜测,而编译器是不会这样做的。

为何编译器需要原型,它就不能在文件中进一步查找,以了解函数是如何定义的吗?这种方法的一个问题是效率不高。编译器在搜索文件的剩余部分时必须停止对 main() 的编译。一个更严重的问题是,函数甚至可能并不在文件中。C++ 允许将一个程序放 在多个文件中,单独编译这些文件,然后再将它们组合起来。在这种情况下,编译器在编译 main() 时,可能无权访问函数代码。如果函数位于库中,情况也将如此。避免使用函数原型的唯一方法是,在首次使用函数之前定义它,但这并不总是可行的。

  1. 原型的语法

函数原型是一条语句,因此必须以分号结束。获得原型最简单的方 法是,复制函数定义中的函数头,并添加分号。 函数原型不要求提供变量名,有类型列表就足够了。通常,在原型的参数列表中,可以包括变量名,也可以不包括。原 型中的变量名相当于占位符,因此不必与函数定义中的变量名相同。

  1. 原型的功能

它们可以极大地降低程序出错的几率。具体来说,原型 确保以下几点:

  • 编译器正确处理函数返回值;
  • 编译器检查使用的参数数目是否正确;
  • 编译器检查使用的参数类型是否正确,如果不正确,则转换为正确的类型。

仅当有意义时,原型化才会导致类型转换。例如,原型不会将整数转换为结构或指针。

在编译阶段进行的原型化被称为静态类型检查(static type checking)。

7.2 函数参数和按值传递
#

C++通常按值传递参数,这意味着将 数值参数传递给函数,而后者将其赋给一个新的变量。

1
double volume = cube(side);

side 是一个变量,被调用时,该函数将创建一个新的名为x的double变量,并将其初 始化为5。这样,cube( )执行的操作将不会影响main( )中的数据,因为 cube( )使用的是side的副本,而不是原来的数据。

在函数中声明的变量(包括参数)是该函数私有的。在函数被调用 时,计算机将为这些变量分配内存;在函数结束时,计算机将释放这些 变量使用的内存。这样的变量被称为局部变量,因为它们被限制在函数中。前面提到过,这样做有助于确保数据的完整性。这还意 味着,如果在main( )中声明了一个名为x的变量,同时在另一个函数中 也声明了一个名为x的变量,则它们将是两个完全不同的、毫无关系的变量。

7.2.1 多个参数
#

函数可以有多个参数。在调用函数时,只需使用逗号将这些参数分开即可。

它使用cin>>ch,而不是cin.get(ch)ch = cin.get()来读取一个字符。这样做是有原因的。前面讲过,这两个cin.get() 函数读取所有的输入字符,包括空格和换行符,而 cin>> 跳过空格和换行符。当用户对程序提示作出响应时,必须在每行的最后按 Enter 键,以生成换行符。cin>>ch 方法可以轻松地跳过这些换行符,但当输入的下一个字符为数字时,cin.get() 将读取后面的换行符,虽然可以通过编程来避开这种麻烦,但比较简便的方法是像该程序那样使用 cin

7.3 函数和数
#

需要将数组名作为参数传递给它,为使函数通用,而不限于特定长度的数组,还需要传递数组长度。

1
int sum_arr(int arr[], int n);

方括号指出 arr 是一个数组,而方括号为空则表明,可以将任何长度的数组传递给该函数。但实际情况并非如此:arr实际上并不是数组,而是一个指针,好消息是,在编写函数的其余部分时,可以将 arr 看作是数组。

7.3.1 函数如何使用指针来处理数组
#

在大多数情况下,C++和C语言一样,也将数组名视为指针。C++将数组名解释为其第一个元素的地址:

1
cookies == &cookies[0];   // array name is the address of first element

首先,数组声明使用数组名来标记存储位置; 其次,对数组名使用sizeof将得到整个数组的长度(以字节为单位); 第三,正如第4章指出的,将地址运算符&用于数组名时,将返回整个数组的地址。

当且仅当在函数头中或者函数原型中,int *arrint arr[] 的含义是相同的。它们都指 arr 是一个 int 指针。

对于数组,以下两个语句是恒等的:

1
2
arr[i] == *(arr+i);  // values in two notations
&arr[i] == arr + i;  // addresses in two notations

7.3.2 将数组作为参数意味着什么
#

传递常规变量时,函数将使用该变量的拷贝;但传递数组时,函数将使用原来的数组(传地址)。实际上,这种区别并不违反C++按值传递的方法,sum_arr() 函数仍传递了一个值,这个值被赋给 一个新变量,但这个值是一个地址,而不是数组的内容。

将数组地址作为参数可以节省复制整个数组所需的时间和内存。如果数组很大,则使用拷贝的系统开销将非常大;程序不仅需要更多的计算机内存,还需要花费时间来复制大块的数据。但另一方面,使用原始数据增加了破坏数据的风险

sum_arr(cookies+4, 4)sum_arr(&cookies[4], 4) 是等效的。

7.3.3 更多数组函数示
#

1.填充数组

2.显示数组及用const保护数组

创建显示数组内容的函数很简单。只需将数组名和填充的元素数目传递给函数,然后该函数使用循环来显示每个元素。然而,还有另一个问题——确保显示函数不修改原始数组。除非函数的目的就是修改传递给它的数据,否则应避免发生这种情况。使用普通参数时,这种保护将自动实现,这是由于C++按值传递数据,函数使用数据的副本。然而,接受数组名的函数将使用原始数据。为防止函数无意中修改数组的内容,可在声明形参时 使用关键字const

1
void show_array(const double arr[], int n);

show_array() 将数组视为只读数据。

3.修改数组

在这个例子中,对数组进行的第三项操作是将每个元素与同一个重新评估因子相乘。需要给函数传递3个参数:因子、数组和元素数目。 该函数不需要返回值,因此其代码如下:

1
2
3
4
5
void revalue(double r, double arr[], int n) {
    for (int i=0; i < n; ++i) {
        arr[i] *= r;
    }
}

由于这个函数将修改数组的值,因此在声明 arr 时,不能使用 const

4.将上述代码组合起来

前面已经讨论了与该示例相关的重要编程细节,因此这里回顾一下 整个过程。我们首先考虑的是通过数据类型和设计适当的函数来处理数据,然后将这些函数组合成一个程序。有时也称为自下而上的程序设计 (bottom-up programming),因为设计过程从组件到整体进行。这种方 法非常适合于OOP——它首先强调的是数据表示和操纵。而传统的过程性编程倾向于从上而下的程序设计(top-down programming),首先指定模块化设计方案,然后再研究细节。这两种方法都很有用,最终的产品都是模块化程序。

6.数组处理函数的常用编写方式

假设要编写一个处理double数组的函数。如果该函数要修改数组, 其原型可能类似于下面这样:

1
void f_modfiy(double arr[], int n);

如果函数不修改数组,其原型可能类似于下面这样:

1
void _f_no_change(const double arr[], int n);

7.3.4 使用数组区间的函数
#

对于处理数组的C++函数,必须将数组中的数据种类、数组的起始位置和数组中元素数量提交给它;传统的C/C++方法是,将指向数组起始处的指针作为一个参数,将数组长度作为第二个参数(指针指出数组的位置和数据类型),这样便给函数提供找到所有数据所需的信息。

还有另一种给函数提供所需信息的方法,即指定元素区间 (range),这可以通过传递两个指针来完成:一个指针标识数组的开头,另一个指针标识数组的尾部

它将 pt 设置为指向要处理的第一个元素(begin指向的元素)的指针,并将*pt(元素的值)加入到 total 中。然后,循环通过递增操作来更新 pt,使之指向下一个元素。只要 pt 不等于 end,这一过程就将继续下去。当 pt 等于 end 时,它将指向区间中最后一个元素后面的一个位置,此时循环将结束。

7.3.5 指针和 const
#

const 用于指针有一些很微妙的地方,可以用两种不同的方式将 const 关键字用于指针。第一种方法是让指针指向一个常量对象,防止使用该指针来修改所指向的值,第二种方法是将指针本身声明为常量,这样可以防止改变指针指向的位置。

首先看一个指向常量的指针:

这里要求 pt 指向的是一个 const int,所以,赋值之后我们是不能用 *pt 来修改 age 的值的。

现在来看一个微妙的问题。pt的声明并不限定着它指向的值就必须得是一个常量,只是对pt来说,这个值是常量。例如,pt指向age,但age不是const。我们是可以直接通过 age 变量来修改 age 的值的,但不能使用 pt 指针来修改它。

我觉得这类指针可以称为:只读指针。这个指针可以移动,但是它只能读出它所指向地址的内容,但是无法通过这个指针修改其中的数据。

C++禁止第二种情况的原因很简单——如果将 g_moon 的地 址赋给 pm,那么就可以用 pm 来修改 g_moon 的值,这使得 g_moonconst 状态很荒谬,因此, C++禁止将 const 的地址赋给非 const 指针

假设有一个由 const 数据组成的数组则禁止将常量数组的地址赋给非常量指针将意味着不能将数组名作为参数传递给使用非常量形参的函数。

只要条件允许,则应将指针形参声明为指向 const 的指针。

尽可能使用 const: 将指针参数声明为指向常量数据的指针有两个优点:

  • 避免由于无意间修改数据而导致的编程错误;
  • 使用 const 使得函数能够处理 const 和非 const 实参,否则将只能接受非 const 数据。

const 只能防止修改 pt 指向的值(这里为39),而不能防止修改 pt 的值。也就是说,可以将一个新地址赋给 pt

第二种使用 const 的方式使得无法修改指针的值:

这样的指针,我想将其称为:静止指针。指针本身定死在一个地址上了,但是地址里的内容是可以通过这个指针随便修改的。

通常,将指针作为函数参数来传递时,可以使用指向const的指针来保护数据。

在该声明中使用 const 意味着 show_array() 不能修改传递给它的数组中的值。只要只有一层间接关系,就可以使用这种技术。例如,这里的数组元素是基本类型,但如果它们是指针或指向指针的指针,则不能使用 const

7.4 函数和二维数组
#

数组名被视为其地 址,因此,相应的形参是一个指针,就像一维数组一样。

Data 是一个数组名,该数组有3个元素。第一个元素本身是一个数组,由4个 int 值组成。因此 data 的类型是指向由4个 int 组成的数组的指针,因此正确的原型如下:

1
int sum(int (*ar2)[4], int size);

还有另外一种格式,这种格式与上述原型的含义完全相同,但可读性更强:

1
int sum(int ar2[][4], int size);

对于二维数据,必须对指针 ar2 执行两次解除引用,才能得到数据。最简单的方法是使用方括号两次:ar2[r][c]。然而,如果不考虑难看的话,也可以使用运算符*两次:

1
ar2[r][c] == *(*(ar2+r)+c);   // 等效

7.5 函数和C-风格字符串
#

C-风格字符串由一系列字符组成,以空值字符(\0)结尾。

7.5.1 将C-风格字符串作为参数的函数
#

假设要将字符串作为参数传递给函数,则表示字符串的方式有三 种:

  • char 数组;
  • 用引号括起的字符串常量(也称字符串字面值);
  • 被设置为字符串的地址的char指针。

但上述3种选择的类型都是char指针(准确地说是char*),因此可 以将其作为字符串处理函数的参数。

可以说是将字符串作为参数来传递,但实际传递的是字符串第一个字符的地址。这意味着字符串函数原型应将其表示字符串的形参声明为 char * 类型。

C-风格字符串与常规 char 数组之间的一个重要区别是,字符串有内置的结束字符。这意味着不必将字符串长度作为参数传递给函数,而函数可以使用循环依次检查字符串中的每个字符,直到遇到结尾的空值字符为止。

不以空值字符结尾的char数组只是数组,而不是字符串。

7.5.2 返回C-风格字符串的函数
#

函数无法返回一 个字符串,但可以返回字符串的地址,这样做的效率更高。这种设计(让函数返回一个指针,该指针指向 new 分配的内存)的缺点是,程序员必须记住使用 delete

7.6 函数和结构体
#

结构体变量的行为更接近于基本的单值变量。也就是说,与数组不同,结构体将其数据组合成单个实体或数据对象,该实体被视为一个整体。前面讲过,可以将一个结构体赋给另外一个结构体。同样,也可以按值传递结构体,就像普通变量那样。在这种情况下,函数将使用原始结构体的副本。另外,函数也可以返回结构体。与数组名就是数组第一个元素的地址不同的是,结构体名只是结构体的名称,要获得结构的地址,必须使用地址运算符 &

使用结构编程时,最直接的方式是像处理基本类型那样来处理结构体;也就是说,将结构体作为参数传递,并在需要时将结构体用作返回值使用。

7.6.1 传递和返回结构体
#

当结构体比较小时,按值传递结构体最合理。

7.6.2 另一个处理结构的函数示例
#

7.6.3 传递结构体的地址
#

假设要传递结构的地址而不是整个结构以节省时间和空间,则需要 重新编写前面的函数,使用指向结构的指针。首先来看一看如何重新编写 show_polar() 函数。需要修改三个地方:

  • 调用函数时,将结构的地址(&pplace)而不是结构本身(pplace) 传递给它;
  • 将形参声明为指向 polar 的指针,即polar *类型。由于函数不应该修改结构,因此使用了 const 修饰符;
  • 由于形参是指针而不是结构,因此应间接成员运算符(->),而不是成员运算符(句点)。

7.7 函数和 string 对象
#

可以将对象作为完整的实体进行传递,如果需要多个字符串,可以声明一个string 对象数组,而不是二维 char 数组。

7.8 函数与 array 对象
#

7.9 递归
#

C++函数有一种有趣的特点——可 以调用自己,但不允许main()调用自己,这种功能称为递归。

我不喜欢递归,太耗资源,可读性还差。

7.9.1 包含一个递归调用的递归
#

7.9.2 包含多个递归调用的递归
#

7.10 函数指针
#

与数据项相似,函数也有地址。函数的地址是存储其机器语言代码 的内存的开始地址。对程序而言很有用,可以编写以另一个函数的地址作为参数的函数。

这样第一个函数就可以找到第二个函数,并运行它。与直接调用另一个函数相比,这种方法很笨拙,但它允许在不同的情况下传递不同函数的地址,这意味着可以在不同的时间使用不同的函数。

7.10.1 函数指针的基础知识
#

1.获取函数的地址 获取函数的地址很简单:只要使用函数名(后面不跟参数)即可。要将函数作为参数进行传递,必须传递函数名,要区分传递的是函数的地址还是函数的返回值。

2.声明函数指针

声明指向函数的指针时,必须指定指针指向的函数类型。意味着声明应指定函数的返回类型以及函数的特征标(参数列表)。也就是说,声明应像函数原型那样指出有关函数的信息:

3.使用指针来调用函数

为何pf和(*pf等价呢?一种学派认为,由于pf是函数指针,而 *pf 是函数,因此应将(*pf)()用作函数调用。另一种学派认为,由于函数名是指向该函数的指 针,指向函数的指针的行为应与函数名相似,因此应将pf( )用作函数调用使用。C++进行了折 衷——这2种方式都是正确的,或者至少是允许的,虽然它们在逻辑上是互相冲突的。在认为 这种折衷粗糙之前,应该想到,容忍逻辑上无法自圆其说的观点正是人类思维活动的特点。

可能看起来比较深奥,但指向函数指针数组的指针并不少见。实际上,类的虚方法实现通常都采用了这种技术(参见第13章)。

7.10.4 使用typedef进行简化
#

这里采用的方法是,将别名当做标识符进行声明,并在开头使用关键字 typedef。 使用typedef可减少输入量,让您编写代码时不容易犯错,并让程序 更容易理解。

7.11 总结
#

函数是C++的编程模块。要使用函数,必须提供定义和原型,并调 用该函数。函数定义是实现函数功能的代码;函数原型描述了函数的接 口:传递给函数的值的数目和种类以及函数的返回类型。函数调用使得 程序将参数传递给函数,并执行函数的代码。

C++提供了3种表示C-风格字符串的方法:字符数组、字符串常量 和字符串指针。它们的类型都是char*char指针),因此被作为char*类型参数传递给函数。C++使用空值字符 \0 来结束字符串,因此字符 串函数检测空值字符来确定字符串的结尾。

C++处理结构体的方式与基本类型完全相同,这意味着可以按值传递结构体,并将其用作函数返回类型。然而,如果结构体非常大,则传递结构体指针的效率将更高,同时函数能够使用原始数据。

《C++ Primer Plus》第六版 - 这篇文章属于一个选集。
§ 1: 本文