C 语言 —— 字符串

C/C++ 专栏收录该内容
33 篇文章 0 订阅

一、什么是字符串?

  定义: 字符串是以空字符(\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() 来自定义输入输出函数。

五、字符串函数

  C库提供了多个处理字符串的函数,ANSI C 把这些函数的原型放在 string.h 头文件中。其中最常用的函数有 strlen()、strcat()、strcmp()、 strncmp()、strcpy() 和 strncpy()。另外,还有 sprintf() 函数,其原型在 stdio.h头文件中。

strlen()

  全称 string length,即 strlen() 函数用于统计字符串的长度。

PS:最终统计的字符串长度不包括空字符。

  原型: size_t strlen(const char*);
  用法: 接受一个字符串参数,返回该字符串的长度。

strcat()

  全称 string catenate(连接),即 strcat() 函数用于字符串拼接。
  原型: char* strcat(char*, const char*);
  用法: 接受两个字符串作为参数,该函数把第 2 个字符串的备份附加在第 1 个字符串末尾,并把拼接后形成的新字符串作为 第 1 个字符串,第 2 个字符串不变。返回第 1 个参数,即拼接第 2 个字符串后的第 1 个字符串的地址。
  注意: 1. 第二个参数的第一个字符替换第一个参数的空字符; 2. 第二个参数指向的字符串拼接到第一个参数指向的字符串之后,在最后添加一个空字符。

strncat()

  学习过 gets() 之后,我们很容易会想到 strcat() 有同样的缺陷 —— 无法保证第一个数组能容纳第二个字符串。如果分配给第 1 个数组的空间不够大,多出来的字符溢出到相邻存储单元时就会出问题。

  解决方法:

  1. 使用 strlen() 来检测是否可以容纳。
  2. 使用 strncat() 来拼接。

  原型: char* strncat(char*, const char*, size_t);
  用法: 该函数的第 3 个参数指定了最大添加字符数。例如 strncat(bugs, addon, 13) 将把 addon 字符串的内容附加给 bugs,在加到第 13 个字符或遇到空字符时停止。
  注意: 第三个参数的设置为 第一个数组的大小 - 1 - 第一个字符串大小

  第二个字符串的第1个字符将覆盖第一个字符串末尾的空字符。不会拷贝第二个字符串中空字符和其后的字符,并在拷贝字符的末尾添加一个空字符。
  因此,拼接后一定是字符串(一定包含空字符)。

strcmp

  全称:string compare,用于比较字符串的内容。

  Q:为什么要用 strcmp() ?
  A: 因为对于字符串 str1 和字符串 str2 来说, str1 == str2 永远是比较的地址,而不是字符串的内容。需要比较字符串内容的时候就要使用 strcmp() 了。
  原型: int strcmp(const char*, const char*);
  用法: 该函数通过比较运算符来比较字符串,就像比较数字一样。如果两个字符串参数相同,该函数就返回 0,否则返回非零值。

  关于返回值的问题,如果第一个参数的在第二参数的后面,返回整数;如果第一个参数和第二个参数下相同,返回0;如果第一个参数在第二个参数前面,返回负数。

stcncmp

  原型: int strncmp(const char*, const char*, size_t);
  用法: 第三个参数指定两个字符串比较的字符数。

strcpy

  Q:为什么要有 strcpy() ?
  A: 和字符串比较相似,str1 = str2 是将字符串 str2 的首地址赋值给 str1,即 str1 和 str2 都指向了同一个字符串,此时内存中只有一个字符串,并没有复制字符串。
  原型: char* strcpy(char*, const char*);
  用法: 将第二个参数指向的字符串拷贝给第一个参数指向的数组中,返回值为第一个参数。

strncpy

  strcpy() 也存在缺陷:第一个参数不一定可以容纳下第二个参数指向的字符串。
  原型: char* strncpy(char*, const char*, size_t);
  用法: 第 3 个参数指明可拷贝的最大字符数。
  注意: strncpy(target, source, n) 是把 source 中的 n 个字符或空字符之前的字符拷贝至 target 中。因此,如果 source 中的字符数小于n,则拷贝整个字符串,包括空字符。但是,strncpy() 拷贝字符串的长度不会超过 n,如果拷贝到第 n 个字符时还未拷贝完整个源字符串,就不会拷贝空字符。所以,拷贝的副本中不一定有空字符。鉴于此,该程序把 n 设置为比目标数组大小少 1(TARGSIZE-1),然后把数组最后一个元素设置为空字符。

sprinf()

  注意: 前面的函数都是在 string.h 这个头文件中,而 sprinf() 函数则是声明在 stdio.h 中。
  功能: 该函数和 prinf() 类似,但是它将数据写入字符串,而不是打印在显示器上。

例如:

// 将后面的字符串写入到 formal 中
sprintf(formal, "%s, %-5s: $%6.2f\n", last, first, prize);
  • 0
    点赞
  • 1
    评论
  • 1
    收藏
  • 一键三连
    一键三连
  • 扫一扫,分享海报

打赏
文章很值,打赏犒劳作者一下
相关推荐
©️2020 CSDN 皮肤主题: 技术工厂 设计师:CSDN官方博客 返回首页

打赏

柚咖

你的鼓励将是我创作的最大动力

¥2 ¥4 ¥6 ¥10 ¥20
输入1-500的整数
余额支付 (余额:-- )
扫码支付
扫码支付:¥2
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、C币套餐、付费专栏及课程。

余额充值