C语言基于void*实现泛型容器化代码编程

前情提要

上一篇博文 基于C语言实现泛型编程(抛砖引玉) 中实现了一种泛型窗口,和操作该窗口的一些泛型方法(算法)。但是实现泛型的方法太简单粗暴了,大体思路就是:

  1. 定义一个数据结构,存储数据,数据类型,窗口大小,序列号等,并在初始化时全部设定好。
  2. 每个函数中都获取该窗口的类型,然后根据类型的不同编写不同的实现。

虽然也实现了任意类型,任意长度的窗口,操作接口一致性,达到了最基础的泛型的要求,但是代码体积太过庞大,每次新增一个类型,就要改每个函数里的实现,这显然不符合我心中的版本。于是花了一天时间做了下优化。



前言

C++通过模板,多态实现泛型编程,其中蕴含的细节编译器已经实现完全了。模板会根据传入类型,自动生成符合该类型的实现,也就是编译的时候帮你写好了这个类型对应的函数体,编译出的可执行文件当然会变大,臃肿,不过都是可以接收的。

C语言就没有这么幸运了,我刚开始看公司项目代码时,针对不同类型(uint8_t , int8_t , int , uint , float , double … )实现了很多个队列,栈,环形缓冲区,代码加起来大几千行,每种类型对应的接口还不一样,一个sort_uint8_t , sort_float … 都写了很多遍,看的人头痛。

于是想写一个类似C++ 中STL里实现的那种泛型容器,实现针对不同类型,接口一致化。

实现泛型的思路

泛型,统一不同的类型,对C程序员来讲应当很容易,所谓变量,不就是一个地址,加上从这个地址开始,有效数据长度,知道这两个信息,就等于能反推整个变量的值。

在这里插入图片描述
身为一个C程序员,应当从内存的角度,从编译器的角度去思考问题。这里就简化下模型,固定在32位系统下,忽略大小端,泛型的本质就是编译器掌握了变量的类型和变量地址,有了类型就知道从这个地址开始,往后(往前,根据出入栈模型不同)多少个字节是有意义的,从这个地址开始,往后多少个字节组合在一起,可以还原整个变量。

想保证接口统一,那么每个接口都通过void* 来操作容器,有了void * 就有了地址,现在还差长度,这个也简单,在初始化时根据容器类型,设定成员大小。


一、泛化的容器

C++ STL中实现了很多容器,和操作容器的算法库。这里就先实现一种,以后慢慢扩展整个项目。

泛化的容器选定为顺序表,理由就是简单,用途广泛。根据顺序表泛化窗口容器,这个叫法是个人习惯,叫顺序表是对的。基于窗口能轻松实现队列,栈,环形缓冲区等常用的数据结构。

想要一个窗口,就需要有窗口本体,窗口类型,窗口大小,窗口类型大小,序列码(判断窗口满没满),根据要求直接定义窗口结构体:

/**
 * @brief 该结构体用于构建基础窗口顺序表
 *        this structure is used to build the basic window sequence table
 */
typedef struct ValueWindowSquential
{
    char* type; // 窗口类型

    void* data; // 窗口本体

    uint16_t type_size; // 类型大小

    uint32_t max_size; // 窗口大小
 
    uint32_t sequence; // 序列号 

} ValueWindowSquential;

然后写一个初始化函数初始化整个窗口,包括窗口大小,窗口类型,成员类型大小等

// 定义需要泛化的类型,这里走查表法
typedef enum
{
    UINT8 = 0,
    INT,
    FLOAT,
    DOUBLE,

    ERROR

} TypeName;

const char* kValueTypeList[ ERROR + 1 ] = {

    "uint8_t",

    "int",
    "float",
    "double",

    "error",
};

/**
 * @brief 将字符串转换成TypeName
 *          private interface
 *
 * @param tmp
 * @return TypeName
 */
TypeName ChangeStringToEnum( const char* tmp )
{
    assert( tmp != NULL );

    TypeName return_tmp = ERROR;

    if ( strcmp( tmp, kValueTypeList[ UINT8 ] ) == 0 )
    {
        return_tmp = UINT8;
    }
    else if ( strcmp( tmp, kValueTypeList[ FLOAT ] ) == 0 )
    {
        return_tmp = FLOAT;
    }
    else if ( strcmp( tmp, kValueTypeList[ DOUBLE ] ) == 0 )
    {
        return_tmp = DOUBLE;
    }
    else if ( strcmp( tmp, kValueTypeList[ INT ] ) == 0 )
    {
        return_tmp = INT;
    }
    else
    {
        printf( "error char* input !!!" );
        assert( 0 );
    }
    return return_tmp;
}

// 初始化窗口
// Initialize window
void InitValueWindow( ValueWindowSquential* tmp, const char* type, uint32_t max_size )
{
    assert( tmp != NULL );

    tmp->type = ( char* ) malloc( strlen( type ) * sizeof( char ) );
    strncpy( tmp->type, type, strlen( type ) );

    tmp->max_size = max_size;
    tmp->sequence = 0;

    switch ( ChangeStringToEnum( tmp->type ) )
    {
        case UINT8: {
            tmp->data = ( uint8_t* ) malloc( max_size * sizeof( uint8_t ) );
            memset( tmp->data, 0, tmp->max_size );

            tmp->type_size = sizeof( uint8_t );
        }
        break;

        case INT: {
            tmp->data = ( int* ) malloc( max_size * sizeof( int ) );
            memset( tmp->data, 0, tmp->max_size );

            tmp->type_size = sizeof( int );
        }
        break;

        case FLOAT: {
            tmp->data = ( float* ) malloc( max_size * sizeof( float ) );
            memset( tmp->data, 0, tmp->max_size );

            tmp->type_size = sizeof( float );
        }
        break;

        case DOUBLE: {
            tmp->data = ( double* ) malloc( max_size * sizeof( double ) );
            memset( tmp->data, 0, tmp->max_size );

            tmp->type_size = sizeof( double );
        }
        break;

        default: {
            printf( "error tmp->type input !!!" );
            assert( 0 );
        }
        break;
    }

    printf( "type is : %s , max_size is : %d , type_size is : %d \r\n",
            tmp->type, max_size, tmp->type_size );
}

// 重置/销毁窗口
void ResetValueWindow( ValueWindowSquential* tmp )
{
    tmp->sequence  = 0;
    tmp->max_size  = 0;
    tmp->type_size = 0;

    if ( tmp->data != NULL )
    {
        free( tmp->data );
        tmp->data = NULL;
    }

    if ( tmp->type != NULL )
    {
        free( tmp->type );
        tmp->type = NULL;
    }
}

调用初始化创建窗口也很简单

// 1. 创建一个泛型窗口,并以float类型,10个窗口大小初始化
    ValueWindowSquential tmp;
    InitValueWindow( &tmp, kValueTypeList[ FLOAT ], 10 );

二、通过void* 泛化窗口

这里指的是,任何操作容器的接口,都必须通过一个void*指针,这样才能忽略参数类型等因素。

工具函数

正常情况下,操作一个窗口(顺序表)无外乎以下几种操作方式:

  1. 实现 buffer[i] = res; 设置任意成员的值
  2. 实现 res = buffer[i]; 获取任意成员的值
  3. 实现 buffer[i] = buffer[j]; 将任意成员的值赋值给任意成员
  4. 实现 buffer[i] <=> buffer[j] ; 实现任意成员比大小
  5. 实现 swap(i,j); 交换任意两个成员值

现在有的信息是什么?窗口中任意成员的地址,任意成员类型所占的大小(几个byte),知道这两个信息,就可以定义一些工具函数来实现上面的需求:

#ifndef __GENERICS_UTIL_H
#define __GENERICS_UTIL_H

#include "test_generics.h"

/*
    1. 实现 buffer[i] = res; 设置任意成员的值
    2. 实现 res = buffer[i]; 获取任意成员的值
    3. 实现 buffer[i] = buffer[j]; 将任意成员的值赋值给任意成员
    4. 实现 buffer[i] <=> buffer[j] ; 实现任意成员比大小
    5. 实现 swap(i,j); 交换任意两个成员值
*/

// 工具函数 获取窗口任意下标对应的地址 不对外暴露
static inline void* GetMemberAddress( ValueWindowSquential* tmp, int i )
{
    return ( void* ) ( tmp->data + i * tmp->type_size );
}

// 打印任意类型数据的十六进制编码 调试用
static inline void ShowTypeChangeToHex( ValueWindowSquential* tmp, int i )
{
    uint8_t* res = ( uint8_t* ) GetMemberAddress( tmp, i );
    for ( int i = 0; i < tmp->type_size; i++ )
    {
        printf( "Hex : 0x%x \r\n", *res++ );
    }
}

// 1. 设置任意成员的值
static inline void SetMemberValue( ValueWindowSquential* tmp, void* value, int i )
{
    memcpy( GetMemberAddress( tmp, i ), value, tmp->type_size );
}

// 2. 获取任意成员的值
static inline void GetMemberValue( ValueWindowSquential* tmp, void* value, int i )
{
    memcpy( value, GetMemberAddress( tmp, i ), tmp->type_size );
}

// 3. 将任意成员的值赋值给任意成员
static inline void SetMemberToMember( ValueWindowSquential* tmp, int i, int j )
{
    memcpy( GetMemberAddress( tmp, i ), GetMemberAddress( tmp, j ), tmp->type_size );
}

// 4. 对比两个成员大小,顺序为 i > = < j
static inline ComparMemberValue( ValueWindowSquential* tmp, int i, int j )
{
    memcmp( GetMemberAddress( tmp, i ), GetMemberAddress( tmp, j ), tmp->type_size );
}

// 5. 交换任意两个成员值
static inline void SwapMemberValue( ValueWindowSquential* tmp, int i, int j )
{
    void* tmp_value = ( void* ) malloc( tmp->type_size );

    GetMemberValue( tmp, tmp_value, i );
    SetMemberToMember( tmp, i, j );
    SetMemberValue( tmp, tmp_value, j );

    free( tmp_value );
}

#endif // __GENERICS_UTIL_H

代码量很小,但是已经实现了上面五个需求,有了这五个工具函数,就可以实现一些顶层函数的设计了。

填充窗口

有了上面五个工具函数,就可以随意操作窗口了,这里先实现两种插入数据接口:
可以一看相比第一版实现,通过void* + 类型大小这种方式非常的清爽,寥寥几行就能实现针对不同类型的泛型插入。

// 滑动往窗口插入数据
SlideWindowState ValueWindowSlideInsert( ValueWindowSquential* tmp, void* data )
{
    SlideWindowState return_tmp = kWindowIsNotFull;

    for ( int i = 1; i < tmp->max_size; i++ )
    {
        SetMemberToMember( tmp, i - 1, i );
    }
    SetMemberValue( tmp, data, tmp->max_size - 1 );

    // just for return window state
    if ( ++tmp->sequence >= tmp->max_size )
    {
        return_tmp    = kWindowIsSliding;
        tmp->sequence = tmp->max_size;
    }

    return return_tmp;
}

// 插入数据直到填满整个窗口
FixedWindowState ValueWindowFixedInsert( ValueWindowSquential* tmp, void* data )
{
    FixedWindowState return_tmp = kWindowNotFull;

    SetMemberValue( tmp, data, tmp->sequence );

    if ( ++tmp->sequence >= tmp->max_size )
    {
        tmp->sequence = 0;
        return_tmp    = kWindowAlreadyFull;
    }
    return return_tmp;
}

ValueWindowSlideInsert 是滑动插入,遵循先入先出原则,就是普通的滑动窗,窗口满了后,每次插入丢掉最老的数据,窗口状态分为未满和滑动中两种,开始滑动后就一直处于滑动状态。

以下状态就是插入中:
在这里插入图片描述
以下状态就是插满了:
在这里插入图片描述
满了以后,每次插入所有数据整体往后移,丢掉最后一个,插入数据到最前面:
在这里插入图片描述

ValueWindowFixedInsert 是固定窗口插入,就是窗口填充满以后,反馈窗口已满,给到用户去操作,再次插入时丢掉整个窗口中的数据,从头插入,并且反馈窗口未满状态。

这种插入方式会让数据降频,什么意思呢,就是比如传感器数据10ms来一次,窗口大小为10个数据,那么就需要插入十次,才能使用窗口一次,否则窗口未插入满不能被使用。

操作窗口

C++ STL中针对容器实现了很多算法,不同类型,不同长度的容器扔到同一个算法里,就能得到期望的结果。有了上面五个工具函数,我们也能很轻易实现这个功能。

先把最通用的排序功能实现:

/**
 * @brief 对窗口进行冒泡排序
 *
 * @param tmp window
 */
static inline void ValueWindowBubbleSort( ValueWindowSquential* tmp )
{
    bool is_end_loop = true;

    for ( int i = 0; i < tmp->max_size && is_end_loop; i++ )
    {
        is_end_loop = false;
        for ( int j = tmp->max_size - 1; j >= i; j-- )
        {
            if ( ComparMemberValue( tmp, j - 1, j ) > 0 )
            {
                is_end_loop = true;
                SwapMemberValue( tmp, j - 1, j );
            }
        }
    }
}

/**
 * @brief 对窗口进行选择排序
 *
 * @param tmp window
 */
static inline void ValueWindowSelectSort( ValueWindowSquential* tmp )
{
    int min_index = 0;

    for ( int i = 0; i < tmp->max_size; i++ )
    {
        min_index = i;

        for ( int j = i; j < tmp->max_size; j++ )
        {
            if ( ComparMemberValue( tmp, min_index, j ) > 0 )
            {
                min_index = j;
            }
        }
        if ( i != min_index )
        {
            SwapMemberValue( tmp, i, min_index );
        }
    }
}

/**
 * @brief 对窗口进行直接插入排序
 *
 * @param tmp window
 */
static inline void ValueWindowInsertSort( ValueWindowSquential* tmp )
{
    void* tmp_value = ( void* ) malloc( tmp->type_size );
    int   j         = 0;

    for ( int i = 1; i < tmp->max_size; i++ )
    {
        if ( ComparMemberValue( tmp, i - 1, i ) > 0 )
        {
            GetMemberValue( tmp, tmp_value, i );

            for ( j = i - 1; ComparMemberValueFree( tmp, j, tmp_value ) && j >= 0; j-- )
            {
                SetMemberToMember( tmp, j + 1, j );
            }
            SetMemberValue( tmp, tmp_value, j + 1 );
        }
    }

    free( tmp_value );
}

无论什么类型,无论多大容量,扔到算法接口中,都能给你处理好,让使用者不需要管太多内部细节。


踩坑历程

int main( void )
{
    // 1. 创建一个泛型窗口,并以float类型,10个窗口大小初始化
    ValueWindowSquential tmp;
    InitValueWindow( &tmp, kValueTypeList[ INT ], 10 );

    int insert_data = 0;
    for ( int i = 0; i < tmp.max_size; i++ )
    {
        // 2. 插入数据到窗口中,直到接收到窗口满反馈
        insert_data = ( tmp.max_size - i ) * 11;
        if ( kWindowAlreadyFull == ValueWindowFixedInsert( &tmp, &insert_data ) )
        {
            // 3. 打印排序前窗口
            printf( "start sort \r\n" );
            ShowTheWindow( &tmp );

            // 4. 通过直接插入排序法对窗口进行排序
            ValueWindowInsertSort( &tmp );

            // 5. 打印排序后的窗口
            printf( "end sort \r\n" );
            ShowTheWindow( &tmp );

            break;
        }
    }
    ResetValueWindow( &tmp );

    printf( "test generics \r\n" );
    return 0;
}

可以看到,插入前的容器内部是按照插入顺序,数值从大到小,排序后能够从小到大。整个用法除了初始化时需要根据期望的窗口类型和窗口大小设置一下,以后的接口都无需这些操作。
请添加图片描述

浮点数的坑

这里有个奇怪的现象,当使用浮点数类型窗口时,排序失效了!

请添加图片描述
gdb跑了下发现是上面一个工具函数的锅:

// 4. 对比两个成员大小,顺序为 i > = < j
static inline ComparMemberValue( ValueWindowSquential* tmp, int i, int j )
{
    memcmp( GetMemberAddress( tmp, i ), GetMemberAddress( tmp, j ), tmp->type_size );
}

这里我通过 memcmp 比较两段内存的大小,整型无论有无符号均没问题,但是一到浮点数不行了。看了下gcc实现的源码,memcmp 内部会用一个char* 去出该地址的首字节,若首字节就相等,就认为相等,不往后比较剩下的字节大小了,这样肯定是不行的。于是就重写了memcmp ,将一个数据的所有有效字节都遍历一遍。

// 4. 对比两个成员大小,顺序为 i > = < j
static inline int ComparMemberValue( ValueWindowSquential* tmp, int i, int j )
{
    int res = 0;

    uint8_t* value_i = ( uint8_t* ) GetMemberAddress( tmp, i );
    uint8_t* value_j = ( uint8_t* ) GetMemberAddress( tmp, j );

    for ( int k = tmp->type_size - 1; k >= 0; k-- )
    {
        res = memcmp( value_i + k, value_j + k, sizeof( uint8_t ) );
        if ( res != 0 )
        {
            break;
        }
    }
    return res;
}

// 4. 对比任意成员与任意值的大小,服务于插入排序
static inline int ComparMemberValueFree( ValueWindowSquential* tmp, int i, void* j )
{
    int res = 0;

    uint8_t* value_i = ( uint8_t* ) GetMemberAddress( tmp, i );
    uint8_t* value_j = ( uint8_t* ) j;

    for ( int k = tmp->type_size - 1; k >= 0; k-- )
    {
        res = memcmp( value_i + k, value_j + k, sizeof( uint8_t ) );
        if ( res != 0 )
        {
            break;
        }
    }
    return res;
}

在这里插入图片描述

负数的坑

浮点数排序也能成功了!就在我以为万事大吉的时候,我试了一下负数,结果又不行了。。。

在这里插入图片描述
一模一样的代码,改成负数就不行了,不用问,还是写的比较成员大小函数的锅,查了下资料,是可以通过一个数字的十六进制来辨识一个数是否为负数的。针对这种情况做一下特殊处理吧。。。同时决定不用 memcmp ,不方便调试。

// 工具函数 获取任意值的正负 不对外暴露
// return  true  负数
//         false 正数(包括0)
static inline bool GetValuePositive( ValueWindowSquential* tmp, int i )
{
    uint8_t res = *( ( uint8_t* ) GetMemberAddress( tmp, i ) + tmp->type_size - 1 );
    return res & ( 1 << 7 );
}

// 4. 对比两个成员大小,顺序为 i > = < j
static inline int ComparMemberValue( ValueWindowSquential* tmp, int i, int j )
{
    int res = 0;

    bool i_position = GetValuePositive( tmp, i );
    bool j_position = GetValuePositive( tmp, j );

    uint8_t* value_i = ( uint8_t* ) GetMemberAddress( tmp, i );
    uint8_t* value_j = ( uint8_t* ) GetMemberAddress( tmp, j );

    do
    {
        if ( i_position != j_position )
        {
            res = ( i_position == false ) ? 1 : -1;
            break;
        }

        for ( int k = tmp->type_size - 1; k >= 0; k-- )
        {
            if ( *( value_i + k ) > *( value_j + k ) )
            {
                res = -1;
                break;
            }
            else if ( *( value_i + k ) == *( value_j + k ) )
            {
                res = 0;
            }
            else
            {
                res = 1;
                break;
            }
        }
    } while ( 0 );

    return res;
}

在这里插入图片描述

浮点型与整型不兼容的坑

终于搞定了,负数也能够正常排序了,当我把窗口类型切换为整型时,又又又出幺蛾子了
在这里插入图片描述

在这里插入图片描述

看来可能是整型在对比负数时,字节排列规则和浮点型不太一样,那么解决的办法和上面负数一样,就是根据十六进制辨识出当前数据是整型还是浮点型,找了很久也没有找到合适的方法,float和int所占用的字节大小都是一样的,分辨不出来。

无奈只能做出妥协,在初始化时传入当前窗口的类型是浮点型还是整型,虽然只增加了一个参数,但使得用户程序比较麻烦,类型错误也会导致很多不可预知,不可调试的错误(内存写花了)。若有好的方法欢迎随时和我联系。

最终成果展示:

在这里插入图片描述
在这里插入图片描述


总结

最后终于调试出了我想要的效果,任意类型的容器(有符号整型,无符号整型,浮点型),任意类型的值(正数,负数,0,浮点数正数,浮点数负数),都能成功的排序。其实排序只是窗口的一个很小很小的算法,但是为了实现排序这个算法,我们实现了操作泛型容器的大部分重要接口,这里就展示下调试最久的比大小:

// 4. 对比两个成员大小,顺序为 i > = < j
static inline int ComparMemberValue( ValueWindowSquential* tmp, int i, int j )
{
    int res = 0;

    bool i_position = GetValuePositive( tmp, i );
    bool j_position = GetValuePositive( tmp, j );

    uint8_t* value_i = ( uint8_t* ) GetMemberAddress( tmp, i );
    uint8_t* value_j = ( uint8_t* ) GetMemberAddress( tmp, j );

    do
    {
        if ( i_position != j_position )
        {
            res = ( i_position == false ) ? 1 : -1;
            break;
        }

        for ( int k = tmp->type_size - 1; k >= 0; k-- )
        {
            if ( *( value_i + k ) > *( value_j + k ) )
            {
                res = -1;
                break;
            }
            else if ( *( value_i + k ) == *( value_j + k ) )
            {
                res = 0;
            }
            else
            {
                res = 1;
                break;
            }
        }
    } while ( 0 );

    if ( !i_position && !j_position )
    {
        res = -res;
    }
    else if ( i_position && j_position && !tmp->is_floating_point )
    {
        res = -res;
    }

    return res;
}

最开始的版本,直接memcmp对比内存,后来遇到了很多很多坑,最终实现了上面的版本,其实只是比大小而已。
现在大家应该对 if(a > b) 有了些自己的理解了把。写编译器的人真的对底层结构太通透了,太强悍了。我在这里也只考虑了相同符号类型之间的比大小,不同符号之间呢?if(uint8_t > float) 该怎么做呢?

有了上述的五个工具函数,理论上就能实现所有的,针对顺序表的任何算法,操作。

先将整个工程开源在github上,以后会继续维护这个项目,实现更多常用的操作方式,实现更多类型,实现用户自定义类型等等。

基于C的泛型窗口容器

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值