数据和C
掌握C的基本数据类型家族——int/char/float/double/类型大小/常变量/printf格式——为数据存储打下扎实根基
学习目标
- 能用自己的话解释 C 的数据类型为什么重要——类型决定了变量占多大内存、能存什么值、该怎么用 printf 输出
- 能正确声明 int/char/float/double 变量,使用修饰符 short/long/unsigned 控制大小和符号,并写出正确的 printf 格式串
- 能回答:
int x = 3.14;和float f = 3 / 2;结果是多少?为什么不是你以为的那样?
为什么 C 语言里要分这么多种"数字盒子"?
想象你在一个零件仓库里:螺丝钉要放在小格子里,螺母要放在中号抽屉,发动机要放在大货架上。如果所有零件都往同一个万能麻袋里塞——小螺丝淹没在角落里找不到、大发动机根本塞不进。
在 C 语言里,数据有自己的“规格”。char 的大小按定义是 1 个 C 字节,但一个字节有多少位由 CHAR_BIT 给出;short、int、long 和浮点类型的具体大小由实现选择,只需满足标准规定的范围与顺序。程序应查询目标实现,而不是把某台机器的常见结果当成语言规则。
如果所有数据都往同一种通用盒子里塞呢? 电脑就帮不了你检查错误——省下的空间白浪费、该存的小数精确度悄悄丢失、输出的时候格式还跟你想象的不一样。这一章就来认识 C 里这些「盒子的种类」,学会怎么挑、怎么装、怎么取出来看得清清楚楚。
C 的数据类型家族
数据变量和常量(Data Variables and Constants)
变量表示可在生命周期中取得不同值的对象;常量、枚举常量和字符串字面量等则受各自语言规则约束。类型决定值域和操作,存储期决定对象存在多久,const 限定符限制通过该左值修改对象,但 C 中的 const 对象并不自动成为所有语境都可用的整数常量表达式。
在 C 语言中,↡编程语言里给数据规定的类别,决定了一个数据占多大内存、能存什么范围的值、能对它做哪些运算。C 要求每个变量在使用前必须声明类型。 是程序最基本的规矩。任何一个变量在声明的那一刻就定死了类型——编译器根据类型来分配内存空间、校验运算是否合法、在 printf 输出时决定该怎么解释那段二进制数据。
类型在 C 里扮演三个角色:① 占用多少存储(sizeof 告诉你)、② 能表示什么范围与精度、③ 允许哪些运算并如何解释对象表示。不同类型不能因为碰巧大小相同就随意互相重解释。
基本数据类型一览——C 里有哪几种盒子
C 语言提供了以下 ↡语言自带的基础类型,不需要 include 任何头文件就能直接用,包括整型、浮点型、字符型。:
- int 家族:
short、int、long、long long——存整数,区别在于字节数和取值范围 - char 家族:
char——存字符或小整数,占 1 字节 - 浮点与复数家族:
float、double、long double及其复数对应类型——区别在于范围、精度和运算语义
下面的图展示一种常见实现的相对布局,用于建立直觉;具体结果必须由当前实现的 sizeof 与标准限制宏确认:
类型决定了三件事,在写程序时时刻都要记住:
- 内存大小:
sizeof(char)恒为 1,其他类型大小由实现给出,并满足标准规定的相对约束 - 取值范围:
UCHAR_MAX、INT_MIN、INT_MAX等宏给出当前实现的精确边界 - 运算行为:整数除法和浮点除法规则不同——
3 / 2得 1,3.0 / 2.0才得 1.5
整数和浮点数(Integer Versus Floating-Point Types)
整数类型表示离散值,整数除法向零截断;无符号运算按模数回绕,而有符号溢出是未定义行为。浮点类型表示有限精度的实数近似,范围、精度和舍入特征由实现提供并可通过 <float.h> 查询。两类参与同一表达式时会经过整数提升和通常算术转换。
整型的修饰符——控制最小范围和符号
C 语言提供了四个 ↡加在基本类型名前面的关键字,用来改变类型的大小范围或符号属性。例如 short int(缩小)、long int(拉长)、unsigned int(把负数区间挪到正数)。 用来调整整型的大小和符号:
| 修饰符 | 作用 | 示例 |
|---|---|---|
short | 范围不大于 int,大小可能相同 | short int s; 或 short s;(int 可省略) |
long | 范围不小于 int,大小可能相同 | long int l; 或 long l; |
long long | 范围不小于 long,至少 64 位值范围 | long long ll; |
unsigned | 去掉负数——原来给负数的范围全部加给正数 | unsigned int u; |
修饰符可以组合:unsigned short、unsigned long long 等都是合法的——你可以同时控制大小和符号。
下面这张图直观对比了不同位宽下 signed 和 unsigned 的取值范围:
在普通算术中可优先考虑 int;unsigned 适合模运算、位掩码或标准接口明确要求的无符号类型。仅因为业务值“不应为负”就改成无符号数,可能让减法和边界比较更难诊断。
可移植类型:stdint.h 与 inttypes.h
<Term def="stdint.h 提供按能力命名的整数类型和边界宏,包括精确宽度、最小宽度、最快类型、最大宽度类型以及可保存对象指针转换结果的整数类型。精确宽度类型只在实现确实提供时才定义。">可移植整数类型</Term> 让代码表达协议真正需要的能力。int32_t 若存在就恰好 32 位且无填充位;int_least32_t 至少 32 位;int_fast32_t 是满足下限的较快类型;intmax_t 是最宽有符号整数类型。
#include <inttypes.h>
#include <stdint.h>
#include <stdio.h>
int32_t packet_id = INT32_C(2026);
uintmax_t total = UINTMAX_C(9000000000);
printf("packet=%" PRId32 "\n", packet_id);
printf("total=%" PRIuMAX "\n", total);<inttypes.h> 的 PRI 宏把类型与 printf 转换说明绑定,避免猜测 int32_t 在某实现上是 int 还是 long。二进制协议还要显式处理字节序;固定宽度类型本身不决定网络字节顺序。
浮点类型——存小数靠它们
整数搞不定的任务交给 ↡能存带小数点的数字的类型,包括 float(单精度)、double(双精度)、long double(扩展精度)。浮点数用科学计数法在内存中近似存储——有舍入误差。。C 提供了三种精度档次:
float:至少 6 位十进制有效数字double:精度和范围至少不低于float,标准最低为 10 位十进制有效数字long double:精度和范围至少不低于double;具体表示由实现决定
浮点数有一个根本特性必须记住:它们是近似存储的。 就像十进制里 1/3 = 0.33333... 写不完,二进制里很多十进制小数也写不完——0.1 在二进制里就是一个无限循环小数。所以浮点运算会有舍入误差。下面这段代码展示了 float 和 double 在精度上的实际差别:
#include <stdio.h>
int main(void) {
float f = 1.0f / 3.0f; // float 除法
double d = 1.0 / 3.0; // double 除法
printf("float: %.20f\n", f); // float 先提升为 double,再格式化
printf("double: %.20f\n", d); // 尾部数字展示二进制近似误差
// float: 0.33333334326744079590
// double: 0.33333333333333331483
return 0;
}复数与虚数类型
C99 的 <complex.h> 提供 float _Complex、double _Complex 和 long double _Complex。宏 I 表示虚数单位,可用 creal、cimag、cabs 等函数读取分量和模。实现是否定义纯虚数类型属于可选能力,但复数类型是标准语言与库的一部分。
#include <complex.h>
#include <stdio.h>
double complex z = 3.0 + 4.0 * I;
printf("real=%.1f imag=%.1f abs=%.1f\n",
creal(z), cimag(z), cabs(z));复数不是两个随意拼接的 double 字段;应通过标准运算与 <complex.h> 函数保持类型语义。编译命令在某些 Unix 工具链上使用数学函数时可能还需链接数学库,例如 cc file.c -lm。
字符类型与转义序列
↡大小按定义为一个 C 字节的整数类型,可表示基本执行字符集中的字符。普通 char 的有符号性由实现决定,且一个 C 字节由 CHAR_BIT 个比特组成。 可保存基本执行字符集中的字符。字符常量具有对应执行字符集的整数值;许多平台采用 ↡七位字符编码标准,定义 0 到 127 的字母、数字、标点和控制码。许多 C 实现使用 ASCII 兼容执行字符集,但 C 标准也允许其他执行字符集。,但 C 语言本身不强制执行字符集必须是 ASCII。
char 属于整数类型。'A' 的数值由执行字符集决定;在 ASCII 兼容实现中是 65。用 %c 按字符输出,用 %d 查看提升后的整数值:
#include <stdio.h>
int main(void) {
char ch = 'A';
printf("字符: %c\n", ch); // 输出 A
printf("编码值: %d\n", ch); // ASCII 兼容实现通常输出 65
return 0;
}有些字符没法直接敲出来——比如换行、制表、双引号本身。这时就需要用到 ↡以反斜杠 \\ 开头、后跟一个或几个字符的组合,表示一个特殊含义。如 \\n 表示换行、\\t 表示制表。反斜杠把普通字母「转」成了控制功能,故称转义。:
转义序列在日常编程中频繁出现——printf("Hello\nWorld"); 会在 "Hello" 和 "World" 之间换行。printf("He said \"Hi\""); 才能在双引号里再放双引号。
常量——"不变的值"怎么写
程序中经常需要表达固定值。<Term def="在翻译或执行中表示固定值的语言实体,包括字面量、枚举常量和宏替换产生的常量表达式。const 声明的是不可通过该左值修改的对象,不等同于所有语境中的整数常量表达式。">常量(constant)</Term> 与只读对象要按具体语言规则区分:
| 方式 | 写法 | 特点 |
|---|---|---|
#define | #define PI 3.14159 | 宏替换列表,不创建名为 PI 的对象 |
const | const int weeks = 7; | 有类型的只读对象;在 C 中不自动成为整数常量表达式 |
enum | enum { RED, GREEN, BLUE }; | 枚举常量——给一组相关的整数常量起名字,默认从 0 开始递增 |
#define 是 C 语言的传统做法——简单粗暴,但没有类型检查,容易出问题。const 是更现代、更安全的方式——编译器知道它的类型,能帮你检查错误。
#include <stdio.h>
#define PI 3.14159 // 宏常量:纯文本替换,没有类型
const int days = 7; // 有类型的只读对象
int main(void) {
// days = 8; // 约束违反:不能通过 const 左值修改
enum { SUN, MON, TUE, WED, THU, FRI, SAT };
// SUN=0, MON=1, TUE=2, ..., SAT=6
printf("圆周率: %f\n", PI); // PI 被替换成 3.14159
printf("一周有 %d 天\n", days);
printf("周三的枚举值是 %d\n", WED); // 输出 3
return 0;
}printf() 格式占位符——输出时告诉电脑"怎么看这段数据"
↡C 标准库中最常用的输出函数,通过格式占位符来决定如何将后续参数的值转换成字符输出。%d 输出整数、%f 输出浮点数、%c 输出字符。 的格式占位符跟你选择的类型是一一绑定的——用错占位符,输出不是你想要的结果。下面是最常用的几种:
| 类型 | printf 占位符 | 示例 |
|---|---|---|
int | %d(有符号)、%u(无符号) | printf("%d", 42); → 42 |
short | %hd | printf("%hd", s); |
long | %ld | printf("%ld", l); |
long long | %lld | printf("%lld", ll); |
char | %c(字符)、%d(整数) | printf("%c", 'A'); → A |
float | %f | printf("%f", 3.14f); → 3.140000 |
double | %f(或 %lf,但 printf 中等价) | printf("%f", 3.14); → 3.140000 |
| 指针/地址 | %p | printf("%p", (void *)&x); |
输出何时出现:缓冲与刷新
标准输出可能是无缓冲、行缓冲或全缓冲。交互终端上的 stdout 常见为行缓冲,写入换行时通常会提交当前行;重定向到文件后常变成全缓冲。标准并不保证每次 printf 都立即出现在屏幕上。
printf("enter value: ");
fflush(stdout); /* 提示没有换行,读取输入前显式提交 */fflush(stdout) 请求把待输出字符交给宿主环境。不要对输入流调用 fflush(stdin) 来“清空输入”,标准 C 对这种用法没有定义相应清理语义;输入验证应读取并丢弃到行尾。
动手:认识不同数据类型的"个头"差异
下面用一个 Stepper 分步演示 char / short / int / double 四种类型在内存中占用的空间,以及 sizeof 运算符如何帮你确认当前平台上每种类型真实的大小。
猜一猜:
sizeof(int)、sizeof(double)和CHAR_BIT在你的实现上分别是多少?先记录猜测,再让程序输出真实结果。
① char——1 个 C 字节
sizeof(char) 按定义为 1,它是 C 对对象大小计量的基本单位。一个字节含 CHAR_BIT 位且至少 8 位;普通 char 的有符号性与精确范围由实现决定,也不要求执行字符集必须是 ASCII。
代码速查:类型声明与 printf 的写法
下面是在你需要时快速对照的 C 类型声明与输出代码:
#include <stdio.h>
int main(void) {
// ===== 整型声明 =====
short s = 32767; // short 2 字节
int i = 42; // 普通整数;大小查询当前实现
long l = 123456789L; // long,后缀 L
unsigned int u = 4000000000U; // unsigned,后缀 U
// ===== 浮点声明 =====
float f = 3.14f; // float,必须后缀 f!
double d = 3.14159265358979; // double(默认)
// ===== 字符声明 =====
char ch = 'A'; // char
char code = 65; // 等价于 'A'
// ===== printf 输出 =====
printf("int: %d\n", i); // 42
printf("float: %f\n", f); // 3.140000
printf("double: %.15f\n", d); // 请求小数点后 15 位,不等于都精确
printf("char: %c = %d\n", ch, ch);// A = 65
return 0;
}无后缀浮点字面量类型是 double,f 后缀产生 float 字面量。赋给 float 时允许转换,但可能发生舍入;需要让表达式按 float 运算时应明确使用 f 后缀。
容易踩的坑
小结
- C 语言要求每个变量在声明时必须指定类型——类型决定了内存大小、取值范围和 printf 的格式占位符
- 基本类型分三类:整型(short/int/long/long long + unsigned 修饰)、浮点型(float/double/long double)、字符型(char)
- 整型修饰符规定范围顺序但不固定字节数;需要协议宽度时使用实现提供的
stdint.h类型 printf格式必须与提升后的实参类型匹配;启用格式警告可捕获很多字面量格式错误- 用
sizeof、CHAR_BIT、limits.h与float.h查询当前目标,不从平台名称猜类型模型
练习
问题 1(改代码型) 下面程序有五处错误或隐患。找出并解释,再写出修正版。
#include <float.h>
#include <limits.h>
#include <stdio.h>
int main(void) {
float pi = 3.14159265358979;
int total = pi; // ①
unsigned int count = -1; // ②
printf("pi = %d\n", pi); // ③
printf("count = %d\n", count); // ④
printf("total = %f\n", total); // ⑤
return 0;
}问题 2(问答型) 读下面程序,写出每行输出并解释,其中无符号最大值用标准宏表达。
#include <stdio.h>
int main(void) {
int a = 5, b = 2;
float c = a / b; // ①
float d = (float)a / b; // ②
int e = 2.9; // ③
printf("c = %f\n", c); // ④ 输出?
printf("d = %f\n", d); // ⑤ 输出?
printf("e = %d\n", e); // ⑥ 输出?
unsigned int u = 0;
u = u - 1;
printf("u = %u\n", u); // ⑦ 输出?
return 0;
}问题 3(综合型) 输出六种类型的 sizeof、CHAR_BIT、INT_MIN/INT_MAX 与 FLT_DIG/DBL_DIG,再比较字符及其执行字符集编码值。
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 类型(type)
数据的一张"规格标签"。它告诉编译器:这个变量占几字节内存、能存什么范围的值、能用什么格式输出。C 是静态类型语言——类型在编译时就确定了,此后不能变。详见本章「C 的数据类型家族」一节。
- 基本数据类型(basic data type)
C 语言内置的最基础的数据种类,不需要 #include 任何头文件就能直接用。包括整型家族(short/int/long/long long)、浮点家族(float/double/long double)、字符型(char)。详见本章「基本数据类型一览」一节和 CTypeSizeDiagram 图示。
- 修饰符(modifier)
加在整型名字前面的关键字,用来调整类型的大小和符号。short 缩小范围却省内存,long 拉大范围但占用更多空间,unsigned 去掉负数、把原来给负数的区间全部加到正数那边。修饰符可以组合使用,如 unsigned long long。详见本章「整型的修饰符」一节和 IntegerRangeDiagram 图示。
- 可移植整数类型
<stdint.h>提供精确宽度、最小宽度、最快和最大宽度整数类型;精确宽度类型只在实现支持时定义。<inttypes.h>的PRI宏用于匹配格式化输出。- 浮点类型(floating-point type)
表示有限精度实数近似的类型家族。标准规定最低范围和精度顺序,当前实现的有效数字与指数范围由
<float.h>宏给出。- char
大小按定义为一个 C 字节的整数类型。一个字节含
CHAR_BIT位,普通char的有符号性与执行字符集由实现决定。- ASCII 码
七位字符编码标准,定义 0 到 127 的字母、数字、标点与控制码。许多 C 实现兼容 ASCII,但 C 标准允许其他执行字符集。
- 转义序列(escape sequence)
以反斜杠 \ 开头、后跟一个或几个字符的组合,用来表示没法直接敲出来的特殊字符。\n 表示换行(把光标移到下一行开头),\t 表示制表(跳格对齐),\ 表示反斜杠本身," 表示双引号。反斜杠就是「转义符」——它把后面那个普通字母的意思给转了。详见本章「字符类型与转义序列」一节和 EscapeSequenceDiagram 图示。
- 常量(constant)
表示固定值的语言实体。字面量、枚举常量和常量表达式各有规则;
const在 C 中声明只读对象,不自动成为所有语境可用的整数常量表达式。- printf()
C 标准库里最常用的输出函数。通过格式占位符(%d、%f、%c 等)告诉电脑「把后面的参数按什么格式解释和输出」。类型和占位符必须一一对应——用错不报错,但输出的是乱码。这是 C 初学者最常见的坑之一。详见本章「printf() 格式占位符」一节。