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)
int4 字节4 字节
long8 字节4 字节
指针8 字节8 字节
long long8 字节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,在内存里是一个挨一个排好的,而不是东一块西一块。

这种连续性的好处巨大:

  1. 省内存:操作系统管理内存有最小单位(见第八节),零散地申请 4 个 int 会占用 4 个内存单元,而一次申请一个 int[4] 只占 1 个单元。

  2. 访问快:因为间隔固定(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 long64 位整型跨平台稳定 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、偏移量、地址输出均为该程序真实运行结果。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐