数据和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 给出;shortintlong 和浮点类型的具体大小由实现选择,只需满足标准规定的范围与顺序。程序应查询目标实现,而不是把某台机器的常见结果当成语言规则。

如果所有数据都往同一种通用盒子里塞呢? 电脑就帮不了你检查错误——省下的空间白浪费、该存的小数精确度悄悄丢失、输出的时候格式还跟你想象的不一样。这一章就来认识 C 里这些「盒子的种类」,学会怎么挑、怎么装、怎么取出来看得清清楚楚。

C 的数据类型家族

数据变量和常量(Data Variables and Constants)

变量表示可在生命周期中取得不同值的对象;常量、枚举常量和字符串字面量等则受各自语言规则约束。类型决定值域和操作,存储期决定对象存在多久,const 限定符限制通过该左值修改对象,但 C 中的 const 对象并不自动成为所有语境都可用的整数常量表达式。

在 C 语言中, 是程序最基本的规矩。任何一个变量在声明的那一刻就定死了类型——编译器根据类型来分配内存空间、校验运算是否合法、在 printf 输出时决定该怎么解释那段二进制数据。

类型在 C 里扮演三个角色:① 占用多少存储sizeof 告诉你)、② 能表示什么范围与精度③ 允许哪些运算并如何解释对象表示。不同类型不能因为碰巧大小相同就随意互相重解释。

基本数据类型一览——C 里有哪几种盒子

C 语言提供了以下

  • int 家族shortintlonglong long——存整数,区别在于字节数和取值范围
  • char 家族char——存字符或小整数,占 1 字节
  • 浮点与复数家族floatdoublelong double 及其复数对应类型——区别在于范围、精度和运算语义

下面的图展示一种常见实现的相对布局,用于建立直觉;具体结果必须由当前实现的 sizeof 与标准限制宏确认:

类型字节符号最小值最大值char1依实现实现定义实现定义普通 char 符号性可变unsigned char1无符号0255纯字符/字节数据short2有符号−32,76832,767unsigned short2无符号065,535int4有符号−2,147,483,6482,147,483,647最常用整型unsigned int4无符号04,294,967,295long4 或 8有符号依平台依平台≥ intlong long8有符号≈ −9.2×10¹⁸≈ 9.2×10¹⁸unsigned long long8无符号0≈ 1.8×10¹⁹float4浮点≈ −3.4×10³⁸≈ 3.4×10³⁸示例 IEEE binary32double8浮点≈ −1.8×10³⁰⁸≈ 1.8×10³⁰⁸示例 IEEE binary64long double8/10/16浮点依平台依平台≥ double示例快照:假设 CHAR_BIT=8、32 位 int、LP64 风格 long;这不是 C 语言固定布局。实际范围与精度请查询 limits.h、stdint.h、float.h 和 sizeof。
一种常见实现的数据模型快照。C 标准不强制补码、固定字节位数或表中的具体类型大小。

类型决定了三件事,在写程序时时刻都要记住:

  1. 内存大小sizeof(char) 恒为 1,其他类型大小由实现给出,并满足标准规定的相对约束
  2. 取值范围UCHAR_MAXINT_MININT_MAX 等宏给出当前实现的精确边界
  3. 运算行为:整数除法和浮点除法规则不同——3 / 2 得 1,3.0 / 2.0 才得 1.5

整数和浮点数(Integer Versus Floating-Point Types)

整数类型表示离散值,整数除法向零截断;无符号运算按模数回绕,而有符号溢出是未定义行为。浮点类型表示有限精度的实数近似,范围、精度和舍入特征由实现提供并可通过 <float.h> 查询。两类参与同一表达式时会经过整数提升和通常算术转换。

整型的修饰符——控制最小范围和符号

C 语言提供了四个 用来调整整型的大小和符号:

修饰符作用示例
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 shortunsigned long long 等都是合法的——你可以同时控制大小和符号。

下面这张图直观对比了不同位宽下 signed 和 unsigned 的取值范围:

有符号(signed)与无符号(unsigned)取值范围对比同一宽度下,unsigned 把负数区间「映射」到正数高端8-bitsigned−1280127unsigned025516-bitsigned−32,768032,767unsigned065,53532-bitsigned≈ −21 亿0≈ 21 亿unsigned0≈ 43 亿64-bitsigned≈ −9.2×10¹⁸0≈ 9.2×10¹⁸unsigned0≈ 1.8×10¹⁹signed 范围 [−2ⁿ⁻¹, 2ⁿ⁻¹−1] · unsigned 范围 [0, 2ⁿ−1]
四种位宽下 signed 和 unsigned 的数值区间对比。unsigned 没有负数段, 但高端可以表示比 signed 最大值更大的正数。

在普通算术中可优先考虑 intunsigned 适合模运算、位掩码或标准接口明确要求的无符号类型。仅因为业务值“不应为负”就改成无符号数,可能让减法和边界比较更难诊断。

可移植类型:stdint.hinttypes.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。二进制协议还要显式处理字节序;固定宽度类型本身不决定网络字节顺序。

浮点类型——存小数靠它们

整数搞不定的任务交给 。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 _Complexdouble _Complexlong double _Complex。宏 I 表示虚数单位,可用 crealcimagcabs 等函数读取分量和模。实现是否定义纯虚数类型属于可选能力,但复数类型是标准语言与库的一部分。

#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 语言本身不强制执行字符集必须是 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;
}

有些字符没法直接敲出来——比如换行、制表、双引号本身。这时就需要用到

转义序列名称含义ASCII 码\n换行 (LF)光标移到下一行开头10 (0x0A)\t水平制表 (Tab)光标移到下一个制表位9 (0x09)\\反斜杠输出一个字面的反斜杠 \92 (0x5C)\"双引号在字符串中输出双引号 "34 (0x22)\'单引号在字符常量中输出单引号 '39 (0x27)\0空字符 (NUL)字符串结束标记,ASCII 00 (0x00)\r回车 (CR)光标移到当前行开头13 (0x0D)\a响铃 (BEL)触发系统蜂鸣声7 (0x07)\b退格 (BS)光标向左退一格8 (0x08)\xhh十六进制转义用十六进制码表示字符,如 \x41 = 'A'hh = 任意十六进制反斜杠 \ 告诉编译器「后面的字符有特殊含义」——把普通字母转成控制字符,故称「转义」。\\xhh 中 hh 为两位十六进制数。除 \\x 外还有 \\ooo(八进制转义),如 \\101 = 'A'(八进制 101 = 十进制 65)。
C 语言常用转义序列一览。反斜杠把普通字符转成控制功能——\\n 不是字母 n,而是「换行」。

转义序列在日常编程中频繁出现——printf("Hello\nWorld"); 会在 "Hello" 和 "World" 之间换行。printf("He said \"Hi\""); 才能在双引号里再放双引号。

常量——"不变的值"怎么写

程序中经常需要表达固定值。<Term def="在翻译或执行中表示固定值的语言实体,包括字面量、枚举常量和宏替换产生的常量表达式。const 声明的是不可通过该左值修改的对象,不等同于所有语境中的整数常量表达式。">常量(constant)</Term> 与只读对象要按具体语言规则区分:

方式写法特点
#define#define PI 3.14159宏替换列表,不创建名为 PI 的对象
constconst int weeks = 7;有类型的只读对象;在 C 中不自动成为整数常量表达式
enumenum { 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() 格式占位符——输出时告诉电脑"怎么看这段数据"

的格式占位符跟你选择的类型是一一绑定的——用错占位符,输出不是你想要的结果。下面是最常用的几种:

类型printf 占位符示例
int%d(有符号)、%u(无符号)printf("%d", 42);42
short%hdprintf("%hd", s);
long%ldprintf("%ld", l);
long long%lldprintf("%lld", ll);
char%c(字符)、%d(整数)printf("%c", 'A');A
float%fprintf("%f", 3.14f);3.140000
double%f(或 %lf,但 printf 中等价)printf("%f", 3.14);3.140000
指针/地址%pprintf("%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 在你的实现上分别是多少?先记录猜测,再让程序输出真实结果。

分步1 / 4

① 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;
}

无后缀浮点字面量类型是 doublef 后缀产生 float 字面量。赋给 float 时允许转换,但可能发生舍入;需要让表达式按 float 运算时应明确使用 f 后缀。

容易踩的坑

小结

  • C 语言要求每个变量在声明时必须指定类型——类型决定了内存大小、取值范围和 printf 的格式占位符
  • 基本类型分三类:整型(short/int/long/long long + unsigned 修饰)、浮点型(float/double/long double)、字符型(char)
  • 整型修饰符规定范围顺序但不固定字节数;需要协议宽度时使用实现提供的 stdint.h 类型
  • printf 格式必须与提升后的实参类型匹配;启用格式警告可捕获很多字面量格式错误
  • sizeofCHAR_BITlimits.hfloat.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(综合型) 输出六种类型的 sizeofCHAR_BITINT_MIN/INT_MAXFLT_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() 格式占位符」一节。

资料与写作方式声明

本章以C Primer Plus(第6版)权威目录界定学习范围,并结合正文列出的技术资料独立重写;不宣称复现原书正文,也不沿用原作表述。

原作版权归作者与出版社所有;本站原创教学结构与表述仅供学习交流。

讨论

评论区加载中…