第11章 字符串和字符串函数

发布于 2021-09-07  652 次阅读


前言

C库提供大量的函数用于读写字符串、拷贝字符串、比较字符串、合并字符串、查找字符串等。

11.1 表示字符串和字符串I/O

字符串是以空字符'\0'结尾的char类型数组。
因此可以把数组和指针的知识运用于字符串。由于字符串十分常用,所以C提供了许多专门用以处理字符串的函数。
本章将讨论字符串的性质、如何声明并初始化字符串、如何在程序中输入和输出字符串,以及如何操控字符出串。
#include<stdio.h>
#define MSG "I am a sysmbolic string constant."
#define MAXLENGTH 81
int main(void)
{
    char words[MAXLENGTH] = "I am a string in an array.";
    const char *pt1 = "Something is ointing at me.";
    puts("Here are some strings:");
    puts(MSG);
    puts(words);
    puts(pt1);
    words[8] = 'p';
    puts(words);

    return 0;
 }

file

和printf()函数一样,puts()函数也属于stdio.h系列的输入/输出函数。但是,与printf()不同的是,puts()函数值只显示字符串,而且自动在显示的字符串末尾加上换行符。
先学习把字符串读入程序涉及的一些操作,最后学习如何输出字符串

11.1.1 在程序中定义字符串

上述代码中使用了多种方法(字符串常量、char类型数组、指向char的指针)定义字符串。程序应确保有足够的空间存储字符串。

1.字符串字面量(字符串常量)

用双引号括起来定内容称为字符串字面量(string literal),也叫做字符串常量(string constant).双引号中的字符和编译器自动加入末尾的\0字符。都作为字符串存储在内存中。所以上述双引号中的内容都是字符串字面量。
从ANSI C标准起,如果字符串字面量之间没有间隔,或者用空白字符分隔,C会将其视为串联起来的字符串字面量。例如:
char greeting[50] = "hello, and""how are" "you" " today!";
与下面代码等价:
char greeting[50] = "hello, and how are you today!";
如果要在字符串内部使用双引号,必须在双引号前面加上一个反斜杠(\):
printf("\n"Run,spot,run!\" exclaimed Dick.\n");
输出如下:
“Run,stop,run!"exclaimed Dick.
字符串常量属于静态存储类别(static storage class),这说明了如果在函数中使用字符串常量,该字符串只会被存储一次,在整个程序的生命周期内存在,即使函数被调用多次。用双引号括起来的内容被指向该字符串存储位置的指针。这类似于把数组名作为该数组位置的指针。
#include<stdio.h>
int main(void)
{
    printf("%s,%p,%c\n","we","are",*"space farers");

    return 0;
}

file

printf()根据%s转换说明打印We,根据%p打印说明打印一个地址,因此如果”are“代表一个地址。printf()将打印该字符串首字符的地址。最后*"space farers"表示该字符串所指向地址上存储的值,应该是字符串*”space farers“的首字符。

2. 字符串数组和初始化

定义字符串数组时,必须让编译器知道需要多少空间。一种方法是用足够的数组存储字符串。在下面声明中,用指定的字符串初始化数组m1:
const char m1[40]="Limit yourself to one line's worth";
const表明不会更改这个字符串这种形式的初始化比标准的数组初始化形式简单得多:
const char m1[40] ={'','',...,'\0'};
注意最后的空字符。没有这个空字符,这就不是一个字符串,而是一个字符串数组。
在指定数组大小时,要确保数组的元素个数至少比字符串长度多1(为了容纳空字符)。所有未被使用的元素都被自动初始化为0(这里的0指的是char形式的空字符,不是数字字符0)。

file

通常,让编译器确定数组的大小很方便。省略数组初始化声明中的大小,编译器会自动计算数组的大小:
const char m2[] = "If you can't think of anything, fake it.";
让编译器确定初始化字符数组的大小很合理。因为处理字符串的函数通常都不知道数组的大小,这些函数通过查找字符串末尾的空字符确定字符串在何处结束。
让编译器计算数组的大小只能用在初始化数组时。如果创建一个稍后再填充的数组,就必须再声明时指定大小。声明数组时,数组大小必须是可求值的整数。在C99新增变长数组之前,数组的大小碧玺是整型常量,包括由整型常量组成的表达式:
int n =8;
char cookies[1];    // 有效
char cakes[2+5];    //有效
char pies[2*sizeof(long double) + 1];   //有效
char crumbs[n];     //在C99之前无效,C99之后这种数组是变长数组
字符数组名和其他数组名一样,是该元素首元素的地址。因此假设有以下初始化:
char car[10] = "Tata";
那么以下表达式都为真:
car == &car[0];
*car =='T'
*(car+1) == car[1] == 'a'
还可以使用指针表示法创建字符串。
const char *pt1 = "Something is pointing at me.";
该声明和下面的声明几乎相同:
const int ar1[] = "Something is pointing at me.";
以上两个声明表示,pt1和ar1都是该字符串的地址。在这两种情况下,带双引号的字符串本省决定了预留给字符串的存储空间。这两种形式并不完全相同。

3. 数组和指针

数组形式和指针形式有什么不同?以上面声明为例子,数组形式(ar1[])在计算机的内存中分配一个内含29个元素的数组(每个元素对于一个字符,还加上一个农委的空字符'\0'),每个元素被初始化为字符串字面量对应的字符。通常,字符串都作为可执行文件的一部分存储在数据段中。当把程序载入内存时,也载入了程序中的字符串。字符串存储在静态存储区(static memory)中。但是,当程序在开始运行时才会为该数组分配内存。此时,才将字符串拷贝到数组中。注意,此时字符串有两个副本。一个是在静态内存中的字符串字面量,另一个是存储在ar1数组中的字符串。
此后,编译器便把数组名为ar1识别为该数组元素的首元素地址(&ar1[0])的别名。这里关键要理解,在数组形式中,ar1是地址常量。不能更改ar1,如果改变了ar1,则意味者改变了数组的存储位置。可以进行类似于ar1+1这样的操作,标识数组的下一个元素。但是不允许经行++ar1这样的操作。递增运算符只能用于变量名前(只能用于可修改的左值),不能用于常量。
指针形式(*pt1)也使得编译器为字符串在静态存储区预留29个元素的空间。另外一旦开始执行程序,它会为指针变量pt1留出来一个存储位置,并把字符串的地址存储在指针变量中。该变量是最初指向该字符串的首支付,但是它的值可以改变。因此,可以使用递增运算符。例如,++pt1将指向第2个字符(o)。
字符串字面量被视为const数据。由于pt1指向这个const数据,所以应该把pt1声明为指向const数据的指针。这意味着不能用pt1改变它数据,但是仍然可以改变pt1的值。如果把一个字符串字面量拷贝给一个数组,就可以随意改变数据,除非把数组声明为const。
总之,初始化数组把静态存储区的字符串拷贝到数组中,而初始化指针置只把字符串的地址拷贝给指针
#include<stdio.h>
#define MSG "I'm special"

int main(void)
{
    char ar[] = MSG;
    const char *pt = MSG;
    printf("address of \"I'm special\":%p\n","I'm special");
    printf("               address ar:%p\n",ar);
    printf("               address pt:%p\n",pt);
    printf("               address MSG:%p\n",MSG);
    printf("address of \"I'm special\":%p\n","I'm special");

    return 0;
}

file

这说明了:
1.pt和MSG的地址相同,而ar的地址不同,这与我们之前的内容一致。
2.虽然字符串字面量”I‘m special“在程序的两个printf()函数中出现了两次,但是编译器只使用了一个存储位置,而且与MSG的地址相同。编译器可以把多次使用的相同字面量存储在一处或多处。另一个编译器可能在不同位置存储了3个”I’m special“。3,静态数据使用的内存与ar使用的动态内存不同.不仅值不同,特定编译器甚至使用不同位数表示两种内存。

4.数组和指针的区别

初始化字符数组来存储字符串和初始化指针来指向串有何区别?
char heart[] = "I love Tillie!";
const char *head = "I love Millie!";
两者的主要区别是:数组名heart是常量,而指针名head是变量。那么,实际使用有什么区别?
首先,两者都可以使用数组表示法:
for(i = 0;i < 6;i++)
    putchar(heart[i]);
putchar('\n');
for(i = 0;i < 6;i++)
    putchar(head[i]);
putchar('\n');
上面两段代码的输出是:
I love
I love
其次两者都能进行指针加法操作:
for(i=0;i < 6;i++)
    putchar(*(heart+1));
putchar('\n');
for(i=0;i < 6;i++)
    putchar(*(head+i));
putchar('\n');
输出如下:
I love
I love
但是,只有指针表示法可以进行递增操作:
while(*(header) != '\0')  //在字符串末尾处结束
 putchar(*(head++));        //打印字符,指针指向下一个位置
 这段代码的输出如下:
 I love Millie!
 假设想让head和heart统一,可以这样做
 head = heart;  //head现在指向数组heart
 这使得head指针指向heart数组首元素。、
但是不能这样做:
heart = head;       //非法构造
这类似于 x = 3;和3 = x;的情况。赋值运算符的左侧必须是变量,(或者概括的说可修改的左值),如*pt_int。顺带一提,head = heart;不会导致head指向的字符串消失,这样做只是改变了存储在head中的地址。除非已经保存了”I love Millie“的地址,否则当head指向别处时,就无法再访问该字符。
另外,还可以改变heart数组中元素的信息:
heart[7] = 'M';或者*(heart + 7) = 'M';
数组的元素变量(除非数组被声明为const),但是数组名不是变量。
我们来看一下未使用const限定符的指针初始化:
char *word = "frame";
是否能使用该指针修改这个字符串?
word[1] = '1';        //是否允许?
编译器可能允许这样做,但是对当前的C标准而言,这样的的行为是未定义的。例如,这样的语句可能导致内存访问错误。原因前面提到过,编译器可以使用内存中的一个副本来表示所以完全相同的字符串字面量。例如下面语句都引用字符串”Klingon“的一个内存位置:
char *p1  = “Klingo”;
p1[0] = 'F';      //OK?
printf("Klingo");
printf(": Beware the %ss!\n","Klingo");
也就是说,编译器可以用相同的地址替换每个"Klingo"实例。如果编译器使用这种单次副本表示法,并允许p1[0]修改'F',那将影响所有使用该字符串的代码。所以以上语句打印字符字面量"Klingo"时,实际上显示的是"Flingo":
Flingo: Beware the Flingo!
实际上在过去,一些编译器由于这方面的原因,其行为难以捉摸,而另一些编译器则导致程序异常中断。因此,建议在把指针初始化为字符串字面量时使用时使用const限定符:
const char *p1 = “Klingon”;     // 推荐用法
然而,把非const数组初始化字符串字面量却不会导致类似的问题。因为数组获得的是原始字符串的副本。
总之,如果打算修改字符串,就不要用指针指向字符串字面量。

5.字符串数组

创建一个字符数组通常很方便,可以通过数组下标访问多个不同的字符串。
指向字符串的指针数组和char类型数组的数组。
//arrchar.c --指针数组,字符串数组
#include<stdio.h>
#define SLEN 40
#define LIM 5
int main(void)
{
    const char *mytalents[LIM] = {
    "Adding numbers swiftly",
    "Multiplying accurately","Stashing data",
    "Following instructions to the letter",
    "Understanding the C language"
    };
    char yourtalents[LIM][SLEN] = {
    "Walking in a straight line",
    "Sleeping","Watching television",
    "Maling letters","Reading email"
    };
    int i;

    puts("Let's compare talents");
    printf("%-36s %-25s\n","My Talents","Your Talents");
    for(i=0;i<LIM;i++)
        printf("%-36s %-25s\n",mytalents[i],yourtalents[i]);
    printf("\nsizof mytalents:%zd,sizeof yourtalents:%zd\n",sizeof(mytalents),sizeof(yourtalents));
    return 0;
 } 

file

从某些方面来看,mytalents和yourtalents非常相似。两者都代表了5个字符串。使用一个下标时都分别表示一个字符串,如mytalents[0]和yourtalents[0];使用两个下标时都分别表示一个字符,如mytalents[1][2]表示mytalents数组中第2个指针所指的字符串的第3个字符'i',yourtalents[1][2]表示youtalents数组的第2个字符串的第3个字符'e',而且,两者的初始化方式也相同。
但是它们也是有区别的。mytalents数组是一个内含5个指针的数组,在我们系统中共占40字节。而yourtalents是一个内含5个数组的数组,每个数组内含40个char类型的值,共占200字节。所以,虽然mytalents[0]和yourtalents[0]都分别表示一个字符串,但mytalents和yourtalents的类型并不同,mytalents中的指针指向初始化时所用的字符串字面量的位置,这些字符串面量被储存在静态内存中:而yourtalents中的数组则储备者字符串字面量的副本,所以每个字符都被存储了两次。此外,为字符串数组分配内存的使用率较低。youtalents中的每个元素的大小必须相同,而且必须能储存最长字符串大小。
我们把youtalents想象成矩形二维数组,每行的长度都是40字节,把mytalents想象成不规则的数组,每行的长度不同。下图演示了这两种数组的情况(实际上,mytalents数组的指针元素所指向的字符串不必存储在连续的内次中,图中所示只是为了强调两种数组的不同)。

file

综上所述,如果要用数组表示一些列带显示的字符串,请使用指针数组,因为它比二维数组的效率高。但是,指针数组也有自身的缺点。mytalents中的指针指向的字符串字面量不能更改;而yourtalents中的内容可以更改。所以,如果要改变字符串或为字符串预留空间,不要使用指向字符串字面量的指针。

11.1.2 指针和字符串

字符串的大多数操作都是通过指针完成的
//p_and_s.c ---指针和字符串
#include<stdio.h>
int main(void)
{
    const char *mesg = "Don't be a fool!";
    const char *copy;

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

    return 0;
 } 

file

你可能认为程序拷贝了字符串“Don’t be a fool!”,程序的输出似乎也验证了你的猜测:
来分析最后两个printf()的输出。首先第一项,mesg和copy都以字符串形式输出(%s转换说明)。这里没问题,两个字符串都是“Don’t be a fool!”。
接着打印第二项,打印两个指针的值。
注意最后一项,显示两个指针的值。所谓指针的值就是它存储的地址。mesg和copy都是同一个。说明它们都指向同一个位置。因此程序并未拷贝字符串。语句copy = mesg;把mesg的值赋给copy,即让copy也指向mesg指向的字符串。
为什么这么做?为何不拷贝整个字符串?假设数组有50个元素,考虑一下那种方法更有效率:拷贝一个地址还是拷贝整个数组?通常,程序完成某项操作只需要知道地址就可以了。如果确实需要拷贝真个数组可以用strcpy()或者strncpy()函数。

擦肩而过的概率