C 语言基础与内存管理
C 语言基础与内存管理:从数据类型到结构体
这篇文章把 C 语言最底层的几块拼图讲清楚:六种基本类型到底占多少内存、一个字符为什么本质是数字、结构体怎么在内存里连续排布、数组为什么省空间,以及操作系统到底怎么管理内存。所有结论都附了本机实测代码和真实输出,不是纸上谈兵。
一、C 语言能干什么(为什么它够"底层")
很多人学 C 语言会疑惑:现在都写 Python、Java 了,为什么还要碰 C?答案在"底层"两个字。
-
AI 模型的底层是 C/C++:你调用的那些深度学习框架,核心算子几乎都是 C/C++ 实现的,Python 只是薄薄的一层壳。
-
操作系统、CAD、单机大型软件:这些贴近硬件、追求性能的程序,底层基本都是 C 写的。
-
硬件交互场景:航天军工、电子锁、机器人、无人机这些要和寄存器、GPIO、传感器打交道的领域,C 是直接操作硬件的母语。
-
它是"母语法":Java、Python 这类高级语言的运行时(虚拟机、解释器)本身也是 C 写的(注意不是 C++)。
C 语言不如高级语言"省心",但它能让你看见内存、地址、字节这些最真实的东西。学 C 不是要你以后天天写 C,而是借它把计算机怎么存数据、怎么算数据这件事彻底想通。
二、六种基本数据类型与它们占多大内存
C 语言有六种基本(内建)数据类型,分三个家族:
| 家族 | 类型 | 含义 |
|---|---|---|
| 整数 | short | 短整型 |
| 整数 | int | 整型 |
| 整数 | long | 长整型 |
| 浮点 | float | 单精度浮点 |
| 浮点 | double | 双精度浮点 |
| 字符 | char | 字符型 |
关键认知:同一个类型,不管你存的是 9 还是 13,它占的内存大小永远一样,由类型决定,和数值大小无关。 比如 short 永远是 16 位(2 字节),你把 1 存进去要补 15 个 0,把 130 存进去也照样是 16 位——就像不同尺寸的盒子,装小物件要填泡沫,装大物件也填泡沫,盒子本身容积是固定的。
那具体占多少?不要背书,直接让编译器告诉你。下面这段是本机(gcc 4.9.2 / MinGW-w64 64 位)实测:
#include <stdio.h>
int main(void)
{
printf("sizeof(short) = %lu\n", (unsigned long)sizeof(short));
printf("sizeof(int) = %lu\n", (unsigned long)sizeof(int));
printf("sizeof(long) = %lu\n", (unsigned long)sizeof(long));
printf("sizeof(long long) = %lu\n", (unsigned long)sizeof(long long));
printf("sizeof(float) = %lu\n", (unsigned long)sizeof(float));
printf("sizeof(double) = %lu\n", (unsigned long)sizeof(double));
printf("sizeof(char) = %lu\n", (unsigned long)sizeof(char));
printf("sizeof(int*) = %lu\n", (unsigned long)sizeof(int *));
return 0;
}
本机真实输出:
sizeof(short) = 2 sizeof(int) = 4 sizeof(long) = 4 sizeof(long long) = 8 sizeof(float) = 4 sizeof(double) = 8 sizeof(char) = 1 sizeof(int*) = 8 <- 指针大小,反映系统是 64 位
2.1 short / int / long 整数家族
short 16 位、int 32 位,这两个在所有平台上几乎一致。但 long 是个"坑"——它的大小取决于编译环境的位数规则,不是简单地"64 位系统就一定是 64 位"。
2.2 float / double 浮点家族
float 4 字节,double 8 字节。浮点数有个反直觉的特性:离 0 越近精度越高,离 0 越远精度越低。 想象一条数轴,整数在上面是等距的点(…-2,-1,0,1,2…),浮点数在 0 附近密密麻麻(精度高、能分辨很细微的差别),越往两端越稀疏,两个相邻浮点数之间的"跨度"可能从很小跳到几万。所以"浮点数一定比整数精确"是个误区——数值很大时,浮点数反而可能丢精度,不如整数稳。
2.3 char 字符类型
char 只占 1 字节(8 位),但千万别以为它"简单"。它底层存的就是一个数字——编码值,细节见第五节。
三、字节、位与内存的最小单元
计算机最底层的存储单元是 1 字节(byte)= 8 位(bit)。每一位就是一个开关,只能是 0 或 1(对应电路的高/低电平)。内存可以想象成无数个小格子,每个格子正好放 1 字节,格子上从 0 开始连续编号,这个编号就是内存地址。
32 位系统的地址是 32 位二进制,最多表示 2³² ≈ 40 亿个地址,对应约 4GB 内存上限——这就是老电脑最多认 4G 内存的根因。64 位系统地址翻倍,寻址空间大到天文数字,所以现在 8G、16G、32G 内存随便上。
四、为什么 long 在 Windows 和 Linux 下不一样大(平台差异)
课堂上常听到"64 位系统里 long 是 64 位",这句话在 Linux(LP64) 下是对的,但在 Windows(LLP64) 下是错的。这是两种数据模型决定的:
| 类型 | Linux 64 位 (LP64) | Windows 64 位 (LLP64,MinGW/MSVC) |
|---|---|---|
int | 4 字节 | 4 字节 |
long | 8 字节 | 4 字节 |
| 指针 | 8 字节 | 8 字节 |
long long | 8 字节 | 8 字节 |
我们用的 Dev-Cpp(MinGW-w64)跑在 Windows 上,所以实测 long 是 4 字节,而指针是 8 字节。这就是为什么写跨平台 C 代码时,要表示"固定 64 位整数"应当用 long long 或 <stdint.h> 里的 int64_t,而不是依赖 long。不要凭感觉记大小,永远用 sizeof 问编译器。
五、字符本质:char 存的是数字(ASCII)
屏幕上看到的字母、数字、符号,在内存里存的不是"图形",而是它的编码值。最基础的是 ASCII 码:
-
大小写字母连续:
A~Z是 6590,`a`z是 97~122,所以'a' - 'A' = 32是固定差值。 -
数字字符也连续:
'0'~'9'是 48~57,所以'7'的编码值就是 55。 -
控制字符也有编码,比如换行
\n就是一个 ASCII 字符。
最关键的认知:内存里同一串二进制,你按整数解释还是按字符解释,结果完全不同。 55 这个十进制数,按 %d 打印就是 55,按 %c 打印就被当成 ASCII 码,变成字符 '7'。下面这段是本机实测:
char x = 'A';
printf("'A' 的十进制编码值 = %d\n", x);
printf("'A' + 1 = %d,作为字符打印是 %c\n", x + 1, x + 1);
printf("'a' - 'A' = %d\n", 'a' - 'A');
int n = 55;
printf("int 55 按 %%d 打印 = %d,按 %%c 打印 = %c\n", n, n);
本机真实输出:
'A' 的十进制编码值 = 65 'A' + 1 = 66,作为字符打印是 B 'a' - 'A' = 32(大小写字母固定差值) '7' 的编码值 = 55 '0' 的编码值 = 48,'9' 的编码值 = 57 同一串比特,解释方式不同结果就不同: int 55 按 %d 打印 = 55,按 %c 打印 = 7
因为 char 底层就是数字,所以它能加减、能比较大小——'A' + 1 得到 66 也就是 'B',这完全合法。理解这一点,字符串处理、加密、编解码的很多操作就通了。
六、数组:连续存储带来的高密度
数组是"相同类型数据的集合",它的核心特征是物理地址上连续存放。比如 8 个 int,在内存里是一个挨一个排好的,而不是东一块西一块。
这种连续性的好处巨大:
-
省内存:操作系统管理内存有最小单位(见第八节),零散地申请 4 个
int会占用 4 个内存单元,而一次申请一个int[4]只占 1 个单元。 -
访问快:因为间隔固定(
int就是 4 字节),CPU 只要"首地址 + 下标 × 单位长度"就能精准定位任意元素,不用逐个找。
七、结构体:把多种类型打包成新类型
基本类型像"原子",结构体(struct)就是用基本类型组合出来的"分子"。它允许你把 short、int、char 等不同成员打包成一个复合类型,比如描述一个点:
struct Point
{
short s1; /* 2 字节 */
int s2; /* 4 字节 */
int s3; /* 4 字节 */
char c; /* 1 字节 */
};
7.1 成员连续存储与偏移量
结构体实例在内存里,成员是连续排布的。拿到实例的地址,编译器就知道每个成员从哪开始、占多长,不需要在中间插分隔符。本机实测各成员相对首地址的偏移:
sizeof(struct Point) = 16 字节(含内存对齐补齐) p.s1=90 p.s2=100 p.s3=200 p.c=X 各成员相对首地址的偏移量: &s1 = 0 &s2 = 4 &s3 = 8 &c = 12
7.2 内存对齐(补齐)带来的"意外"大小
你可能算过:2 + 4 + 4 + 1 = 11 字节,为什么 sizeof 出来是 16?这是内存对齐在起作用。CPU 访问内存时,喜欢按"字长"对齐的地址读取,所以编译器会在成员之间插空白(补齐),让 int 落在 4 的倍数地址上,s2 因此从偏移 4(而不是 2)开始,最后整个结构体长度再补齐到 4 的倍数,得到 16。写底层通信协议、做内存映射时,这个补齐经常是 bug 来源,必须心里有数。
八、操作系统怎么管内存(4KB 为一页)
程序运行时,数据都在内存里;程序本身是躺在硬盘上的(硬盘断电不丢,内存断电即空)。内存底层由无数电容组成,充放电表示 0/1,每个电容就是一个比特。
操作系统的内存管理有个重要概念:它不会真的按字节给你分配,而是以"页"(page)为单位,常见的页大小是 4KB(4096 字节)。 也就是说,你哪怕只申请一个 4 字节的 int,系统也给你划出去一整页 4KB,剩下的空间暂时空着(逻辑上归你,物理上没被别人用)。这就是为什么定义一大堆小变量会"看起来"费内存——实际消耗 = 变量个数 × 一页。
同时,CPU 每次发一次地址指令,现代系统会顺手把相邻的 4096 字节一起搬回来,减少"发指令—等返回"的延迟。理解这点,就能明白为什么数组这种一次申请、连续存放的方式比零散变量高效得多。
小知识:内存占用 40%~70% 是正常且高效的状态,系统会尽量把闲内存用起来提速,不用一看到占用高就慌。
附:关键类型 / 关键字速查
| 名称 | 作用 | 备注 |
|---|---|---|
short | 短整型 | 2 字节 / 16 位 |
int | 整型 | 4 字节 / 32 位 |
long | 长整型 | Windows 下 4 字节,Linux 下 8 字节(平台相关) |
long long | 64 位整型 | 跨平台稳定 8 字节 |
float | 单精度浮点 | 4 字节,近 0 精度高、远 0 精度低 |
double | 双精度浮点 | 8 字节 |
char | 字符型 | 1 字节,存的是 ASCII 编码值(数字) |
sizeof | 求类型/变量字节数 | 编译期确定,结果类型是 size_t |
struct | 自定义复合类型 | 成员连续存储,存在内存对齐 |
malloc / free | 动态申请 / 释放堆内存 | 申请的单位是字节,需强制类型转换 |
| ASCII | 字符↔数字映射表 | 'A'=65,'a'=97,'0'=48 |
附:本文实测代码
完整可运行源码见 ../demos/816-basics/basics.c,用以下命令编译运行(编译器为 Dev-Cpp 自带的 MinGW-w64 gcc 4.9.2):
gcc basics.c -o basics ./basics
实测环境:Windows + MinGW-w64 (tdm64-1) gcc 4.9.2,64 位程序,指针 8 字节。文中所有 sizeof、偏移量、地址输出均为该程序真实运行结果。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)