再论 C 语言的字符转义

Posted by zhuizhuhaomeng Blog on April 20, 2024

问题来了

虽然 C 语言的语法特性已经是最简单的了,但是作为一个 C 语言的老鸟,免不了还是要栽在 C 语言的大坑里。

最近在做一个项目,需要将多个字符串拼接为 LV 格式的字符串。 这里的 LV 是代表 Length Value 格式,而不是奢侈品的 LV 包包的 LV

因为子字符串的长度只能小于 256 字节, 因此 L 用一个字节来表示。 比如 “abc” “com” 两个字符串要表示成 LV 格式就是 “\x03abc\x03com”。

可以看到我们上面用 16 进制的转义系列。

但是,问题就出在 16 进制的转义系列。乍一看还以为很完美,我们一直都是这么搞 16 进制转义系列的。

比如常见的 to_hex_str() 这样的函数将字符串转义为16进制,而 from_hex_str() 将 16 进制字符串转换成字节序列。

像 Shell 的 hexdump 就像将字节系列转换为 16 进制。下面就是 Hello world 的 16 进制表示。

1
2
3
echo -n "Hello world" | hexdump -C
00000000  48 65 6c 6c 6f 20 77 6f  72 6c 64                 |Hello world|
0000000b

在 C 语言中我们可以直接这样表示:

char s[] = "Hello world";

也可以用 16 进制表示:

char s[] = "\x48\x65\x6c\x6c\x6f\x20\x77\x6f\x72\x6c\x64";

那么到底问题出现在哪里呢?这就要从 16 进制的语法表示说起。

转义字符的语法表示

  • escape-sequence:
    • simple-escape-sequence
    • octal-escape-sequence
    • hexadecimal-escape-sequence
    • universal-character-name
  • simple-escape-sequence: one of
    • ' " \? \
    • \a \b \f \n \r \t \v
  • octal-escape-sequence:
    • \ octal-digit
    • \ octal-digit octal-digit
    • \ octal-digit octal-digit octal-digit
  • hexadecimal-escape-sequence:
    • \x hexadecimal-digit
    • hexadecimal-escape-sequence hexadecimal-digit

从上面可以看到,8 进制可以用 1~3 个数字表示,而 16 进制则是递归的方式。 因此从语法上讲,\xa, \xab, \xabc 都是 16 进制表示。

然而这就是一个坑,或许当初专家为了节省储存空间,理所当然认为大家都知道这些门门道道。 没有想到世界发展如此之快,电脑变得唾手可得,普通人都能成为一个程序员。

专家挖坑,普通人踩坑,大家都有收获。 :joy: :boom:

讲这么多,还是不容易理解,那么只能 show me the fucking code

C 代码片段

#include <stdio.h>

int main(int argc, char **argv)
{
    unsigned char s[] = "\x0123";
    for (int i = 0; i < sizeof(s); i++) {
        printf("%02x\n", s[i]);
    }

    return 0;
}

将上面的代码保持为 t.c, 然后执行编译得到结果如下:

1
2
3
4
5
$ gcc -Wall t.c
t.c: In function ‘main’:
t.c:5:33: warning: hex escape sequence out of range
    5 |     unsigned char s[] = "\x0123";
      |                                 ^

程序编译通过了,但是有警告,这就是该死的 16 进制转义。。

同时也应该记住,-Wall -Werror 是良好的实践:死在前面总比不知道怎么死的来得好。

解决问题

那么如何解决 16 进制转义呢?其中一个方法就是使用像 Helllo world 那样的方式,将所有的字节转义。 比如 “abc” “com” 的 LV 的 Hex 表示是: “\x03\x61\x62\x63\0x03\x63\x6f\x6d”。

但是这种转义方式存在的问题就是代码阅读很不方便。我们的目标就是要混合转义和非转义字符。 这种情况下最好的方式就是使用 3 个八进制的转义字符了。因此上面的 “abc” “com” 两个字符串 的 C 转义系列就可以表示成 “\003abc\003com”。

当然,如果你属于艺高人胆大的,还是可以通过判断 16 进制后面跟的是不是合法的 16 进制字符来实现 混合 16 进制转义, 8 进制转义来达到目标的。但是 16 进制需要多一个 x 字符,所以其实多数情况下还是用 4 个字节的转义系列表示一个字节,并没有比 8 进制来得少。

欢迎入坑 C 语言。