文章目录

  • 一、什么是字符串?
  • 二、字符串的声明及初始化
  • 1. 字符串常量(字面量)
  • 2. 字符串数组和初始化
  • 2.1. 用足够空间储存字符串
  • 2.2. 编译器自动计算数组的大小
  • 3. 指针表示法创建字符串
  • 4. 指针表示法和数组表示法的选择
  • 三、字符串输入
  • 1. 读取字符串的函数
  • 2. scanf
  • 3. gets
  • 4. fgets
  • 5. 小结
  • 四、字符串输出


一、什么是字符串?

  定义: 字符串是以空字符(\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 会将其视为串联起来的字符串常量。

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

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

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

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

  例如,希望输出的字符串是 “Hello, World!”

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

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

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

2. 字符串数组和初始化

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

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

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

// 1. 数组初始化(最后必须加上'\0',否则只是字符数组,不是字符串)
const char m1[40] = { 'L','i', 'm', 'i', 't', ' ', 'y', 'o', 'u', 'r', 
's', 'e', 'l', 'f', ' ', 't', 'o', ' ', 'o', 'n', 'e', ' ','l', 'i', 
'n', 'e', '\'', 's', ' ', 'w', 'o', 'r','t', 'h', '.', '\0' };
// 2. 字符串常量初始化
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

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

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

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

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

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

  注意:

  1. 让编译器计算数组的大小只能用在初始化数组时。如果创建一个稍后再填充的数组,就必须在声明时指定大小。
  2. 声明数组时,数组大小必须是可求值的整数。在 C99 新增变长数组之前,数组的大小必须是整型常量。
  3. 字符数组名和其他数组名一样,是该数组首元素的地址。

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

  还可以使用指针表示法创建字符串。

例如,下面两条声明语句几乎相同。

const char* p1 = "you ka";
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 可以读取字符串,但是遇到空白符会停止读取,因此更像是一个“读取单词”的函数。
  例如:

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

输入:you ka
输出:you

3. gets

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

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

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() 来自定义输入输出函数。