抽象数据类型简介
抽象数据类型是数据结构的一种应用形式。与普通数据类型相似,抽象数据类型由类型描述(例如取值范围)和操作集合共同定义。例如,普通的整型数据类型由整数的取值范围和整数能参与的一系列加减乘除运算共同定义:unsinged char的取值范围是0~255,其操作集合为“+、-、、/、、|、~……”。抽象数据类型是由一类数据结构及相关操作函数构成。例如,我们可以将存储和表达环形队列的数据缓冲以及访问指针封装成结构体类型,此时该结构体类型与相关的队列操作函数就构成了环形队列的抽象数据类型。
值得一提的是,如果我们将队列初始化、释放和访问的函数以函数指针的形式存储在抽象数据类型的结构体中,则完成了面向对象的封装。此时的抽象数据类型就可以被称之为类(Class);具体的结构体变量就可以被称之为对象的实例(Instance);结构体中所有的函数指针构成了虚函数表(Virtual Function Table);队列的初始化函数和释放函数可以分别被称之为构造(Constructor)/析构函数(Destructor);其它队列的操作函数称之为方法(Method)。结构体中允许外部直接访问的变量称之为成员变量(Member)或属性(Property),其公有或是私有属性由类型的掩码结构体决定:掩码结构体中公开的成员变量具有公有(Public)属性;被屏蔽的部分具有私有(Private)属性。
抽象数据类型 (Abstract //代码效果参考:http://www.zidongmutanji.com/bxxx/324478.html
Data Type)一类数据结构,其实现形式和内部操作过程对用户是不透明的,我们称这类数据结构及其操作为抽象数据类型。
面向对象编程 (Object-Oriented Programming)
将数据以及对其进行操作的函数封装在一起构成抽象数据类型的编程方式,称为面向对象编程。
面向对象C语言开发 (Object-Oriented Programming with ANSI-C)
程序员使用C语言手工构造抽象数据类型实现面向对象的开发,称为面向对象C语言开发。C语言本身是面向过程的(Procedure-Oriented),并不能很好的支持面向对象的开发。C++是面向对象的编程语言(Object-Oriented Language),编译器本身会根据用户定义的类去自动构造抽象数据类型。
掩码结构体 (Masked Structure)
在定义抽象数据类型时,为了向外界屏蔽结构体内的某些信息而定义的与实际抽象数据类型占用相同存储器空间、仅公开某些成员而屏蔽大部分信息的结构体类型,称之为掩码结构体。这种结构常用于使用C语言实现的类封装结构中。
抽象数据类型构成要素
使用C语言构建抽象数据类型主要借助以下手段:
i) 使用结构体来封装数据与成员变量;
ii) 使用typedef来定义抽象数据类型;
iii) 如果需要将抽象数据类型封装成对象,还需要借助函数指针构成虚函数表;
iv) 如果需要在不同的对象操作中使用不同的参数,还需要借助可变参数列表的支持。
函数指针
指向函数入口地址的指针称为函数指针。与普通指针类似,函数指针的本质也是一个整型变量;定义函数指针时需要详细的制定其指向函数的参数表和返回值类型。下面的代码就定义了一个指向函数void Example(unsigned char pstrData)的函指针:
1 / 测试函数 /
2 void ExampleA(unsigned char pstrData) { };
3 void ExampleB(unsigned char pstrData) { };
4 ……
5 / 声明一个函数指针fnPointToFunction 并对其进行初始化 /
6 void ( fnPointToFunction)(unsigned char pstrData) = ExampleA;
7 ……
8 void main(void)
9 {
10 ……
11 / 通过函数指针调用函数 /
12 (fnPointToFunction)(“Hello world”);
13 / 直接调用函数 /
14 Example(“Hello world”);
15 / 更改函数指针的值 /
16 fnPointToFunction = ExampleB; / “”运算符不是必须的,可以省略/
17 / 函数的名称就代表它的入口地址 /
18 / 使用函数指针调用ExampleB /
19 fnPointToFunction(“Bye!”); / ““运算符也不是必须的,可以省略 /
20 ……
21 }
函数指针可以像普通指针那样作为函数的参数或返回值,也可以用来构建结构体和数组。下面的代码声明了一个使用函数指针作为参数和返回值的函数,其中,函数名为Example,下划线指示的部分为函数名以及参数列表,其余部分为函数返回值类型说明:
/ 一个使用函数指针作为参数和返回值的函数例子 /
void (Example (void ( fnParameter)(unsigned char pchData)))(unsigned char pchData);
这样的函数声明可读性非常的糟糕,因此我们使用typedef来逃离这一窘境:
1 / 使用typedef 定义函数指针 /
2 typedef void ( P_FUN)(unsigned char pchData); / 定义了一个函数指针类型P_FUN /
3
4 / 测试函数 /
5 void ExampleA(unsigned char pstrData) { }; /假设这个函数在LCD上输出字符 /
6 void ExampleB(unsigned char pstrData) { }; /假设这个函数在打印机上输出字符 /
7 / 使用新定义的函数指针类型声明函数指针和数组 /
8 P_FUN fnFunctionToPoint; / 声明了一个函数指针 /
9 P_FUN fnFunctionTable【】 = / 声明了一个函数指针数组 /
10 {
11 ExampleA, ExampleB / 初始化数组 /
12 };
13 / 使用新定义的类型声明了一个和前面代码片断相同的函数:
14 使用函数指针作为输入参数和返回值 /
15 P_FUN Example(P_FUN fnParameter)
16 {
17 ……
18 }
19 void main(void)
20 {
21 / 使用范例 /
22 unsigned char n = 0;
23 / 在所有有效的输出设备上都输出 Hello world /
24 for (n = 0;n < (sizeof(fnFuntionTable) / sizeof(fnFunctionTable【0】)); n++)
25 {
26 / 依次遍历函数数组 /
27 fnFunctionTable【n】(“Hello world!”);
28 }
29 }
可变参数列表
可变参数是指某一函数被调用时,并不知道具体传递进来的参数类型和参数的数目,例如大家熟知的函数printf():
1 / 一个使用可变参数的例子 /
2 printf(“Hello! \n”); / 只有一个字符串参数 /
3 printf(“Today is the %dth day of this week. \n”,Week); / 有两个参数 /
4 printf(“ %d + %d = %d”,a,b,a b); / 三个参数 /
C语言是通过软件堆栈的方式进行参数传递的,对于下面的函数,从左至右依次压入栈中的变量为:a、b、c。如果存在更多的参数,只要在函数在真正被调用前按照同样的顺序依次压入栈中就可以完成任意数量参数的传递。这就是可变参数传递的原理,在函数声明时,在参数列表最右边加入一个省略号“...”作为参数就可以将一个函数声明为可变参数传递。例如:
1 / 一个使用可变参数的例子 /
2 void printf(char pString,…); / 使用可变参数作为函数参数 /
可变参数实际上是具有参数类型va_list。在函数内部必须要首先声明一个可变参数变量,以便依次取出所有传入的数据,例如:
1 va_list Example; / 定义一个可变参数列表 /
va_list可以像普通变量类型一样充当函数的参数和返回值,例如:
1 / 定义一个函数,需要上级函数传递一个va_list型变量的指针 /
2 void FunctionExample(va_list pva);
我们可以通过宏va_start()告知函数准备从堆栈中取数据。其中,使用va_start()需要传递两个参数,分别为va_list变量及函数参数列表中“…”左边的第一个形参的名称。例如:
1 va_start(Example,pString); / 告知函数准备开始从可变参数列表Example中取数据 /
与va_start()对应,我们可以通过宏va_end()告知函数不再继续进行参数的提取。例如:
1 va_end(Example); / 结束参数提取 /
在va_start()和va_end()所划定的范围内,我们可以通过va_arg()依次提取所需的参数,其中提取参数的顺序与调用函数时传送参数的顺序相同。例如:
1 unsigned int A = va_arg(Example,unsigned int); / 提取一个unsigned int型的数据 /
也可以通过va_copy为当前的参数列表做一个备份(备份当前的参数读取位置),例如:
/ 保存当前的参数栈 /
va_list ExampleB; / 定一个新的可变参数列表 /
va_copy(ExampleB,Example); / 复制当前的参数栈信息到ExampleB中 /
综合演示
该范例用于实现向指定的设备输出可变数量的字符串。我们首先需要利用函数指针构造一个输出设备驱动函数表,将所有的输出设备已数组的形式组织在一起:
/ 定义输出设备驱动函数的原形 /
typedef void OUTPUT_DRV(unsigned char pstr,va_list pArg);
/ 注意这里不是定义函数指针 /
/ 而是定义了一个函数原形 /
OUTPUT_DRV LCD_Drv; / 定义了一个函数LCD_Drv() /
OUTPUT_DRV PRN_Drv; / 定义了一个函数PRN_Drv() /
/ 定义指向OUTPUT_DRV类型函数的函数指针 /
typedef OUTPUT_DRV P_DRV; / 这里定义了一个函数指针 /
/ 使用函数指针构造了一个驱动函数表 /
P_DRV OutputDrivers【 】 = {
LCD_Drv, PRN_Drv
};
接下来我们将通过可变参数实现一个向指定设备输出类似printf格式字符串的函数,具体的设备需要用户通过字符串的形式给出,例如“LCD”或者“PRN”。该函数将根据用户输入的字符串决定输出的设备和字符串:
#include
#include [span style="color: rgba(0, 0, 255, 1)">string.h>
/ 定义输出设备驱动函数的原形 /
int Print(unsigned char DrvNAME,…)
{
unsigned char pstr = NULL;
P_DRV fnDrv = NULL;
va_list Arg; / 定义可变参数列表 /
/ 健壮性检测 /
if (DrvNAME == NULL)
{
return -1;
}
/ 确定使用哪个设备进行输出 /
if (strcmp(DrvNAME,”LCD”) == 0) / 如果输入的第一个字符串为LCD /
{
fnDrv = OutputDrivers 【0】; / 使用LCD驱动 /
}
else if (strcmp(DrvNAME,”PRN”) == 0) / 如果输入的第一个字符串为PRN /
{
fnDrv = OutputDrivers 【1】; / 使用打印机驱动 /
}
else / 未知的设备 /
{
return -1;
}
va_start(Arg, DrvNAME); / 开始取参数 /
pstr = va_arg(Arg,unsigned char ); / 获取一个字符串 /
fnDrv(pstr,Arg); / 调用指定的设备驱动 /
va_end(Arg); / 结束取参数 /
}
/ 驱动函数实体 /
void LCD_Drv(unsigned char pstr,va_list pArg)
{
……
/ 在函数中可以通过 va_arg(pArg,类型) 来依次提取参数,不需要
通过va_end(pArg)来标注取参数结束,如果通过va_copy生成了
一个新的va_list变量,则需要在取出参数后通过va_end()将该变
量关闭。/
……
}
void PRN_Drv(unsigned char pstr,va_list pArg)
{
……
}
可以使用下面的方式调用函数Print():
1 / Print() 的操作范例 /
2 unsigned char Day = 3;
3 Print(“LCD”, “Its the %dth day of this week.\n”,Day);</p><p>如果LCD驱动编写无误,我们将在LCD设备上看到以下的内容:</p><p>It
s the 3th day of this week.
构建抽象数据类型
抽象数据类型就是对数据结构和相关操作的封装。以前面介绍的字符串输出系统为例,通过改造和对比,你可以很明显找到抽象数据类型和普通数据结构的区别。首先,我们需要分析所需抽象的对象在数据结构上有那些共同的特质,或者说我们如何将对象的共性和特型区别开来。
以字符串输出设备为例,LCD、打印机、显示器、超级终端……这些设备都可以通过数据流的方式进行输出,都在一定程度上兼容流控制,因此可以使用类似的函数接口形式,以“print打印”格式字符串的形式进行输出,基于以上条件我们定义如下的驱动接口函数原型(Prototype):
1 / 定义驱动函数原型 OUTPUT_DRV /
2 typedef int OUTPUT_DRV(unsigned char pstr,va_list pArg);
3 / 定义指向OUTPUT_DRV 的函数指针 /
4 typedef OUTPUT_DRV P_DRV;
接下来,我们可以为不同的驱动指定一个唯一的ID,并将这些信息同驱动接口函数(指针)封装在一起,我们称之为一个输出设备 (Output Device):
1 # include
2 / 定义输出设备(Output Device) /
3 typedef struct
4 {
5 uint16_t ID; / 设备ID /
6 P_DRV fnDriver; / 驱动函数指针 /
7 }OUTPUT_DEV;
这就初步完成了对抽象数据类型的封装。OUTPUT_DEV是一个自定义类型,包含了设备ID和设备的驱动函数指针——这是一个典型的抽象数据类型。当用户使用我们下面将要定义的通用接口函数来操作设备时,所有的具体细节都是透明的,这就是抽象(Abstract)一词的精髓所在:
1 # include
2 typedef uint16_t ERR_CODE;
3 # define ERROR_NONE_ERROR 0x0000
4 # define ERROR_ILLEGAL_PARAMETER 0xFFFF
5 / 设备通用接口函数 /
6 ERR_CODE Print_Device(OUTPUT_DEV pDev,unsigned char pStr,…)
7 {
8 ERR_CODE Err;
9 va_list Arg;
10 if ((pDev == NULL) || (pStr == NULL)) / 强壮性检测 /
11 {
12 return ERROR_ILLEGAL_PARAMETER;
13 }
14 va_start(Arg,pStr);
15 Err = (pDev -> fnDriver)(pStr,Arg);
16 va_end(Arg);
17 return Err; / 无错误产生 /
18 }
<img src="//assets.