5.12 字符与字符串处理
本节要介绍一些字符串处理的标准库函数。这里介绍的技术适用于开发文本编辑器、字处理器、桌面排版软件、计算机化打字系统和其他文本处理软件。我们这里使用基于指针的字符串,本书稍后还将介绍把字符串作为成熟的对象。
5.12.1 字符与字符串基础
字符是 C++ 编程语言的基本组件。每个程序都是由一系列字符用有意义的方式组合而成的,计算机将其解释为一系列指令,用来完成一组任务。程序可能包含字符常量(character constant)。字符常量是表示为单引号字符的整数值。字符常量的值是机器字符集中该字符的整数值。例如 z
表示z的整数值(在 ASCII 字符集中为 122), '\n'
表示换行符的整数值(在 ASCII 字符集中为 10)。
字符串就是把—系列字符当作一个单元处理。字符串可能包含字母、数字和 +、-、*、/、$ 等各种特殊字符(special character)。C++ 中的字符串直接量(string literal)或字符串常量(string constant)放在双引号中如下所示:
”John Q.Doe” (姓名)
”9999 Nain Street” (街道)
”Waltham,Massachusetts” (州)
”(201)555—1212” (电话号码)
C++ 中的字符串是以 null 终止符 '\0'
结尾的字符数组。通过字符串中第一个字符的指针来访问字符串。字符串的值是字符串中第一个字符的地址(常量),这样,c++中可以说字符串是个常量指针,是指向字符串中第一个字符的指针。从这个意义上说,字符串像数组一样,因为数组名也是第一个元素的(常量)指针。
可以在声明中将字符串指定为字符数组或char*类型的变量。下列声明:
char color[] = "blue";
char *ColorPtr = "blue";
分别将变量初始化为 blue。第一个声明生成5个元素的数组 color,包含字符 'b'、'l'、'u'、'e' 和 'w'。第二个声明生成指针变量 colorPtr,指向内存中的字符串 blue。
可移植性提示5. 5
用字符串直接量初始化 char*
类型的变量时,有些编译器将字符串放在内存中无法修改字符串的位置。如果要修改字符串直接量,则应将其存放在字符数组中,以便在所有系统中修改。
声明 char color[]={"blue"};
也可以改写成:
char color[] = {'b','l','u','e','\0'};
声明包含字符串的字符数组时,数组应足够大,能存放字符串及其null终止符。上述声明自动根据初始化值列表中提供的初始化值的个数确定数组长度。
常见编程错误 5.15
字符数组中没有分配能存放字符串及其null终止符的足够空间。
常见编程错误 5.16
生成或使用不包含null终止符的字符串。
编程技技巧 5.5
在字符数组中存放字符串时,一定要保证能存放要存的最长字符串。C++允许存放任意长度的字符串。如果字符串长度超过字符数组长度,则越界字符会改写数组后面的内存地址中存放的数据。
字符串可以用cin通过流读取赋给数组。例如,下列语句将字符串赋给字符数组 word[20]
:
cin >> word;
用户输入的字符串存放在 word 中。上述语句读取字符,直到遇到空格、制表符、换行符或文件结束符。注意,字符串不能超过19个字符,因为还要留下 null 终止符的空间。第2章介绍的setw流操纵算子可以用于保证读取到 word 的字符串不超过字符数组长度。例如,下列语句:
cin >> setw( 20 ) >> word;
指定 cin 最多读取19个字符到数组 word 中,并将数组第20个位置用于保存字符串的 null 终止符。setw 流操作算子只能用于输入的下一个值。
有时,可以将整行文本输入数组中。为此,C++ 提供了 cin.getline 函数。cin.getline 函数取三个参数:存放该行文本的字符数组、长度和分隔符。例如,下列程序段:
char sentence[80];
cin.getline(sentence,80,'\n');
声明 80 个字符的数组 sentence,然后从键盘读取一行文本到该数组中。函数遇到分隔符 '\n'
、输入文件结束符或读取的字符数比第二个参数的长度少1时停止读取字符(最后一个字符位置用于保存字符串的 null 终止符)。如果遇到分隔符,则读取该分隔符并不再读入下一字符。cin.getline 函数的第三个参数默认值为 '\n'
,因此上述函数调用也可以改写成:
cin.getline(sentence,80);
第11章 C++ 输入/输出流 中将会详细介绍 cin.getline 和其他函数。
常见编程错误 5.17
将单个字符作为字符串处理可能导致致命的运行时错误。字符串是指针,可以对应一个大整数。而单个字符是个小整数(0--255 的 ASCII 值)在许多系统中.这会导致错误,因为低内存地址是用于特殊用途的,如操作系统中断处理器,因此会发生 访问无效 的错误。
常见编程错误 5.18
需要字符串参数时将字符传入函数可能导致致命的运行时错误。
常见编程错误 5.19
需要字符参数时将字符串传入函数是个语法错误。
5.12.2 字符串处理库的字符串操作函数
字符串处理库提供许多操作字符串数据、比较字符串、搜索字符串中的字符与其他字符串、将字符串标记化(将字符串分成各个逻辑组件)和确定字符串长度的字符串操作函数。本节介绍字符串处理库(标准库)中常用的字符串操作函数。图 5.29 总结了这些函数。
注意图 5.29 中的几个函数包含 size_t 数据类型的参数。这是在头文件 <stddef.h> 标准库中的头文件,标准库中还有许多其他标准库头文件,包括 <string.h> 中定义为 unsigned int 或 unsigned long 之类的无符号整数类型。
常见编程错误 5. 20
使用字符串处理库中的函数而不包括 <string.h> 头文件。
函数 stcpy 将第二个参数(字符串)复制到第一个参数(字符数组)中,这个字符数组的长度应当足以放下字符串及其 null 终止符。函数 strncpy 与 strcpy 相似,只是 strncpy 指定从字符串复制到字符数组的字符数。注意函数 strncpy 不一定复制第二个参数的 null 终止符,null 终止符要在复制的字符数比字符中长度至少多1时才复制。例如,如果第二个参数为 test,则只在 strncpy 的第三个参数至少为 5(test 的长度加 null 终止符)时才复制 null 终止符。如果第三个参数大于 5,则数组后面添加 null 终止符,直到写入第三个参数指定的总字符数。
函数原型 | 函数说明 |
---|---|
char *strcpy(char *s1,const char *s2) | 将字符串 s2 复制到字符数组 s1 中、返回 s1 的值 |
char *strncpy(char *s1,char *s2,size_t n) | 将字符串 s2 中最多n个字符复制到字符数组s1中,返回 s1 的值 |
char *strcat(char *s1,const char *s2) | 将字符串 s2 添加到字符串 s1 后面。s2 的第一个字符重定义 s1 的 null 终止符。返 回s1的值 |
char *strncat(char *s1,const char *s2,size_t n) | 将字符串 s2 中最多n个字符添加到字符串 s1 后面。s2 的第一个字符重定义 s1 的 null 终止符。返回s1的值 |
int strcmp(const char *s1,const char *s2) | 比较字符串 s1 与字符串 s2 函数在s1等于、小于或大于 s2 时分别返回 0、小于 0 或大于 0 的值 |
int strncmp(const char *s1,const char *s2,size_t n) | 比较字符串s1中的n个字符与字符串 s2。函数在s1等于、小于或大于s2时分别返回0、小于 0 或大于 0 的值 |
char *strtok(char *s1,const char *s2) | 用一系列 strtok 调用将 s1 字符串标记化(将字符串分成各个逻辑组件,如同一行文本中的每个单词),用字符串 s2 所包含的字符分隔。首次调用时包含 s1 为第一个参数,后面调用继续标记化同一字符串,包含 NULL 为第一个参数。每次调用时返回当前标记的指针。如果函数调用时不再有更多标记,则返回 NULL |
size_t strlen(const char *s) | 确定字符串的长度,返回 null 终止符之前的字符数 |
图 5.29 字符串处理库的字符串操作函数
常见错误 5.21
第三个参数小于或等于第二个参数的宇符串长度时不在strncpy的第一个参数中添加null终止料可能造成严重的运行时错误。
图 5.30 的程序用 strcpy 将数组x中的整个字符串复制到数组y中,并用 strncpy 将数组x的前 14 个字符复制到数组2中。将 null 字符 '\0'
添加到数组 z,因为程序中调用 strncpy 时没有写入 null 终止符(第三个参数小于或等于第二个参数的字符串长度)。
1 // Fig. 5.30:fig05 30.cpp
2 // using strcpy and strncpy
3 #include <iostream.h>
4 #include <string.h>
5
6 int main()
7 {
8 char x[] = "Happy Birthday to You";
9 char x[ 25 ], Z[ 15 ];
10
11 cout << "The string in array x is: "<< x
12 << "\nThe string in array y is: "<< strcpy( y, x )
13 << '\n';
14 strncpy( z, x, 14 ); // does not copy null character
15 z[ 14 ] = '\0';
16 cout << "The string in array z is: " << z << endl;
17
18 return 0;
19 }
输出结果:
The string in array x is: Happy Birthday to You
The stringin array y is: Happy Birthday to You
The string in array z is: Happy Birthday
图 5.30 使用 stcpy 和 strncpy 函数
函数 strcat 将第二个参数(字符串)添加到第一个参数(字符数组)中。第二个参数的第一个字符代替终止第一个参数中字符串的 null 终止符 '\0'
。程序员要保证存放第一个字符串的数组应足以存放第一个字符串、第二个字符串和 null 终止符(从第二个字符串复制)的合并长度。函数 strncat 从第二个字符串添加指定字符数到第一个字符串中,并在结果中添加 null 终止符。图 5.31 的程序演示了函数 stcat 和 strncat。
图 5.32 用 strcmp 和 strncmp 比较三个字符串。函数 strcmp 一次一个字符地比较第一个字符串参数与第二个字符串参数。如果字符串相等,则函数返回0;如果第一个字符串小于第二个字符串,则函数返回负值;如果第一个字符串大于第二个字符串,则函数返回正值。函数 strncmp 等价于函数 strcmp,只是 strncmp 只比较到指定字符数。函数 strncmp 不比较字符串中 null 终止符后面的字符。
程序打印每次函数调用返回的整数值。
常见编程错误 5.22
假设 strcmp 和 strncmp 在参数相等时返回1是个逻辑错误。strcmp 和 strncmp 在参数相等时返回 0(C++ 的 false 值)。因此测试两个字符串的相等性时,strcmp 和 strncmp 的结果应与 0 比较,确定字符串是否相等。
1 // Fig. 5.31:fig05 31.cpp
2 // using strcat and strncat
3 #include <iostream.h>
4 #include <string.h>
5
6 int main()
7 {
8 char s1[ 20 ] = "Happy ";
9 char s2[] = "New Year ";
10 char s3[ 40 ] = "";
11
12 cout << "s1 =" << s1 << "\ns2 =" << s2;
13 cout << "\nstrcat(s1, S2) = "<< strcat( s1, s2 );
14 cout << "\nstrncat(s3, s1, 6) =" << strncat( S3, s1, 6 );
15 cout << "\nstrcat(s3, s1) = "<< strcat( S3, s1 ) << endl;
16
17 return 0;
18 }
输出结果:
s1 = Happy
s2 = New Year
strcat(sl, s2) = Happy New Year
$trncat{s3, s1, 6) = Happy
strcat(s3, s1) = Happy Happy New Year
图 5.31 使用 strcat 和 strncat 函数
1 // Fig. 5.32: fig0532.cpp
2
3 #include <iostream.h>
4 #include <iomanip.h>
5 #include <string.h>
6
7 int main()
8 {
9 char *s1 = "Happy New Year";
10 char *s2 = "Happy New Year";
11 char *s3 = "Happy Holidays";
12
13 cout << "s1 =" << s1 << "\ns2 = "<< s2
14 << "\ns3 = "<< s3 << "\n\nstrcmp(s1, S2) ="
15 << setw( 2 ) << strcmp( s1, s2 )
16 << "\nstrcmp(s1, s3) = "<< setw( 2 )
17 << strcmp( s1, s3 ) << "\nstrcmp(s3, s1) ="
18 << setw( 2 ) << strcmp( s3, s1 );
19
20 cout << "\n\nstrncmp(s1, s3, 6) = "<< setw( 2 )
21 << strncmp( si, s3, 6) << "\nstrncmp(sl, s3, 7) ="
22 << setw( 2 ) << strncmp( s1, s3, 7 )
23 << "\nstrncmp(s3, s1, 7) ="
24 << setw( 2 ) << strncmp( s3, s1, 7 ) << endl;
25 return 0;
26 }
输出结果:
s1 = Happy New Year
s2 = Happy New Year
s3 = Happy Holidays
strcmp(s1, s2) = 0
strcmp (s1, s3) = 1
strcmp (s3, s1) = -1
strncmp(s1, s3, 6) = 0
strncmp(s1, s3, 7) = 1
strncmp(s3, s1, 7) = -1
图 5.32 使用 strcmp 和 strncmp 函数
要了解一个字符串大于或小于另一字符串的含义,可以考虑一系列姓氏的字母顺序表。读者一定会把“Jones"放在“Smith"之前,因为"Jones"的第一个字母在"Smith"的第一个字母之前。
但字母表中不仅有26个字母,而是个字母顺序表,每个字母在表中有特定位置。z 不仅表示字母,而且是字母表中第二十六个字母。
计算机怎么知道一个字母在另一字母之前呢?所有字符在计算机中均表示为数字代码,计算机比较两个字符串时,实际上是比较字符串中字符的数字代码。
可移植性提示 5.6
不同计算机上可能用不同的内部数字代码表示字符。
可移植性提示 5.7
不要显式测试ASCII码如 if(ch ==65)
,而要用对应的字符常量,例如 if(ch == 'A')
为了实现标准化字符表示,大多数计算机厂家将机器设计成使用两种常用编码系统:ASCII和EBCDIC。ASCII 指 美国标准信息交换码(American Standard Code for infomation Interchange),EBCDIC 指 扩展二进制编码的十进制交换码(Extended Binary CodedDecimal Interchange Code)。还有其他编码系统,但这是两种最常用的编码系统。
ASCII 和 EBCDIC 称为字符编码(character code)或字符集(character set)。字符串和字符操作实际上是在操作相应的数字代码,而不是操作字符本身。因此C++中字符和小整数具有互换性。由于数字代码之间有大于、等于、小于的关系,因此可以将不同字符或字符串通过字符编码相互比较。
附录B 列出了 ASCII 字符编码。
函数 strtok 将字符串分解为一系列标记(token)标记就是一系列用分隔符(delimiting chracter,通常是空格或标点符号)分开的字符。例如,在一行文本中,每个单词可以作为标记,空格是分隔符。
需要多次调用 strtok 才能将字符串分解为标记(假设字符串中包含多个标记)。第一次调用strtok包含两个参数,即要标记化的字符串和包含用来分隔标记的字符的字符串(即分隔符):在图 5.33 的例子中,下列语句:
tokenPtr = Strtok(string, " ");
将 tokenPtr 赋给 string 中第一个标记的指针。strtok 的第二个参数 ” ”
表示 string 中的标记用空格分开。
函数 strtok 搜索 string 中不是分隔符(空格)的第一个字符,这是第一个标记的开头。然后函数寻找字符串中的下一个分隔符,将其换成 null(,w,) 字符,这是当前标记的终点。函数 strtok 保存 string 中标记后面的下一个字符的指针,并返回当前标记的指针。
后面再调用 strtok 时,第一个参数为 NULL,继续将 string 标记化。NULL 参数表示调用 strtok 继续从 string 中上次调用 strtok 时保存的位置开始标记化。如果调用 strtok 时已经没有标记,则 strtok 返回 NULL。图 5.33 的程序用strtok将字符串 This is sentence with 7 tokens 标记化。分别打印每个标记。注意 strtok 修改输入字符串,因此,如果调用 strtok 之后还要在程序中使用这个字符串,则应复制这个字符串。
常见编程错误 5.23
没有认识到 strtok 修改正在标记化的字符串,调用 sstrtok 后还在程序中使用这个字符串(以为还是原字符串)函数 strlen 取一个字符串作为参数,并返回字符串中的字符个数,长度中不包括 null 终止符。
图 5.34 的程序演示了函数 strlen。
1 // Fig. 5.33:fig05 33.cpp
2 // Using strtok
3 #include <iostream.h>
4 #include <string.h>
5
6 int main()
7 {
8 char string[] = "This is a sentence with 7 tokens";
9 char *tokenPtr;
10
11 cout << "The string to be tokenized is:\n" << string
12 << "\n\nThe tokens are:\n";
13
14 tokenPtr = strtok( string, " " );
15
16 while ( tokenPtr != NULL ) {
17 cout << tokenPtr << '\n';
18 tokenPtr = strtok( NULL, " " );
19 }
2O
21 return 0;
22 }
输出结果:
The string to be tokenized is:
This is a sentence with 7 tokens
The tokens are:
This
is
a
sentence
7
tokens
图 5.33 使用 strtok 函数
1 // Fig. 5.34: fig05_34.cpp
2 // Using strlen
3 #include <iostream.h>
4 #include<string.h>
5
6 int main()
7 {
8 char *string1 = "abcdefghijklmnopqrstuvwxyz";
9 char *string2 = "four";
10 char string3 = "Boston";
11
12 cout << "The length of \"" << string1
13 << "\" is "<< strlen( string1 )
14 << "\nThe length of \"" << string2
15 << "\" is" << strlen( string2 )
16 << "\nThe length of \"" << string3
17 << "\" is "<< strlen( string3 ) << endl;
18
19 return 0;
20 }
输出结果:
The length of "abcdefghijklmnopqrstuvwxyz" is 26
The length of "four" is 4
图 5.34 使用 strlen 函数