Solidity 初级面试题
以下是 1–12 题 的简要答案汇总,供快速回顾:
1. 私有、内部、公共和外部函数之间的区别?
| 可见性 | 当前合约 | 派生合约 | 外部(合约/账户) | 备注 |
|---|---|---|---|---|
private | ✅ | ❌ | ❌ | 仅当前合约 |
internal | ✅ | ✅ | ❌ | 默认可见性 |
public | ✅ | ✅ | ✅ | 自动生成 getter |
external | ❌* | ❌* | ✅ | 内部需用 this.f() 调用 |
2. view 和 pure 函数有什么区别?
view:可读取状态变量,但不能修改。pure:既不读取也不修改状态,仅依赖参数或常量。
3. 1 ether 相当于多少个 wei,多少个 gwei?
- 1 ether = 10¹⁸ wei
- 1 ether = 10⁹ gwei
4. Solidity 0.8.0 版本对算术运算的重大变化?
默认启用算术溢出检查。加、减、乘、除、自增、自减等溢出时直接 revert(不再静默环绕)。需要旧版环绕行为需写在 unchecked { ... } 块中。
5. 实现 allowlist 用映射还是数组更好?
映射 (mapping(address => bool)) 更好:
- 成员检查 O(1),数组需要遍历 O(n)
- 增删方便且 gas 可预测
- 若需遍历所有地址,可额外维护数组 + 映射组合。
6. 以太坊主要使用什么哈希函数?
Keccak‑256(常被误称为 SHA‑3)。
7. assert 和 require 有什么区别?
require | assert | |
|---|---|---|
| 用途 | 输入验证、条件检查 | 内部错误、不变式检查 |
| 失败后 Gas | 返还剩余 gas | 消耗所有剩余 gas |
| 错误消息 | 支持自定义字符串 | 不支持 |
| 适用场景 | 可预期的拒绝 | 不应发生的 bug |
8. 智能合约最大大小?
24,576 字节(24 KB),由 EIP‑170 规定。
9. 为什么 Solidity 废弃了 years 关键字?
因为 1 years 被固定为 365 天,无法处理闰年,容易误导时间计算。改用 365 days 显式表达。
10. Solidity 提供哪些关键字来测量时间?
时间单位:seconds,minutes,hours,days,weeks。
全局变量:block.timestamp(秒为单位的时间戳)。
11. 存储 -1 的 int256 变量十六进制如何表示?
0xffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff
(64 个 f,所有位为 1)
12. uint256 最大值及获取方式?
最大值 = 2²⁵⁶ − 1 =
115792089237316195423570985008687907853269984665640564039457584007913129639935
获取:type(uint256).max(Solidity ≥0.8.0)
13. uint8、uint32、uint64、uint128、uint256 都是有效的 uint 大小,还有其他的吗?
有。Solidity 支持任何以 8 为步长、从 8 到 256 位的无符号整数:
uint8, uint16, uint24, uint32, uint40, …, uint256。
但开发中常用的是 uint8, uint16, uint32, uint64, uint128, uint256,其余较少见。
14. 为什么 Solidity 不支持浮点数运算?
因为以太坊要求所有节点计算结果完全一致(确定性)。浮点数标准(如 IEEE 754)在不同平台或编译器优化下可能产生微小差异,导致共识失败。Solidity 采用定点数(整数)和自定义小数位来模拟精度。
15. fallback 和 receive 之间有什么区别?
| 函数 | 触发条件 |
|---|---|
receive() | 调用数据为空(msg.data.length == 0)且合约收到纯以太转账。 |
fallback() | 任何其他不匹配函数签名的情况(包括有数据的调用,或没有 receive 时的空数据转账)。 |
两者都需标记 external payable。如果合约只写了 fallback 而没有 receive,空数据转账仍会走 fallback。
16. Solidity 中的修饰符 modifier 有什么作用?
代码复用和前置/后置检查。常用于:
- 访问控制(如
onlyOwner) - 输入验证
- 重入锁(
nonReentrant) - 函数执行前后的统一操作
修饰符可以接收参数,并按顺序叠加使用。
17. 有哪些方式可以向智能合约中存入以太币?
- 直接调用
receive()或fallback()(通过转账) - 调用
payable函数 并附带以太币 - 使用
transfer(address.transfer(amount),2300 gas,失败 revert) - 使用
send(类似transfer,但返回 bool,不推荐) - 使用
call{value: amount}("")(推荐,灵活控制 gas) selfdestruct(强制向指定地址发送余额,非自愿存款)- 挖矿奖励(仅对矿工地址)
18. Solidity 访问控制有哪些,有什么用?
常见模式:
onlyOwner:限制只有合约所有者调用(使用msg.sender检查)- 基于角色(如 OpenZeppelin
AccessControl):DEFAULT_ADMIN_ROLE、MINTER_ROLE等 - 时间锁(
TimelockController) - 修饰符 +
require自定义逻辑
作用:防止未授权调用,提升安全性。
19. 运行以太坊独立验证节点所需的最小以太数量是多少?
32 ETH(以太坊 PoS 质押的最低要求)。只有质押 32 ETH 才能激活自己的验证节点(独立节点)。
20. 以太坊什么机制阻止了无限循环的永远运行?
Gas 机制。每笔交易有 gasLimit,每条 EVM 指令消耗 Gas。无限循环会持续消耗 Gas 直到用尽,交易失败并回滚,但不会永远运行。
21. 在 EIP-1559 之前,如何计算以太坊交易的美元成本?
公式:
交易成本 (USD) = gasPrice (Gwei) × gasUsed × 1e-9 × ETH/USD 价格
其中 gasPrice 由用户竞价(第一价格拍卖),gasUsed 由交易复杂度决定。
22. 上海升级后,每个区块的 gas 限制是多少?
没有硬性固定值,由验证者动态投票调整。当前主流值约为 30,000,000 gas(三千万)。上海升级本身没有改变 gas 限制的调整机制。
23. 在一个智能合约中调用另一个智能合约时可以转发多少 gas?
默认情况下,<address>.call{value: 0}("") 会转发当前剩余的所有 gas(除了少量内部消耗)。也可以手动指定 {gas: 50000} 限制。
最大不能超过交易剩余的 gas 减去调用开销。
24. ERC20 合约中的 transfer 和 transferFrom 有什么区别?
| 函数 | 调用者 | 移动谁的代币 | 需要授权 |
|---|---|---|---|
transfer | 代币持有者本人 | 调用者自己 | 无需授权(直接操作自己的余额) |
transferFrom | 第三方(或本人) | 从 from 地址转给 to | 需要 from 先批准额度给调用者 |
25. 在区块链上如何使用随机数?
区块链本身是确定性的,没有真正的随机数。常用方法:
- 预言机随机数(如 Chainlink VRF)—— 推荐,安全可靠
- 区块哈希 + 工作量证明(如
blockhash(block.number-1),但可被矿工操纵) - 承诺-揭示(Commit-Reveal)方案
- RANDAO(以太坊信标链内置)
绝对避免:block.timestamp、block.difficulty 等可被矿工影响的值做关键随机数。
26. 什么是检查-效果-交互模式(Checks-Effects-Interactions)?
一种防止重入攻击的编程模式:
- 检查(Checks):先用
require验证条件。 - 效果(Effects):更新合约内部状态(如减少余额)。
- 交互(Interactions):最后进行外部调用(如转账)。
这个顺序确保在外部调用发起前,所有状态已修改,即使攻击者再入,也无法重复利用旧的余额。
27. create 和 create2 之间的区别?
| 特性 | create | create2 |
|---|---|---|
| 地址计算 | keccak256(deployer_address, nonce) | keccak256(0xff, deployer_address, salt, init_code_hash) |
| 确定性 | 依赖 nonce,不可预测(nonce 递增变化) | 完全确定性,只要参数相同,地址不变 |
| 主要用途 | 普通部署 | 反事实部署、工厂模式预测地址、无交易部署 |
28. 代理合约需要哪种特殊的 call 才能工作?
delegatecall。
代理合约使用 delegatecall 将函数调用转发给逻辑合约,同时保留代理自己的存储(msg.sender 和 msg.value 也保留)。实现逻辑与存储分离。
29. tx.origin 和 msg.sender 有什么区别?
msg.sender:直接调用者(可能是外部账户或另一个合约)。tx.origin:原始发起交易的外部账户(总是 EOA)。
示例:用户 A → 合约 B → 合约 C。
在合约 C 中,msg.sender 是合约 B,tx.origin 是用户 A。
安全警示:切勿使用
tx.origin做身份验证,容易受钓鱼攻击。
30. abi.encode 和 abi.encodePacked 有什么区别?
abi.encode:将所有参数按 ABI 规范填充为 32 字节的倍数,末尾填充零,适合标准解码(abi.decode)。abi.encodePacked:紧密打包,不填充,更省空间但不唯一,可能产生哈希碰撞(例如encodePacked("a","bc")与encodePacked("ab","c")相同)。
使用场景:encodePacked 适合做 keccak256 哈希时节省 gas,但需注意碰撞风险。
31. 根据 Solidity 编程风格,函数应该如何排序?
推荐顺序(按可见性和类型分组):
- 构造函数 (
constructor) receive函数fallback函数- 外部函数 (
external) - 公共函数 (
public) - 内部函数 (
internal) - 私有函数 (
private) 在每个组内可再按逻辑或字母顺序排列。
32. 根据 Solidity 编程风格,函数修饰符应该如何排序?
修饰符的顺序就是执行顺序(从左到右)。
例如:
function withdraw() public onlyOwner nonReentrant { ... }
先执行 onlyOwner,再执行 nonReentrant,最后执行函数体。
风格建议:将更通用的修饰符放前面,更具体的放后面。
33. Solidity 中整数除法是不是遵循四舍五入?
不是。Solidity 整数除法向零截断(floor 对于正数,ceil 对于负数,但通常只考虑无符号整数)。
例如:
5 / 2 = 2(不是 2.5,也不是四舍五入的 3)7 / 3 = 2
没有内置的四舍五入,需自己用公式(x + y/2) / y模拟。