在这篇博文中我们准备一步一步的使用C来实现一个支持泛型,众所周知,C++
有模板机制来支撑泛型(所谓的泛型栈其实就是指一个可以存放任何类型包括用户自定义类型的栈),那么对于C我们如何来支持泛型呢,下面我们将一步一步来现实这个需求:
一:首先我们先实现一个特定类型的栈(比如用来存储整型的栈),来看一下实现栈一般
都需要哪些内容:
我们分三个文件来实现(stack.h;stack.c;Main.c):
stack.h //声明文件,里面包括对Stack以及相应操作(函数)的定义。
///
// Stack.h : declaration file
//
// Version: 1.1
// Date: 12 September 2014
// Author: Yu maosheng
// Email: yums467@nenu.edu.cn
// Copyright (c) 20014. All Rights Reserved.
//
// History:
/*
12.12.2014 11:12 Yu maosheng
Added int类型 Stack declaration;
*/
// 定义一个整型栈
typedef struct
{
// 指向栈空间的元素
int *element;
// 栈顶指针
int logLength;
// 初始化时栈的长度
int allocLength;
}stack;
//声明 初始化栈的函数
void stackNew(stack *s);
// 声明 销毁栈的函数
void stackDispose(stack *s);
// 声明 向栈中推入一个元素的函数
void stackPush(stack *s, int value);
// 声明 从栈中退出一个元素的函数
int stackPop(stack *s);
通过这个头文件我们可以看出实现一个栈,我们需要一个栈空间,一个指示栈顶
元素的标示,为了实现方便我们还添加了一个指示栈空间大小的元素。
同时我们还有针对栈实现的操作:初始化栈,推入一个元素,退出一个元素,
销毁栈。
下面Stack.c文件是对Stack.h里面声明的函数的实现。
///
// Stack.c : Implementation file
//
// Version: 1.1
// Date: 12 September 2014
// Author: Yu maosheng
// Email: yums467@nenu.edu.cn
// Copyright (c) 20014. All Rights Reserved.
//
// History:
/*
12.12.2014 11:12 Yu maosheng
Added int Stack function Implementation;
*/
#include <stdlib.h>
#include <assert.h>
#include "Stack.h"
//stackNew的实现
void stackNew(stack *s)
{
//初始化栈顶的位置
s->logLength = 0;
//设定栈的初始大小(应该理解为所存放元素个数)
s->allocLength = 4;
//分配栈空间
s->element = malloc(s->allocLength * sizeof(int));
//检查地址是否分配成功
assert(s->element != NULL);
return;
}
//stackDispose的实现
void stackDispose(stack *s)
{
//释放栈空间
free(s->element);
return;
}
//stackPush的实现
void stackPush(stack *s, int value)
{
//检查栈空间是否已满,如果满则拓展空间
if (s->logLength == s->allocLength)
{
s->allocLength *= 2;
s->element = realloc(s->element, s->allocLength * sizeof(int));
assert(s->element != NULL);
}
//将元素放到栈顶位置
//printf("%d \n", s->logLength);
s->element[s->logLength++] = value;
return;
}
//stackPop函数的实现
int stackPop(stack *s)
{
//将栈顶元素弹出
assert(s->logLength > 0);
return s->element[--s->logLength];
}
经过上面的代码我们就实现了具体栈上的操作。下面我们来对这个栈进行
测试工作,看看咱们是否已经正确实现了这个栈。
Main.c
///
// Main.c : Test file
//
// Version: 1.1
// Date: 12 September 2014
// Author: Yu maosheng
// Email: yums467@nenu.edu.cn
// Copyright (c) 20014. All Rights Reserved.
//
// History:
/*
12.12.2014 18:00 Yu maosheng
Added int Stack Test in Main ;
*/
#include "Stack.h"
int main(int argc, char const *argv[])
{
stack s;
stackNew(&s);
for (int i = 0; i < 4; ++i)
{
stackPush(&s, i);
}
printf("%d\n", stackPop(&s));
//销毁栈
stackDispose(&s);
return 0;
}
执行后,程序在控制台输出了3,这说明我们的实现是正确的!
二、按照上文所述,我们已经实现了一个可以存放整型数据的栈,并实现了
栈上的操作(栈的初始化、入栈、出栈、销毁栈),然而对于一个栈,我们不能
仅仅用来存放整型数据呀,如果我想存放double类型的数据怎么办呢?一种
方式是我们再实现一个double类型的栈,那如果我又想存放一个自定义类型的
数据呢?看来上面的方式是行不通了,在这里我们将要实现一个支持泛型的
栈,实现一个泛型栈的技巧则是利用void指针。
首先我们要修改结构体stack的定义,将指向栈空间的指针元素element由int*修
改为viod*,同时添加一个指示用户数据结构大小的变量int elemSize。
然后我们还要修改关于stack一些操作的函数的声明:
//声明 初始化栈的函数
void stackNew(stack *s, int elemSize);
函数参数添加一项 int elemSize,这一项将用在为栈开辟
空间时决定每一个元
素的大小的。
// 声明 向栈中推入一个元素的函数
//void stackPush(stack *s, int value);
void stackPush(stack *s, void *elemAddr);
添加参数 void *elemAddr,用来指示要入栈元素的地址
// 声明 从栈中退出一个元素的函数
//int stackPop(stack *s);
void stackPop(stack *s,void *elemAddr);
添加参数 void *elemAddr,用来指示用来存储出栈元素的地址
这就是stack.h所做的修改
到现在stack.h文件代码如下:
///
// Stack.h : declaration file
//
// Version: 1.1
// Date: 12 September 2014
// Author: Yu maosheng
// Email: yums467@nenu.edu.cn
// Copyright (c) 20014. All Rights Reserved.
//
// History:
/*
12.12.2014 11:12 Yu maosheng
Added int类型 Stack declaration;
12.12.2014 18:37 Yu maosheng
修改了stack结构体的定义;
12.13 2014 13:42 Yu maosheng
修改了stackNew,stackPush,stackPop三个函数的定义
*/
// 定义一个整型栈
typedef struct
{
// 指向栈空间的元素
//int *element;
//支持泛型,重新定义栈空间元素
void *element;
//添加一项指示用户数据结构大小的变量
int elemSize;
// 栈顶指针
int logLength;
// 初始化时栈的长度
int allocLength;
}stack;
//声明 初始化栈的函数
//void stackNew(stack *s);
void stackNew(stack *s, int elemSize);
// 声明 销毁栈的函数
void stackDispose(stack *s);
// 声明 向栈中推入一个元素的函数
//void stackPush(stack *s, int value);
void stackPush(stack *s, void *elemAddr);
// 声明 从栈中退出一个元素的函数
//int stackPop(stack *s);
void stackPop(stack *s,void *elemAddr);
继续,我们来修改stack.c文件,修改上述函数的具体实现。
1.stackNew函数要修改分配空间的代码,空间的大小不再是
s->allocLength * sizeof(int),而是s->allocLength * elemSize。
void stackNew(stack *s, int elemSize)
{
//初始化栈顶的位置
s->logLength = 0;
//设定栈的初始大小
s->allocLength = 4;
//给栈成员elemSize赋值
s->elemSize = elemSize;
//分配栈空间
s->element = malloc(s->allocLength * elemSize);
//检查地址是否分配成功
assert(s->element != NULL);
return;
}
2.stackPush函数要修改空间增长的代码,元素入栈代码。
为了代码的优雅,我们将空间增长代码也封装成一个函数stackGrow(),
在获得栈顶指针的时候,我们不能再单纯的使用栈顶元素logLength指示了,
我们需要从具体的内存地址中找到相应的栈顶位置。
void *target = (char *)(s->element) + s->logLength * s->elemSize;
target指针指示的就是栈顶位置的内存地址,我们将其要入栈元素的地址拷贝到我们的栈顶地址
中去:
memcpy(target, elemAddr, s->elemSize);
具体实现代码如下:
void stackPush(stack *s, void *elemAddr)
{
//检查栈空间是否已满,如果满则拓展空间
if (s->logLength == s->allocLength)
{
//s->allocLength *= 2;
//s->element = realloc(s->element, s->allocLength * sizeof(int));
//调用增长栈空间的函数
stackGrow(s);
assert(s->element != NULL);
}
//将元素放到栈顶位置
//printf("%d \n", s->logLength);
//s->element[s->logLength++] = value;
//获得栈顶地址
void *target = (char *)(s->element) + s->logLength * s->elemSize;
memcpy(target, elemAddr, s->elemSize);
s->logLength++;
//printf("%d %d %s\n", s->logLength, s->elemSize, (char *)(s->element) + (s->logLength - 1) * s->elemSize);
return;
}
3.stackPop函数跟stackPush修改的地方类似,主要是寻找栈顶元素地址方式的改变,
我们用source指针来指示栈顶地址。
void stackPop(stack *s, void *elemAddr)
{
//将栈顶元素弹出
assert(s->logLength > 0);
//s->logLength--;
void *source = (char *)(s->element) + (s->logLength - 1) * s->elemSize;
memcpy(elemAddr, source, s->elemSize);
s->logLength--;
printf("Pop:%d ", s->logLength);
//return s->element[--s->logLength];
}
那么stack.c总体的代码如下:
///
// Stack.c : Implementation file
//
// Version: 1.1
// Date: 12 September 2014
// Author: Yu maosheng
// Email: yums467@nenu.edu.cn
// Copyright (c) 20014. All Rights Reserved.
//
// History:
/*
12.12.2014 11:12 Yu maosheng
Added int Stack function Implementation;
12.13.2014 13:44 Yu maosheng
重新修改了stackNew,stackPush,stackPop的实现
添加了stackGrow函数
*/
#include <stdlib.h>
#include <stdio.h>
#include <assert.h>
#include <string.h>
#include "Stack.h"
//增长栈空间的函数
static void stackGrow(stack *s)
{
s->allocLength *= 2;
s->element = realloc(s->element, s->allocLength * s->elemSize);
return;
}
//stackNew的实现
void stackNew(stack *s, int elemSize)
{
//初始化栈顶的位置
s->logLength = 0;
//设定栈的初始大小
s->allocLength = 4;
//给栈成员elemSize赋值
s->elemSize = elemSize;
//分配栈空间
s->element = malloc(s->allocLength * elemSize);
//检查地址是否分配成功
assert(s->element != NULL);
return;
}
//stackDispose的实现
void stackDispose(stack *s)
{
//释放栈空间
free(s->element);
return;
}
//stackPush的实现
void stackPush(stack *s, void *elemAddr)
{
//检查栈空间是否已满,如果满则拓展空间
if (s->logLength == s->allocLength)
{
//s->allocLength *= 2;
//s->element = realloc(s->element, s->allocLength * sizeof(int));
//调用增长栈空间的函数
stackGrow(s);
assert(s->element != NULL);
}
//将元素放到栈顶位置
//printf("%d \n", s->logLength);
//s->element[s->logLength++] = value;
//获得栈顶地址
void *target = (char *)(s->element) + s->logLength * s->elemSize;
memcpy(target, elemAddr, s->elemSize);
s->logLength++;
//printf("%d %d %s\n", s->logLength, s->elemSize, (char *)(s->element) + (s->logLength - 1) * s->elemSize);
return;
}
//stackPop函数的实现
void stackPop(stack *s, void *elemAddr)
{
//将栈顶元素弹出
assert(s->logLength > 0);
//s->logLength--;
void *source = (char *)(s->element) + (s->logLength - 1) * s->elemSize;
memcpy(elemAddr, source, s->elemSize);
s->logLength--;
printf("Pop:%d ", s->logLength);
//return s->element[--s->logLength];
}
接下来我们使用Main.c文件来测试
///
// Main.c : Test file
//
// Version: 1.1
// Date: 12 September 2014
// Author: Yu maosheng
// Email: yums467@nenu.edu.cn
// Copyright (c) 20014. All Rights Reserved.
//
// History:
/*
12.12.2014 18:00 Yu maosheng
Added int Stack Test in Main ;
12.13.2014 14:22 Yu maosheng
对支持泛型的栈添加测试用例(字符串数组)
2014 12.19 13:00 Yu maosheng
测试程序,寻找在哪出现了段错误
stackPush(&strStack, copy);错误在这:应该取copy变量的地址作为push函数的参数
stackPush(&strStack, ©);像这个样子
因为向栈中写入的应该是字符串的地址而不是字符串本身
*/
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include "Stack.h"
#pragma warning(disable:4996)//之所以加这一句是因为strdup报错
int main(int argc, char const *argv[])
{
const char *friends[] = {"Alex","Bob","Caser"};
stack strStack;
stackNew(&strStack, sizeof(char *));
for (int i = 0; i < 3; ++i)
{
//stackPush(&s, i);
//strdup功 能: 将串拷贝到新建的位置处
//strdup在内部调用了malloc()为变量分配内存,
//不需要使用返回的字符串时,需要用free()释放
//相应的内存空间,否则会造成内存泄漏。
char *copy = strdup(friends[i]);
stackPush(&strStack, ©);
//free(copy);
}
for (int j = 0; j < 3; j++)
{
char *name;
name = malloc(sizeof(char *));
stackPop(&strStack, &name);
printf("%s\n", name);
free(name);
}
//printf("%d\n", stackPop(&s));
//销毁栈
stackDispose(&strStack);
printf("Over\n");
return 0;
}
我们向栈中推送三个字符串,然后将这三个字符串再推出栈并输出到控制台中:
在这我想强调一点,以前我们在实现整型栈的时候栈空间元素存放的是入栈元素
本身的值,而对于泛型栈,我们在泛型栈空间里存放的是指向元素的指针。
拿上面那三个字符串为例,我们在栈中存放的是指向"Carse"的指针,而不是"Carse"
本身。
写到这,貌似问题都解决了吧? 是吗? 如果我们的栈内元素本身就是指针或者句柄
怎么办(他们本身也开辟了存储空间,我们需要释放这些空间)?在这里我们要修改
stack结构体的定义,在里面添加一项用来指向函数的指针成员,这个函数具体让用
户自己定义来实现释放自定义数据类型空间。
void (*freefn)(void *element);
我们在stackNew函数中给这个结构体中函数指针赋值
修改stackNew如下:
void stackNew(stack *s, int elemSize, void (*freefn)(void *element))
{
//初始化栈顶的位置
s->logLength = 0;
//设定栈的初始大小
s->allocLength = 4;
//给栈成员elemSize赋值
s->elemSize = elemSize;
//给函数指针赋值
s->freefn = freefn;
//分配栈空间
s->element = malloc(s->allocLength * elemSize);
//检查地址是否分配成功
assert(s->element != NULL);
return;
}
给stackNew函数添加一个参数,用来初始化栈的指针函数成员。
void stackDispose(stack *s)
{
if (s->freefn != NULL)
{
for (int i = 0; i < s->logLength; i++)
{
s->freefn((char *)s->element + i * s->elemSize);
}
}
//释放栈空间
free(s->element);
return;
}
在实际应用中我们根据自己定义的数据结构空间的申请方式来定义
void (*freefn)(void *element);
的具体实现函数,并作为一个参数在栈初始化的时候赋值。
到此我们就完整的实现了一个支持泛型的栈。