C语言之字符串

C语言之字符串

字符串字面值

用双引号括起来的内容称为字符串字面量,也叫做字符串常量。双引号中的字符和编译器自动加入末尾的\0字符,都作为字符串储存在内存中。

字符串字面值的存储方式

我们已经知道C语言将字符串当成字符数组来存储,具体而言:

长度为n的字符串字面值,会在虚拟内存的只读数据段中,分配n+1长度的字符数组,用来存储字符序列及一个终止的空字符’\0’。

比如一个字符串字面值"abc"是这样存储的:

字符串字面值被存储在只读数据段中,这意味着字符串字面值有以下特点:

  1. 不可变、只读性。
  2. 静态存储期限,生命周期与程序的整个执行时间相同。(数据段内存储的数据都具有静态存储期限)

总之,字符串字面值本质上就是一个存储在只读数据段当中的字符数组。注意:

所有的字符串都是以终止字符’\0’结尾的字符数组,但并不是所有的字符数组都可以视为有效的字符串。

于是:

字符串字面值在代码中的大多数场景下,可以直接视为一个字符数组的数组名使用,也就是视为字符数组的首元素指针去使用。比如你可以写出下面的代码:

// 以下场景中,"hello"在代码中会被视为数组名,也即首元素指针

// 指针p指向数据段中的只读字符串字面值数组,此时指针p无法修改指向内容
char* p = "hello";

// &"hello"在标准C语言中是不允许的,因为"hello"本身就是地址,无需再取地址。不要这么做!
// char *p2 = &"hello";

// 既然被视为数组名,就支持索引运算。字符c此时是'o'
char c1 = "hello"[4];

// 指针p_element指向字符'o'
char* p_element = "hello" + 4;

// 索引运算就是算术运算和解引用运算的语法糖。字符c此时是'o'
char c2 = *("hello" + 4);

同样的,在以下场景中,字符串字面值无法视为视为首元素指针:

sizeof运算符。sizeof("hello")是计算此字符数组的大小,也就是字符串的长度 + 1,这里的结果是6。

字符串字面值的操作

字符串字面值既不能指向一个新的字符数组,指向的内容也不可修改,

之所以有这样的特点,原因在于:

  1. 数组名视为指针时就是一个不可改变指向的指针,该指针固定指向首元素地址,不可更改。
  2. 字符串字面值存储在只读数据段中,具有只读性,所以内容也不可变。任何尝试修改字符串字面值内容的操作都会引发未定义行为!

除此之外,我们还将演示两种字符串字面值操作的场景:

  1. 用指针变量接收字符串字面值
  2. 将字符串字面值作为参数传递

这两种场景中,字符串字面值在代码中都会被视为数组名,充当"首元素指针"。

字符串变量

C语言并没有专门的、独立的字符串类型,只是规定任何一维的字符数组都可以用来存储字符串,只要保证以空字符 \0 结尾即可。

这种设计看起来很简单,但为字符串的实际使用带来了很大的困难。主要体现在以下几点:

  1. 无法确定一个字符数组是否表示字符串。
  2. 编写处理字符串的函数,需要正确处理空字符’\0’。
  3. C语言的字符串不可能包含空字符,因为它被视为结束符。
  4. 字符串本身并不会自带长度信息,C语言中获取字符串的长度,最简单的方式就是遍历整个字符串,这个效率是不高的,尤其是长字符串时。(但求字符串长度几乎是字符串最常用的操作)

字符串变量的声明

  1. 以字符数组为载体,所以声明字符串变量实际也就是声明字符数组,但会稍有区别,多出来的一个位置用来存储空字符’\0’。
  2. 关于使用指针声明字符串数组名大多数情况下可以视为首元素指针,所以用字符指针类型来声明字符串类型,也是可以的

字符串变量的初始化

数组长度可不可以小于字符串长度 + 1呢?

可以这么做,但此时的字符数组由于没有存储’\0’,这样的字符数组是不能作为字符串使用的。所以一定要确保字符数组的长度大于字符串的长度,至少大1个长度。

字符串变量和字符指针的初始化

为了加深大家对字符串变量的理解,将它完全和字符串字面值区分开。我们简单比较一下两个非常相似的初始化语法——字符串变量和字符指针的初始化。

// main:
char str[] = "Showmaker";
char *p = "Showmaker";

它们的区别在于:

  1. str是一个字符串变量,也就是一个字符数组。此初始化中,"Showmaker"是一个初始化式的简写形式,不应将它理解成一个字符串字面值。str数组是一个在栈上创建的独立的新数组,借助"Showmaker"这个初始化式,完成初始化。此数组的内容是完全可变的!
  2. p是一个指向字符的指针变量。此时"Showmaker"就需要理解成字符串字面值。p指针指向的是只读数据段中,存储字符串字面值的字符数组的首元素地址。字符串字面值是不可变的!
str[4] = 'M';  // 该代码是可行的,修改后的字符串变为"ShowMaker"
p[4] = 'M';   // 这样的代码会导致未定义行为,字符串字面值是只读的

字符串变量的读写

在字符串处理的场景下:

  1. 读意味着编写代码,调用函数从键盘录入、文件等数据源中输入数据。
  2. 写意味着编写代码,调用函数将数据输出到显示器、文件等目的地,输出数据。

字符串字面值是不可修改的,所以它们只能进行写的操作,无法实现读。

写字符串

所谓写字符串,也就是将整个字符串遍历,逐个输出字符或者整个输出。字符串变量和字面值都可以进行写的操作。

  1. 如果是逐个输出,一般需要循环遍历,

  2. 当然更常见的,我们会整个输出字符串

  3. 如果希望输出字符串的一部分,可以使用转换说明中的**%.ps**来控制,其中p是一个正整数,用于确定要显示的字符。

  4. 为了更方便快捷的打印字符串,通过包含头文件stdio.h,我们就可以使用一个C标准库函数——puts函数。

注意:

  1. puts() 只能用于输出字符串,而 printf() 可用于输出各种数据类型。
  2. puts() 在字符串后自动添加换行符,而 printf() 则不会,除非明确添加了 \n。
  3. puts() 通常比 printf() 快,因为它不需要解析格式字符串。
  4. printf() 显然更加灵活,因为它支持格式化输出。

scanf读字符串变量

注意:

  1. scanf函数传参时,转换说明后面的参数需要传地址(指针变量),但str数组名作为参数传递时会退化为指针。切记不要写成"&str"!!

  2. scanf函数读键盘录入字符串的特点是:

    跳过前面的所有空白字符,从第一个非空字符开始读取,遇到第一个空白字符(空格,制表等)结束,中间的内容都是录入的内容。

    如果键盘录入内容:

    ​ abcd ef

    那么录入的字符串内容实际是:

    abcd

  3. scanf函数在表示录入字符串时,会在录入结束后,在字符信息的后面自动存储一个空字符。

    比如上面得到的"abcd"字符串的字符数组,内容是:

    ‘a’, ‘b’, ‘c’, ‘d’, ‘\0’

  4. 利用scanf录入的字符串,永远不可能包含空白字符(空格,制表等),因为遇到空白字符它就会结束录入。

scanf函数是不安全的,采取以下做法让它更安全

scanf函数是不安全的,在录入字符串时它会自动在字符数组后面存储一个空字符。

这就意味着,最多只能录入数组长度-1的字符数量,若超出则会出现数组下标越界问题。你可以使用转换说明"%长度s"来限制最大读取的字符数量,其中长度可设定为数组长度-1的字面值。

例如上面代码中,字符数组长度是10,采用下列转换说明限制最多读取9个字符:

scanf("%9s", str);    // 限制最多读取9个字符

这种做法避免了将键盘输入读入字符数组时,发生越界访问非法内存区域的情况,是推荐的做法!!

gets读字符串变量

scanf函数无法录入空白字符,且它无法确保录入一整行的数据。为了实现这两个目标,我们可以使用标准库函数——gets(),使用它同样需要包含头文件stdio.h。

此函数的行为是:

  1. 传参传入一个字符数组用于存放待录入的字符信息
  2. gets函数会完全将一整行的键盘输入数据,作为字符信息录入目标数组当中,碰到换行符结束键盘录入,并且在目标数组的末尾自动存储一个空字符。
  3. 如果录入数据成功,函数会返回目标数组的指针。如果遇到文件结束(EOF)或读取错误,函数返回空指针。

注意事项:

  1. gets不会跳过前面的空白字符,也不会遇到空白字符停止录入。gets在碰到换行符时才会停止录入!总之就是完整的录入一整行键盘输入!
  2. gets函数使用方便且高效,但gets函数完全不能对录入数据的长度做出限制,相对于scanf,它更加的不安全。

推荐使用更安全的fgets函数替代gets函数

假如需要从标准输入流(stdin,也就是键盘录入)中读取一行字符数据,并存储在一个字符串变量中,建议使用fgets这个函数:

fgets(str, sizeof(str), stdin);   // str是一个存储数据的字符数组

该函数的行为是:

  1. 同样的录入一整行字符信息,不会跳过前面的空白字符,遇到空白字符也不会停止录入。碰到换行符才会停止录入!
  2. 若str数组的长度是10,此函数调用最多会将9个字符存储到字符数组中,因为会留一个位置存空字符’\0’。所以此函数是安全的,不会导致数组越界,访问非法数据。
  3. fgets函数和gets有一个非常大的不同是:
    1. fgets函数在容量充足的情况下,会将换行符’\n’存入字符数组,再存一个空字符。比如[ ‘h’, ‘e’, ‘l’, ‘l’, ‘o’, ‘\n’, ‘0’]
    2. 而gets函数则不会存储换行符,只会存储一个空字符。比如[ ‘h’, ‘e’, ‘l’, ‘l’, ‘o’, ‘0’]

fgets函数会将换行符存入数组的特点,在有些时候是比较讨厌的,比如:

  1. printf函数以"%s"打印字符串时,仍然会存在一个换行。
  2. 在比较两个字符串时,'\n’换行符也会参与比较,导致比较结果不正确。

总之,如果你不需要这个额外存入的换行符’\n’,可以用下列手段去掉它:

// str是fgets函数录入数据的一个字符串,空字符前可能存在一个换行符
int len = strlen(str);    // strlen获取当前字符串的长度,也就是空字符前面字符的个数

// 换行符如果有,那肯定是字符串最后一个有效字符
if (len > 0 && str[len - 1] == '\n') {
    str[len - 1] = '\0';
}

C语言字符串库

C 语言中的字符串处理与其他高级编程语言有所不同。在许多编程语言中,字符串被当作一个独立的类型,并提供了一套丰富的运算符来操作这些字符串,如复制、比较和拼接。

但在C语言中,字符串实际上就是以空字符’\0’结束的字符数组。这意味着常规的C运算符不能直接用于字符串操作。

但幸运的是,C 语言为我们提供了丰富的字符串处理相关的库函数,来帮助我们处理字符串相关的任务。

这些函数基本上,都声明在头文件string.h当中,所以使用它们需要包含这个头文件。在这里,我们不可能将所有的函数都逐一介绍,这里我们只介绍几种最基本的和最常用的。

strlen获取字符串长度

作用是:返回当前字符串的长度,也就是字符数组中空字符’\0’前面的字符数量。

int len;
char str[] = "abcd";
char str2[10] = "12345";
char str3[5] = { 'a','\0','c' };
len = strlen("abc");  /* len is now 3 */
len = strlen("");   /* len is now 0 */
len = strlen(str);    /* len is now 4 */
len = strlen(str2);   /* len is now 5 */
len = strlen(str3);   /* len is now 1 */

char str4[4] = "1234";
len = strlen(str4);   // str4并不能表示一个字符串,该函数调用会引发未定义行为

注意:

  1. 函数传参的字符数组,必须是一个能够表示字符串的字符数组。否则,函数调用会产生未定义行为。
  2. 此函数的形参类型是:const char *s。它表示函数传入的指针,是一个不能用于修改指向内容的指针。这是一种很好的编程实践,特别是在函数声明中,它说明函数不会修改传入的数据。事实上,该函数也确实只用于求字符串的长度,并不会尝试修改内容。

strcpy字符串复制

char *strcpy(char *dest, const char *src);

函数作用: strcpy函数会将 src(source,源数组) 中存储的以空字符 ‘\0’ 结束的字符串复制到 dest(destination,目的数组) 所指向的数组中。

也就是说,会从首字符开始逐个字符地从 src 复制字符,包括空字符在内,都会从dest首位置开始,复制到dest当中。

这个过程中,src数组是不会被修改的,所以它被const修饰。

总之,该函数调用后,dest 将包含 src 的完整副本。

函数返回值:

该函数会返回指向目标数组 dest 的指针。一般来说,该函数的返回值没有什么意义,推荐忽略它。但某些场景下,这个返回值允许strcpy 函数的调用,可以被嵌套使用或者用于更复杂的表达式中。

char src[] = "hello";
char dest[10];
char dest2[10];

// 可以直接利用printf、puts函数打印复制后的目标数组
puts(strcpy(dest, src));

// 更加复杂的函数调用
// 将src复制到dest,再将dest复制给dest2
strcpy(dest2, strcpy(dest, src));

注意:

  1. strcpy函数是不安全的,它并不会检查dest是否真的能够包含src数组。如果dest不够大,就会因数组越界而产生未定义行为。
  2. 为了安全起见,可以考虑使用strncpy函数解决这一问题,虽然它可能效率更低一些。

更安全的字符串复制函数strncpy

strncpy函数,全名是string copy (n characters),即复制字符串的(n个字符)。该函数的声明如下:

char *strncpy(char *dest, const char *src, size_t n);

该函数会将最多n的字符从src中复制到dest数组中:

  1. 如果 n <= src字符串的长度,那么只有 src 的前 n 个字符会被复制到 dest 中,并且 dest 不会以空字符结尾。这种情况下,dest就无法表示一个字符串了,不过好在,它不会引起越界问题,是一个安全的操作。
  2. 如果 n 恰好是 src 字符串长度加上空字符(即 strlen(src) + 1)时,strncpy函数会将src完整复制到dest数组中,包括空字符。
  3. 如果 n 大于 src 字符串长度加上空字符(即 strlen(src) + 1)时,不仅会完整复制src字符串到dest中,还会将剩余 (n - strlen(src) + 1)个字符设置为空字符。

所以,在实际开发中建议将n的值设定为dest长度-1,并主动将dest的最后一个元素设置为空字符,这样总能安全地得到一个字符串。

也就是以下操作代码:

strncpy(dest, src, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0';

实际的代码案例如下:

char src[] = "hello";
char dest[10];
// 这个复制是没有问题的
strncpy(dest, src, sizeof(dest) - 1);
dest[sizeof(dest) - 1] = '\0';

char dest2[5];
// 这个复制虽然没有真的将"hello"都复制到dest2,但是一个安全的操作。最终dest2存放字符串"hell"
strncpy(dest2, src, sizeof(dest2) - 1);
dest2[sizeof(dest2) - 1] = '\0';

strcat字符串拼接

strcat 函数的全名是 string_concat,顾名思义,它用于将一个字符串拼接到另一个字符串的末尾。

该函数的声明为:

char *strcat(char *dest, const char *src);

函数作用: strcat 函数会将 src(source,源字符串)中存储的以空字符 ‘\0’ 结束的字符串拼接到 dest(destination,目标字符串)的末尾。

具体来说:

会从 dest 字符串的空字符 ‘\0’ 开始,替换这个空字符,然后将src中表示字符串的字符数据从开头到空字符,全部拼接到dest末尾。

这个过程中,src 字符串不会被修改,所以它被 const 修饰。

总之,该函数调用后,dest 将包含 dest 和 src 的字符串拼接后的副本。

函数返回值:

该函数会返回指向目标数组 dest 的指针。一般来说这个返回值可以忽略,但有些场景中可以利用该返回值,对函数调用进行嵌套处理。

char dest[20] = "Hello, ";
char src[] = "World!";
char src2[] = " Have a nice day!";

// 嵌套,将src和src2都拼接到dest上
strcat(strcat(dest, src), src2);

// 输出最终的拼接字符串
printf("%s", dest); // 输出 "Hello, World! Have a nice day!"

注意:

  1. strcat 函数与 strcpy 一样,不会检查 dest 数组是否能够容纳拼接后的字符串。如果 dest 不够大,就会产生未定义行为,这是潜在的安全隐患。
  2. 可以考虑使用更安全的strncat函数来实现字符串拼接,它允许指定最大拼接的字符数。

更安全的字符串拼接函数strncat函数

strncat函数,它的声明如下:

char *strncat(char *dest, const char *src, size_t n);

该函数的行为是:

  1. 仍旧是找到dest字符串末尾的空字符,然后从该字符开始,将src字符数组中至多n个字符拼接到dest末尾。
  2. 如果src的长度小于n,则拼接直到遇到结束符’\0’为止。
  3. 若事先留好一个位置给空字符或者dest数组足够长,则该函数会将一个空字符写在dest末尾。注意若n的取值没有留出足够的位置,此函数也会发生越界访问的情况!!

为了安全的使用此函数,基于函数的特点,我们就可以得出n的计算公式:

int n = sizeof(dest) - strlen(dest) - 1;  // dest数组的长度 - 已存储字符串的长度 - 1(留给存空字符)

表达式中-1是必要的,否则会导致数组越界情况发生。

给strncat函数传入这样的一个n参数,就能够保证一定安全的得到一个拼接后的结果字符串。

当然若dest容量不够大,拼接只会截取src的一部分字符数据。但安全是更重要的事情,这样的结果是我们可以接受的。

实际开发中,建议采取以下方式来调用此函数,避免dest空间不足导致溢出:

char src[] = “world”;
char dest[10] = "hello, ";
// 确保dest是一个数组类型,而不是一个指针类型才能这样操作
int n = sizeof(dest) - strlen(dest) - 1;
strncat(dest, src, n);

也就是说,dest最多再拼接**(自身数组长度 - 字符串长度 - 1)**个字符,最后留一个字节,strncat函数会自动拼接一个空字符表示字符串结束。

strcmp字符串比较大小

strcmp函数的全名是 string_compare,它用于比较两个字符串的大小。(当然这个大小肯定不是一般的数值大小)

该函数的声明为:

int strcmp(const char *str1, const char *str2);

函数作用: **strcmp函数按照字典顺序比较两个字符串的大小。**当你在 C 程序中调用 strcmp函数时,它会逐个字符地比较两个字符串 str1 和 str2:

  1. 它从字符串的第一个字符开始进行两两比较,并且继续向后比较**,直到两个字符串中的字符不相同,或者某个字符串达到末尾以及都达到末尾。**
  2. 如果两个字符串在某个字符处不同,strcmp函数会返回这两个字符在ASCII码表中的编码值之差(str1中的字符编码值 - str2中的字符编码值)。所以:
    1. 如果str1中的字符在编码集中,处在str2中字符的后面,strcmp函数就会返回一个正数。此时str1字符串大于str2字符串。
    2. 如果str1中的字符在编码集中,处在str2中字符的前面,strcmp函数就会返回一个负数。此时str1字符串小于str2字符串。
  3. 如果其中一个字符串达到了末尾(空字符),而另一个字符串没有:
    1. 如果str1达到了末尾,那么返回0这个ASCII码值和str2中字符相减的结果,结果是一个负数。此时str1字符串小于str2字符串。
    2. 如果str2达到了末尾,那么返回str1中字符和0这个编码值相减的结果,结果是一个正数。此时str1字符串大于str2字符串。
  4. 如果两个字符串在比较过程中的每个对应字符都相同,两个字符串同时达到末尾,strcmp函数返回 0,表示两个字符串相等。

注意:

  1. 这个函数不会修改任何一个字符串,所以两个参数都被 const 修饰。
  2. 不同编译器和环境可能对strcmp函数返回值的表现有所不同。在某些环境中**(比如VS)**,出于简化的目的,可能会将返回值归一化为 -1、0 或 1。但是标准的 C 语言库中 strcmp 的返回值是根据实际字符的ASCII值差异来确定的,而不是简单的 -1、0 或 1。
  3. 要确保传入的两个参数数组都表示字符串,都以空字符结尾,否则可能会导致比较越界。
  4. strcmp 是区分大小写的比较,如果需要进行不区分大小写的比较,可以使用 strcasecmp 函数。
  5. 由于strcmp是基于ASCII值进行比较的,它在处理非ASCII或多字节字符(如UTF-8编码的文本)时可能不会表现出预期的行为。

判断字符的库函数

在操作字符串的过程中,经常需要对单个字符做出判断,比如:

  1. 判断字符是大写字母还是小写
  2. 判断字符是不是数字

C 标准函数库在头文件 <ctype.h> 中定义了大量此类功能函数。比如:

islower 和 isupper 函数是 C 标准库中声明在 <ctype.h> 头文件中的函数,它们用于检查给定的字符是否为小写字母或大写字母。

  1. islower(int c) 检查传入的字符 c 是否为小写字母(a 到 z)。如果是,函数返回非零值(真),如果不是,返回零(假)。
  2. isupper(int c) 检查传入的字符 c 是否为大写字母(A 到 Z)。如果是,函数返回非零值(真),如果不是,返回零(假)。

除此之外,<ctype.h> 头文件中还包括了以下类似的库函数:

  1. isalpha(int c):检查传入的字符是否为字母(包括大写和小写)。
  2. isdigit(int c):检查传入的字符是否为十进制数字(0到9)。
  3. isalnum(int c):检查传入的字符是否为字母或数字。
  4. isspace(int c):检查传入的字符是否为空白字符,比如空格、制表符、换行符等。
  5. isblank(int c):检查传入的字符是否为空格或制表符。

总之,我们建议大家在操作字符串时,碰到某个需求可以优先查找一下是否有现成的库函数实现,若有则不需要重复造轮子。

字符串数组

在实际应用中,我们经常需要处理一组字符串。因此,我们就需要思考这样一个问题:如何存储一组字符串集合呢?

在C语言中,处理字符串集合时,我们通常有两种主要的存储方式:

字符二维数组来存储字符串集合

char week_days[][10] = 
  { "Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday" };

字符指针数组来存储字符串集合

char* week_days[] =
  { "Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday" };

这两种方式都可以称之为"字符串数组"

命令行参数


总结与复习

1

char char_arr[10];

这是一个字符数组的声明。它分配了10个连续的char类型的存储空间。你可以使用这个数组来存储最多9个字符的字符串(第10个字符用来存储字符串结束符’\0’),或者任何其他的字符数据。

它可以执行的操作有:

  1. 利用索引运算符取、改元素。如:

    char_arr[0] = 'A';
    
  2. 当此字符表示存储字符串时,可以直接作为参数传递给字符串处理函数。

2

char *char_p;

这是一个指向char类型的指针变量的声明。它可以存储一个字符变量的地址,或者更常见的是指向字符数组(字符串)的首地址。

它可以执行的操作是:

  1. 指向一个字符(如char c; p = &c;)。

  2. 指向一个字符串或字符数组的首字符(如char str[] = "Hello"; p = str;)。

  3. 指向字符串的首字符后,它可以用于各种遍历数组的场合。比如:

    while (*char_p){  // 字符串字符数组会以'\0'结尾,这个循环条件就可以遍历完整个字符串
      printf("%c\n", *char_p++);  // 逐个遍历打印字符串的字符
    }
    

3

char *p_arr[10];

这是一个字符指针数组的声明,数组里有10个元素,每个元素都是指向char类型的指针。通常用来存储多个字符串的地址,也就是存储一组字符串的集合。

字符指针数组是字符串数组最常见的形式之一。

4

char (*arr_p)[10];

这是一个指针的声明,它指向一个具有10个char的数组。通常用于指向二维字符数组中的一行,或者一个固定大小字符数组的指针。

它可用于字符二维护数组的遍历,但一般比较少见。

  • 24
    点赞
  • 22
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

如是我闻艺

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

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

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

打赏作者

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

抵扣说明:

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

余额充值