C语言基础教程:基础数据类型

今天这次咱们就初步看看 C 语言的所有基本数据类型。

理解数据类型,不仅是学习基础知识,更是开始去理解你要用代码去进行建模的世界你的代码要运行的目标平台的约束。选对类型,程序才能既高效又可靠;举一反三,面对新的事物也容易推测出其含义了。

对于初学者,尤其是有 Python 编程经验 的学习者来说,C语言可能显得“不够灵活”:

  • Python 中的整数是任意精度的,可以轻松表示天文数字;而 C 语言的 intlong 等类型有固定长度(如 4 字节或 8 字节),超出范围就会溢出。
  • Python 是动态类型语言,变量无需声明即可赋值,且类型可随时改变(如 x = 5 后再 x = "hello");而 C 语言要求所有变量必须提前声明具体类型,且一旦定义就不能更改。

然而,这种“不灵活”恰恰是 C 语言的优势所在:

固定精度确保了内存占用可预测,程序行为稳定;
静态类型让编译器能在编译期完成类型检查和优化,大幅提升运行效率;
没有运行时类型推断和对象管理开销,使 C 成为系统编程、嵌入式开发等性能敏感场景的首选。

还要注意,C 语言标准只规定了各类数据类型的最小范围(如 int 至少 16 位),但具体长度由操作系统和编译器决定。例如,在 32 位系统上 long int 可能是 4 字节,而在 64 位 Linux 上则可能是 8 字节。因此,在编写可移植代码前,务必使用标准库查询当前平台的实际限制


一、整型(Integer Types)——离散世界

整型用于表示没有小数部分的数值,如年龄、计数器、数组下标等。 在货币相关的场景中,由于货币存在最小单位(如人民币的“分”),金额本质上是离散量——不存在0.5分,因此用整型存储金额,在数值不是特别大的情况下,是比较精确的做法。注意,只是说精确,不一定是正确,因为有时候得权衡。

示例 1:用整型处理货币数值

#include <stdio.h>

int main() {
    // === 场景一:使用浮点数(元)—— 会出现精度问题 ===
    double price1 = 0.1;  // 0.1 元
    double price2 = 0.2;  // 0.2 元
    double total_float = price1 + price2;

    printf("【浮点数计算(元)】\n");
    printf("0.1 + 0.2 = %.17f\n", total_float);  // 显示足够多小数位以暴露误差
    if (total_float == 0.3) {
        printf("结果等于 0.3 元?是\n");
    } else {
        printf("结果等于 0.3 元?否!实际值 ≠ 0.3\n");
    }
    printf("\n");

    // === 场景二:使用整数(分)—— 完全精确 ===
    int price1_cents = 10;   // 0.10 元 = 10 分
    int price2_cents = 20;   // 0.20 元 = 20 分
    int total_cents = price1_cents + price2_cents;

    printf("【整数计算(分)】\n");
    printf("%d 分 + %d 分 = %d 分\n", price1_cents, price2_cents, total_cents);
    printf("换算为元: %d.%02d 元\n", total_cents / 100, total_cents % 100);

    if (total_cents == 30) {
        printf("结果等于 30 分(即 0.30 元)精确无误\n");
    }

    return 0;
}

上面的代码得到的输出如下所示:

【浮点数计算(元)】
0.1 + 0.2 = 0.30000000000000004
结果等于 0.3 元?否!实际值  0.3

【整数计算(分)】
10  + 20  = 30 分
换算为元: 0.30 元
结果等于 30 分(即 0.30 元)精确无误

完全以“分”为单位,所有金额都变成整数,完全规避了浮点运算带来的舍入误差,但是对应的数值就变得特别大,就可能会导致超出数据类型的取值范围。

现实场景更复杂得多,可能会有大额交易,尤其是在高频交易、跨境结算、资产估值、基金净值计算等场景中,金额可能高达数万甚至数亿元。 此时若仍以“分”为单位,int(最大约21亿)会迅速溢出,即使使用 long long(最大约9×10¹⁸),也很难应付。

在这种情况下,可以有如下的应对思路:

  • 如果只能用浮点数,则应该避免特别多次的累加操作,优先使用较少次数的公式计算;
  • 如果浮点数的数值出现特别小的尾端,可以在关键节点进行四舍五入到最小货币单位
  • 采用定点数库(如 IBM 的 decNumber)实现真正的十进制算术。

上面这些思路,此处就不演示了,感兴趣的同学们可以自行探索一下。 一定要注意,没有绝对正确的类型,只有最适合场景的权衡:小额、高频率、强一致性场景可以用整型;大额、低频、需复杂计算的场景可根据情况酌情处理并辅以校验机制。


二、浮点型(Floating-Point Types)——连续世界的近似

自然科学与工程计算中,有很多的物理量(如长度、时间、电压)常被视为连续变量。但实际上计算机无法真正表示“无限可分”的连续性,好在浮点数通过巧妙设计,在有限内存中提供了大范围、高动态、可计算的近似表示,极大便利了科学计算。 这个设计是有妥协的,代价就是前面的0.1+0.2≠0.3,而且这种不准确还可能随着累加而积累增大,变得难以控制等等。反正先粗略看看浮点数的设计吧。

浮点数是如何设计的?

浮点数遵循 IEEE 754 标准,其核心思想源自科学计数法
一个实数可表示为:
$$ x = \text{sign} \times \text{mantissa} \times 2^{\text{exponent}} $$

在32位 float 中: - 1位:符号位(0正1负) - 8位:指数(Exponent),偏移127 - 23位:尾数(Mantissa),隐含前导1(即实际精度24位)

例如,十进制数 9.5 的二进制为 1001.1,规格化为 1.0011 × 2³: - 符号位:0(正) - 指数:3 + 127 = 130 → 二进制 10000010 - 尾数:0011000...0(23位)

这种设计使得浮点数能表示从极小(如 \(10^{-38}\))到极大(如 \(10^{38}\))的数,但牺牲了精确性——许多十进制小数(如0.1)在二进制中是无限循环小数,只能被近似存储。

示例 2:展示浮点数的不精确性与正确比较方式

#include <stdio.h>
#include <math.h>

int main() {
    double a = 0.1f;
    double b = 0.2f;
    double sum = a + b;
    double target = 0.3;

    // 展示浮点数的实际存储值(体现不精确性)
    printf("0.1f = %.17f\n", a);
    printf("0.2f = %.17f\n", b);
    printf("sum  = %.17f\n", sum);
    printf("0.3f = %.17f\n", target);

    // 正确做法:使用容差(epsilon)进行近似比较
    #define EPSILON 1e-6f
    if (fabsf(sum - target) < EPSILON) {
        printf("容差比较(|%.17f - %.17f| < %g):近似相等\n", sum, target, EPSILON);
    } else {
        printf("容差比较:不近似相等\n");
    }
    return 0;
}

上面的代码运行结果如下:

0.1f = 0.10000000149011612
0.2f = 0.20000000298023224
sum  = 0.30000000447034836
0.3f = 0.29999999999999999
容差比较(|0.30000000447034836 - 0.29999999999999999| < 1e-06):近似相等

浮点数不能精确表示,因此,千万千万要注意,永远不要用 == 直接比较浮点数,而应使用一个微小的容差(epsilon)进行判断。

适用场景:物理仿真、信号处理、机器学习等允许微小误差且需要宽动态范围的领域。


三、无符号整型与字符型——离散状态的编码

不记得自然数里面有没有0了,我记得我小时候最开始学数学的时候,小学的教师都说自然数没有0,到了中学又都说有0。 此刻咱们也不管他们数学教材的概念什么的是不是变来变去的,只说现实中大家能直观感知到的数值,肯定是有0的。 从0开始,1,2,3这样的,与其使用整型当中的正数部分,就不如直接使用无符号整数unsigned,能有更大的取值范围。 unsigned 类型常用于表示非负计数、状态码、像素值等离散信息,天然契合“一份一份”的我们感知到的朴素世界。 另外,文本字符char本质上也是无符号的证书,毕竟字母也是逐个的,没有负的字母。

示例 3:表示颜色通道与交易ID

#include <stdio.h>

int main() {
    unsigned char pixel_red = 255;           // RGB红色通道,取值 0~255
    unsigned int transaction_id = 4294967295U; // 交易流水号,永不为负

    printf("红色通道值: %u\n", pixel_red);
    printf("交易ID: %u\n", transaction_id);
    return 0;
}

上述代码运行结果如下:

红色通道值: 255
交易ID: 4294967295

unsigned char 常用于图像处理,每个像素通道就是 0~255 的离散整数;unsigned int 则适合表示永不为负的编号或计数器。


四、布尔与空类型——逻辑与通用性的抽象

bool 表示离散的真假状态;void 则是一种“无类型”占位符,用于函数返回或通用指针。

示例 4:布尔类型用于业务逻辑判断

#include <stdio.h>
#include <stdbool.h>

bool is_valid_transaction(int amount) {
    return amount > 0; // 金额必须为正(离散判断)
}

int main() {
    int tx = 100; // 1元
    if (is_valid_transaction(tx)) {
        printf("交易有效。\n");
    }
    return 0;
}

上面代码运行之后就会提示:

交易有效。

bool 类型使逻辑判断更清晰。注意需包含 <stdbool.h> 才能使用 true/false


五、跨平台类型检查:确保可移植性

由于不同平台对数据类型的实现不同,应使用 <limits.h><float.h> 查询实际范围:

示例 5:打印当前平台的数据类型信息

#include <stdio.h>
#include <limits.h>
#include <float.h>

int main() {
    printf("=== 平台数据类型信息 ===\n");
    printf("char:        %zu 字节, 范围 %d ~ %d\n", sizeof(char), CHAR_MIN, CHAR_MAX);
    printf("short:       %zu 字节, 范围 %hd ~ %hd\n", sizeof(short), SHRT_MIN, SHRT_MAX);
    printf("int:         %zu 字节, 范围 %d ~ %d\n", sizeof(int), INT_MIN, INT_MAX);
    printf("long:        %zu 字节, 范围 %ld ~ %ld\n", sizeof(long), LONG_MIN, LONG_MAX);
    printf("long long:   %zu 字节, 范围 %lld ~ %lld\n", sizeof(long long), LLONG_MIN, LLONG_MAX);
    printf("\nfloat 精度: %d 位有效数字, 范围 ~10^%d\n", FLT_DIG, FLT_MAX_10_EXP);
    printf("double 精度: %d 位有效数字, 范围 ~10^%d\n", DBL_DIG, DBL_MAX_10_EXP);
    printf("2^63 - 1 是 long long 最大值:%lld\n", LLONG_MAX);
    return 0;
}

这里运行上面的代码,得到的结果是:

=== 平台数据类型信息 ===
char:        1 字节, 范围 -128 ~ 127
short:       2 字节, 范围 -32768 ~ 32767
int:         4 字节, 范围 -2147483648 ~ 2147483647
long:        4 字节, 范围 -2147483648 ~ 2147483647
long long:   8 字节, 范围 -9223372036854775808 ~ 9223372036854775807
float 精度: 6 位有效数字, 范围 ~10^38
double 精度: 15 位有效数字, 范围 ~10^308
2^63 - 1  long long 最大值:9223372036854775807

若需处理超大整数(如密码学等场景),C 语言需借助 GMP 等第三方库;而 Python 内置支持开发者设置要允许最多多长的整数,更为方便一些。
日常开发中,C 的固定类型反而带来确定性、高性能和低内存开销,这正是其不可替代的价值。


六、大模型时代常见的其他精度

现在下载各种模型,经常能看到有FP16或者INT8和INT4等等字样,这些也都指的是数值精度。

16位浮点数(FP16)也被称为半精度,大语言模型中常用来表示模型参数和中间计算结果。 相比传统的32位单精度(FP32),FP16能将显存占用减半、提升计算速度,尤其适合在支持Tensor Core的现代GPU上加速训练和推理。 不过没有完美的数据类型,FP16可能因数值范围较小而出现溢出或下溢,因此常配合混合精度训练(如用FP32保存主权重、FP16做前向/反向计算)来兼顾效率与稳定性。

C语言标准本身没有直接定义FP16类型,其内置浮点类型主要是float(对应FP32)、double(FP64)和long double。 在CUDA编程中,通过头文件提供了__half或half类型,明确支持FP16,并且NVIDIA GPU的Tensor Core对FP16有专门优化。 除了FP16,大模型常用的精度还包括FP32(高精度训练基准)、BF16(指数位更宽、更适合大模型训练)、TF32(NVIDIA Ampere架构专用,平衡速度与范围)、INT8/INT4(用于推理阶段的量化压缩)以及新兴的FP8(进一步降低显存与计算开销)。


C 语言数据类型

类型名称 主要用途 Python 对应类型
char 存储单字符、小整数、字节流 str[0](本质是 Unicode 码点)
unsigned char 图像像素、状态码、二进制数据 int(但无符号语义)
short / signed short 节省内存的小整数 int
unsigned short 端口号、计数器 int
int / signed int 通用整数、循环变量、数组索引 int(任意精度)
unsigned int 交易 ID、哈希值、非负计数 int
long 长整数(平台依赖强) int
unsigned long 时间戳(如 time_t 在部分系统) int
long long 超大整数(如文件偏移) int
unsigned long long 极大 ID、位掩码 int
_Bool / bool 布尔逻辑判断 bool
float 单精度浮点 float
double 双精度浮点 float
bfloat16(BF16) 半精度AI 训练(Google TPU/NVIDIA Ampere+)
int8_t / uint8_t 固定位宽整数(跨平台) int
int16_t / uint16_t 音频采样、传感器数据 int
int32_t / uint32_t 网络协议、文件格式 int
int64_t / uint64_t 时间戳(Unix ns)、大 ID int
分类: C