你今天把数据类型彻底搞明白了,以后写代码就会知道:什么时候用int,什么时候用float,什么时候该用long long防止溢出,什么时候用double保证精度。这些不是死记硬背的,理解了自然就会选。
一、数据类型到底在干嘛?
咱们先不说有哪些类型,先搞清楚数据类型是干啥用的。
计算机内存里存的全是0和1,一串二进制数字。你光看这些0和1,根本不知道它代表什么——它可能是一个整数,可能是一个浮点数,可能是一个字符,也可能是一条机器指令。数据类型就是告诉编译器"这一串0和1应该被解释成什么"。
另外,不同类型占用不同大小的内存空间。比如在大多数系统上,int占4个字节,char占1个字节,double占8个字节。编译器知道这些信息之后,就知道该给你划多大一块地。
#include <stdio.h> int main() { int a = 65; // 二进制是 00000000 00000000 00000000 01000001 char b = 65; // 二进制是 01000001 float c = 65.0; // 二进制完全是另一套编码方式 // 同样的65,不同类型打印出来结果不同 printf("a: %d\n", a); // 输出 65 printf("b: %c\n", b); // 输出 'A'(ASCII码65对应A) printf("b: %d\n", b); // 输出 65 printf("c: %f\n", c); // 输出 65.000000 printf("大小:%lu %lu %lu\n", sizeof(a), sizeof(b), sizeof(c)); // 输出:4 1 4(float在这台机器上是4字节) return 0; }
看到没?a和b存的都是65,但因为类型不同,用%d和%c打印出来完全不一样。float更是连存储方式都跟整数不同。这就是数据类型的意义——同样的0和1,不同的解释方式,得出不同的结果。
二、基本数据类型全家桶
C语言的基本数据类型分这么几大类:整型、浮点型、字符型。咱们一个个拆开讲。
1. 整型家族——专门存整数
整型分好几种,主要区别在于"能存多大范围"和"占多少字节"。
| 类型 | 通常占字节 | 取值范围(32位系统) |
|---|---|---|
char |
1 | -128 ~ 127 |
unsigned char |
1 | 0 ~ 255 |
short |
2 | -32768 ~ 32767 |
unsigned short |
2 | 0 ~ 65535 |
int |
4 | -2147483648 ~ 2147483647 |
unsigned int |
4 | 0 ~ 4294967295 |
long |
4(Windows)/ 8(Linux 64位) | 视平台而定 |
long long |
8 | -9.2e18 ~ 9.2e18 |
unsigned long long |
8 | 0 ~ 1.8e19 |
注意:上面的字节数"通常"是这些,但C标准并没有强制规定。它只规定了short至少2个字节,int至少2个字节,long至少4个字节,long long至少8个字节。具体多大取决于编译器。但你在写代码的时候,如果不确定,可以用sizeof运算符看。
#include <stdio.h> int main() { printf("char: %lu\n", sizeof(char)); // 1 printf("short: %lu\n", sizeof(short)); // 2 printf("int: %lu\n", sizeof(int)); // 4 printf("long: %lu\n", sizeof(long)); // 8(Linux 64位)或4(Windows) printf("long long: %lu\n", sizeof(long long)); // 8 // 有符号和无符号的区别 int a = -10; unsigned int b = 10; // unsigned int c = -10; // 语法上可以,但会变成很大的正数 // 溢出问题 int max = 2147483647; printf("%d\n", max); // 2147483647 printf("%d\n", max + 1); // -2147483648,溢出了,变负数 unsigned int u_max = 4294967295U; printf("%u\n", u_max); // 4294967295 printf("%u\n", u_max + 1); // 0,无符号溢出回到0 return 0; }
重点:整数溢出是个大坑。 你定义了一个int,往里面放的值超出了它的范围,它不会报错,而是"绕回来"变成另一个值。比如int最大值加1变成了最小值,unsigned int最大值加1变成了0。这种Bug极其隐蔽,代码跑着跑着结果突然变了。
小贴士: 如果你不确定你的数会不会超过范围,就用大一点的类型。long long能存到9亿亿,基本什么整数都够放了。但long long占8个字节,内存多一些,一般情况用int就够。
2. 浮点型家族——存小数
浮点型用来存带小数点的数,比如3.14、-0.5、2.71828。
| 类型 | 通常占字节 | 精度 | 取值范围(约) |
|---|---|---|---|
float |
4 | 6-7位有效数字 | ±3.4e-38 ~ ±3.4e38 |
double |
8 | 15-16位有效数字 | ±1.7e-308 ~ ±1.7e308 |
long double |
16(视平台) | 更高 | 视平台而定 |
#include <stdio.h> int main() { float f = 3.1415926535f; // float,精度有限 double d = 3.1415926535; // double,精度更高 printf("float: %.10f\n", f); // 3.1415927410,后面几位不准了 printf("double: %.10f\n", d); // 3.1415926535,准确 // 浮点数的精度问题 float a = 0.1f; float b = 0.2f; float c = a + b; printf("%.20f\n", c); // 0.30000001192092895508,不是精确的0.3 // 判断两个浮点数相等不要用 == if (c == 0.3f) { printf("相等\n"); // 这行可能不会执行 } else { printf("不相等\n"); // 通常会走这里 } // 正确做法:判断差值是否在容忍范围内 if (c - 0.3f < 0.000001 && c - 0.3f > -0.000001) { printf("近似相等\n"); } printf("大小:%lu %lu %lu\n", sizeof(float), sizeof(double), sizeof(long double)); return 0; }
这块有两个重点要记住:
第一,float精度有限,大概6到7位有效数字。如果你的数据需要高精度,用double。
第二,浮点数不能直接用==比较是否相等。因为浮点数在计算机里存的是近似值,0.1+0.2不一定是0.3。正确的做法是比较两个数的差值的绝对值是否小于一个很小的数(比如1e-6)。
3. 字符型——其实也是整数
char类型虽然叫"字符型",但它在C语言里本质上是整数。它存的是字符的ASCII码,所以你可以把char当整数来算。
#include <stdio.h> int main() { char ch = 'A'; printf("%c %d\n", ch, ch); // 输出 A 65 // 字符可以做算术运算 ch = ch + 1; printf("%c %d\n", ch, ch); // 输出 B 66 // 大小写转换 char lower = 'a'; char upper = lower - 32; // 'a' 97, 'A' 65,差32 printf("%c %c\n", lower, upper); // a A // 数字字符转成对应的数字 char digit = '5'; int num = digit - '0'; // '5' 53, '0' 48,差5 printf("%d\n", num); // 5 // signed char和unsigned char signed char sc = -128; unsigned char uc = 255; printf("%d %d\n", sc, uc); // -128 255 return 0; }
记住几个关键ASCII码:
-
'0' = 48,'1' = 49,…… '9' = 57
-
'A' = 65,'B' = 66,…… 'Z' = 90
-
'a' = 97,'b' = 98,…… 'z' = 122
-
大写字母 + 32 = 对应的小写字母(65+32=97)
-
'\0' = 0(空字符,字符串结束标志)
-
'\n' = 10(换行)
三、类型转换——不同类型混在一起会怎样?
你在写表达式的时候,经常会有不同类型的数据混在一起运算。比如int a = 5; double b = 2.5;然后算a + b。这时候编译器会怎么处理?
C语言的规则是:把低精度的类型往高精度的类型转换,保证精度不丢失。
转换的等级大致是:char/short → int → unsigned int → long → unsigned long → float → double → long double
#include <stdio.h> int main() { int a = 5; double b = 2.5; // 隐式转换:a被转成double,然后相加 double result = a + b; printf("%lf\n", result); // 7.500000 // 整数除法 vs 浮点数除法 int x = 5, y = 2; printf("%d\n", x / y); // 2(整数除法,截断小数) printf("%f\n", x / (double)y); // 2.500000(强制转成double) // 强制类型转换(显式转换) int m = 10, n = 3; float f = (float)m / n; // (float)把m转成float,除法变成浮点数除法 printf("%f\n", f); // 3.333333 // 转换陷阱:大数转小数会溢出 int big = 1000000; short small = (short)big; printf("%d\n", small); // 可能输出16960,数据被截断了 return 0; }
关键注意:
整数除法会截断小数部分,两个整数相除结果还是整数。如果你想要小数结果,至少把其中一个操作数转成浮点数。
强制类型转换(也叫"显式转换")就是你主动告诉编译器"把这个数当成XX类型来用"。写法是(类型)表达式。
把大类型转成小类型可能丢失数据,比如long long转成int,或者double转成float,精度或范围会损失。你要心里有数。
四、void类型——表示"什么都没有"
void在C语言里有两种用法:
1. 函数没有返回值
void print_hello() { printf("Hello\n"); // 没有return语句,或者 return; }
2. 没有类型的指针——通用指针
void*可以指向任何类型的数据,但不能直接解引用。
#include <stdio.h> int main() { int a = 10; double b = 3.14; char c = 'A'; void *p; // void指针,可以指向任何类型 p = &a; printf("%d\n", *(int*)p); // 必须转回int*才能解引用 p = &b; printf("%f\n", *(double*)p); p = &c; printf("%c\n", *(char*)p); // void指针不能直接做算术运算 // p++; // 错误,不知道p指向的类型,不知道移动多少字节 return 0; }
void*常见于malloc这类函数,它返回一个void*,你可以把它转成任何类型的指针。
int *p = (int*)malloc(sizeof(int) * 10); // malloc返回void*,转成int*
五、typedef——给类型起个别名
typedef给已有的类型起一个新名字,主要是为了方便阅读和管理。
#include <stdio.h> typedef int Age; // Age就是int typedef float Score; // Score就是float typedef struct Student { char name[20]; int id; } Student; // Student就是struct Student int main() { Age myAge = 25; // 就是int Score math = 95.5; // 就是float Student s1; // 不用写struct Student s1 printf("%d %.1f\n", myAge, math); return 0; }
typedef和#define的区别:
-
#define是文本替换,没有类型检查 -
typedef是真正的类型定义,有类型检查
#define INT_PTR int* INT_PTR a, b; // 展开成 int* a, b; 实际上a是int*,b是int typedef int* INT_PTR2; INT_PTR2 c, d; // c和d都是int*
六、sizeof运算符——问它"你占多大"
sizeof是一个运算符,不是函数。它告诉你某个类型或变量占多少个字节。
#include <stdio.h> int main() { int a; double b; printf("%lu\n", sizeof(int)); // 4 printf("%lu\n", sizeof(a)); // 4 printf("%lu\n", sizeof(b)); // 8 printf("%lu\n", sizeof(long)); // 8或4,看平台 printf("%lu\n", sizeof(10)); // 4,10是int常量 // sizeof数组 int arr[10]; printf("%lu\n", sizeof(arr)); // 40 = 10 * 4 printf("%lu\n", sizeof(arr) / sizeof(arr[0])); // 10,数组元素个数 return 0; }
常见用法:sizeof(arr) / sizeof(arr[0]) 可以计算数组里有多少个元素。
注意:sizeof在编译时计算,所以它后面跟的表达式不会实际执行。
int a = 10; int b = sizeof(a++); // sizeof只问"a占多大",不会执行a++ printf("%d\n", a); // 还是10,a没有被++过
七、几个必踩的坑
坑1:整数除法截断
int a = 5, b = 2; float c = a / b; // c = 2.0,不是2.5
解决:float c = (float)a / b;
坑2:char到底是signed还是unsigned
C标准没有规定char是有符号还是无符号,这取决于编译器。如果你需要明确的符号性,用signed char或unsigned char。
坑3:类型的大小不确定
int不一定是4个字节,long不一定是8个字节。写跨平台代码的时候不要假设大小,用sizeof来判断,或者用stdint.h里的固定宽度类型(比如int32_t、int64_t)。
#include <stdint.h> int32_t a; // 确定是32位有符号整数 uint64_t b; // 确定是64位无符号整数
坑4:浮点数直接用==比较
if (0.1 + 0.2 == 0.3) // 大概率是false
解决:比较差值是否小于一个很小的数。
坑5:类型转换时溢出
int a = 300; char b = (char)a; // 300超过了char的范围,数据被截断
坑6:sizeof用在数组参数上失效
void func(int arr[]) { printf("%lu\n", sizeof(arr)); // 输出指针的大小,不是数组大小 }
在函数参数里,数组会退化(降级)为指针,sizeof(arr)得到的是指针的大小。
最后说两句:
数据类型是C语言一切操作的基础。你选对了类型,程序就稳了;选错了类型,各种溢出、截断、精度丢失就会找上门。
练到什么程度算过关? 你知道每种基本类型占多大内存、取值范围是多少。能解释int和long long的区别,float和double的区别。知道整数除法会截断小数,浮点数不能直接用==比较。会主动使用sizeof来确认类型大小。写代码的时候能根据数据范围选择合适的类型。