2. 基本数据类型:整型、浮点型、字符型、类型转换、sizeof运算符、数据在内存中的存储

好,咱们今天聊聊C语言里最基础的东西——基本数据类型。你别看它基础,我干了这么多年系统编程,踩过的坑有一半都跟数据类型有关。说白了,计算机不认识什么「整数」「小数」,它只认0和1。数据类型就是咱们告诉编译器:「嘿,这块内存我要这么用。」

2.1 整型:不只是整数那么简单

C语言里的整型家族挺庞大的。我刚开始学的时候也觉得不就是个整数嘛,搞这么多种类干嘛?后来在嵌入式项目里调一个定时器溢出bug,调了整整两天,才发现是用了 int 而不是 unsigned int——嗯,从那以后我再也不敢小看整型了。

类型 关键字 典型大小(32位系统) 取值范围
短整型 short 2字节 -32768 ~ 32767
整型 int 4字节 -2^31 ~ 2^31-1
长整型 long 4或8字节 取决于平台
长长整型 long long 8字节 -2^63 ~ 2^63-1

关键点:加上 unsigned 前缀,表示无符号整型。比如 unsigned int 的取值范围是 0 ~ 4294967295。我个人习惯:只要数值不可能为负,一律用 unsigned。这能避免很多边界问题。

#include <stdio.h>

int main() {
    int a = 10;
    unsigned int b = 20;
    short c = 5;
    long long d = 1234567890123LL;  // 注意LL后缀
    
    printf("a = %d, b = %u, c = %hd, d = %lld\n", a, b, c, d);
    return 0;
}

注意:整型溢出是C语言里最常见的bug之一。我曾经在写网络协议栈时,一个计数器用了 int,结果流量一大就变成负数了……你想想看,一个数据包序号变成负数,那整个协议栈都得崩。

2.2 浮点型:小数点的艺术

浮点数在内存里的存储方式跟整型完全不一样。它用的是科学计数法——只不过是用二进制表示的。我建议你记住一句话:浮点数是不精确的。这不是C语言的锅,是IEEE 754标准决定的。

类型 关键字 大小 有效位数
单精度 float 4字节 约7位
双精度 double 8字节 约15位
#include <stdio.h>

int main() {
    float f = 0.1f;      // 注意f后缀
    double d = 0.1;
    
    printf("float:  %.20f\n", f);
    printf("double: %.20lf\n", d);
    return 0;
}

你猜输出是什么?0.10000000149011611938 和 0.10000000000000000555。看到了吧?0.1在二进制里是无限循环小数,根本存不精确。我在做金融交易系统时,就因为用 float 存金额,导致对账差了0.0001元——虽然钱不多,但合规过不去啊。

经验之谈:比较两个浮点数是否相等,千万别用 ==。正确的做法是判断它们的差是否小于一个很小的数,比如 fabs(a - b) < 1e-6。

2.3 字符型:不只是字母

字符型 char 在C语言里其实就是一个字节的整型。它存储的是字符的ASCII码值。比如 'A' 就是65,'0' 就是48。

#include <stdio.h>

int main() {
    char ch = 'A';
    printf("字符: %c, ASCII码: %d\n", ch, ch);
    
    // 字符可以做算术运算
    char lower = ch + 32;  // 大写转小写
    printf("小写: %c\n", lower);
    
    return 0;
}

你想想看,既然 char 本质上是整型,那它就有 signed 和 unsigned 之分。标准并没有规定 char 默认是 signed 还是 unsigned——这取决于编译器实现。我曾经在ARM和x86之间移植代码时,就因为这个吃了亏。

避坑指南:如果你要存储0~255之间的数值,明确写成 unsigned char。如果你要处理ASCII字符,用 char 就行。别偷懒,写清楚点。

2.4 类型转换:隐式与显式

类型转换分两种:隐式转换和显式转换(也叫强制类型转换)。

隐式转换是编译器自动做的。比如:

int a = 10;
double b = a;  // int 自动转成 double

这里有个规则叫「整型提升」——小于 int 的类型在运算时会先转成 int。我记得有一次写一个字节拼接的代码,没注意这个规则,结果高位字节全丢了。

显式转换就是咱们用 (类型) 强制转换:

double pi = 3.14159;
int intPi = (int)pi;  // 截断小数部分,intPi = 3

核心原则:隐式转换虽然方便,但容易隐藏bug。我个人习惯:涉及不同类型运算时,一律显式转换。代码看起来啰嗦一点,但安全。

2.5 sizeof运算符:不是函数,是运算符

sizeof 是C语言里一个很特别的运算符。它返回一个类型或变量所占的字节数,类型是 size_t(通常是无符号整型)。

#include <stdio.h>

int main() {
    printf("int: %zu 字节\n", sizeof(int));
    printf("float: %zu 字节\n", sizeof(float));
    printf("double: %zu 字节\n", sizeof(double));
    printf("char: %zu 字节\n", sizeof(char));
    
    int arr[10];
    printf("数组大小: %zu 字节\n", sizeof(arr));
    printf("数组元素个数: %zu\n", sizeof(arr) / sizeof(arr[0]));
    
    return 0;
}

实用技巧:用 sizeof(arr) / sizeof(arr[0]) 计算数组长度,比硬编码数字安全得多。我写代码时从来不用魔数,全部用sizeof算。

2.6 数据在内存中的存储:大端与小端

终于到了最核心的部分。数据在内存里怎么存的?说白了就是字节序的问题。

  • 大端模式:高位字节存低地址,低位字节存高地址。就像咱们写数字一样,从左到右是高位到低位。
  • 小端模式:低位字节存低地址,高位字节存高地址。x86架构就是小端。

举个例子,整数 0x12345678 在内存中的存储:

地址 大端 小端
0x00 0x12 0x78
0x01 0x34 0x56
0x02 0x56 0x34
0x03 0x78 0x12

曾经踩过的坑:我在做网络协议解析时,网络字节序是大端,而x86是小端。直接拿 int 去读网络数据包,解析出来的端口号全是反的。后来老老实实用 ntohs() 和 htonl() 做转换。

下面我用一张图来展示本章的知识体系:

基本数据类型 整型 short / int / long / long long signed / unsigned 浮点型 float / double IEEE 754 标准 字符型 char / unsigned char ASCII 编码 类型转换 隐式 / 显式 sizeof运算符 计算字节数 内存存储 大端 / 小端 整型提升 自动类型提升规则

这张图把本章的知识点串起来了。你看,整型、浮点型、字符型是三大基础类型,而类型转换、sizeof、内存存储则是围绕它们的重要概念。我个人建议你把这个图记在脑子里,写代码时随时对照。

总结一下:

  • 整型要注意有无符号和溢出问题
  • 浮点数永远不要用 == 比较
  • char 本质上是一个字节的整型
  • 类型转换要显式写出来,别依赖编译器
  • sizeof 是运算符,不是函数
  • 内存字节序搞不清楚,网络编程会出大问题

好了,这一章的内容就到这儿。记住,数据类型是C语言的基石,地基打不牢,后面盖什么楼都得塌。下一章咱们聊聊运算符和表达式——嗯,到时候再细说。

可可精品资料库