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 的整数类型参与大多数算术运算时,会先提升为 intunsigned 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

大致优先级:

  1. 有浮点类型时,整数通常转成浮点。
  2. 没有浮点时,先做整型提升。
  3. 再根据有符号/无符号、类型宽度选择共同整数类型。

常见直觉是“大类型吃小类型”:表达式会先把较小或较低优先级的类型转换到共同类型,再计算。

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;  // 错误倾向:不是四舍五入,而是截断为 3
char 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 charunsigned char
  • int8_t / uint8_t 常常表现得像字符类型,打印数值时可先转成 int
  • bool 在 C99 中来自 <stdbool.h>,本质上仍然参与整数转换。
  • 浮点数不适合直接表示金额;需要精确小数时用整数分单位或专门十进制库。
  • 数组到指针转换不是普通数值转换,但它同样会在表达式和函数调用中自动发生,是 C 语言最重要的隐式转换之一。

相关:CandCpp数据变量进阶C语言指针细节未定义行为

学习路径