【c++】面 向 对 象 与 抽 象 数 据 类 型

💻作 者 简 介:曾 与 你 一 样 迷 茫,现 以 经 验 助 你 入 门 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)抽 象

  1. 抽 象 是 只 暴 露 "关 键 信 息"。对 使 用 者 而 言,只 需 知 道 ADT 的 函 数(用 来 做 什 么,如 何 使 用),无 需 知 道 内 部 如 何 存 储 数 据(如 栈 是 用 数 组 还 是 链 表 实 现)、如 何 执 行 操 作(如 Pop 时 如 何 移 动 指 针)。
  2. 例 如 :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)封 装

  1. 封 装 是 隐 藏 内 部 的 "实 现 细 节" 将 ADT 的 "数 据 存 储(如 数 组、链 表)" 和 "操 作 实 现(如 push 的 代 码)" 封 装 成 一 个 独 立 的 模 块 即 头 文 件 stack,不 允 许 用 户 直 接 修 改 内 部 数 据,只 能 通 过 定 义 好 的 "基 本 操 作"(函 数) 访 问。
  2. 好 处:若 后 续 需 要 修 改 实 现(如 把 "数 组 栈" 改 成 "链 表 栈"),只 要 保 持 操 作 接 口 不 变,所 有 使 用 该 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 的 核 心 优 势

  1. 降 低 耦 合 度:修 改 内 部 实 现 不 会 影 响 用 户 代 码。
  2. 提 高 复 用 性:ADT 的 接 口 是 标 准 化 的(如 栈 的 push/pop),可 在 不 同 程 序 中 直 接 复 用。
  3. 增 强 安 全 性:禁 止 用 户 直 接 操 作 内 部 数 据,避 免 因 非 法 修 改 导 致 的 逻 辑 错 误(如 直 接 修 改 栈 的 数 组 下 标 导 致 越 界 访 问)。
  4. 简 化 程 序 逻 辑:用 户 无 需 关 注 底 层 细 节,只 需 聚 焦 实 现 逻 辑(如 用 栈 解 决 "括 号 匹 配" 问 题 时,只 需 调 用 push/pop,无 需 关 心 栈 的 存 储)。

七、ADT 的 应 用:面 向 对 象 编 程 的 基 础

(1)类

ADT 的 思 想 直 接 催 生 了 面 向 对 象 编 程 的 核 心 概 念 - - - 类(Class)

  1. 类 的 成 员 变 量 对 应 ADT 的 "数 据 对 象(D)" 和 "数 据 关 系(R)";
  2. 类 的 成 员 方 法 对 应 ADT 的 "基 本 操 作(P)";
  3. 类 的 "访 问 控 制(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 相 关 操 作 函 数 有 以 下 共 性:

  1. 每 个 函 数 的 第 一 个 参 数 都 是 Stack*
  2. 函 数 中 必 须 要 对 第 一 个 参 数 检 测,因 为 该 参 数 可 能 会 为 NULL
  3. 函 数 中 都 是 通 过 Stack* 参 数 操 作 栈 的
  4. 调 用 时 必 须 传 递 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,能 帮 助 开 发 者 从 "关 注 代 码 细 节" 提 升 到 "设 计 数 据 模 型" 的 更 高 层 次。

相关推荐
云闲不收2 小时前
golang编译
开发语言·后端·golang
数据知道2 小时前
Go语言:加密与解密详解
开发语言·后端·golang·go语言
tt5555555555552 小时前
嵌入式开发面试八股文详解教程
linux·c++·驱动开发·面试
Flower#2 小时前
【算法】树上启发式合并 (CCPC2020长春 F. Strange Memory)
c++·算法
灰太狼大王灬3 小时前
Go 项目从开发到部署笔记
开发语言·笔记·golang
小树懒(-_-)3 小时前
SEO:Java项
java·开发语言
奔跑吧邓邓子3 小时前
【C++实战(75)】筑牢安全防线,攻克漏洞难题
c++·安全·实战·漏洞
胖咕噜的稞达鸭4 小时前
二叉树进阶面试题:最小栈 栈的压入·弹出序列 二叉树层序遍历
开发语言·c++
wzg20164 小时前
pyqt5 简易入门教程
开发语言·数据库·qt