【C语言-数据类型】C语言中的数据类型——变量到底是个什么"类型"的盒子

开发技术 作者: 精灵星空 SKY315.COM 更新于: 浏览: 255
码字发文不易,转载请注明出处【精灵星空 SKY315.COM】

你今天把数据类型彻底搞明白了,以后写代码就会知道:什么时候用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来确认类型大小。写代码的时候能根据数据范围选择合适的类型。