目录
挨打
咳咳,大家不要讲话啊,先听我说,我是说挑战一下我的极限,标题纯属忽略您进来的。
先别着急喷,来都来了,看看再走呗,有好东西哦!
开扯
我们都知道,c语言是一门底层语言,拥有着赫赫荣光,然而它的另一位兄弟c艹却广受诟病,更有linus事件引得各方激争。啊,扯远了,我今天不是来说这事的哈,c语言,简洁,以及出色的底层能力使得其广受好评,然而c++就得提它那令人脑袋打结的泛型和stl容器。
传说中c可以实现上层的面向对象的语言,然而小渣渣们也没见过啊,就让大佬们争去吧。抱着这个心态的童鞋们看好了,在下今天就给各位表演一个节目------使用c语言实现c++的泛型,并实现一个简易vector向量容器!
引言
写过面向对象的童鞋回过来再写c的都知道,这c啊,好归好,但限制也多,最简单的一个例子
cl_int errNum;
cl_uint numPlatforms;
cl_platform_id firstPlatformId;
cl_context context = NULL;
errNum = clGetPlatformIDs(1, &firstPlatformId, &numPlatforms);
if (errNum != CL_SUCCESS || numPlatforms <= 0)
{
printf("Failed to find any OpenCL platforms.");
return NULL;
}
errNum = clGetDeviceIDs(firstPlatformId, CL_DEVICE_TYPE_GPU, 1,
device, NULL);
if (errNum != CL_SUCCESS)
{
printf("There is no GPU, trying CPU...");
errNum = clGetDeviceIDs(firstPlatformId,
CL_DEVICE_TYPE_CPU, 1, device, NULL);
}
这是一段使用opencl接口的代码,很容易发现,c接口获取信息很麻烦,总是需要申请内存,然后用缓冲区接收信息。每次我在写的时候就会想,你就不能给我一个数组么。。。啰嗦得要死,这就是没有向量容器的痛苦吗?(大佬们轻喷,引题,引题)。
好了,它来了,还记得c传说中的宏吗,linux内核源码里面最难的宏,击退了多少想研究内核源码的梦。还记得传说中的侵入式链表吗?哈哈哈,我想说其实我也怕。
不过今天,我们要用宏实现一个数组模板,没错,就是哪种支持任何数据类型的数组模板,有了这个东西,你就可以像java一样带着长度到处跑了,传参不要你指定长度,也不会出现那种奇奇怪怪的scanf_s(),strcpy_s()的接口。
开搞
数组么,不就是一个带着长度到处跑的结构嘛,这有什么稀奇的,但是,每单开一个项目,重复写一个数组会不会恶心你,写了int的,明天要用float,后天要用struct,哎,我为什么不是写c++呢,我一个template秒杀!行,今天咱就用c写一个template。
原理:template不就是根据类型展开模板嘛,既然c不展开,我自己展开不行么,所以关键点就是解决两个问题:函数重命名和展开。
先解决第一个问题,重命名,怎么搞勒,c++怎么搞?不就是往函数名字后门加字符串嘛,把Arrary改成Araray_int不就可以了嘛。好了,问题来了,怎么让编译器自己给我加一个尾巴?
端端端的一阵乱翻,思考,哎,还记得这个东西吗?
#define add(p1,p2) "#p1 is not #p2"
没见过的可以先去查了,比较不常见。
那么这个可以实现吗?显然不行,替换要单串识别啊,有空格才能用,比如
#define add(p1,p2) #p1#p2 可以不?
可以去试一下哈,应该是不行的。
还有没有办法,当然有,一个不行,那就加俩##
//函数命名重定义解决
#define RENAME(name,param) Template_##name##_##param
##可以连接字串,这个操作就是把指定两个参数替换成Template_XXX_XXX字串了。那为什么可以解决重命名呢?如果name是Array,param是int,如果我使用这个去替换类型名,是不就得到了Array_int了,如果是float,那就是Array_float,是不是很神奇?比如
struct RENAME(ARRAY,type){ };
第一个问题解决了,下面解决第二个问题,怎么展开?
请看vcr
#define add(type) \
++type;
注意这个\,这个很重要,有这个代码才能读,不然代码要直通天际了。
简单组合一下
#define Template_Array(type) \
struct RENAME(Array,type){ };
哎,好像可以替换是吧,如果type=int,那么就展开成struct Template_Array_int{},嘿嘿,编译器这回不会认错了吧,尾巴都不一样了。
正题
好,来实现。
//函数命名重定义解决
#define RENAME(name,param) Template_##name##_##param
//泛型数组
#define Template_Array(type) \
struct RENAME(Array,type){ \
const size_t length;\
type* const head;\
};
为啥安全,长度是常量,只要我不捉死改它类型,它就很安全。还有一个是把指针定义成常量指针(名字不一定对啊,大家知道就行),嘻嘻,数组内容你随便改,指针指向你不准动,你敢动它就报错,我都提醒你了还要动?泄露了那是你命有此结!
来一个例子试试哈
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#pragma warning( disable : 4996 )
typedef struct A {
char* name;
}A;
//手动展开
Template_Array(A)
typedef struct Template_Array_A A_Array;
//正常情况下,如果指针域数组成员存在堆资源,那么释放时需要指定数组长度以供遍历释放
void free_function(A_Array* array) {
int i = 0;
for (i = 0; i < array->length; i++) {
free(array->head[i].name);
}
free(array->head);
}
int main() {
char* p = NULL;
int i = 0;
A* A_ptr = (A*)malloc(sizeof(A) * 3);
for (i = 0; i < 3; i++) {
p = (char*)malloc(5);
strcpy(p, "abc");
p[3] = i + '0';
p[4] = '\0';
A_ptr[i].name = p;
}
A_Array array = { 3,A_ptr };
for (i = 0; i < 3; i++) {
printf("%d: %s\n", i, array.head[i].name);
}
free_function(&array);
return 0;
}
跑一跑
哦,没有问题!
为什么要用这个例子呢?主要还是有代表性,像这种需要遍历来释放元素资源的处理函数,很容易造成内存泄露。如果在开始分配时就记得先写释放还好,如果只是记着,代码写长了,申请释放组写多了,哎,我是哪里还申请内存来着?当然,还有一个优点是写释放或者子处理过程方便多了,再也没必要写这种scanf_s()这种啰嗦东西。子过程也安心,这个长度很放心,没作死就不会错,安全检查这个环节就可以省下来了。
提升
还记得开题前说的向量容器吗?既然数组可以实现,那么向量肯定没问题,说干就干,开整!
//泛型类声明,以供成员函数引用
#define Vector_statement(type) \
struct RENAME(Vector,type);
//成员函数声明,以供类定义引用
#define Vector_function_statement(type) \
typedef void ( *RENAME(void_param, type) )(struct RENAME(Vector,type)* ); \
typedef void ( *RENAME(type_param_function,type) )(struct RENAME(Vector,type)* ,const type *); \
typedef void ( *RENAME(Move_function,type) )(struct RENAME(Vector,type)* ,struct RENAME(Vector,type)*); \
typedef void ( *RENAME(Copy_function,type) )(struct RENAME(Vector,type)* ,const struct RENAME(Vector,type)*);
注意,面向对象思想就是在结构体力面加几个成员函数的指针,不过c语言不支持静态成员,也就是说实例得多占几个指针空间了。这里声明的意义是解决嵌套引用问题,第二部分定义函数指针以供结构体定义成员。
向量声明
//泛型类定义
//泛型向量
#define Vector_define(type) \
Vector_statement(type) \
Vector_function_statement(type) \
struct RENAME(Vector,type) {\
size_t length;\
size_t capacity;\
type* value;\
RENAME(void_param,type) destructor; \
RENAME(type_param_function,type) push_back; \
RENAME(Move_function,type) move; \
RENAME(Copy_function,type) copy; \
};
可以看见我们简单提供了复制函数和移动函数,这会使得相互赋值简单许多,同时给了一个添加函数,这使得我们的向量可以扩展。至于【】运算c里面不支持,但是struct是public,利用指针就可以直接访问了。注意:我们不提供构造函数,原因是我们需要给实例的成员函数赋值,那么构造函数就单独提出来,也就是说除了构造函数,其他的就可以像类一样直接调用了,利用代码提示,这效率就嘎嘎上来了。注意到我们在定义向量前调用了声明宏,这使得在向量声明前相关函数声明以及展开,不会出现未定义符号问题。
成员函数实现
//泛型函数实现
//构造函数实现
//析构函数实现
//插入函数实现
//移动函数实现
//复制函数实现
#define Vector_function_implement(type) \
void RENAME(destructor_impl,type)(struct RENAME(Vector,type)* pthis){ \
if(pthis->value!=NULL ) \
free(pthis->value); \
pthis->value=NULL; \
} \
void RENAME(push_back_impl,type)(struct RENAME(Vector,type)* pthis,const type* value){\
if(pthis->length<pthis->capacity){ \
pthis->value[pthis->length]=*value;\
}\
else{ \
size_t i = 1; \
while (i<=pthis->capacity){ \
i*=2; \
} \
type* newh=(type*)malloc(sizeof(type)*i); \
memcpy(newh,pthis->value,sizeof(type) * (pthis->length)); \
free(pthis->value); \
pthis->value = newh; \
pthis->value[pthis->length]=*value; \
pthis->capacity=i; \
} \
++pthis->length; \
}; \
void RENAME(move_impl,type)(struct RENAME(Vector,type)* pthis,struct RENAME(Vector,type)* src) { \
if (pthis->value != NULL) { \
free(pthis->value); \
}\
pthis->length=src->length; \
pthis->capacity=src->capacity; \
pthis->value = src->value; \
src->value = NULL; \
src->length=0; \
src->capacity=0; \
}; \
void RENAME(copy_impl,type)(struct RENAME(Vector,type)* pthis,const struct RENAME(Vector,type)* src) { \
if (pthis->value != NULL) { \
free(pthis->value); \
}\
pthis->value=(type*)malloc(src->capacity);\
memcpy(pthis->value, src->value, sizeof(type) *src->length); \
pthis->length=src->length; \
pthis->capacity=src->capacity; \
}; \
void RENAME(constructor_impl,type)(struct RENAME(Vector,type)* pthis) {\
pthis->length = 0;\
pthis->capacity = 0; \
pthis->value = NULL; \
pthis->destructor = RENAME(destructor_impl,type); \
pthis->push_back = RENAME(push_back_impl,type); \
pthis->move = RENAME(move_impl,type); \
pthis->copy = RENAME(copy_impl,type); \
}
利用 \ ,我们可以把实现定义成模板,这样就可以根据指定类型进行展开,得到相应版本的成员函数,同时利用重命名宏,我们还解决了多个用例存在时命名冲突问题。
最后,我们利用
#define Template_Vector(type) \
Vector_define(type) \
Vector_function_implement(type)\
来定义展开顺序,这样就可以像之前一样手动展开了,统一风格又简洁!
来跑一个例子看看
Template_Vector(int)
typedef struct Template_Vector_int Int_Vector;
#include<stdio.h>
int main(){
Int_Vector vector,vector2;
Template_constructor_impl_int(&vector);
Template_constructor_impl_int(&vector2);
int i = 5;
vector.push_back(&vector, &i);
++i;
vector.push_back(&vector, &i);
printf("v1[0]: %d\nv1[1]: %d\n",vector.value[0],vector.value[1]);
vector2.move(&vector2, &vector);
printf("v1[0]: %d\nv1[1]: %d\n", vector2.value[0], vector2.value[1]);
vector.destructor(&vector);
vector2.destructor(&vector);
return 0;
}
结果:
完美运行!(我私下改的时候可狼狈了,哈哈)
结尾
这是全部头文件代码
#pragma once
#include<stdlib.h>
#include<string.h>
//函数命名重定义解决
#define RENAME(name,param) Template_##name##_##param
//泛型数组
#define Template_Array(type) \
struct RENAME(Array,type){ \
const size_t length;\
type* const head;\
}; //不能带;以供typedef定义展开
//泛型类声明,以供成员函数引用
#define Vector_statement(type) \
struct RENAME(Vector,type);
//成员函数声明,以供类定义引用
#define Vector_function_statement(type) \
typedef void ( *RENAME(void_param, type) )(struct RENAME(Vector,type)* ); \
typedef void ( *RENAME(type_param_function,type) )(struct RENAME(Vector,type)* ,const type *); \
typedef void ( *RENAME(Move_function,type) )(struct RENAME(Vector,type)* ,struct RENAME(Vector,type)*); \
typedef void ( *RENAME(Copy_function,type) )(struct RENAME(Vector,type)* ,const struct RENAME(Vector,type)*);
//泛型类定义
//泛型向量
#define Vector_define(type) \
Vector_statement(type) \
Vector_function_statement(type) \
struct RENAME(Vector,type) {\
size_t length;\
size_t capacity;\
type* value;\
RENAME(void_param,type) destructor; \
RENAME(type_param_function,type) push_back; \
RENAME(Move_function,type) move; \
RENAME(Copy_function,type) copy; \
};
//泛型函数实现
//构造函数实现
//析构函数实现
//插入函数实现
//移动函数实现
//复制函数实现
#define Vector_function_implement(type) \
void RENAME(destructor_impl,type)(struct RENAME(Vector,type)* pthis){ \
if(pthis->value!=NULL ) \
free(pthis->value); \
pthis->value=NULL; \
} \
void RENAME(push_back_impl,type)(struct RENAME(Vector,type)* pthis,const type* value){\
if(pthis->length<pthis->capacity){ \
pthis->value[pthis->length]=*value;\
}\
else{ \
size_t i = 1; \
while (i<=pthis->capacity){ \
i*=2; \
} \
type* newh=(type*)malloc(sizeof(type)*i); \
memcpy(newh,pthis->value,sizeof(type) * (pthis->length)); \
free(pthis->value); \
pthis->value = newh; \
pthis->value[pthis->length]=*value; \
pthis->capacity=i; \
} \
++pthis->length; \
}; \
void RENAME(move_impl,type)(struct RENAME(Vector,type)* pthis,struct RENAME(Vector,type)* src) { \
if (pthis->value != NULL) { \
free(pthis->value); \
}\
pthis->length=src->length; \
pthis->capacity=src->capacity; \
pthis->value = src->value; \
src->value = NULL; \
src->length=0; \
src->capacity=0; \
}; \
void RENAME(copy_impl,type)(struct RENAME(Vector,type)* pthis,const struct RENAME(Vector,type)* src) { \
if (pthis->value != NULL) { \
free(pthis->value); \
}\
pthis->value=(type*)malloc(src->capacity);\
memcpy(pthis->value, src->value, sizeof(type) *src->length); \
pthis->length=src->length; \
pthis->capacity=src->capacity; \
}; \
void RENAME(constructor_impl,type)(struct RENAME(Vector,type)* pthis) {\
pthis->length = 0;\
pthis->capacity = 0; \
pthis->value = NULL; \
pthis->destructor = RENAME(destructor_impl,type); \
pthis->push_back = RENAME(push_back_impl,type); \
pthis->move = RENAME(move_impl,type); \
pthis->copy = RENAME(copy_impl,type); \
}
#define Template_Vector(type) \
Vector_define(type) \
Vector_function_implement(type)
更新:
这样的方式使得c语言也能使用模板来缩减工作量,参考stl实现,我们还可以实现set,map,list等相应的容器。例如可以这样定义键值对
//泛型键值对
#define Template_Pair(Key,Value) \
struct RENAME(Pair,Key##_##Value){ \
Key key; \
Value value; \
};
通过键值对,想来实现一个map应该不是难事。 细心的小伙伴会发现其实并不一定要RENAME宏来处理重命名,或许直接拼接可能更直观,比如上面的key##_##value,当然,这也是本文的bug之一,或许代码就是这样修修补补的过程。
虽然这样的实现有点那种炫技的嫌疑,不过不确定是否有人会有这样的需要。当然了,也可能有人会说都这样了,我干嘛不去写c++呢?要我说:您说对,确实没必要这么麻烦。原因一是这种写法并不像c++那么简洁,自定义模板很麻烦,由于c语言宏定义的问题,这样的代码没办法调试。展开后的源码没有办法断点调式,其一会导致定义模版很复杂,其二是源代码难调式。当然,对于模板定义复杂问题也可以这样解决,先实现一个int版本,调式无碍后再将其改成对应模板。不过,这也是一定要这么做的人需要了。其二的原因是本身c++是c的超集,实在麻烦,大可使用g++等c++编译器去写c代码也行,按照c语法,同时只添加template功能,也会使得这个过程简便更多,不过需要注意extern “C”导出c函数命名接口。可以这么实现的另外一点是江湖上流传着使用c++编译器编译c代码能提升性能的传说,当然,这点我没去求证过,感兴趣的小伙伴可以去试试。