C primer plus 11.1 表示字符串和字符串I/O

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

字符串是以空字符(\0)结尾的char类型数组。因此,可 以把上一章学到的数组和指针的知识应用于字符串。不过,由于字符串十分 常用,所以 C提供了许多专门用于处理字符串的函数。

重点内容:字符串的 性质、如何声明并初始化字符串、如何在程序中输入和输出字符串,以及如何操控字符串。

下列示例程序演示了在程序中表示字符串的几种方式:

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

11.1.1 在程序中定义字符串

在上述示例程序中使用了多种方法定义字符串:

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

用双引号括起来的内容称为字符串字面量(string literal),也叫作字符 串常量(string constant)。双引号中的字符和编译器自动加入末尾的\0字 符,都作为字符串储存在内存中,所以"I am a symbolic stringconstant."、"I am a string in an array."、"Something is pointed at me."、"Here are some strings:"都是字符串字面量。

从ANSI C标准起,如果字符串字面量之间没有间隔,或者用空白字符 分隔,C会将其视为串联起来的字符串字面量。例如:

char greeting【50】=“Hello,and”“how are ”“you” “today !”;

与下面代码等价:

char greeting[50]="Hello,and how are you today!";

如果要在字符串内部使用双引号,必须在双引号前面加上一个反斜杠 (\):

printf(""Run, Spot, run!" exclaimed Dick.\n");

输出如下:

"Run, Spot, run!" exclaimed Dick.

字符串常量属于静态存储类别(static storage class),这说明如果在函 数中使用字符串常量,该字符串只会被储存一次,在整个程序的生命期内存 在,即使函数被调用多次。用双引号括起来的内容被视为指向该字符串储存 位置的指针。这类似于把数组名作为指向该数组位置的指针。

下列示例程序演示了上述原理:

printf()根据%s 转换说明打印 We,根据%p 转换说明打印一个地址。因 此,如果"are"代表一个地址,printf()将打印该字符串首字符的地址(如果使 用ANSI之前的实现,可能要用%u或%lu代替%p)。最后,*"space farers"表 示该字符串所指向地址上储存的值,应该是字符串 *"space farers"的首字符。

下面是程序的输出:

2.字符串数组和初始化

定义字符串数组时,必须让编译器知道需要多少空间。一种方法是用足 够空间的数组储存字符串。在下面的声明中,用指定的字符串初始化数组 m1:

const char m1[40]="Limit yourself to one line's worth.";

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

这种形式的初始化比标准的数组初始化形式简单得多:

const char m1[40]= { 'L','i', 'm', 'i', 't', ' ', 'y', 'o', 'u', 'r', 's', 'e', 'l', 'f', ' ', 't', 'o', ' ', 'o', 'n', 'e', ' ','l', 'i', 'n', 'e', '", 's', ' ', 'w', 'o', 'r','t', 'h', '.' , '\0' };

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

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

通常,让编译器确定数组的大小很方便。回忆一下,省略数组初始化声 明中的大小,编译器会自动计算数组的大小:

const charm2[]="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 char ar1[] = "Something is pointing at me.";

以上两个声明表明,pt1和ar1都是该字符串的地址。

在这两种情况下, 带双引号的字符串本身决定了预留给字符串的存储空间。尽管如此,这两种 形式并不完全相同。

3.数组和指针

数组形式:在上述声明中,数组形式(ar1[]) 在计算机的内存中分配为一个内含29个元素的数组(每个元素对应一个字 符,还加上一个末尾的空字符'\0'),每个元素被初始化为字符串字面量对应的字符。字符串都作为可执行文件的一部分储存在数据段中。当把 程序载入内存时,也载入了程序中的字符串。字符串储存在静态存储区 (static memory)中。但是,程序在开始运行时才会为该数组分配内存。此时,才将字符串拷贝到数组中(第 12 章详细讲解)。注意,此时字符串有两个副本,一个是在静态内存中的字符串字面量,另一个是储存在ar1数组中的字符串。

此后,编译器便把数组名ar1识别为该数组首元素地址(&ar1[0])的别 名。这里关键要理解,在数组形式中,ar1是地址常量。不能更改ar1,如果 改变了ar1,则意味着改变了数组的存储位置(即地址)。可以进行类似 ar1+1这样的操作,标识数组的下一个元素。但是不允许进行++ar1这样的操 作。递增运算符只能用于变量名前(或概括地说,只能用于可修改的左 值),不能用于常量。

指针形式:指针形式(*pt1)也使得编译器为字符串在静态存储区预留29个元素的空间。另外,一旦开始执行程序,它会为指针变量pt1留出一个储存位置, 并把字符串的地址储存在指针变量中。该变量最初指向该字符串的首字符, 但是它的值可以改变。因此,可以使用递增运算符。例如,++pt1将指向第 2 个字符(o)。

字符串字面量被视为const数据。由于pt1指向这个const数据,所以应该 把pt1声明为指向const数据的指针。这意味着不能用pt1改变它所指向的数 据,但是仍然可以改变pt1的值(即,pt1指向的位置)。如果把一个字符串 字面量拷贝给一个数组,就可以随意改变数据,除非把数组声明为const。

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

下列示例程序演示了这一点:

下面是在我们的系统中运行该程序后的输出:

该程序的输出说明了:
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是变量。

实际使用中的区别:

1.两者都可以使用数组表示法:

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

2.两者都可以进行指针加法操作:

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

但是只有指针表示法可以进行递增操作:

while ((head) != '\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 = "Klingon";

p1[0] = 'F'; // ok?

printf("Klingon");

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

也就是说,编译器可以用相同的地址替换每个"Klingon"实例。如果编译 器使用这种单次副本表示法,并允许p1[0]修改'F',那将影响所有使用该字 符串的代码。所以以上语句打印字符串字面量"Klingon"时实际上显示的 是"Flingon":

Flingon: Beware the Flingons!

实际上在过去,一些编译器由于这方面的原因,其行为难以捉摸,而另 一些编译器则导致程序异常中断。因此,建议在把指针初始化为字符串字面 量时使用const限定符:

const char * pl = "Klingon"; // 推荐用法

然而,把非const数组初始化为字符串字面量却不会导致类似的问题。 因为数组获得的是原始字符串的副本。

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

5.字符串数组

如果创建一个字符数组会很方便,可以通过数组下标访问多个不同的字符串。

下列示例程序演示了两种方法:指向字符串的指针数组和char类型数组的数组:

输出结果如下:

从某些方面来看,mytalents和yourtalents非常相似。两者都代表5个字符 串。使用一个下标时都分别表示一个字符串,如mytalents[0]和 yourtalents[0];使用两个下标时都分别表示一个字符,例如 mytalents[1][2]表 示 mytalents 数组中第 2 个指针所指向的字符串的第 3 个字符'l', yourtalents[1][2]表示youttalentes数组的第2个字符串的第3个字符'e'。而且, 两者的初始化方式也相同。

但是,它们也有区别。mytalents数组是一个内含5个指针的数组,在我 们的系统中共占用40字节。而yourtalents是一个内含5个数组的数组,每个数 组内含40个char类型的值,共占用200字节。

所以,虽然mytalents[0]和 yourtalents[0]都分别表示一个字符串,但mytalents和yourtalents的类型并不相 同。mytalents中的指针指向初始化时所用的字符串字面量的位置,这些字符 串字面量被储存在静态内存中;而 yourtalents 中的数组则储存着字符串字面 量的副本,所以每个字符串都被储存了两次。

此外,为字符串数组分配内存 的使用率较低。yourtalents 中的每个元素的大小必须相同,而且必须是能储 存最长字符串的大小。

我们可以把yourtalents想象成矩形二维数组,每行的长度都是40字节; 把mytalents想象成不规则的数组,每行的长度不同。下图演示了这两种数 组的情况(实际上,mytalents 数组的指针元素所指向的字符串不必储存在连续的内存中,图中所示只是为了强调两种数组的不同)。

综上所述,如果要用数组表示一系列待显示的字符串,请使用指针数 组,因为它比二维字符数组的效率高。但是,指针数组也有自身的缺点。**

**mytalents 中的指针指向的字符串字面量不能更改;而yourtalentsde 中的内容 可以更改。所以,如果要改变字符串或为字符串输入预留空间,不要使用指 向字符串字面量的指针。

11.1.2 指针和字符串

在讨论字符串时或多或少会涉及指针。实际 上,字符串的绝大多数操作都是通过指针完成的。

下列示例程序演示了指针和字符串的联系:

注意

如果编译器不识别%p,用%u或%lu代替%p。

你可能认为该程序拷贝了字符串"Don't be a fool!",程序的输出似乎也验证了你的猜测:

程序分析:仔细分析最后两个printf()的输出。首先第1项,mesg和copy都以 字符串形式输出(%s转换说明)。这里没问题,两个字符串都是"Don't be a fool!"。

接着第2项,打印两个指针的地址。如上输出所示,指针mesg和copy分 别储存在地址为0x0012ff48和0x0012ff44的内存中。

注意最后一项,显示两个指针的值。所谓指针的值就是它储存的地址。 mesg 和 copy 的值都是0x0040a000,说明它们都指向的同一个位置。因此, 程序并未拷贝字符串。语句copy = mesg;把mesg的值赋给copy,即让copy也指 向mesg指向的字符串。

为什么要这样做?为何不拷贝整个字符串?假设数组有50个元素,考虑 一下哪种方法更效率:拷贝一个地址还是拷贝整个数组?通常,程序要完成 某项操作只需要知道地址就可以了。如果确实需要拷贝整个数组,可以使用 strcpy()或strncpy()函数。

posted @ 2022-02-07 17:38  喻雅芬  阅读(129)  评论(0)    收藏  举报