C语言学习记录(十二)之字符串和字符串函数

一、字符串和字符串I/O

字符串和格式化输入/输出一节中我们讲过,字符串是以空字符 “ \0 ”结尾的char类型的数组。由于字符串十分常用,所以C提供了许多专门处理字符串的函数。下面详细讲述字符串的性值、如何声明并初始化字符串、在程序中输入和输出字符串以及如何操控字符串。

#include <stdio.h>

#define MSG "I LOVE C"
#define LARGE 40

int main(void){
    
    
    
    // 定义一个字符数组
    char words[LARGE] = "I am studying C";
    // 定义一个不能改变的字符串指针
    const char *Str1 = "that`s funny";
    // 打印以下信息
    puts("Some string display Here: ");
    puts(MSG);
    puts(words);
    puts(Str1);
    // 将words中的第8个元素改为P
    words[8] = 'P';
    puts(words);
    return 0;
}

和printf()函数一样,puts函数也属于stdio.h系列的输入/输出函数。但是,和printf()不同的是,puts()函数只显示字符串(不能打印数字),而且自动在显示的字符串末尾加上换行符。


1.1 定义字符串

在上面的程序中,有三种定义字符串的方式。分别是:

  1. 字符串字面量(字符串常量)
#define MSG "I LOVE C"
    puts(MSG);
  1. 字符串数组
#define LARGE 40
// 定义一个字符数组
char words[LARGE] = "I am studying C";
	// 输出字符串
    puts(words);
  1. 字符串指针
// 定义一个不能改变的字符串指针
const char *Str1 = "that`s funny";
puts(Str1);

1.1.1 字符串字面量(字符串常量)

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

如果字符串字面量之间没有间隔或者用空白字符间隔,C会将其视为串联起来的字符串字面量

#include <stdio.h>

int main(void){
    
    

    char msg[50] = "Hello""World" "LOVE C";
    puts(msg); // 输出结果:HelloWorldLOVE C

    return 0;
}

等价于:

#include <stdio.h>

int main(void){
    
    

    char msg[50] = "HelloWorldLOVE C";
    puts(msg); // 输出结果:HelloWorldLOVE C

    return 0;
}

如果要在字符串中打印出双引号则需要加上转义符 “ \ ”。

#include <stdio.h>

int main(void){
    
    

    char msg[50] = "Hello \"World\" LOVE C";
    puts(msg); // 输出结果:Hello "World" LOVE C

    return 0;

}

字符串常量属于静态存储类别,说明如果在函数中使用字符串常量,该字符串只会被存储一次,在整个程序的生命周期内存在,即时函数被调用多次。


1.1.2 字符串数组和初始化

定义字符串数组时,必须让编译器知道需要多少空间。一种方法是用足够空间的数组存储字符串。如下:

const char str[20] = "I LOVE C";

const表示不会更改这个字符串。

上述初始化比标准数组初始化(下述程序)简单的多:

const char str[20] = {
    
    'I', ' ', 'L', 'O', 'V', 'E', ' ', 'C', '\0'};

注意最后的空字符。没有这个空字符,这就不是一个字符串,而是一个字符数组。

在指定数组大小时,要确保数组的元素个数至少比字符串长度多1(为了容纳空字符)。所有未被使用的元素都自动初始化为0(这里的0指的是char形式的空字符,不是数字0)。

在这里插入图片描述
通常,让编译器自己确定数组的大小更方便。因为处理字符串的函数通常都不知道数组的大小,这些函数通过查找字符串末尾的空字符确定字符串在何处结束。

const char str[] = "I LOVE C"; 

声明数组时,数组的大小必须是可求值的整数。在C99新增边长数组之前,数组的大小必须是整型常量。

int n = 8;

char str1[1];	// 有效
char str2[2+5];	// 有效
char str3[2*sizeof(int)+1];	// 有效
char str4[n];	// 在C99标准之前无效,C99之后是变长数组

1.1.3 数组和指针

const char *pt1 = "LOVE C";
const char ar1[] = "LOVE C";

在上面的声明中,数组形式(ar1[ ])在计算机的内存中分配一个含有7个 元素的数组(每个元素对应一个字符,还加上末尾的空字符 ‘\0’),每个元素被初始化为字符串字面量对应的字符。通常,字符串都作为可执行文件的一部分存储在数据段中。当把程序载入内存时,也载入了程序中的字符串。字符串存储在静态存储区中。但是,当程序开始运行时才会为该数组分配内存。此时,才将字符串拷贝到数组中。此时字符串有两个副本,一个是在静态内存中的字符串字面量,另一个是存储在ar1数组中的字符串。此后,编译器便把数组名ar1识别为该数组首元素地址(ar1[0])的别名。在数组形式中,ar1是地址变量。不能更改ar1,如果改变了ar1。则意味着改变了数组的存储位置(即地址)。可以进行类似ar1+1这样的操作,标识数组的下一个元素。但是不允许进行++ar1这样的操作。递增运算符只能用于变量名前,不能用于常量。
指针形式也使得编译器为字符串在静态存储区预留7个元素的空间。另外,一旦开始执行程序,他会为指针变量pt1留出一个存储位置,并把字符串的地址存储在指针变量中。该变量最初指向该字符串的首字符,但是它的值可以改变。因此可以使用递增操作。

字符串字面量被视为const数据。由于pt1指向这个const数据,所以应该把pt1声明为指向const数据的指针。这意味着不能用pt1改变它所指向的数据,但是仍然可以改变pt1的值。

总之,初始化数组把静态存储区的字符串拷贝到数组中,而初始化指针只把字符串的地址拷贝给指针。

#include <stdio.h>

#define MSG "LOVE C"

int main(void){
    
    

    const char ar[] = MSG;
    const char *pt = MSG;

    printf("the address of ar :%p\n", ar);//the address of ar :00000094583ff831
    printf("the address of pt :%p\n", pt);//the address of pt :00007ff63fa6a000
    printf("the address of MSG :%p\n", &MSG);//the address of MSG :00007ff63fa6a000

    return 0;
}

1.1.4 数组和指针的区别

两者的主要区别在于:数组名ar是常量,而指针名pt是变量。
两者中只有指针表示法可以进行递增操作。

数组的元素是变量(除非数组被声明为const),但是数组名不是变量。

建议指针初始化字符串时使用const限定符:

const char *pt = "LOVE C";

总之,如果打算修改字符串,就不要用指针指向字符串字面量

如果要改变字符串或为字符串输入预留空间,不要使用指向字符串字面量的指针。


二、字符串输入

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


2.1 分配空间

要做的第一件事就是要为稍后读入的字符串分配足够的空间。

在这里插入图片描述
在上述的代码中,编译器给出了警告。在读入name时,name可能会擦写掉程序中的数据或代码,从而导致程序异常终止。因为scanf()要把信息拷贝至参数指定的地址上,而此时该参数是个未初始化的指针,name可能指向任何地方。
解决上述问题最简单的方法是在声明时指明数组的大小:

char name[40];

2.2 gets()函数 (不建议使用)

在读取字符串时,scanf()和转换说明%s只能读取一个单词。可在程序中经常要读取一整行输入,而不仅仅是一个单词。许多年前,gets()函数就用于处理这种情况。

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

#include <stdio.h>

#define LEN 40

int main(void){
    
    

    char words[LEN];
    puts("Enter a string :");
    gets(words);	// 典型用法
    printf("%s\n", words);
    puts(words);
    puts("Done");
    
   return 0;
}

但gets()函数在一些编译器中会弹出警告信息。问题在于gets()唯一的参数,它无法检查数组是否装的下输入行,前面介绍过,数组名会被转换成该数组元素的首地址,因此,gets()函数只知道数据的开始处,并不知道数组中有多少个元素。

如果输入的字符串过长,会导致缓冲区溢出,即多余的字符超出了指定的目标空间。如果这些多余的字符只是占用了未使用的内存,就不会立即出现问题;如果它们擦写掉程序中的其他数据,会导致程序异常终止。

因为gets()函数的这个特性,有些人通过系统编程,运行一些破环系统安全的代码。

C99标准委员承认了gets()的问题并建议不要使用它。C11标准委员会则从标准中直接废除了gets()函数。

然而在实际应用中,编译器为了兼容以前的代码,大部分都继续支持gets()函数。


2.3 gets()的替代品

2.3.1 fgets()函数(和fputs())

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

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

因为fgets()函数把换行符放在字符串的末尾(假设输入行不溢出),通常要与fputs()函数(和puts()类似)配对使用,除非该函数不在字符串末尾添加换行符。fputs()函数的第二个参数指明它要写入的文件。如果要显示在计算机显示器上,应使用stdout作为参数。

#include <stdio.h>

#define SIZE 4

int main() {
    
    

    char words[SIZE];

    puts("please enter a string:");
    fgets(words, 4, stdin);
    fputs(words,stdout);

    return 0;
}



2.3.2 gets_s()函数

C11新增的get_s()函数(可选)和fgets()类似,用一个参数限制读入的字符数。两者的主要区别在于:

  1. get_s()只从标准输入中读取数据,所以不需要第3个参数。
  2. 如果get_s()读到换行符,会丢弃而不是储存它。
  3. 如果get_s()读到最大字符数都没有读到换行符,会执行以下几步。首先把目标数组中的首字符设置为空字符,读取并丢弃随后的输入直至读到换行符或文件结尾,然后返回空指针。接着,调用依赖实现的“处理函数”,可能会中止或退出程序。
#include <stdio.h>

#define SIZE 14

// 需要在C11中执行
int main() {
    
    

    char words[SIZE];

    puts("please enter a string:");
    gets_s(words, 4);
    fputs(words,stdout);

    return 0;
}


2.3.3 scanf()函数

scanf()函数有两种方法确定输入结束。无论哪种方法,都是从第1个非空白字符作为字符串的开始,如果使用 “%s”,转换说明,以下一个空白字符(空格、空行、制表符和换行符)作为字符串的结束(字符串不包括空白字符)。如果指定了字段宽度,如%10s,那么scanf()将读取10个字符或读到第1个空白字符停止(先满足的条件即是结束输入的条件)

输入语句 原输入序列 name中的内容 剩余输入序列
scanf(“%s”, name) fleebert hup fleebertt [空格]hup
scanf(“%5s”, name) fleebert hup fleeb ert hup
scanf(“%5s”, name) ann ular ann [空格]ular

四、字符串输出

C有3个标准库函数用于打印字符串:puts()、fputs()和printf()

4.1 puts()函数

puts()函数很容易使用,只需把字符串的地址作为参数传递给它即可。

#include <stdio.h>

#define MSG "Hello World"
#define SIZE 40

int main(void){
    
    

    char flower[SIZE] = "I LOVE C";
    puts(flower);
    puts(MSG);
    
   return 0;
}
//输出结果:
I LOVE C
Hello World

如上所示,每个字符串单独占一行,因为puts()在显示字符串时会自动在其末尾添加一个换行符。

该程序再次说明,用双引号括起来的内容是字符串常量,且被视为该字符串的地址。

#include <stdio.h>

#define MSG "Hello World"
#define SIZE 40

int main(void){
    
    
	
	// flower不是字符串数组,而是一个字符数组
    char flower[SIZE] = {
    
    'I', 'L', 'O', 'V', 'E', 'C'};
    puts(flower);
    puts(MSG);

   return 0;
}

上述中的flower不是一个字符串,因为缺少一个表示结束的空白符。


4.2 fputs()函数

fputs()函数是puts()针对文件定制的版本。它们的区别如下:

  1. fputs()函数的第二个参数指明要写入数据的文件。如果要打印在显示器上,可以用定义在stdio.h中的stdout(标准输出)作为参数。
  2. 与puts()不同,fputs()函数不会在输出的末尾添加换行符。

4.3 printf()函数

和puts()函数一样,printf()也把字符串的地址作为参数,printf()函数用起来没有puts()函数那么方便,但是它更加多才多艺,因为它可以格式化不同的数据类型。

与puts()不同的是,printf()不会自动在每个字符串的末尾加上换行符。因此,必须在参数中指明应该在哪里使用换行符。

五、字符串函数

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


5.1 strlen()函数

strlen()函数用于统计字符串长度。

#include <stdio.h>
#include "string.h"

#define MSG "Hello world"

int main(void){
    
    
    //"%llu"为unsigned long long int
    printf("%llu", strlen(MSG));
    
   return 0;
}

5.2 strcat()函数

strcat()(用于拼接字符串)函数接收两个字符串作为参数。该函数把第2个字符串的备份附加在第一个字符串的末尾,并把拼接后形成的新字符串作为第一个字符串,第二个字符串不变。strcat()函数类型是char *(即指向char的指针)。strcat()函数返回第一个参数,即拼接第二个字符串后的第一个字符串的地址。

#include <stdio.h>
#include "string.h"

#define MSG " Hello world"
#define SIZE 80

int main(void){
    
    

    char flower[SIZE];
    puts("please enter a flower:");
    gets(flower);
    strcat(flower, MSG);
    printf("%s\n", flower); //iris Hello world
    
   return 0;
}

5.3 strncat()函数

strcat()函数无法检查第一个数组是否容纳第二个数组。如果分配给第一个数组的空间不够大,多出来的字符溢出到相邻存储单元时就会出问题。

strncat()函数该函数的第三个参数指定了最大添加字符数。例如:strncat(flower,MSG, 4)将把MSG字符串的内容附加给flower,在加到第4个字符或遇到空字符停止。因此算上空字符flower数组应该足够大。

#include <stdio.h>
#include "string.h"

#define MSG " Hello world"
#define SIZE 40

int main(void){
    
    

    char flower[SIZE];
    puts("please enter a flower:");
    gets(flower);
    strncat(flower, MSG, 8);
    printf("%s\n", flower); // iris Hello w

   return 0;
}

5.4 strcmp()和strncmp()函数

两个函数都是比较两个字符串参数是否相同。如果两个字符串相同则返回0,否则返回非零值。

5.4.1 strcmp()函数

该函数要比较的是字符串的内容,不是字符串的地址。

#include <stdio.h>
#include "string.h"

#define MSG "iris"
#define SIZE 40

int main(void){
    
    

    char flower[SIZE];
    puts("please enter a flower:");
    gets(flower);
    // 返回值为0或非零值
    int bl =  strcmp(flower, MSG);
    printf("%d\n", bl);

   return 0;
}

strcmp()函数比较的是字符串,不是整个数组。虽然数组占用了40个字节,而存储在其中的iris只占用了5个字节(还有一个空字符),strcmp()函数只会比较空字符前面的部分。

注意:strcmp()函数比较的是字符串,不是字符。


5.4.2 strncmp()函数

strncmp()函数比较字符串中的字符,直到发现不同的字符为止,这一过程可能会持续到字符串的末尾。而strncmp()函数在比较两个字符串时,可以比较到字符不同的地方,也可以只比较第三个参数指定的字符数。

#include <stdio.h>
#include "string.h"

#define MSG "iris"
#define SIZE 40

int main(void){
    
    

    char flower[SIZE];
    puts("please enter a flower:");
    gets(flower);
    int bl =  strncmp(flower, MSG, 4);
    printf("%d\n", bl);

   return 0;
}


5.5 strcpy()和strncpy()函数

5.5.1 strcpy()函数

如果要拷贝整个字符串,要使用strcpy()函数。

strcpy()函数第二个参数指向的字符串被拷贝至第一个参数指向的数组中。拷贝出来的字符串被称为目标字符串,最初的字符串被称为源字符串。即第一个是目标字符串,第二个是源字符串。

#include <stdio.h>
#include "string.h"

#define MSG "iris"
#define SIZE 40

int main(void){
    
    

    char flower[SIZE];
    puts("please enter a flower:");
    gets(flower);
    puts(flower);
    strcpy(flower, MSG);
    printf("%s\n", flower);
    
   return 0;
}

strcpy()函数的其他属性:

  1. strcpy()的返回类型是char *,该函数返回的是第一个参数的值,即一个字符的地址。
  2. 第一个参数不必指向数组的开始。这个属性可用于拷贝数组的一部分。

5.5.2 strncpy()函数

strcpy()函数和strcat()函数无法检查第一个数组是否容纳第二个数组。如果分配给第一个数组的空间不够大,多出来的字符溢出到相邻存储单元时就会出问题。

拷贝字符串用strncpy()函数更安全,该函数的第3个参数指明可拷贝的最大字符数。

#include <stdio.h>
#include "string.h"

#define MSG "iris"
#define SIZE 40

int main(void){
    
    

    char flower[SIZE];
    puts("please enter a flower:");
    gets(flower);
    puts(flower);
    strncpy(flower, MSG, 2);
    printf("%s\n", flower);

   return 0;
}


5.6 sprintf()函数

sprintf()函数声明在<stdio.h>中,而不是"string.h"中。该函数和printf()类似,但是它是把数据写入字符串,而不是打印在显示器上。因此,该函数可以把多个元素组合成一个字符串。sprintf()函数的第一个参数是目标字符的地址。其余参数和printf()相同,即格式字符串和代写入项的列表。

#include <stdio.h>

#define MSG "iris"
#define SIZE 40

int main(void){
    
    

    char flower[SIZE];
    char pt[2 * SIZE + 10];
    puts("please enter a flower:");
    gets(flower);
    puts(flower);
    sprintf(pt, "%s, %s", MSG, flower);
    printf("%s\n", pt);

   return 0;
}

猜你喜欢

转载自blog.csdn.net/qq_46292926/article/details/127819819