C语言学习笔记——字符串操作

C语言学习笔记——字符串操作

  • C语言字符串操作
    • 字符串表示方式
      • 字符串常量
      • 字符串数组及初始化
      • 数组和指针
      • 数组和指针的区别
      • 字符串数组
        • 指向字符串的指针数组
        • char类型数组的数组
      • 指针和字符串
    • 字符串输入
      • 不安全的`gets()`函数
      • `gets()`的替代品
        • `fgets()`函数
        • 自定义的`s_gets()`函数
      • `scanf()`函数
    • 字符串输出
      • `puts()`函数
      • `fputs()`函数
      • `printf()`函数
    • 自定义输入/输出函数

C语言字符串操作

字符串是C语言中最重要的数据类型之一。最近借助《C Primer Plus》一书来学习C中的常用字符串操作,在此作为笔记记录。本文中的示例程序均出自《C Primer Plus》,操作系统为CentOS 8.1.1911,使用的编译器为gcc 8.3.1

字符串表示方式

字符串常量

用双引号括起来的内容称为字符串常量,例如:"Hello, World!"为一个字符串常量。双引号中的字符和编译器自动加入末尾的\0字符,都作为字符串存储在内存中。

字符串常量存储示例

字符串常量属于静态存储类别。当在函数中使用字符串常量时,该字符串只会被存储一次,用双引号括起来的内容被视为指向该字符串存储位置的指针,如以下例程所示:

/* strptr.c -- 把字符串看做指针 */
#include 

int main(void) {

    printf("%s, %p, %c\n", "Who", "you", *"are");

    return 0;
}

程序输出结果如下:

Who, 0x400668, a

字符串数组及初始化

定义字符串数组时,必须让编译器知道需要多少空间。字符串数组的定义方法之一,可以用字符串初始化数组

const char s1[25] = "This is a string array.";

定义方式之二,标准的数组初始化形式:

const char s1[25] = {
    'T', 'h', 'i', 's', ' ', 'i', 's', ' ', 
    'a', ' ', 's', 't', 'r', 'i', 'n', 'g', 
    ' ', 'a', 'r', 'r', 'a', 'y', '.', '\0'
};

以上初始化形式,需注意最后的空字符\0。如果没有\0,就不是字符串,而是一个字符数组。
定义方式之三,省略初始化声明中的大小

const char s1[] = "This is a string array.";

以上的这种声明方式,编译器会自动计算数组大小(只能用在初始化数组时)。
字符串数组名和其他数组名一样,是该数组首元素的地址。假设有如下初始化:

char flower[10] = "Rose";

则,以下表达式均为真:

flower == &flower[0];	// true
*flower == 'R';		// true
*(flower + 1) == flower[1] == 'l';	// true

定义方式之四,使用指针表示法创建字符串

const char *pt1 = "This is a string array.";

数组和指针

现有如下的字符串声明:

const char arr1[] = "String array and pointer";	// 24个字符
const char *pt1 = "String array and pointer";

数组形式arr1)在计算机内存中分配为一个内含25个元素的数组(包含'\0')。当把程序载入内存时,也载入了程序中的字符串(位于静态存储区)。但程序在开始运行时,才会为数组arr1分配内存,此时,才将字符串拷贝到数组中。
编译器把数组名arr1识别为数组首元素地址(&arr1[0])的别名,即,在数组形式中,arr1是地址常量——不允许进行++arr1这样的操作,因为自增运算只能用于变量,不能用于常量。

指针形式*pt1)会使得编译器为字符串在静态存储区预留25个元素的空间,一旦开始执行程序,它会为指针变量pt1留出一个存储位置,用于存储字符串的地址。

总之,初始化数组把静态存储区的字符串拷贝到数组中,而初始化指针只把字符串的地址拷贝给指针。如以下例程所示:

/* addresses.c -- 字符串的地址 */
#include 

#define MSG "Blue till I die"

int main(void) {
    char arr[] = MSG;
    char *pt = MSG;

    printf("address of \"Blue till I die\": %p\n", "Blue till I die");
    printf("              address of arr: %p\n", arr);
    printf("               address of pt: %p\n", pt);
    printf("              address of MSG: %p\n", MSG);
    printf("address of \"Blue till I die\": %p\n", "Blue till I die");
    return 0;
}

运行结果如下:

[root@gavinpan p2]# !g
gcc -o addresses addresses.c 
[root@gavinpan p2]# !.
./addresses 
address of "Blue till I die": 0x4006d8
              address of arr: 0x7ffdcda3f2e0
               address of pt: 0x4006d8
              address of MSG: 0x4006d8
address of "Blue till I die": 0x4006d8

数组和指针的区别

part1 有如下两个声明:

char heart[] = "I am the heart";
char *head = "I am the head";

若想让hearthead统一,可以执行如下操作:

head = heart;	// 指针head指向了数组heart

但以下操作是非法的:

heart = head;		// heart为地址常量,不能作为左值

part2 以下未使用const限定的指针初始化:

char *word = "frame";

是否可以使用该指针来修改该字符串?

word[1] = 'l';	// 是否允许?

C Primer Plus》中是这样描述的:编译器可能允许这样做,但是对于当前的C标准而言,这样的行为是未定义的。列如,这样的语句可能导致内存访问错误。原因:编译器可以使用内存中的一个副本来表示所有完全相同的字符串字面量。参见如下示例(错误示例):

/*
 * BUGGY: Segmentation fault (core dumped)
 * str_constant_pt.c -- 使用指针修改字符串常量 
 */
#include 

int main(void) {
    char *p1 = "Klingon";
    p1[0] = 'F';

    printf("Klingon");
    printf(": Beware the %ss!\n", "Klingon");
    return 0;
}

原书中显示的结果是:

Flingon: Beware the Flingons!

而我自己的自行结果如下:

[root@gavinpan p2]# !g
gcc -o str_constant_pt str_constant_pt.c 
[root@gavinpan p2]# !.
./str_constant_pt 
Segmentation fault (core dumped)

Segmentation fault (core dumped)多为内存不当操作造成。空指针、野指针的读写操作,数组越界访问,破坏常量等1

基于上述问题,建议在把指针初始化为字符串常量时使用const限定符:

const char *pt = "Klingon";

在把非const数组初始化为字符串常量时,不会导致类似问题,因为数组获得的是原始字符串的副本

因此,如果打算修改字符串,就不要用指针指向字符串常量

字符串数组

指向字符串的指针数组

const char *pointer_arr[3] = {"alpha", "beta", "theta"};

pointer_arr是一个内含3个指针的数组,在当前操作系统CentOS 8中,占用24个字节:

printf("size of pointer_arr: %d\n", sizeof(pointer_arr));

输出:

size of pointer_arr: 24

char类型数组的数组

char arrays[3][10] = {"alpha", "beta", "theta"};

arrays是内含3个数组的数组,每个数组内含10个char类型的值,共占用30个字节:

printf("size fo arrays: %d\n", sizeof(arrays));

输出:

size fo arrays: 30

pointer_arr中的指针指向初始化时所用字符串常量的位置;而arrays中的数组则存储了字符串常量的副本,故每个字符串都被存储了两次。

基于上述两种声明方式,可以把pointer_arr想象为不规则数组,而arrays则为矩形二维数组:

C语言学习笔记——字符串操作_第1张图片
C语言学习笔记——字符串操作_第2张图片
综上,如果要用数组表示一些列带显示的字符串,请使用指针数组;如果要改变字符串或为字符串输入预留空间,不要使用指向字符串字面量的指针。

指针和字符串

一个例子:

/* p_and_s,c --指针和字符串 */
#include 

int main(void) {
    const char *msg = "Don't be a fool!";
    const char *copy;

    copy = msg;

    printf("%s\n", copy);
    printf("msg = %s; &msg = %p; value = %p\n", msg, &msg, msg);
    printf("copy = %s; © = %p; copy = %p\n", copy, &copy, copy);

    return 0;
}

输出结果如下:

[root@gavinpan p2]# ./p_and_s 
Don't be a fool!
msg = Don't be a fool!; &msg = 0x7ffcb7cb1348; value = 0x4006d8
copy = Don't be a fool!; &copy = 0x7ffcb7cb1340; copy = 0x4006d8

其中,&msg = 0x7ffcb7cb1348© = 0x7ffcb7cb1340说明指针msgcopy分别存储在地址为0x7ffcb7cb13480x7ffcb7cb1340的内存中;最后一项value = 0x4006d8,说明两个指针指向了同一个位置,因此,程序并未拷贝字符串。

字符串输入

不安全的gets()函数

gets()函数读取整行输入,直到遇到换行符,然后丢弃其余字符,存储其余字符;并在这些字符的末尾添加一个空字符\0
使用示例:

char words[10];
gets(words);
puts(words);

gets()函数唯一的参数是words,它无法坚持该数组是否装得下输入行。因此,gets()函数只知道数组的开始位置,但不知都数组中有多少个元素。
一旦输入的字符过长,就会导致缓冲区溢出(buffer overflow),即多余的字符超出了指定的目标空间。如果这些多余的字符只是占用了尚未使用的内存,就不会立即出现问题;但如果它们擦写掉了程序中的其他数据,就会导致程序异常终止;或者其他情况。

gets()的替代品

fgets()函数

该函数专门设计用于处理文件输入。`使用示例如下:

char words[10];
fgets(words, 10, stdin);
fputs(words, stdout);

fgets()gets()的区别如下:

1 第2个参数指明了读入字符的最大数量。如果该参数为n,那么fgets()将读入n-1个字符,或者遇到第一个换行符为止。
2 如果fgets()读到一个换行符,会把它存储咋字符串中。而gets()则会丢弃换行符。
3 第3个参数,指明要读入的文件。若要从键盘读取输入,则以stdin标准输入)作为参数。stdin定义在stdio.h中。

fgets()通常与fputs()配对使用。fputs()的第二个参数,指明它要写入的文件,如果要输出到屏幕,则使用stdout标准输出)作为参数。

fgets()的返回值是指向char的指针。如果一切顺利,返回的地址与第1个参数相同;如果读到文件结尾,则会返回空指针(null pointer)。代码示例如下:

/* fgets2.c -- 使用fgets()和fputs() */
#include 

#define STLEN 10

int main(void) {
    char words[STLEN];

    puts("Enter strings (empty line to quit):");
    while (fgets(words, STLEN, stdin) != NULL && words[0] != '\n') {
        fputs(words, stdout);
    }

    puts("Done.");
    return 0;
}

输出:

Enter strings (empty line to quit):
hello
hello
c primer plus
c primer plus

Done.

系统使用缓冲的I/O。意味着在按下Return键之前,输入都被存储在缓冲区。按下Return键会在输入中增加一个换行符\n,并把整行输入发送给fgets()。对于输出,fputs()把字符发送给另一个缓冲区,当发送换行符时,缓冲区的内容被发送至屏幕上。

fgets()会存储输入中的换行符\n,那么如何处理换行符呢?参见如下示例:

/* 
 * fgets3.c
 * 处理通过fgets()获取的字符串中的换行符,
 * 若无换行符,则丢弃多余字符。
 *
 */
#include 

#define STLEN 10

int main(void) {
    char words[STLEN];

    puts("Enter strings (empty line to quit):");

    while (fgets(words, STLEN, stdin) != NULL && words[0] != '\n') {
        int i = 0;
        while (words[i] != '\n' && words[i] != '\0') {
            i++;
        }

        if (words[i] == '\n') {            // 处理换行符
            words[i] = '\0';
        } else {
            while (getchar() != '\n') {    // 丢弃输入中的多余字符
                continue;
            }
        }
        puts(words);
    }
	puts("Done.");
    return 0;
}

输出:

[root@gavinpan p2]# !.
./fgets3 
Enter strings (empty line to quit):
hello
hello
c primer plus
c primer 

Done.

相比于fgets2.cfgets3.c中丢弃了多余的输入,相关代码为:

while (getchar() != '\n') { 
    continue;
}

自定义的s_gets()函数

示例代码:

/**
 * char *s_gets(char *, int);
 *
 * 获取字符串输入
 */
char *s_gets(char *st, int len) {
    char *ret;  // 接收fgets()函数的返回值
    int i = 0;

    ret = fgets(st, len, stdin);

    if (ret) {  // ret != NULL
        while (st[i] != '\n' && st[i] != '\0') {
            i++;
        }

        if (st[i] == '\n') {            // 处理换行符\n
            st[i] = '\0';
        } else {
            while (getchar() != '\n') { // 丢弃多余字符
                continue;
            }
        }
    }

    return ret;
}

那么为什么要丢弃输入行中的多余字符呢?因为,输入行中的多余字符会被留在缓冲区,成为下一次读取语句的输入。

scanf()函数

相比于gets()fgets()scanf()更像是“获取单词”的函数。scanf()函数有两种方法确定输入结束:
其一,如果使用%s转换说明,则以下一个空白字符(空行、空格、制表符或换行符)作为字符串的结束(不包括空白字符)。
其二,如果指定了字段宽度,如%10s,那么scanf()将读取10个字符或读到第1个空字符停止。
见如下示例:

/**
 * scanf1.c
 * scanf()函数输入结束示例
 */
#include 

#define STLEN 10

void clear_buf(void);

int main(void) {
    char name[STLEN];

    puts("Enter some lines");

    scanf("%s", name);
    printf("result of \"%%s\": %s\n", name);
    clear_buf();        // 清除缓冲区多余字符


    scanf("%5s", name);
    printf("result of \"%%5s\": %s\n", name);
    clear_buf();        

    scanf("%5s", name);
    printf("result of \"%%5s\": %s\n", name);
    clear_buf();        

    return 0;
}

void clear_buf(void) {
    while (getchar() != '\n') {
        continue;
    }
}

运行结果示例:

[root@gavinpan p2]# ./scanf1 
Enter some lines
Heroes come and go
result of "%s": Heroes	// 空格结束
Heroes come and go
result of "%5s": Heroe	// 5个字符长度结束
Kobe Bryant
result of "%5s": Kobe	// 空格结束

在看scanf()的使用示例:

/**
 * scanf()函数的使用示例
 */
#include 

#define STRLEN 12

int main(void) {
    char name1[STRLEN], name2[STRLEN];
    int count;

    printf("Please enter 2 names:\n");

    count = scanf("%5s %10s", name1, name2);

    printf("I read the %d names %s and %s\n", count, name1, name2);

    return 0;
}

以下为3个输出示例:

[root@gavinpan p2]# !.
./scanf_str1 
Please enter 2 names:
Terry Lampard
I read the 2 names Terry and Lampard

[root@gavinpan p2]# !.
./scanf_str1 
Please enter 2 names:
Cech Drogba
I read the 2 names Cech and Drogba

[root@gavinpan p2]# !.
./scanf_str1 
Please enter 2 names:
Lampard Kante
I read the 2 names Lampa and rd

如果输入行的内容过长,scanf()也会导致数据溢出。不过,在%s转换说明中使用字段宽度,可防止溢出。

字符串输出

puts()函数

puts()函数的入参为字符串的地址,在显示字符串时,会自动在末尾添加一个换行符。其使用示例如下:

/**
 * put_out1.c
 * puts()函数的使用
 */
#include 

#define STRLEN 50
#define DEF "I am a #defined string."

int main(void) {
    char str1[STRLEN] = "An array was initialized to me.";
    const char *pt1 = "A pointer was initilized to me.";

    puts("I am an argument to \"puts()\".");
    puts(DEF);
    puts(str1);
    puts(pt1);

    puts(&str1[3]);		// 从第4个字符开始
    puts(pt1 + 3);		// 从第4个字符开始
    return 0;
}

输出如下:

[root@gavinpan p2]# ./put_out1 
I am an argument to "puts()".
I am a #defined string.
An array was initialized to me.
A pointer was initilized to me.
array was initialized to me.
ointer was initilized to me.

puts()函数在遇到空字符\0时就会停止输出,因此必须确保有空字符。一下为一个错误示例:

/**
 * nono_puts.c
 * puts()的错误示例
 *
 * BUGGY
 */
#include 

int main(void) {
    char side_a[] = "Side A";
    char dont[] = {'O', 'h', ',', 'n', 'o', '!'};
    char side_b[] = "SIde B";

    puts(dont); /* WARNING: dont并不是一个字符串! */
    return 0;
}

输出:

[root@gavinpan p2]# !.
./nono_puts 
Oh,no!Side A

fputs()函数

fputs()显示字符串时,不会在末尾添加换行符。fputs()常与fgets()配对使用。
使用示例:

/**
 * fputs()的使用
 */
#include 

int main(void) {
    fputs("A \"fputs()\" string\n", stdout);
    return 0;
}

输出:

[root@gavinpan p2]# !.
./fputs1 
A "fputs()" string

printf()函数

示例:

/* printf1.c --printf()函数使用 */
#include 

int main(void) {
    printf("%d strings \"%s\" and \"%s\"\n", 2, "red", "blue");
    return 0;
}

输出:

[root@gavinpan p2]# !.
./printf1 
2 strings "red" and "blue"

自定义输入/输出函数

可以基于getchar()putchar()自定义所需的函数。
自定义输入的简单示例:

/* 自定义输出 */
#include 

#define STLEN 10

void get1(char *st, int len);

int main(void) {
    char words[STLEN];

    printf("Enter your string:\n");

    get1(words, STLEN);

    printf("Your string is: %s\n", words);
    return 0;
}

void get1(char *st, int len) {
    int i;
    char c;
    for (i = 0; (c = getchar()) != '\n' && i < len - 1; i++) {
        st[i] = c;
    }
    st[i] = '\0';       // 最后一位存储空字符

    if (c != '\n') {
        while (getchar() != '\n') {     // 丢弃输入中的多余字符
            continue;
        }
    }

输出结果:

[root@gavinpan p2]# !.
./get1 
Enter your string:
Welcome
Your string is: Welcome
[root@gavinpan p2]# !.
./get1 
Enter your string:
Your string is good.
Your string is: Your stri

自定义输出简单示例:

/* put1.c -- 打印字符串, 不添加\n */
#include 

void put1(const char *str1);

int main(void) {
    char *str1 = "hello, world!";
    put1(str1);
    return 0;
}

void put1(const char *str1) {
    while (*str1) {
        putchar(*str1++);
    }
}

输出结果:

[root@gavinpan p2]# !.
./put1 
hello, world![root@gavinpan p2]# 	// 不添加换行符

Mark Done.


  1. https://www.cnblogs.com/kuliuheng/p/11698378.html ↩︎

你可能感兴趣的:(C,c语言)