前言
通常情况下,C语言并不具备泛型及重载功能。而指针赋予了C语言无限的可能,本文中所描述的C语言泛型及函数重载,就是在大量使用指针下实现的。本文以实现基本数据结构链表为例,围绕两个核心 void* 和不定参数展开。
链表有关的结构体
typedef struct _dualNode
{
void* value;
struct _dualNode * pre;
struct _dualNode * next;
}DualNode;
typedef struct
{
int length;
int elemSize;
DualNode * head;
DualNode * tail;
}DualList;
在节点结构体定义中,节点的值并不使用固定的类型,或者是宏定义的数据类型。节点的值用一个 void* 的指针 value 来指向。当节点被创建时,其 value 也申请与链表数据类型大小相等的内存,并把节点值按照字节拷贝进去(显而易见,这是浅拷贝)。用宏定义来决定数据类型,看似好像是有点泛型的影子,但并不具备实质上的泛型。
如果想更好的封装链表,可以再引入一个迭代器(Iterator)结构体,并实现相关的功能。
链表的基本操作
- 创建链表
DualList createDualList(const int elemSize)
{
DualList ans;
ans.length = 0;
ans.elemSize = elemSize;
ans.head = NULL;
ans.tail = NULL;
return ans;
}
这个函数创建了一个存储大小为 elemSize 数据类型的链表。初始状态下链表为空。
- 向链表中插入元素(非不定参数版)
void push_back_DualList(DualList *src,const void* value)
{
if(src->length==0)
{
src->head = malloc(sizeof(DualNode));
src->head->pre = NULL;
src->head->value = malloc(src->elemSize);
memcpy(src->head->value,value,src->elemSize);
src->head->next = NULL;
(src->length)++;
src->tail = src->head;
return;
}
src->tail->next = malloc(sizeof(DualNode));
src->tail->next->pre = src->tail;
src->tail = src->tail->next;
src->tail->value = malloc(src->elemSize);
memcpy(src->tail->value,value,src->elemSize);
src->tail->next = NULL;
(src->length)++;
}
这个函数实现了向链表的末尾插入一个元素的功能。但是这个函数在某些使用场景并不方便,比如我建立一个存储 int 的链表,我想往链表里存储 1,我只能这样写:
DualList list = createDualList(sizeof(int));
int value = 1;
push_back_DualList(&list,&value);
看上去无伤大雅,但是需要再定义一个 value,总感觉有点别扭…用C语言实现一个伪函数重载,就显得有十分的意义。
- 向链表中插入元素(不定参数版)
首先要包含头文件
#include <stdarg.h>
这是不定参数有关数据类型及函数(实际上是宏)的头文件。
和非不定参数版相比,使用不定参数只需要额外三行:
void push_back(DualList* src,...)
{
va_list list;
va_start(list,src);
void* value = (void*)list;
if(src->length==0)
{
src->head = malloc(sizeof(DualNode));
src->head->pre = NULL;
src->head->value = malloc(src->elemSize);
memcpy(src->head->value,value,src->elemSize);
src->head->next = NULL;
(src->length)++;
src->tail = src->head;
return;
}
src->tail->next = malloc(sizeof(DualNode));
src->tail->next->pre = src->tail;
src->tail = src->tail->next;
src->tail->value = malloc(src->elemSize);
memcpy(src->tail->value,value,src->elemSize);
src->tail->next = NULL;
(src->length)++;
va_end(list);
}
上述操作可以看到,list 实际上就是一个指针,在经过 va_start 宏的处理后,list 指向了不定参数的第一个参数。(正因为 va_start 是宏不是函数,所以 list 不需要取址)我们约定,虽然在这里使用了不定参数,但是我们需要的是不定参数的不定类型,而不是不定数量。在使用此函数时,我们应该坚持只传入两个参数(实际上也只会处理两个参数)。
这样,向链表中插入元素就方便多了:
DualList list = createDualList(sizeof(int));
push_back(&list,1);
- 其他的操作
至于对链表的其他的操作,就与泛型关系不是很大了,在对节点进行操作时注意内存管理就可以了。
不定参数的缺陷
不定参数在使用中其实是有十分致命的缺陷的,因为:
在C语言中,调用一个不带原型声明的函数时:
调用者会对每个参数执行“默认实际参数提升(default argument promotions)。
同时,对可变长参数列表超出最后一个有类型声明的形式参数之后的每一个实际参数,也将执行上述提升工作。
提升工作如下:
——float类型的实际参数将提升到double
——char、short和相应的signed、unsigned类型的实际参数提升到int
——如果int不能存储原值,则提升到unsigned int
然后,调用者将提升后的参数传递给被调用者。
缺陷十分明了:不定参数无法处理 float, char, short 等数据类型。
因此,我个人建议使用时尽量用自定义的结构体包括基础数据类型,或者直接停止不定参数的使用。
多说两句
写完不定参数版 push_back 函数,我自然而然的想到实现一个通过不定参数进行批量插入元素的函数。但实际上并不好实现。假设有函数原型:
void push_backs(DualList* src,int count,...);
在这个函数中,我们先有操作:
va_list list;
va_start(list,count);
void* value = (void*)list;
这样我们可以顺利的读取第一个不定参数。但是从第二个不定参数开始就遇到了麻烦:我们无法正确地得到后面不定参数的地址。
首先,直接对 value 进行地址的加减是无法得到正确的地址的。因为操作系统及编译器的不同,程序运行时不定参数在内存中的存储并不是连续的,纯粹是一个“玄学”问题。
其次,使用 va_arg 宏也不能解决问题。因为我们不能在写代码时就决定参数的数据类型。
因此,这个函数实现非常复杂。(至少我无法处理)
结尾
C++ 永远滴神(
如有错误,望不吝赐教。