2. 基本数据类型
好,咱们今天聊聊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() 做转换。
下面我用一张图来展示本章的知识体系:
这张图把本章的知识点串起来了。你看,整型、浮点型、字符型是三大基础类型,而类型转换、sizeof、内存存储则是围绕它们的重要概念。我个人建议你把这个图记在脑子里,写代码时随时对照。
总结一下:
- 整型要注意有无符号和溢出问题
- 浮点数永远不要用 == 比较
- char 本质上是一个字节的整型
- 类型转换要显式写出来,别依赖编译器
- sizeof 是运算符,不是函数
- 内存字节序搞不清楚,网络编程会出大问题