ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C与C++中sizeof ‘a‘为何不同:字符字面量类型的设计差异

C与C++中sizeof ‘a‘为何不同:字符字面量类型的设计差异 先说我第一次见到这个题的反应吧笔试题目里写着一行printf(%d, sizeof a);问在 C 语言和 C 里分别输出什么。我当时的直觉是这有什么可问的不都是 4 吗结果到了 C 环境里一跑输出变成了 1。那一瞬间我才意识到这个题目根本不是考你记不记得sizeof怎么用而是在考你对两种语言设计哲学的理解。这个现象在初学 C/C 的人群里几乎每年都会被翻出来问一次。它最迷人的地方在于看起来完全相同的写法只是因为编译器的语言标准不同结果就分道扬镳。如果你只背结论——C 里是 4C 里是 1——那你换一个平台、换一个编译器、或者换一个更复杂的表达式照样会翻车。所以这篇文章我打算把这条结论背后的所有逻辑都拆开包括标准条文、历史原因、实际工程里的连锁反应以及几个跟它总是被放在一起提问的易混点。1. 先复现一下同一行代码两种语言给出两个答案1.1 在 C 语言环境里的实测先写一个最朴素的 C 文件#include stdio.h int main(void) { printf(%d\n, sizeof a); return 0; }用 GCC 编译并运行gcc test.c -o test_c ./test_c输出结果4这里sizeof a返回的是size_t类型的值在大多数 64 位系统上size_t本质上是unsigned long但我用%d打印依然得到4。原因是sizeof a在当前平台上算出来的实际数值就是 4整数 4 被当成int传给%d格式匹配上并没有引起错误。如果你在 Windows 上用 Visual Studio 或者 Dev-C 跑结果也基本是 4因为 PC 平台上sizeof(int)几乎都是 4。这个环节有个小坑你得注意考试题里经常出现全角符号比如标题里那对中文引号、全角逗号和分号。直接从网页复制代码丢进编译器第一轮报错往往不是逻辑问题而是符号不是合法的 C/C 标点。写代码前先把引号、括号、分号都换成半角这个动作比调逻辑更优先。1.2 在 C 环境里的实测现在把同一个文件换个后缀名或者直接新建 C 文件#include cstdio int main() { printf(%d\n, sizeof a); return 0; }用 g 编译运行g test.cpp -o test_cpp ./test_cpp输出结果变成了1看到没有一行代码一字不改语言从 C 换成 C结果直接从 4 变成了 1。而且这个差异跟编译标准版本基本无关C98 到 C20 都是 1C89 到 C11 都是 4。至少在我测过的 GCC、Clang 和 MSVC 上结论高度一致。1.3 顺带解决一个关于 sizeof 括号的疑问不少人看到sizeof a没加括号会疑惑sizeof不是函数吗函数调用不是必须带括号吗这里有个很实用的语法规则sizeof是运算符不是函数。当它的操作数是一个表达式时括号可以省略当操作数是类型名时括号必须加。sizeof a /* 合法a 是表达式 */ sizeof(a) /* 也合法多写一对括号只是习惯 */ sizeof int /* 非法 */ sizeof(int) /* 合法int 是类型名 */很多教材从头到尾都写sizeof(int)导致大家形成sizeof 一定要加括号的肌肉记忆然后看到sizeof a这种写法就觉得别扭。实际上编译器在处理sizeof时把它当作一元运算符来解析就像-x和!x一样。真正需要留神的反而是sizeof int为什么非法——因为语法要求操作数是类型名时必须用括号括起来。2. 白纸黑字的标准条文一个是 int一个是 char2.1 C 标准对字符字面量的规定很多初学者会把a天然理解成字符然后想当然地认为它的类型是char。但在 C 语言的标准里结论完全不同。C11 标准N1570 草案的 6.4.4.4 小节关于字符常量是这么写的一个整数字符常量的类型是int。也就是说C 语言里的a本质上是一个整数只不过这个整数的值恰好是字符a对应的编码。正因为类型是int所以sizeof a实际上等于sizeof(int)在当前绝大多数平台上就是 4。你可以在标准文档里找到明确表述大致意思是 An integer character constant has type int。这不是编译器厂商自己拍脑袋定的而是从 C89 开始就延续下来的规则。2.2 C 标准里的相反规定C 标准对普通字符字面量的规定则是包含单个字符的普通字符字面量类型是char。这个规定从 C98 一直到 C17 都保持一致。C 标准文档里关于字符字面量的章节写的很清楚一个普通的字符字面量即不带 u8、u、U、L 前缀的字符字面量如果包含单个字符它的类型是char。所以sizeof a在 C 里等于sizeof(char)也就是 1。这里sizeof(char)恒等于 1 是两种语言共同认可的规则不是因为 char 一定占 1 字节而是标准定义一个char的大小就是 1 个 char 单位在这个语境下结果就是 1。2.3 平台数据模型对结果的影响这里牵涉到另一个知识点sizeof(int)并不在所有平台都是 4。C 标准只规定了int的取值范围不得小于 16 位也就是说在不同平台上int可能是 2 字节、4 字节、8 字节。在实际常见的平台上平台/数据模型int 大小C 中 sizeof(a)C 中 sizeof(a)Windows x86/x64LLP644 字节41Linux x86/x64LP644 字节41部分嵌入式平台int22 字节21极端平台int88 字节81sizeof(a)在 C 语言里永远等于当前平台sizeof(int)在 C 里永远等于 1。如果你在某个嵌入式编译器上跑出sizeof(a) 2不能背结论说它错了因为它遵循的是 C 的规则。3. 分歧背后的设计博弈没有原型的 C 和要重载的 C3.1 为什么 C 语言把字符字面量做成 int回到 C 语言的诞生年代KR 时代有一个很重要的事实函数可以没有原型声明。你调用一个函数编译器不知道它的参数类型只能按照默认参数提升规则来处理。char在几乎所有整数运算里都会先被提升为int函数参数如果没有显式声明也会默认按int处理。既然整个系统都在用int那么把字符字面量直接设计成int就是最省事的选择——它可以无缝参与表达式计算、函数传参不用再做任何转换。另一个原因是历史遗留的语言传统。C 语言从早期的 B 语言演变而来那时候字符常量和整数常量的界限本来就模糊。多字符常量如abc、\n这类写法也被允许存在而它们显然没法塞进一个char里于是标准索性把所有字符常量都定为int。换句话说C 语言让a是int是类型系统简化 历史惯性双重作用下的结果。3.2 C 为什么非要改成 charC 的设计目标之一是在保持 C 兼容的同时提高类型安全引入函数重载后字符字面量的类型问题就藏不住了。假设a还是int你写void foo(char c) { /* ... */ } void foo(int i) { /* ... */ } foo(a);编译器在重载决议时会发现a是int然后毫不犹豫地选择foo(int)。foo(char)就永远无法通过一个普通字符字面量调用到这显然违背了函数重载的直觉。字符字面量的本意就是表示一个字符函数接收char参数才符合语义。Bjarne Stroustrup 在《C 语言的设计与演化》里专门讨论过这个决策结论很明确为了让重载和模板推导符合直觉普通字符字面量必须是char而不是int。这个改变虽然造成了和 C 的行为差异但对 C 的类型精确定位至关重要。3.3 顺带波及的其它字面量类型差异这个设计分歧并不只影响单个字符\nC 里是intC 里是char。多字符常量abc两种语言都规定为int因为多个字符无法放进一个char。宽字符LaC 和 C 都是wchar_t。UTF-8 字符u8aC17 引入了这个前缀类型是charC 语言没有完全对应的标准写法。看到规律了吧C 优先让字面量形状匹配单字符就给char只有塞不下时才会升级成int。C 没有这种精细匹配的需求所以一律int。4. 这个坑从笔试题蔓延到工程里的几个方向4.1 printf 格式匹配%d、%c、%zu 之间的恩怨sizeof返回的类型是size_t严格来说在 printf 里应该用%zu来打印这个格式说明符是 C99 引入的printf(%zu\n, sizeof a); // C 输出 4printf(%zu\n, sizeof a); // C 输出 1但在实际开发里很多人习惯用%d这在大部分场景下也能跑因为sizeof(a)的结果值正好落在int范围内。可这不代表代码就安全了。在 64 位系统上size_t是 64 位的无符号整数你用%d去读 64 位数据本质上是格式串和实参类型不匹配属于未定义行为。只是这个具体值太小碰巧不会出错。还有一点要注意Windows 上老版本的 MSVC 运行库对%zu的支持不太好直接输出字符串zu的情况我也遇到过。VS2015 之后用的 UCRT 基本没问题了但如果你还在维护老代码就要小心这个历史遗留问题。一个更稳妥的写法是强转成unsigned long再用%lu或者干脆用cout输出避免格式串问题。很多笔试题用%d只是为了让候选人不陷入size_t格式符的纠结毕竟题目核心是sizeof的结果。但到了工程里格式串匹配是硬约束这里的坑比你想的常见。4.2 函数重载、模板推导和常量表达式里的表现C 里a是char会让重载选择更合理但在模板编程里这个类型信息会传导到更远的地方#include iostream void check(char) { std::cout char\n; } void check(int) { std::cout int\n; } int main() { check(a); // char check(97); // int check(sizeof a); // size_t, 会隐式转成 int return 0; }再看一个稍微隐蔽的例子如果你想用一个字符字面量初始化模板参数的类型C 会保留char类型而 C 里没有模板这个问题天然不存在。还有auto推导auto c a; // char auto n sizeof a; // size_t把代码从 C 风格迁移到 C 时这是很容易被忽略的隐性差异表达式结果类型变了但算术运算的最终值往往一样导致你很长一段时间发现不了问题直到某些依赖类型的语法出现偏差。4.3 当 C 代码被按 C 标准重新编译很多项目的历史包袱是 C 代码后来为了用 STL 或 lambda 改成 C 编译。这种迁移中sizeof(a)的变化基本无害但同一套类型精确化的理念会波及更多字面量相关代码。比如printf(%f, 3)在 C 里也可能因为格式匹配问题输出错误但 C 编译器在严格检查下更容易给出警告。再比如字符数组初始化char c a;这种代码在 C 和 C 里都安全因为int到char的窄化转换在 C 里会被悄悄接受在 C 里如果你用列表初始化写char c {a}编译器甚至会因为a不是char类型而给出警告或错误。但注意区分这里的字面量本身在 C 是char所以char c{a}在 C20 反而是合法的。这个细小的差别让两边的调试体验完全不同。想真正排查这类迁移问题建议打开编译器警告GCC/Clang 用-Wall -WextraMSVC 用/W4能提前暴露很多字面量类型不匹配的隐患。5. 和 sizeof(a) 纠缠在一起的三四件小事5.1 sizeof 是运算符不是函数也不需要头文件sizeof函数需要头文件这个说法是错的。sizeof是语言内置运算符跟 - * /一样不需要任何头文件就能用。你可能想问为什么标准库的size_t还需要头文件这是两回事sizeof关键字本身不需要size_t这个类型名在 C 里通常来自stddef.h或stdio.h你写printf(%zu, sizeof x)时光用sizeof不需要头文件但想拿size_t类型名就得包含头文件。还有一层关系malloc和sizeof总是成对出现比如malloc(10 * sizeof(char))有人便误以为sizeof是库函数。真实原因是malloc需要知道字节数而sizeof恰好能在编译期给出这个数两件事独立但总被连用。5.2 sizeof 和 strlen 的分工完全不一样网络上搜sizeof(a)时大概率还会关联到sizeof和strlen的区别这两者很容易被新手搅浑。我直接给个对照表对比项sizeofstrlen本质运算符函数作用时期编译期运行期计算对象类型或表达式字符串char 数组结果含义占用的内存字节数字符串长度不含结尾的 \0对指针算指针本身大小从指针地址数到 \0举个例子char s[] abc; printf(%zu %zu\n, sizeof(s), strlen(s)); // 4 3sizeof(s)是整个数组的大小4 个字节包含结尾的\0strlen(s)只统计可见字符是 3。换成指针char *p s; printf(%zu %zu\n, sizeof(p), strlen(p)); // 8 3sizeof(p)变成了指针自身的字节数64 位系统下是 8跟字符串内容没有关系。字符串相关题目里这是最容易失分的地方。5.3 关于 printf 中文乱码的那点事题目搜出来经常带着一个衍生热词printf 在终端里输出中文乱码。这个和sizeof(a)没有逻辑关系但因为它总跟着 C 语言学习内容一同出现我顺手说清楚成因。printf本身不做任何编码转换它只是把内存里的字节原样写到标准输出。中文乱码的本质是程序里的字节编码与终端希望接收的编码不一致。Windows 控制台默认可能是 GBK/936 编码而你用现代编辑器保存的源文件是 UTF-8Linux 终端默认 UTF-8但程序里写入的是 GBK 编码字符串这两者对不上自然全是乱码。解决办法有几个方向Windows 下在程序开头调用SetConsoleOutputCP(CP_UTF8)并把源文件保存为 UTF-8。在.cpp或.c文件头加#pragma execution_character_set(utf-8)让编译器把字符串字面量的编码强制改成 UTF-8。MSVC 编译时加/utf-8选项。Linux 下把终端 locale 调成 UTF-8用locale命令确认。这是老生常谈但每次有新人问 printf 问题都会带出来我就在这里一并归档了。5.4 笔试题排版里的全角符号陷阱回到最开始那个标题。printf(“%d“,sizeof ‘a‘)里面引号是全角、逗号是全角、分号是全角直接拿去编译必挂。老师出题时可能是从 Word 文档复制的成了看起来能跑的代码。真实答题环境里你先把这些符号全部替换成半角再去看逻辑。我个人建议拿到任何一段代码先做三件事——检查符号全半角、检查头文件、检查结尾分号。这三件事跟算法无关但决定了你是否能进入真正的问题讨论。写在最后的一个小技巧这个sizeof(a)的题目之所以经典是因为它同时考察了字面量类型、运算符体系、语言标准差异和平台数据模型。如果你能不看编译器就推断出结果并且能解释 C 和 C 为什么给出不同答案说明你对两种语言的基本盘已经有比较扎实的认知了。我在面试中看到候选人背出4 和 1时一般会追加一个问题如果某个平台上sizeof(int)变成 8C 语言的sizeof(a)是多少答案会从 4 变成 8而 C 仍然稳定输出 1。其实这个变换还可以继续往下推如果有一天 C 标准决定把字符字面量改成char类型那大量历史代码的运算行为都会跟着变因为a参与表达式时不需要整型提升的规则会被改写。理解到这一层你才算真正把printf(%d, sizeof a)这行看似简单的代码吃透了。
返回列表