想象你所在的小区,物业新装了一台全自动售货机。它二十四小时无人值守,却从不出错:你投币,它出货;余额不足,它就亮灯拒退;有人往货架塞假货,它第二天自动切到“检修”状态。这台机器的所有规则在出厂那天就焊死在电路里——这正是智能合约的缩影。Solidity 就是给这台机器写“出厂说明书”的语言:address 决定你是谁,mapping 决定你打开哪个柜格,动态数组决定货架能塞多少货,结构体把商品信息钉成一张卡,枚举描述机器当前状态,gas 是每次操作的电费,而安全陷阱则是那些想趁找零口打开瞬间偷钱的小偷。读完这篇教程,你就能从零看懂这台“链上售货机”的全部原理,并亲手写出安全高效的合约。

岗位关联

对 AI 从业者而言,理解 Solidity 意味着你能读懂链上数据与智能合约逻辑,为 DeFi、NFT 与去中心化 AI 应用打下可信计算的底层直觉——这是 AI 与区块链交汇时代的高价值技能。

目录
  1. 可视化演示
  2. 地址与账户类型
  3. mapping 映射
  4. 动态数组
  5. 结构体与枚举
  6. gas 优化技巧
  7. 安全陷阱防护
  8. AI 应用场景
  9. Python 演示
  10. 常见误区
  11. 面试题

可视化演示

展示Solidity核心数据类型、Gas优化对比、安全防护机制及智能合约结构,通过交互理解智能合约开发核心语法与最佳实践。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Solidity核心数据类型 address:20字节以太坊账户地址 mapping:键值映射,Gas固定 struct:自定义复合数据结构 Type: address, mapping, array, struct, enum, uint

地址与账户类型

社区售货机上电后,第一件事就是给每位顾客登记会员指纹。这张“指纹卡”只有 20 字节,浓缩着你唯一的身份——Solidity 里它叫 address。同一种卡还分两种:普通卡只能取货验货,而背面印着投币口标志的“收款卡”才能收钱找零,后者叫 address payable。别小看这张卡片的区别,后面所有转账逻辑都从这张卡开始。

在 Solidity 中,address 是一个 20 字节(160 位)的值类型,用来表示一个账户。它既可以代表你这种“人肉账户”(外部账户,EOA),也可以代表另一台售货机(合约账户)。为了让“谁收钱、谁找零”在类型上就分清楚,Solidity 规定:只有 address payable 才有 transfer 与 send 方法,普通 address 必须调用 payable(addr) 显式升级后才能动钱。

开发中还常遇到三个成员:msg.sender 表示当前插卡操作用户,address(this) 是售货机自己的地址,addr.balance 查询某账户余额。三者配合可以完成“验卡、查账、转账”的完整闭环。合约地址与 EOA 地址长度相同,但合约地址背后带着一段可执行的“程序卡”,这一点在鉴权时尤其需要留意。

有了身份卡体系,售货机终于能分清谁是顾客、谁是维护员、该给谁找零。但这台机器还没货可卖——下一步,我们得把货物柜格装起来。

p=payable(a),a:address,p:address payablep = \text{payable}(a),\quad a : \text{address},\quad p : \text{address payable}
这条式子的意思是:把一张只读的普通身份卡,通过 payable() 这个“升级槽”变成一张可以收钱找零的收款卡——类型变了,能力也变了。
  1. 左侧赋值 p声明了一个可以收币的新变量,相当于把升级后的卡插进读卡器。
    p:address payablep : \text{address payable}
  2. 转换函数 payable(a)对普通卡执行“升级操作”,把地址 a 包装成带收款能力的卡。
    payable(⋅):address→address payable\text{payable}(\cdot) : \text{address} \to \text{address payable}
  3. 类型标注 a转换前的卡仍是普通 address,不能直接调用 transfer——这是类型系统为你设下的安全围栏。
    a:addressa : \text{address}

mapping 映射

售货机内部是一面巨大的储物柜墙,每格柜门都贴着一个编号。你报出柜号,机器就打开对应柜门;柜号没人知道也没关系,机器只负责“报号开柜”。这个“柜号→柜中物”的系统就是 mapping,Solidity 里管理键值关系的核心数据结构:mapping(KeyType => ValueType)。

与 Python 字典最大的不同在于:mapping 的键根本不写入链上存储,机器只把“键”和“声明位置”揉在一起做一次 keccak256 哈希,把哈希结果当作真正的柜门编号,然后在那个槽位里存放值。所以 mapping 没有 length、没有 keys()、不能遍历——它是一面没有总目录的储物柜墙,想遍历时必须另用一个动态数组手动记录所有钥匙。

实际开发中 mapping 常用来记录“地址到余额”(mapping(address => uint))、“地址到授权状态”(mapping(address => bool))、甚至嵌套的授权表 mapping(address => mapping(address => uint)) 表示“A 授权 B 花多少钱”。访问不存在的键不会报错,而是返回默认值 0 或空地址——这一点常被新手误当成程序 bug。

柜格装好,机器已经能按号存取货物。但货架是死的,一次只能摆固定数量的货。想随时补货,就需要一面能伸缩的货架——这正是数组的用武之地。

value=storage[keccak256(k⋅p)]\text{value} = \text{storage}\big[\mathrm{keccak256}(k \cdot p)\big]
翻译成售货机语言:报出柜号 k,机器把柜号和储物柜区域编号 p 一起塞进哈希磨钥匙机,得到真正的柜门编号,再去仓库对应槽位取出货物。
  1. storage 数组以太坊把合约数据放在一个个 32 字节的“仓格”里,这一步表示按槽位取数。
    storage[⋅]:N→{0,1}256\text{storage}[\cdot] : \mathbb{N} \to \{0,1\}^{256}
  2. keccak256 哈希函数编译器的“磨钥匙机”,把任意输入变成 256 位均匀散开的数字,同一输入永远得到同一把钥匙。
    keccak256:{0,1}∗→{0,1}256\mathrm{keccak256} : \{0,1\}^* \to \{0,1\}^{256}
  3. 拼接 k·p键 k 与 mapping 声明处槽位指针 p 相连,保证两个 mapping 即使键相同也不会撞开同一扇柜门。
    k⋅p=k ∥ pk \cdot p = k \,\|\, p

动态数组

社区商品越卖越多,固定 20 格的储物柜显然不够。补货员推来一辆可伸缩的推车:往末端一推,货架就多一格;从末端一抽,货架就少一格。这就是 Solidity 里的动态数组——不用提前声明长度,随时加货撤货。

uint[] 是最常见的动态数组。arr.push(x) 相当于往车尾塞一件货,并返回塞完后的新长度;arr.pop() 撤走最后一件;arr.length 告诉你当前载货量。元素在存储中的位置也很有规律:从 keccak256(数组声明槽位) 开始连续排布,第 i 个元素就放在起始位置加 i 的槽里,这正是数组能按下标随机访问的原理。

除了 storage 里的动态数组,函数内部还常用 memory 数组,比如 uint[] memory tmp = new uint[](5)——长度固定,用完即焚,不占链上仓库,因而更便宜。二维数组也遵循同样规则,比如 uint[][3] 表示外层动态、内层固定三格的“三层货架”,常用于矩阵或批次数据管理。

货架能伸缩之后,机器终于能装下五花八门的商品。可每件货品的名称、价格、保质期都该记在哪里?总不能每个属性占一格货架。给商品做一张“信息卡”,正是结构体要解决的下一件事。

storage[keccak256(p)+i]=ai,i∈[0,len)\mathrm{storage}\big[\mathrm{keccak256}(p) + i\big] = a_i, \quad i \in [0, \mathrm{len})
意思是:数组像一列连续的储物格,起点由声明位置哈希决定,第 i 件货就放在“起点 + i”这一格,且下标必须落在货物数量范围内。
  1. 起始位置 base数组的“货架起点”,由声明槽位 p 哈希得到,与 mapping 的寻址方式同源。
    base=keccak256(p)\text{base} = \mathrm{keccak256}(p)
  2. 偏移量 i第 i 个元素紧挨着前一个摆放,所以是起点加 i。
    sloti=base+i\text{slot}_i = \text{base} + i
  3. 范围约束只有在下标小于数组长度时,访问才合法,越界会被编译器拦下。
    0≤i<len0 \le i < \mathrm{len}

结构体与枚举

售货机的每个货格里,都钉着一张商品信息卡:名称、单价、库存、保质期,四个字段被同一枚图钉固定在一起。这张“多字段信息卡”在 Solidity 里就是结构体 struct,它把多个不同类型的变量打包成一个整体,让“一件货品”在代码里成为一个完整的对象。

定义 struct Product { string name; uint price; uint stock; } 之后,就可以声明 Product[] public products 来维护整个货架,或 mapping(address => Product) 记录每个用户的资料。访问时用点号:product.price。结构体是值类型与引用类型的混合体——放在 storage 里会占据连续槽位,放在 memory 里则只是一张用完即弃的临时草稿纸。

机器还需要一个“状态旋钮”:营业中、补货中、维护中,三选一且只能选一个。枚举 enum 正是为此而生:enum Status { Open, Refilling, OutOfService },内部实际上是一个从 0 开始编号的整数(默认 uint8),可以转成 uint 比较,也可以作为状态机转移的条件。把 enum 与 struct 结合,就能优雅地实现订单生命周期这类复杂状态管理。

信息卡与旋钮都齐了,这台售货机功能上已经完整。但每一次开柜、找零、补货都要耗电——电费就是 gas。想让它长期低成本运转,接下来得研究怎么省电。

Status  =^  {营业中, 补货中, 维护中}  ↔  {0,1,2}\text{Status} \;\widehat{=}\; \{\text{营业中},\ \text{补货中},\ \text{维护中}\} \;\leftrightarrow\; \{0,1,2\}
枚举就是售货机的档位旋钮:看似三个名字,底层只是三个连续整数,且因为编号很小,编译器会偷偷用最小的 uint8 来存储,省下宝贵的存储空间。
  1. 枚举值集合三个互斥的命名常量,就像旋钮的三个档位,同一时刻只能停在其中一个上。
    {Open,Refilling,OutOfService}\{\text{Open}, \text{Refilling}, \text{OutOfService}\}
  2. 一一对应每个档位对应唯一的整数编号,从 0 开始依次递增。
    Open=0, Refilling=1, OutOfService=2\text{Open}=0,\ \text{Refilling}=1,\ \text{OutOfService}=2
  3. 存储宽度编号不会超过 255,所以编译器默认用最小的 uint8 存放,相当于用 1 字节旋钮而不是 32 字节大表盘。
    {0,1,2}⊂{0,…,255}=uint8 的值域\{0,1,2\} \subset \{0,\dots,255\} = \text{uint8 的值域}

gas 优化技巧

售货机每执行一个动作,物业都要交一笔电费:开一次仓门、亮一次显示屏、打一张小票。区块链上这笔“电费”叫 gas,由所有验证节点一起支付算力成本。gas 不是按代码行数收费,而是按“动了多少次仓库”收费——搞清楚电表怎么走,才能对症下药。

存储读写是 gas 大头:往一个新的存储槽写值(SSTORE)约消耗 20000 gas,修改已有槽约 5000,读取(SLOAD)约 100;而内存读写几乎可以忽略。所以第一条黄金法则是:能少写存储就少写,绝不在循环里反复写仓库,而是先在内存算完再一次性落盘。

第二条法则是“槽位打包”。每个存储槽是 32 字节,一个 uint256 刚好占满整槽;但五个 uint8 合起来也是 32 字节——把它们声明在一起,编译器就会自动塞进同一个槽,写一次只花一份 SSTORE 的钱。此外,常量用 constant/immutable 修饰会在编译期替换或部署时写入,不进普通存储;函数参数尽量用 calldata 而非 memory,省去拷贝开销。这些技巧叠加起来,往往能把部署和调用成本砍掉一大半。

电费降下来,售货机终于能高效运转。但省电的同时,也引来了小偷的注意:趁找零口打开的一瞬间反复伸手,或在计费表即将翻转到 999 时做手脚——下一节讲的安全锁,是这台机器最后的防线。

G≈20000×S写+100×S读,N槽=⌈B32⌉\displaystyle G \approx 20000 \times S_{\text{写}} + 100 \times S_{\text{读}}, \qquad N_{\text{槽}} = \left\lceil \frac{B}{32} \right\rceil
电费账单可以这样估算:每写一个新仓格收 20000 gas,每读一次收 100 gas。而所有变量占的字节数除以 32 向上取整,就是占用的仓格数——打包的目标就是让这个数字变小。
  1. 写入成本项每写一个新的存储槽,约付 20000 gas,是整个账单里最大的一笔。
    20000×S写20000 \times S_{\text{写}}
  2. 读取成本项每读一个存储槽约 100 gas,写比读贵两个数量级,所以要尽量“少写多算”。
    100×S读100 \times S_{\text{读}}
  3. 槽位数计算所有变量占用的总字节数 B 除以 32 向上取整就是槽数,打包小变量可以让 N 明显变小。
    N槽=⌈B/32⌉N_{\text{槽}} = \lceil B / 32 \rceil

安全陷阱防护

售货机生意兴隆后,小偷开始研究机器的漏洞。最经典的招数是“找零口抖动”:机器先吐币、后记账,小偷就在币吐出的瞬间不断触发退货,让机器误以为钱还没付。另一种是“计费表回零”:计费表只有三位,999 再加 1 就变成 000,钱货记录瞬间错乱。前者叫重入攻击,后者叫整数溢出。

Solidity 0.8 之后,默认的加减乘除在溢出时会自动回滚交易,相当于给计费表装了防回零弹簧;但在 unchecked { } 块里仍可关闭检查以求省 gas,此时必须由开发者自己保证不溢出,比如要求 a + b >= a。uint8(255) + 1 这类回绕一旦发生,就可能让资产数额在存储中悄悄颠倒。

对付重入攻击,行业标准是“检查-效果-交互”模式:先校验条件(require),再立即更新内部状态(把余额归零),最后才调用外部合约转账或回调。必要时加上互斥锁,用一个 locked 字段在函数入口挡住重入,就像给找零口加装单向翻板。权限控制同样重要:只有管理员能改价格、提现,必须用 require(msg.sender == owner) 把钥匙锁进保险柜。

装上这三把锁——防回零的溢出检查、防抖动的重入守卫、防偷配的权限控制,这台售货机才算真正坚不可摧。回想整个搭建过程:身份卡、储物柜、货架、商品卡、电费表、防盗锁,每一块都是 Solidity 智能合约的日常必修课。

a+b≥a⏟溢出校验且locked≥1⏟重入守卫\displaystyle \underbrace{a + b \geq a}_{\text{溢出校验}} \quad \text{且} \quad \underbrace{\mathrm{locked} \geq 1}_{\text{重入守卫}}
两把锁:加法和若小于任一加数,说明计费表已经回零出错;locked 等于 1 时机器正在执行关键操作,此时任何再次进入的调用都会被挡在门外。
  1. 溢出校验不等式无符号整数正常加法一定满足“和大于等于任一加数”,一旦违反就说明发生了回绕。
    a+b≥aa + b \geq a
  2. 重入守卫条件locked 初值为 0,进入函数前先要求锁未被占用,进入后立即置 1,执行完再归 0;期间任何重入调用看到锁为 1 都会回滚。
    locked∈{0,1},执行中⇒locked=1\mathrm{locked} \in \{0,1\},\quad \text{执行中} \Rightarrow \mathrm{locked} = 1

AI 应用场景

DeFi 借贷协议开发

阶段:链上资金逻辑与数据建模

解决的问题:传统 P2P 借贷依赖中介担保,流程慢、成本高、不透明。用智能合约把借据和抵押品数字化:每个地址一个仓位,超额抵押自动清算,全天候无人值守。

具体案例:在 Compound 风格协议中,mapping(address => Position) 记录用户仓位,Position 是含抵押量、负债、清算阈值的 struct;enum LoanState 表示可借、清算中、已平仓;存款函数用 payable 接收 ETH;全部逻辑按“检查-效果-交互”排序,杜绝重入导致的双花。

NFT 盲盒铸造与白名单

阶段:链上资产发行与权限控制

解决的问题:抢购场景防机器人刷单、防合约重入导致盲盒多发、防白名单被冒用。

具体案例:白名单用 mapping(address => bool) whitelist 记录,mint 前 require 校验;盲盒编号用动态数组存储随机种子,struct Metadata 记录稀有度与外观字段;mint 函数用 payable 收款。攻击者尝试在转账回调中重入时,被 locked 标志拦回。

去中心化 AI 推理结果存证

阶段:链上可验证计算与数据溯源

解决的问题:AI 推理结果上链后如何防篡改、如何防止推理节点重复领取奖励(重放与重入风险)。

具体案例:推理网络把模型版本号与输入哈希作为 mapping 键,存储推理结果与置信分;任务完成时按“检查-效果-交互”先标记任务结束再发放代币奖励,杜绝节点利用外部回调二次领奖;struct 记录模型指纹与算力参数,供链上审计追溯。

Python 演示

python73 行
import hashlib  def keccak256(data: bytes) -> int:    return int.from_bytes(hashlib.sha3_256(data).digest(), "big")  class MiniEVM:    def __init__(self):        self.storage = {}        self.gas_used = 0     def read(self, slot: int) -> int:        self.gas_used += 100        return self.storage.get(slot, 0)     def write(self, slot: int, value: int) -> None:        if self.storage.get(slot, 0) != value:            self.storage[slot] = value            self.gas_used += 20000  def mapping_slot(pointer: int, key: int) -> int:    p = pointer.to_bytes(32, "big")    k = key.to_bytes(32, "big")    return keccak256(p + k)  def pack_uint8(items):    slots = {}    for i, v in enumerate(items):        slot = i // 5        shift = (i % 5) * 8        slots[slot] = slots.get(slot, 0) | (v << shift)    return slots  class Vault:    def __init__(self):        self.locked = 0        self.balances = {"alice": 100}     def withdraw(self) -> int:        assert self.locked == 0, "reentry blocked"        self.locked = 1        amount = self.balances["alice"]        self.balances["alice"] = 0        self.locked = 0        return amount  def main():    evm = MiniEVM()    slot = mapping_slot(1, 42)    evm.write(slot, 999)    print("mapping 查询:", evm.read(slot))     plain = MiniEVM()    for i, v in enumerate([255, 1, 2, 3, 4]):        plain.write(i, v)    print("未打包 5 个 uint8 的 gas:", plain.gas_used)     packed = MiniEVM()    for slot, v in pack_uint8([255, 1, 2, 3, 4]).items():        packed.write(slot, v)    print("打包后 5 个 uint8 的 gas:", packed.gas_used)     pool = Vault()    print("取款:", pool.withdraw(), "锁状态:", pool.locked)  if __name__ == "__main__":    main()
目的:用 Python 模拟以太坊 EVM 的存储槽寻址与 gas 计费,复现 mapping 哈希定位、uint8 槽位打包和重入锁三个核心机制,运行后可直接对比打包前后的 gas 差异。
  1. def keccak256(data: bytes)用 SHA3-256 模拟 Solidity 的 keccak256,是定位存储槽的“磨钥匙机”
  2. self.gas_used += 20000模拟一次新槽写入的 SSTORE 电费,只有值变化才收费
  3. return keccak256(p + k)复刻 mapping 槽位公式:键与声明指针拼接后哈希
  4. slot = i // 5每 5 个 uint8 共用一个槽,8 位 × 5 = 40 位,远小于 256 位
  5. slots.get(slot, 0) | (v << shift)按位移把第 i 个字节拼进同一个 32 字节槽
  6. assert self.locked == 0重入锁入口检查,锁上后再次进入直接抛异常

常见误区

误区mapping 应该和 Python 字典差不多吧?我可以用 for 循环把里面所有键都打印出来,还能直接拿 length 看有多少条记录。

正解售货机储物柜只支持“报号开柜”,没有“把全部柜门扫一遍”的总目录。mapping 没有 length、没有 keys()、不能遍历,访问不存在的键也不会报错,只会返回默认值。想遍历,必须额外用一个动态数组把所有键记下来,再逐个查询——这是与 Python dict 最本质的差异。

为何常见:绝大多数编程语言的哈希表都支持迭代和求长度,初学者把惯性认知直接迁移到 Solidity,忽略了链上存储“按需寻址”的设计约束。

误区address 类型自带余额,那它应该天然就能给对方转账,为什么编译器还报错?

正解普通 address 只是“取货卡”,只有升级成 address payable 才带投币口。必须显式调用 payable(a) 转换后,才能使用 transfer 或 send。这是 Solidity 用类型系统强行划分“能否动钱”的权限边界,一旦写错,编译期就会被拦下,保护了开发者。

为何常见:直觉上“地址就是钱包”,忽略了收款能力被单独建模成 address payable 类型的这一设计。

误区uint8 最大 255,那 255+1 应该等于 256 呀,跟数学课上学的一样。

正解售货机计费表只有三位,转到 999 后再加 1 就归零。Solidity 0.8 之前 uint8(255)+1 会静默变成 0;0.8 之后默认抛错回滚,但在 unchecked 块里仍会回绕。写安全合约时永远要假设“它可能溢出”,必要处手动校验。

为何常见:Python 等语言的整数无限大,人们默认数学直觉成立,意识不到定长二进制整数的回绕特性。

误区函数写得越短、注释删得越多,gas 自然就省下来了。

正解电费单来自“每动一次仓库”:写新存储槽约 20000 gas、改槽约 5000、读槽约 100,而代码行数几乎无关。把 5 个 uint8 打包进一格、把循环内的写存储挪到内存算完再落盘,才是真正的省钱方案。

为何常见:传统编程优化看代码量和时间复杂度,链上优化看的是存储访问频率与槽位利用率,两者视角完全不同。

误区我的合约不收币也不直接转币,只是更新一下状态,应该没有重入风险吧?

正解任何调用外部合约的操作——比如调用某个 NFT 的 safeTransferFrom——都可能在执行中回调你的合约。攻击者趁你的内部状态还没更新,反复进出函数。规范做法是“检查-效果-交互”:先校验、再改状态、最后做外部交互,必要时加重入锁。

为何常见:把重入理解成“转钱瞬间被抢”,忽略了外部调用本身自带回调面,只要有一次对外交互就存在重入窗口。

面试题

Q1. Solidity 中 address 与 address payable 的核心区别是?

单选题 · easy

  • A.普通 address 也可以收发 ETH
  • B.只有 payable 能接收与转账← 正确答案
  • C.两者在功能上完全相同
  • D.payable 类型占用的字节更少

答案:B。address 像“取货卡”,address payable 是带投币口的收款卡,只有 payable 才能调用 transfer/send 收钱转账,普通 address 必须经 payable(a) 显式升级。选项 A、C 混淆了权限,D 混淆了存储大小——两者都是 20 字节。

Q2. 关于 mapping 类型,下列说法正确的是?

单选题 · mid

  • A.可以用 length 获取元素个数
  • B.可以像数组那样整体遍历键
  • C.只能按键查询无法遍历全部← 正确答案
  • D.键值对会按插入顺序存储

答案:C。mapping 如同凭号开柜的储物柜墙,没有总目录,只能按键查值;无 length、不可遍历、不记录插入顺序。这正是它与 Python dict 最本质的区别,选项 A、B、D 都是对哈希表通用特性的误移。

Q3. 下列哪些操作会消耗较多的 gas?

多选题 · mid

  • A.读取存储中的已有变量
  • B.写入一个全新的存储槽← 正确答案
  • C.修改一个已存在的存储槽← 正确答案
  • D.读取内存中的临时数组

答案:BC。存储读写是 gas 大头:新写槽约 20000、改槽约 5000,读槽约 100,而内存数组读写几乎免费。A 是冷读仍属存储读取,D 基本可忽略。B、C 分别对应 SSTORE 的新写与改写,是优化时要重点盯防的账单。

Q4. enum Status { Open, Locked, Closed } 中 Locked 的整数值是?

单选题 · easy

  • A.0
  • B.1← 正确答案
  • C.2
  • D.3

答案:B。枚举就像售货机的档位旋钮,从 0 开始编号:Open=0、Locked=1、Closed=2,与 C 语言枚举一致。因为编号很小,编译器默认用 uint8 存储,为合约省下空间。

Q5. 动态数组 arr.push(x) 的返回值是什么?

单选题 · mid

  • A.被推入的元素 x 本身
  • B.数组 push 后的新长度← 正确答案
  • C.数组起始存储槽的编号
  • D.返回空值没有任何信息

答案:B。push 像往货架尾塞货,返回值是塞完后的新长度,方便拿到最新下标。元素本身需用 arr[length-1] 访问;起始槽位是存储布局问题,与 push 的返回值无关,因此 B 正确。

Q6. 下列哪些做法能降低合约的 gas 消耗?

多选题 · hard

  • A.多个小类型变量打包进一槽← 正确答案
  • B.在循环里反复写入存储槽
  • C.函数参数用 calldata 声明← 正确答案
  • D.把常量声明为 constant 修饰← 正确答案

答案:ACD。打包槽位能减少 SSTORE 次数,calldata 免去拷贝到内存的消耗,constant 在编译期替换、不占存储槽。B 恰恰相反:循环内反复写存储是最烧 gas 的反模式,应先在内存算完再一次性落盘。

Q7. 关于重入攻击,下列哪种防御策略最正确?

单选题 · mid

  • A.先转账给调用者再更新状态
  • B.检查-效果-交互顺序执行← 正确答案
  • C.给所有函数加上 payable 修饰
  • D.把关键函数统一改成 internal

答案:B。正确顺序是先检查条件、再更新内部状态、最后才与外界交互,必要时配重入锁。A 是先交互后更新,恰是攻击温床;C 的 payable 只决定能否收币,D 的 internal 只是可见性,都与重入无关。

Q8. mapping 中某个键的存储槽位是如何确定的?

单选题 · hard

  • A.直接使用键数值作为槽位编号
  • B.由编译器随机分配的存储位置
  • C.对键与声明槽位做 keccak256← 正确答案
  • D.按插入顺序连续递增到下一格

答案:C。槽位 = keccak256(key 与 mapping 声明槽位指针的拼接),这是一种确定性哈希寻址:同一键永远映射到同一槽,且不同 mapping 之间不会互相覆盖。它不是顺序存储,也不可能是随机分配,否则数据将无法找回。

Q9. payable 修饰函数的主要作用是?

单选题 · easy

  • A.让函数可以接收 ETH 转入← 正确答案
  • B.让函数执行时完全不消耗 gas
  • C.让函数只能由合约自己调用
  • D.让函数自动把余额分发给所有人

答案:A。payable 相当于给函数打开投币口,让交易可以附带 ETH 进入合约,是接收资金的必要条件。B 的免 gas 不存在于任何函数;C 是 internal/private 的职责;D 与 payable 无关。

Q10. Solidity 0.8 之后关于整数溢出的说法正确的是?

多选题 · hard

  • A.默认溢出会触发交易回滚← 正确答案
  • B.unchecked 块内不检测溢出← 正确答案
  • C.溢出后结果自动变为浮点数
  • D.溢出时编译器会自动重试

答案:AB。0.8 起默认开启溢出检查,溢出即回滚;在 unchecked 块中可关闭检查让结果回绕,这常被用于 gas 优化场景。C 是错误想象——链上整数不会自动升级为浮点;D 的自动重试也不存在,交易只会回滚或回绕。