remix编译运行solidity
上一篇: Remix 部署 Solidity 合约的完整标准操作步骤
上一篇文章写了一个计数器,先+1,然后-1,并且在remix成功地编译、部署了。
Counter(计数器)
功能:
- increment()
- decrement()
- reset()
- get()
要求: 使用 event
编译、部署、在 Remix 中交互测试
counter.sol
1 | |
remix部署后的日志
在remix里输出了
很好,这说明你的 Counter 合约已经成功部署 在 Remix 的虚拟机环境(VM)中了。我们可以来解读一下你看到的输出信息:
1️⃣ [vm] from: 0x5B3...eddC4
- 这是 部署者地址,也就是你在 Remix 右上角选择的账户地址。
- 说明这个地址发起了合约部署交易。
2️⃣ to: Counter.(constructor)
- 表示这是一个 构造函数交易,即部署合约时自动执行的
constructor。 - 因为 Counter 合约没有特别写 constructor,所以是默认的空构造函数。
3️⃣ value: 0 wei
- 表示部署交易中 没有发送 ETH,即部署合约不带以太币。
- 这是正常的,Counter 合约不需要接收 ETH。
4️⃣ data: 0x608...e0033
- 这是部署交易的 字节码(Bytecode),Solidity 编译器生成的 EVM 可执行代码。
- 用于在区块链上创建合约实例。
5️⃣ logs: 0
- 表示部署合约的交易 没有触发任何事件
- 正常,因为构造函数里没有
emit事件。
6️⃣ hash: 0x39b...a9e86
- 这是交易的 哈希值(Tx Hash)
- 可以用它在区块浏览器或者 Remix 的 Transactions 面板查到部署记录。
7️⃣ 绿色对号 ✅
- 表示 部署成功,合约已经在虚拟机(VM)上可用。
- 你可以在 Remix 左侧 Deployed Contracts 面板看到 Counter 合约实例,开始调用函数测试
increment、decrement等。
💡 下一步建议:
- 点击
increment()→ 再看get()返回值 - 点击
decrement()→get()返回值减 1 - 点击
reset()→get()返回 0 - 在 Logs/Console 面板查看事件输出(比如
CounterChanged)
在 Remix 中交互测试

这些步骤是否要交gas费
是的,在 Remix 中调用某些函数确实会消耗 gas,但具体情况要看函数类型:
1️⃣ 需要消耗 gas 的情况
所有 修改区块链状态的函数 都需要 gas,因为它们会生成交易并写入区块链。
在你的 Counter 合约中:
increment()✅decrement()✅reset()✅
这些函数都会修改
count的值,因此每次调用都会消耗 gas,并在 Remix 的 VM 中生成一笔交易。
2️⃣ 不需要消耗 gas 的情况
只读函数(
view或pure)不会修改区块链状态,不会产生交易,也不消耗 gas。在你的 Counter 合约中:
get()✅
调用
get()只会返回当前计数值,不会消耗 gas,也不会触发交易。
3️⃣ 解释 Remix 的虚拟机(VM)环境
- 在 JavaScript VM 或 Injected Web3 本地测试中,你虽然仍然“消耗 gas”,但它只是模拟消耗,不需要真实支付 ETH。
- 在 真实以太坊主网 部署时,修改状态的函数才会消耗真实 ETH gas。
💡 总结表格:
| 函数 | 修改状态 | 是否消耗 gas | 备注 |
|---|---|---|---|
| increment() | 是 | 是 | count +1 |
| decrement() | 是 | 是 | count -1 |
| reset() | 是 | 是 | count = 0 |
| get() | 否 | 否 | 只读取 count 值 |
如果你愿意,我可以帮你画一个 “哪些函数消耗 gas,哪些函数不消耗 gas 的示意图”,直观显示 Counter 合约调用成本。
你希望我画吗?
Bank(简单存取款)
功能:
- deposit() payable
- withdraw(amount)
- balanceOf(msg.sender)
要求:
- mapping(address => uint)
- require 校验
- msg.value
- 防止取款超过余额
1 | |
存款

取款

我输入1,然后点transact,点了几下,结果存款balance变成了Balance: 2.999999999999999997 ETH,为什么存款不是2或者1?
你输入的 1,在 Remix 中默认被解释成 1 wei,而不是 1 ETH。
在 Solidity 中,所有的整数字面量与金额参数 默认都是 wei。
存款 deposit() 时,你在 Value 栏 输入的数字是以太(你选择了 ether)
但 withdraw(amount) 的输入框 没有单位选择,所以输入 1 表示 1 wei(非常小)
wei和eth的单位是怎样的关系
1 ETH = 1,000,000,000,000,000,000 wei(10^18 wei)
也就是:
1 ETH = 10^18 wei
ETH 的完整单位表(最常用)
单位名称 与 wei 的关系 与 ETH 的关系
wei 1 wei 10^-18 ETH
gwei 10^9 wei 10^-9 ETH
ether/ETH 10^18 wei 1 ETH
✅ 为什么 Solidity 一律使用 wei?
因为:
整数精度高
小数可能导致严重的精度问题
以太坊的设计理念就是所有金额必须精确,不允许浮点数
因此:
所有金额都用 wei 存储,只有用户界面使用 ETH 显示
1后面有18个0,怎么读?一亿?
“1 后面有 18 个 0” 的正确读法不是“一亿”。
一亿是 1 后面 8 个 0。
你说的这个数字是:
1 | |
它的中文正确读法是
✅ 一百京(yī bǎi jīng)
因为:
| 数字级别 | 0 的数量 | 单位 |
|---|---|---|
| 万 | 4 | 万 |
| 亿 | 8 | 亿 |
| 兆 | 12 | 兆 |
| 京 | 16 | 京 |
| 垓 | 20 | 垓 |
你的是 10^18,属于:
1 | |
也就是:
✅ 10^18 = 一百京(1 百京)
✅ 关于 ETH 中的 1e18
在 Solidity 中:
1 | |
所以:
1 ETH 的 wei 数量为 “一百京 wei”。
1 wei = 一百京分之一 eth
总结
我已经知道了pragma可以指定solidity编译器的版本, contract可以定义合约的名词,然后需要用花括号括起来,里面可以用function关键字定义函数名
emit是触发一个事件,可以把内容更新到以太坊网络上,并且需要消耗gas费
1.public是什么意思?
function withdraw(uint amount) public {
public 是 Solidity 中 函数和变量的可见性(visibility)修饰符之一,用来控制外部是否可以调用该函数。
在你的例子里:
1 | |
表示 withdraw() 这个函数对所有人开放,任何账户都可以调用。
✅ Solidity 的可见性修饰符(Visibility Specifiers)
Solidity 有 4 种可见性:
| 修饰符 | 外部能否调用 | 合约内部能否调用 | 子合约能否调用 |
|---|---|---|---|
| public | ✅ 可以 | ✅ 可以 | ✅ 可以 |
| external | ✅ 只能外部 | ❌ 不能 | ✅ 可以(外部调用方式) |
| internal | ❌ 不能 | ✅ 可以 | ✅ 可以 |
| private | ❌ 不能 | ✅ 可以 | ❌ 不可以 |
✅ public 的含义(函数)
当一个函数被标记为 public 时:
任何人都可以调用
- 用户
- 合约
- 甚至机器人脚本
- 区块链外部的交易
内部(合约内部)也可以调用
例如:- Withdraw 调用内部逻辑
- 其他函数可以复用这个函数
如果没有显式指定可见性,Solidity 会报警告
从 0.5.0 开始,必须明确指定可见性。
✅ public 的含义(变量)
如果你声明:
1 | |
Solidity 会自动生成一个 getter 函数:
1 | |
相当于自动写了一个 get 函数。
✅ withdraw(uint amount) 为什么要 public?
因为你的 Bank 合约中,这个函数是给用户调用的:
- 用户自己存款
- 用户自己取款
- 任何人都可以调用自己的余额取款
因此必须使用 public,否则用户无法从外部调用函数。
✅ 小结
public 表示:
- 对所有外部用户开放
- 对所有外部合约开放
- 合约内部也可以调用
- 属于最开放的访问级别
如果你愿意,我可以帮你把:
- public
- external
- internal
- private
做成一个可视化表格 + 简图,让你彻底掌握可见性机制。
2.Solidity 的 require
不是的,Solidity 的 require 和 Python 的 input() 完全不是一回事,两者功能完全不同,甚至属于不同类别的语句。
我用专业方式帮你区分清楚:
✅ require 的真实作用(Solidity)
require 是 运行时条件检查(runtime assertion)。
功能:
判断一个条件是否成立
如果不成立,立即 revert(回退交易)
回退后的效果:
- 本次交易不写入区块链
- 状态不改变(所有修改都被撤销)
- 返回提示信息
例如:
1 | |
意思是:
- 如果
amount <= 0,
整个 withdraw() 调用失败,
所有操作回滚(balances 不会减少,ETH 不会发送)。
它是一种 防御式编程(defensive programming) 机制,用来保护合约不被错误调用。
✅ Python 的 input() 做什么?
Python 的:
1 | |
是 从用户处读取输入的文本,等待用户从键盘输入。
功能:
- 用于交互式输入
- 接收字符串
- 不会执行任何验证,也不会终止程序
✅ 两者根本不是同一种用途
| 对比点 | Solidity require |
Python input() |
|---|---|---|
| 类别 | 条件验证语句(assert) | 输入函数 |
| 作用 | 检查条件,不满足就回退交易 | 接收用户的键盘输入 |
| 用于什么场景 | 安全保障、输入验证、防止错误 | 和用户交互收集字符串 |
| 会不会抛错 | ✅ 条件失败立刻 revert | ❌ 不会,它只是等待输入 |
| 是否影响程序状态 | ✅ 回退所有状态修改 | ❌ 不影响,程序继续运行 |
✅ require 的正确类比
require 更像 Python 中的:
1 | |
或者更接近:
1 | |
✅ 小结(非常重要)
Solidity 的 require 是:
安全检查 + 输入验证 + 交易保护机制
用于防止错误调用和恶意行为,确保合约安全。
Python 的 input() 是:
收集用户输入的函数。
两者用途完全不同。
如果你愿意,我可以继续帮你解释:
require和revert、assert的区别- 在智能合约中什么时候用 require,什么时候用 assert
- 如何写“安全的 withdraw 函数”最佳实践
你要继续深入哪一块?
3.Solidity 的 private修饰符
这是一个非常好的问题,说明你已经开始关注 访问权限、安全性、接口设计,这是智能合约开发中必须掌握的核心能力。
下面我用专业方式解释为什么:
✅ 为什么 Bank 合约中的余额 mapping 要用 private?
你的代码里是:
1 | |
之所以必须使用 private,主要有 三个专业原因:安全性、封装性、接口控制。
✅ 原因 1:public 会自动生成 getter,会暴露所有人的余额(隐私泄漏)
如果你写成:
1 | |
Solidity 会自动生成一个 public getter 函数:
1 | |
这会导致:
- 任何人可以查询任意地址的余额
- 这属于 隐私泄露,特别是银行类合约(Bank)不应允许公开所有用户余额
例如,在 DeFi 或钱包模型中,用户余额必须保护。
private 可以阻止自动 getter 的生成,从而避免隐私外泄。
✅ 原因 2:让用户通过 balanceOf(address) 访问,可以加入安全规则
你的合约提供了:
1 | |
为什么需要自定义?
因为你可以在自定义 getter 中加入检查、安全逻辑或未来扩展。
例如,你可以后来加入:
- 黑名单检查
- 访问权限控制
- 转账冻结检查
- 防止第三方读取余额
- 基于角色的访问
如果使用 public mapping,你就失去了控制能力。
✅ 原因 3:避免直接修改 mapping,保证封装性
如果 mapping 是 public:
- 外部没有办法修改 mapping(Solidity 不允许 public mapping 写入),
- 但内部代码容易不规范,逻辑变散。
使用 private:
- 所有余额修改必须通过 deposit / withdraw
- 所有入口统一由安全检查保护(require)
这样合约安全性更高。
✅ 原因 4:遵循最小公开原则(Principle of Least Exposure)
这是软件工程原则:
只暴露必要的接口,不暴露内部状态。
mapping 是核心状态,应当封装,而不是暴露给外部世界。
✅ 例子:如果用 public,会怎样?
1 | |
用户可以执行:
1 | |
直接查询任何人的余额。
这对金融类合约通常是不允许的。
✅ 结论(非常关键)
为什么用 private?
| 原因 | 内容 |
|---|---|
| ✅ 安全 | 不暴露所有人的余额,避免隐私泄漏 |
| ✅ 封装 | mapping 状态不暴露,防止外部干扰内部逻辑 |
| ✅ 接口控制 | 使用 balanceOf() 实现自定义访问规则 |
| ✅ 可扩展 | 未来可以在 balanceOf() 中添加权限、过滤、逻辑 |
| ✅ 专业实践 | 合约内部状态应尽量封装(最小公开原则) |
因此:
1 | |
是 正确、专业、安全的写法。
如果你愿意,我可以帮你:
✅ 画一张“Bank 状态封装结构图”
✅ 讲解 public / private / internal 访问权限最佳实践
✅ 讲解为什么 public mapping 容易导致漏洞
写一个 Struct + Mapping 组合的合约
struct.sol
1 | |

这是添加用户的功能,下面的getUser(),输入以太坊的地址,就可以返回用户名和分数。
这个代码说明了struct可以存储一组数据信息。
mapping可以将用户信息和地址进行映射。
struct 这个结构体,我感觉在c++里面学过
完全正确!Solidity 的 struct 和 C++ 里的 struct 非常相似,核心思想都是把一组相关的数据打包在一起,形成一个自定义类型。
对比一下:
C++:
1 | |
Solidity:
1 | |
几乎一模一样:
- 都可以包含多种类型的数据(字符串、数字、布尔等)。
- 都可以用来创建“对象”或存储数据集合。
不同点主要在 Solidity:
- 存储位置:
memory/storage/calldata对 struct 的处理很重要。 - 访问方式:通常和
mapping结合使用,用地址或 ID 快速访问。 - 不支持方法:Solidity 的 struct 不能像 C++ 的 class 一样有成员函数,只能用外部函数操作。
如果你想,我可以画一张 Solidity struct + mapping 的内存示意图,直观展示它是怎么存储和访问的,这样理解会更清晰。
你希望我画吗?
双向 Mapping 的合约
mapping.sol
1 | |

所谓双层映射就是地址A,地址B,是否关注。 这两个关系的映射。
如果关注了,也就是true,会返回以下结果
1 | |
有双层mapping,那么有没有三层mapping
有,而且 Solidity 支持任意层数的嵌套 mapping,不仅可以双层(mapping → mapping),也可以三层、四层、五层……理论上无限层,只要你愿意写。
- 三层 mapping 的定义示例
三层 mapping 的结构如下:
✅ 示例 1:三层 mapping(地址 → 地址 → 地址 → uint)
1 | |
结构相当于:
1 | |
可用来表达复杂的关系,比如:
- 用户 A → 项目 B → 任务 C → 分数
- 国家 → 城市 → 区域 → 数据
- 用户 → 商品 → 小分类 → 数值
✅ 示例 2:三层 mapping(地址 → uint → 地址 → bool)
1 | |
用途:
- 某个用户的第 N 个资源,对某个地址是否开放访问
- NFT 系列 → tokenId → 授权代理 → 是否允许
- 三层 mapping 的可视化示意图
假设结构:
1 | |
示意图:
1 | |
每一层都是 “字典中套字典”,直到最后一层的值。
- 带操作函数的三层 mapping 示例合约
你可以这样写:
1 | |
调用模式:
1 | |
- 实战建议:为什么不推荐超过两层?
理论上可无限嵌套,但实践中:
✅ 问题:
- 越多层越难读
- 越多层越难调试
- 越多层 gas 成本更高(访问每层都是一次 storage slot 读取)
- 越多层越容易写错 key 结构
✅ 建议:
- 两层 mapping 最常用(如 tokenOwner → approvals)
- 三层 mapping 有时必要(如用户 → tokenId → operator → 权限)
- 多于三层时,通常要考虑是否应该换成 struct 组合 mapping
如果你愿意,我可以帮你写:
- 一个真实项目中使用三层 mapping 的案例
- 设计一个更优雅的结构(避免过深嵌套)
- 或把你的业务逻辑改写成 struct + 双层 mapping,更易读
你想继续深入吗?
我感觉solidity比python要难
从工程角度来看,你的感觉非常正确,而且是大多数开发者在第一次接触 Solidity 时的共识:
Solidity 的学习曲线确实比 Python 更陡峭。
下面我给你系统地解释为什么 Solidity 比 Python 难,以及你应该如何降低学习难度。
- 为什么 Solidity 比 Python 难?
① Solidity = 区块链 + 编程语言
Python 只需要关注语言本身。
Solidity 需要你同时理解:
- EVM(以太坊虚拟机)
- Gas 成本
- 状态变量 + storage/memory/calldata
- 区块、交易、nonce
- Reentrancy(重入攻击)
- 安全性(非常核心)
- 合约间调用
- ABI、事件日志
- 钱包签名、msg.sender、msg.value 的语义
Python 是运行在本地机器上,Solidity 是运行在一台“人人都能看到且不能删改”的全球计算机上。
自然就复杂很多。
② Solidity 是“可被黑客攻击”的语言
在 Python 里写错一个变量,程序崩了,重新运行即可。
在 Solidity 里写错一行代码:
- 可能导致 钱被黑客盗走
- 可能导致合约永久无法升级
- 可能导致资金永久无法取出
- 链上的状态永远无法修改或回滚
所以 Solidity 的设计必须非常严格、偏底层、安全性强。
这就使得学习成本非常高。
③ Solidity 是“有限资源”的语言
- 链上存储最贵
- 每次操作都有 gas 消耗
- 不能无限循环
- 不能无限递归
- 不能存太大数据结构
- 不能随便用字符串,太贵
Python 完全没这些限制。
④ Solidity 的调试环境弱
Python:
- logging
- debugger
- breakpoint
- 快速重跑
Solidity:
- 不能用 print
- 调试全靠事件 event
- 修改状态需要重新部署合约
- 调试成本高得多
- 如何让学习 Solidity 变得轻松?
我建议你用这条路线:
✅ 第 1 阶段(1–2 周):只学基本语法
- 合约、函数、modifier
- struct + mapping
- storage/memory 的区别(你现在已经开始理解了)
- 事件
你目前做的练习(UserInfo、follow)完全正确,就是应该这样写小合约。
✅ 第 2 阶段(2–4 周):掌握常用设计模式
例如:
- AccessControl(权限控制)
- Ownable(onlyOwner)
- Pausable(暂停合约)
- Pull payment / withdraw pattern
- Checks-Effects-Interactions
这些比语法更重要。
✅ 第 3 阶段(1–2 月):学习攻击与防御
安全研究出身的你在这方面有绝对优势。
重点学习:
- reentrancy(重入攻击)
- integer overflow(溢出)
- delegatecall 攻击
- storage layout 劫持
- call injection
- frontrunning
- access control bypass
你学会了这些,你就是 80% 的 Auditor。
✅ 第 4 阶段(2–6 月):审计真实项目
- 看 OpenZeppelin 源码
- 看 Uniswap、Aave、Compound
- 阅读审计报告
- 自己动手复现 CTF 漏洞
你当前的节奏完全没问题。
- 用一句话回答你的感受
是的,Solidity 比 Python 难非常多,但难的部分主要是:
- 安全
- 状态机
- Gas 优化
- 区块链原理
而不是语法本身。
你现在做的小合约(UserInfo、follow)非常正确,是最佳入门方式。
如果你愿意,我可以继续给你安排:
- 更高级的 mapping 练习
- struct + mapping 的真实案例
- 写一个完整的“微博 Follow 系统合约”
- 或帮你做 2 个月的 Solidity 速成路线
你希望继续练习哪个方向?