文章目录
- 一、什么是字符串?
- 二、字符串的声明及初始化
- 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.";
注意:
- 如果使用之前学习的字符数组初始化的方法,最后一定要加上空字符
'\0'
,否则只是字符数组,而不是字符串。 - 如果使用的是字符串常量来初始化的话,不需要在最后加空字符
'\0'
,因为编译器在处理字符串常量时,在存储时会自动在最后加上空字符,不需要我们手动加。 - 由之前数组知识可以知道 数组的大小 >= 字符数量,因为字符串是空字符结尾的字符数组,因此对于有 35 个字符(包括空格)的
Limit yourself to one line's worth.
来说,存储它的字符数组大小至少是 36(因为还有一个空字符)。 - 如果字符数组的大小大于字符串长度+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
注意:
- 让编译器计算数组的大小只能用在初始化数组时。如果创建一个稍后再填充的数组,就必须在声明时指定大小。
- 声明数组时,数组大小必须是可求值的整数。在 C99 新增变长数组之前,数组的大小必须是整型常量。
- 字符数组名和其他数组名一样,是该数组首元素的地址。
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() 的区别:
- fgets() 的第二个参数指明读入字符的最大数目。如果参数值为 n,那么 fgets() 将读入 n-1 个字符,或者遇到第一个换行符。
- fgets() 读到第一个换行符,会储存在字符串中,而 gets() 会丢弃换行符。
- 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() 来自定义输入输出函数。