网站首页 > 技术文章 正文
很多人第一次接触 '\n'、'\x41'、'\101'、'\u4E2D' 这类写法时,会本能地问一句:它们在内存里到底是哪些数?而编译器又是按什么规则把源码里的字符翻译成目标文件中的字节?这篇文章把“字符常量”“转义序列”“编码”三件事放到一张图里讲清楚,帮你写出可移植、可预期的 C 代码。
一、字符常量究竟是什么
- 在 C(C11/C17)里,简单字符常量(如 'A')的类型是 int,其值是该字符在**执行字符集(execution character set)**中的编码数值。别被字面量的外观迷惑了:sizeof 'A' == sizeof(int) 在多数实现上都成立。
- 这意味着在采用 ASCII/UTF-8 的常见环境里,'A' == 65 往往为真,因为 ASCII 中 'A' 的码位是 65。换到 EBCDIC 等环境则不保证仍是 65——这正是“执行字符集”一词存在的意义。
#include <stdio.h>
int main(void) {
printf("'A' as int = %d\n", 'A'); // 常见环境输出 65
printf("'\\n' as int = %d\n", '\n'); // 常见环境输出 10
}
结论:**字符常量的值等于字符在“执行字符集”中的编码。**不要把它和“源码文件的存储编码”混为一谈。
二、转义序列的语义与典型数值
转义序列的设计初衷是与源文件编码解耦。不论你的 .c 文件是 UTF-8 还是 GBK,'\n' 都表示“换行这一抽象字符”。
常见转义序列与在 ASCII/UTF-8 环境下的典型数值(十进制):
- '\0':空字符(NUL)→ 0
- '\n':换行(LF)→ 10
- '\r':回车(CR)→ 13
- '\t':水平制表(TAB)→ 9
- '\v':垂直制表 → 11
- '\f':换页 → 12
- '\a':响铃 → 7
- '\b':退格 → 8
- '\''、'\"'、'\\'、'\?':分别为 '、"、\、? 本身
请注意:这些数值不是由转义序列决定,而是由执行字符集决定;ASCII/UTF-8 只是最常见的一种。
三、数字转义:八进制与十六进制
C 提供两种“按数值写字节”的方式:
- 八进制:\ooo(o 为 0–7,最多 3 位)。例如 '\101' 等于 'A'(在 ASCII 环境中是 65)。
- 十六进制:\xhh...(h 为 0–9A–F,不限位数,尽可能多地吞)。例如 '\x41' 等于 'A'。
两点坑位要避:
- 贪婪规则:\x 会一直读到遇到“非十六进制字符”为止。"\x41B" 并不是 A 后接 B,而是一个单个转义,值为十六进制 0x41B,随后再拼上字符串结尾的 \0。这很可能导致超范围或与预期不符。写法建议: const char *s1 = "\x41""B"; // 邻接字符串常量分隔,得到 "AB"
const char *s2 = "\x41" "B"; // 同上,清晰且可移植 - 范围问题:无论八/十六进制,得到的数值若超出 unsigned char 的可表示范围,其结果是实现定义(甚至可能发出诊断)。务必自觉限制到 0–255。
四、通用字符名与宽/窄字符的落地
为了跨平台表达非 ASCII 字符,C 还提供了通用字符名(Universal Character Name):
- '\u4E2D':表示 Unicode 码位 U+4E2D(“中”)
- '\U0001F600':表示 U+1F600()
与之配套的几种字符常量形式:
- L'中' 或 L'\u4E2D':宽字符常量,类型为 wchar_t,值为“执行宽字符集”中的对应码位。
- u'\u4E2D':类型为 char16_t(C11 起)。
- U'\u4E2D':类型为 char32_t(C11 起)。
- 字符串方面还有 u8"中"(UTF-8 字符串字面量,C11 就有;字符字面量 u8'a' 在 C23 才加入,一些编译器已先行支持)。
要点:
- 窄字符常量(无前缀的 '…')只有当该字符可以用执行字符集的单字节表示时才有良好定义;否则就落入实现定义/不可表示的灰区。
- 若你用的是 UTF-8 执行编码,"中" 在内存里是 三个字节(0xE4 0xB8 0xAD),而 L'中' 则是一个 wchar_t 值(在许多系统为 32 位的 0x00004E2D)。
五、源字符集 vs 执行字符集:编译器做了哪些“翻译”
C 标准把编译抽象为若干“翻译阶段”。与本文最相关的是:
- 源字符集解码:编译器先把源文件从“源字符集”(UTF-8、GBK…)解码到一个内部统一表述。
- 转义处理:把 '\n'、'\x41' 等替换为抽象字符或数值。
- 映射到执行字符集:把字面量最终落到目标平台的“执行字符集/执行宽字符集”编码上。
因此,只要你用转义序列或通用字符名,就能避开源文件编码的陷阱。反之,直接在源码里写非 ASCII 字符,可移植性取决于编译器对源文件编码的假设与选项(如 -finput-charset、/source-charset:)。
六、多字符常量:能用,但别用
写法如 'AB'、'XYZ',标准称为多字符常量,类型仍是 int,其值是实现定义的组合(常见实现把各字节按目标端序拼进一个 int)。
- 在小端系统上,'AB' 往往等于 'A' + ('B'<<8);在大端则相反。
- 这类写法用于手工构造四字符代码(FourCC)时偶见,但不建议在文本语义中使用,因为跨平台数值不稳定。
七、char 的有符号性:数值回读的隐形炸弹
char 在 C 里既可以是有符号的,也可以是无符号的,由实现决定。影响:
- 你把 '\xFF' 存进 char,在 signed char 实现上读回可能是 -1,在 unsigned char 实现上是 255。
- 解决之道:
- 涉及原始字节时,用 unsigned char。
- 涉及文本时,用 char + 明确的执行编码约定(推荐 UTF-8),并避免超出 0x7F 的“单字节字面量”。
八、实践清单(可直接套用)
- 统一编码约定:源码统一用 UTF-8,编译器相应配置到 UTF-8 输入;执行时也默认 UTF-8(现代 Linux/macOS 如此,Windows 建议打开 UTF-8 代码页)。
- 优先语义字面量:表示控制字符用 '\n'、'\t';只在确有需要时才用 \x.. / \ooo。
- 跨语言字符:用通用字符名 + 前缀选择合适的类型:u8"…"(字符串)、U'…'/u'…'/L'…'(字符)。
- 防贪婪:十六进制转义后紧跟引号拼接:"\x41""B"。
- 打印数值:用 printf("%d", (unsigned char)c) 明确数值范围。
- 避免多字符常量:除非构造 FourCC 且接受实现差异。
- 文件换行:内部始终用 '\n'。Windows 文本模式下 I/O 层会在磁盘上做 \r\n 转换,不要自己再加 '\r'。
九、几个高频“混淆题”
- '0'、0、'\0' 有何不同? '0' 是字符 '0',在 ASCII 下数值 48;0 是整型常量零;'\0' 是空字符(NUL),数值为 0。
- 为什么 "\x41B" 和我想的 "AB" 不一样? 因为 \x 会吞掉后面的 B(十六进制 0xB)。改写为 "\x41""B"。
- '\n' 在 Windows 是 10 还是 13? 仍是 10(LF)。'\r' 才是 13(CR)。文本文件中 \n 可能被 I/O 转换为 \r\n 两字节,这是文件层的事。
- '中' 写成窄字符安全吗? 取决于执行字符集是否可单字节表示该字符。最稳妥:用 L'\u4E2D'/u'\u4E2D'/U'\u4E2D' 或放到 u8"中" 字符串里。
十、动手验证:一段小程序
#include <stdio.h>
int main(void) {
printf("'\\n'=%d, '\\r'=%d, '\\t'=%d\n", '\n', '\r', '\t');
printf("'A'=%d, '\\101'=%d, '\\x41'=%d\n", 'A', '\101', '\x41');
printf("'\\0'=%d, '\\\\'=%d, '\\''=%d, '\"'=%d\n", '\0', '\\', '\'', '\"');
unsigned char u = '\xFF';
signed char s = '\xFF';
printf("u=%%u -> %u, s=%%d -> %d\n", (unsigned)u, (int)s);
// 注意:下面这行仅用于演示,不建议在生产中使用多字符常量
printf("'AB' (impl-defined) = %d\n", 'A'<<8 | 'B'); // 常见拼法,接近多数实现
}
运行你的编译器/平台,感受执行字符集与**char 符号性**带来的差异。这些差异并不可怕——只要理解“字符常量的值=执行字符集中的编码”这一核心命题,配合规范的写法,你的程序就能稳稳地跨平台工作。
猜你喜欢
- 2025-09-21 C#串口组件的使用方法_c#串口控件使用方法
- 2025-09-21 Python 变量学习_python怎么用变量
- 2025-09-21 生产环境使用HBase,你必须知道的最佳实践 | 百万人学AI
- 2025-09-21 5年程序员总结—这几个C语言问题超纲了,小白勿进
- 2025-09-21 C 语言“翻译阶段”全解析:从字符到记号,编译器到底干了什么?
- 2025-09-21 C++编程语言日常20个高级经典用法(含完整源码与输出)
- 2025-09-21 【C语言·023】变长数组的栈分配机制与使用限制
- 2025-09-21 剑指Offer:字符串转整数的实现与边界处理
- 2024-08-06 【PythonTip题库精编300题】第35题:十六进制转换为二进制
- 2024-08-06 学习编程第135天 python编程二、八、十、十六进制转换
- 最近发表
- 标签列表
-
- cmd/c (90)
- c++中::是什么意思 (84)
- 标签用于 (71)
- 主键只能有一个吗 (77)
- c#console.writeline不显示 (95)
- pythoncase语句 (88)
- es6includes (74)
- sqlset (76)
- apt-getinstall-y (100)
- node_modules怎么生成 (87)
- chromepost (71)
- flexdirection (73)
- c++int转char (80)
- mysqlany_value (79)
- static函数和普通函数 (84)
- el-date-picker开始日期早于结束日期 (76)
- js判断是否是json字符串 (75)
- c语言min函数头文件 (77)
- asynccallback (87)
- localstorage.removeitem (74)
- vector线程安全吗 (70)
- java (73)
- js数组插入 (83)
- mac安装java (72)
- 无效的列索引 (74)