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


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

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

    #include 
    
    #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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

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


    1.1 定义字符串

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

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

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

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

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

    #include 
    
    int main(void){
    
        char msg[50] = "Hello""World" "LOVE C";
        puts(msg); // 输出结果:HelloWorldLOVE C
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    等价于:

    #include 
    
    int main(void){
    
        char msg[50] = "HelloWorldLOVE C";
        puts(msg); // 输出结果:HelloWorldLOVE C
    
        return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

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

    #include 
    
    int main(void){
    
        char msg[50] = "Hello \"World\" LOVE C";
        puts(msg); // 输出结果:Hello "World" LOVE C
    
        return 0;
    
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

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


    1.1.2 字符串数组和初始化

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

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

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

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

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

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

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

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

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

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

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

    1.1.3 数组和指针

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

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

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

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

    #include 
    
    #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
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    1.1.4 数组和指针的区别

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

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

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

    const char *pt = "LOVE C";
    
    • 1

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

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


    二、字符串输入

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


    2.1 分配空间

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

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

    char name[40];
    
    • 1

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

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

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

    #include 
    
    #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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    但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 
    
    #define SIZE 4
    
    int main() {
    
        char words[SIZE];
    
        puts("please enter a string:");
        fgets(words, 4, stdin);
        fputs(words,stdout);
    
        return 0;
    }
    
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    2.3.2 gets_s()函数

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

    1. get_s()只从标准输入中读取数据,所以不需要第3个参数。
    2. 如果get_s()读到换行符,会丢弃而不是储存它。
    3. 如果get_s()读到最大字符数都没有读到换行符,会执行以下几步。首先把目标数组中的首字符设置为空字符,读取并丢弃随后的输入直至读到换行符或文件结尾,然后返回空指针。接着,调用依赖实现的“处理函数”,可能会中止或退出程序。
    #include 
    
    #define SIZE 14
    
    // 需要在C11中执行
    int main() {
    
        char words[SIZE];
    
        puts("please enter a string:");
        gets_s(words, 4);
        fputs(words,stdout);
    
        return 0;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    2.3.3 scanf()函数

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

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

    四、字符串输出

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

    4.1 puts()函数

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

    #include 
    
    #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
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

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

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

    #include 
    
    #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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    上述中的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把这些函数的原型放在头文件中。其中最常用的函数有strlen()、strcat()、strcmp()、strncmp()、 strcpy()和strncpy()。


    5.1 strlen()函数

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

    #include 
    #include "string.h"
    
    #define MSG "Hello world"
    
    int main(void){
        //"%llu"为unsigned long long int
        printf("%llu", strlen(MSG));
        
       return 0;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11

    5.2 strcat()函数

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

    #include 
    #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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    5.3 strncat()函数

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

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

    #include 
    #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;
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16

    5.4 strcmp()和strncmp()函数

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

    5.4.1 strcmp()函数

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

    #include 
    #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;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

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

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


    5.4.2 strncmp()函数

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

    #include 
    #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;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17

    5.5 strcpy()和strncpy()函数

    5.5.1 strcpy()函数

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

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

    #include 
    #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;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    strcpy()函数的其他属性:

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

    5.5.2 strncpy()函数

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

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

    #include 
    #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;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18

    5.6 sprintf()函数

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

    #include 
    
    #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;
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
  • 相关阅读:
    程序是怎样跑起来的
    浅谈DDoS攻击和CC攻击的区别
    每日一题AC
    心情不好就狂吃?好心情心理:这是病,得治!
    LeetCode——29. 两数相除
    IPv4地址转换成整数
    一文了解云计算
    【JAVA多线程】ForkJoinPool,为高性能并行计算量身打造的线程池
    推荐10个地推拉新app推广接单平台,都是一手单 官签渠道
    【博学谷学习记录】超强总结,用心分享|架构师-nacos功能应用
  • 原文地址:https://blog.csdn.net/qq_46292926/article/details/127819819