字符串常量是一个字符数组,它写成
"I am a string"
在内部表示中,数组以空字符 '\0' 结尾,这样程序就能找到其结尾。因此内存中的长度就比双引号之间的字符数多一。
可能最常见的字符串常量是作为函数参数来使用的,例如
printf("hello, world\n");
当像这样的字符串常量出现在程序中时,是通过字符指针来访问的;printf 收到一个指向字符数组开头的指针。也就是说,字符串常量是通过指向其首个元素的指针来访问的。
字符串常量不一定是函数参数。如果 pmessage 声明为
char *pmessage;
则语句
pmessage = "now is the time";
将 pmessage 赋值为指向字符数组的指针。这不是字符串拷贝;只涉及到指针。C语言没有提供任何能将字符数组作为一个整体单元来操作的操作符。
下面两个定义有重要的区别
char amessage[] = "now is the time"; /* 数组 */
char *pmessage = "now is the time"; /* 指针 */
amessage 是一个数组,恰好足够大,以包含初始的字符串序列及末尾的 '\0'。数组里面每个字符都可以独立修改,但 amessage 总是指向同一个内存空间。另一方面,pmessage 是一个指针,初始化时指向了一个字符串常量;这个指针后续可以修改以指向其他位置,但如果你想要修改字符内容的话,其结果是未定义的。
我们将研究从标准库改编而来的两个函数,用以说明指针和数组更多方面的内容。第一个函数是 strcpy(s, t),将字符串 t 拷贝到字符串 s。如果能简单写个 s=t 就好了,可惜那只是指针拷贝,而不是每个字符的拷贝。为了拷贝这些字符,我们需要使用循环。首先是数组版本:
/* 把t拷贝到s,数组下标版本 */
void strcpy(char *s, char *t)
{
int i;
i = 0;
while ((s[i] = t[i]) != '\0')
i++;
}
对比下面的指针版本
/* 把t拷贝到s,指针版本1 */
void strcpy(char *s, char *t)
{
while ((*s = *t) != '\0') {
s++;
t++;
}
}
由于参数是值传递的,strcpy 可以用它喜欢的任何方式来使用参数 t 和 s。在这里,它们都是已经初始化好的指针,每次都分别在各自的数组上前进一个字符,直到 t 结尾的 '\0' 拷贝到 s 后才停止。
实际上,strcpy 不会写成上面那样。有经验的程序员会写成
/* 把t拷贝到s,指针版本2 */
void strcpy(char *s, char *t)
{
while ((*s++ = *t++) != '\0')
;
}
这里把对 s 和 t 的递增移到了循环的检查部分。*t++ 的值是 t 在递增之前指向的字符;后缀的 ++ 要到这个字符被取出来之后才会改变 t。同样的,这个字符也被保存到 s 在递增之前的老位置。这个字符的值还被用来和 '\0' 比较,以控制这个循环。最终的效果就是字符都被从 t 拷贝到 s, 一直到(而且包括)末尾的 '\0'。
最后还能做个简化,因为问题仅仅是表达式是否为零,可知与 '\0' 的比较是多余的。因此这个函数可能写成:
/* 把t拷贝到s,指针版本3 */
void strcpy(char *s, char *t)
{
while (*s++ = *t++)
;
}
尽管第一眼看起来这有点晦涩难懂,但这种写法是相当方便的,而且你应当掌握这种习惯用法(idiom),因为你将会在C程序中频繁遇见它。
标准库(
我们要审视的第二个例程是 strcmp(s, t),它比较两个字符串 s 和 t,当 s 的字典顺序小于、等于或大于 t 的时候,分别返回负数、0 和 正数。返回值的获取,是将 s 和 t 第一个不相同的字符进行相减得到的。
int strcmp(char *s, char *t)
{
int i;
for (i = 0; s[i] == t[i]; i++)
if (s[i] == '\0')
return 0;
return s[i] - t[i];
}
指针版本为
int strcmp(char *s, char *t)
{
for ( ; *s == *t; s++, t++)
if (*s == '\0')
return 0;
return *s - *t;
}
由于 ++ 和 -- 不是前缀就是后缀操作符,也会有 * 和 ++/-- 的其他组合,尽管出现频率低一些。例如
*--p
在获取 p 指向的字符之前对 p 进行递减。实际上,下面这对语句
*p++ = val; /* 推val入栈 */
val = *--p; /* 栈顶出栈赋给val */
是出栈和入栈的标准用法;参见 4.3 节。
标准库头文件
练习5-3、写出第二章中 strcat 函数的指针版本:strcat(s, t) 将字符串 t 连接到字符串 s 的末尾。
练习5-4、写个函数 strend(s, t),如果字符串 t 出现在字符串 s 的末尾,则返回 1,否则返回 0。
练习5-5、实现库函数 strncpy,strncat 和 strncmp ,这些函数最多操作它们字符串参数的 n 个字符。例如, strncpy(s, t, n) 最多将 n 个字符从 t 拷贝到 s。函数的完整描述参见附录B。
练习5-6、前面章节正文和练习中的程序,如果是用数组下标的,将其改用指针来重写。这些程序至少包括 getline(第1、4章),atoi,itoa及其变种(第2、3、4章),reverse(第3章),以及 strindex 和 getop(第4章)