终于!字符串!还有什么比这更简单的呢?
嗯,事实证明在 C 里字符串其实并不是“字符串”。没错!它们是指针!它们当然是指针!
和数组很像,C 里的字符串勉强算是存在。
不过还是来看看吧——其实真没什么大不了的。
开始之前,先说说 C 里的字符串字面量。它们是包在双引号(")里的一串字符。(单引号括起来的是字符,完全是另一回事。)
例如:
"Hello, world!\n"
"This is a test."
"When asked if this string had quotes in it, she replied, \"It does.\""第一个末尾有一个换行符——这是很常见的情况。
最后一个里嵌入了引号,但你能看到每个引号前面都有一个反斜杠(\)——我们说它被“转义”了——表示此处字符串里应当有一个字面意义上的引号。C 编译器正是靠这一点来区分“要打印的双引号”和“字符串结尾的双引号”。
既然已经知道怎么创建字符串字面量,我们就把它赋给一个变量,好拿它做点什么。
char *s = "Hello, world!";注意那个类型:指向 char 的指针。字符串变量 s 实际上是指向该字符串第一个字符的指针,也就是那个 H。
我们可以用 %s(代表“string”)格式说明符把它打印出来:
char *s = "Hello, world!";
printf("%s\n", s); // 输出 "Hello, world!" 另一种写法是这样,与上面的 char* 用法几乎等价:
char s[14] = "Hello, world!";
// 或者,如果我们够懒,让编译器
// 替我们算出长度:
char s[] = "Hello, world!";这意味着你可以用数组写法来访问字符串里的字符。我们就这么做,把字符串里的所有字符打印在同一行上:
#include <stdio.h>
int main(void)
{
char s[] = "Hello, world!";
for (int i = 0; i < 13; i++)
printf("%c", s[i]);
printf("\n");
}注意我们用的是 %c 格式说明符来打印单个字符。
另外看看这个。如果我们把 s 的定义改成 char* 类型,程序依然能正常工作:
#include <stdio.h>
int main(void)
{
char *s = "Hello, world!"; // 这里用 char*
for (int i = 0; i < 13; i++)
printf("%c", s[i]); // 但这里仍然用数组……?
printf("\n");
}而且打印时我们照样能用数组写法把活干完!这挺让人意外,但说到底还是因为我们还没讲过数组与指针的等价性。不过这也再次暗示:数组和指针在骨子里是同一回事。
我们已经见过一些用字符串字面量初始化字符串变量的例子:
char *s = "Hello, world!";
char t[] = "Hello, again!";但这两个初始化有细微的差别。字符串字面量和整数字面量一样,其内存由编译器自动为你管理!对于整数这种大小固定的数据,编译器管理起来相当容易。但字符串是字节数不定的怪物,编译器只好把它丢进一块内存,再给你一个指向它的指针,以此来驯服它。
这种写法指向该字符串被放置的位置。出于性能与安全方面的原因,那个位置通常远在程序其余内存之外的“远方”——只读内存。
char *s = "Hello, world!";所以,如果你用下面这段代码去改动那个字符串:
char *s = "Hello, world!";
s[0] = 'z'; // 坏消息:试图改动字符串字面量!行为是未定义的。视你的系统而定,很可能导致崩溃。
但把它声明为数组就不一样了。编译器不会把那些字节存到城市的另一头,它们就在同一条街上。这一份是字符串可变的副本——我们可以随意改动:
char t[] = "Hello, again!"; // t 是该字符串的数组副本
t[0] = 'z'; // 没问题
printf("%s\n", t); // 输出 "zello, again!"所以要记住:如果你有一个指向字符串字面量的指针,别试图去改动它!另外,如果用双引号里的字符串来初始化数组,那其实不算字符串字面量。
你做不到,因为 C 不会替你记录长度。而我说“做不到”时,其实意思是“做得到”63。<string.h> 里有一个 strlen() 函数,可以用它来计算任何字符串的字节长度64。
#include <stdio.h>
#include <string.h>
int main(void)
{
char *s = "Hello, world!";
printf("The string is %zu bytes long.\n", strlen(s));
}strlen() 函数返回 size_t 类型,这是一种整数类型,所以你可以拿它做整数运算。我们用 %zu 打印 size_t。
上面的程序会打印:
The string is 13 bytes long.太好了!所以获取字符串长度确实是可能的!
可是……如果 C 根本没在任何地方记录字符串的长度,它怎么知道字符串有多长呢?
C 处理字符串的方式和许多编程语言略有不同,事实上和几乎所有现代编程语言都不同。
在设计一门新语言时,要把字符串存进内存,基本上有两种选择:
把字符串的各个字节存下来,再附上一个表示字符串长度的数字。
只存字符串的字节,并用一个叫作终止符的特殊字节来标记字符串的结尾。
如果想得到长度超过 255 个字符的字符串,方案 1 至少需要两个字节来存长度。而方案 2 只需一个字节就能终止字符串。这样就能省下一点空间。
当然,如今还为省下一个字节(或者 3 个——很多语言都乐意让你拥有长达 4 GB 的字符串)而操心,显得有点可笑。但在当年,这可是件大事。
于是 C 选择了方案 2。在 C 里,“字符串”由两个基本特征来定义:
NUL 字符65),位于指针之后的某个内存位置上,用来表示字符串的结尾。在 C 代码里,NUL 字符可以写成 \0,不过你通常不必这么做。
当你在代码里写上双引号括起来的字符串时,NUL 字符会被自动、隐式地包含进去。
char *s = "Hello!"; // 实际上幕后是 "Hello!\0"明白了这一点,我们就自己写一个 strlen() 函数,它数一数字符串里的 char,直到遇到 NUL 为止。
做法是沿着字符串往下找那个 NUL 字符,一边找一边计数66:
int my_strlen(char *s)
{
int count = 0;
while (s[count] != '\0') // 单个字符用单引号
count++;
return count;
}内置的 strlen() 基本上就是这么干的。
你不能用赋值运算符(=)来复制字符串。它做的只是复制指向第一个字符的指针……结果你就得到了两个指向同一个字符串的指针:
#include <stdio.h>
int main(void)
{
char s[] = "Hello, world!";
char *t;
// 这复制的是指针,不是字符串!
t = s;
// 我们改动 t
t[0] = 'z';
// 但打印 s 却显示出改动!
// 因为 t 和 s 指向同一个字符串!
printf("%s\n", s); // 输出 "zello, world!"
}如果你想复制一个字符串,就得一个字节一个字节地复制——也就是说,你要把字符串的各个字节从内存中的一处取出来,再复制到内存中的另一处。用 strcpy() 函数会方便一些。67
复制字符串之前,先确认你有足够的空间放它,也就是说,用来存放字符的目标数组至少要和你复制的字符串一样长。
#include <stdio.h>
#include <string.h>
int main(void)
{
char s[] = "Hello, world!";
char t[100]; // 每个 char 占一个字节,所以空间足够
// 这次复制的是字符串!
strcpy(t, s);
// 我们改动 t
t[0] = 'z';
// 而 s 不受影响,因为它是另一个字符串
printf("%s\n", s); // 输出 "Hello, world!"
// 但 t 已经被改过了
printf("%s\n", t); // 输出 "zello, world!"
}注意 strcpy() 的第一个实参是目标指针,第二个是源指针。我记这事的口诀是:如果赋值 = 对字符串管用,你本来就会按这个顺序写 t 和 s——源在右,目标在左。