提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档
文章目录
- [1. 为什么需要继承](#1. 为什么需要继承)
-
- [1.1 代码复用的问题](#1.1 代码复用的问题)
- [1.2 继承的解决方案](#1.2 继承的解决方案)
- [1.3 继承的实际应用场景](#1.3 继承的实际应用场景)
- [2. 单继承](#2. 单继承)
-
- [2.1 单继承基础语法](#2.1 单继承基础语法)
- [2.2 访问权限](#2.2 访问权限)
- [3. 多重继承](#3. 多重继承)
-
- [3.1 多重继承基础](#3.1 多重继承基础)
- [3.2 C3线性化算法](#3.2 C3线性化算法)
- [4. super关键字](#4. super关键字)
-
- [4.1 super的作用](#4.1 super的作用)
- [4.2 单继承中的super](#4.2 单继承中的super)
- [4.3 多重继承中的super](#4.3 多重继承中的super)
- [5. 构造函数继承](#5. 构造函数继承)
-
- [5.1 构造函数执行顺序](#5.1 构造函数执行顺序)
- [5.2 构造函数参数传递](#5.2 构造函数参数传递)
- [5.3 多重继承的构造函数](#5.3 多重继承的构造函数)
- [6. 函数重写](#6. 函数重写)
-
- [6.1 virtual和override关键字](#6.1 virtual和override关键字)
- [6.2 函数签名必须匹配](#6.2 函数签名必须匹配)
- [6.3 多重继承中的override](#6.3 多重继承中的override)
- [6.4 使用super在重写中调用父函数](#6.4 使用super在重写中调用父函数)
- [7. 抽象合约](#7. 抽象合约)
-
- [7.1 什么是抽象合约](#7.1 什么是抽象合约)
- [8. 接口](#8. 接口)
-
- [8.1 什么是接口](#8.1 什么是接口)
- [8.3 ERC20接口标准](#8.3 ERC20接口标准)
- [8.4 接口用于合约交互](#8.4 接口用于合约交互)
1. 为什么需要继承
1.1 代码复用的问题
在没有继承机制的情况下,开发者会遇到严重的代码复用问题。
问题场景:
假设你要创建三个代币合约:稳定币、治理代币、奖励代币。它们都需要:
- ERC20基本功能(transfer、approve等)
- 权限控制(只有owner可以执行某些操作)
- 暂停功能(紧急情况下暂停转账)
没有继承的做法:
bash
// 稳定币合约
contract StableCoin {
// 复制粘贴ERC20代码
mapping(address => uint256) public balanceOf;
function transfer(address to, uint256 amount) public { }
// 复制粘贴权限控制代码
address public owner;
modifier onlyOwner() { }
// 复制粘贴暂停功能代码
bool public paused;
modifier whenNotPaused() { }
function pause() public { }
}
// 治理代币合约
contract GovernanceToken {
// 又复制粘贴一遍所有代码...
mapping(address => uint256) public balanceOf;
function transfer(address to, uint256 amount) public { }
address public owner;
// ... 完全重复的代码
}
// 奖励代币合约
contract RewardToken {
// 再次复制粘贴...
}
这种做法的问题:
代码冗余:
-
三个合约有90%的代码相同
- 浪费存储空间
- 增加部署成本
-
维护困难:
- 发现bug需要修改三个地方
- 容易遗漏
- 一致性难以保证
-
升级麻烦:
- 添加新功能需要修改所有合约
- 无法批量更新
- 测试成本高
-
容易出错:
- 复制粘贴可能出错
- 某个合约可能用旧版本代码
- 难以追踪哪个版本最新
1.2 继承的解决方案
继承(Inheritance)是面向对象编程的核心特性,它允许一个合约(子合约)继承另一个合约(父合约)的属性和方法。
使用继承的做法:
bash
// 基础合约1:ERC20功能
contract BaseERC20 {
mapping(address => uint256) public balanceOf;
function transfer(address to, uint256 amount) public virtual returns (bool) {
require(balanceOf[msg.sender] >= amount);
balanceOf[msg.sender] -= amount;
balanceOf[to] += amount;
return true;
}
}
// 基础合约2:权限控制
contract Ownable {
address public owner;
constructor() {
owner = msg.sender;
}
modifier onlyOwner() {
require(msg.sender == owner, "Not owner");
_;
}
}
// 基础合约3:暂停功能
contract Pausable {
bool public paused;
modifier whenNotPaused() {
require(!paused, "Paused");
_;
}
function pause() internal {
paused = true;
}
}
// 子合约:继承所有功能
contract StableCoin is BaseERC20, Ownable, Pausable {
// 自动获得所有父合约的功能
// 只需要添加特有功能
function emergencyPause() public onlyOwner {
pause();
}
}
contract GovernanceToken is BaseERC20, Ownable, Pausable {
// 同样继承所有功能
}
contract RewardToken is BaseERC20, Ownable, Pausable {
// 同样继承所有功能
}
继承的优势:
-
代码复用:
- 公共功能只写一次
- 多个子合约共享
- 减少90%以上的重复代码
-
易于维护:
- bug修复只改一处
- 所有子合约自动受益
- 保证一致性
-
模块化设计:
- 功能分离清晰
- 每个合约职责单一
- 易于理解和测试
-
灵活扩展:
- 子合约可以添加新功能
- 可以重写父合约函数
- 组合不同功能模块
1.3 继承的实际应用场景
场景1:代币项目
// 基础代币 → 标准代币 → 项目代币
ERC20 → ERC20Burnable → MyToken
场景2:权限管理
// 基础权限 → 角色管理 → 具体合约
Ownable → AccessControl → ProjectContract
场景3:安全功能
// 基础合约 → 安全增强 → 最终合约
BaseContract → ReentrancyGuard + Pausable → SecureContract
场景4:可升级合约
// 存储合约 → 逻辑合约 → 代理合约
Storage → Logic → Proxy
2. 单继承
2.1 单继承基础语法
单继承是最简单的继承形式,一个子合约只继承一个父合约。
基本语法:
bash
contract Parent {
// 父合约代码
}
contract Child is Parent {
// 子合约代码
// 自动继承Parent的所有内容
}
关键字说明:
- is:表示继承关系
- Child:子合约(派生合约)
- Parent:父合约(基础合约)
简单示例:
bash
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.19;
contract Parent {
uint256 public value;
function getValue() public view returns (uint256) {
return value;
}
function setValue(uint256 _value) public {
value = _value;
}
}
contract Child is Parent {
// 自动继承:
// - value状态变量
// - getValue()函数
// - setValue()函数
// 添加新功能
function doubleValue() public view returns (uint256) {
return value * 2; // 可以直接访问父合约的value
}
}
子合约获得了什么:
bash
// 部署Child合约后,可以调用:
Child child = new Child();
child.value(); // 继承自Parent
child.getValue(); // 继承自Parent
child.setValue(100); // 继承自Parent
child.doubleValue(); // Child自己的函数
2.2 访问权限
子合约对父合约的访问权限取决于可见性修饰符。

- public:最开放,子合约和外部都能访问
- internal:只有子合约能访问,外部不能
- private:最严格,连子合约都不能访问
3. 多重继承
3.1 多重继承基础
Solidity支持多重继承,一个合约可以同时继承多个父合约。
基本语法:
bash
contract Child is Parent1, Parent2, Parent3 {
// 同时继承多个父合约
}
继承顺序:
- 从左到右列出父合约
- 顺序很重要
- 影响super调用和函数解析
基础示例:
bash
contract Parent1 {
function foo() public virtual returns (string memory) {
return "Parent1";
}
}
contract Parent2 {
function bar() public virtual returns (string memory) {
return "Parent2";
}
}
// 多重继承
contract Child is Parent1, Parent2 {
// 自动获得foo()和bar()
function test() public view returns (string memory, string memory) {
return (foo(), bar());
}
}
3.2 C3线性化算法
Solidity使用C3线性化算法(C3 Linearization)确定继承顺序。
基本规则:
- 从左到右:Child is A, B, C,A最先,C最后
- 深度优先:先处理父合约的父合约
- 保持单调性:不能打乱已有的顺序
简单示例:
继承声明:contract C is A, B
继承顺序:C → A → B
调用链:
C.foo()
→ A.foo()
→ B.foo()
4. super关键字
4.1 super的作用
super关键字用于调用父合约的函数,即使子合约已经重写了该函数。
基本概念:
bash
contract Parent {
function greet() public virtual returns (string memory) {
return "Hello from Parent";
}
}
contract Child is Parent {
function greet() public override returns (string memory) {
// 调用父合约的greet()
string memory parentGreeting = super.greet();
return string.concat("Hello from Child, ", parentGreeting);
}
}
调用过程:
Child.greet()
↓
获取super.greet()的返回值:"Hello from Parent"
↓
拼接:"Hello from Child, Hello from Parent"
↓
返回最终结果
4.2 单继承中的super
在单继承中,super指向唯一的父合约。
bash
contract Counter {
uint256 public count;
function increment() public virtual {
count += 1;
}
}
contract DoubleCounter is Counter {
function increment() public override {
// 先调用父合约的increment(+1)
super.increment();
// 再自己加一次(再+1)
count += 1;
// 最终效果:每次+2
}
}
4.3 多重继承中的super
在多重继承中,super不是指向单个父合约,而是按照继承顺序调用下一个合约。
关键理解:
bash
contract A {
function foo() public virtual returns (string memory) {
return "A";
}
}
contract B is A {
function foo() public virtual override returns (string memory) {
return string.concat("B->", super.foo());
}
}
contract C is A {
function foo() public virtual override returns (string memory) {
return string.concat("C->", super.foo());
}
}
contract D is B, C {
function foo() public override(B, C) returns (string memory) {
return string.concat("D->", super.foo());
}
}
调用链分析:
1)、Solidity 从右到左处理继承列表。
2)、C 在继承列表中更靠右,所以在 D 之后首先出现。
3)、B 在 C 之后。
4)、A 是共同的基类,放在最后。
bash
D.foo() 执行
↓
super.foo() → 指向 C(线性化顺序的下一个)
↓
C.foo() 执行
↓
C 中的 super.foo() → 指向 B(不是 A!)
↓
B.foo() 执行
↓
B 中的 super.foo() → 指向 A
↓
A.foo() 返回 "A"
↓
B.foo() 返回 "B->A"
↓
C.foo() 返回 "C->B->A"
↓
D.foo() 返回 "D->C->B->A"
重要提示: super 的含义:在多重继承中,super 不是指"直接父合约",而是指C3 线性化顺序中的下一个合约。 在 D 的上下文中,C 虽然直接继承自 A,但 C 的 super 会指向 B 这是因为在整个继承链中,C 的下一个是 B,而不是直接跳到 A
5. 构造函数继承
5.1 构造函数执行顺序
构造函数总是按照继承顺序执行,从父到子。
执行规则:
- 父合约优先:所有父合约构造函数先执行
- 按继承顺序:从左到右
- 最后是子合约:子合约构造函数最后执行
示例:
bash
contract A {
uint256 public valueA;
constructor() {
valueA = 1;
// 第1个执行
}
}
contract B {
uint256 public valueB;
constructor() {
valueB = 2;
// 第2个执行
}
}
contract C is A, B {
uint256 public valueC;
constructor() {
valueC = 3;
// 第3个执行
}
}
执行顺序:
部署C合约
↓
- A的构造函数执行(valueA = 1)
↓- B的构造函数执行(valueB = 2)
↓- C的构造函数执行(valueC = 3)
↓
完成
5.2 构造函数参数传递
当父合约的构造函数需要参数时,有两种传递方式。
方式1:在继承声明时传递(固定值)
bash
contract Parent {
uint256 public value;
constructor(uint256 _value) {
value = _value;
}
}
// 在继承声明时传递固定值
contract Child is Parent(100) {
constructor() {
// Parent构造函数接收100
}
}
特点:
- 适合固定值
- 代码简洁
- 不够灵活
方式2:在子构造函数中传递(动态值)
bash
contract Parent {
uint256 public value;
constructor(uint256 _value) {
value = _value;
}
}
// 在子构造函数中传递动态值
contract Child is Parent {
constructor(uint256 _value) Parent(_value) {
// 通过参数传递给Parent
}
}
特点:
- 更灵活
- 可以传递动态值
- 推荐使用
5.3 多重继承的构造函数
多个父合约都需要参数时,必须全部初始化。
bash
contract A {
uint256 public valueA;
constructor(uint256 _a) {
valueA = _a;
}
}
contract B {
uint256 public valueB;
constructor(uint256 _b) {
valueB = _b;
}
}
contract C is A, B {
uint256 public valueC;
// 方式1:混合传递
constructor(uint256 _a, uint256 _c)
A(_a) // 动态传递给A
B(200) // 固定值传递给B
{
valueC = _c;
}
// 方式2:全部动态传递(推荐)
constructor(uint256 _a, uint256 _b, uint256 _c)
A(_a)
B(_b)
{
valueC = _c;
}
}
执行顺序保持不变:
无论如何传递参数,执行顺序始终是:
A() → B() → C()
6. 函数重写
6.1 virtual和override关键字
函数重写(Function Overriding)允许子合约修改父合约函数的行为。
基本规则:
- 父合约:函数必须标记为virtual
- 子合约:重写函数必须标记为override
- 两者必须配对:缺一不可
基础示例:
bash
contract Parent {
// virtual:表示这个函数可以被重写
function getValue() public virtual returns (uint256) {
return 100;
}
}
contract Child is Parent {
// override:表示重写父合约的函数
function getValue() public override returns (uint256) {
return 200; // 修改返回值
}
}
6.2 函数签名必须匹配
重写函数必须与父合约函数签名完全一致。
必须相同的部分:
- 函数名
- 参数类型
- 参数顺序
- 返回类型
可以不同的部分:
- 可见性(可以更开放,不能更严格)
- 状态修饰符(可以更严格,不能更宽松)
示例:
bash
contract Parent {
function foo(uint256 a) public virtual returns (uint256) {
return a;
}
}
contract Child is Parent {
// 正确:签名完全相同
function foo(uint256 a) public override returns (uint256) {
return a * 2;
}
// 错误:参数不同
// function foo(uint256 a, uint256 b) public override returns (uint256) {
// return a + b;
// }
// 错误:返回类型不同
// function foo(uint256 a) public override returns (string memory) {
// return "hello";
// }
// 正确:internal → public(更开放)
function foo() public override returns (uint256) {
return 2;
}
// 错误:internal → private(更严格)
// function foo() private override returns (uint256) {
// return 2;
// }
}
6.3 多重继承中的override
当多个父合约有同名函数时,必须明确指定重写哪些。
bash
contract A {
function foo() public virtual returns (string memory) {
return "A";
}
}
contract B {
function foo() public virtual returns (string memory) {
return "B";
}
}
contract C is A, B {
// 必须明确指定:override(A, B)
function foo() public override(A, B) returns (string memory) {
return "C";
}
// 错误:不明确
// function foo() public override returns (string memory) {
// return "C";
// }
}
语法规则:
bash
// 单继承:简单override
function foo() public override returns (string memory) { }
// 多重继承:明确指定
function foo() public override(Parent1, Parent2) returns (string memory) { }
// 如果继续被继承,还要加virtual
function foo() public virtual override(Parent1, Parent2) returns (string memory) { }
6.4 使用super在重写中调用父函数
bash
contract Logger {
event Log(string message);
function log(string memory message) public virtual {
emit Log(message);
}
}
contract TimestampLogger is Logger {
function log(string memory message) public override {
// 先调用父合约的log
super.log(message);
// 再添加时间戳日志
emit Log(
string.concat(
"Timestamp: ",
uint2str(block.timestamp)
)
);
}
function uint2str(uint _i) internal pure returns (string memory) {
if (_i == 0) return "0";
uint j = _i;
uint len;
while (j != 0) {
len++;
j /= 10;
}
bytes memory bstr = new bytes(len);
uint k = len;
while (_i != 0) {
k = k-1;
uint8 temp = (48 + uint8(_i - _i / 10 * 10));
bytes1 b1 = bytes1(temp);
bstr[k] = b1;
_i /= 10;
}
return string(bstr);
}
}
7. 抽象合约
7.1 什么是抽象合约
抽象合约(Abstract Contract)是包含至少一个未实现函数的合约。
定义语法:
bash
abstract contract 合约名 {
// 至少一个未实现的函数
}
基本示例:
bash
abstract contract Animal {
// 抽象函数:只有声明,没有实现
function makeSound() public virtual returns (string memory);
// 普通函数:可以有实现
function sleep() public pure returns (string memory) {
return "Zzz...";
}
// 可以有状态变量
uint256 public age;
}
// 实现抽象合约
contract Dog is Animal {
// 必须实现makeSound
function makeSound() public pure override returns (string memory) {
return "Woof!";
}
}
contract Cat is Animal {
function makeSound() public pure override returns (string memory) {
return "Meow!";
}
}
抽象合约的特点:
- 不能直接部署:必须被继承
- 可以有实现:部分函数可以有实现
- 可以有状态变量:可以定义状态变量
- 可以有构造函数:可以有构造函数
- 强制实现:子合约必须实现所有抽象函数

8. 接口
8.1 什么是接口
接口(Interface)是纯粹的接口定义,只声明函数签名,不包含任何实现。
定义语法:
bash
interface 接口名 {
// 只有函数声明
}
基本示例:
bash
interface ICounter {
// 所有函数必须是external
function getCount() external view returns (uint256);
function increment() external;
function decrement() external;
// 可以定义事件
event CountChanged(uint256 newCount);
}
// 实现接口
contract Counter is ICounter {
uint256 private count;
event CountChanged(uint256 newCount);
function getCount() external view override returns (uint256) {
return count;
}
function increment() external override {
count++;
emit CountChanged(count);
}
function decrement() external override {
count--;
emit CountChanged(count);
}
}
8.2 接口的特点和限制
接口的特点:
使用interface关键字
- 不能有实现:所有函数都是声明
- 不能有状态变量:不能定义storage变量
- 不能有构造函数
- 所有函数必须external
- 可以继承其他接口
- 可以定义事件
接口vs合约对比:

8.3 ERC20接口标准
ERC20是最经典的接口定义示例。
bash
interface IERC20 {
// 查询函数
function totalSupply() external view returns (uint256);
function balanceOf(address account) external view returns (uint256);
function allowance(address owner, address spender)
external view returns (uint256);
// 操作函数
function transfer(address to, uint256 amount) external returns (bool);
function approve(address spender, uint256 amount) external returns (bool);
function transferFrom(
address from,
address to,
uint256 amount
) external returns (bool);
// 事件
event Transfer(address indexed from, address indexed to, uint256 value);
event Approval(address indexed owner, address indexed spender, uint256 value);
}
8.4 接口用于合约交互
接口最重要的应用是合约间交互。
场景:合约A调用合约B
bash
// 定义接口
interface IToken {
function transfer(address to, uint256 amount) external returns (bool);
function balanceOf(address account) external view returns (uint256);
}
// 使用接口与其他合约交互
contract Exchanger {
function swapTokens(address tokenAddress, address recipient, uint256 amount)
public
{
// 通过接口与代币合约交互
IToken token = IToken(tokenAddress);
// 检查余额
uint256 balance = token.balanceOf(address(this));
require(balance >= amount, "Insufficient balance");
// 执行转账
bool success = token.transfer(recipient, amount);
require(success, "Transfer failed");
}
}
接口的优势:
- 解耦:不需要知道合约的完整代码
- 标准化:统一的接口规范
- 互操作性:不同合约可以互相调用
- 节省gas:不需要导入完整合约代码