【C语言】02-变量、数据类型与存储

作者简介

Nebula嵌入式 | 嵌入式软件工程师

🏆技术背景:

22届普通二本院校毕业生

专注于嵌入式软件开发(MCU/RTOS/Linux应用/Linux驱动开发 持续学习中)

📚内容创作:

小红书、抖音、B站同名账号,分享嵌入式知识和日常工作,欢迎关注!

一、前言

学 C 最先卡住的往往不是「语法长什么样」,而是三件底层事:

  1. 变量在内存里到底是什么。
  2. 类型如何决定「占几字节、能算什么」。
  3. auto / static / extern 如何决定「活多久、谁看得见」。

读完本文应能:看懂 sizeof 输出、区分声明与定义、解释为何局部未初始化不能读、并在嵌入式场景优先选用定宽类型。

二、变量:程序与内存交互的核心载体

在 C 语言中,变量本质上是一块被命名的内存空间,用来存储、读取、修改运行时数据。

读图要点:

  • 变量 = 名字(标签)+ 类型(柜子规格)+ 值(柜内物品),本质是命名内存。
  • 读改变量就是按名字找到地址再读写;类型决定占多少字节、能做什么运算。

可以把内存比作带编号的储物柜:

  • 变量名:柜门上的标签(编译期符号,运行时通常已换成地址/寄存器)
  • 数据类型:柜子规格(能放什么、占多少字节、如何解释比特)
  • 变量值:柜子里的物品(某时刻的比特模式)

传感器读数、计数器、临时计算结果,都依赖变量完成内存读写。没有类型信息,同样一串比特既可以当整数,也可以被误读成字符或浮点。

三、数据类型

数据类型决定内存占用、编码方式以及允许的运算。

3.1 基础类型(以常见 32 位目标为例)

读图要点:

  • 表中宽度是常见桌面/32 位目标的典型值;long 在 LP64/LLP64 上可能是 4 或 8。
  • 嵌入式优先 <stdint.h> 定宽类型,避免依赖「int 到底几字节」。

类型

典型宽度

取值范围(常见)

用途

char

1 字节

signed: -128127;unsigned: 0255

字符 / 8 位数据

short

2 字节

约 -32768~32767

小范围整数

int

4 字节

约 -231~231-1

通用整型

long

4 或 8 字节

依赖 LP64/LLP64

更大整数

long long

8 字节

约 -263~263-1

64 位整数

float

4 字节

IEEE754 单精度

传感器等浮点

double

8 字节

IEEE754 双精度

更高精度

嵌入式工程优先使用 <stdint.h> 定宽类型:uint8_tint16_tuint32_t 等,避免依赖「int 到底几字节」。

3.2 为什么嵌入式更爱 uint32_t?

场景

int/long 的问题

用定宽类型

协议字段、寄存器位宽

换平台宽度变化,掩码写错

uint16_t/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 charuint8_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 对比

概念

内存操作

时机

示例

声明

通常不分配

编译期可见性

extern int a;

定义

分配存储

链接/运行布局

int a;

初始化

首次写入

与定义相关

int a = 10;

赋值

覆盖已有值

运行期

a = 20;

五、存储类别

读图要点:

  • 局部 auto 在栈上,进出作用域即失效;static/全局在静态区,程序全程存活。
  • 未初始化:栈上局部值不确定勿读;静态区默认按 0 清零(含未显式赋初值的静态对象)。

存储类别

典型分区

生命周期

作用域

未显式初始化

auto(局部默认)

所在块执行期间

块内

不确定(勿读)

static

静态区

整个程序

文件或块内

0

extern

静态区(引用别处定义)

整个程序

跨文件

由定义处决定

register

建议放寄存器

块内

块内

需自行初始化

说明: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 + 单一定义。

六、避坑指南

  1. 先定义后使用(C99 起可在块中部定义,但仍应先有定义再读)。
  2. 同一作用域不要重名;内层可遮蔽外层(独立存储,易读错对象)。
  3. 局部变量优先初始化,不要读未初始化的 auto 变量。
  4. 多变量声明注意 * 只修饰一个名字
int *a, b;   /* a 是 int*,b 是 int */
int *a, *b;  /* 两个都是指针 */
  1. 跨平台、驱动、协议字段优先 stdint.h;MMIO 寄存器加 volatile
  2. 不要用有符号溢出「当环形计数」;环形缓冲下标用无符号或显式取模。

七、比特、字节与十六进制

单位

含义

bit(位)

二进制一位,取值 0/1

byte(字节)

通常 8 bit,内存最小可寻址单位之一

十六进制

一位十六进制对应 4 bit;两字节常写成 0xAB

字面量写法:

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,为后续指针与内存管理打基础。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Nebula嵌入式

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值