Skip to main content

文本

Oh what a tangled web we weave, when we first practice to deceive ——Walter Scott

⚠️ 你先别急 ⚠️ 🚧 本页面仍在施工中 🚧

字符串

与一些语言(例如 Python)不同,C 语言是区分字符(character)和字符串(string)的。用单引号括起来的是字符,双引号括起来的是字符串。因此这两个符号也是不可以混淆的。一些编译器可能会允许单引号内包含多个字符(并一般会同时给出警告),但是一定要切记不能依赖于这样的扩展特性。

字符串的末尾是有一个隐式的'\0'作为结尾的,因此即使是空字符串也是有一个字符在里面的。在进行初始化的时候,如果空间没给够,最后一个'\0'会被省略。(C++ 禁止这样的初始化)

char hi0[] = "hi"; // sizeof(hi0) == sizeof("hi") == 3
char hi1[3] = "hi"; // char hi1[3] = {'h', 'i', '\0'};
char hi2[2] = "hi"; // char hi2[2] = {'h', 'i'}; and Error in C++

更加广为人诟病的是,明明作为一个实际上不应该可变的字符数组,字符串常量却在 C 语言中拥有非const的类型,进而也可以赋值给非const的字符指针。

char* s = "string-literal"; // OK in C, Error since C++11
s[0] = 'r'; // UB

如果一个字符串常量非常长,想要写到多行里面,怎么办?一种古老的方法是在行末加反斜杠\来抵消换行符,如

const char* greetings = "hello\
world";

这个特性实际上更广泛的用于多行的宏定义里面:

#define LOG(fp, msg)  \
printf("%s", msg); \
fprintf(fp, "%s", msg)

但是要注意,这样的抵消在注释里仍然奏效,可能会带来意料之外的后果:

// 下一行代码总是不会被执行 \
...

因为第一行行末换行符被抵消了,这会导致下一行代码也被注释掉。

不过对于字符串常量来说,还有一个特殊的特性,那就是相邻的两个字符串常量自动连接:

const char* greetings1 = "hello"
" world";
const char* greetings2 = "hello" " world";
const char* greetings3 = "hello"" world";

这三个与上面的greetings都是等价的。

这个语法还可以用来避免一些尴尬的情况:

const char* p = "\x41F"; // Error: 0x41F 太大了
const char* q = "\x41""F"; // OK: 包含 'A' 'F' 和 '\0' 三个字符

当然这样的连接有可能会发生在你意想不到的地方:

const char* messages[] = {
"find",
"trouble" // 哎呀!忘了一个逗号
"maker", // OK,末尾多余的逗号是被允许的
};

可以发现,最后两个字符串意外的被连起来了,我们很可能并不是想要一个"troublemaker",而是两个字符串常量。

此外,字符串常量是可以有前缀的,"..."的类型是char[N],而L"..."的类型则是wchar_t[N]。在连接的时候,如果其中一个字符串常量是有前缀的,那么就会使用这种前缀:

const wchar_t* p = L"123" "123"; // L"123123"
const wchar_t* q = "123" L"123"; // 同上
const wchar_t* r = L"123" L"123"; // 同上

第一种和第三种情况比较容易理解,第二种写法可能会引起误会,但也可以接受。无法接受的是混合前缀:

u"123" U"123" // 注意大小写

u"..."的类型是char16_t[N],而U"..."的类型则是char32_t[N]。这下你给编译器整不会了,听谁的好呢?随着标准收紧这种奇怪的写法不再被允许,但即使在被允许的时候,也几乎没有任何编译器给出一个合理的实现!

这些不同宽度的字符类型,自然是为了不同编码的字符而来。C 的宏(C++ 的关键字)wchar_t,是平台相关的宽字符,在 Windows 下是两字节(默认为 GBK 或其他本地字符集),而在 *nix 下是四字节(Unicode)。而 C 的宏(C++ 的关键字)char8_tchar16_tchar32_t,虽说是编译器决定,但出于其极强的暗示性,往往都被实现为 UTF-8 UTF-16 和 UTF-32。


但是无论是初始化时字符串常量赋值给数组,还是相邻的字符串常量连接在一起,这都是编译器在耍花招而已——那只是编译时字面量的操作而已。当你真的需要给字符数组赋值或者连接两个字符数组时,你还是需要调用库函数strcpystrcat来实现。

转义序列

转义序列(escape sequence)指的是利用反斜杠\及其后面的若干个字符来表示一些特殊的字符的方式。

转义序列含义ASCII
\'单引号0x27
\"双引号0x22
\?问号0x3f
\\反斜杠0x5c
\a响铃0x07
\b退格0x08
\f换页0x0c
\n换行0x0a
\r回车0x0d
\t水平制表0x09
\v垂直制表0x0b
\ooo三位八进制的值
\xhh...十六进制的值
\uhhhh (C99)四位十六进制的 Unicode 值
\Uhhhhhhhh (C99)八位十六进制的 Unicode 值

问号为什么需要转义?你先别急!后文将会揭晓。

数字转义序列

开始前先个问你一个问题:'\42'是多少?

答案是34。八进制总是令人着迷。你还会再见到它的。


八进制转义序列最多接受三位八进制数,且会在第一个非八进制数字处停止。十六进制转义序列没有这个限制,接受任意位十六进制数,但也会在第一个非十六进制数字处停止(你也可以将其理解为一种贪心法)。如果转义序列的值超出了它字符类型的范围,结果是未定义的。

Unicode 的转义序列要求的长度是固定的。短了非法,长了直接不算作转义序列的一部分。由于 Unicode 编码的取值范围是00xD800,0xE0000x10FFFF(中间为什么缺了一段?为什么最大值是这么奇怪的一个数字?请参见 UTF-16 的设计方案),所以八位十六进制的 Unicode 转义序列注定至少有前两位是 0(我也很难理解他们为什么要这么设计……可能是为了说明字符确实是 32 位?抑或是为日后的扩展做准备?)。

Unicode 转义序列还有一个不同之处在于,它表示的永远是一个实实在在的 Unicode 字符,而不是表示任何一种编码。它会根据字符串的编码进行具体的转译,例如对于字符 🍌(U+1f34c)来说:

char ch0     =  '🍌'; // ill-formed
char16_t ch1 = u'🍌'; // ill-formed
char32_t ch2 = U'🍌'; // OK
char s0[] = "\U0001f34c"; // 0xf0, 0x9f, 0x8d, 0x8c
char16_t s1[] = u"\U0001f34c"; // 0xd83c, 0xdf4c
char32_t s2[] = U"\U0001f34c"; // 0x0001f34c
// 注:上面的代码里的 🍌 和 \U0001f34c 是可以互换的,效果一样

char16_t err[] = u"\ud83c\udf4c"; // ERROR: U+d83c 和 U+df4c 不是 Unicode 字符

可以看到,最后一行的表示,看上去似乎跟第五行的表示是一样的,那就犯了想当然的错误。Unicode 转义序列一定是表示一个完整的字符,而不能是其中的某一部分。一个字符到底如何编码、占多少字节,并不是由这个转义序列本身决定的。

字符集与编码

字符集(character set)就是一系列字符组成的集合。而这些字符与整数的对应关系,被称为编码(encoding)。

你的键盘能直接打出来的字毕竟是少数。借助输入法的确可以补充不少,但依然杯水车薪。

上面的转义序列或许展示了一种方法在字符串里进行字符代替。但是源代码有时同样需要转义!

🚧 施工中 🚧

一种替换的方案在这里介绍。

通用字符名

🚧 施工中 🚧

双字符与三字符

双字符(digraph)和三字符(trigraph)可以用来代替一些缺少特殊字符的键盘不能打出来的基本字符。

原字符双字符
{<%
}%>
[<:
]:>
#%:
##%:%:
原字符三字符
{??<
}??>
[??(
]??)
#??=
\??/
^??'
|??!
~??-

二字符跟原字符是完全等价的。与其说是二字符被替换成了原字符,不如说是两者是同一符号的不同写法而已:

%:include <stdio.h>
int main(void) <%
int a<:10:>;
printf("%:%:"); // 输出 %:%:
// 二字符只有解读为运算符的时候才有原字符的意义
%>

而三字符广为人诟病——因为它进行的是替换,而且时间太早了,可能远超你的想象

// 为什么下一行代码总是不会被执行??/
...

??/被替换成了\,然后后面的换行符就被抵消了,所以下一行直接被注释了。

printf("发生什么事了??!");

输出"发生什么事了|"。你可以在字符串里面使用'\?'来避免解读为三字符。但如果仅改为"\??!",依然不奏效,仍然会被替换为"\|"。所以你必须写成"?\?!",打断三字符的构成。你甚至可以认为,它替换的优先级高于一切:

??=include <stdio.h> // #include
printf("??/n"); // 输出换行符
x ??'= 3; // x ^= 3;

相信你看到三字符满脑子肯定都是??!。实际上,三字符早已过时,还问题重重。C++17 已经正义删除了这个特性。某些编译器,例如 gcc,在默认情况下也不会允许三字符的使用。


下表总结了三种转义符号会被翻译的情形。

双字符转义序列三字符
源代码转义\u\U转义且优先
字符串不转义转义转义且优先
注释不转义不转义转义且优先