多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

C语言类型转换详解:隐式转换、整型提升与强制转换

C语言类型转换详解:隐式转换、整型提升与强制转换 1. 一个看似简单却让人翻车的表达式先从一个实际场景说起。假如你写了一段这样的代码#include stdio.h int main() { char c 127; c c 1; printf(%d\n, c); return 0; }猜猜输出是什么很多刚学C语言的读者会脱口而出“128”但在大多数环境下跑一下出来的结果是-128。为什么这就涉及到C语言中不同数据类型之间运算的底层机制整型提升、隐式转换、溢出行为。如果只背“char的取值范围是-128到127”那只能算知其然要解释清楚为什么127加1变成了-128就必须理解char类型在运算过程中到底发生了什么。再来看一个更反直觉的例子#include stdio.h int main() { unsigned int a 1; int b -2; if (a b 0) { printf(positive\n); } else { printf(negative\n); } return 0; }结果输出的是“positive”。一个负数和正数相加结果竟然是正的这其实不是数学错了而是C语言的类型转换规则在起作用。如果不清楚有符号数和无符号数混算时的转换方向这类问题在真实项目中排查的时候能浪费掉整整一个下午。这篇文章就围绕C语言中不同数据类型之间的运算做一次完整的梳理覆盖隐式转换、整型提升、强制类型转换以及它们在实际编程中的各种表现。无论你是刚入门C语言的新手还是已经写了几年嵌入式、系统级代码但偶尔被类型转换坑过的开发者这篇文章都值得花十分钟细看。内容尽量把规则讲透把例子跑通把我自己踩过的坑也一起放进来。2. 隐式转换编译器替我们做的类型适配2.1 隐式转换的本质与触发场景隐式转换也叫自动类型转换是编译器在编译阶段自动完成的类型适配行为。程序员不需要写任何额外的语法编译器看到两侧操作数类型不一致时会按照一套固定的规则把其中一个或两个操作数转成同一类型再执行运算。这套规则的本质是尽量让运算不丢精度、不丢信息。C语言标准定义了一套“类型等级”体系编译器转换的时候默认往“更高级”的类型靠。从低到高大致是int unsigned int long unsigned long long long unsigned long long float double long double注意C语言标准其实不是这么简单线性的还有整数转换等级、实浮点等级等细分规则但对于大多数日常编程场景上面这个“直觉版本”已经够用了。关键在于理解方向char和short参与运算会被提升为intint和double混算时int会被转成double有符号和无符号混算时有符号会被转成无符号。最常见的触发场景有三类。第一类是算术运算比如int double、int / long第二类是赋值操作比如把一个double赋值给一个int变量或者把int赋值给char第三类是函数调用实参类型和形参类型不一致时也会发生隐式转换。2.2 算术运算中的隐式转换方向算术运算中的隐式转换规则是面试和笔试里出现频率最高的考点。看这个例子#include stdio.h int main() { int i 5; double d 2.0; double result i / d; printf(%f\n, result); return 0; }i / d是一个int除以double类型不一致编译器判定int的等级低于double于是把i转成double再参与运算结果是2.500000。这个转换是“无损”的因为任何int值都能精确地表示成double虽然double表示整数时也有精度上限但对32位int来说是足够的。真正容易踩坑的是下面这种#include stdio.h int main() { int a 5; int b 2; double result a / b; printf(%f\n, result); return 0; }输出是2.000000不是2.500000。为什么因为a和b都是int两个相同类型的操作数运算不需要任何转换先执行整数除法得到2然后才把这个整数结果赋值给double变量赋值时发生隐式转换变成2.0。这个“先运算、后赋值”的顺序极其重要是我见过初学者最容易犯的错误之一。要想得到2.5至少要把其中一个操作数转成double比如a / (double)b。2.3 赋值操作中的隐式转换与精度损失赋值操作中的隐式转换和算术运算不太一样。算术运算是“往更高级转”赋值却是“往目标类型转”不管目标类型是高级还是低级。这就意味着赋值转换可能发生精度丢失。#include stdio.h int main() { double pi 3.14159; int truncated pi; printf(%d\n, truncated); return 0; }输出是3。double转int直接截断小数部分不会四舍五入。这一行为在C标准里称为“向零取整”。如果pi是负数比如-3.7赋给int的结果是-3同样是向零截断。还有更隐蔽的精度问题#include stdio.h int main() { float f 0.1; double d f; printf(double from float: %.17f\n, d); printf(double literal: %.17f\n, 0.1); return 0; }很多人以为float转double是零成本的升级赋值之后d就等于0.1。实际上float只能保证约6位有效十进制数字的精度0.1f在二进制里本身就是一个无限循环小数存进float时已经被截断了一部分。转换到double时这个被截断的值会被原样扩展成double的精度而不是重新变成更精确的0.1。所以d和字面量0.1是不相等的。这就是为什么在项目中做浮点数比较时不要直接用而是比较差值的绝对值是否小于某个精度阈值否则各种诡异bug层出不穷。这部分后面实战章节展开。2.4 有符号数与无符号数混合运算的“暗雷”有符号数和无符号数混合运算是隐式转换里最容易埋雷的地方也是无数线上事故的根源。规则本身很简单同等级的有符号和无符号混合时有符号转成无符号。问题在于转成无符号之后负数会变成一个巨大的正数。#include stdio.h int main() { int a -1; unsigned int b 1; if (a b) { printf(a b\n); } else { printf(a b\n); } return 0; }直觉上-1肯定小于1但输出是“a b”。原因是比较时a被转成unsigned int-1在无符号表示下是4294967295自然大于1。这个坑在真实项目里非常常见。比如遍历容器时习惯性写for (int i 0; i len; i)没问题但如果在某个库里len是size_t而你又把i换成了unsigned int类型再配合递减循环就很容易写出死循环。更经典的是for (unsigned int i n; i 0; i--) { // 这段代码永远不会结束 }因为i 0这个条件对于任何无符号数都是恒真的i--在i为0时会回绕到4294967295死循环。在处理这类问题的时候我个人的习惯是只要涉及比较或运算尽量让所有操作数保持同一个符号性。要么全有符号要么全无符号不要混用。如果在写库函数时必须接受外部传入的无符号参数内部运算时注意显式转换。3. 整型提升藏在char和short背后的隐形规则3.1 什么是整型提升为什么需要它整型提升是隐式转换的一种特殊形式规则非常明确所有小于int的整数类型char、short、_Bool、枚举类型以及它们的signed/unsigned变体在参与表达式运算时一律先提升为int如果int放不下比如unsigned short在某些平台和int等宽就提升为unsigned int。为什么要做这一步直接原因和CPU的寄存器设计有关。现代CPU的算术逻辑单元ALU通常以机器字长为单位进行运算int类型的宽度正好是对齐机器字长的。比如在32位或64位平台上int都是32位和通用寄存器等宽。如果允许char直接做加法运算CPU需要额外处理“只使用低8位”的情况会增加额外的掩码指令。与其让硬件去适配各种窄类型不如C标准规定运算前统一提升到int用全寄存器宽度算完最后按需截断。性能更好硬件设计也更简洁。拿文章开头的例子来分析char c 127; c c 1;c 1中的c是char先整型提升为int得到127然后和int型的1相加得到128。到这一步完全没有问题。接下来把128赋值回char变量c赋值转换要求把int窄化成char。在signed char占8位、使用补码表示的平台上128已经超出char能表示的最大值127于是发生有符号整数溢出。C标准里有符号整数溢出属于未定义行为但在几乎所有x86/ARM平台上结果表现为回绕截断128的二进制是10000000截断到8位后解释为有符号数就是-128。所以输出-128实际上是整型提升赋值窄化溢出行为三者共同作用的结果。3.2 整型提升在表达式求值中的具体表现#include stdio.h int main() { char a 100; char b 100; char c a b; printf(a b %d\n, a b); printf(c %d\n, c); return 0; }看看输出a b 200 c -56a b两个char运算都提升为int得到200所以第一行输出200。赋值给char时200对char来说超出了范围最大127截断后变成-56。这里200的二进制是11001000在8位有符号解释下正好是-56补码。这种情况在编写数组下标、循环计数、字节拼接等场景中特别容易出问题。比如你在做字符串处理时写char s[] hello; int len strlen(s); for (char i 0; i len; i) { // 如果len超过127这个循环就会出问题 }上面这段代码在现代平台上几乎必然出错因为char类型的循环变量最大只能到127而字符串长度很容易超过这个值一旦i超过127再自增就会溢出变成负数循环条件i len永远成立死循环或者跳出循环的行为都变得不可预期。正确的做法是把循环变量声明成int或size_t。3.3 一个容易忽略的细节sizeof运算符与整型提升sizeof运算符和整型提升的关系很多人没注意到。C标准规定sizeof的操作数如果是表达式不会真的对表达式求值但类型需要确定。由于整型提升发生在“表达式求值”阶段sizeof作用于表达式时并不触发整型提升。看这段代码#include stdio.h int main() { char c a; printf(%zu\n, sizeof(c)); printf(%zu\n, sizeof(c c)); return 0; }第一行输出1第二行输出4。sizeof(c)直接看char类型是1字节sizeof(c c)中c c经过整型提升变成int所以大小是4字节在32位/64位平台上。这个例子用来区分“表达式本身被提升”和“sizeof只关心类型不关心求值”再好不过。还有一个衍生坑。sizeof(a)在C语言里的结果是什么答案是4不是1。因为在C语言中字符字面量a的类型本身就是int不是char。这跟C里的行为不一样C里a的类型是char。很多从C转到C的开发者在这上面栽过跟头。如果要在这段代码里得到“字符占1字节”的印象必须写成sizeof((char)a)。3.4 整型提升在变长参数里的影响printf中的经典翻车现场printf函数是变长参数函数参数在传递时会发生默认实参提升。这个过程和整型提升关系密切所有小于int的整数类型如char、short会提升成intfloat会提升成double。这就是为什么用printf打印float时无论如何都要用%f——因为float早被提升成double了根本不存在“以float形式读参数”的路径。如果用%lf去打印float提升后的double在C99之后%lf和%f对于double类型的参数等价所以问题不大。但如果你写了个自定义的变长参数函数没有正确处理提升后的类型就会出现未定义行为。举个更隐蔽的例子#include stdio.h int main() { short s 32767; printf(%hd, %d\n, s, s); return 0; }%hd表示按short读取但由于默认实参提升实际传入的已经是int%hd会把这个int转换为short后读取通常结果正确。然而如果格式串和实际类型不匹配比如用%d去打印long long或者用%f去打印int那就是纯粹的未定义行为程序可能直接段错误。这类问题的根源越来越多人不知道变长参数列表里窄类型已经被悄悄提升了你必须在格式串中按照提升后的类型去匹配。实践中如果对不上不要指望printf纠错它只会用错误的长度去内存里抓数据。4. 强制类型转换程序员越过程序员的权力4.1 显式转换的三种写法与各自的适用场景当隐式转换的结果不符合需求时程序员可以用强制类型转换主动干预。C语言里显式转换的语法核心是(type)expression写法如下double x 3.7; int n (int)x; // 截断n为3 int m (int)(x 0.5); // 手动四舍五入m为4在C语言中强制类型转换本质上就是一个一元运算符优先级很高和、--这类运算符同级。它可以作用于变量、表达式或指针生成的指令往往和对应宽度的截断/扩展指令一一对应。在嵌入式和驱动开发中访问硬件寄存器时需要反复在整数地址和指针类型之间转换#define REG_BASE 0x40000000UL volatile uint32_t *reg (volatile uint32_t *)REG_BASE; *reg 0x01;这里把整数地址强制转换成指向volatile修饰的32位无符号整数的指针本质上是告诉编译器“我知道这个地址是硬件寄存器别优化我的读写。”如果少了volatile编译器可能把连续的写操作优化得面目全非。4.2 强制转换不是万能药它只是重新解释比特位强制类型转换的最大特点是它不改变内存中的二进制内容只是改变编译器解释这组比特位的方式除了算术转换中涉及宽度变化的情况。看下面这个例子#include stdio.h #include stdint.h int main() { float f 1.0f; uint32_t u *(uint32_t *)f; printf(float bits: 0x%08x\n, u); return 0; }输出结果是一个十六进制数0x3f800000这正是IEEE 754标准下单精度浮点数1.0的二进制表示。这里我们通过指针把一个float的底层位模式“重新解释”成整数而不是把数值1.0转成整数1。这两者有天壤之别。(uint32_t)f得到的是1是数值转换*(uint32_t *)f得到的是0x3f800000是位模式重解释。搞不清这个区别是很多底层编程新手调试不出问题的原因。IEEE 754的布局最高1位符号位接着8位指数再接着23位尾数。1.0的符号位为0指数位偏移127为127即0x7f尾数位全0合起来得到0x3f800000。4.3 浮点与整型互转时的细节溢出与截断方向浮点转整型时如果浮点数的值超出了目标整型的范围结果是未定义行为。这一点经常被忽略。代码#include stdio.h int main() { double d 1e300; int n (int)d; // 未定义行为 printf(%d\n, n); return 0; }1e300远超int的表示范围强制转换结果在多数平台上是一个无意义的垃圾值但在标准层面这属于未定义行为编译器可以做任何事。更稳妥的做法是转换前先判断范围if (d INT_MIN d INT_MAX) { int n (int)d; } else { // 溢出处理 }另外C语言浮点转整型是向零截断不是四舍五入。这一点在金融、统计数据计算中很容易埋雷。比如统计一批数据的平均值得到2.7直接转整型得到2累计多次之后误差会很大。如果需要四舍五入可以自己写int rounded (int)(value (value 0 ? 0.5 : -0.5));不要依赖round()这个库函数在某些旧平台上的链接问题这个手动做法虽然土但在各种环境里都可移植。4.4 指针类型之间的强制转换需要格外谨慎的领域指针之间的强制转换比数值之间更危险。C标准规定不同类型的对象指针之间互相转换后如果对齐要求不满足直接用转换后的指针解引用属于未定义行为。简单说就是把一个对齐要求更高的指针转成对齐要求更低的指针通常是安全的但反过来不一定。比如#include stdio.h int main() { char buf[4] {0x12, 0x34, 0x56, 0x78}; int *p (int *)buf; printf(0x%08x\n, *p); return 0; }在x86平台上通常能跑出某个值小端序下是0x78563412但在ARM等需要严格对齐的平台上buf的地址很可能不是4字节对齐的直接解引用*p会触发总线错误程序崩溃。这种代码拿来做字节序转换是典型的错误示范。正确做法是使用memcpy编译器在优化阶段往往能把memcpy优化成和强制转换等效的指令既安全又高效#include stdio.h #include string.h int main() { char buf[4] {0x12, 0x34, 0x56, 0x78}; int n; memcpy(n, buf, sizeof(n)); printf(0x%08x\n, n); return 0; }ptrcast的另一个常见坑是函数指针和数据指针的互转。C标准并不保证数据指针和函数指针大小相同也不保证能互相转换并再次调用。在POSIX系统上dlsym返回void*需要转换成函数指针类型才能调用这是平台扩展行为很多编译器提供了支持但严格来说它不属于标准C的范畴。写了这种代码就要做好换平台就可能跑不起来的心理准备。5. 不同类型混合运算的完整实战案例5.1 浮点数精度问题为什么0.1加0.2不等于0.3先看一个几乎所有C/C程序员都遇到过的经典问题#include stdio.h int main() { float a 0.1f; float b 0.2f; if (a b 0.3f) { printf(equal\n); } else { printf(not equal\n); } return 0; }输出是不等。原因在于浮点数在计算机中是用二进制科学计数法存储的而0.1、0.2、0.3这些十进制小数在二进制里都是无限循环小数无法被有限的二进制位数精确表示。IEEE 754单精度浮点数的尾数只有23位相当于大约7位有效十进制数字。0.1f实际存储的值是0.1000000014901161193847656250.2f实际存储的值是0.20000000298023223876953125两者相加得到0.300000011920928955078125而这个值不等于0.3f实际存储的0.300000011920928955078125吗巧的是0.3f的实际存储值正好也是0.300000011920928955078125。那为什么输出还是not equal关键在于算术运算中的类型转换。如果用double来算#include stdio.h int main() { double a 0.1; double b 0.2; double c a b; printf(c 0.3 ? %d\n, c 0.3); printf(c %.17f\n, c); printf(0.3 %.17f\n, 0.3); return 0; }输出c 0.3 ? 0 c 0.30000000000000004 0.3 0.29999999999999999所以0.1和0.2在double中的近似值相加并不等于0.3的近似值。而之前float版本中a b因为整型提升不浮点参与运算时没有“整型提升”的说法但float之间做加法时运算按float精度执行还是按double精度执行呢这里有个关键点C标准规定如果FLT_EVAL_METHOD为0则float运算以float精度求值如果是2则以long double精度求值。不同平台表现可能不同这也是浮点代码可移植性的坑之一。在常见的x86-64 Linux上FLT_EVAL_METHOD为0float运算直接按float精度算。那0.1f 0.2f的结果是0.300000011920928955078125而0.3f也是0.300000011920928955078125这两者其实相等啊为什么上面的代码输出not equal仔细看上面的代码问题出在比较的时候a b 0.3f的右侧是0.3f它是float类型。左侧a b也是float类型。两个float的比较应该是相等的。让我重新跑一下这个逻辑。实际上0.1f0.2f的结果和0.3f在二进制上不完全相同因为0.1f和0.2f的误差叠加方式和直接存0.3f的误差方式不同。两者只是都接近0.3但不一定完全相等。数值计算一下0.1f暗含的二进制浮点数实际值是0.1000000014901161193847656250.2f是0.20000000298023223876953125相加得到0.300000004470348358154296875而0.3f是0.300000011920928955078125。注意这两个值在小数点后第8位左右开始不同0.30000000447和0.30000001192。所以float版本同样是不相等的。我之前记的0.30000001192895是0.3f本身而相加结果是0.30000000447035确实不同。这就是为什么比较浮点数的教科书建议从来都是“不要用比较浮点数”必须引入容差#include stdio.h #include math.h int main() { double a 0.1; double b 0.2; double c a b; double epsilon 1e-9; if (fabs(c - 0.3) epsilon) { printf(approximately equal\n); } return 0; }注意fabs返回double和epsilon比较时两者类型一致不发生隐式转换的意外。如果epsilon定义成float比较时会被提升为double这没问题但如果你写if (fabsf(c - 0.3f) 1e-6f)这里的类型匹配就要格外小心。5.2 整型除法与取余的隐式转换过程两个整数相除得到整数结果这是初学C语言时必背的一条规则。但混合了不同类型之后规则会复杂一些#include stdio.h int main() { int a 7; unsigned int b 2; double result a / b; printf(%f\n, result); return 0; }a / b中a是intb是unsigned int两者等级相同但符号性不同按照隐式转换规则a被转成unsigned int然后做整数除法。7 / 2的整数除法结果是3再把3转成double输出3.000000。如果a是负数比如int a -7那么a被转成unsigned int后变成一个很大的正数除以2之后依然是一个很大的正数结果和数学上的-3.5差了十万八千里。这种bug不亲自踩一次很难长记性。取余运算%的规则与除法一致只要一侧是无符号数整个运算就变成无符号取余。如果你在写校验和或者哈希算法时遇到负数参与取余结果往往会出乎意料。比如常见的“判断是否为偶数”的代码int n -5; if (n % 2 0) { // ... }这没问题因为两个操作数都是int-5 % 2的结果在C99标准里是-1向零截断不等于0正确判断为奇数。但如果写成unsigned int m 5; int n -5; if (n % m 0) { // ... }n会被隐式转换成一个巨大的无符号数取余的结果就毫无意义了。这种代码在代码审查的时候属于必须打回重写的类型。5.3 混合运算在真实项目中的排查思路实际项目里的类型问题很少像教科书一样直接摆在面前大都藏在层层调用之后。我在排查线上问题时的经验是先看编译警告。GCC和Clang在开启-Wall -Wextra之后很多隐式转换问题会直接给出警告信息比如“comparison of integer expressions of different signedness”这类提示。看到这条警告几乎可以肯定代码里有有符号和无符号混用的隐患。代码审查时我一般重点盯这几个位置函数参数传递时实参类型和形参类型不一致的地方。循环变量和容器长度size_t比较的地方。位运算操作数中有无有符号数和无符号数混用。返回值类型和接收变量类型不一致的地方。举一个我自己处理过的真实例子。某个嵌入式项目里有一段计算温控步数的代码原始写法类似int16_t temperature get_temperature(); uint16_t step (uint16_t)((temperature - 25) * 10);逻辑是读取一个温度值减去25摄氏度基准乘以10得到步进数。问题是temperature是int16_t可能为负。当温度为20时temperature - 25得到-5然后被转成uint16_t变成一个65531之类的值再乘以10反复溢出最终步进数完全乱套。修复方案是先把差值存到有符号变量里确认数值在合理范围内后再处理。有时候最简单的修复不是加一堆强制转换而是把中间变量换成范围足够大、符号性正确的类型并显式检查边界。这种问题的排查链路通常是从现象反推——步进数异常、温度越低越乱怀疑负数处理失败然后查类型——确认int16_t和uint16_t混算最后用最小复现代码验证——在PC上用同样的类型定义模拟一遍确认行为一致。我在排查文章里写这一段想表达的核心是面对诡异的数值结果先怀疑类型转换再怀疑算法逻辑。因为类型转换错误往往比算法错误更隐蔽编译器不会帮你发现。5.4 数据截断经典模型大端小端与强制转换数据截断是强制类型转换最常见的后果之一。当一个宽类型转成窄类型时高位的比特被直接丢掉。例如#include stdio.h int main() { unsigned int value 0x12345678; unsigned char low (unsigned char)value; printf(0x%02x\n, low); return 0; }输出是小端序平台上的0x78。在小端字节序中unsigned int的最低字节存放在内存地址最低处强制转成unsigned char时保留低位所以得到0x78。在大端平台上得到的是0x12。这个差异意味着直接通过截断来提取“某个字节”写出来的代码是不可移植的。如果你需要提取特定的字节应该用位运算unsigned char byte0 (value 0) 0xFF; unsigned char byte1 (value 8) 0xFF;这种写法不依赖字节序在任何平台上行为一致。底层开发人员处理通信协议、解析报文时应该养成用移位和掩码的习惯而不是依赖强制类型转换加指针解引用。另一个相关的经典错误是用强制转换解析网络字节序的报文char buf[4] {0x00, 0x00, 0x01, 0x00}; uint32_t len *(uint32_t *)buf;除了前面提到的对齐问题这份代码还假设了本机字节序和报文字节序一致。如果报文是大端序而本机是小端序解析出来的值就完全错了。正确做法是用ntohl这类网络字节序转换函数或者手动拼接收移位uint32_t len ((uint32_t)buf[0] 24) | ((uint32_t)buf[1] 16) | ((uint32_t)buf[2] 8) | ((uint32_t)buf[3]);这里注意每项强制转成uint32_t再移位否则buf[0]是char整型提升后可能产生符号扩展移位结果就错了。这个例子恰好串联了整型提升、符号扩展、字节序和强制转换多个知识点值得多看两遍。6. C语言类型转换的企业级自查清单遇到和类型转换、混合运算相关的代码问题把它当成一个系统性的排查流程来做效率会高很多。下面这份自查清单是我多年写C代码总结出来的供参考。检查编译警告在GCC/Clang中用-Wall -Wextra -Wconversion编译。-Wconversion特别有用它能检测隐式转换可能改变数值的场合虽然有时会误报但在开发阶段宁可多处理几条警告也不要漏掉真正的隐患。确认每个操作数的“真实类型”要特别小心typedef带来的“假类型”。typedef int INT32;之后INT32和int在编译器眼里完全等价不会提供任何类型防护。如果你希望类型本身能防止混用应该考虑用结构体包装或者用C语言的_Generic配合静态断言。混合运算前手动把关键操作数转成期望的类型。不要依赖隐式转换显式写出你的意图。比如比较运算前确定清楚两边到底都按有符号还是都按无符号处理用强制转换明确。对浮点数比较统一用fabs(a - b) epsilon的方案并明确epsilon的量级取决于你的精度需求。如果是金额计算使用整数类型如以分单位的整数而不是浮点类型。位运算优先用unsigned int或uint32_t一类的类型避免有符号类型参与位运算导致实现定义的行为。有符号右移是算术右移还是逻辑右移由实现决定这个坑在生产代码里非常常见。#include stdio.h #include stdint.h int main() { int32_t signed_val -1; uint32_t unsigned_val 0xFFFFFFFFU; printf(signed 4 %d\n, signed_val 4); printf(unsigned 4 %u\n, unsigned_val 4); return 0; }大多数平台上有符号-1右移4位还是-1算术右移无符号0xFFFFFFFF右移4位变成0x0FFFFFFF。如果不知道这个区别在写位压缩协议、实现位移加密逻辑时很容易出错。使用memcpy替代转换指针类型来解析字节几乎所有主流编译器都能把固定长度的memcpy优化成一条加载指令性能不输指针转换但规避了对齐和别名aliasing规则的风险。永远假设隐式转换可能发生并在代码审查时明确讨论每一个跨类型赋值。我见过太多bug到最后都是“没注意那边类型是unsigned”。为了防止自己在新代码里踩坑我现在写C代码时的习惯是这样的类型声明时尽量少用char做算术运算循环变量始终和容器长度同类型浮点计算后如果需要整数结果先检查范围再转换强制转换时在旁边写一行注释说明转换的假设前提比如“这里假设value在0到255之间后面的代码依赖这个前提”。这些习惯看起来琐碎但长期坚持下来能省下大量排查崩溃和数据异常的时间。C语言给了程序员极大的控制权也给了极大的犯错空间。理解类型转换的底层逻辑是能在C语言世界里安全行走的前提。如果读完这篇你再看到127 1变成-128的题目能直接讲清楚“整型提升到int计算得到128赋值回char时溢出截断补码表示为-128”这个完整链路那这篇文章的目的就算达到了。
返回列表