第三章:指针和字符串字符串的学习将进一步将指针和数组联系起来。同时本章还将阐述标准C中一些字符串函数的作用方式,以及指针必须在何时或者应该怎样传递给函数。
C中,字符串是字符数组。这句话不一定适用于其它语言。在Basic,Pascal,Fortran或者其它什么语言中,字符串有他自己的数据类型。而在C中,字符串没有自己的数据类型,他是一个以二进制0字符(写作'\0')结束的字符数组。开始讨论之前,我们将看一段代码,这段代码更适合用来做演示说明,你在实际程序中可能永远不会这么写。好,上代码:
- 代码: 全选
char my_string[40];
my_string[0] = 'T';
my_string[1] = 'e';
my_string[2] = 'd';
my_string[3] = '\0';
尽管没人会这样构建一个字符串,但最终结果还是个字符串,一个以空字符(nul character)结尾的字符数组。根据定义,C中的字符串是一个以空字符结尾的字符数组。空字符的“空”跟“NULL”是不一样的。空字符的空是转义字符'\0',并且他只占一字节内存。另一方面,NULL则是一个初始化空指针的宏的名称。NULL在头文件中以#defined形式被定义,而空字符根本不需要定义。
上面那种代码的写法非常浪费时间,C提供了两种替代方案。第一种,你可以写成:
- 代码: 全选
char my_string[40] = {'T','e','d','\0'};
但是这种写法还是不够方便。因此,C还允许另一种写法:
- 代码: 全选
char my_string[40] = "Ted";
当使用双引号而不是单引号时,空字符('\0')自动追加到字符串的最后。上面的例子中发生了同样的事情:编译器预留一段40字节的连续内存来保存字符,并且初始化前4个字符为Ted\0。
现在,接着看下面的程序:
- 代码: 全选
------------------program 3.1-------------------------------------
/* Program 3.1 from PTRTUT10.HTM 6/13/97 */
#include <stdio.h>
char strA[80] = "A string to be used for demonstration purposes";
char strB[80];
int main(void)
{
char *pA; /* 一个字符指针 */
char *pB; /* 另一个字符指针 */
puts(strA); /* 打印strA */
pA = strA; /* 将pA指向strA */
puts(pA); /* 打印pA指向的字符串 */
pB = strB; /* pB指向strB */
putchar('\n'); /* 换行 */
while(*pA != '\0') /* A行 */
{
*pB++ = *pA++; /* B行 */
}
*pB = '\0'; /* C行 */
puts(strB); /* 打印strB */
return 0;
}
--------- end program 3.1 -------------------------------------
上面的代码我们定义了2个大小为80的字符数组。由于他们是全局定义,所以他们先被初始化为'\0',接着strA被初始化为引号中47个字符组成的字符串。
我们声明了2个字符指针,并在屏幕上打印了字符串。然后,我们将指针pA指向strA。实际上,表达式的意思是,将strA[0]的地址复制给变量pA。接着,我们用puts()将pA指向的值打印到屏幕上。puts()函数的原型是:
- 代码: 全选
int puts(const char *s);
暂时忽略const。传递给puts()的参数是一个指针,更确切的说是这个指针的值(因为C中传递的所有参数都是值),这个指针的值就是指针指向的地址,或者简单的说puts()的参数就是一个地址。因此,当我们写成puts(strA),实际上传递的是strA[0]的地址。
同理,我们写puts(pA);时,传参是同样的地址,因为pA = strA;
根据这个,接着看A行的while()表达式。A行的意思是:
只要pA指向的字符(即*pA)不是空字符(即没有用'\0'结尾),就一直执行下面的代码:
B行的意思是:复制pA指向的字符到pB指向的空间,然后自增pA确保它指向下一个字符,同时pB也指向下一个空间。
一旦我们复制了最后一个字符,pA就指向结尾的空字符并且循环终止。然而,我们并没有复制空字符,但是C中规定字符串必须以空字符结尾。所以我们在C行加上空字符。
运行这个程序,用调试器逐行观察strA,strB,pA和pB,对你的理解非常有帮助。如果将上面的strB的定义替换为如下:
- 代码: 全选
strB[80] = "12345678901234567890123456789012345678901234567890";
将会对你的理解更加有帮助。这里数字串的长度大于strA的长度,然后再单步运行下看看这些变量的变化。赶快动手试试吧!
咱们暂时回到puts()的原型上来,const是变量修饰器,他告诉用户这个函数不会修改s指向的字符串,就是说他将字符串看成一个常量。
当然,上面的代码实际上是一个简单的复制字符串的方法。试过上面的代码之后,你应该很了解到底发生了什么。我们甚至可以创建我们自己的函数来替代C的标准函数strcpy()。代码如下:
- 代码: 全选
char *my_strcpy(char *destination, char *source)
{
char *p = destination;
while (*source != '\0')
{
*p++ = *source++;
}
*p = '\0';
return destination;
}
(译者注:下面的代码更好一些
char *my_strcpy(char *destination, const char *source) {
assert((destination != NULL) && (source != NULL)); // assert用来检验参数是否正确
char *tmpAddress = destination; // 声明一个临时变量用来存储destination的首地址,因为在下面的代码中destination的地址发生了变化
while ((*destination++ = *source++) != '\0') ; // 将source全部复制到destination,复制完\0,退出循环
return tmpAddress; // 返回临时变量存储的destination的首地址,方便链式调用
}
)
这个例子中,我按照标准惯例返回了最终值的指针。
(译者注:这样做是为了函数的链式调用,如 int length = strlen(my_strcpy(strDest,"hello world"));)
这个函数被设计成可以接受两个字符指针的值(指针),因此前面的代码可以写成:
- 代码: 全选
int main(void) {
my_strcpy(strB,strA);
puts(strB);
}
我稍稍偏离了标准C的原型,标准C的原型是这么写的:
- 代码: 全选
char *my_strcpy(char *destination, const char *source);
这里的const修饰器告诉用户这个函数不会修改源指针指向的内容。你可以试着修改上面的函数和原型,将const修饰器添加进去。然后,在函数里你可以添加一个表达式去尝试修改源指针指向的内容,如下:
- 代码: 全选
*source = 'X';
这个表达式正常情况下会修改字符串的首字符为X。但是const修饰器却会让编译器捕获一个错误。动手试试看。
好,我们来看看上面的例子想告诉我们些什么。首先,*ptr++先返回ptr指向的值,然后再增加指针的值。这是根据操作符的优先级来的。当我们写成(*ptr)++时,我们增加的不是指针的值,而是增长指针指向的值!也就是说,如果(*ptr)++用在上面的例子中,首字母'T'会增长为'U'。你可以写些简单的示例代码证明这一点。
再次提醒下,字符串只不过是以'\0'结尾的字符数组。上面的例子实际上就是复制了一个数组,不过恰巧拷贝了字符数组。这种技术还可以用来复制整数数组,双精度数组等。只不过,因为处理的不是字符串,所以数组的结尾不是空字符。我们可以实现一个依赖结尾特殊值来复制的版本。比如说,我们可以复制一个结尾用负数标识的正整数数组。另一个更常见的情况,写一个函数去复制不是字符串的数组的部分元素。我们给函数传递需要复制元素的数量以及数组的地址,原型大概像下面这样:
- 代码: 全选
void int_copy(int *ptrA, int *ptrB, int nbr);
这里的nbr是需要复制整数的数量。你可以试着写int_copy(),再创建个整数数组,看看你的程序能不能跑起来。
这种方法还可以用来操作大数组。假设我们想操作一个包含5000个整数的数组,我们只需要给这个函数传数组的地址(当然还有其它辅助的参数,像上面的nbr参数,这取决于我们想做什么)。数组本身没有被传递,只传递了他的地址,就是说,函数调用前整个数组并没有被复制进堆栈。
这跟传递一个整数给函数是不一样的。当我们传递一个整数时,会生成一个复本,即将整数的值放进堆栈。函数中对这个传递值的任何操作都不会影响到原始整数。但是,由于数组和指针传递时是变量的地址,所以操作会改变原始变量的值。