当前课程知识点:基于Linux的C++ > 第八讲 链表与程序抽象 > 8.10 抽象链表(一) > LinuxCPP0810
有了函数指针的概念之后
我们现在就可以设计抽象的链表
就像我刚才讲的qsort那个函数里
它有一个函数指针类型的形式参数
那个函数指针指向的那个函数
我们就称它为回调函数
允许通过函数指针
调用未来才会实现的代码
这个就叫回调函数 凡是回调函数
它都会需要依赖后续的设计
否则你不知道它调用哪一个
你传哪一个函数入口地址给它
它就调用哪一个 对吧
像DoCompareObject
你把这个DoCompareObject传给qsort
qsort就会调用DoCompareObject
你如果传的不是这个函数
传的是另外一个函数
它就会调用另外一个函数 这是肯定的
所以说 你传哪一个 它就调用哪一个
而这个函数本身还是你未来才会实现的
就相对于qsort那个函数的设计
和编写的那个实现而言
DoCompareObject这个函数实现
是远远在它的未来之后
所以说对qsort来说
它会调用你未来才会实现的DoCompareObject
替你排序未来才会实现的数组
qsort本身 它只能完成排序的基本动作
它不知道你的数组是什么样子
它只知道你的数组的入口地址在哪里
你的数组里包含多少个元素
你的数组每个元素尺寸是多少
所有的这些还都是
你在调用qsort的时候告诉它的
其次它也不知道怎么比较元素的关系
怎么比较呢 也是你告诉它的
你写一个比较函数
把这个函数的入口地址给它
它就调用你的函数 替你比较你的数据
这是非常重要的一个设计
回调函数的作用就体现在这里
有的时候我们需要在回调函数
和主调函数之间
产生一个很特殊的信息的交互
这种东西我们称它为附加的信息
我可能想传附加的信息
然后你可能会说
那我传多少个附加的数据信息是够的呢
你可能就会说我传一个
那如果是传俩信息怎么办
我如果写俩参数
如果需要传三个信息怎么办呢 也不成啊
难道就真的一点办法都没有吗 当然不是
我们前面不讲了吗
我们有一个通用型的东西
有一个通用型的数据对象
有一个通用型的数据抽象类型
叫什么 哑型指针 void*,ADT嘛
我就用它来表达这个附加的参数
如果传的附加信息是一个
我们就直接传这个附加信息
或这个附加信息的地址
如果这个附加信息
和我们的哑型指针尺寸刚好是一般大
你就不用传它的地址了
你就传它就可以了
附加信息如果是一个
很典型 我用一个ADT代替它
如果附加信息是两个呢
然后你还搞两个ADT 吗 不需要
我就把附加信息——不论它多少个
只要超过两个 大于等于2
对吧 超过一个了
把它做成一个结构体
然后传那个结构体的地址
转换成ADT 不就够了吗
所以一个ADT事实上就可以传递所有的东西
不管这个附加信息有多少
一个ADT就足够用了 特别重要这一条
回到我们的抽象链表的考虑
我们这里面链表的每一个结点的data字段
它会存储我们这个链表
作为一个容器所容纳的目标数据对象
或者目标数据对象的地址
那么我们现在就有一个巨大的问题
如果我们这个链表结点的data字段里面
存的是一个目标数据对象的地址
那么当我在删除这个结点的时候
毫无疑问必须在销毁这个结点之前
完成目标数据的销毁
这个你必须要做到
就像我们抽象链表
刚才那个版本所实现的那样
想删除我们这个结点
在此之前你必须先删除
data字段所指向的点数据结构
然后才能删除那个结点本身
把结点本身全都删完了
然后你才能删除这个链表本身
这是一个非常明确的存储管理的顺序
如果我们使用这个链表来保存的是一些整数
就像我刚才说的
我实际上是可以把这个int直接转换成void*
(注:在32位系统和编译器下两者存储尺寸一致)
就存在data字段里面的
这个时候我们的data字段
它并不指向任意的有意义的存储区
它其实的原始意义就是一个整数
结果被我们当成void* 存在data字段里面了
所以在这种时候
如果我需要销毁这个链表的结点
那么data字段是不需要去管理它的
也就是说这个时候
你是不可以销毁data字段
所指向的那个目标数据区域的
那个并不是我们真正
内存分配出来的存储区域
所以当我们设计抽象链表的时候
就需要考虑到这一点
就是说你删除这个链表结点的时候
它里面的目标数据对象是不是需要删除
如果是 你怎么办 如果不是 你怎么办
这个在你设计抽象链表的时候
你必须能够考虑到
第二个 如果链表的结点存储的是指针
你就需要参数 否则就不需要
设计抽象链表的时候
我们并不需要了解这个结点
实际存储的数据是不是指针
我们设计抽象链表的时候
我们可能并不了解
结点实际存储的数据是不是指针
所以有的时候可能无法来决定这个结点
它的操作的逻辑
这是非常重要的一个地方
就是说我用一个抽象链表存了一个数据
那么我们还不知道
它所存储的那个目标数据
完整的意义的时候
我们实际上是不了解那个东西的——data字段
是指针吗 也许
不是指针吗 也许 怎么办呢
所以这个时候就需要特别特别去注意
我们先来看回调函数怎么用
我要同学们编写一个函数
遍历我们的链表
然后结点数据的具体操作方法呢
我们现在是不知道的 不知道该怎么做
我就是你遍历这个链表然后做某件事
注意某件事就是某件事
并不是一定是某件事 什么意思
其实就是说当我遍历这个链表的时候
我只知道要做一件事情
但是这件事情具体做什么
其实我不清楚
至少我在实现这个函数的时候我不清楚
也就是说有些信息
在我实现这个函数的时候是未知的
怎么办呢
凡是未知的东西都应该使用量来代替
就像我们使用量来代替一个具体的数一样
你不知道那个数是什么 没关系
我们操纵的是这个量
你不知道操作是什么 没关系
我们使用一个函数指针 就是这个意思
所以我们需要使用一个回调函数
来提供这个结点数据的具体的操作方法
你看这里面有一个函数指针类型
MANIPULATE_OBJECT
有一个操纵对象的函数指针类型
它接受一个ADT类型的一个参数
返回值是void
接下来我们实现LlTraverse函数 遍历函数
传一个链表进去
然后传一个MANIPULATE_OBJECT
这样的操纵对象的函数指针类型的指针
形式参数名字叫manipulate
注意 平时我们用参数的名字
不管形式参数还是实际参数的名字
还是变量的名字
我们平时都使用名词 对吧
我们这个bool量 偶尔会使用形容词
我们这里面 我们使用的是动词
为什么 因为它是函数指针类型的变量
所以它往往代表的是一个动作
所以我们使用一个动词 这是可以的
manipulate 操纵
定义一个PNODE类型的一个量t
初始化成list->head
让t指向链表表头结点 循环
如果manipulate这个值不是NULL
就表示你传了一个实际的函数的入口地址
那么我们就调用它(*manipulate)(t->data)
t->data就是ADT类型 对吧
我就调用它
然后t=t->next 然后循环
因为我在LlTraverse这个函数里面
提供了一个回调函数
这实际上就意味着我们这个LlTraverse
可以在遍历的时候
执行你想要它做的操作
你想要它打印 你就写一个打印函数
作为实际参数传给LlTraverse
让manipulate指向那个打印函数
它就会打印 如果你想求和
那么你写一个求和的函数传给manipulate
manipulate就会指向那个求和函数
然后求和 按照这样的程序设计
我们LlTraverse这个函数
它的适用性显然
要比原来的那个方式要灵活得多
至少你看到 在我们LlTraverse这个函数内部
没有对PtTransformIntoString函数的调用
这就意味着对LlTraverse这个函数来讲
它不需要知道PPOINT、点库的任何细节
管你是点库还是什么库
尤其是注意到 你看我这个数据
一旦形成一个抽象的链表库的话
那我们的data字段
它压根就不是PPOINT类型的
它是ADT类型的 完全把它抽象化了
你想用的时候把PPOINT转换成ADT
然后在内部再重新把ADT转换成PPOINT回去
它是按照这样方式将链表库和点库
完全给独立开来的
数据结构 它们互相不使用了
算法 它们互相之间也不使用了
那不就独立了嘛
点库是点库 抽象链表库是链表库
而我们抽象链表库
它的应用场合也就不再局限于点库
这个就是我们LlTraverse
有了回调函数以后给我们揭示的
最深刻的一个地方
接下来 我们的回调函数参数
我们可以对这个MANIPULATE_OBJECT
这个函数指针类型做一个改变
添加第二个参数 ADT tag
作为我们的附加参数
也就是说 这个函数指针类型MANIPULATE_OBJECT
它带两个参数 第一个参数是ADTe
第二个参数是ADT tag
返回值是一个void
这就是我们的操纵函数
如果你觉得这个函数指针类型的声明
仍然不够一般化 那我建议你
还可以把它的返回值也设成void*
就什么东西都可以带回来了 也可以
没有什么东西可带 返回NULL
有什么东西可带
你就构造一个动态数据对象
然后把它的地址返回来就完了
这也很典型吧
可以按照这个方式去做
然后看我们LlTraverse这个函数
这个时候我就需要传三个参数
第一个参数是抽象的链表list
第二个参数是操纵对象 是个manipulate
第三个参数就是我们的附加参数tag
调用LlTraverse的那个程序员或者那片代码
它负责传三个实际参数进来
我们先看LlTraverse这个函数的实现
三个参数 PLIST类型的list
MANIPULATE_OBJECT类型的manipulate
ADT类型的tag
tag就是我们的附加参数
我首先让list->head初始化给t
PNODE类型的t 然后while
if(manipulate),(*manipulate)(t->data, ag),
t = t->next
你看到了 我们的程序代码
就是把这个第三个参数tag
也就是我们的附加参数
传递给manipulate所指向的目标函数
tag这个参数传递给LlTraverse的
LlTraverse本身不用它
它内部没有使用过tag
它就是简单地拿到一个附加参数
然后转手扔给了manipulate
所指向的那个目标函数
它自己没对它做任何操作
-1.1 提纲
-1.2 程序设计的基本概念
-1.3 简单C/C++程序介绍
-1.4 程序设计的基本流程
-1.5 基本语法元素
-1.6 程序设计风格
-1.7 编程实践
-第一讲 C/C++基本语法元素--编程实践提交入口
-2.1 提纲
-2.2 结构化程序设计基础
-2.3 布尔数据
-2.4 分支结构
-2.5 break语句
-2.6 循环结构
-2.7 编程实践
-第二讲 程序控制结构--编程实践提交入口
-3.1 提纲
-3.2 函数声明、调用与定义
-3.3 函数调用栈框架
-3.4 编程实践
-第三讲 函数--编程实践提交入口
-4.1 提纲
-4.2 算法概念与特征
-4.3 算法描述
-4.4 算法设计与实现
-4.5 递归算法(一)
-4.6 递归算法(二)
-4.7 容错与计算复杂度
-4.8 编程实践
-第四讲 算法--编程实践提交入口
-5.1 提纲
-5.2 库与接口
-5.3 随机数库(一)
-5.4 随机数库(二)
-5.5 作用域与生存期
-5.6 典型软件开发流程(一)
-5.7 典型软件开发流程(二)
-5.8 编程实践
-第五讲 程序组织与开发方法--编程实践提交入口
-6.1 提纲
-6.2 字符
-6.3 数组(一)
-6.4 数组(二)
-6.5 结构体
-6.6 编程实践
-第六讲 复合数据类型--编程实践提交入口
-7.1 提纲
-7.2 指针基本概念
-7.3 指针与函数
-7.4 指针与复合数据类型(一)
-7.5 指针与复合数据类型(二)
-7.6 字符串
-7.7 动态存储管理(一)
-7.8 动态存储管理(二)
-7.9 引用
-7.10 编程实践
-第七讲 指针与引用--编程实践提交入口
-8.1 提纲
-8.2 数据抽象(一)
-8.3 数据抽象(二)
-8.4 链表(一)
-8.5 链表(二)
-8.6 链表(三)
-8.7 链表(四)
-8.8 函数指针(一)
-8.9 函数指针(二)
-8.10 抽象链表(一)
-8.11 抽象链表(二)
-8.12 编程实践
-第八讲 链表与程序抽象--编程实践提交入口
-9.1 提纲
-9.2 程序抽象与面向对象
-9.3 类类型
-9.4 对象(一)
-9.5 对象(二)
-9.6 类与对象的成员(一)
-9.7 类与对象的成员(二)
-9.8 类与对象的成员(三)
-9.9 继承(一)
-9.10 继承(二)
-9.11 继承(三)
-9.12 多态(一)
-9.13 多态(二)
-9.14 编程实践
-第九讲 类与对象--编程实践提交入口
-10.1 提纲
-10.2 四则运算符重载(一)
-10.3 四则运算符重载(二)
-10.4 关系与下标操作符重载
-10.5 赋值操作符重载(一)
-10.6 赋值操作符重载(二)
-10.7 赋值操作符重载(三)
-10.8 赋值操作符重载(四)
-10.9 赋值操作符重载(五)
-10.10 流操作符重载(一)
-10.11 流操作符重载(二)
-10.12 流操作符重载(三)
-10.13 操作符重载总结
-10.14 编程实践
-第十讲 操作符重载--编程实践提交入口
-11.1 提纲
-11.2 泛型编程概览
-11.3 异常处理机制(一)
-11.4 异常处理机制(二)
-11.5 运行期型式信息(一)
-11.6 运行期型式信息(二)
-11.7 模板与型式参数化
-11.8 题外话:术语翻译
-11.9 泛型编程实践(一)
-11.10 泛型编程实践(二)
-11.11 泛型编程实践(三)
-11.12 泛型编程实践(四)
-11.13 泛型编程实践(五)
-11.14 泛型编程实践(六)
-11.15 泛型编程实践(七)
-11.16 泛型编程实践(八)
-11.17 泛型编程实践(九)
-11.18 泛型编程实践(十)
-11.19 编程实践
-第十一讲 泛型编程--编程实践提交入口
-12.1 提纲
-12.2 程序执行环境(一)
-12.3 程序执行环境(二)
-12.4 程序执行环境(三)
-12.5 程序执行环境(四)
-12.6 输入输出(一)
-12.7 输入输出(二)
-12.8 文件系统
-12.9 设备
-12.10 库(一)
-12.11 库(二)
-12.12 makefile文件(一)
-12.13 makefile文件(二)
-12.14 makefile文件(三)
-12.15 编程实践
-第十二讲 Linux系统编程基础--编程实践提交入口
-13.01 提纲
-13.02 进程基本概念
-13.03 信号
-13.04 进程管理(一)
-13.05 进程管理(二)
-13.06 进程管理(三)
-13.07 进程间通信(一)
-13.08 进程间通信(二)
-13.09 进程间通信(三)
-13.10 进程间通信(四)
-13.11 进程池
-13.12 编程实践
-第十三讲 进程编程--编程实践提交入口
-14.1 提纲
-14.2 线程基本概念
-14.3 线程管理(一)
-14.4 线程管理(二)
-14.5 线程管理(三)
-14.6 线程管理(四)
-14.7 线程同步机制(一)
-14.8 线程同步机制(二)
-14.9 C++11线程库(一)
-14.10 C++11线程库(二)
-14.11 C++11线程库(三)
-14.12 C++11线程库(四)
-14.13 C++11线程库(五)
-14.14 编程实践
-第十四讲 线程编程--编程实践提交入口
-15.1 提纲
-15.2 Internet网络协议
-15.3 套接字(一)
-15.4 套接字(二)
-15.5 编程实践
-第十五讲 网络编程--编程实践提交入口