作者简介
Nebula嵌入式 | 嵌入式软件工程师
🏆技术背景:
22届普通二本院校毕业生
专注于嵌入式软件开发(MCU/RTOS/Linux应用/Linux驱动开发 持续学习中)
📚内容创作:
小红书、抖音、B站同名账号,分享嵌入式知识和日常工作,欢迎关注!
一、前言
学 C 最先卡住的往往不是「语法长什么样」,而是三件底层事:
- 变量在内存里到底是什么。
- 类型如何决定「占几字节、能算什么」。
auto/static/extern如何决定「活多久、谁看得见」。
读完本文应能:看懂 sizeof 输出、区分声明与定义、解释为何局部未初始化不能读、并在嵌入式场景优先选用定宽类型。
二、变量:程序与内存交互的核心载体
在 C 语言中,变量本质上是一块被命名的内存空间,用来存储、读取、修改运行时数据。

读图要点:
- 变量 = 名字(标签)+ 类型(柜子规格)+ 值(柜内物品),本质是命名内存。
- 读改变量就是按名字找到地址再读写;类型决定占多少字节、能做什么运算。
可以把内存比作带编号的储物柜:
- 变量名:柜门上的标签(编译期符号,运行时通常已换成地址/寄存器)
- 数据类型:柜子规格(能放什么、占多少字节、如何解释比特)
- 变量值:柜子里的物品(某时刻的比特模式)
传感器读数、计数器、临时计算结果,都依赖变量完成内存读写。没有类型信息,同样一串比特既可以当整数,也可以被误读成字符或浮点。
三、数据类型
数据类型决定内存占用、编码方式以及允许的运算。
3.1 基础类型(以常见 32 位目标为例)

读图要点:
- 表中宽度是常见桌面/32 位目标的典型值;long 在 LP64/LLP64 上可能是 4 或 8。
- 嵌入式优先
<stdint.h>定宽类型,避免依赖「int 到底几字节」。
|
类型 |
典型宽度 |
取值范围(常见) |
用途 |
|
|
1 字节 |
signed: -128127;unsigned: 0255 |
字符 / 8 位数据 |
|
|
2 字节 |
约 -32768~32767 |
小范围整数 |
|
|
4 字节 |
约 -231~231-1 |
通用整型 |
|
|
4 或 8 字节 |
依赖 LP64/LLP64 |
更大整数 |
|
|
8 字节 |
约 -263~263-1 |
64 位整数 |
|
|
4 字节 |
IEEE754 单精度 |
传感器等浮点 |
|
|
8 字节 |
IEEE754 双精度 |
更高精度 |
嵌入式工程优先使用 <stdint.h> 定宽类型:uint8_t、int16_t、uint32_t 等,避免依赖「int 到底几字节」。
3.2 为什么嵌入式更爱 uint32_t?
|
场景 |
用 |
用定宽类型 |
|
协议字段、寄存器位宽 |
换平台宽度变化,掩码写错 |
|
|
跨文件 ABI / 结构体内存布局 |
对齐与填充随 ABI 变 |
定宽 + 明确对齐策略 |
|
代码审查 |
「这里要 32 位」说不清 |
类型名自带宽度语义 |
桌面练习用 int 没问题;一旦涉及硬件手册、通信帧、Flash 布局,优先定宽。
3.3 类型的作用与强制转换
- 决定分配多少字节
- 约束可执行的运算
- 决定编码(整型补码、浮点 IEEE754 等)
#include <stdio.h>
#include <stdint.h>
#include <limits.h>
int main(void)
{
printf("char: %zu, CHAR_BIT=%d\n", sizeof(char), CHAR_BIT);
printf("int: %zu\n", sizeof(int));
printf("uint32_t: %zu\n", sizeof(uint32_t));
printf("long: %zu\n", sizeof(long)); /* 观察本机是 4 还是 8 */
float pi = 3.14f;
int pi_int = (int)pi; /* 向零截断,得到 3,不是四舍五入 */
printf("pi_int = %d\n", pi_int);
/* 有符号溢出是未定义行为;演示「会出错」请用更大类型或检查 */
uint8_t u = 255;
u = (uint8_t)(u + 1); /* 无符号按模 2^n 回绕,得到 0,行为有定义 */
printf("u after wrap = %u\n", (unsigned)u);
return 0;
}
注意:
- 用
%zu打印sizeof的结果(size_t),比%lu更可移植。
- 浮点转整型是截断不是四舍五入。
- 无符号溢出按模回绕;有符号溢出是未定义行为,不要依赖「翻转到负数」。
3.4 char 到底有没有符号?
char 是否等价于 signed char 由实现决定。后果:
- 把字节当「0~255」处理时,应写
unsigned char或uint8_t。
- 做字符分类(
<ctype.h>)时,必须先转成unsigned char再传入(后续 ctype 专章展开)。
char c = (char)0xFF;
printf("as int=%d\n", (int)c); /* signed char 平台常打印 -1 */
printf("as uchar=%u\n", (unsigned)(unsigned char)c); /* 255 */
四、声明、定义、初始化与赋值
4.1 声明
声明把名称与类型告诉编译器,不一定分配内存。多文件共享全局变量时,在头文件用 extern 声明,在某一个 .c 中定义一次:
/* a.h */
extern int global_var; /* 仅声明 */
/* a.c */
int global_var = 0; /* 唯一定义 */
错误示范(不要这样写):
/* extern 与 static 不能这样组合:static 表示内部链接,不可被其他翻译单元 extern */
/* extern static char flag; // 非法 / 无意义 */
文件内私有全局变量应写成:
static char flag; /* 本文件可见,其他文件不能 extern 引用 */
为什么要区分? 若每个 .c 都写 int global_var;(无 extern、无 static),在传统 C 模型下可能产生多重定义或公共符号合并,链接期难查;规范做法是「一处定义,多处 extern 声明」。
4.2 定义
定义为变量分配存储。全局/静态对象在静态存储区;块内自动变量通常在栈上。
int local_var; /* 局部:定义在栈上(未初始化则值不确定) */
char global_ch; /* 全局:静态区,未显式初始化则为 0 */
4.3 初始化
定义时写入初值,避免局部变量「野值」:
int age = 18;
static char ch = 'b';
int total; /* 全局:隐式初始化为 0 */
局部未初始化不是「等于 0」,而是不确定值;读它可能看起来像随机数,开启 -Wall 时常有警告。
4.4 赋值
在已分配的存储上覆盖数据,可多次执行:
age = 20;
age = age + 5;
age = (int)25.9; /* 得到 25 */
4.5 对比
|
概念 |
内存操作 |
时机 |
示例 |
|
声明 |
通常不分配 |
编译期可见性 |
|
|
定义 |
分配存储 |
链接/运行布局 |
|
|
初始化 |
首次写入 |
与定义相关 |
|
|
赋值 |
覆盖已有值 |
运行期 |
|
五、存储类别

读图要点:
- 局部
auto在栈上,进出作用域即失效;static/全局在静态区,程序全程存活。
- 未初始化:栈上局部值不确定勿读;静态区默认按 0 清零(含未显式赋初值的静态对象)。
|
存储类别 |
典型分区 |
生命周期 |
作用域 |
未显式初始化 |
|
|
栈 |
所在块执行期间 |
块内 |
不确定(勿读) |
|
|
静态区 |
整个程序 |
文件或块内 |
0 |
|
|
静态区(引用别处定义) |
整个程序 |
跨文件 |
由定义处决定 |
|
|
建议放寄存器 |
块内 |
块内 |
需自行初始化 |
说明:register 只是给编译器的提示,现代编译器常忽略;不保证一定在 CPU 寄存器,也不能取地址。
#include <stdio.h>
int global_val; /* 静态区,默认 0 */
void test_storage(void)
{
int auto_val = 0; /* 每次进入重新初始化 */
static int static_val = 0; /* 只初始化一次 */
auto_val++;
static_val++;
printf("auto_val=%d, static_val=%d\n", auto_val, static_val);
}
int main(void)
{
test_storage(); /* auto=1, static=1 */
test_storage(); /* auto=1, static=2 */
printf("global_val=%d\n", global_val); /* 0 */
return 0;
}
如何读这段输出: auto_val 每次进函数都从 0 再来;static_val 跨调用保留。调试器里也能看到:局部在栈帧上,静态变量地址在数据/BSS 一带(后续内存布局专章再展开)。
选用经验:
- 默认局部变量即可;不要到处
static「图省事」。
- 需要「函数内记住上次状态」再用函数内
static。
- 需要模块私有全局用文件作用域
static;需要跨文件共享用extern+ 单一定义。
六、避坑指南
- 先定义后使用(C99 起可在块中部定义,但仍应先有定义再读)。
- 同一作用域不要重名;内层可遮蔽外层(独立存储,易读错对象)。
- 局部变量优先初始化,不要读未初始化的
auto变量。 - 多变量声明注意
*只修饰一个名字:
int *a, b; /* a 是 int*,b 是 int */
int *a, *b; /* 两个都是指针 */
- 跨平台、驱动、协议字段优先
stdint.h;MMIO 寄存器加volatile。 - 不要用有符号溢出「当环形计数」;环形缓冲下标用无符号或显式取模。
七、比特、字节与十六进制
|
单位 |
含义 |
|
bit(位) |
二进制一位,取值 0/1 |
|
byte(字节) |
通常 8 bit,内存最小可寻址单位之一 |
|
十六进制 |
一位十六进制对应 4 bit;两字节常写成 |
字面量写法:
int a = 0xFF; /* 十六进制整数:255 */
int b = 255;
char c1 = '\x41'; /* 字符转义:十六进制,等于 'A' */
char c2 = '\101'; /* 字符转义:八进制,等于 'A' */
注意:
0x用在整型常量前(如0x0A)。
\x用在字符/字符串转义里(如'\x0A'、"\x0D\x0A")。
- 不要把
'0xA'当成十六进制字符——单引号里多个字符是实现定义的多字符常量,应避免。
寄存器手册里的 bit3、掩码 0x08,本质都是「按位解释变量里的比特」;后续位操作专章会系统练习。
八、小结
- 变量 = 命名存储;类型定大小与规则;存储类别定分区与寿命。
- 声明 ≠ 定义;
extern用于跨文件引用,static用于限制链接/延长局部寿命,二者用途不同。
- 局部未初始化值不确定;全局/静态默认为 0;有符号溢出不可依赖,无符号按模回绕。
- 嵌入式场景多用定宽类型与
volatile,为后续指针与内存管理打基础。
3787

被折叠的 条评论
为什么被折叠?



