专题十二、字符串

字符串

  • 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 Reminder5 Saturday class5 6:00 - Dinner with Marge and Russ7 10:30 - Dental appointment12 Saturday class12 Movie - "Dazed and Confused"24 Susan's birthday26 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 9int 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.mzph.cn/news/803828.shtml

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

相关文章

【Python系列】pydantic版本问题

&#x1f49d;&#x1f49d;&#x1f49d;欢迎来到我的博客&#xff0c;很高兴能够在这里和您见面&#xff01;希望您在这里可以感受到一份轻松愉快的氛围&#xff0c;不仅可以获得有趣的内容和知识&#xff0c;也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…

jdk和Eclipse软件安装与配置(保姆级别教程)

目录 1、jdk的下载、安装、配置 1.1 jdk安装包的的下载地址&#xff1a;Java Archive | Oracle &#xff0c;点击进入&#xff0c;然后找到你想要的版本下载&#xff0c;如下图&#xff1a; 2.1 开始下载&#xff0c;如下图&#xff1a; 3.1 登入Oracle账号就可以立即下载了…

Docker 搭建私有镜像仓库

一、镜像仓库简介 Docker的镜像仓库是一个用于存储和管理Docker镜像的中央位置。镜像仓库的主要作用是提供一个集中的地方&#xff0c;让用户可以上传、下载、删除和共享Docker镜像。镜像仓库又可以分为公共镜像仓库和私有仓库镜像仓库&#xff1a; 公共镜像仓库 Docker Hub 是…

java Web在线考试管理系统用eclipse定制开发mysql数据库BS模式java编程jdbc

一、源码特点 JSP 在线考试管理系统是一套完善的web设计系统&#xff0c;对理解JSP java 编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。开发环境为 TOMCAT7.0,eclipse开发&#xff0c;数据库为Mysql5.0&#xff0c;使…

网络网络层之(7)PPPOE协议

网络网络层之(7)PPPOE协议 Author: Once Day Date: 2024年4月7日 一位热衷于Linux学习和开发的菜鸟&#xff0c;试图谱写一场冒险之旅&#xff0c;也许终点只是一场白日梦… 漫漫长路&#xff0c;有人对你微笑过嘛… 全系列文档可参考专栏&#xff1a;通信网络技术_Once-Day…

LeetCode-94(二叉树的中序遍历)

1.递归 时间复杂度O(n) public List<Integer> inorderTraversal(TreeNode root) {List<Integer> res new ArrayList<>();accessTree(root,res);return res;}public void accessTree(TreeNode root,List<Integer>res){if(root null){return;}accessT…

最新剧透前沿信息GPT-5或将今年发布

GPT2 很糟糕 &#xff0c;GPT3 很糟糕 &#xff0c;GPT4 可以 &#xff0c;但 GPT5 会很好。 PS:GPT2 很糟糕,3 很糟糕,4 可以,5 很可以。 如果想升级GPT4玩玩&#xff0c;地址 今年发布的具有推理功能的 GPT5不断发展&#xff0c;就像 iPhone 一样 Sam Altman 于 17 日&am…

OpenAI曾转录100万小时视频数据,训练GPT-4

4月7日&#xff0c;纽约时报在官网发布了一篇名为《科技巨头如何挖空心思&#xff0c;为AI收集数据》的技术文章。 纽约时报表示&#xff0c;OpenAI曾在2021年几乎消耗尽了互联网有用的文本数据源。为了缓解训练数据短缺的难题&#xff0c;便开发了知名开源语音识别模型Whispe…

019——IIC模块驱动开发(基于EEPROM【AT24C02】和I.MX6uLL)

目录 一、 IIC基础知识 二、Linux中的IIC&#xff08;韦东山老师的学习笔记&#xff09; 1. I2C驱动程序的层次 2. I2C总线-设备-驱动模型 2.1 i2c_driver 2.2 i2c_client 三、 AT24C02 介绍 四、 AT24C02驱动开发 实验 驱动程序 应用程序 一、 IIC基础知识 总线类…

Idea中 maven 下载jar出现证书问题

目录 1&#xff1a; 具体错误&#xff1a; 2&#xff1a; 忽略证书代码&#xff1a; 3&#xff1a; 关闭所有idea&#xff0c; 清除缓存&#xff0c; 在下面添加如上忽略证书代码 4&#xff1a;执行 maven clean 然后刷刷新依赖 完成&#xff0c;撒花&#xff01;&#x…

A Learning-Based Approach for IP Geolocation

下载地址:Towards IP geolocation using delay and topology measurements | Proceedings of the 6th ACM SIGCOMM conference on Internet measurement 被引次数:185 Abstract 定位IP主机地理位置的能力对于在线广告和网络攻击诊断等应用程序是非常吸引力的。虽然先前的方…

[Kubernetes集群:master主节点初始化]:通过Calico和Coredns网络插件方式安装

文章目录 前置&#xff1a;Docker和K8S安装版本匹配查看0.1&#xff1a;安装指定docker版本 **[1 — 7] ** [ 配置K8S主从集群前置准备操作 ]一&#xff1a;主节点操作 查看主机域名->编辑域名->域名配置二&#xff1a;安装自动填充&#xff0c;虚拟机默认没有三&#xf…

深度学习-多尺度训练的介绍与应用

一、引言 在当今快速发展的人工智能领域&#xff0c;多尺度训练已经成为了一种至关重要的技术&#xff0c;特别是在处理具有复杂结构和不同尺度特征的数据时。这种技术在许多应用中发挥着关键作用&#xff0c;例如图像识别、自然语言处理和视频分析等。 多尺度训练的定义 多尺…

「44」直播间换脸,揭开神秘的面纱……

「44」换脸神器 让你瞬间秒变「明星脸」带货 DeepFace是Facebook的人脸识别系统之一&#xff0c;旨在在照片和视频中准确识别和标识人脸。它使用深度学习和神经网络技术来进行高度精确的人脸匹配和验证。 DeepFace利用了大量的训练数据和先进的人脸识别算法&#xff0c;能够…

Word 画三线表模板---一键套用

1、制作三线表 1&#xff09;设置为无边框 选中表格&#xff0c;点击「右键」——「边框」——「无框线」。 2&#xff09;添加上下边框线 选中表格后&#xff0c;点击【右键】——【表格属性】——【边框和底纹】&#xff0c;边框线选择【1.5磅】&#xff0c;然后点击【上框…

【数组】【最长距离】使循环数组所有元素相等的最少秒数

本文涉及知识点 数组 最长距离 LeetCode2808. 使循环数组所有元素相等的最少秒数 给你一个下标从 0 开始长度为 n 的数组 nums 。 每一秒&#xff0c;你可以对数组执行以下操作&#xff1a; 对于范围在 [0, n - 1] 内的每一个下标 i &#xff0c;将 nums[i] 替换成 nums[i] …

react17+18 中 setState是同步还是异步更新

在类组件中使用setState&#xff0c;在函数式组件中使用hooks的useState。 setstate目录 1. 类组件1.1 react 17版本1.2 react 18版本 2、函数式组件 1. 类组件 1.1 react 17版本 参考内容&#xff1a;第十一篇&#xff1a;setState 到底是同步的&#xff0c;还是异步的&…

Selenium+Chrome Driver 爬取搜狐页面信息

进行selenium包和chromedriver驱动的安装 安装selenium包 在命令行或者anaconda prompt 中输入 pip install Selenium 安装 chromedriver 先查看chrome浏览器的版本 这里是 123.0.6312.106 版 然后在http://npm.taobao.org/mirrors/chromedriver/或者https://googlechrom…

EasyPOI复杂表格导入

EasyPOI复杂表格导入 多表头数据导入方式一导入表格实体类文件导入代码测试结果 方式二导入表格实体类文件导入代码测试结果 总结 设置表格从哪行读取表格内容 多表头数据导入 方式一 导入的表格样式如下 导入表格实体类 package com.demo.entity;import cn.afterturn.eas…

基于令牌桶算法对高并发接口的优化

业务背景 项目中有一个抽奖接口&#xff0c;此接口需要处理高并发问题以及使用脚本作弊的问题。 本文主要探讨如何最大程度地减少脚本作弊行为对抽奖业务的影响。 设计思路 如何减少脚本作弊行为对抽奖业务的影响 使用令牌桶算法&#xff0c;对频率过高的用户请求进行拦截 …