在 Unit 1 中简单介绍了字符串和字符数组的概念,这里先复习一下之前讲解的字符串的知识,然后再详细学习一下字符串。

一、什么是字符串?

定义: 字符串是以空字符(\0)结尾的字符(char)数组

  字符串是一种特殊的字符数组,特殊在字符串是以空字符 ‘\0’ 结尾上,这样只需要给出字符串的起始地址,编译器就可以知道字符串的范围是从起始地址到空字符,不需要我们再指出数组长度,或者结束地址了。

因为字符串是一种字符数组,因此数组和指针的知识都可以运用到字符串上。但是字符串实在是太常用了,因此 C 提供了很多用于处理字符串的函数,这些函数基本都是需要掌握的,因为这些函数都是可以大大提高我们处理字符串的效率。

二、字符串的声明及初始化

在 C 语言中,可以使用多种方法声明字符串,但是无论哪种方法,都需要确保程序有足够的空间储存字符串

1. 字符串常量(字面量)

定义: 双引号括起来的内容成为字符串常量(String Constant),也叫做字符串字面量(String Literal)。例如,"I am a string constant." 就表示一个字符串。
其实之前我们就已经接触了字符串常量了,只是我们当时并没有学到字符串的知识,printf() 和 scanf() 函数中的 "" 括起来的内容就是字符串。

之前说过,字符串以空字符(\0)结尾,但是在字符串常量的双引号中并没有空字符,这是因为编译器会对双引号中的字符自动的在末尾加上空字符(\0),不需要我们显式的在字符串末尾加上空字符。
也就是说,上面的 “I am a string constant.”,在存储的时候被保存为 “I am a string constant.\0”。

注意1:从 ANSI C 标准起,如果字符串常量之间没有间隔,或者用空白字符分隔,C 会将其视为串联起来的字符串常量。

例如下面的两种写法是等价的。

  1. char greeting1[50] = "Hello, and"" how are" " you" " today!";
  2. char greeting2[50] = "Hello, and how are you today!";

虽然是等价的,但是为了程序的可读性,推荐使用的还是第二种。

注意2: 因为字符串是由双引号包裹起来的,如果想要在字符串内部使用双引号,必须用反斜杠 \ 进行转义。
例如,希望输出的字符串是 “Hello, World!”

  1. printf("Hello, World!"); // 输出的只是 Hello, World!
  2. printf("\"Hello, World!\""); // 输出的才是 "Hello, World!"
  3. printf("Hel\"lo, Wor\"ld!"); // 输出的是 Hel"lo, Wor"ld!

注意3:字符串常量属于静态存储类别(static storage class),这说明如果在函数中使用字符串常量,该字符串只会被储存一次,在整个程序的生命期内存在,即使函数被调用多次。用双引号括起来的内容被视为指向该字符串储存 位置的指针。这类似于把数组名作为指向该数组位置的指针。

  1. printf("%s, %p, %c\n", "We", "are", *"space farers");
  2. // 输出:"We",0x100000f61,s

2. 字符串数组和初始化

定义字符串数组时,必须让编译器知道需要多少空间。
一种方法是,用足够空间的数组储存字符串。
另一种方法是,在声明字符串时初始化,由编译器自动计算数组大小。

2.1. 用足够空间储存字符串

演示1:用足够空间存储字符串。

  1. // 1. 数组初始化(最后必须加上'\0',否则只是字符数组,不是字符串)
  2. const char m1[40] = { 'L','i', 'm', 'i', 't', ' ', 'y', 'o', 'u', 'r',
  3. 's', 'e', 'l', 'f', ' ', 't', 'o', ' ', 'o', 'n', 'e', ' ','l', 'i',
  4. 'n', 'e', '\'', 's', ' ', 'w', 'o', 'r','t', 'h', '.', '\0' };
  5. // 2. 字符串常量初始化
  6. const char m2[40] = "Limit yourself to one line's worth.";

注意:

  1. 如果使用之前学习的字符数组初始化的方法,最后一定要加上空字符 '\0',否则只是字符数组,而不是字符串。
  2. 如果使用的是字符串常量来初始化的话,不需要在最后加空字符 '\0',因为编译器在处理字符串常量时,在存储时会自动在最后加上空字符,不需要我们手动加。
  3. 由之前数组知识可以知道 数组的大小 >= 字符数量,因为字符串是空字符结尾的字符数组,因此对于有 35 个字符(包括空格)的Limit yourself to one line's worth.来说,存储它的字符数组大小至少是 36(因为还有一个空字符)。
  4. 如果字符数组的大小大于字符串长度+1(字符串字符+空字符),剩余的位置上都会被自动初始化为空字符 \0
    4.3.1 字符串简介 - 图1

    2.2. 编译器自动计算数组的大小

    回忆一下在学习数组时,可以省略数组初始化声明中的大小,编译器会自动计算数组大小。

    1. int[] arr = {1, 3, 5, 7};// 编译器会自动计算数组大小为 4

    演示2:编译器自动计算数组大小。

    1. // 这个是字符数组,而不是字符串
    2. const char ch1[] = {'y', 'o', 'u', ' ', 'k', 'a'};// 编译器自动计算大小为 6
    3. // 这个是字符数组,也是字符串
    4. const char str[] = {'y', 'o', 'u', ' ', 'k', 'a', '\0'};// 编译器自动计算大小为 7
    5. // 这个是字符数组,也是字符串
    6. const char str[] = "you ka";// 编译器自动计算大小为 7

    注意:

  5. 让编译器计算数组的大小只能用在初始化数组时。如果创建一个稍后再填充的数组,就必须在声明时指定大小。

  6. 声明数组时,数组大小必须是可求值的整数。在 C99 新增变长数组之前,数组的大小必须是整型常量。
  7. 字符数组名和其他数组名一样,是该数组首元素的地址。

    3. 指针表示法创建字符串

    还可以使用指针表示法创建字符串。例如,下面两条声明语句几乎相同。
  1. const char* p1 = "you ka";
  2. const char arr1[] = "you ka";

注意:指针表示法和数组表示法来创建字符串只是几乎相同,还是有一定的区别的。
字符串 “you ka” 会保存在内存中,如果使用指针表示法,比如上面的 p1,那么 p1 指向的就是字符串 “you ka” 的起始地址。
这意味着,如果有 const char* p2 = "you ka";*p1 == *p2,此时内存中只有一份 “you ka” 字符串。
如果使用的是数组表示法,则会在内存中以 arr1 为首地址保存 “you ka” 的一个副本,此时内存中有两份 “you ka” 字符串。

4. 指针表示法和数组表示法的选择

如果要用数组表示一系列待显示的字符串,使用指针数组,因为它比二维字符数组的效率高。
如果要改变字符串或为字符串输入预留空间,不要使用指向字符串字面量的指针。

三、字符串输入

如果想要将一个字符串读入到程序,首先必须预留存储这个字符串的空间,然后用输入函数获取字符串。

1. 读取字符串的函数

函数 描述
scanf() 配合 %s 占位符使用,读取到空白字符,即可以读取一个单词。
gets() 读取到换行符,即可以读取一行。
fgets() 同 gets(),主要是作为 gets() 的替代品。

2. scanf

配合 %s 可以读取字符串,但是遇到空白符会停止读取,因此更像是一个“读取单词”的函数。
例如:

  1. char str[100];
  2. scanf("%s", str);
  3. printf("%s\n", str);

输入:you ka
输出:you

3. gets

scanf() 只能读取一个单词,但是在读取字符串的时候,往往需要一整行读取输入,而不仅仅是一个单词。gets() 这个函数就是用于处理这种情况的。

gets() 简单易用,读取整行输入,直到遇到换行符,然后丢弃换行符,存储其他字符,并在这些字符的末尾添加一个空字符,使其成为一个 C 字符串

  1. gets (char *__str)

但是,gets() 有一个缺陷,它的参数只有一个指针变量,无法检查数组是否装得下输入行。 因此,gets() 只知道数组的开始,并不知道数组有多少个元素。如果输入的字符串过长,会导致缓冲区溢出。

gets() 函数的不安全行为造成了 安全隐患。过去,有些人通过系统编程,利用 gets() 插入和运行一些破坏系统安全的代码。

4. fgets

因为 gets() 函数存在安全隐患,所以需要一个能够替代 gets() 的函数。过去通常用 fgets() 来代替 gets(),fgets() 函数稍微复杂些,在处理输入方面与 gets() 略有不同。

C11标准新增的 gets_s() 函数也可代替 gets()。该函数与 gets() 函数更接近,而且可以替换现有代码中的 gets()。但是,它是 stdio.h 输入/输出函数系列中的可选扩展,所以支持C11的编译器也不一定支持它。

fgets() 函数通过第2个参数限制读入的字符数来解决溢出的问题。该函数专门设计用于处理文件输入,所以一般情况下可能不太好用。

fgets() 和 gets() 的区别:

  1. fgets() 的第二个参数指明读入字符的最大数目。如果参数值为 n,那么 fgets() 将读入 n-1 个字符,或者遇到第一个换行符。
  2. fgets() 读到第一个换行符,会储存在字符串中,而 gets() 会丢弃换行符。
  3. fgets() 的第三个参数指明需要读入的文件,如果是从键盘输入的数据,则用 stdin 作为参数,该标识定义在 stdin.h 中。

    因为 fgets() 函数把换行符放在字符串的末尾,通常要与 fputs() 函数配对使用,如果用 puts() 输出的话,会多打印一个换行。

fgets() 储存换行符有好处也有坏处。坏处是你可能并不想把换行符储存在字符串中,这样的换行符会带来一些麻烦。好处是对于储存的字符串而言,检查末尾是否有换行符可以判断是否读取了一整行。如果不是一整行,要妥善处理一行中剩下的字符。

5. 小结

函数 功能 备注
int scanf (const char, …) 配合占位符使用来读取键盘输入的数据。返回成功匹配和赋值的个数。 如果到达文件末尾或发生读错误,则返回 EOF。
遇到空白字符停止。
char gets(char str) 从标准输入 stdin 读取一行,并把它存储在 str 所指向的字符串中。如果成功,该函数返回 str;如果发生错误或者到达文件末尾时还未读取任何字符,则返回 NULL。 读取到换行符停止,不会保存换行符
并不安全,没有考虑字符数组手否足够容纳字符串。
char fgets(char str, int n, FILE* stream) 从指定的流 stream 读取一行。如果读取成功,返回相同的 str 字符串;如果到达文件末尾或者没有读取到任何字符,str 的内容保持不变,并返回一个空指针。 读取到换行符,或者读取 n-1 字符时停止,会保存换行符
gets_s(char*, int) gets() 的安全版,通过第二个参数控制读取的字符串长度。当字符串长度超过第二个参数,会丢弃。 不会保存换行符。可选项,并不是每个编译器都会支持。

四、字符串输出

函数 参数 功能 备注
printf() 不定参数 格式化输出不同的数据类型。 不会添加换行符,执行时机更长,但更灵活。
puts() 一个参数,字符串地址 打印字符串。遇到空字符停止输出。 打印完字符串,会添加一个换行符。
需要保证有空字符。
fputs() 两个参数,字符串地址、输出流 向指定输出流打印字符串。 打印完字符串,不会添加换行符。
需要保证有空字符。

还可以用 getchar(),putchar() 来自定义输入输出函数。