概要
-
目的:提供一种用c实现泛型编程的思路
-
环境:
-
操作系统:wsl - ubuntu22.04
-
编译器:gcc 11.4.0
-
C标准:gnu11 (需要用到编译器扩展,并不一定要是GCC,也可以是其他编译器,提供对应的扩展即可)
-
什么是泛型编程
泛型编程,顾名思义,就是 泛化类型 的编程。
通俗点讲,就是一套代码,可以重复使用而无需更改,比如 C++ 中的 模板
比如下面这个加法函数,适用于 int 类型
int add_int(int a, int b) { return a + b; }
如果我们要写另一个加法函数,适用于 double 类型,就需要像下面这样
double add_double(double a, double b) { return a + b; }
我们可以看到,这两个函数的 逻辑 是相同的,只不过 类型 不同。那有没有办法让 一套逻辑 适用于 任何类型 呢?在 C++ 中你可以使用模板
template<typename T>
T add(T a, T b) { return a + b; }
这样 将类型抽象出来 就是 泛型 了
如何抽象类型
类型 可以包含相当多的信息,这里我们先讨论它最基本的信息 --- 大小
比如说,int 类型的大小就可以用 sizeof(int) 得到,double 同理,sizeof(double),这些是 固定大小 的类型
而对于 可变大小 的类型,比如 字符串 ,他的大小应该是 字符串的长度,但我们有时没法直接使用 sizeof(str) 得到字符串的长度,原因是 sizeof() 是编译期就计算出结果,如果你要计算的字符串不是一个 常量,那么他得到的结果就是 一个指针的大小
总结一下,就是 类型的基本信息 --- 大小:固定和可变
然后我们再来讨论变量,变量也可以称作 类型的实例,他除了具有 大小 这个信息之外,还具备另一个基本信息 地址。
比如 int a = 10; sizeof(a) 是这个 整形实例 的 大小 信息,而 &a 是它的 地址 信息。两者相结合就是这个变量的 值 了
总结一下,就是 类型的实例基本上是由大小和地址组成
所有,我们要 抽象类型,换句话说就是 用我们自己的方式来传递类型信息
所以,我们这里已经可以产生一个粗浅但有效的方法了
struct type {
void *addr;
unsigned size;
};
来点实操
我们主要通过实现三个函数,来体会用c实现泛型编程,最后我们能够得到一个 类型无关的动态数组
这三个函数是:(只是原型,我们会逐步优化,形成最终的函数签名)
array_init(); // 初始化动态数组
array_append(); // 在尾部添加一个元素
array_free(); // 销毁动态数组及其元素
首先,我们定义之前分析得到的类型
typedef struct {
void *ptr;
unsigned size;
} any_t;
然后,定义我们的动态数组结构体,动态数组的每个元素都是 any_t 类型
typedef struct {
unsigned count; // 元素个数
unsigned capacity; // 容量
any_t *items; // 元素列表
} array_t;
初始化我们的动态数组
void array_init(array_t *arr)
{
arr->count = 0;
arr->capacity = 0;
arr->items = NULL;
}
实现动态数组的尾部添加
void array_append(array_t *arr, any_t item)
{
// 对动态数组进行扩容
if (arr->capacity <= arr->count) {
arr->capacity = arr->capacity==0 ? 8 : 2*arr->capacity;
arr->items = realloc(arr->items,
sizeof(any_t)*arr->capacity);
assert(arr->items != NULL);
}
// 添加元素到尾部
arr->items[arr->count++] = item;
}
销毁动态数组
void array_free(array_t *arr)
{
if (arr->items) free(arr->items);
arr->count = 0;
arr->capacity = 0;
arr->items = NULL;
}
到这里,我们已经实现完了 动态数组的具体逻辑 了,然后这是我们提供的函数签名
void array_init(array_t *arr)
void array_append(array_t *arr, any_t item)
void array_free(array_t *arr)
很遗憾的是,我们的动态数组还是没办法工作,这是因为我们 实现过程 使用的是我们自己定义的类型 any_t,但 使用过程 使用的是 用户自定义的类型 或者 基础类型
为了解决这个,我们需要一个 解包和包装 的工具,用来实现 用户类型和any_t 之间的转换
但是这里还是有一个问题,那就是,函数的参数是强类型的,我们甚至没法将任意类型的变量传递给函数,更别谈进一步的处理了
所以,我们使用 宏 来实现 转换的工具(因为宏仅仅是文本替换,并不关心类型)
将 any_t 转换为 用户类型
#define ANY_AS(type, any) (*(type *) (any).ptr)
很简单对吧,就是将 any所指向的数据强制解释为用户所指定的类型
将 用户类型 转换为 any_t
#define ANY_OF(value) \
({ \
typeof(value) tmp = (value); \
any_t res; \
res.size = sizeof(tmp); \
res.ptr = malloc(res.size); \
memcpy(res.ptr, &tmp, res.size); \
res; \
})
这就有点复杂了,对吧,不过他的逻辑其实很简单,主要是使用了 typeof 和 语句表达式 这两个 GCC 扩展,具体的使用方法大家可以上网查找
注意到了吗?我们这里又 malloc 了一次,所以要记得更新我们的 array_free
void array_free(array_t *arr)
{
// 释放每个any_t存储数据的缓冲区
for (unsigned i = 0; i < arr->count; i++) {
if (arr[i].ptr) free(arr[i].ptr);
arr[i].ptr = NULL;
arr[i].size = 0;
}
// 释放动态数组存储any_t的缓冲区
if (arr->items) free(arr->items);
arr->count = 0;
arr->capacity = 0;
arr->items = NULL;
}
ok,现在让我们来体验体验吧
int main(void)
{
array_t arr;
array_init(&arr);
for (int i = 0; i < 5; i++) {
array_append(&arr, ANY_OF(i));
}
for (int i = 4; i >= 0; i--) {
printf("%d\n", ANY_AS(int, arr.items[i]));
}
array_free(&arr);
return 0;
}

结果如上图所示
大家也可以换其他类型试试,比如自己定义的结构体
问题
如果大家有试过添加字符串类型的话,会发现,动态数组没法工作,这是因为,字符串 是 可变大小 的,而我们的动态数组目前只能处理 固定大小 的类型
一个比较简单的办法是,你可以将字符串封装在另一个结构体中,然后使用将这个结构体传递给动态数组,这时候就能够正常工作了
struct string {
char *ptr;
};
但是,这并不是我们希望的,试想一下,你想要添加 "hello", 还要将其封装在 struct string 中,然后获取这个字符串,还要再访问他的 ptr 成员
总之,会需要绕一点,但这并不是没有解决办法,我们下期再来解决这个问题
总结
本篇文章给大家演示了 如何通过抽象类型信息来实现泛型编程
不过此时的进度仅适用于 固定大小 的类型,无法适用于 可变大小 的类型
源代码
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
typedef struct {
void *ptr;
unsigned size;
} any_t;
typedef struct {
unsigned count;
unsigned capacity;
any_t *items;
} array_t;
#define ANY_AS(type, any) (*(type *) (any).ptr)
#define ANY_OF(value) \
({ \
typeof(value) tmp = (value); \
any_t res; \
res.size = sizeof(tmp); \
res.ptr = malloc(res.size); \
assert(res.ptr != NULL); \
memcpy(res.ptr, &tmp, res.size); \
res; \
})
void array_init(array_t *arr)
{
arr->count = 0;
arr->capacity = 0;
arr->items = NULL;
}
void array_append(array_t *arr, any_t item)
{
if (arr->capacity <= arr->count) {
arr->capacity = arr->capacity==0 ? 8 : 2*arr->capacity;
arr->items = realloc(arr->items,
sizeof(any_t)*arr->capacity);
assert(arr->items != NULL);
}
arr->items[arr->count++] = item;
}
void array_free(array_t *arr)
{
for (unsigned i = 0; i < arr->count; i++) {
if (arr->items[i].ptr) free(arr->items[i].ptr);
arr->items[i].ptr = NULL;
arr->items[i].size = 0;
}
if (arr->items) free(arr->items);
arr->count = 0;
arr->capacity = 0;
arr->items = NULL;
}
int main(void)
{
array_t arr;
array_init(&arr);
for (int i = 0; i < 5; i++) {
array_append(&arr, ANY_OF(i));
}
for (int i = 4; i >= 0; i--) {
printf("%d\n", ANY_AS(int, arr.items[i]));
}
array_free(&arr);
return 0;
}
posted on
浙公网安备 33010602011771号