预计阅读本页时间:-
当然,最基本的您已经知道了:字符串(character string)是以空字符(\o)结尾的char数组。因此,您所学的数组和指针知识就可以用到字符串上。但是由于字符串的使用非常广泛,C提供了很多专为字符串设计的函数。本章将讨论字符串的特性、声明和初始化方法、如何在程序中输入输出字符串,以及字符串的操作。
程序清单11.1给出了一个程序,其中说明了建立、读入和输出字符串的几种方式。该程序使用了两个新的函数:gets()和puts(),其中gets()读入字符串,puts()输出字符串(您可能已经注意到了它们和getchar()、putchar()系列函数的相似性)。程序的其他部分在您看起来会比较熟悉。
广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元
程序清单11.1 strings.c程序
下面是一个运行示例,可以看到程序的功能:
与其逐行解释程序清单11.1,不如采用一种更为行之有效的方法。首先,我们看一下在程序中定义字符串的几种方法;然后您将了解把字符串读入程序中所涉及的操作;最后,您将学会如何输出字符串。
11.1.1 在程序中定义字符串
阅读程序清单11.1时您可能已经注意到,定义字符串的方法很多。基本的办法是使用字符串常量、char数组、char指针和字符串数组。程序应确保有存储字符串的地方,这一点我们稍后也会讨论到。
一、字符串常量(字符串文字)
字符串常量(string constant),又称字符串文字(string literal),是指位于一对双引号中的任何字符。双引号里的字符加上编译器自动提供的结束标志\0字符,作为一个字符串被存储在内存里。程序中使用了几个这样的字符串常量,大多数是用作函数printf()和puts()的参数。注意,还可以用#define来定义字符串常量。
如果字符串文字中间没有间隔或者间隔的是空格符,ANSI C会将其串联起来。例如,
和
是相等的。
如果想在字符串中使用双引号,可以在双引号前加一个反斜线符号,如下所示:
它的输出如下:
字符串常量属于静态存储(static storage)类。静态存储是指如果在一个函数中使用字符串常量,即使是多次调用了这个函数,该字符串在程序的整个运行过程中只存储一份。整个引号中的内容作为指向该字符串存储位置的指针。这一点与把数组名作为指向数组存储位置的指针类似。如果事实确实如此,程序清单11.2中的程序的输出会是什么?
程序清单11.2 quotes.c程序
%s格式将输出字符串We。%p格式产生一个地址。因此如果“are”是个地址,那么%p应该输出字符串中第一个字符的地址(ANSI之前的实现中可能用%u或%lu而不用%p)。最后,*“space farers”应该产生所指向的地址中的值,即字符串“space farers”中的第一个字符。真的是这样吗?下面是输出结果:
二、字符串数组及其初始化
定义一个字符串数组时,您必须让编译器知道它需要多大空间。一个办法就是指定一个足够大的数组来容纳字符串,下面的声明用指定字符串中的字符初始化数组m1:
const表明这个字符串不可以改变。
这种初始化和下面所示的标准数组初始化相比是很简短的:
注意标志结束的空字符。如果没有它,得到的就只是一个字符数组而不是一个字符串。
指定数组大小时,一定要确保数组元素数比字符串长度至少多1(多出来的1个元素用于容纳空字符)。未被使用的元素均被自动初始化为0。这里的0是char形式的空字符,而不是数字字符0。请参见图11.1。
图11.1 数组初始化
通常,让编译器决定数组大小是很方便的。回忆一下,在进行初始化声明时如果省略了数组大小,则该大小由编译器决定。
初始化字符数组是体现由编译器决定数组大小的优点的又一个例子。这是因为字符串处理函数一般不需要知道数组的大小,因为它们能够简单地通过查找空字符来确定字符串结束。
请注意程序必须为数组name明确分配大小:
由于直到程序运行时才能读取name的内容,所以除非您说明,编译器无法预先知道需要为它预留多大空间。当前没有字符串常量可以让编译器计算字符数,因此我们假定80个字符足以容纳用户的名字。声明一个数组时,数组的大小必须为整型常量,而不能是运行时得到的变量值。编译时数组大小被锁定到程序中(事实上,在C99中可以使用变长数组,但仍然无法预先知道数组大小应为多大)。
和任何数组名一样,字符数组名也是数组首元素的地址。因此,下面的式子对于数组ml成立:
的确,可以使用指针符号建立字符串。例如,程序清单11.1中使用了下面的声明:
这个声明和下列声明的作用几乎相同:
上面两个都声明m3是一个指向给定字符串的指针。在两种情况下,都是被引用的字符串本身决定了为字符串预留的存储空间大小。尽管如此,这两种形式并不完全相同。
三、数组与指针
那么,数组和指针形式的不同是什么呢?数组形式(m3[])在计算机内存中被分配一个有38个元素的数组(其中每个元素对应一个字符,还有一个附加的元素对应结束的空字符';\0')。每个元素都被初始化为相应的字符。通常,被引用的字符串存储在可执行文件的数据段部分;当程序被加载到内存中时,字符串也被加载到内存中。被引用的字符串被称为位于静态存储区。但是在程序开始运行后才为数组分配存储空间。这时候,把被引用的字符串复制到数组中(第12章“存储类、链接和内存管理”会更详细地讲述内存管理)。此后,编译器会把数组名m3看作是数组首元素的地址&m3[0]的同义词。这里重要的一点是,在数组形式中m3是个地址常量。您不能更改m3,因为这意味着更改数组存储的位置(地址)。可以使用运算符m3+l来标识数组里的下一个元素,但是不允许使用++m3。增量运算符只能用在变量名前,而不能用在常量前。
指针形式(*m3)也在静态存储区为字符串预留38个元素的空间。此外,一旦程序开始执行,还要为指针变量m3另外预留一个存储位置,以在该指针变量中存储字符串的地址。这个变量初始时指向字符串的第一个字符,但是它的值是可以改变的。因此,可以对它使用增量运算符。例如,++m3将指向第二个字符E。
总之,数组初始化是从静态存储区把一个字符串复制给数组,而指针初始化只是复制字符串的地址。
这些区别重要吗?通常并不重要,但是这要取决于做什么。下面的讨论中将用一些例子来说明。
四、数组和指针的差别
我们研究一下初始化一个存放字符串的字符数组和初始化一个指向字符串的指针这两者的不同(指向字符串其实是指向字符串的第一个字符)。例如,考虑下面两个声明:
主要的差别在于数组名heart是个常量,而指针head则是个变量。实际使用中又有什么不同呢?
首先,两者都可以使用数组符号:
以下是输出:
其次,两者都可以使用指针加法:
输出仍然如下:
但是只有指针可以使用增量运算符:
产生的输出如下:
假定希望head与heart相同,可以这样做:
这就使得head指针指向heart数组的首元素。但是,不能这样做:
这种情况类似于x=3;和3=x;。赋值语句的左边必须是一个变量或者更一般地说是一个左值(lvalue) ,比如*p_int。顺便提一下,head=heart;不会使Millie字符串消失,它只是改变了head中存储的地址。但是除非已在别处保存了“I love Millie! ”的地址,否则当head指向另一个地址时就没有办法访问这个字符串了。
可以改变heart中的信息,方法是访问单个的数组元素:
或者:
数组的元素是变量(除非声明数组时带有关键字const),但是数组名不是变量。
让我们回到对指针初始化的讨论:
可以用指针改变这个字符串吗?
您的编译器可能会允许上面的情况,但按照当前的C标准,编译器不应该允许这样做。这种语句可能会导致内存访问错误。原因在于编译器可能选择内存中的同一个单个的拷贝,来表示所有相同的字符串文字。例如,下面的语句都指向字符串“Klingon”的同一个单独的内存位置。
这就是说,编译器可以用相同的地址来替代每一个“Klingon”的实例。如果编译器使用这种单个拷贝表示法并且允许把pl[0]改为‘F’的话,那将会影响到所有对这个字符串的使用。于是,打印字符串文字“Klingon”的语句实际将会显示“Flingon”:
实际上,有些个编译器确实是按这种容易混淆的方式工作,而其他的一些则会产生程序异常中断。因此,建议的做法是初始化一个指向字符串文字的指针时使用const修饰符:
用一个字符串文字来初始化一个非const的数组,则不会导致此类问题,因为数组从最初的字符串得到了一个拷贝。
五、字符串数组
有一个字符串数组是很方便的。这样就可以使用下标来访问多个不同的字符串。程序清单11.1就使用了下面这个例子:
让我们研究一下上面的声明。因为LIM是5,所以mytal是一个由5个指向char的指针组成的数组。也就是说,mytal是个一维数组,而且数组里的每一个元素都是一个char类型值的地址。第一个指针是mytal[0],它指向第一个字符串的第一个字符。第二个指针是mytal[1],它指向第二个字符串的开始。一般地,每一个指针指向相应字符串的第一个字符:
依此类推。mytal数组实际上并不存放字符串,它只是存放字符串的地址(字符串存在程序用来存放常量的那部分内存中)。可以把mytal[0]看作表示第一个字符串,*mytal[0]表示第一个字符串的第一个字符。由于数组符号和指针之间的关系,也可以用mytal[0][0]表示第一个字符串的第一个字符,尽管mytal并没有被定义成二维数组。
字符串数组的初始化遵循数组初始化的规则。花括号里那部分的形式如下:
省略号代表我们懒得键入的内容。关键之处是第一对双引号对应着一对花括号,用于初始化第一个字符串指针。第二对双引号初始化第二个指针,等等。相邻字符串要用逗号隔开。
另一个方法就是建立一个二维数组:
在这里mytal_2是一个5个元素的数组,每一个元素本身又是一个81个char的数组。在这种情况下,字符串本身也被存储在数组里。两者差别之一就是第二种方法选择建立了一个所有行的长度都相同的矩形(rectangular)数组。也就是说,每一个字符串都用81个元素来存放。而指针数组建立的是一个不规则(ragged)的数组,每一行的长度由初始化字符串决定:
这个不规则数组不浪费任何存储空间。图11. 2示意了这两种类型的数组(实际上,mytal数组元素指向的字符串不必在内存中连续存放,但该图确实示意了存储需求的不同)。
另外一个区别就是mytal和mytal_2的类型不同;mytal是一个指向char的指针的数组,而mytal_2是一个char数组的数组。一句话,mytal存放5个地址,而mytal_2存放5个完整的字符数组。
图11.2 矩形数组和不规则数组
11.1.2 指针和字符串
可能您已经注意到在对字符串的讨论中会不时地用到指针。绝大多数的C字符串操作事实上使用的都是指针。例如,考虑一下程序清单11.3所示的用于起到指示作用的程序。
程序清单11.3 p_and_s.c程序