我们已经说过,char 类型其实只是小整数类型……单引号里的字符也是一样。
但双引号里的字符串类型是 const char *。
原来字符串和字符还有好几种类型,而这会把你引向这门语言里最臭名昭著的兔子洞之一:多字节/宽字符/Unicode/本地化这一整套东西。
我们打算往那个兔子洞里张望一下,但不进去。……暂时不!
我们习惯了由普通字母、标点和数字构成的字符串和字符:
char *s = "Hello!";
char t = 'c';但如果我们想在里面放一些键盘上打不出来的特殊字符(比如“€”),因为它们压根不存在,或者我们就是想要一个单引号字符,那该怎么办?显然,下面这样是不行的:
char t = ''';为了做到这些,我们使用所谓的转义序列。它由反斜杠字符(\)后跟另一个字符组成。这两个(或多个)字符合在一起便具有特殊含义。
回到单引号字符的例子,我们可以在中间那个单引号前面加一个转义符(也就是 \)来解决:
char t = '\'';现在 C 知道 \' 只代表我们想打印的一个普通引号,而不是字符序列的结束。
在这种语境下,你说“反斜杠”或者“转义”(“把那个引号转义一下”)都行,C 开发者都能明白你在说什么。另外,这里的“转义”既不同于你键盘上的 Esc 键,也不同于 ASCII 里的 ESC 码。
依我浅见,这些转义字符占了所有转义的 99.2%139。
| 代码 | 说明 |
|---|---|
\n |
换行符——打印时,后续输出转到下一行 |
\' |
单引号——用于单引号字符常量 |
\" |
双引号——用于字符串字面量中的双引号 |
\\ |
反斜杠——用于在字符串或字符中表示字面的 \ |
下面是一些转义的示例,以及它们被打印时的输出。
printf("Use \\n for newline\n"); // 用 \n 表示换行
printf("Say \"hello\"!\n"); // 说 "hello"!
printf("%c\n", '\''); // '不过转义还有更多!只是这些你不常看到。
| 代码 | 说明 |
|---|---|
\a |
警报。让终端发出声音或闪烁,或者两者兼有! |
\b |
退格。把光标往回移一个字符。并不删除该字符。 |
\f |
换页。这会移到下一个“页”,但这个词在现代已没多大意义。在我的系统上,它的行为和 \v 一样。 |
\r |
回车。移到同一行的行首。 |
\t |
水平制表。移到下一个水平制表位。在我的机器上,它对齐到 8 的整数倍列,但你的情况可能不一样。 |
\v |
垂直制表。移到下一个垂直制表位。在我的机器上,它会移到下一行的同一列。 |
\? |
字面的问号。有时你需要用它来避开三字符组,如下所示。 |
\b 或 \r 的一个用途是显示状态更新:让更新出现在屏幕的同一行上,不至于让显示内容滚动。下面是一个从 10 开始倒计时的例子。(如果你的编译器不支持多线程,可以使用来自 <unistd.h> 的非标准 POSIX 函数 sleep()——如果你用的不是类 Unix 系统,就搜一下你的平台上与 sleep 等价的函数。)
#include <stdio.h>
#include <threads.h>
int main(void)
{
for (int i = 10; i >= 0; i--) {
printf("\rT minus %d second%s... \b", i, i != 1? "s": "");
fflush(stdout); // 强制刷新输出
// 休眠 1 秒
thrd_sleep(&(struct timespec){.tv_sec=1}, NULL);
}
printf("\rLiftoff! \n");
}第 7 行上发生了不少事。首先,我们用 \r 开头,回到当前行的行首,然后用当前的倒计时数字覆盖掉那里原有的内容。(那儿还有个三目运算符,确保我们打印的是 1 second 而不是 1 seconds。)
另外,... 后面有一个空格。这是为了当 i 从 10 掉到 9、宽度少了一列时,我们能正确覆盖掉最后一个 .。去掉那个空格试试,你就明白我的意思了。
最后我们用一个 \b 收尾,退回到那个空格上,好让光标以一种赏心悦目的方式恰好停在这一行的末尾。
注意第 15 行末尾也有一大串空格,用来覆盖掉倒计时先前留下的字符。
最后,里面还有一句莫名其妙的 fflush(stdout),管它是什么意思呢。简短的回答是:大多数终端默认是行缓冲的,也就是说在遇到换行符之前它们其实什么都不会显示。由于我们这里没有换行(只有 \r),要是没有这一行,程序就会一直干等着,直到 Liftoff! 那一刻才把所有内容一次性打印出来。fflush() 会覆盖这一行为,强制输出立刻发生。
何必费这个劲呢?毕竟下面这样完全没问题:
printf("Doesn't it?\n");加上转义符,也照样没问题:
printf("Doesn't it\?\n"); // 注意 \?那它到底有什么用??!
我们再加一个问号和一个感叹号,把语气加强些:
printf("Doesn't it??!\n");我编译这段代码时,得到了这样的警告:
foo.c: In function ‘main’:
foo.c:5:23: warning: trigraph ??! converted to | [-Wtrigraphs]
5 | printf("Doesn't it??!\n");
| 而运行它,会得到这个意想不到的结果:
Doesn't it|那么三字符组?这到底是什么鬼??!
我确信以后还会再回到这门语言这个积灰的角落,但简单说来就是:编译器会寻找以 ?? 开头的某些三字符组合,并把它们替换成别的字符。所以如果你用的是一台键盘上没有竖线符号(|)的老古董终端,就可以改打 ??!。
把第二个问号转义一下就能解决,像这样:
printf("Doesn't it?\?!\n");这样它就能编译通过,并按预期工作。
当然,如今已经没人用三字符组了。不过如果你决定在字符串里用它来表示强调,那个 ??! 有时确实会冒出来。
此外,还有办法在字符串或字符常量内部指定数值常量或其他字符值。
如果你知道某个字节的八进制或十六进制表示,就可以把它写进字符串或字符常量。
下表中的数字只是示例,实际可以使用任意十六进制或八进制数。必要时用前导零补齐,以达到要求的位数。
| 代码 | 说明 |
|---|---|
\123 |
嵌入八进制值为 123 的字节,正好 3 位数字。 |
\x4D |
嵌入十六进制值为 4D 的字节,2 位数字。 |
\u2620 |
嵌入码点为十六进制值 2620 的 Unicode 字符,4 位数字。 |
\U0001243F |
嵌入码点为十六进制值 1243F 的 Unicode 字符,8 位数字。 |
下面是一个示例,用不太常见的八进制记法在 A 和 C 之间表示字母 B。通常这种写法是留给某种不可打印的特殊字符的,不过我们在下面还有别的办法做到这点,这里只是演示八进制:
printf("A\102C\n"); // 在 ASCII/UTF-8 中,102 是 `B`注意用这种方式写八进制数时,前面不加前导零。但它的长度必须是三个字符,所以需要时用前导零补齐。
不过如今更常见的是使用十六进制常量。下面这个示例你不该照着用,它演示的是把 UTF-8 字节 0xE2、0x80 和 0xA2 嵌入字符串,它们对应 Unicode 的“项目符号”字符(•)。
printf("\xE2\x80\xA2 Bullet 1\n");
printf("\xE2\x80\xA2 Bullet 2\n");
printf("\xE2\x80\xA2 Bullet 3\n");如果你在 UTF-8 控制台上,会产生下面的输出(如果不是,大概就是一堆乱码):
• Bullet 1
• Bullet 2
• Bullet 3
但这样处理 Unicode 实在不怎么样。你可以用转义 \u(16 位)或 \U(32 位)直接按码点编号引用 Unicode 字符。项目符号在 Unicode 中是 2022(十六进制),所以你可以这样写,得到可移植性更好的结果:
printf("\u2022 Bullet 1\n");
printf("\u2022 Bullet 2\n");
printf("\u2022 Bullet 3\n");记得给 \u 补上足够的前导零凑满四位十六进制数字,给 \U 补够八位。
例如,那个项目符号可以用 \U 加四个前导零来表示:
printf("\U00002022 Bullet 1\n");可谁有工夫写得那么啰嗦呢?