
💻作 者 简 介:曾 与 你 一 样 迷 茫,现 以 经 验 助 你 入 门 C++。
💡个 人 主 页:@笑口常开xpr 的 个 人 主 页
📚系 列 专 栏:C++ 炼 魂 场:从 青 铜 到 王 者 的 进 阶 之 路✨代 码 趣 语:ADT 是 "装 修 效 果 图" - - - 先 画 好 图(逻 辑 定 义),施 工 队(数 据 结 构)怎 么 砌 墙(数 组 / 链 表)我 不 管,结 果 对 就 行。
💪代 码 千 行,始 于 坚 持,每 日 敲 码,进 阶 编 程 之 路。
📦gitee 链 接:gitee

写 C/C++ 时,想 实 现 "栈" 却 先 纠 结 用 数 组 还 是 链 表?其 实 不 用!抽 象 数 据 类 型(ADT)就 是 解 决 这 一 问 题 的 关 键。本 文 拆 解 ADT 的 "抽 象 + 封 装" 核 心,结 合 栈 的 案 例 对 比 C/C++ 实 现,帮 你 搞 懂 ADT 为 何 是 数 据 结 构 与 面 向 对 象 编 程 的 "桥 梁",摆 脱 "先 纠 结 实 现、再 迷 失 逻 辑" 的 困 境。
一、抽 象 数 据 类 型 的 定 义
抽 象 数 据 类 型(ADT)是 计 算 机 科 学 中 描 述 数 据 结 构 的 逻 辑 层 面 概 念,它 聚 焦 于 "数 据 是 什 么" 和 "数 据 能 做 什 么",而 刻 意 隐 藏 "数 据 如 何 实 现" 的 细 节,比 如 C/C++ 中 头 文 件 提 供 的 函 数,即 隐 藏 内 部 实 现 细 节,只 提 供 相 关 的 函 数 供 用 户 使 用。其 核 心 思 想 是 封 装 与 抽 象,将 数 据 的 "逻 辑 特 性" 与 "物 理 实 现" 分 离,从 而 降 低 程 序 复 杂 度、提 高 代 码 复 用 性 和 可 维 护 性。
二、ADT 的 核 心 定 义
ADT 本 质 上 是 一 个 "数 据 模 型",使 用 3 个 部 分 构 成,即 数 据 对 象(D),数 据 关 系(R)和 基 本 操 作(P)。通 常 使 用 "三 元 组" 表 示:ADT = (D, R, P)。
组成部分 | 核心含义 | 举例说明 |
---|---|---|
数据对象(D) | 具有相同性质的数据元素的集合 | "栈"的数据对象是 "所有整数的集合" |
数据关系(R) | 数据对象中元素之间的逻辑关系集合 | "栈"的数据关系是元素按'后进先出'的顺序排列 |
基本操作(P) | 数据对象的合法操作 | "栈"的基本操作包括:Push(入栈)、Pop(出栈)、GetTop(取栈顶)、IsEmpty(判断空)等 |
三、ADT 的 核 心 思 想
ADT 的 核 心 价 值 在 于 解 决 "用 户 无 需 关 心 内 部 实 现,只 需 关 注 如 何 使 用" 的 问 题,这 依 赖 于 两 个 关 键 思 想:
(1)抽 象
- 抽 象 是 只 暴 露 "关 键 信 息"。对 使 用 者 而 言,只 需 知 道 ADT 的 函 数(用 来 做 什 么,如 何 使 用),无 需 知 道 内 部 如 何 存 储 数 据(如 栈 是 用 数 组 还 是 链 表 实 现)、如 何 执 行 操 作(如 Pop 时 如 何 移 动 指 针)。
- 例 如 :C++ 使 用 "栈" 时,你 只 需 先 引 用 头 文 件
#include<stack>
,然 后 调 用 push(x) 就 能 让 x 入 栈,不 需 要 关 心 栈 是 用 数 组 的 "下 标 + 1" 实 现,还 是 使 用 链 表 的 "头 插 法" 实 现。
javascript
#include<iostream>
#include<stack>
using namespace std;
int main()
{
stack<int> mystack;
int i = 0;
for (i = 0; i < 5; i++)
{
mystack.push(i);
}
while (!mystack.empty())
{
cout << mystack.top() << " ";
mystack.pop();
}
cout << endl;
return 0;
}

(2)封 装
- 封 装 是 隐 藏 内 部 的 "实 现 细 节" 将 ADT 的 "数 据 存 储(如 数 组、链 表)" 和 "操 作 实 现(如 push 的 代 码)" 封 装 成 一 个 独 立 的 模 块 即 头 文 件 stack,不 允 许 用 户 直 接 修 改 内 部 数 据,只 能 通 过 定 义 好 的 "基 本 操 作"(函 数) 访 问。
- 好 处:若 后 续 需 要 修 改 实 现(如 把 "数 组 栈" 改 成 "链 表 栈"),只 要 保 持 操 作 接 口 不 变,所 有 使 用 该 ADT 的 代 码 都 无 需 修 改,极 大 降 低 维 护 成 本。
四、经 典 ADT 示 例 - - - 栈
为 了 更 直 观 理 解 ADT,这 里 采 用 自 然 语 言 完 整 描 述 "栈" 的 ADT 定 义。
(1)数 据 对 象(D)
D = {a₀, a₁, ..., aₙ₋₁ | aᵢ ∈ 整 数 集 合,i = 0, 1, ..., n-1, n ≥ 0}
(2)数 据 关 系(R)
R = {<aᵢ₋₁, aᵢ> | aᵢ₋₁ 是 aᵢ 的 前 一 个 元 素,且 aₙ₋₁ 是 栈 顶 元 素,元 素 只 能 从 栈 顶 插 入 / 删 除 }(即 "后 进 先 出" 关 系)
(3)基 本 操 作(P)
这 里 需 要 注 意 的 是 如 果 C++ 中 引 入 了 头 文 件 stack
,则 无 需 写 栈 的 初 始 化 函 数,因 为 栈 属 于 内 置 类 型,编 译 器 会 自 动 生 成 栈 的 构 造 函 数。
操作名称 | 前置条件 | 功能 | 后置条件 |
---|---|---|---|
构造函数 | 无 | 初始化一个空栈S | S为空栈 |
push | 栈未满 | 将元素x插入栈顶 | 栈顶元素变为x,栈长度+1 |
pop | S 非空 | 删除栈顶元素 | 栈顶元素变为原栈顶的下一个元素,栈长度-1 |
top | S 非空 | 得到栈顶元素 | 栈的结构不发生改变 |

五、ADT 与 "数 据 结 构" 的 区 别
很 多 人 会 混 淆 ADT 和 数 据 结 构,核 心 区 别 在 于 逻 辑 层 面 与 物 理 层 面 的 区 别。
对比维度 | 抽象数据类型(ADT) | 数据结构 |
---|---|---|
关注层面 | 逻辑层面("做什么") | 物理层面("怎么做") |
核心内容 | 定义数据的逻辑关系和操作接口 | 实现ADT的具体方式(如用数组 / 链表存储数据) |
示例 | "栈"的ADT(定义后进先出关系和具体操作关系) | "数组实现的栈"、"链表实现的栈" |
关系 | ADT是"需求",数据结构是"实现方案" | 一个ADT可以对应多种数据结构(如栈可由数组或链表实现) |
六、ADT 的 核 心 优 势
- 降 低 耦 合 度:修 改 内 部 实 现 不 会 影 响 用 户 代 码。
- 提 高 复 用 性:ADT 的 接 口 是 标 准 化 的(如 栈 的 push/pop),可 在 不 同 程 序 中 直 接 复 用。
- 增 强 安 全 性:禁 止 用 户 直 接 操 作 内 部 数 据,避 免 因 非 法 修 改 导 致 的 逻 辑 错 误(如 直 接 修 改 栈 的 数 组 下 标 导 致 越 界 访 问)。
- 简 化 程 序 逻 辑:用 户 无 需 关 注 底 层 细 节,只 需 聚 焦 实 现 逻 辑(如 用 栈 解 决 "括 号 匹 配" 问 题 时,只 需 调 用 push/pop,无 需 关 心 栈 的 存 储)。
七、ADT 的 应 用:面 向 对 象 编 程 的 基 础
(1)类
ADT 的 思 想 直 接 催 生 了 面 向 对 象 编 程 的 核 心 概 念 - - - 类(Class)
- 类 的
成 员 变 量
对 应 ADT 的 "数 据 对 象(D)" 和 "数 据 关 系(R)"; - 类 的
成 员 方 法
对 应 ADT 的 "基 本 操 作(P)"; - 类 的 "访 问 控 制(public/private)" 实 现 了 ADT 的 "封 装"(private 隐 藏 实 现,public 暴 露 接 口)。
(2)C 语 言 实 现 栈
javascript
#include<stdio.h>
#include<assert.h>
#include<stdlib.h>
typedef int DataType;
typedef struct Stack
{
DataType* array;
int capacity;
int size;
}Stack;
void StackInit(Stack* ps)
{
assert(ps);
ps->array = (DataType*)malloc(sizeof(DataType) * 3);
if (NULL == ps->array)
{
assert(0);
return;
}
ps->capacity = 3;
ps->size = 0;
}
void StackDestroy(Stack* ps)
{
assert(ps);
if (ps->array)
{
free(ps->array);
ps->array = NULL;
ps->capacity = 0;
ps->size = 0;
}
}
void CheckCapacity(Stack* ps)
{
if (ps->size == ps->capacity)
{
int newcapacity = ps->capacity * 2;
DataType* temp = (DataType*)realloc(ps->array,
newcapacity * sizeof(DataType));
if (temp == NULL)
{
perror("realloc申请空间失败!!!");
return;
}
ps->array = temp;
ps->capacity = newcapacity;
}
}
void StackPush(Stack* ps, DataType data)
{
assert(ps);
CheckCapacity(ps);
ps->array[ps->size] = data;
ps->size++;
}
int StackEmpty(Stack* ps)
{
assert(ps);
return 0 == ps->size;
}
void StackPop(Stack* ps)
{
if (StackEmpty(ps))
return;
ps->size--;
}
DataType StackTop(Stack* ps)
{
assert(!StackEmpty(ps));
return ps->array[ps->size - 1];
}
int StackSize(Stack* ps)
{
assert(ps);
return ps->size;
}
int main()
{
Stack s;
StackInit(&s);
StackPush(&s, 1);
StackPush(&s, 2);
StackPush(&s, 3);
StackPush(&s, 4);
printf("%d\n", StackTop(&s));
printf("%d\n", StackSize(&s));
StackPop(&s);
StackPop(&s);
printf("%d\n", StackTop(&s));
printf("%d\n", StackSize(&s));
StackDestroy(&s);
return 0;
}
C 语 言 是 面 向 过 程 的 编 程 语 言,面 向 过 程 围 绕 的 是 步 骤
和 函 数
展 开,强 调 的 是 怎 么 做
。通 过 结 构 体 和 函 数 实 现 栈,在使用时Stack 相 关 操 作 函 数 有 以 下 共 性:
- 每 个 函 数 的 第 一 个 参 数 都 是
Stack*
- 函 数 中 必 须 要 对 第 一 个 参 数 检 测,因 为 该 参 数 可 能 会 为
NULL
- 函 数 中 都 是 通 过
Stack*
参 数 操 作 栈 的 - 调 用 时 必 须 传 递
Stack
结 构 体 变 量 的 地 址
C 语 言 中 结 构 体 中 只 能 定 义 存 放 数 据 的 结 构,操 作 数 据 的 方 法 不 能 放 在 结 构 体 中,即 数 据 和 操 作 数 据 的 方 式 是 分 离 开 的,而 且 实 现 上 相 当 复 杂 一 点,涉 及 到 大 量 指 针 操 作,稍 不 注 意 可 能 就 会 出 错。
(3)C++ 实 现 栈
在 C++ 中,用 类 实 现 "栈" 的 ADT:
javascript
#include<iostream>
using namespace std;
typedef int DataType;
class Stack
{
public:
void Init()
{
_array = (DataType*)malloc(sizeof(DataType) * 3);
if (NULL == _array)
{
perror("malloc申请空间失败!!!");
return;
}
_capacity = 3;
_size = 0;
}
void Push(DataType data)
{
CheckCapacity();
_array[_size] = data;
_size++;
}
void Pop()
{
if (Empty())
return;
_size--;
}
DataType Top()
{
return _array[_size - 1];
}
int Empty()
{
return 0 == _size;
}
int Size()
{
return _size;
}
void Destroy()
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
}
private:
void CheckCapacity()
{
if (_size == _capacity)
{
int newcapacity = _capacity * 2;
DataType* temp = (DataType*)realloc(_array, newcapacity * sizeof(DataType));
if (temp == NULL)
{
perror("realloc申请空间失败!!!");
return;
}
_array = temp;
_capacity = newcapacity;
}
}
private:
DataType* _array;
int _capacity;
int _size;
};
int main()
{
Stack s;
s.Init();
s.Push(1);
s.Push(2);
s.Push(3);
s.Push(4);
printf("%d\n", s.Top());
printf("%d\n", s.Size());
s.Pop();
s.Pop();
printf("%d\n", s.Top());
printf("%d\n", s.Size());
s.Destroy();
return 0;
}
C++ 是 面 向 对 象 的 编 程 语 言,面 向 对 象 围 绕 的 是 对 象
和 对 象
之 间 的 交 互,强 调 的 是 做 什 么
。C++ 中 通 过 类 可 以 将 数 据 以 及 操 作 数 据 的 方 法 进 行 完 美 结 合,通 过 访 问 权 限 可 以 控 制 那 些 方 法 在 类 外 可 以 被 调 用,即 封 装,在 使 用 时 就 像 使 用 自 己 的 成 员 一 样,更 符 合 人 类 对 一 件 事 物 的 认 知。而 且 每 个 方 法 不 需 要 传 递 Stack*
的 参 数 了,编 译 器 编 译 之 后 该 参 数 会 自 动 还 原,即 C++ 中 Stack*
参 数 是 this 指 针,它 是 由 编 译 器 维 护 的,C 语 言 中 需 要 用 户 自 己 维 护。
八、总 结
抽 象 数 据 类 型(ADT)是 一 种 "从 问 题 出 发" 的 思 维 方 式:它 先 定 义 数 据 的 逻 辑 特 性 和 操 作 能 力,再 通 过 具 体 的 数 据 结 构 (数 组、链 表 等)实 现。其 核 心 价 值 在 于 "分 离 逻 辑 与 实 现",是 降 低 程 序 复 杂 度、提 高 代 码 质 量 的 关 键 工 具,也 是 面 向 对 象 编 程 的 理 论 基 础。掌 握 ADT,能 帮 助 开 发 者 从 "关 注 代 码 细 节" 提 升 到 "设 计 数 据 模 型" 的 更 高 层 次。