当然,最基本的您已经知道了:字符串(character string)是以空字符(\o)结尾的char数组。因此,您所学的数组和指针就可以用在字符串上。但是由于字符串的使用非常广泛,C提供了很多专为字符串设计的函数。本章将讨论字符串的特性、声明和初始化方法、如何在程序中输入输出字符串,以及字符串的操作。
程序清单11.1 string.c程序
//string.c --使用字符串与用户交互
#include <stdio.h>
#define MSG "You must have many talents. Tell me some. "
#define LIM 5
#define LINELEN 81 //最大字符串长度加1
int main(void)
{
char name[LINELEN];
char talents[LINELEN];
int i;
const char m1[40]="Limit yourself to one line's worth. "; //初始化一个大小已确定的char数组
const char m2[]="If you can't think of anything,fake it. ";//让编译器计算数组大小
const char *m3="\nEnough about me - what's your name? "; //初始化一个指针
const char *mytal[LIM]={"Adding numbers swiftly",
"Multiplying accurately","Stashing date",
"Following instructions to the letter",
"Understanding the C language"}; //初始化一个字符串指针的数组
printf("Hi!I'm clyde the computer. "" I have many talents.\n");
printf("Let me tell you some of them.\n");
puts("What were they? Ah,yes,here's a partial list. ");
for (i=0;i<LIM;i++)
puts(mytal[i]); //打印计算机功能的列表
puts(m3);
gets(name);
printf("Well,%s,%s\n",name,MSG);
printf("%s\n%s\n",m1,m2);
gets(talents);
puts("Let's see if I've got that list: ");
puts(talents);
printf("Thanks for the information,%s.\n",name);
return 0;
}
11.1.1 在程序中定义字符串
阅读程序清单11.1时您可能已经注意到,定义字符串的方法很多。基本的办法是使用字符串常量、char数组、char指针和字符串数组。程序应该确保有存储字符串的地方,这一点我们稍后也会讨论到。
一、字符串常量
/*quotes.c 把字符串看作指针*/
#include <stdio.h>
int main(void)
{
printf("%s, %p, %c\n","we","are",*"space farers");
return 0;
}
%s格式将输出字符串we。%p格式产生一个地址,因此,如果“are”是一个地址,那么%p应该输出字符串中第一个字符的地址(ANSI之前可能用%u 或 %lu)。最后,*“space farers”应该产生所指向的地址中的值,即字符串“space farers”中的第一个字符。真的是这样吗?下面是输出结果:
we, 0x0040c010, s
二、字符串数组及其初始化
定义一个字符串数组时,您必须让编译器知道它需要多大空间。一个办法就是指定一个足够大的数组来容纳字符串,下面的声明用指定字符串中字符数初始化一个数组m1:
const char m1[40] = "Limit yourself to one line's worth. " ;
const表明这个字符串不可以改变。
注意标志结束的空字符。如果没有它,得到的就只是一个字符数组而不是一个字符串。
指定数组大小时,一定要确保数组元素数比字符串长度至少大1(多出来的一个元素用于容纳空字符)。
未被使用的元素均被自动初始化为0.这里的0是char形式的空字符,而不是数字字符0.
通常,让编译器决定数组大小 是很方便的。回忆一下,在进行初始化声明时如果活力了数组大小,则该大小由编译器决定。
const char m2 [ ] = "If you can't think of anything,fake it . ";
初始化字符数组是体现由编译器决定数组大小 的优点的又一个例子。这是因为字符串处理函数一般不需要知道数组的大小,因为它们能够简单的通过查找空字符来确定字符串的结束。
请注意程序必须为数组name明确分配大小:
#defeine LINELEN 81
......
char name [LINELEN];
由于直到程序运行时才能读取name的内容,所以除非您说明,编译器无法预先知道需要为它预留多大空间。声明一个数组时,数组 的大小必须为整形常量,而不能是运行时得到的变量值。
和任何数组一样,字符数组名也是数组首元素的地址。因此下面的式子对于数组m1成立:
m1 == &m1[0] , *m1 == 'L' , and *(m1+1) == m1[1] == 'i'
的确,可以使用指针符号建立字符串。例如,程序清单11.1中使用了下面的声明:
const char *m3 = "\nEnough about me - what's your name? " ;
这个声明和下面的声明的作用几乎相同:
char m3[ ] = "\nEnough about me - what's your name? " ;
上面两个声明m3是一个指向给定字符串的指针。在两种情况下,都是被引用的字符串本身决定了为字符串预留的存储空间的大小。尽管如此,这两个形式并不完全相同。
三、数组和指针
那么,数组和指针形式的不同之处是什么呢?
数组形式(m3 [ ])在计算机内存中被分配 一个有38个元素的数组,每个元素都被初始化为相应的字符。通常,被引用的字符存储在可执行文件的数据段部分;当程序被加载到内存中时,字符串也被加载到内存中。被引用的字符串被称为位于静态存储区。但是在程序开始运行后才为数组分配存储空间。这时候,把被引用的字符串复制到数组中。此后,编译器会把数组 名m3看作是数组首元素&m3[0]的同义词。这里重要的一点是,在数组形式中m3是个地址常量,您不能更改m3,因为这意味着更改数组存储的位置(地址)。可以使用运算符m3 + 1 来标识下一个元素,但是不允许使用++m3。增量运算符只能用在变量名前,而不能用在常量名前。
指针形式(*m3)也在静态存储区为字符串预留38个元素的空间。此外,一旦程序开始执行,还要为指针变量m3另外预留一个存储位置,以在该指针变量中存储字符串的地址。这个变量初始时指向字符串的第一个字符,但是它是可以变的。因此,可以对它使用增量运算符。例如,++m3将指向第二个字符E。
总之,数组初始化是从静态存储区把一个字符串复制给数组,而指针初始化只是复制字符串的地址。
这些区别重要与否,主要取决于做什么。
四、数组和指针的差别
我们研究一下初始化一个存放字符串的数组和初始化一个指向字符串的指针这两者的不同(指向字符串其实是指向字符串的第一个字符)。例如,考虑下面的两个声明:
char heart [ ] = "I love Tillie!" ;
char *head = "I love Millie!" ;
主要差别在于,数组名heart是个常量,而指针heart是个变量。实际使用中又有什么不同呢?
首先,两者都可以使用数组符号:
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+i));
putchar('\n');
for(i=0;i<6;i++)
putchar(*(head+i));
putchar('\n');
输出不变
但是,只有指针可以使用增量运算符:
while(*(head)!='\o')
putchar(*(head++));
产生如下输出: I love Millie!
假定希望head与heart相同,可以这样做:
head = heart ; /*现在head指向数组heart*/
但是不能这样做:
heart = head ; /*非法语句*/
赋值语句的左边必须是一个变量或者更一般的说是一个左值(lvalue),比如*p_int。顺便提一下,head = heart;不会使Millie字符串消失,它只是改变了head中存储的地址。但是,除非已在别处保存了“I love Millie!"的地址,否则当head指向另一个地址时就没有办法访问这个字符串了。
可以改变heart中的信息,方法是访问单个的数组元素:
heart[7] = 'M' ; 或者 *(heart + 7) = 'M' ;
数组的元素是变量(除非声明时带有关键字const),但是数组名不是变量。
让我们回到对指针初始化的讨论:
char * word = "frame" ;
可以用指针改变这个字符串吗?
word[1] = 'l' ;
您的编译器可能会允许上厕所情况,但按照当前的C标准,编译器不应该允许这样做。这种语句可能会导致内存访问错误。原因在于编译器可能选择内存中的同一个单个的拷贝,来表示所有相同的字符串文字。例如,下面的语句都指向字符串“Klingon"的同一个单独的内存位置。
char * p1 = "Klingon" ;
p1[0] = 'F' ;//ok?
printf("Klingon");
printf(": Beware the %ss!\n","Klingon");
这就是说,编译器可以用栅的地址来替代每个“Klingon"实例。如果编译器使用这种单个拷贝法并且允许把p1[0]改为‘F‘的话,那将影响到所有对这个字符串的使用。于是,打印字符串文字“Klingon"的语句将为显示为“Flingon"。
因此,建议的做法是初始化一个指向字符串文字的指针时使用const修饰符:
const char * p1 = "Klingon" ; //推荐做法
用一个字符串文字来初始化一个非const数组,则不会导致此类问题,因为数组从最初的字符串得到了一个拷贝。
五、字符串数组
字符串的初始化遵循数组初始化的规则。花括号里那部分的形式如下:
{{...},{...},...,{...}} ;
省略号代表我们懒得键入的内容。关键之处是第一对双引号对应着一对花括号,用于初始化第一个字符串指针。第二对双引号初始化第二个指针,等等。相邻字符串要用逗号隔开。
另一个方法就是建立一个二维数组:
char mytal_2[LIM] [LINLIM] ;
在这里mytal_2是一个5个元素的数组,每一个元素本身又是一个81个char的数组。在这种情况下,字符串本身也被存储在数组里。两者差别之一就是第二种方法选择建立了一个所有行的升序都相同的矩形(rectangular)数组。也就是说,每一个字符串都用81个元素来存放。而指针数组建立的是一个不规则的数组,每一行的长度由初始化字符串决定:
char * mytal [LIM] ;
这个不规则数组不浪费任何存储空间。
另外一个区别就是mytal和mytal_2的类型不同:mytal是一个指向char的指针的数组,而mytal_2是一个char数组的数组。一句话说,mytal存放5个地址,而mytal_2存放5个完整的字符数组。
11.1.2 指针和字符串
绝大多数的C字符串操作事实上使用的都是指针。例如,考虑一下程序清单11.3所示的用于起到指示作用的程序。
程序清单 11.3 p_and_s.c程序
/* p_and_s.c --指针和字符串 */
#include <stdio.h>
int main (void)
{
char * mesg = "Don't be a fool! ";
char * copy;
copy = mesg;
printf("%s\n",copy);
printf("mesg = %s; &mesg = %p; value = %p\n",
mesg,&mesg,mesg);
printf("copy = %s; © = %p; value = %p\n",
copy,©,copy);
return 0;
}
输出结果如下:
Don't be a fool!
mesg = Don't be a fool! ; &mesg = 0022FF4C; value = 00403024
copy = Don't be a fool! ; © = 0022FF48; value = 00403024
首先,mesg和copy以字符串形式输出(%s)。这里并没有发生奇怪的事。
每一行的下一项是指定指针的地址。mesg和copy这两个指针分别存放在位置0022FF4C和0022FF48。
注意最后一项,即value。它是指定指针的值。指针的值是该指针中存放的地址,可以看到mesg指向00403024,copy也是如此。因此,字符串本身没有被复制。语句copy = mesg ;所做的事情就是产生指向同一个字符串的第二个指针。
为什么如此谨慎行事?为什么不干脆复制整个字符串?好了,问一下自己哪一种方式更有效率?复制一个地址还是复制50个单个的元素?通常只有地址才是程序执行所需要的。
来源:oschina
链接:https://my.oschina.net/u/2754880/blog/733752