数据类型 (Data Types)

开篇故事

想象你面前有一盒画笔,有粗毛笔、细勾线笔、橡皮和尺子。你不会用毛笔写字,也不会用尺子画画——每种工具都有自己最适合的工作。选错了工具,不仅效率低,还可能搞砸整幅画。

C 语言的数据类型就是这样的工具箱。int 是整数专用的勾线笔,double 是处理小数的细毛笔,char 是处理单个字符的橡皮。每种类型规定了数据占多少字节、能表示多大的范围、能存多精确。int8_t 只占 1 字节,适合存开关状态;int64_t 有 8 字节,能装下几十亿的大数。如果你用 int8_t 去存 256,就像拿橡皮去刻石头——不是不行,是根本不对路。

在 C 语言中,选择正确的数据类型是理解计算机如何管理内存的第一步。每种类型都有确定的字节数、范围和精度。掌握了这些,你就掌握了 C 语言的核心。

工具选对了,画画就顺手了。——民间谚语

本章适合谁

  • 刚学完"变量与表达式",想知道 C 语言有哪些数据类型
  • intfloatchar 只停留在表面认识,想深入理解它们的区别
  • 听说过 int32_tuint64_t 但不知道为什么需要它们
  • 想知道 sizeof 运算符和类型修饰符 const 的用法
  • 希望理解不同平台上类型大小可能不同的问题

你会学到什么

  • int8_t / int16_t / int32_t / int64_t ——精确宽度的整数类型
  • floatdouble ——浮点数的精度差异
  • charsigned charunsigned char ——字符和单字节整数
  • sizeof 运算符 ——查询类型或变量的字节大小
  • INT_MAX / FLT_MAX 等极限常量 ——了解每种类型的取值范围
  • signed / unsigned 修饰符 ——正负范围的切换
  • const 类型修饰符 ——定义只读常量

前置要求

  • 已完成 变量与表达式 章节
  • 已配置 C 编译环境(gccclang
  • 了解基本的 printf 用法

💡 小知识:本教程代码符合 C17 标准(-std=c17),使用 <stdint.h> 中的精确宽度类型。

第一个例子

这是本章最简短的例子——看看 sizeof 能告诉你什么:

#include <stdio.h>
#include <stdint.h>

int main(void) {
    printf("char    = %zu 字节\n", sizeof(char));
    printf("int     = %zu 字节\n", sizeof(int));
    printf("int64_t = %zu 字节\n", sizeof(int64_t));
    printf("float   = %zu 字节\n", sizeof(float));
    printf("double  = %zu 字节\n", sizeof(double));
    return 0;
}

完整源码在仓库 src/basic/datatype.c

原理解析

1. 整数类型:从 intint64_t

C 语言提供多种整数类型。传统的 intshortlong 在不同平台上的大小可能不同——这在 cross-platform 编程中是个大坑。

从 C99 开始,<stdint.h> 提供了一组精确宽度类型,保证在所有平台上大小一致:

类型宽度取值范围用途
int8_t8 位-128 ~ 127状态标志、紧凑数据
int16_t16 位-32,768 ~ 32,767端口号
int32_t32 位±21 亿最常用的整数
int64_t64 位±9.2×10¹⁸大文件偏移量、时间戳
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>

int main(void) {
    int8_t  status   = -1;
    int32_t count    = 42;
    int64_t filesize = 9223372036854775807LL;

    // 使用 PRId8、PRId32、PRId64 格式化宏(来自 <inttypes.h>)
    printf("status:   %" PRId8 "\n", status);
    printf("count:    %" PRId32 "\n", count);
    printf("filesize: %" PRId64 "\n", filesize);
    return 0;
}

💡 小知识PRId8 是一个格式化宏,在不同平台上会被展开为 "d" 或其他合适的格式。它确保 printf 能正确读取 int8_t 类型。

2. 浮点类型:float vs double

浮点数用于表示小数。C 语言提供两种基本浮点类型:

float  pi_float  = 3.14159f;       // 4 字节,单精度
double pi_double = 3.14159265358979; // 8 字节,双精度
  • float:大约 7 位有效数字
  • double:大约 15 位有效数字

我的经验:除非在嵌入式系统中内存非常紧张,否则我默认使用 double。精度不足的代价可能远超节省的 4 字节。

3. 字符类型:不只是字符

char 在 C 语言中是一个字节,但它既可以是字符,也可以是小整数:

char        c  = 'A';      // 字符,ASCII 值 65
signed char   sc = -42;     // 有符号,范围 -128 ~ 127
unsigned char uc = 200;     // 无符号,范围 0 ~ 255

重要char 到底是有符号还是无符号,取决于编译器。如果你需要明确的符号行为,请始终使用 signed charunsigned char

4. sizeof 运算符

sizeof 是 C 语言的内置运算符(不是函数!),返回类型或变量的字节数。

#include <stdio.h>

int main(void) {
    int x = 42;
    printf("int 类型大小: %zu 字节\n", sizeof(int));   // 类型
    printf("变量 x 大小:  %zu 字节\n", sizeof(x));      // 变量
    printf("表达式大小:   %zu 字节\n", sizeof(x + 1));   // 表达式
    return 0;
}

🧪 动手试试:试试 sizeof(double)sizeof(double) 是否相等。试试 sizeof(3.14) 的结果是多少?(提示:3.14 默认是 double 类型)

5. 类型极限(Limits)

C 标准库在 <limits.h><float.h> 中定义了每种类型的最大值和最小值:

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

int main(void) {
    printf("INT_MAX  = %d\n", INT_MAX);     // 2147483647
    printf("INT_MIN  = %+d\n", INT_MIN);    // -2147483648
    printf("UINT_MAX = %u\n", UINT_MAX);    // 4294967295
    printf("FLT_MAX  = %.2e\n", FLT_MAX);   // 3.40e+38
    printf("DBL_MAX  = %.2e\n", DBL_MAX);   // 1.79e+308
    printf("CHAR_BIT = %d\n", CHAR_BIT);    // 8(一个 char 有多少位)
    return 0;
}

我的建议:在写数值相关的代码时,养成查极限常量的习惯。它可以帮你避免溢出错误。

6. signed 与 unsigned

类型前面的 signedunsigned 修饰符决定了该类型能否表示负数。

signed int   si = -10;   // 可以表示负数
unsigned int ui = 10;    // 只能表示 0 及正数

// unsigned 的范围更大(正数多一倍)
// signed int:   -2147483648 ~ 2147483647
// unsigned int: 0           ~ 4294967295

⚠️ 危险:将 signedunsigned 混用进行算术运算或比较时,C 会自动将 signed 转为 unsigned-1 会变成一个巨大的正数!

#include <stdio.h>

int main(void) {
    signed int   a = -1;
    unsigned int b = 2;
    if (a > b) {
        printf("-1 > 2 ??\n");  // 会打印!因为 -1 转为 unsigned 后是一个巨大的数
    }
    return 0;
}

7. const 类型修饰符

const 告诉编译器:这个变量的值在初始化后不应该被修改。

const int   MAX_RETRIES = 3;
const float GRAVITY     = 9.81f;
const char *GREETING    = "Hello, C!";

我的理解:const 不仅仅是编译时的安全检查——它也向阅读代码的人传达意图。看到 const,我就知道"这个值不应该被改变"。

常见错误

❌ 错误 1:赋值超出类型范围

#include <stdint.h>

int8_t temperature = 200;  // ❌ int8_t 最大值是 127!

编译器警告(使用 -Wall -Wextra):

warning: implicit conversion from 'int' to 'int8_t' changes value from 200 to -56

✅ 修复:使用更大类型

int16_t temperature = 200;  // ✅ int16_t 范围 -32768 ~ 32767

❌ 错误 2:修改 const 变量

const int MAX = 100;
MAX = 200;  // ❌ error: assignment of read-only variable 'MAX'

✅ 修复:如果值需要改变,去掉 const

int max = 100;  // ✅ 可以修改
max = 200;

❌ 错误 3:signed/unsigned 比较陷阱

#include <stdio.h>

int main(void) {
    int array_length = -1;      // -1
    unsigned int size = 5;      // 5
    if (array_length < size) {  // ❌ -1 被当作 unsigned,变成一个很大的数
        printf("-1 < 5\n");     // 这行不会执行!
    }
    return 0;
}

✅ 修复:确保比较的两个变量类型一致

int array_length = -1;
int size = 5;
if (array_length < size) {      // ✅ 都是 signed int
    printf("-1 < 5\n");
}

动手练习

🟢 练习 1:sizeof 计算器

编写一段代码,依次打印 charshortintlonglong longfloatdoublelong double 的字节大小,并找出哪个是当前平台上最大的基本类型。

查看答案
#include <stdio.h>

int main(void) {
    printf("char        = %zu 字节\n", sizeof(char));
    printf("short       = %zu 字节\n", sizeof(short));
    printf("int         = %zu 字节\n", sizeof(int));
    printf("long        = %zu 字节\n", sizeof(long));
    printf("long long   = %zu 字节\n", sizeof(long long));
    printf("float       = %zu 字节\n", sizeof(float));
    printf("double      = %zu 字节\n", sizeof(double));
    printf("long double = %zu 字节\n", sizeof(long double));
    return 0;
}

在 macOS 上,long double 通常是 16 字节,是最大的基本类型。

🟡 练习 2:类型安全计数器

编写一个函数 void safe_increment(uint8_t *counter),接受一个 uint8_t 指针作为计数器。如果计数器没有达到最大值(255),则加 1;否则打印 "overflow" 并保持值不变。在主函数中演示从 253 加到 256 的过程(注意 256 无法用 uint8_t 表示)。

查看答案
#include <stdio.h>
#include <stdint.h>

void safe_increment(uint8_t *counter) {
    if (*counter == UINT8_MAX) {
        printf("overflow! 计数器已达最大值 %" PRIu8 "\n", *counter);
    } else {
        (*counter)++;
    }
}

int main(void) {
    uint8_t counter = 253;
    for (int i = 0; i < 4; i++) {
        printf("当前值: %" PRIu8 "  → ", counter);
        safe_increment(&counter);
        printf("结果: %" PRIu8 "\n", counter);
    }
    return 0;
}

输出:

当前值: 253  → 结果: 254
当前值: 254  → 结果: 255
当前值: 255  → overflow! 计数器已达最大值 255
当前值: 255  → overflow! 计数器已达最大值 255

🔴 练习 3:自定义类型信息结构体

定义一个结构体 TypeInfo { const char *name; size_t bytes; long long min_val; unsigned long long max_val; },编写函数 print_type_info(TypeInfo info) 来格式化打印类型信息。然后为 int8_tint16_tint32_tint64_tuint8_t 创建 TypeInfo 实例并打印。

查看答案
#include <stdio.h>
#include <stdint.h>
#include <limits.h>

typedef struct {
    const char       *name;
    size_t           bytes;
    long long        min_val;
    unsigned long long max_val;
} TypeInfo;

void print_type_info(TypeInfo info) {
    printf("  %-10s | %zu 字节 | min: %20lld | max: %20llu\n",
           info.name, info.bytes, info.min_val, info.max_val);
}

int main(void) {
    TypeInfo types[] = {
        {"int8_t",   sizeof(int8_t),   SCHAR_MIN,     UCHAR_MAX},
        {"int16_t",  sizeof(int16_t),  SHRT_MIN,      USHRT_MAX},
        {"int32_t",  sizeof(int32_t),  INT_MIN,       (unsigned)INT_MAX},
        {"int64_t",  sizeof(int64_t),  LLONG_MIN,     ULLONG_MAX},
        {"uint8_t",  sizeof(uint8_t),  0,             UCHAR_MAX},
    };

    int count = sizeof(types) / sizeof(types[0]);
    printf("  类型       | 字节数 | 最小值               | 最大值               \n");
    printf("  -----------|--------|----------------------|----------------------\n");

    for (int i = 0; i < count; i++) {
        print_type_info(types[i]);
    }
    return 0;
}

这个练习综合了结构体、sizeof、极限常量、格式化输出等多个知识点。

故障排查 (FAQ)

Q: 为什么 int 在我的电脑上是 4 字节,但在别人的电脑上是 2 字节?

A: C 标准只规定了 shortintlonglong long 的相对大小,不规定绝对值。使用 <stdint.h> 中的 int32_t 等精确宽度类型可以避免这个问题。

Q: sizeof 返回的是什么类型?为什么我用 %d 打印会有警告?

A: sizeof 返回 size_t 类型(通常等同于 unsigned longunsigned long long)。应该用 %zu 格式打印:printf("%zu\n", sizeof(int))

Q: floatdouble 在什么情况下应该选择 float

A: 三种情况:(1) 大量数据存储时节省内存;(2) GPU 编程中 float 通常比 double 快;(3) 嵌入式系统内存受限。其他情况,用 double

Q: char 到底是有符号还是无符号?

A: C 标准没有规定,取决于你的平台。x86 上通常是 signed,ARM 上通常是 unsigned。如果需要明确的行为,始终用 signed charunsigned char

知识扩展 (选学)

类型别名与 typedef

你可以用 typedef 为已有类型创建别名,这在项目中非常常见:

typedef uint32_t pixel_t;    // 定义一个表示像素的类型
typedef uint8_t  byte_t;     // byte_t 就是 uint8_t 的别名

pixel_t color = 0xFF0000;    // 比 uint32_t 更具语义

浮点数精度问题

floatdouble 使用 IEEE 754 标准表示,不能精确表示所有小数

double x = 0.1 + 0.2;
printf("%.20f\n", x);  // 输出: 0.30000000000000004000,不是 0.3!

我的建议:比较浮点数时,不要直接用 ==,而是用误差范围

#include <math.h>
if (fabs(a - b) < 1e-9) {
    // a 和 b 在误差范围内相等
}

_Bool 类型(C99 起)

C99 引入了 _Bool 类型,<stdbool.h> 提供了更友好的别名 bool

#include <stdbool.h>

bool is_ready = true;
if (is_ready) {
    printf("Ready!\n");
}

小结

本章介绍了 C 语言的核心数据类型体系:

类别关键类型要点
整数int8_tint64_t使用精确宽度类型,避免平台差异
浮点float / double注意精度,比较时用误差范围
字符char / signed char / unsigned char注意 char 符号性取决于平台
运算符sizeof返回 size_t,用 %zu 打印
极限值INT_MAX / FLT_MAX写数值代码前查极限常量
修饰符signed / unsigned / const明确符号性,用 const 表达意图

关键术语

  • 精确宽度类型intN_t,保证在所有平台上大小一致
  • 溢出:数值超出类型范围,导致不可预期的行为
  • 格式化宏PRIdN 系列,用于正确打印精确宽度类型
  • IEEE 754:浮点数的标准表示方式

术语表

英文中文说明
data type数据类型值的类型和内存占用
integer type整数类型表示整数的类型
floating-point type浮点类型表示小数的类型
signed / unsigned有符号 / 无符号能否表示负数
precision精度浮点数的有效数字位数
overflow溢出数值超出类型可表示范围
type modifier类型修饰符修饰基础类型的关键词(const, signed 等)
format specifier格式说明符printf 中描述类型的占位符
format macro格式化宏PRId8 等用于精确宽度类型的宏
byte字节最小可寻址内存单元(通常 8 位)
size_tsize_tsizeof 的返回类型,无符号整数
IEEE 754IEEE 754浮点数表示的国际标准

延伸阅读

继续学习


本章代码位于仓库 src/basic/datatype.csrc/basic/datatype_sample.c。 运行 make build && make run 查看完整演示输出。