什么是 storage、memory 和 calldata?数据放在哪里,为何影响修改和 Gas
storage、memory 和 calldata 决定数据的生命周期、可修改性与复制方式。本文用对比和实例讲清引用、副本、Gas 与常见代码风险。
同样是一个数组,在函数参数后写 storage、memory 或 calldata,结果可能完全不同:改一个元素,有时会永久改变链上状态,有时只改临时副本,有时编译器直接不允许修改。这里的差别不是命名风格,而是数据究竟放在哪里、活多久,以及赋值时复制还是共享引用。
这三个关键词主要服务于数组、结构体、映射、bytes 和 string 等引用类型。理解它们,要同时回答三件事:生命周期多长、能否修改、与另一个变量赋值后是否指向同一份数据。
storage 是持久状态,memory 是一次调用里的临时工作区,calldata 是外部调用带来的只读输入。
storage:跟随合约长期存在
状态变量默认位于 storage,数据写入链上状态并跨交易保留。修改 storage 通常会产生较高 Gas 成本,因为全网需要对新状态达成一致并保存。映射只能存在于 storage,不能整体搬进 memory 后照常使用。
函数内写 uint[] storage ref = values; 时,ref 通常不是独立副本,而是指向已有 storage 对象的局部引用。通过 ref 修改元素、追加或删除,会直接改变 values。名字是局部的,影响却是持久的。

memory:只活在当前调用中
memory 是执行期间的临时区域,函数结束后不再保留。它适合中间计算、构造返回值或处理需要修改的临时数组。把 storage 数据赋给 memory,会创建独立副本;修改副本不会自动写回状态,除非随后明确赋值回 storage。
memory 不是“免费”。分配、扩展和复制都消耗 Gas,大数组可能显著增加执行成本。优化时应先减少不必要的数据规模和复制,而不是机械地把所有变量改成 memory。
calldata:外部调用携带的只读输入
calldata 是非持久、不可修改的数据区域,通常保存函数参数。它避免先把完整参数复制到 memory,并通过只读约束表达“函数不会改这份输入”。官方文档建议在语义允许时优先使用 calldata,因为可以避免复制。
但 calldata 不能在函数中随意新建,也不能原地修改。如果算法需要排序、过滤或重写数组,就要复制到 memory,或改为只读遍历。选择位置首先要正确表达意图,Gas 优化排在其后。

三种位置快速对比
| 位置 | 生命周期 | 可修改 | 典型用途 |
|---|---|---|---|
| storage | 随合约状态长期存在 | 可以 | 余额、配置、订单、映射 |
| memory | 当前外部调用期间 | 可以 | 临时计算、可变副本、返回值 |
| calldata | 当前调用期间 | 不可以 | 外部传入的只读参数 |
真正容易出错的是赋值语义
Solidity 官方文档说明,storage 与 memory 之间,或从 calldata 赋值到其他位置时,会产生独立复制;memory 变量之间的赋值通常只建立引用,所以修改其中一个,另一个也可能看到变化;从 storage 赋给局部 storage 变量同样通常只是引用。
| 赋值方向 | 大致行为 | 常见误解 |
|---|---|---|
| storage → memory | 复制 | 改 memory 会自动写回 |
| memory → storage 状态变量 | 复制并更新状态 | 只是换了一个局部名字 |
| storage → 局部 storage | 引用 | 局部变量不会影响原数据 |
| memory → memory | 常为引用 | 两份数据完全独立 |
| calldata → memory | 复制 | 复制没有执行成本 |
一个简单例子为何会产生两种结果
假设状态数组 values 为 [1, 2]。函数先创建 uint[] storage a = values,再执行 a[0] = 9,交易成功后 values 会变成 [9, 2]。如果创建的是 memory 副本并把首项改成 9,values 仍然是 [1, 2],除非代码最后执行 values = copy。
阅读代码时不能只看变量名叫 temp 或 copy,要看数据位置和赋值来源。一个名为 cache 的 storage 引用照样能永久改状态,一个名为 records 的 memory 变量也可能只是临时副本。
为什么引用会制造隐蔽副作用
内部函数接收 storage 引用时,可以修改调用者传入的状态对象。函数名如果看起来只是“计算”或“检查”,这种副作用很容易在审查中被忽略。应使用清楚命名,限制修改范围,并用测试验证调用前后哪些存储槽发生变化。
为什么复制会带来 Gas 与正确性问题
把大型 storage 数组整体复制到 memory,再只读取一个元素,可能浪费 Gas;把 calldata 复制后忘记写回,又可能让开发者误以为状态已更新。反过来,为省复制而传递 storage 引用,也会扩大可修改范围。优化必须建立在明确的数据流和基准测试上。
外部、公共与内部函数怎样选择
现代 Solidity 允许在多种函数可见性中使用 memory 或 calldata 参数。只读引用类型输入通常适合 calldata;需要构造或修改临时值时用 memory;只有函数确实要操作既有链上对象时,内部或私有函数才接受 storage 引用。构造函数参数不能使用 calldata。
还要知道 transient storage
较新的 Solidity 还支持 transient 状态变量:它像键值存储,但只在当前交易内存在,交易结束后清零,常见用途是可组合的重入锁。它不是 memory,也不是引用类型可随意使用的第四种通用位置。官方文档目前说明,引用类型尚不能以 transient 作为数据位置,且使用时要注意多次调用组合产生的状态残留。
开发者检查清单
- 每个引用类型变量的数据位置是否明确?
- 局部 storage 变量指向哪个状态对象?
- memory 之间赋值后是否共享引用?
- calldata 是否被无必要地整体复制?
- 临时修改是否真的需要写回 storage?
- 大数组操作是否做过 Gas 和上限测试?
- 内部函数的状态副作用是否从命名和接口可见?
普通用户为什么也值得了解
用户不需要读懂每个存储槽,但可以理解两类审计提示:“错误使用 storage 引用”可能导致永久状态被意外修改;“无界复制或遍历”可能让交易因 Gas 不足而无法执行。看到项目宣称“已优化 Gas”,也应关注功能和安全是否保持一致,而不是只看单次费用数字。
官方资料
本文的数据位置与赋值规则依据 Solidity Types:Data Location;transient 状态说明参考 Solidity Contracts:Transient Storage。具体语法与限制请以项目编译器版本对应文档为准。
storage、memory 和 calldata 的选择,本质上是在声明数据的所有权、生命周期与修改权限。先判断数据需不需要持久化、需不需要修改,再确认赋值是复制还是引用,绝大多数困惑就能沿着这条线解开。
本文为链上指南原创科普内容,仅用于解释智能合约机制,不构成任何投资建议。智能合约开发和链上交互均有技术风险,请结合锁定版本的官方文档、测试与独立审计。