专题十二、字符串

news2024/10/7 20:33:57

字符串

  • 1. 字符串字面量
    • 1.1 字符串字面量中的转义序列
    • 1.2 延续字符串字面量
    • 1.3 如何存储字符串字面量
    • 1.4 字符串字面量的操作
    • 1.5 字符串字面量与字符常量
  • 2. 字符串变量
    • 2.1 初始化字符串变量
    • 2.2 字符数组与字符指针
  • 3. 字符串的读和写
    • 3.1 用 `printf` 函数和 `puts` 函数写字符串
    • 3.2 用 `scanf` 函数和 `gets` 函数读字符串
    • 3.3 逐个字符读字符串
  • 4. 访问字符串中的字符
  • 5. 使用 C 语言的字符串库
    • 5.1 `strcpy` 函数
    • 5.2 `strlen` 函数
    • 5.3 `strcat` 函数
    • 5.4 `strcmp` 函数
  • 6. 字符串惯用法
    • 6.1 搜索字符串的结尾
    • 6.2 复制字符串
  • 7. 字符串数组

本专题介绍 C 语言中的字符串字面量和字符串变量,讨论用于处理字符串的函数,并通过描述如何创建数组元素是指向不同长度字符串的指针的数组,说明 C 语言如何使用这种数组为程序提供命令行支持。

参考资料:《C 语言程序设计 · 现代方法 第 2 2 2 版》

1. 字符串字面量

字符串字面量 string literal \text{string literal} string literal)是用一对双引号括起来的字符序列:

"When you come to a fork in the road, take it."

字符串字面量常常作为格式串出现在 printf 函数和 scanf 函数的调用中。

1.1 字符串字面量中的转义序列

字符串字面量可以像字符常量一样包含转义序列。例如,字符串

"Candy\nIs dandy\nBut liquor\nIs quicker.\n  --Ogden Nash\n"

中每一个字符 \n 都会导致光标移到下一行:

Candy
Is dandy
But liquor
Is quicker.
  --Ogden Nash

虽然字符串字面量中的八进制数和十六进制数的转义序列也都是合法的,但是它们不像字符转义序列那样常见。

在字符串字面量中要小心使用八进制数和十六进制数的转义序列。八进制数的转义序列在 3 3 3 个数字之后结束,或者在第一个非八进制数字符处结束。例如,字符串 \1234 包含两个字符(\1234),而字符串 \189 包含 3 3 3 个字符(\189)。而十六进制数的转义序列则不限制为 3 3 3 个数字,而是直到第一个非十六进制数字符截止。但是大部分编译器会把十六进制数的转义序列通常范围限制在 \x0 ∼ \sim \xff

1.2 延续字符串字面量

如果字符串字面量太长而无法放置在单独一行内,只要把第一行用字符 \ 结尾,C 语言就允许在下一行延续字符串字面量。除了末尾的换行符,在同一行不可以有其他字符跟在 \ 后面:

printf("When you come to a fork in the road, take it.	\
--Yogi Berra");

字符 \ 可以用来把两行或更多行的代码连接成一行,这一过程称为 “拼接”( splicing \text{splicing} splicing)。使用 \ 有一个缺陷:字符串字面量必须从下一行的起始位置继续。但是这就破坏了程序的缩进结构。根据下面的规则,处理长字符串字面量有一种更好的方法:当两条或更多条字符串字面量相邻时(仅用空白字符分割),编译器会把它们合并成一条字符串。这条规则允许把字符串分割放在两行或者更多行中:

printf("When you come to a fork in the road, take it. "
	   "--Yogi Berra");

1.3 如何存储字符串字面量

从本质而言,C 语言把字符串字面量作为字符数组来处理。当 C 语言编译器在程序中遇到长度为 n n n 的字符串字面量时,它会为字符串字面量分配长度为 n + 1 n + 1 n+1 的内存空间。这块内存空间将用来存储字符串字面量中的字符,以及一个用来标志字符串末尾的额外字符(空字符)。空字符是一个所有位都为 0 0 0 的字节,因此用转义序列 \0 来表示。注意不要混淆空字符 '\0' 和零字符 '0'。空字符的码值为 0 0 0,而零字符则有不同的码值,在 ASCII 中为 48 48 48

例如,字符串字面量 "abc" 是作为有 4 4 4 个字符的数组来存储的(abc\0):
在这里插入图片描述
字符串字面量可以为空,空字符串 "" 作为单独一个空字符来存储。

既然字符串字面量是作为数组来存储的,那么编译器会把它看作是 char * 类型的指针。例如,printf 函数和 scanf 函数都接收 char * 类型的值作为它们的第一个参数。例如 printf("abc");,当调用 printf 函数时,会传递 "abc" 的地址,即指向存储字母 a 的内存单元的指针。

1.4 字符串字面量的操作

通常情况下可以在任何 C 语言允许使用 char * 指针的地方使用字符串字面量。例如,字符串字面量可以出现在赋值运算符的右边:

char *p;
p = "abc";

这个赋值操作不是复制 "abc" 中的字符,而是使 p 指向字符串中的第一个字符。

C 语言允许对指针取下标,因此可以对字符串字面量取下标:

char ch;
ch = "abc"[1];

ch 的新值将是字母 b。其他可能的下标是 0 0 0a)、 2 2 2c)和 3 3 3(空字符)。字符串字面量的这种特性并不常用,但有时也比较方便 。思考下面的函数,这个函数把 0 ∼ 15 0 \sim 15 015 的数转换成等价的十六进制的字符形式:

char digit_to_hex_char(int digit)
{
	return "0123456789ABCDEF"[digit];
}

试图改变字符串字面量会导致未定义的行为:

char *p = "abc";
*p = 'd';		/*** WRONG ***/

1.5 字符串字面量与字符常量

只包含一个字符的字符串字面量不同于字符常量。字符串字面量 "a" 是用指针来表示的,这个指针指向存放字符 "a" 的内存单元。字符常量 'a' 是用整数来表示的。

不要在需要字符串的时候使用字符,反之亦然。例如函数调用 printf("\n"); 是合法的,而 printf('\n'); 是非法的。

2. 字符串变量

只要保证字符串是以空字符结尾的,任何一维的字符数组都可以用来存储字符串。这种方法很简单,但使用起来有很大难度。有时很难辨别是否把字符数组作为字符串来使用。如果编写自己的字符串处理函数,请千万注意要正确地处理空字符。

假设需要用一个变量来存储最多有 80 80 80 个字符的字符串。由于字符串在末尾处需要有空字符,我们把变量声明为含有 81 81 81 个字符的数组:

#define STR_LEN 80
...
char str[STR_LEN + 1];

这里把 STR_LEN 定义为 80 80 80 而不是 81 81 81,强调的是 str 可以存储最多有 80 80 80 个字符的字符串;然后才在 str 的声明中对 STR_LEN 1 1 1。这是 C 程序员常用的方式。

当声明用于存放字符串的字符数组时,要始终保证数组的长度比字符串的长度多一个字符,因为 C 语言规定每个字符串都要以空字符结尾。如果没有给空字符预留位置,可能会导致程序运行时出现不可预知的结果,因为 C 函数库中的函数假设字符串都是以空字符结束的。

声明长度为 STR_LEN + 1 的字符数组并不意味着它总是用于存放长度为 STR_LEN 的字符串。字符串的长度取决于空字符的位置,而不是取决于用于存放字符串的字符数组的长度。有 STR_LEN + 1 个字符的数组可以存放长度范围从空字符串到 STR_LEN 的字符串。

2.1 初始化字符串变量

字符串变量可以在声明时进行初始化:

char date1[8] = "June 14";

编译器将把字符串 "June 14" 中的字符复制到数组 date1 中,然后追加一个空字符从而使 date1 可以作为字符串使用。date1 将如下所示:
在这里插入图片描述
"June 14" 看起来是字符串字面量,但其实不然。C 编译器会把它看成是数组初始化式的缩写形式。实际上,我们可以写成:

char date1[8] = {'J', 'u', 'n', 'e', ' ', '1', '4', '\0'};

不过原来的方式更便于阅读。

如果初始化式太短以致于不能填满字符串变量,编译器会添加空字符。因此,在声明

char date2[9] = "June 14";

之后,date2 将如下所示:
在这里插入图片描述
这与 C 语言处理数组初始化式的方法一致。当数组的初始化式比数组本身短时,余下的数组元素会被初始化为 0 0 0。在把字符数组额外的元素初始化为 \0 这点上,编译器对字符串和数组遵循相同的规则。

如果初始化式比字符串变量长,这对字符串而言是非法的。然而 C 语言允许初始化式(不包括空字符)与变量有完全相同的长度:

char date3[7] = "June 14";

由于没有给空字符留空间,所以编译器不会试图存储字符:
在这里插入图片描述
如果计划对用来放置字符串的字符数组进行初始化,一定要确保数组的长度要长于初始化式的长度,否则,编译器将忽略空字符,这将使得数组无法作为字符串使用。

字符串变量的声明中可以省略它的长度。这种情况下,编译器会自动计算长度:

char date4[] = "June 14";

编译器为 date4 分配 8 8 8 个字符的空间,这足够存储 "June 14" 中的字符和一个空字符。不指定 date4 的长度并不意味着以后可以改变数组的长度,一旦编译了程序,date4 的长度就固定是 8 8 8 了。如果初始化式很长,那么省略字符串变量的长度是特别有效的,因为手工计算长度很容易出错。

2.2 字符数组与字符指针

一起来比较一下下面这两个看起来很相似的声明:

char date[] = "June 14";
char *date = "June 14";

前者声明 date 是一个数组,后者声明 date 是一个指针。正因为有了数组和指针之间的紧密关系,才使上面这两个声明中的 date 都可以用作字符串。尤其是,任何期望传递字符数组或字符指针的函数都能够接收这两种声明的 date 作为参数。

然而,需要注意,不能错误地认为上面这两种 date 可以互换。两者之间有很大的差异:

  • 在声明为数组时,就像任意数组元素一样,可以修改存储在 date 中的字符。在声明为指针时,date 指向字符串字面量,而字符串字面量是不可以修改的。
  • 在声明为数组时,date 是数组名。在声明为指针时,date 是变量,这个变量可以在程序执行期间指向其他字符串。

如果希望可以修改字符串,那么就要建立字符数组来存储字符串,声明指针变量是不够的。下面的声明使编译器为指针变量分配了足够的内存空间:

char *p;

可惜的是,它不能为字符串分配空间,因为我们没有指明字符串的长度。在使用 p 作为字符串之前,必须把 p 指向字符数组。一种可能是把 p 指向已经存在的字符串变量:

char str[STR_LEN + 1], *p;
p = str;

现在 p 指向了 str 的第一个字符,所以可以把 p 作为字符串使用了。另一种可能是让 p 指向一个动态分配的字符串。

使用未初始化的指针变量作为字符串是非常严重的错误。考虑下面的例子,它试图创建字符串 "abc"

char *p;
p[0] = 'a';			/*** WRONG ***/
p[1] = 'b';			/*** WRONG ***/
p[2] = 'c';			/*** WRONG ***/
p[3] = '\0';		/*** WRONG ***/

因为 p 没有被初始化,所以我们不知道它指向哪里。用指针 p 把字符 abc\0 写入内存会导致未定义的行为。

3. 字符串的读和写

3.1 用 printf 函数和 puts 函数写字符串

转换说明 %s 允许 printf 函数写字符串。例如:

char str[] = "Are we having fun yet?";
printf("%s\n", str);

输出会是

Are we having fun yet?

printf 函数会逐个写字符串中的字符,直到遇到空字符才停止。如果空字符丢失,printf 函数会越过字符串的末尾继续写,直到最终在内存的某个地方找到空字符为止。

如果只想显示字符串的一部分,可以使用转换说明 %.ps,这里 p p p 是要显示的字符数量。语句 printf("%.6s\n", str); 会显示 Are we

字符串跟数一样,可以在指定字段内显示。转换说明 %ms 会在大小为 m m m 的字段内显示字符串。对于超过 m m m 个字符的字符串,printf 函数会显示出整个字符串,而不会截断。如果字符串少于 m m m 个字符,则会在字段内右对齐输出。如果要强制左对齐,可以在 m m m 前加一个减号。 m m m 值和 p p p 值可以组合使用:转换说明 %m.ps 会使字符串的前 p p p 个字符在大小为 m m m 的字段内显示。

C 函数库还提供了 puts 函数:puts(str);puts 函数只有一个参数,即需要显示的字符串。在写完字符串后,puts 函数总会添加一个额外的换行符,从而前进到下一个输出行的开始处。

3.2 用 scanf 函数和 gets 函数读字符串

转换说明 %s 允许 scanf 函数把字符串读入字符数组:

scanf("%s", str);

scanf 函数调用中,不需要在 str 前添加运算符 &,因为 str 是数组名,编译器在把它传递给函数时会把它当作指针来处理。

调用时,scanf 函数会跳过空白字符,然后读入字符并存储到 str 中,直到遇到空白字符为止。scanf 函数始终会在字符串末尾存储一个空字符。

scanf 函数读入字符串永远不会包含空白字符。因此,scanf 函数通常不会读入一整行输入。换行符会使 scanf 函数停止读入,空格符或制表符也会产生同样的结果。为了一次读入一整行输入,可以使用 gets 函数:gets(str);。类似于 scanf 函数,gets 函数把读入的字符放到数组中,然后存储一个空字符。然而,在其他方面 gets 函数有些不同于 scanf 函数。

  • gets 函数不会在开始读字符串之前跳过空白字符,scanf 函数会跳过。
  • gets 函数会持续读入直到找到换行符才停止,scanf 函数会在任意空白字符处停止。此外,gets 函数会忽略掉换行符,不会把它存储到数组中,用空字符代替换行符。

在把字符读入数组时,scanf 函数和 gets 函数都无法检测数组何时被填满。因此,它们存储字符时可能越过数组的边界,这会导致未定义的行为。通过用转换说明 %ns 代替 %s 可以使 scanf 函数更安全。这里的数字 n n n 指出可以存储的最多字符数。可惜的是,gets 函数天生就是不安全的,fgets 函数则是一种好得多的选择。

3.3 逐个字符读字符串

因为对许多程序而言,scanf 函数和 gets 函数都有风险且不够灵活,C 程序员经常会自己编写输入函数。通过每次一个字符的方式来读入字符串,这类函数可以提供比标准输入函数更大程度的控制。

如果决定设计自己的输入函数,那么就需要考虑下面这些问题。

  • 在开始存储字符串之前,函数应该跳过空白字符吗?
  • 什么字符会导致函数停止读取:换行符、任意空白字符还是其他某种字符?需要存储这类字符还是忽略掉?
  • 如果输入的字符串太长以致无法存储,那么函数应该做些什么:忽略额外的字符还是把它们留给下一次输入操作?

假定我们所需要的函数不会跳过空白字符,在第一个换行符处停止读取,换行符不存储到字符串中,并且忽略额外的字符。函数将有如下原型:

int read_line(char str[], int n);

str 表示用来存储输入的数组,而 n 是读入字符的最大数量。如果输入行包含多于 n n n 个的字符,read_line 函数将忽略多余的字符。read_line 函数会返回实际存储在 str 中的字符数量( 0 0 0 n n n 之间的任意数)。我们不可能总是需要 read_line 函数的返回值,但是有这个返回值也没问题。

read_line 函数主要由一个循环构成。只要 str 中还有空间,此循环就会调用 getchar 函数逐个读入字符并把它们存储到 str 中。在读入换行符时循环终止。严格地说,如果 getchar 函数读入字符失败,也应该终止循环,但是这里暂时忽略这种复杂情况。下面是 read_line 函数的完整定义:

int read_line(char str[], int n)
{
	int ch, i = 0;

	while ((ch = getchar()) != '\n')
		if (i < n)
			str[i++] = ch;
	str[i] = '\0';				/* terminates string */
	return i;					/* number of characters stored */
}

注意,ch 的类型为 int 而不是 char,因为 getchar 把它读取的字符作为 int 类型的值返回。

返回之前,read_line 函数在字符串的末尾放置一个空字符。scanfgets 等标准函数会自动在输入字符串的末尾放置一个空字符;然而,如果要自己写输入函数,必须手工加上空字符。

4. 访问字符串中的字符

字符串是以数组的方式存储的,因此可以使用下标来访问字符串中的字符。例如,为了对字符串 s 中的每个字符进行处理,可以设定一个循环来对计数器 i 进行自增操作,并通过表达式 s[i] 来选择字符。

假定需要一个函数来统计字符串中空格的数量。利用数组取下标操作可以写出如下函数:

int count_spaces(const char s[])
{
	int count = 0, i;
	
	for (i = 0; s[i] != '\0'; i++)
		if (s[i] == ' ')
			count++;
	return count;
}

s 的声明中加上 const 表明 count_spaces 函数不会改变数组。许多 C 程序员不会像例子中那样编写 count_spaces 函数,他们更愿意使用指针来跟踪字符串中的当前位置。这种方法对于处理数组来说一直有效,在处理字符串方面尤其方便。

下面用指针算术运算代替数组取下标来重新编写 count_spaces 函数。这次不再需要变量 i,而是利用 s 自身来跟踪字符串中的位置。通过对 s 反复进行自增操作,count_spaces 函数可以逐个访问字符串中的字符。下面是 count_spaces 函数的新版本:

int count_spaces(const char *s)
{
	int count = 0;
	
	for ( ; *s != '\0'; s++)
		if (*s == ' ')
			count++;
	return count;
}

注意,const 没有阻止 count_spaces 函数对 s 的修改,它的作用是阻止函数改变 s 所指向的字符。而且,因为 s 是传递给 count_spaces 函数的指针的副本,所以对 s 进行自增操作不会影响原始的指针。count_spaces 函数示例引出了一些关于如何编写字符串函数的问题。

  • 用数组操作或指针操作访问字符串中的字符,哪种方法更好一些呢?只要使用方便,可以随意使用任意一种方法,甚至可以混合使用两种方法。从传统意义上来说,C 程序员更倾向于使用指针操作来处理字符串。
  • 字符串形式参数应该声明为数组还是指针呢count_spaces 函数的两种写法说明了这两种选择:第 1 1 1 种写法把 s 声明为数组,而第 2 2 2 种写法则把 s 声明为指针。实际上,这两种声明之间没有任何差异,因为编译器会把数组型的形式参数视为指针。
  • 形式参数的形式(s[] 或者 *s)是否会对实际参数产生影响呢?不会的。当调用 count_spaces 函数时,实际参数可以是数组名、指针变量或者字符串字面量。count_spaces 函数无法说明差异。

5. 使用 C 语言的字符串库

在 C 语言中把字符串当作数组来处理,因此对字符串的限制方式和对数组的一样,特别是,它们都不能用 C 语言的运算符进行复制和比较操作。

直接复制或比较字符串会失败。例如,假定 str1str2 有如下声明:

char str1[10], str2[10];

利用 = 运算符来把字符串复制到字符数组中是不可能的:

str1 = "abc";		/*** WRONG ***/
str2 = str1;		/*** WRONG ***/

从专题十一中可知,把数组名用作 = 的左操作数是非法的。但是,使用 = 初始化 字符数组是合法的:char str1[10] = "abc";,这是因为在声明中,= 不是赋值运算符。

试图使用关系运算符或判等运算符来比较字符串是合法的,但不会产生预期的结果:

if (str1 == str2) ...		/*** WRONG ***/

这条语句把 str1str2 作为指针来进行比较,而不是比较两个数组的内容。因为 str1str2 有不同的地址,所以表达式 str1 == str2 的值一定为 0 0 0

C 语言的函数库为完成对字符串的操作提供了丰富的函数集,这些函数的原型驻留在 <string.h> 头中。在 <string.h> 中声明的每个函数至少需要一个字符串作为实际参数。字符串形式参数声明为 char * 类型,这使得实际参数可以是字符数组、char * 类型的变量或者字符串字面量。然而,要注意那些没有声明为 const 的字符串形式参数,这些形式参数可能会在调用函数时发生改变,所以对应的实际参数不应该是字符串字面量。

5.1 strcpy 函数

strcpy 函数的原型如下:

char *strcpy(char *s1, const char *s2);

strcpy 函数把字符串 s2 复制给字符串 s1,准确地讲,应该说成是 “strcpy 函数把 s2 指向的字符串复制到 s1 指向的数组中”。也就是说,strcpy 函数把 s2 中的字符复制到 s1 中直到遇到 s2 中的第一个空字符为止,该空字符也需要复制。strcpy 函数返回 s1,即指向目标字符串的指针。这一过程不会改变 s2 指向的字符串,因此将其声明为 conststrcpy 函数的存在弥补了不能使用赋值运算符复制字符串的不足。

大多数情况下我们会忽略 strcpy 函数的返回值,但有时候 strcpy 函数调用是一个更大的表达式的一部分,这时其返回值就比较有用了。例如,可以把一系列 strcpy 函数调用连起来:

strcpy(str1, strcpy(str2, "abcd"));			/* both str1 and str2 now contain "abcd" */

strcpy(str1, str2) 的调用中,strcpy 函数无法检查 str2 指向的字符串的大小是否真的适合 str1 指向的数组。假设 str1 指向的字符串长度为 n n n,如果 str2 指向的字符串中的字符数不超过 n − 1 n - 1 n1,那么复制操作可以完成。但是,如果 str2 指向更长的字符串,那么结果就无法预料了,因为 strcpy 函数会一直复制到第一个空字符为止,所以它会越过 str1 指向的数组的边界继续复制。

尽管执行会慢一点,但是调用 strncpy 函数仍是一种更安全的复制字符串的方法。strncpy 类似于 strcpy,但它还有第三个参数可以用于限制所复制的字符数。为了将 str2 复制到 str1,可以使用如下的 strncpy 调用:

strncpy(str1, str2, sizeof(str1));

只要 str1 足够装下存储在 str2 中的字符串,包括空字符,复制就能正确完成。当然,strncpy 本身也不是没有风险。如果 str2 中存储的字符串的长度大于 str1 数组的长度,strncpy 会导致 str1 中的字符串没有终止的空字符。下面是一种更安全的用法:

strncpy(str1, str2, sizeof(str1) - 1);
str1[sizeof(str1) - 1] = '\0';

5.2 strlen 函数

strlen 函数的原型如下:

size_t strlen(const char *s);

定义在 C 函数库中的 size_t 类型是一个 typedef 名字,表示 C 语言中的一种无符号整型。除非是处理极长的字符串,否则不需要关心其技术细节。我们可以简单地把 strlen 的返回值作为整数处理。

strlen 函数返回字符串 s 的长度:s 中第一个空字符之前的字符个数,不包括空字符。下面是几个示例:

int len;
...
len = strlen("abc");		/* len is now 3 */
len = strlen("");			/* len is now 0 */
strcpy(str1, "abc");
len = strlen(str1);			/* len is now 3 */

最后一个例子说明了很重要的一点:当用数组作为实际参数时,strlen 不会测量数组本身的长度,而是返回存储在数组中的字符串的长度。

5.3 strcat 函数

strcat 函数的原型如下:

char *strcat(char *s1, const char *s2)

strcat 函数把字符串 s2 的内容追加到字符串 s1 的末尾,并且返回字符串 s1。下面列举了一些使用 strcat 函数的例子:

strcpy(str1, "abc");
strcat(str1, "def");		/* str1 now contains "abcdef" */
strcpy(str1, "abc");
strcpy(str2, "def");
strcat(str1, str2);			/* str1 now contains "abcdef" */

同使用 strcpy 函数一样,通常忽略 strcat 函数的返回值。下面的例子说明了可能使用返回值的方法:

strcpy(str1, "abc");
strcpy(str2, "def");
strcat(str1, strcat(str2, "ghi");
	/* str1 now contains "abcdefghi"; str2 contains "defghi" */

如果 str1 指向的数组没有大到足以容纳 str2 指向的字符串中的字符,那么调用 strcat(str1, str2) 的结果将是不可预测的。strncat 函数比 strcat 更安全,但速度也慢一些。与 strncpy 一样,它有第三个参数来限制所复制的字符数。下面是调用的形式:

strncat(str1, str2, sizeof(str1) - strlen(str1) - 1);

strncat 函数会在遇到空字符时终止 str1,第三个参数没有考虑该空字符。在上面的例子中,第三个参数计算 str1 中的剩余空间,然后减去 1 1 1 以确保为空字符留下空间。

5.4 strcmp 函数

strcmp 函数的原型如下:

int strcmp(const char *s1, const char *s2);

strcmp 函数比较字符串 s1 和字符串 s2,然后根据 s1 是小于、等于或大于 s2,函数返回一个小于、等于或大于 0 0 0 的值。例如,为了检查 str1 是否小于 str2,可以写

if (strcmp(str1, str2) < 0)		/* is str1 < str2 */
...

通过选择适当的关系运算符或判等运算符,可以测试 str1str2 之间任何可能的关系。

类似于字典中单词的编排方式,strcmp 函数利用字典顺序进行字符串比较。更精确地说,只要满足下列两个条件之一,那么 strcmp 函数就认为 s1 是小于 s2 的。

  • s1s2 的前 i i i 个字符一致,但是 s1 的第 i + 1 i + 1 i+1 个字符小于 s2 的第 i + 1 i + 1 i+1 个字符。例如,"abc" 小于 "bcd""abd" 小于 "abe"
  • s1 的所有字符与 s2 的字符一致,但是 s1s2 短。例如,"abc" 小于 "abcd"

当比较两个字符串中的字符时,strcmp 函数会查看字符对应的数值码。一些底层字符集的知识可以帮助预测 strcmp 函数的结果。例如,下面是 ASCII 字符集的一些重要性质。

  • A ∼ \sim Za ∼ \sim z0 ∼ \sim 9 这几组字符的数值码都是连续的。
  • 所有的大写字母都小于小写字母。在 ASCII 码中, 65 ∼ 90 65 \sim 90 6590 的编码表示大写字母, 97 ∼ 122 97 \sim 122 97122 的编码表示小写字母。
  • 数字小于字母。 45 ∼ 57 45 \sim 57 4557 的编码表示数字。
  • 空格符小于所有打印字符。ASCII 码中空格符的值是 32 32 32

程序 remind.c:显示一个月的提醒列表
下面的程序会显示一个月的每日提醒列表。用户需要输入一系列提醒,每条提醒都要有一个前缀来说明是一个月中的哪一天。当用户输入的是 0 0 0 而不是有效日期时,程序会显示出录入的全部提醒列表,按日期排序。

总体策略不是很复杂。把字符串存储在二维的字符数组中,数组的每一行包含一个字符串。在程序读入日期以及相关的提醒后,通过使用 strcmp 函数进行比较来查找数组从而确定这一天所在的位置。然后,程序会使用 strcpy 函数把此位置之后的所有字符串往后移动一个位置。最后,程序会把这一天复制到数组中,并且调用 strcat 函数来把提醒附加到这一天后面。

当然,总会有少量略微复杂的地方。例如,希望日期在两个字符的字段中右对齐以便它们的个位可以对齐。有很多种方法可以解决这个问题。这里选择用 scanf 函数把日期读入到整型变量中,然后调用 sprintf 函数把日期转换成字符串格式。sprintf 是个类似于 printf 的库函数,不同之处在于它会把输出写到字符串中。函数调用

sprintf(day_str, "%2d", day);

day 的值写到 day_str 中。因为 sprintf 在写完后会自动添加一个空字符,所以 day_str 会包含一个由空字符结尾的字符串。

另一个复杂的地方是确保用户没有输入两位以上的数字,为此将使用下列 scanf 函数调用:

scanf("%2d", &day);

即使输入有更多的数字,在 %d 之间的数 2 2 2 也会通知 scanf 函数在读入两个数字后停止。

/* Prints a one-month reminder list */

#include <stdio.h>
#include <string.h>

#define MAX_REMIND 50           /* maximum number of reminders    */
#define MSG_LEN 60              /* max length of reminder message */

int read_line(char str[], int n);

int main(void)
{
    char reminders[MAX_REMIND][MSG_LEN+3];
    char day_str[3], msg_str[MSG_LEN+1];
    int day, i, j, num_remind = 0;

    for (;;) {
        if (num_remind == MAX_REMIND) {
            printf("-- No space left --\n");
            break;
        }

        printf("Enter day and reminder: ");
        scanf("%2d", &day);
        if (day == 0)
            break;
        sprintf(day_str, "%2d", day);
        read_line(msg_str, MSG_LEN);

        for (i = 0; i < num_remind; i++)
            if (strcmp(day_str, reminders[i]) < 0)
                break;
        for (j = num_remind; j > i; j--)
            strcpy(reminders[j], reminders[j-1]);

        strcpy(reminders[i], day_str);
        strcat(reminders[i], msg_str);

        num_remind++;
    }

    printf("\nDay Reminder\n");
    for (i = 0; i < num_remind; i++)
        printf(" %s\n", reminders[i]);

    return 0;
}

int read_line(char str[], int n)
{
    int ch, i = 0;

    while ((ch = getchar()) != '\n')
        if (i < n)
            str[i++] = ch;
    str[i] = '\0';
    return i;
}

这个程序的运行过程如下(用户的输入用下划线标注):
Enter day and reminder: 24 Susan’s birthday ‾ \underline{\text{24 Susan's birthday}} 24 Susan’s birthday
Enter day and reminder: 5 6:00 - Dinner with Marge and Russ ‾ \underline{\text{5 6:00 - Dinner with Marge and Russ}} 5 6:00 - Dinner with Marge and Russ
Enter day and reminder: 26 Movie - “Chinatown” ‾ \underline{\text{26 Movie - “Chinatown”}} 26 Movie - “Chinatown”
Enter day and reminder: 7 10:30 - Dental appointment ‾ \underline{\text{7 10:30 - Dental appointment}} 7 10:30 - Dental appointment
Enter day and reminder: 12 Movie - “Dazed and Confused” ‾ \underline{\text{12 Movie - “Dazed and Confused”}} 12 Movie - “Dazed and Confused”
Enter day and reminder: 5 Saturday class ‾ \underline{\text{5 Saturday class}} 5 Saturday class
Enter day and reminder: 12 Saturday class ‾ \underline{\text{12 Saturday class}} 12 Saturday class
Enter day and reminder: 0 ‾ \underline{0} 0

Day Reminder
  5 Saturday class
  5 6:00 - Dinner with Marge and Russ
  7 10:30 - Dental appointment
 12 Saturday class
 12 Movie - "Dazed and Confused"
 24 Susan's birthday
 26 Movie - "Chinatown"

6. 字符串惯用法

6.1 搜索字符串的结尾

许多字符串操作需要搜索字符串的结尾。strlen 函数就是一个重要的例子。下面的 strlen 函数搜索字符串参数的结尾,并且使用一个变量来跟踪字符串的长度:

size_t strlen(const char *s)
{
	size_t n;
	
	for (n = 0; *s != '\0'; s++)
		n++;
	return n;
}

指针 s 从左至右扫描整个字符串,变量 n 记录当前已经扫描的字符数量。当 s 最终指向一个空字符时,n 所包含的值就是字符串的长度。

下面的版本运行速度可能会更高一些:

size_t strlen(const char *s)
{
	const char *p = s;
	
	while (*s)
		s++;
	return s - p;
}

这个版本的 strlen 函数通过定位空字符位置的方式来计算字符串的长度,然后用空字符的地址减去字符串中第一个字符的地址。运行速度的提升得益于不需要在 while 循环内部对 n 进行自增操作。请注意,在 p 的声明中出现了单词 const,如果没有它,编译器会注意到把 s 赋值给 p 会给 s 指向的字符串造成一定风险。

下面两个语句都是 “搜索字符串结尾的空字符” 的惯用法。第一个版本最终使 s 指向了空字符。第二个版本更加简洁,但是最后使 s 正好指向空字符后面的位置。

while (*s)
	s++;
while (*s++)
	;

6.2 复制字符串

char *strcat(char *s1, const char *s2)
{
	char *p = s1;
	while (*p != '\0')
		p++;
	while (*s2 != '\0') {
		*p = *s2;
		p++;
		s2++;
	}
	*p = '\0';
	return s1;
}

strcat 函数的这种写法采用了两步算法:

  1. 确定字符串 s1 末尾空字符的位置,并且使指针 p 指向它;
  2. 把字符串 s2 中的字符逐个复制到 p 所指向的位置。

函数中的第一个 while 语句实现了第 1 1 1 步。程序中先把 p 设定为指向 s1 的第一个字符,接着 p 开始自增直到指向空字符为止。循环终止时,p 指向空字符。

第二个 while 语句实现了第 2 2 2 步。循环体把 s2 指向的一个字符复制到 p 指向的地方,接着 ps2 都进行自增,当 s2 指向空字符时循环终止。接下来,程序在 p 指向的位置放置空字符,然后 strcat 函数返回。

类似于对 strlen 函数的处理,也可以简化 strcat 函数的定义,得到下面的版本:

char *strcat(char *s1, const char *s2)
{
	char *p = s1;
	
	while (*p)
		p++;
	while (*p++ = *s2++)
		;
	return s1;
}

改进的 strcat 函数的核心是 “字符串复制” 的惯用法:

while (*p++ = *s2++)
	;

如果忽略了两个 ++ 运算符,那么圆括号中的表达式会简化为普通的赋值:*p = *s2。这个表达式把 s2 指向的字符复制到 p 所指向的地方。正是由于有了这两个 ++ 运算符,赋值之后 ps2 才进行了自增。重复执行此表达式所产生的效果就是把 s2 指向的一系列字符复制到 p 所指向的地方。

由于圆括号中的主要运算符是赋值运算符,所以 while 语句会测试赋值表达式的值,也就是测试复制的字符。除空字符以外的所有字符的测试结果都为真,因此,循环只有在复制空字符后才会终止。而且由于循环是在赋值之后终止,所以不需要单独用一条语句来在新字符串的末尾添加空字符。

7. 字符串数组

存储字符串数组的最明显的解决方案是创建二维的字符数组,然后按照每行一个字符串的方式把字符串存储到数组中。考虑下面的例子:

char planets[][8] = {"Mercury", "Venus", "Earth",
					 "Mars", "Jupiter", "Saturn",
					 "Uranus", "Neptune", "Pluto"};

下面给出了 planets 数组的可能形式。并非所有的字符串都足以填满数组的一整行,所以 C 语言用空字符来填补。因为只有 3 3 3 个行星的名字需要用满 8 8 8 个字符(包括末尾的空字符),所以这样的数组有一点浪费空间。
在这里插入图片描述
因为大部分字符串集都是长字符串和短字符串的混合,所以这些例子所暴露的低效性是在处理字符串时经常遇到的问题。我们需要的是参差不齐的数组 ragged array \text{ragged array} ragged array),即每一行有不同长度的二维数组。C 语言本身并不提供这种 “参差不齐的数组类型”,但它提供了模拟这种数组类型的工具。秘诀就是建立一个特殊的数组,这个数组的元素都是指向字符串的指针。

下面是 planets 数组的另外一种写法,这次把它看成是指向字符串的指针的数组:

char *planets[] = {"Mercury", "Venus", "Earth",
				   "Mars", "Jupiter", "Saturn",
				   "Uranus", "Neptune", "Pluto"};

看上去改动不是很大,只是去掉了一对方括号,并且在 planets 前加了一个星号。但是,这对 planets 存储方式产生的影响却很大:
在这里插入图片描述
planets 的每一个元素都是指向以空字符结尾的字符串的指针。虽然必须为 planets 数组中的指针分配空间,但是字符串中不再有任何浪费的字符。

为了访问其中一个行星名字,只需要对 planets 数组取下标。由于指针和数组之间的紧密关系,访问行星名字中的字符的方式和访问二维数组元素的方式相同。例如,为了在 planets 数组中搜寻以字母 M 开头的字符串,可以使用下面的循环:

for (i = 0; i < 9; i++)
	if (planets[i][0] == 'M')
		printf("%s begins with M\n", planets[i]);

命令行参数

运行程序时经常需要提供一些信息 —— 文件名或者是改变程序行为的开关。考虑 UNIX 的 ls 命令。如果我们运行 ls,将会显示当前目录中的文件名;但是,如果键入 ls -l,那么会显示一个详细的文件列表,包括每个文件的大小、文件的所有者、文件最后改动的日期和时间等。为了进一步改变 ls 的行为,可以指定只显示一个文件的详细信息:ls -l remind.c

命令行信息不仅对操作系统命令可用,它对所有程序都是可用的。为了能够访问这些命令行参数(C 标准中称为程序参数),必须把 main 函数定义为含有两个参数的函数,这两个参数通常命名为 argcargv

int main(int argc, char *argv[])
{
	...
}

argc(参数计数)是命令行参数的数量(包括程序名本身),argv(参数向量)是指向命令行参数的指针数组,这些命令行参数以字符串的形式存储。argv[0] 指向程序名,而从 argv[1]argv[argc-1] 则指向余下的命令行参数。

argv 有一个附加元素,即 argv[argc],这个元素始终是一个空指针。空指针是一种不指向任何地方的特殊指针,用宏 NULL 表示。如果用户输入命令行 ls -l remind.c,那么 argc 将为 3 3 3argv[0] 将指向含有程序名的字符串,argv[1] 将指向字符串 "-l"argv[2] 将指向字符串 "remind.c",而 argv[3] 将为空指针:
在这里插入图片描述
此处没有详细说明程序名,因为根据操作系统的不同,程序名可能会包括路径或其他信息。如果程序名不可用,那么 argv[0] 会指向空字符串。

因为 argv 是指针数组,所以访问命令行参数非常容易。常见的做法是,期望有命令行参数的程序将会设置循环来按顺序检查每一个参数。设定这种循环的方法之一就是使用整型变量作为 argv 数组的下标。例如,下面的循环每行一条地显示命令行参数:

int i;
for (i = 1; i < argc; i++)
	printf("%s\n", argv[i]);

另一种方法是构造一个指向 argv[1] 的指针,然后对指针重复进行自增操作来逐个访问数组余下的元素。因为 argv 数组的最后一个元素始终是空指针,所以循环可以在找到数组中一个空指针时停止:

char **p;
for (p = &argv[1]; *p != NULL; p++)
	printf("%s\n", *p);

因为 p 是指向字符的指针的指针,所以必须小心使用。设置 p 等于 &argv[1] 是有意义的,因为 argv[1] 是一个指向字符的指针,所以 &argv[1] 就是指向指针的指针。因为 *pNULL 都是指针,所以测试 *p != NULL 是没有问题的。对 p 进行自增操作看起来也是对的 —— 因为 p 指向数组元素,所以对它进行自增操作将使 p 指向下一个元素。显示 *p 的语句也是合理的,因为 *p 指向字符串中的第一个字符。

程序 planet.c:核对行星的名字
下面的程序说明了访问命令行参数的方法。设计此程序的目的是为了检查一系列字符串,从而找出哪些字符串是行星的名字。程序执行时,用户将把待测试的字符串放置在命令行中,程序会指出每个字符串是否是行星的名字。如果是,程序还将显示行星的编号。
我们把最靠近太阳的行星编号为 1 1 1。并且规定除非字符串的首字母大写并且其余字母小写,否则程序不会认为字符串是行星的名字。

/* Checks planet names */

#include <stdio.h>
#include <string.h>

#define NUM_PLANETS 9

int main(int argc, char *argv[])
{
    char *planets[] = {"Mercury", "Venus", "Earth",
                       "Mars", "Jupiter", "Saturn",
                       "Uranus", "Neptune", "Pluto"};
    int i, j;

    for (i = 1; i < argc; i++) {
        for (j = 0; j < NUM_PLANETS; j++)
            if (strcmp(argv[i], planets[j]) == 0) {
                printf("%s is planet %d\n", argv[i], j + 1);
                break;
            }
        if (j == NUM_PLANETS)
            printf("%s is not a planet\n", argv[i]);
    }

    return 0;
}

这个程序的运行过程如下(用户的输入用下划线标注):
planet Jupiter venus Earth fred ‾ \underline{\text{planet Jupiter venus Earth fred}} planet Jupiter venus Earth fred

Jupiter is planet 5
venus is not a planet
Earth is planet 3
fred is not a planet

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1580942.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

通俗白话了解资产负债现金利润三张表

看到一本小书不错《财务小白轻松入门》&#xff0c;里面通俗说了三张表之间的关系。贴图摘录下&#xff1a;

SSL证书添加与ICP备案,对于SpringBoot的要求

配置了SSL证书之后&#xff0c;在SpringBoot的resources文件夹里的application.properties会添加以下代码&#xff1a; server.port443 不需要添加server.address。不然会报错。 https类型的请求默认在Postman里面不可请求。 经过SSL证书处理的网页&#xff0c;链接中使默认…

RAG文本加载和分块调研

文本加载和分块 一、文本加载 文本加载是RAG文本增强检索重要环节。文件有不同类型&#xff08;excel、word、ppt、pdf、png、html、eps、gif、mp4、zip等&#xff09;&#xff0c;衍生出了很多第三方库。使用python处理文件是各种python开发岗位都需要的操作。主要涉及到的标准…

TryHackMe - HTTP Request Smuggling

学完、打完后的复习 HTTP 1 这部分比较简单&#xff0c;直接略过 HTTP2请求走私 首先要了解HTTP2的结构&#xff0c;与HTTP1之间的一些差异 HTTP2中不再使用CRLF来作为字段的边界限定&#xff0c;而是在二进制中直接通过长度、名字、值长度、值&#xff0c;来确认边界 而这…

【C++】用红黑树封装map和set

我们之前学的map和set在stl源码中都是用红黑树封装实现的&#xff0c;当然&#xff0c;我们也可以模拟来实现一下。在实现之前&#xff0c;我们也可以看一下stl源码是如何实现的。我们上篇博客写的红黑树里面只是一个pair对象&#xff0c;这对于set来说显然是不合适的&#xff…

软件可靠性基本概念_1.定义和定量描述

1.软件可靠性定义 软件可靠性&#xff08;Software Reliability&#xff09;是软件产品在规定的条件下和规定的时间区间完成规定功能的能力。规定的条件是指直接与软件运行相关的使用该软件的计算机系统的状态和软件的输入条件&#xff0c;或统称为软件运行时的外部输入条件&am…

【MATLAB源码-第183期】基于matlab的图像处理GUI很全面包括滤波,灰度,边缘提取,RGB亮度调节,二值化等。

操作环境&#xff1a; MATLAB 2022a 1、算法描述 1. RGB颜色亮度调整 1.1 RGB颜色模型 RGB颜色模型是一种加色模型&#xff0c;使用红色&#xff08;R&#xff09;、绿色&#xff08;G&#xff09;、蓝色&#xff08;B&#xff09;三种颜色的不同组合来表示各种颜色。每种…

OceanBase V4.2 MySQL模式下,如何通过DBLINK实现跨数据源访问

概述 跨数据源访问可通过 DBLINK&#xff08;以下简称DBLINK&#xff09;实现&#xff0c;从而使得业务代码能够像访问本地数据库一样轻松访问远端数据库。原先&#xff0c;DBLINK主要服务于Oracle模式&#xff0c;但由于OceanBase 的MySQL模式租户同样存在访问远端数据库的需…

【二分查找】Leetcode 点名

题目解析 LCR 173. 点名 算法讲解 1. 哈希表 class Solution { public:int takeAttendance(vector<int>& nums) {map<int, int> Hash;for(auto n : nums) Hash[n];for(int i 0; i < nums[nums.size() - 1]; i){if(Hash[i] 0)return i;}return nums.si…

【ZZULIOJ】1053: 正弦函数(Java)

目录 题目描述 输入 输出 样例输入 Copy 样例输出 Copy code 题目描述 输入x&#xff0c;计算上面公式的前10项和。 输入 输入一个实数x。 输出 输出一个实数&#xff0c;即数列的前10项和&#xff0c;结果保留3位小数。 样例输入 Copy 1 样例输出 Copy 0.841 c…

Python学习笔记11 - 列表

1. 列表的创建与删除 2. 列表的查询操作 3. 列表的增、删、改操作 4. 列表元素的排序 5. 列表生成式

PostgreSQL入门到实战-第十弹

PostgreSQL入门到实战 PostgreSQL数据过滤(三)官网地址PostgreSQL概述PostgreSQL中OR操作理论PostgreSQL中OR实操更新计划 PostgreSQL数据过滤(三) 了解PostgreSQL OR逻辑运算符以及如何使用它来组合多个布尔表达式。 官网地址 声明: 由于操作系统, 版本更新等原因, 文章所列…

免费SSL证书跟付费SSL证书有什么区别?

免费SSL证书与付费SSL证书的主要区别如下&#xff1a; 1. 类型与验证级别&#xff1a; - 免费SSL证书通常仅提供域名验证&#xff08;DV&#xff09;&#xff0c;这是一种最基本的验证级别&#xff0c;仅验证域名的所有权&#xff0c;确认申请者对所申请域名的有效控制。 - 付费…

如何在 iOS 项目中集成 MiniApp SDK,快速构建智能小程序?

本文介绍如何在 iOS 项目中&#xff0c;集成 MiniApp SDK&#xff0c;使之能够构建智能生活小程序&#xff0c;运行在你的 IoT App 上。 准备工作 在集成 MiniApp SDK 之前&#xff0c;您需要在 涂鸦 IoT 开发平台 上&#xff1a; 注册开发者账号、创建产品、创建功能点等。…

汇智知了堂:AIGC引领数字营销新革命,你准备好了吗?

ChatGPT火了&#xff0c;带火的还有AIGC。 经过长时间的蓄力&#xff0c;AIGC技术落地的应用场景呈平铺式展开&#xff0c;已经逐渐渗透到各行各业中&#xff0c;AIGC时代已来。 AIGC浪潮来袭&#xff0c;电商行业从业者该如何应对&#xff1f; AIGC技术会给电商行业带来哪些变…

排序算法—快速排序

文章目录 快速排序一、递归实现二、非递归实现总结 快速排序 以下均以排升序为最终目的。 一、递归实现 有一个排序能解决所有问题吗&#xff1f;没有&#xff01;不过&#xff0c;快速排序这种排序适用于大多数情况。 我们前面讨论排序算法一般都是先讨论一趟的情况&#…

传输层 --- TCP (上篇)

目录 1. TCP 1.1. TCP协议段格式 1.2. TCP的两个问题 1.3. 如何理解可靠性 1.4. 理解确认应答机制 2. TCP 报头中字段的分析 2.1. 序号和确认序号 2.1.1. 序号和确认序号的初步认识 2.1.2. 如何正确理解序号和确认序号 2.2. TCP是如何做到全双工的 2.3. 16位窗口大小…

TypeScript系列之-理解TypeScript类型系统画图讲解

TypeScript的输入输出 如果我们把 Typescript 编译器看成一个黑盒的话。其输入则是使用 TypeScript 语法书写的文本或者文本集合。 输出是编译之后的 JS 文件 和 .d.ts 的声明文件 其中 JS 是将来需要运行的文件(里面是没有ts语法&#xff0c;有一个类型擦除的操作)&#xff0…

Shopee虾皮100%有效提高广告效果的案例分享

Shopee 店铺运营中存在三种广告类型&#xff0c;分别是:关键词广告、关联广告和店铺广告。其中使用最为普遍&#xff0c;主控权最为直接的就是关键词广告&#xff0c;TA的适用范围最广&#xff0c;起效最快&#xff0c;并且可根据自身运营的能力去调控投入产出比&#xff0c;深…

【Python】基础(专版提升1)

Python基础 1. 导学1.1 学习理念1.1.1 弱语法&#xff0c;重本质1.1.2 是技术&#xff0c;更艺术 1.2 学习方法1.2.1 当天知识必须理解 2. Python 简介2.1 计算机基础结构2.1.1 硬件2.1.2 软件 2.2 基础知识2.2.1 Python介绍2.2.1.1定义2.2.1.2优势2.2.1.3从业岗位 2.2.2 Pytho…