文本
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_t
、char16_t
、char32_t
,虽说是编译器决定,但出于其极强的暗示性,往往都被实现为 UTF-8 UTF-16 和 UTF-32。
但是无论是初始化时字符串常量赋值给数组,还是相邻的字符串常量连接在一起,这都是编译器在耍花招而已——那只是编译时字面量的操作而已。当你真的需要给字符数组赋值或者连接两个字符数组时,你还是需要调用库函数strcpy
和strcat
来实现。
转义序列
转义序列(escape sequence)指的是利用反斜杠\
及其后面的若干个字符来表示一些特殊的字符的方式。
转义序列 | 含义 | ASCII |
---|---|---|
\' | 单引号 | 0x27 |
\" | 双引号 | 0x22 |
\? | 问号 | 0x3f |
\\ | 反斜杠 | 0x5c |
\a | 响铃 | 0x07 |
\b | 退格 | 0x08 |
\f | 换页 | 0x0c |
\n | 换行 | 0x0a |
\r | 回车 | 0x0d |
\t | 水平制表 | 0x09 |
\v | 垂直制表 | 0x0b |
\ ooo | 三位八进制的值 | |
\x hh... | 十六进制的值 | |
\u hhhh (C99) | 四位十六进制的 Unicode 值 | |
\U hhhhhhhh (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)。
你的键盘能直接打出来的字毕竟是少数。借助输入法的确可以补充不少,但依然杯水车薪。
上面的转义序列或许展示了一种方法在字符串里进行字符代替。但是源代码有时同样需要转义!
🚧 施工中 🚧
一种替换的方案在这里介绍。
通用字符名
🚧 施工中 🚧