← 返回
Web3与WASM 2026.03.09

存储位置与拷贝机制:storage、memory、calldata

Web3与WASM

学习目标

理解 EVM 中三种数据存储位置的特点,以及引用类型在不同存储位置之间赋值时的拷贝规则。

前置知识

已学习值类型和引用类型(数组、结构体、映射、字符串)。


三种存储位置

storage —— 持久化存储

  • 类似数据库,数据永久保存在区块链上
  • 成员变量(状态变量)默认存储在 storage
  • 读写 gas 成本最高

memory —— 临时内存

  • 函数执行期间存在,函数返回后销毁
  • 局部变量默认存储在 memory
  • gas 成本适中

calldata —— 调用数据

  • 来自 transaction 的 msg.data只读
  • 不可修改,gas 成本最低
  • 适用于 external 函数的参数

存储位置对引用类型的影响

关键规则:

  • 不同 location 的同一引用类型,编译器视为不同类型
  • public / external 函数参数只能是 memorycalldata
  • internal / private 函数参数可以是 storage

综合示例

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

contract DataLocations {
    struct MyStruct {
        uint256 foo;
        string text;
    }

    mapping(address => MyStruct) myStructs;

    function examples(uint[] calldata y, string calldata s) external returns (uint[] memory) {
        myStructs[msg.sender] = MyStruct({foo: 123, text: "bar"});

        // storage 引用:修改会持久化
        MyStruct storage myStruct = myStructs[msg.sender];
        myStruct.text = "foo";

        // memory 副本:修改不影响存储
        MyStruct memory readOnly = myStructs[msg.sender];
        readOnly.foo = 456; // 不会影响 myStructs

        _internal(y);

        uint[] memory memArr = new uint[](3);
        memArr[0] = 234;
        return memArr;
    }

    function _internal(uint[] calldata y) private pure {
        uint x = y[0];
    }
}

值拷贝 vs 引用拷贝

这是 Solidity 中最容易混淆的知识点之一,务必反复理解。

核心概念:成员变量的特殊性

在 EVM 中,成员变量(状态变量)指向固定的 storage 数据块(slot),不能像一般引用变量那样切换指向的数据块。因此对成员变量赋值,只能是值拷贝

判定算法

对于赋值操作 x = a,按以下规则判定是值拷贝还是引用拷贝:

1. 如果 x 是成员变量 → 值拷贝
2. 如果 x 是局部变量:
   - x 与 a 的 location 相同 → 引用拷贝
   - x 与 a 的 location 不同 → 值拷贝

检查算法

当判定为值拷贝时,编译器还会进行以下检查:

1. 检查类型中是否包含 mapping → 有则编译错误(mapping 不支持拷贝)
2. 检查 x 是否为 calldata → 是则编译错误(calldata 只读)
3. 通过检查 → 执行值拷贝

一句话总结

当赋值被解释为引用拷贝时,如果不与更高优先级的设计选择相冲突,则为引用拷贝;否则为值拷贝。

常见场景速查

赋值场景拷贝类型说明
成员变量 = storage引用值拷贝成员变量始终值拷贝
成员变量 = memory变量值拷贝成员变量始终值拷贝
storage局部变量 = 成员变量引用拷贝同为 storage,指向同一数据
memory局部变量 = memory变量引用拷贝同为 memory,指向同一数据
memory局部变量 = storage变量值拷贝不同 location
storage局部变量 = memory变量编译错误storage 局部变量只能引用已有 storage 数据

关于默认值与初始化

  • 成员变量:自动初始化为默认值(uint → 0,bool → false,address → 0x0 等)
  • memory 局部变量:自动初始化为默认值
  • storage 局部变量必须经过赋值才能使用,不会自动初始化

历史安全漏洞:早期 Solidity 版本中,未赋值的 storage 局部变量会默认指向 slot 0,可能意外覆盖其他状态变量的数据,造成严重安全问题。现代编译器已修复此问题。


完整 storage 交互示例

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

contract DataLocations {
    uint[] public arr;
    mapping(uint => address) map;
    struct MyStruct {
        uint foo;
    }
    mapping(uint => MyStruct) myStructs;

    function f() public {
        // 将 storage 变量传递给 internal 函数
        _f(arr, map, myStructs[1]);

        // storage 局部变量:引用拷贝,指向 myStructs[1]
        MyStruct storage myStruct = myStructs[1];

        // memory 局部变量:独立副本
        MyStruct memory myMemStruct = MyStruct(0);
    }

    function _f(
        uint[] storage _arr,
        mapping(uint => address) storage _map,
        MyStruct storage _myStruct
    ) internal {
        // 操作 storage 引用,修改会持久化
    }

    function g(uint[] memory _arr) public returns (uint[] memory) {
        // 操作 memory 数组,函数返回后销毁
    }

    function h(uint[] calldata _arr) external {
        // 操作 calldata 数组(只读,gas 更低)
    }
}

小结

存储位置生命周期可写Gas 成本典型用途
storage永久最高状态变量
memory函数执行期间中等局部变量、函数参数
calldata函数执行期间最低external 函数参数

拷贝规则核心

  • 赋值给成员变量 → 始终值拷贝
  • 局部变量间赋值 → 同 location 引用拷贝,不同 location 值拷贝
  • mapping 不可值拷贝,calldata 不可写入

上一篇引用类型详解