变量存储属性

官方信息技术老师·25 页·深入(追求细节与边界)·0 次浏览·2 天前
存储期作用域链接属性内存布局

变量存储属性

搞清存储期、链接与作用域,看透变量在内存中的安身规则

按 空格/→ 演示下一步

1 / 25 页

全部页面点击任意一页,跳回舞台从这页播放

存储期作用域链接属性内存布局

变量存储属性

搞清存储期、链接与作用域,看透变量在内存中的安身规则

1第 1 页 · 变量存储属性

为什么需要存储属性

CPU 一条指令一条指令往下走,每执行完一条都会产生中间结果——如果当下不落到某个地方,下一条指令就拿不到。这就是变量必须依赖存储的根本原因。

顺序执行的产物
指令逐条往下执行,每一步产生的中间结果必须落下来,否则下一步无从读取
无状态计算单元
CPU 只认指令不存数据,自身完全没记忆能力,只能借助外存
跨指令数据桥梁
多个运算环节要共用同一份数据,存储是它们之间唯一传值的通道
常量的边界例外
字面量这种立即数编译期就嵌进指令了,反而不用走变量存储
流水线工人写黑板对应 →CPU 顺序执行需要存储

工人算完中间结果写上黑板,下道工序直接读;没黑板流水线就断了

2第 2 页 · 为什么需要存储属性

四种存储类别总览

以根为中心向外辐射,四类分支各带两条核心特性。先看分支名记住身份,再看子节点理解行为。

图解渲染中…
栈内存函数调用时在栈上分配的临时内存无地址register 变量不能用 & 取地址静态数据区程序整个运行期间都存在的内存区域声明非定义extern 只声明变量,不分配存储
3第 3 页 · 四种存储类别总览

存储类型决定什么

四种存储类别不只是一个标签,它们具体决定了变量的三个维度:作用域、生命周期、内存位置。选哪种类别,等于同时在做三个决策。

作用域
代码中哪些位置可以访问这个变量,由定义位置和存储类别共同决定
生命周期
变量从诞生到消亡的时间跨度,由存储类别决定何时分配、何时释放
内存位置
变量实际放在哪里:栈、堆、数据段还是寄存器,直接影响访问速度
三者联动
改一个维度会牵动另外两个:想延长生命周期就要权衡作用域和内存代价
租一套房对应 →变量的存储属性

租期≈生命周期、能用哪些房间≈作用域、房子在哪≈内存位置——三件事随房型一起定

4第 4 页 · 存储类型决定什么

内存区域划分

程序内存的栈区、堆区、静态区、代码区分布

内存区域划分
程序内存的栈区、堆区、静态区、代码区分布
5第 5 页 · 内存区域划分

局部变量的本质

内存划分好后,最常见的一类变量——函数内部定义的局部变量——它们的「家」又在哪里?

局部变量定义
写在函数或代码块内部、用 let/var 声明的命名存储
作用域=大括号内
从声明处到所在大括号结束,外部完全看不见
生命周期=调用期
每次调用函数都重新分配一份,函数返回即销毁
栈上为主
值类型直接放栈;引用类型的盒子在栈,指向的实例在堆
被捕获就逃逸
若被闭包捕获,Swift 会把它从栈搬到堆,以延长寿命
酒店房间的临时房卡对应 →局部变量

入住分配、退房作废——对应当前调用栈帧;房卡信息被复刻带走(捕获)后,权限就逃出房间继续生效

6第 6 页 · 局部变量的本质

局部变量代码示例

python

用 Python 直观演示局部变量的定义、每次调用重建的生命周期、以及作用域边界。

代码高亮加载中…

items 在函数内被定义;每次调用都创建一份新的,函数返回即被回收;连续两次调用拿到的列表互不相干;函数外访问触发 NameError,划清作用域边界。

7第 7 页 · 局部变量代码示例

全局变量的本质

上页说局部变量像办公桌抽屉——自己用自己管。但项目里多个函数往往要共享同一个数据,这时候就需要全局变量出场了。

定义位置
写在所有函数体外面,不从属于任何函数
作用域
从定义处到本文件结束,所有函数都能直接读写
生命周期
程序启动即分配内存,程序退出才释放
存储位置
位于全局数据区,与栈区是两条不同的街
办公室公共白板对应 →全局变量

白板放公共区,谁都能读能写,内容一直留着直到下班清理

8第 8 页 · 全局变量的本质

全局变量代码示例

c

通过一份精简 C 代码,看清定义、extern 声明与初始化规则的真实差异。

代码高亮加载中…

L3 仅声明不占存储;L4 带初始化进 .data;L5 无初值仍是定义,运行时清零;L6 const 进入只读段;main 内可直接使用所有全局。

9第 9 页 · 全局变量代码示例

全局变量的副作用

上一页用代码展示了全局变量的便利——但便利的另一面就是失控。任何角落都能读写同一个值,四类麻烦就会冒出来。

隐式耦合
函数未声明却悄悄读写全局值,调用顺序一变结果就变
命名污染
多人或多模块协作时,同名变量互相覆盖难以察觉
调试黑洞
值可在任意文件被改,出错时难定位修改点和时机
测试与并发
用例互相污染无法隔离;多线程下产生竞态条件
办公室公用白板对应 →全局变量

所有人都能看能写,谁都敢擦,出错找不到责任人

10第 10 页 · 全局变量的副作用

局部 vs 全局变量

作用域、存储位置、生命周期、默认值——四个维度环环相扣,混用时定位 bug 极难。

局部变量
  • 作用域:限定在函数或代码块内
  • 存储位置:栈(Stack)
  • 生命周期:函数调用时创建,结束即销毁
  • 默认值:未初始化则值不确定(栈垃圾)
全局变量
  • 作用域:整个程序可见,跨文件需 extern 声明
  • 存储位置:静态存储区(数据段)
  • 生命周期:程序启动到结束,与函数调用无关
  • 默认值:自动初始化为 0
默认优先用局部——可控、可测、线程安全;需跨函数共享配置时才用全局,并加 static 收敛作用域。
11第 11 页 · 局部 vs 全局变量

auto 存储类别

局部变量是函数里临时用的数据,它默认属于哪种存储类别?答案是 `auto`——而且通常你连这个关键字都不用写。

默认存储类别
局部变量的默认属性就是 auto,auto 关键字可以省略不写
栈区存放
变量分配在函数的栈帧上,函数结束栈帧销毁,变量随之释放
自动生命周期
进入作用域时构造,离开作用域时自动析构,无需手动释放
C++11 后含义变化
auto 被借去做类型推导,原表示存储类别的用法要靠上下文判断
酒店入住与退房对应 →auto 变量生命周期

进入函数相当于入住分配房间,函数结束相当于退房,房间自动收回给下一个调用者

12第 12 页 · auto 存储类别

register 存储类别

请求将变量存储在寄存器中以加快访问,但不一定成功

register 存储类别
请求将变量存储在寄存器中以加快访问,但不一定成功
13第 13 页 · register 存储类别

static 局部变量

你已经见过 auto 和 register 局部变量。它们有个共同点:函数一返回就'蒸发'。static 局部变量偏要打破这个规矩。

关键字 static
加在局部变量前的修饰符,告诉编译器这变量要活到程序结束
生命周期延长
从函数调用期间,变成整个程序运行期间
作用域不变
仍然只能在声明它的函数内部访问
只初始化一次
首次调用时初始化,之后保留上次的值
搬到全局数据区
物理上与全局变量同住一片内存,但作用域仍为局部
宿舍里的个人抽屉对应 →static 局部变量

只有室友能打开(作用域不变),但毕业典礼前不会被清理(生命周期延长)

14第 14 页 · static 局部变量

static 局部变量示例

c

三次调用同一个 counter(),count 的值不归零,而是持续累加。

代码高亮加载中…

L4 的 static 让 count 只初始化一次;L10~L12 三次调用证明:每次进入 counter,count 都带着上次的值继续累加,不会归零。

15第 15 页 · static 局部变量示例

static 全局变量

全局变量虽然贯穿整个程序,但任何 .c 文件都能 extern 引用——这太危险了。给全局变量加 static,等于给它装上文件级门禁:其他文件再也够不着它。

本质仍是全局变量
生命周期不变,仍存于全局数据区,从程序启动活到结束
作用域缩到本文件
其他 .c 文件无法 extern 引用,符号对外不可见
文件级封装
对内是共享数据,对外是隐藏实现细节
避免命名冲突
不同文件可有同名 static 全局变量,互不干扰
公司公告栏对应 →static 全局变量

普通全局像大堂公告谁都改,static 后等于把公告搬进部门办公室——只有本部门能看能改

16第 16 页 · static 全局变量

extern 存储类别

上页我们用 static 把全局变量关进当前文件。可工程里多个 .c 文件经常要共享同一个全局变量——这次得开门迎客。extern 就是那把开门钥匙。

声明而非定义
告诉编译器变量在别处存在,自己不分配内存
跨文件访问入口
其他 .c 文件想用全局变量,必须用 extern 声明
声明可多次,定义仅一次
整个程序多处 extern 声明合法,变量却只能定义一处
典型工程模式
在 .h 头文件 extern 声明,某个 .c 文件中实际定义
共享文档链接对应 →extern 声明

原始文档在某 .c 文件真实存在(定义才分配存储),链接只是指向,多人收到同一链接都不额外占空间

17第 17 页 · extern 存储类别

extern 跨文件示例

c

两个文件,一份存储:file1 定义,file2 通过 extern 共享同一块内存。

代码高亮加载中…

第 4 行真正分配存储;第 14 行 extern 只是声明;第 18、19 行证明两文件操作同一块内存。

18第 18 页 · extern 跨文件示例

静态存储的过程

静态变量的完整生命周期:编译期划位、加载期落座、运行期驻守、结束期归还。

1
编译期划位
编译器扫到 static/extern 声明,就在数据段标好大小与位置
2
加载期落座
程序装入内存时,数据段随之进入物理地址并写入初值
3
运行期驻守
从 main 执行前到 main 返回后,这块内存一直占着,值跨函数保留
4
结束期归还
进程退出时,OS 一次性回收整片内存,静态变量随之消失
19第 19 页 · 静态存储的过程

动态存储的过程

动态存储要求程序员手动完成申请、使用、释放三大阶段,每一步都不能漏。

1
malloc 申请
向堆区申请一块指定大小的内存,返回首地址
2
检查指针
确认 malloc 返回值非 NULL,申请失败必须处理
3
按需使用
通过指针读写这块内存,大小由程序员自己记着
4
free 释放
把内存归还堆区,操作系统可重新分配给别人
5
指针置空
free 后指针仍指向旧地址,置 NULL 防悬空指针
20第 20 页 · 动态存储的过程

生命周期全景对比

三条横向轨道分别画三种变量的生死节点;纵向对比同一时刻谁生谁亡。

图解渲染中…
a2绿色节点=变量此刻存活、占用内存a3灰色节点=变量未诞生或已消亡s3static局部函数退出后仍存活g3全局变量程序结束时才消亡
21第 21 页 · 生命周期全景对比

存储类别完整对照表

四种存储类别的关键字、作用域、生命周期、存储位置四维对比

存储类别完整对照表
四种存储类别的关键字、作用域、生命周期、存储位置四维对比
22第 22 页 · 存储类别完整对照表

核心概念自测

点击作答

在 C 语言中,函数体内声明的 static 变量,哪项描述最准确?

23第 23 页 · 核心概念自测

课程要点总结

  • 作用域回答“在哪里可见”,生命周期回答“何时存在”
  • static 可延长局部变量寿命,也可限制全局符号可见性
  • extern 实现跨文件引用,但本身不分配存储
  • 存储类别共同决定名称可见范围与对象生存区间
延伸主题:符号链接与编译单元const 与 volatile栈溢出与静态区限制
24第 24 页 · 课程要点总结

课后思考

先遮住答案区自己思考,想清楚再看右侧提示——课后琢磨比课上听懂更重要。

1static 修饰局部变量是'延长生命周期',修饰全局变量却是'限制作用域'。一个关键字为何承担两种相反语义?

参考答案两种用法都围绕'改变存储/链接方式':局部 static 从栈搬到静态区(延长生命周期);全局 static 从外部链接改为内部链接(限制可见性)。对立表象下,机制是统一的。

2为什么 extern 声明通常写在 .h 头文件,而不是每个 .c 都重复一遍?反过来放在 .c 里会有什么问题?

参考答案头文件统一管理声明,避免每个 .c 重复写、漏写;extern 是声明不是定义,多次包含不冲突。若把定义放头文件,每个 .c 都会复制一份,链接时报重复定义错误。

3既然全局变量副作用大,其他语言是怎么避免的?反过来,C 中有没有办法既保留全局变量的便利,又规避它的风险?

参考答案其他语言用类/命名空间收窄可见性,把'全局'封装成受控接口。C 中类似思路:变量藏 .c 内、用 static 限为内部链接,只通过函数暴露读写——这是 C 版的模块化封装。

25第 25 页 · 课后思考