C 语言类型转换规则
内容简介
C 的类型转换规则决定表达式如何计算。它们不是语法边角料,而是很多 bug 的来源:整数除法、小整数提升、有符号/无符号混用、浮点精度丢失、指针强转和对象表示问题都在这里。
本篇以 C 语言为主,C++ 的 static_cast 等写法见 cpp自动类型转换。
用法
赋值转换
赋值时,右侧表达式会转换成左侧对象的类型。
double price = 23.9;
int value = price; // value 为 23,小数部分被截断
unsigned char byte = 300; // 结果依赖 unsigned char 的取模范围,常见为 44风险:
- 浮点转整数会丢失小数部分。
- 大范围转小范围可能溢出或得到实现定义结果。
- 负数赋给无符号整数会按无符号范围取模。
大类型装进小类型
把范围更大的类型赋给范围更小的类型,可能发生截断、回绕、实现定义结果或未定义行为。直觉上可以理解成“小盒子装不下大数”。
int big = 1000;
char small = big; // small 装不下 1000,结果依赖 char 范围和实现
long long file_size = 5000000000LL;
int size = file_size; // int 通常装不下,结果不可靠无符号整数转换更像按范围取模:
unsigned char byte = 300; // 如果 unsigned char 是 8 位,常见结果是 44
unsigned int u = -1; // 常见结果是 UINT_MAX有符号整数溢出是未定义行为:
int max = 2147483647;
int overflow = max + 1; // 错误:有符号整数溢出是未定义行为因此不要把“机器上看起来会回绕”当成 C 语言保证。
浮点转整数:小数被吞
浮点数转整数时,小数部分会向 0 截断,不是四舍五入。
int a = 3.9; // 3
int b = -3.9; // -3如果浮点值超出目标整数能表示的范围,行为未定义。
double too_big = 1e100;
int value = too_big; // 错误:超出 int 范围如果业务需要四舍五入,应显式调用数学函数,而不是依赖赋值转换:
#include <math.h>
int rounded = (int)round(3.6);如果业务需要精确金额,不要用 double 存金额再转 int,应使用整数分单位:
int price_cent = 1999; // 19.99 元整型提升
小于 int 的整数类型参与大多数算术运算时,会先提升为 int 或 unsigned int。
char a = 100;
char b = 30;
int sum = a + b; // a 和 b 通常先提升为 int因此 sizeof(a + b) 常常不是 sizeof(char)。
通常算术转换
二元算术运算会把两边转换到一个共同类型再计算。
int a = 8;
double b = 5.0;
double r = a / b; // a 转成 double,结果 1.6大致优先级:
- 有浮点类型时,整数通常转成浮点。
- 没有浮点时,先做整型提升。
- 再根据有符号/无符号、类型宽度选择共同整数类型。
常见直觉是“大类型吃小类型”:表达式会先把较小或较低优先级的类型转换到共同类型,再计算。
int count = 3;
double avg = 10.0 / count; // count 先转成 double但这个规则不能简单理解为“结果总是更安全”。有符号和无符号混用时,负数可能先变成很大的无符号数。
整数除法
int a = 8 / 5; // 1
double b = 8 / 5; // 1.0,先整数除法,再转 double
double c = 8.0 / 5; // 1.6需要小数结果时,至少让一个操作数在除法前就是浮点类型。
容易误解的写法:
int total = 8;
int count = 5;
double avg1 = total / count; // 1.0,整数除法已经完成
double avg2 = (double)(total / count); // 1.0,强转太晚
double avg3 = (double)total / count; // 1.6,正确原则:转换要发生在运算之前,而不是运算之后。
有符号和无符号混用
int a = -1;
unsigned int b = 1;
if (a < b) {
/* 很多平台上条件为假,因为 a 会转换成很大的 unsigned int */
}这类转换最容易隐藏在比较、循环边界和数组下标中。
size_t len = 0;
printf("%zu\n", len - 1); // 下溢,变成很大的 size_t强制类型转换
C 风格强制转换会要求编译器按目标类型解释表达式。
double result = (double)8 / 5;强转不是“修复类型系统”,只是告诉编译器“我知道自己在做什么”。尤其是指针强转,必须确认真实对象类型、对齐和生命周期都正确。
指针相关转换
void* 可以保存对象指针,常用于通用接口:
int value = 10;
void *raw = &value;
int *p = raw; // C 中 void* 可隐式转回对象指针
printf("%d\n", *p);注意:
void*不能直接解引用。- 函数指针和对象指针不是一类东西,不应互相强转。
- 把一种对象指针强转成另一种不兼容对象指针后解引用,可能违反严格别名规则。
数组到指针转换
数组名在大多数表达式中会自动转换为指向首元素的指针,这也是“数组和指针关系”最容易混淆的地方。
int arr[4] = {1, 2, 3, 4};
int *p = arr; // arr 转换为 &arr[0]但数组不是指针。数组对象本身包含所有元素,指针对象只保存一个地址。
printf("%zu\n", sizeof(arr)); // 4 个 int 的总字节数
printf("%zu\n", sizeof(p)); // 指针本身的字节数数组到指针转换的完整规则、函数参数退化和接口写法见 C语言指针细节。
最佳代码实践
- 不依赖隐式转换“猜对”,关键处显式写出目标类型。
- 大范围转小范围前先做范围检查。
- 浮点转整数前先明确是截断、四舍五入、向上取整还是向下取整。
- 整数除法前先确认是否需要浮点结果。
- 避免有符号和无符号混合比较;
size_t只用于大小、长度和字节数。 - 数组接口不要依赖数组形参里的长度,数组传参规则见 C语言指针细节。
- 处理二进制协议、文件格式、寄存器时使用
<stdint.h>的固定宽度整数。 - 编译时打开转换警告,例如
-Wall -Wextra -Wconversion,把警告当成设计反馈。 - 指针强转前先确认真实对象类型、对齐要求和生命周期。
错误用法
double r = (double)(8 / 5); // 错误倾向:除法已经按整数完成int value = 3.99; // 错误倾向:不是四舍五入,而是截断为 3char small = 1000; // 错误倾向:目标类型可能装不下for (int i = 0; i < strlen(text); ++i) {
/* strlen 返回 size_t,且每轮都重新计算长度 */
}int i = -1;
size_t n = 3;
if (i < n) {
/* 错误倾向:i 可能先转成 size_t */
}float f = 0.1f;
if (f == 0.1) {
/* 错误倾向:浮点表示和转换都可能影响结果 */
}double d = 3.14;
int *p = (int*)&d;
printf("%d\n", *p); // 错误:类型、对齐和别名规则都不可靠注意事项
- C 里的“转换后结果是什么”分为几类:标准明确、实现定义、未指定、未定义行为。不能都当成“机器会随便但通常可用”。
char是否有符号由实现决定。需要数值范围时写signed char或unsigned char。int8_t/uint8_t常常表现得像字符类型,打印数值时可先转成int。bool在 C99 中来自<stdbool.h>,本质上仍然参与整数转换。- 浮点数不适合直接表示金额;需要精确小数时用整数分单位或专门十进制库。
- 数组到指针转换不是普通数值转换,但它同样会在表达式和函数调用中自动发生,是 C 语言最重要的隐式转换之一。
相关:CandCpp数据变量进阶、C语言指针细节、未定义行为