第8章 字符串:分析文本

计算机处理器只理解数字,它们如何与人沟通?答案是通过一种特殊的的编码为每个字母分配编号。这是理解文本字符串的基础,所以本章首先讨论该主题。

C++多年来都支持一个高级的string类来简化文本字符串处理。例如,以下代码连接两个字符串,不必关心字符串长度或容量,就是这么神奇!

 string titled_name = "Sir" + beatle_name

本章先介绍“老式” C 字符串类型。但如果想直接学习高级的,更容易使用的 string类,也不妨跳到 8.3 节。

8.1 计算机如何存储文本

对于文本数据,每个字节都是和特定字符对应的特殊代码,称为ACSCII码。假定声明以下字符串:
char str[] = “Hello!”;

C++ 将恰好分配7个字节,每个字符对应一个字节,另加上用于终止的空字节,这称为“标准C 字符串”,以便和高级(和更易使用的)的string 类区分。C 字符串是简单 char数组。下图是字符串数据在内存中的样子。
在这里插入图片描述 附录D列出了每个文本字符的ASCII码。计算机实际不存储文本字符,只存储对应的数值编码。那么,数值在什么时候,以什么方式转换成文本字符呢?

转换至少发生两次:通过键盘输入数据时,以及在显示器上显示时。例如,按键盘上的H键,底层会采取一系列行动将H的ASCII代码(72)读入程序,该值作为最终数据存储。

在其他时间,文本字符串不过是一系列数字,具体地说是一系列0到255之间的字节。但作为程序员,我们可认为C++将文本字符存储到内存中,每个字符对应一个字母。(例外:国际标准Unicode 每个字符使用多个字节)。

计算机如何翻译程序?

为了理解这个问题,首先要知道C++源代码存储在文本文件中,就像存储短文或备忘录。但如前所述,文本字符以数值形式存储。所以,当编译器处理这种数据时,它会用另一种形式来处理数字,对数据进行求值,并根据精确的逻辑规则来做出判断,如下图所示。
在这里插入图片描述
这正是计算机程序的本质,它是CPU看得懂的说明书。计算机程序相当于中介,是一系列指令和数据。计算机各种各样的“本事”从它的程序而来。程序使计算机能做各种事情,其中包括翻译包含C++代码的文本文件。
编译器是一种特殊程序,但所做的事情并未超纲。作为程序,它还是如前所述的一本“说明书”,作用是告诉计算机如何读取C++源代码文件并输出另一本“说明书”,也就是以可执行的形式保存的C++程序。
第一批编译器必须用机器码来写。以后就可以用旧的写新的。结果是在一系列“自举”过程之后,即使最老道的程序员也慢慢不用写机器码了。

获取正确的字符串

通过第6章对数组的学习,你或许已猜出了字符串的本质:基类型为char的数组。
从技术上说,char 是整数类型,1字节宽度,最多能存储256个值(范围从0 到 255),足以容纳所有标准字符,包括大写和小写字母,数字以及标点符号。(注意,包括中文和日文在内的一些语言远远不止256个字符,所以需要更宽的字符类型。)

可创建一个大小固定,但没有初始值的 char数组:
char str[10];
这就创建了一个最多能容纳10个字节,但尚未初始化的字符串。程序员更常见的做法是在声明字符串时初始化,例如:
char str[10] = “Hello!”;
该声明将创建 char 数组,并将数组起始地址和名称 str 关联(记住数组名称总是转换成起始地址,或者说第一个元素的地址)。下图只显示了字符而没有显示ASCII码。但在底层只会存储ASCII码。
在这里插入图片描述 字符串 \0 是 C++用于表示空(NULL)字符的记号方法,该字节实际存储的是值0(相比之下,为字符 ‘0’ 存储的是 ASCII码48).C++字符串以一个空字节终止,代表字符串数据在这里终止。

不明确指定大小,但又对字符串进行了初始化,C++会为字符串分配刚好能容纳数据的空间(含空终止字节)。
char s[] = “Hello!”;
char *p = “Hello!”;
如下图所示,两个语句的效果大致相同(区别在于 s 是数组名,是常量所以不可修改,而 p 是指针,可重新赋值来指向不同的地址)。C++在两种情况下都会在数据区域分配刚好大的空间,并将起始地址赋给名称 s (不可修改) 或 p 的初始值(可修改)。

字符串处理函数

就像提供数学函数来处理数字,C++也提供了函数来处理字符串。这些函数获取指针参数: 换言之,获取字符串的地址,但处理的是所指向的字符串数据。下表总结了较常用的字符串函数。

函数说明
strcpy(s1, s2)s2的内容拷贝到目标字符串s1
strcat(s1, s2)s2的内容连接到s1 末尾
strlen(s)返回字符串s的长度(不计空终止符)
strncpy(s1, s2, n)s2最多n个字符拷贝到 s1
strncat(s1, s2, n)s2最多n个字符连接到s1末尾

最常用的或许是 strcpy(string copy) 和 strcat(string concatenation) 函数。下面展示了用法:

cahr s[80];
strcpy(s, "One");
strcat(s, "Two");
strcat(s, "Three");
cout << s;

输出如下:

OneTwoThree

这个例子虽然十分简单,但仍然能从中看出一些要点。

  • 声明字符串变量s时, 必须留出足够大的空间来容纳最终字符串的所有字符。这一点很重要。C++不保证有足够空间来容纳所有必要的字符串数据;这是你的责任。
  • 虽然字符串没有初始化,但总共为它留出了80个字节。本例假定最终要存储80个字符(含空终止符)。
  • 字符串字面值 One, Two 和 Three 是实参。遇到代码中的字符串字面值,C++会为字符串分配空间,并返回数据的地址。所以,Two和Three被解释成地址实参。

如下图所示,语句strcat(s, “Two”):是像这样执行的:
在这里插入图片描述这些字符串函数存在风险。怎样保证第一个字符串足够大,除了能包含现有字符串数据,还能包含将来新增的?一个办法是使目标字符串尽可能大,分配你认为永远都不会用完的空间。更安全的办法是使用 strncpy 和 strncat 函数,它们最多只拷贝(或连接) n 个字符(含空终止符)。例如,以下操作符保证不会超过为 s1分配的内存。

char s1[20];
// ...
strncpy(s1, s2, 20);
strncat(s1, s3, 20 - strlen(s1));

例 8.1: 构造字符串

先研究一个简单的字符串操作:基于较小字符串构造一个较大的。以下程序从用户处获取两个字符串(通过调用稍后要讲到的 getline 函数),构造一个较大的并打印结果。

// buildstr.cpp
# include <iostream>
# include <cstring>
using namespace std;

int main()
{
	char str[600];
	char name[100];
	char addr[200];
	char work[200];

	// 从用户处获取三个字符串
	cout << "输入姓名并按Enter:";
	cin.getline(name, 100);
	cout << "输入地址并按Enter:";
	cin.getline(addr, 200);
	cout << "输入工作单位并按Enter:";
	cin.getline(work, 200);

	// 构造输出字符串并打印
	strcpy_s(str, "\n 我叫 ");
	strcat_s(str, name);
	strcat_s(str, ",住在 ");
	strcat_s(str, addr);
	strcat_s(str, ",\n 工作单位是 ");
	strcat_s(str, work);
	strcat_s(str, ".");
	cout << str << endl;
	return 0;
}

VS 目前将某些函数定义为不安全函数。要继续使用函数而不报错。有两种方案:
1 将 strcpy 和 strcat 改为 strcpy_s 和 strcat_s
2 在项目属性对话框中编辑预处理器定义,添加 _CRT_SECURE_NO_WARNINGS 这一行

练习

练习 8.1.1

strcat(str, addr);
替换为如下语句:
strncat(str, addr, 600 - strlen(str));
// 练习 8.1.1
# include <iostream>
# include <cstring>
using namespace std;

int main()
{
	char str[600];
	char name[100];
	char addr[200];
	char work[200];

	// 从用户处获取三个字符串
	cout << "输入姓名并按Enter:";
	cin.getline(name, 100);
	cout << "输入地址并按Enter:";
	cin.getline(addr, 200);
	cout << "输入工作单位并按Enter:";
	cin.getline(work, 200);

	// 构造输出字符串并打印
	strcpy_s(str, "\n 我叫 ");
	strcat_s(str, 600 - strlen(str), name);
	strcat_s(str, ",住在 ");
	strcat_s(str, 600 - strlen(str), addr);
	strcat_s(str, ",\n 工作单位是 ");
	strcat_s(str, 600 - strlen(str), work);
	strcat_s(str, ".");
	cout << str << endl;
	return 0;
}

练习 8.1.2

完成上个练习后,测试你为str 字符串添加的限制措施是否合格。最好将数字600 替换为符号常量 STRMAX,在程序开头添加以下 #define 指令。预处理期间,该指令会指示编译器将 STRMAX 在源代码中的所有实例都替换成指定文本(600).
#define STRMAX 600
然后,可用 STRMAX 来声明 str 的长度:
char str[STRMAX];
再用 STRMAX 确定最多拷贝多少字节:
strncpy(str, "\n My name is ", STRMAX);
strncat(str, name, STRMAX - strlen(str));
该设计最大的好处在于,如需要更改最大字符串长度,只需要更改一行代码(包含 #define 指令的那一行),然后重新编译一下。

// 练习 8.1.2
# include <iostream>
# include <cstring>

# define STRMAX 600
using namespace std;

int main()
{
	char str[STRMAX];
	char name[100];
	char addr[200];
	char work[200];

	// 从用户处获取三个字符串
	cout << "输入姓名并按Enter:";
	cin.getline(name, 100);
	cout << "输入地址并按Enter:";
	cin.getline(addr, 200);
	cout << "输入工作单位并按Enter:";
	cin.getline(work, 200);

	// 构造输出字符串并打印
	strcpy_s(str, STRMAX, "\n 我叫 ");
	strcat_s(str, STRMAX - strlen(str), name);
	strcat_s(str, STRMAX, ",住在 ");
	strcat_s(str, STRMAX - strlen(str), addr);
	strcat_s(str, STRMAX, ",\n 工作单位是 ");
	strcat_s(str, STRMAX - strlen(str), work);
	strcat_s(str, ".");
	cout << str << endl;
	return 0;
}

转义序列

转义序列可能造成一些奇怪的该骂,例如以下语句:
cout << “\n and I live at”;
相当于:
cout << endl << “and I live at”;
为了理解 \n and 这种奇怪的字符串,关键在于记住以下语言规范。

编译器遇到 C++源代码中的反斜杠(\)时,紧接着它的下一个字符会被解释成具有特殊含义。

除了代表换行符的\n,其他转义序列还有\t(制表符)和\b(退格符)。喜欢刨根问底的人会问:“怎样打印一个实际的反斜杠?”答案很简单。连续两个反斜杠(\)代表一个反斜杠。例如以下语句:
cout << “\n and I live at”;
会打印如下输出:
\n and I live at
第17章 在介绍C++14新特性时,会解释如何创建“原始字符串字面值”使反斜杠(\)不在转义。

读取字符串输入

从键盘输入的所有数据最初都是文本,即ASCII码。所以,当用户在键盘上按“1”和“5”时,发生的第一件事情是这些字符进入下图所示的输入流。
在这里插入图片描述
计算机指示cin 对象获取一个文本输入,分析并生成整数值:本例是值15.该数字在如下所示的语句中赋给整数变量:

cin >> n;

如 n 的类型不同(比如 double 类型),就会进行不同的转换。浮点格式要求生成一种不同类型的值。通常,由 cin 对象解释的流输入操作符(>>)能帮你完成所有这些操作。

上一节提到了 getline 方法,它采用了一种奇怪的的语法:

cin.getline(name, 100);

原点操作符( . )是必须的,它表明getline 是 cin 对象的成员。显然,这里出现了一些你可能还不理解的新术语。

第10章将完整讲述对象。目前请将对象想象成一种数据结构,内部集成了如何做特定事情的机制。指示对象做某事只需调用它的成员函数:

对象.函数(实参)

对象是函数从属于的东西,本例是 cin。本例的函数时 getline(第9章介绍的文件输入对象也支持该函数)。除了调用 cin.getline,还可用流操作符 >> 获取输入:

cin >> var;

以前曾用这种语句获取 int 和 double 数据。能不能用于字符串?答案是肯定的。

cin >> name;

该语句的问题在于结果可能和你预期的不符。它并非获取整行输入(从用户开始输入数据开始,一直到Enter键),而是最多获取第一个空白字符(空格、制表符或换行符)之前的数据。所以,假定用键盘输入下面一行:

Niles Cavendish

执行 cin >> name; 会将“Niles”移动到字符串变量 name 中。“Cavendish” 则会留在输入流,直到由下一个输入操作获取。

假定用户输入以下内容,并按Enter键:

50 3.141592 Joe Bloe

如果意图是连续读取两个数字和两个字符串,且所有数字和字符串都以一个空格分隔,那么可用以下语句成功读取输入:

cin >> n >> pi >> first_name >> last_name;

但流输入操作符通常会造成你缺乏有效的控制。我自己的选择是尽量避免用它,除非是一些简单的测试程序。该操作符的一个局限在于不允许设置默认值。假定你提示用户输入一个数字:

cout << "输入数字";
cin >> n

如果用户直接按Enter键,不输入任何东西,那么什么事情都不会发生。计算机会静悄悄地等待用户输入数字并再次按Enter键。如用户持续按Enter键,程序会一直等下去,像一个固执的孩子。

就个人来说,我比较喜欢让程序支持以下提示所指定的行为:
输入数字(或按Enter输入0):
显然,让 0(或你选择的其他数字)作为默认值会方便许多。但怎么实现这种行为呢?下例对此进行了演示。

使用 getline 函数后,再用流输入操作符(>>)可能出现异常行为。这是由于 getline 函数和流输入操作符对于如何“消耗”换行符进行了不同的设定。所以,在程序中最好坚持只用其中一种方法。

例 8.2:获取数字

以下程序获取数字并打印其平方根,直到用户输入0 或者在提示后直接按 Enter 键。

// get_num.cpp
# include <iostream>
# include <cstring>
# include <cmath>
# include <cstdlib>
using namespace std;
double get_number();

int main()
{
	double x = 0.0;
	while (true)
	{
		cout << "输入一个数(直接按 Enter 退出):";
		x = get_number();
		if (x == 0.0)
		{
			break;
		}
		cout << "x的平方根是:" << sqrt(x);
		cout << endl;
	}
	return 0;
}

// get-number 函数
// 获取用户输入的数,只获取输入的第一个数。
// 如用户按 Enter而不是输入, 返回默认值 0.0
double get_number()
{
	char s[100];
	cin.getline(s, 100);
	if (strlen(s) == 0)
	{
		return 0.0;
	}
	return atof(s);
}

atof 函数 获取字符串输入并生成浮点(double)值。对应地,atoi 函数生成 int 值。
return atof(s); // 返回浮点数值
return atoi(s); // 返回 int 值
使用 atof 函数需要包含< cstdlib >

练习

练习 8.2.1

重写 例 8.2 只接受整数输入。提示:将所有受影响的类型从 double 改成 int 格式,包括常量。

// 例 8.2.1
# include <iostream>
# include <cstring>
# include <cmath>
# include <cstdlib>
using namespace std;

int get_number();

int main()
{
	int n = 0;

	while (true)
	{
		cout << "输入一个数(直接按Enter 表示 退出):";
		n = get_number();
		if (n == 0)
		{
			break;
		}
		cout << "它的平方根是:" << sqrt(n);
		cout << endl;
	}
	return 0;
}

// 只接受整数输入
int get_number()
{
	char s[100];

	cin.getline(s, 100);
	if (strlen(s) == 0)
	{
		return 0;
	}
	return atoi(s);
}

例 8.3:转换成大写

本例展示一个访问单独字符的简单程序。虽然可将字符串视为单一实体,但实际由一系列字符构成,通常(但并非一定)是大写和小写字母。

// upper.cpp
# include <iostream>
# include <cstring>
# include <cctype>
using namespace std;
void convert_to_upper(char *s);

int main()
{
	char s[100];
	cout << "输入转换成大写的字符串并按Enter:";
	cin.getline(s, 100);
	convert_to_upper(s);
	cout << "转换后的字符串是:" << endl;
	cout << s << endl;
	return 0;
}

void convert_to_upper(char *s)
{
	int length = strlen(s);
	for (int i = 0; i < length; i++)
	{
		s[i] = toupper(s[i]);
	}
}
函数说明
toupper( c )c是小写就返回大写;否则原样返回c
tolower( c )c是大写就返回小写;否则原样返回c

使用这些函数时,需要包含 < cctype >

练习

练习 8.3.1

写一个和例8.3 相似的程序,但将输入的字符串转换成全部小写。提示:使用C++库中的 tolower 函数。

// 练习 8.3.1
# include <iostream>
# include <cstring>
# include <cctype>
using namespace std;
void convert_to_lower(char *s);

int main()
{
	char s[100];
	cout << "输入转换成大写的字符串并按Enter:";
	cin.getline(s, 100);
	convert_to_lower(s);
	cout << "转换后的字符串是:" << endl;
	cout << s << endl;
	return 0;
}

void convert_to_lower(char *s)
{
	int length = strlen(s);
	for (int i = 0; i < length; i++)
	{
		s[i] = tolower(s[i]);
	}
}

练习 8.3.2

重写例 8.3 来使用直接指针引用(参见第6章末尾的说明),而不是使用数组索引。如抵达字符串尾,当前字符的值就是一个空终止符,所以可用 *p = ‘\0’ 测试是否抵达字符串尾。更简单的写法是直接拿 *p 作为条件,不指向零(NULL)值就非零。
while(*p++)
{
// 做一些事。。。
}

// 练习 8.3.2
# include <iostream>
# include <cstring>
# include <cctype>
using namespace std;
void convert_to_upper(char *s);

int main()
{
	char s[100];
	cout << "输入转换成大写的字符串并按Enter:";
	cin.getline(s, 100);
	convert_to_upper(s);
	cout << "转换后的字符串是:" << endl;
	cout << s << endl;
	return 0;
}

void convert_to_upper(char *s)
{
	for (char *p = s; *p; ++p)
	{
		*p = toupper(*p);
	}
}

8.2 单字符和字符串

C++区分单字符和字符串,基于使用单引号还是双引号。
表达式’A’ 是单字符。编译时C++将该表达式替换成字母A的ASCII值,即十进制65.
而表达式"A" 是长度为1 的字符串。C++遇到该表达式时会在数据区域放两个字节。

  • 字母A的ASCII码(和使用单引号时一样)。
  • 一个空终止字节

然后,C++将表达式“A”替换在成该字节数组的地址。‘A’ 和 “A”不同之处在于前者转换成整数值,后者是字符串所以要转换成地址。

这可能需要时间来消化,目前只需要特别注意引号的使用。以下代码演示了如何混用两种引号:
char s[] = “A”;
if (s[0] == ‘A’)
{
cout << “字符串第一个字母是’ A’.”;
}

这会获得正确的结果。但像下面这样比较字符和地址会出错:
if (s[0] == “A”)
{
// 错误!
// …
}
它试图将字符串数组s 的一个元素和一个地址表达式(“A”)进行比较,所以非法。请记住以下语言规范。

单引号表达式(比如 ‘A’ )在转换成ASCII码后被视为数值,不是数组

双引号表达式(比如“A”)是字符数组,所以会转换成地址。

例 8.4:用strtok分解输入

读取一行文本时(例如使用getline 函数),经常需要将其分解成更小的字符串。例如以下文本输入:
Me, myself, and I
要把它分解成逗号和空格(定界符)分隔的的多个字符,再用单独的行打印:
Me
Myself
and
I
笨办法是手动查找定界符来检索子串,但更聪明的办法是使用C++标准库的strtok(全称是 string token)函数。这里的 token 是指包含单个词的子串。如下表所示,该函数由两种用法。

函数用法说明
strtok(source_string, delims)根据由delims 指定的定界符返回源字符串的第一个token
strtok(nullptr, delims)使用之前的strtok 调用所指定的源字符串,获取下一个token。使用 delims指定的定界符

visual studio 目前将strtok 函数定义为不安全函数。要想继续使用该函数而不报错,方案是在项目属性对话框中编辑预处理定义,添加 _CRT_SECURE_NO_WARNINGS 这一行。

strtok 首次调用需要指定源字符串和定界符,返回指向第一个子串(即 token)的指针。例如:
p = strtok(the_string, “,”);
要找下一个 token 就再次调用strtok,为第一个实参指定空值。函数自己记得操作的哪个字符串和在字符串中的什么位置:
p = strtok(nullptr, “,”);
如果再次指定 source_string, strtok 会重新开始并返回第一个token。
strtok 通常返回指向 token 的指针,没有更多 token(子字符串)则返回空值,可以测试是否等于零或false。

较老的编译器可能需要将nullptr 替换成NULL。该关键字自C++11引入

下面这个简单的程序将空格和逗号解释成定界符(分隔符),用单独的行打印每个子字符串(token)。

// tokenize.cpp
# define _CRT_SECURE_NO_WARNINGS
# include <iostream>
# include <cstring>

using namespace std;
int main()
{
	char the_string[81], *p;

	cout << "输入要分解的字符串:";
	cin.getline(the_string, 81);
	p = strtok(the_string, ",");
	while (p != nullptr)
	{
		cout << p << endl;
		p = strtok(nullptr, ",");
	}
	return 0;
}

练习

练习 8.4.1

修改例子,除了打印token(子字符串),最后还打印找到了多少个token。

// 练习 8.4.4
# define _CRT_SECURE_NO_WARNINGS
# include <iostream>
# include <cstring>

using namespace std;

int main()
{
	int n = 0;
	char the_string[81], *p;
	
	cout << "输入要分解的字符串:";
	cin.getline(the_string, 81);
	p = strtok(the_string, ",");
	while (p != nullptr)
	{
		++n;
		p = strtok(nullptr, ",");
	}
	cout << "token 的数目是:" << n << endl;
	return 0;
}

练习 8.4.2

用&连接所有 token 并打印。

// 练习 8.4.2
# define _CRT_SECURE_NO_WARNINGS
# include <iostream>
# include <cstring>

using namespace std;

int main()
{
	char the_string[81], *p;

	cout << "输入要分解的字符串:";
	cin.getline(the_string, 81);
	p = strtok(the_string, ",");
	while (p != nullptr)
	{
		cout << p << "&";
		p = strtok(nullptr, ",");
	}
	cout << endl;
	return 0;
}

练习 8.4.3

用&作为定界符。

// 练习 8.4.3
# define _CRT_SECURE_NO_WARNINGS
# include <iostream>
# include <cstring>

using namespace std;

int main()
{
	char the_string[81], *p;

	cout << "输入要分解的字符串:";
	cin.getline(the_string, 81);
	p = strtok(the_string, "&, ");
	while (p != nullptr)
	{
		cout << p << endl;
		p = strtok(nullptr, "&, ");
	}
	return 0;
}

8.3 C++语言的string类

string 类型是类的一个例子,单独的字符串和 cin/cout 一样是对象。例如,假定有两个字符串,分别称为 first_name 和 last_name:

# include <string>
using namespace std;
...
string first_name("Abe ");
string last_name("Lincoln");

不用担心数组或字符的索引,现在把这些对象当作普通数据来操作即可,如下图所示。
在这里插入图片描述
例如,可以用加号(+)连接字符串,不必担心长度或容量问题。
string full_name = first_name + last_name;
如下图所示,该语句连接两个字符串,构成一个新的 full_name 字符串。它自动具有正确的长度。不用关心新字符串是否有足够空间来容纳连接起的姓名。string 类负责所有存储问题。
在这里插入图片描述
索引单独字符也没问题,具体和 C 字符串一样,注意,字符同样是 char 类型。

string s = "I am what I am.";
cout << s[3] ; // 打印第4个字符(m)。

string 类具有 C 字符串类型的几乎一切优点,还更易使用。缺点是不兼容 strtok 函数,后者只支持 C 字符串。

添加对 string 类的支持

使用新的 string 类型需做的第一件事情就是用 # include <string> 指令开启对它的支持,这有别于开启 C 字符串支持的指令:

# include <string> // 支持新的 string类

记住支持 C 字符串是用 cstring 而不是 string:

# include <cstring> // 支持旧式字符串函数

增减一个 C 就大不一样。顺便说一下, 可同时开启对两者的支持。但只有在需要调用 strcpy 这样的旧式函数时才需要包含 cstring。

和 cin 和 cout 一样, string 这个名称必须用 std 前缀来限定,除非在程序开头添加以下 using 语句:

using namespace std;

不添加上述语句,每次都要用 std::string 引用新的字符串类。添加 using namespace 语句后,C++库的任何东西都不用添加 std::前缀了。

声明和初始化 string 类的变量

一旦开启对 string 类的支持,就可以非常简单地用它来声明变量。(再次声明,如果没有写 using namespace 语句,就要用 std::string 而不是 string。)

string a, b, c;

这就创建了 string 类的三个变量。注意这有多简单,不需要担心它们需要多大空间。可采取多种方式初始化字符串,例如:

string a("Here is a string."), b("Here's another.");

还可使用赋值操作符(=):

string a, b;
a = "Here is a string.";
b = "Here's another.";

还可将声明和初始化合二为一:

string a = "Here is a string.";

操作string 类的变量

标准库 string 类的操作方式更符合习惯。和C 语言的字符串不同,string 对象不需要调用库函数就能拷贝和比较。

例如,假定有以下字符串变量:

string cat = "Persian";
string dog = "Dane";

可以将新数据赋给这些变量而不必担心容量。例如,原本容纳了4个字符的 dog 字符串能“自动”扩容来容纳7个字符:

dog = "Persian";

用相等性测试操作符(==)比较两个字符串的内容。这符合习惯:内容一样就返回 true(比较 c 字符串则需调用 strcmp)。

if (cat == dog)
{
	cout << "cat 和 dog 同名";
}

用赋值操作符(=)将一个 string 变量的数据拷贝给另一个。这也符合习惯:拷贝字符串内容而不是指针值。

string country = dog;

用加号(+)连接字符串:

string new_str = a + b;

甚至能在这种操作中嵌入字符串字面值:

string str = a + " " + b;

但以下语句无法成功编译:

string str = "The dog" + " is my friend";  	// 错误!

问题在于,虽然加号(+)能连接两个 string 变量,或连接一个string 变量和一个 C 字符串, 但不能连接两个C语言的字符串(字符串字面值仍是C语言的字符串)。

附加“s” 后缀使两个字符串字面值成为C++字符串类的真正实例可解决该问题。具体在第17章解释。另一个方案使将加号替换成空格或换行符。

输入和输出

和你预期的一样,string类型的变量能像你预期的那样和 cin/cout 配合使用:

string prompt = "输入姓名:";
string name;
cout << prompt;
cin >> name;

使用流输入操作符 >> 存在和 C 字符串一样的缺点:只能返回第一个空白字符之前的字符。但可用 getline 函数将整行输入都放到一个 string 变量中。该版本不要求指定读入的最大字符数,因字符串变量能存储任意大小的数据。

getline(cin, name);

例8.5:用 string 类构造字符串

本例用 string 变量实现例 8.1的功能。

// buildstr2.cpp
# include <iostream>
# include <string> // 包含对string 类的支持。
using namespace std;

int main()
{
	string str, name, addr, work;

	// 从用户获取三个字符串
	cout << "输入姓名并按Enter:";
	getline(cin, name);
	cout << "输入住址并按Enter:";
	getline(cin, addr);
	cout << "输入工作单位并按Enter:";
	getline(cin, work);

	// 构造输出字符串并打印
	str = "\n 我叫" + name + ", " + "住在 " + addr + ",\n 工作单位是 " + work + ".\n";
	cout << str << endl;
	return 0;
}

练习

练习 8.5.1

从用户处收集三项信息:一只狗的名字、品种和年龄。打印一句话来合并信息。

答案:

# include <iostream>
# include <string>
using namespace std;

int main()
{
	string str, name, breed, age;

	cout << "输入狗的名字并按Enter:";
	getline(cin, name);
	cout << "输入狗的品种并按Enter:";
	getline(cin, breed);
	cout << "输入狗的年龄并按Enter:";
	getline(cin, age);

	str = "\n 狗的名字叫 " + name + ", " + "品种是 " + breed + ", 今年 " + age + " 岁了。";
	cout << str << endl;

	return 0;
}

练习 8.5.2

不用一句话,而是在一个段落中用多个句子来多次使用上个练习收集到的信息。

答案:

# include <iostream>
# include <string>

using namespace std;

int main()
{
	string str1, str2, str3;
	string name, breed, age;

	cout << "输入狗的名字并按Enter:";
	getline(cin, name);
	cout << "输入品种并按Enter:";
	getline(cin, breed);
	cout << "输入狗的年龄并按Enter:";
	getline(cin, age);

	str1 = "那里有一只叫 " + name + " 的狗, ";
	str1 = str1 + name + " 是一只很乖的狗, " + age + " 岁了。";
	str2 = name + " 的品种是 " + breed + "。";
	str3 = "一天飓风来袭,但是 " + name;
	str3 = str3 + " 拯救了整个村庄。";

	cout << str1 << endl << str2 << endl << str3 << endl;

	return 0;
}

例 8.6:加法机二号

字符串(无论C 字符还是 string 类)允许一次获取整行输入,并智能地对其进行处理。本例配合使用 getline 函数和指针来实现第3章 加法机程序的一个更好的版本。

原来的版本要求使用数字0来终止输入序列,这造成了显而易见的问题。这个改进的版本还是每次接收一个数,直到用户什么都不输入,直接按 Enter 终止输出。两种字符串都可以用: 传统C 字符串(空终止 char 数组)或者 STL string 类的实例。但两种都用过之后,你恐怕会同意后者更好用。

# include <iostream>
# include <string>
using namespace std;

bool get_next_num(int *p);

int main()
{
	int sum = 0;
	int n = 0;
	while (get_next_num(&n))
	{
		sum += n;
	}
	cout << "总和是:" << sum << endl;
	return 0;
}

bool get_next_num(int *p)
{
	string input_line;
	cout << "输入一个数(直接按Enter退出):";
	getline(cin, input_line);
	if (input_line.size() == 0)
	{
		return false;
	}
	*p = stoi(input_line);
	return true;
}

练习

练习 8.6.1

修改 get_next_num 函数,通过一个实参来指定默认值。如用户直接按Enter 而不输入任何文本,函数就返回默认值。

答案:

# include <iostream>
# include <string>
using namespace std;

bool get_next_num(int *p, int def_val);

int main()
{
	int sum = 0;
	int n = 0;
	while (get_next_num(&n, 0))
	{
		sum += n;
	}
	cout << "总和是:" << sum << endl;
	return 0;
}

bool get_next_num(int *p, int def_val)
{
	string input_line;
	cout << "输入一个数(直接按Enter退出):";
	getline(cin, input_line);
	if (input_line.size() == 0)
	{
		*p = def_val;
		return false;
	}
	*p = stoi(input_line);
	return true;
}

练习8.6.2

修改例子,接收浮点数并打印浮点结果。记住,C++支持 stof 和 atof 函数。

答案:

# include <iostream>
# include <string>
using namespace std;

bool get_next_num(double *p);

int main()
{
	double sum = 0.0;
	double x = 0.0;

	while (get_next_num(&x))
	{
		sum += x;
	}
	cout << "总和是:" << sum << endl;
	return 0;
}

bool get_next_num(double *p)
{
	string input_line;
	cout << "输入一个数(按Enter退出):";
	getline(cin, input_line);
	if (input_line.size() == 0)
	{
		return false;
	}
	*p = stof(input_line);
	return true;
}

对 string 类型的其他操作

可用和访问 C 字符中的字符一样的语法来访问 string 对象中的字符:

string[index]

例如,以下代码打印字符串中的字符,每个字符一行:

# include <string>
using namespace std;
//...
string dog = "Mac";
for (int i = 0; i < dog.size(); i++)
{
cout << dog[i] << endl;
}

运行时,上述代码会输出以下结果:
M
a
c
和C语言字符串以及C语言的任何数组一样,string 变量使用基于零的索引。所以 i 初始化为0.循环条件取决于字符串长度。C语言字符串用strlen 函数获取长度。 string 对象则用 size 成员函数。

int length = dog.size();

小结

  • 文本字符按它们的ASCII代码存储在计算机中。例如,字符串“Hello!” 表示成字节值 71, 101, 108, 108, 111, 33 和 0(空终止符)。
  • 传统 C 字符串使用空终止符(字节值0)使字符串处理函数判断字符串在什么地方终止。声明字符串字面值(比如“Hello!”)是,C++自动为该空终止符分配空间。
  • 字符串的当前长度(通过搜索空终止符来判断)并不等于为字符串保留的总存储空间大小。以下声明为str 保留 10 字节存储空间,但会初始化它,使它的当前长度只为6.所以,该字符串最终还有三个未使用的字节(空终止符占了一个字节),使其能根据需要进行扩展。
    char str[10] = “Hello!”;
  • strcpy(string copy) 和 strcat(string concatenation)等库函数可能改变现有字符串的长度。执行这些操作时,必须保证字符串保留足够打的空间以适应新的字符串长度。
  • strlen 获取字符串当前长度。
  • 包含 cstring 提供字符串处理函数所需的类型信息。
    #include <cstring>
  • 增大字符串的大小,但没有保留足够大的空间,可能覆盖另一个变量的数据区域,造成不容易发现的bug。
    char str[] = “Hello!”:;
    strcat(str, " So happy to see you."); // 错误!
  • 使用 strncat 和 strncpy 函数确保不会将过量字符拷贝到字符串。
    char str[100];
    strncpy(str, s2, 100);
    strncat(str, s2, 100 - strlen(str));
  • 流操作符(>>) 和 cin 对象配合使用,只能对输入进行有限的控制。用它将数据发送到一个字符串地址时,最多只能获取第一个空白字符(空格、制表符或换行符)之前的字符。
  • 为了获取整行输入,可以使用 cin.getline 成员函数(方法)。第二个实参指定要拷贝到字符串的最大字符数(不计空终止符)。
    cin.getline(input_string, max);
  • 像’A’ 这样的表达式代表单个整数值(转换成 ASCII 代码后);像 “A” 这样的表达式代表一个 char 数组,所以会转换成内存地址。
  • STL string 类允许创建、拷贝(=)、测试相等性(==)和 连接(+)字符串而不必担心大小问题。
  • 使用 string 类需包含<string>。记住,全名是 std::string,但std 前缀可用 using namespace 语句移除。
    #include <string>
    using namespace std;
  • 可像 C 字符串那样索引 string 对象来获取单独的字符(char 值)。
    char c = str_obj[2]; // c = 第三个字符
  • 调用 getline 函数将整行输入读入 string 对象(这是个更灵活的操作,因为不需要指定最大字符数)。这是全局函数而非成员。
    getline(cin, str_obj); // str_obj 获取整行输入
  • C++库提供函数 stoi 和 stof 将 string 对象转换成数值。还提供函数 atoi 和 atof 将 char*(C字符串)转换成数值。分别转换成整数和浮点(double)值。
  • 调用 string 对象的 c_str 方法将 string 对象转换成 C 字符串。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值