概要

  • 目的:提供一种用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;
}

1

结果如上图所示

大家也可以换其他类型试试,比如自己定义的结构体

问题

如果大家有试过添加字符串类型的话,会发现,动态数组没法工作,这是因为,字符串可变大小 的,而我们的动态数组目前只能处理 固定大小 的类型

一个比较简单的办法是,你可以将字符串封装在另一个结构体中,然后使用将这个结构体传递给动态数组,这时候就能够正常工作了

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 2025-06-26 23:39  Dylaris  阅读(73)  评论(0)    收藏  举报