如何使用 C 语言的字符串库

目录汇总:C 语言零基础入门教程

一些编程语言提供的运算符可以对字符串进行复制、比较、拼接、选择子串等操作,但 C 语言的运算符根本无法操作字符串。在 C 语言中把字符串当作数组来处理,因此对字符串的限制方式和对数组的一样,特别是它们都不能用 C 语言的运算符进行复制和比较操作。

注意

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

1
char str1[10], str2[10];

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

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

C 语言用数组名作为指针 可知,把数组名用作 = 的左操作数是非法的。但是,使用 = 初始化字符数组是合法的:

1
char str1[10] = "abc";

这是因为在声明中,= 不是赋值运算符。

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

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

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

幸运的是,字符串的所有操作功能都没有丢失:C 语言的函数库为完成对字符串的操作提供了丰富的函数集。这些函数的原型驻留在 <string.h> 头中,所以需要字符串操作的程序应该包含下列内容:

1
#include <string.h>

<string.h> 中声明的每个函数至少需要一个字符串作为实际参数。字符串形式参数声明为char *类型,这使得实际参数可以是字符数组、char * 类型的变量或者字面串——上述这些都适合作为字符串。然而,要注意那些没有声明为 const 的字符串形式参数。这些形式参数可能会在调用函数时发生改变,所以对应的实际参数不应该是字面串。

<string.h> 中有许多函数,这里将介绍几种最基本的。在后续的例子中,假设 str1str2 都是用作字符串的字符数组。

一、strcpy 函数

strcpy(字符串复制)函数在 <string.h> 中的原型如下:

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

strcpy 函数把字符串 s2 复制给字符串 s1。(准确地讲,应该说是“strcpy 函数把 s2 指向的字符串复制到 s1 指向的数组中”。)也就是说,strcpy 函数把 s2 中的字符复制到 s1 中,直到遇到 s2 中的第一个空字符为止(该空字符也需要复制)。strcpy 函数返回 s1(即指向目标字符串的指针)。这一过程不会改变 s2 指向的字符串,因此将其声明为 const

strcpy 函数的存在弥补了不能使用赋值运算符复制字符串的不足。例如,假设我们想把字符串 "abcd" 存储到 str2 中,不能使用下面的赋值:

1
str2 = "abcd";            /*** WRONG ***/

这是因为 str2 是数组名,不能出现在赋值运算的左侧。但是,这时可以调用 strcpy 函数:

1
strcpy(str2, "abcd");     /* str2 now contains "abcd" */

类似地,不能直接把 str2 赋值给 str1,但是可以调用 strcpy

1
strcpy(str1, str2);       /* str1 now contains "abcd" */

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

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

注意

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

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

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

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

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

第二条语句确保 str1 总是以空字符结束,即使 strncpy 没能从 str2 中复制到空字符。

二、strlen 函数

strlen(求字符串长度)函数的原型如下:

1
size_t strlen (const char *s);

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

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

1
2
3
4
5
6
int len;

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

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

三、strcat 函数

strcat(字符串拼接)函数的原型如下:

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

strcat 函数把字符串 s2 的内容追加到字符串 s1 的末尾,并且返回字符串 s1(指向结果字符串的指针)。

下面列举了一些使用 strcat 函数的例子:

1
2
3
4
5
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 函数的返回值。下面的例子说明了可能使用返回值的方法:

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

注意

如果 str1 指向的数组没有大到足以容纳 str2 指向的字符串中的字符,那么调用 strcat(str1, str2) 的结果将是不可预测的。考虑下面的例子:

1
2
3
char str1[6] = "abc";

strcat(str1, "def");    /*** WRONG ***/

strcat 函数会试图把字符 def\0 添加到 str1 中已存储的字符串的末尾。不幸的是,str1 仅限 6 个字符,这导致 strcat 函数写到了数组末尾的后面。

strncat 函数函数比 strcat 更安全,但速度也慢一些。与 strncpy 一样,它有第三个参数来限制所复制的字符数。下面是调用的形式:

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

strncat 函数会在遇到空字符时终止 str1,第三个参数(待复制的字符数)没有考虑该空字符。在上面的例子中,第三个参数计算 str1 中的剩余空间(由表达式 sizeof(str1) – strlen(str1) 给出),然后减去 1 以确保为空字符留下空间。

四、strcmp 函数

strcmp(字符串比较)函数的原型如下:

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

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

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

为了检查 str1 是否小于或等于 str2,可以写

1
2
if (strcmp(str1, str2) <= 0)   /* is str1 <= str2? */
  ...

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

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

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

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

  • A~Z、a~z、0~9 这几组字符的数值码是连续的。
  • 所有的大写字母都小于小写字母。(在 ASCII 码中,65~90 的编码表示大写字母,97~122 的编码表示小写字母。)
  • 数字小于字母。(48~57 的编码表示数字。)
  • 空格符小于所有打印字符。(ASCII 码中空格符的值是 32。)

程序 显示一个月的提醒列表

为了说明 C 语言字符串函数库的用法,现在来看一个程序。这个程序会显示一个月的每日提醒列表。用户需要输入一系列提醒,每条提醒都要有一个前缀来说明是一个月中的哪一天。当用户输入的是 0 而不是有效的日期时,程序会显示出输入的全部提醒的列表,并按日期排序。下面是与程序的会话示例:

Enter day and reminder:  24 Susan's birthday
Enter day and reminder:  5 6:00 - Dinner with Marge and Russ
Enter day and reminder:  26 Movie - "Chinatown"
Enter day and reminder:  7 10:30 - Dental appointment
Enter day and reminder:  12 Movie - "Dazed and Confused"
Enter day and reminder:  5 Saturday class
Enter day and reminder:  12 Saturday class
Enter day and reminder:  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"

总体策略不是很复杂:程序需要读入一系列日期和提醒的组合,并且按照顺序进行存储(按日期排序),然后显示出来。为了读入日期,会用到 scanf 函数。为了读入提醒,会用到 C 语言字符串的读和写 介绍的 read_line 函数。

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

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

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

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

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

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

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

解决了上述细节问题之后,程序编写如下:

remind.c

 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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/* 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;
}

虽然程序 remind.c 很好地说明了 strcpy 函数、strcat 函数和 strcmp 函数,但是作为实际的提醒程序,它还缺少一些东西。该程序显然有许多需要完善的地方,从小调整到大改进都有(例如,当程序终止时把提醒保存到文件中)。

请参阅

(完)

comments powered by Disqus