柔性数组和环形队列

1 定长数组包

在平时的开发中,缓冲区数据收发时,如果采用缓冲区定长包,假定大小是 1k,MAX_LENGTH为 1024。结构体如下:

//  定长缓冲区
struct max_buffer
{
    int   len;
    char  data[MAX_LENGTH];
};

数据结构的大小 >= sizeof(int) + sizeof(char) * MAX_LENGTH为了防止数据溢出的情况,data 的长度一般会设置得足够大,但也正是因为这样,才会导致数组的冗余。

假如发送 512 字节的数据,  就会浪费 512 个字节的空间, 平时通信时,大多数是心跳包,大小远远小于 1024,除了浪费空间还消耗很多流量。

内存申请:

if ((m_buffer = (struct max_buffer *)malloc(sizeof(struct max_buffer))) != NULL)
{
    m_buffer->len = CUR_LENGTH;
    memcpy(m_buffer->data, "max_buffer test", CUR_LENGTH);
    printf("%d, %s\n", m_buffer->len, m_buffer->data);
}

内存释放:

free(m_buffer);
m_buffer = NULL;

指针数据包

为了避免空间上的浪费,我们可以将上面的长度为 MAX_LENGTH 的定长数组换为指针, 每次使用时动态的开辟 CUR_LENGTH 大小的空间。数据包结构体定义:

struct point_buffer
{
    int   len;
    char  *data;
};

数据结构大小 >= sizeof(int) + sizeof(char *)但在内存分配时,需要两步进行:

  • 需为结构体分配一块内存空间;

  • 为结构体中的成员变量分配内存空间;

内存申请:

if ((p_buffer = (struct point_buffer *)malloc(sizeof(struct point_buffer))) != NULL)
{
    p_buffer->len = CUR_LENGTH;
    if ((p_buffer->data = (char *)malloc(sizeof(char) * CUR_LENGTH)) != NULL)
    {
        memcpy(p_buffer->data, "point_buffer test", CUR_LENGTH);
        printf("%d, %s\n", p_buffer->len, p_buffer->data);
    }
}

free(p_buffer->data);
free(p_buffer);
p_buffer = NULL;

虽然这样能够节约内存,但是两次分配的内存是不连续的, 需要分别对其进行管理,导致的问题就是需要对结构体和数据分别申请和释放内存,这样对于程序员来说无疑是一个灾难,因为这样很容易导致遗忘释放内存造成内存泄露。

有没有更好的方法呢?那就是今天的主题柔性数组。

2 柔性数组

什么是柔性数组?

柔性数组成员(flexible array member)也叫伸缩性数组成员,这种代码结构产生于对动态结构体的需求。在日常的编程中,有时候需要在结构体中存放一个长度动态的字符串,鉴于这种代码结构所产生的重要作用,C99 甚至把它收入了标准中

柔性数组是 C99 标准引入的特性,所以当你的编译器提示不支持的语法时,请检查你是否开启了 C99 选项或更高的版本支持。

C99 标准的定义如下:

struct test {
    short len;  // 必须至少有一个其它成员
    char arr[]; // 柔性数组必须是结构体最后一个成员(也可是其它类型,如:int、double、...)
};

  • 柔性数组成员必须定义在结构体里面且为最后元素;

  • 结构体中不能单独只有柔性数组成员;

  • 柔性数组不占内存。

在一个结构体的最后,申明一个长度为空的数组,就可以使得这个结构体是可变长的。对于编译器来说,此时长度为 0 的数组并不占用空间,因为数组名本身不占空间,它只是一个偏移量,数组名这个符号本身代表了一个不可修改的地址常量,

但对于这个数组的大小,我们可以进行动态分配,对于编译器而言,数组名仅仅是一个符号,它不会占用任何空间,它在结构体中,只是代表了一个偏移量,代表一个不可修改的地址常量!

对于柔性数组的这个特点,很容易构造出变成结构体,如缓冲区,数据包等等, 其实柔性数组成员在实现跳跃表时有它特别的用法,在Redis的SDS数据结构中和跳跃表的实现上,也使用柔性数组成员。它的主要用途是为了满足需要变长度的结构体,为了解决使用数组时内存的冗余和数组的越界问题

//柔性数组
struct soft_buffer
{
    int   len;
    char  data[0];
};

数据结构大小 = sizeof(struct soft_buffer) = sizeof(int),这样的变长数组常用于网络通信中构造不定长数据包, 不会浪费空间浪费网络流量。

申请内存:

if ((softbuffer = (struct soft_buffer *)malloc(sizeof(struct soft_buffer) + sizeof(char) * CUR_LENGTH)) != NULL)
{
    softbuffer->len = CUR_LENGTH;
    memcpy(softbuffer->data, "softbuffer test", CUR_LENGTH);
    printf("%d, %s\n", softbuffer->len, softbuffer->data);
}

释放内存:

free(softbuffer);
softbuffer = NULL;

对比使用指针和柔性数组会发现,使用柔性数组的优点:

  • 由于结构体使用指针地址不连续(两次 malloc),柔性数组地址连续,只需要一次 malloc,同样释放前者需要两次,后者可以一起释放。

  • 在数据拷贝时,结构体使用指针时,必须拷贝它指向的内存,内存不连续会存在问题,柔性数组可以直接拷贝。

  • 减少内存碎片,由于结构体的柔性数组和结构体成员的地址是连续的,即可一同申请内存,因此更大程度地避免了内存碎片。另外由于该成员本身不占结构体空间,因此,整体而言,比普通的数组成员占用空间要会稍微小点。

缺点:对结构体格式有要求,必要放在最后,不是唯一成员。

3 总结

在日常编程中,有时需要在结构体中存放一个长度是动态的字符串(也可能是其他数据类型),可以使用柔性数组,柔性数组是一种能够巧妙地解决数组内存的冗余和数组的越界问题一种方法。非常值得大家学习和借鉴。

 

什么是环形队列?

环形缓冲区是一个非常典型的数据结构,这种数据结构符合生产者,消费者模型,可以理解它是一个水坑,生产者不断的往里面灌水,消费者就不断的从里面取出水。

 

图片

 

那就可能会有人问,既然需要灌水,又需要取出水,为什么还需要开辟一个缓冲区内存空间呢?直接把生产者水管的尾部接到消费者水管的头部不就好了,这样可以省空间啊。

 

图片

 

答案是不行的,生产者生产水的速度是不知道的,消费者消费水的速度也是不知道的,如果你强制接在一起,因为生产和消费的速度不同,就非常可能存在水管爆炸的情况,你说这样危险不危险?

 

图片

 

在音频系统框架下,alsa就是使用环形队列的,在生产者和消费者速度不匹配的时候,就会出现xrun的问题。

 

环形队列的特点

1、数组构造环形缓冲区

假设我们用数组来构造一个环形缓存区,如下图

图片

我们需要几个东西来形容这个环形缓冲区,一个的读位置,一个是写位置,一个是环形缓冲区的长度

图片

从图片看,我们知道,这个环形缓冲区的读写位置是指向数组的首地址的,环形缓冲区的长度是 5 。

那如何判断环形缓冲区为空呢?

如果 R == W  就是读写位置相同,则这个环形缓冲区为空

那如何判断环形缓冲区满了呢?

如果 (W - R )= Len ,则这个环形缓冲区已经满了。

2、向环形缓冲区写入 3个数据

图片

写入 3 个数据后,W 的值等于 3 了,R 还是等于 0。

3个企鹅已经排列

3、从环形缓冲区读取2个数据

图片

读出两个数据后,R = 2 了,这个时候,W还是等于 3,毕竟没有再写过数据了。

4、再写入3个数据

图片

如果 W > LEN 后,怎么找到最开始的位置的呢?这个就需要进行运算了,W%LEN 的位置就是放入数据的位置 ,6%5 = 1。

5、再写入1个数据

图片

这个时候环形队列已经满了,要是想再写入数据的话,就不行了,(W - R) = 5 == LEN

代码实现

/* 实现的最简单的ringbuff 有更多提升空间,可以留言说明 */
#include "stdio.h"
#include "stdlib.h"

#define LEN 10

/*环形队列结构体*/
typedef struct ring_buff{
int array[LEN];
int W;
int R;
}*ring;

/*环形队列初始化*/
struct ring_buff * fifo_init(void)
{
struct ring_buff * p = NULL;
p = (struct ring_buff *)malloc(sizeof(struct ring_buff));
if(p == NULL)
{
printf("fifo_init malloc error\n");
return NULL;
}
p->W = 0;
p->R = 0;
return p;
}

/*判断环形队列是否已经满了*/
int get_ring_buff_fullstate(struct ring_buff * p_ring_buff)
{
/*如果写位置减去读位置等于队列长度,就说明这个环形队列已经满*/
if((p_ring_buff->W - p_ring_buff->R) == LEN)
{
return (1);
}
else
{
return (0);
}
}

/*判断环形队列为空*/
int get_ring_buff_emptystate(struct ring_buff * p_ring_buff)
{
/*如果写位置和读的位置相等,就说明这个环形队列为空*/
if(p_ring_buff->W == p_ring_buff->R)
{
return (1);
}
else
{
return (0);
}
}
/*插入数据*/
int ring_buff_insert(struct ring_buff * p_ring_buff,int data)
{
if(p_ring_buff == NULL)
{
printf("p null\n");
return (-1);
}

if(get_ring_buff_fullstate(p_ring_buff) == 1)
{
printf("buff is full\n");
return (-2);
}

p_ring_buff->array[p_ring_buff->W%LEN] = data;

p_ring_buff->W ++;
//printf("inset:%d %d\n",data,p_ring_buff->W);
return (0);
}

/*读取环形队列数据*/
int ring_buff_get(struct ring_buff * p_ring_buff)
{
int data = 0;

if(p_ring_buff == NULL)
{
printf("p null\n");
return (-1);
}

if(get_ring_buff_emptystate(p_ring_buff) == 1)
{
printf("buff is empty\n");
return (-2);
}

data = p_ring_buff->array[p_ring_buff->R%LEN];
p_ring_buff->R++;
return data;
}

/*销毁*/
int ring_buff_destory(struct ring_buff * p_ring_buff)
{
if(p_ring_buff == NULL)
{
printf("p null\n");
return (-1);
}

free(p_ring_buff);

return (0);
}

int main()
{
int i = 0;

/*定义一个环形缓冲区*/
ring pt_ring_buff = fifo_init();

/*向环形缓冲区中写入数据*/
for(i = 0;i<10;i++)
{
ring_buff_insert(pt_ring_buff,i);
}

/*从环形缓冲区中读出数据*/
for(i = 0;i<10;i++)
{
printf("%d ",ring_buff_get(pt_ring_buff));
}

/*销毁一个环形缓冲区*/
ring_buff_destory(pt_ring_buff);

return (1);
}

 

换一个写法,这个写法是各种大神级别的

/* 实现的最简单的ringbuff 有更多提升空间,可以留言说明 */
#include "stdio.h"
#include "stdlib.h"

#define LEN 64

/*环形队列结构体*/
typedef struct ring_buff{
int array[LEN];
int W;
int R;
}*ring;

/*环形队列初始化*/
struct ring_buff * fifo_init(void)
{
struct ring_buff * p = NULL;
p = (struct ring_buff *)malloc(sizeof(struct ring_buff));
if(p == NULL)
{
printf("fifo_init malloc error\n");
return NULL;
}
p->W = 0;
p->R = 0;
return p;
}

/*判断环形队列是否已经满了*/
int get_ring_buff_fullstate(struct ring_buff * p_ring_buff)
{
/*如果写位置减去读位置等于队列长度,就说明这个环形队列已经满*/
if((p_ring_buff->W - p_ring_buff->R) == LEN)
{
return (1);
}
else
{
return (0);
}
}

/*判断环形队列为空*/
int get_ring_buff_emptystate(struct ring_buff * p_ring_buff)
{
/*如果写位置和读的位置相等,就说明这个环形队列为空*/
if(p_ring_buff->W == p_ring_buff->R)
{
return (1);
}
else
{
return (0);
}
}
/*插入数据*/
int ring_buff_insert(struct ring_buff * p_ring_buff,int data)
{
if(p_ring_buff == NULL)
{
printf("p null\n");
return (-1);
}

if(get_ring_buff_fullstate(p_ring_buff) == 1)
{
printf("buff is full\n");
return (-2);
}

p_ring_buff->array[p_ring_buff->W%LEN] = data;
//p_ring_buff->array[p_ring_buff->W&(LEN -1)] = data;
p_ring_buff->W ++;
//printf("inset:%d %d\n",data,p_ring_buff->W);
return (0);
}

/*读取环形队列数据*/
int ring_buff_get(struct ring_buff * p_ring_buff)
{
int data = 0;

if(p_ring_buff == NULL)
{
printf("p null\n");
return (-1);
}

if(get_ring_buff_emptystate(p_ring_buff) == 1)
{
printf("buff is empty\n");
return (-2);
}

//data = p_ring_buff->array[p_ring_buff->R%LEN];
data = p_ring_buff->array[p_ring_buff->R&(LEN -1)];
p_ring_buff->R++;
return data;
}

/*销毁*/
int ring_buff_destory(struct ring_buff * p_ring_buff)
{
if(p_ring_buff == NULL)
{
printf("p null\n");
return (-1);
}

free(p_ring_buff);

return (0);
}

int main()
{
int i = 0;

/*定义一个环形缓冲区*/
ring pt_ring_buff = fifo_init();

/*向环形缓冲区中写入数据*/
for(i = 0;i<10;i++)
{
ring_buff_insert(pt_ring_buff,i);
}

/*从环形缓冲区中读出数据*/
for(i = 0;i<10;i++)
{
printf("%d ",ring_buff_get(pt_ring_buff));
}

/*销毁一个环形缓冲区*/
ring_buff_destory(pt_ring_buff);

return (1);
}

总结

环形队列的使用场景非常多,安卓的音频数据读写,很多都用到环形队列,我们在开发过程中使用的环形队列肯定比我上面的那个例子要复杂的多,我这里演示的是比较简单的功能,但是麻雀虽小,五脏俱全,希望这个麻雀让你们了解这个数据结构。在实际项目中大展身手。

 

柔性数组关于arr[]和arr[0]补充内容

柔性数组的两种书写方式

struct starr{                   struct starr{
    int i;                                int i;
    int arr[0];                         int arr[];
};                                    };
  

上面都是定义柔性数组的方式。需要注意两个问题

1、 结构体中必须存在至少一个除柔性数组以外的元素。

2、 柔性数组的必须在结构体的最后一个位置

arr[] 的写法是C99标准引入的,叫做incomplete type,不完全类型,引入的原因是发现这样的写法非常实用。

arr[0] 是非标准扩展支持,也就是在C99出现之前的C89,就已经存在这种非标准扩展支持了,有些脑瓜子灵光的人,发现了这个机制,就用起来,然后C99才正式给他纳入正规军。

所以我们写成 arr[0] 也是没有问题的,编译器会自动解释为柔性数组。

 

3、柔性数组的地址和数组地址问题

我们知道,结构体在定义的时候就已经确定了地址位置,柔性数组实际上是不占用原结构体空间的,柔性数组的空间是使用malloc来申请的,既然是这样,他们的地址空间就不是在一个位置上的。柔性数组也就纯粹是挂羊头卖狗肉了

测试地址空间

#include "stdio.h"
#include"malloc.h"

struct flex_array{
    int i;
    int arr[0];
};

int main()
{
    int len = 10;
    struct flex_array AAA;
    struct flex_array * p_soft_arr = &AAA;
    p_soft_arr = (struct flex_array *)malloc(sizeof(struct flex_array) + sizeof(int)*len);

    printf("%p %p\n",&AAA,p_soft_arr);
    printf("sizeof(struct flex_array)=%ld\n",sizeof(struct flex_array));
    return 0;
}

代码输出

weiqifa@bsp-ubuntu1804:~/c$ gcc ringbuffer.c && ./a.out
0x7ffd52554514 0x55e3c0fa1260
sizeof(struct starr)=4
weiqifa@bsp-ubuntu1804:~/c$

结构体定义的地址和malloc出来的地址不是一个位置,至少他们不是连续的,而且使用malloc出来的内存后,使用完之后,需要使用free释放内存。

 

4、使用柔性数组实现环形队列

/* 实现的最简单的ringbuff 有更多提升空间,可以留言说明 */
#include "stdio.h"
#include "stdlib.h"
#include "time.h"


#define LEN 64
typedef int datatype;

/*环形队列结构体*/
typedef struct ring_buff{
    int W;
    int R;
    int array[];
}*ring;

/*环形队列初始化*/
struct ring_buff * fifo_init(void)
{
    struct ring_buff * p = NULL;
    p = (struct ring_buff *)malloc(sizeof(struct ring_buff) + sizeof(datatype)*LEN);
    if(p == NULL)
    {
        printf("fifo_init malloc error\n");
        return NULL;
    }
    p->W = 0;
    p->R = 0;
    return p;
}

/*判断环形队列是否已经满了*/
int get_ring_buff_fullstate(struct ring_buff * p_ring_buff)
{
    /*如果写位置减去读位置等于队列长度,就说明这个环形队列已经满*/
    if((p_ring_buff->W - p_ring_buff->R) == LEN)
        return (1);
    else
        return (0);
}

/*判断环形队列为空*/
int get_ring_buff_emptystate(struct ring_buff * p_ring_buff)
{
    /*如果写位置和读的位置相等,就说明这个环形队列为空*/
    if(p_ring_buff->W == p_ring_buff->R)
        return (1);
    else
        return (0);
}
/*插入数据*/
int ring_buff_insert(struct ring_buff * p_ring_buff,int data)
{
    if(p_ring_buff == NULL)
    {
        printf("p_ring_buff is null\n");
        return (-1);
    }

    if(get_ring_buff_fullstate(p_ring_buff) == 1)
    {
        printf("buff is full\n");
        return (-2);
    }

    p_ring_buff->array[p_ring_buff->W%LEN] = data;
    p_ring_buff->W ++;
    //printf("insert:%d %d\n",data,p_ring_buff->W);
    return (0);
}

/*读取环形队列数据*/
int ring_buff_get(struct ring_buff * p_ring_buff)
{
    int data = 0;

    if(p_ring_buff == NULL)
    {
        printf("p null\n");
        return (-1);
    }

    if(get_ring_buff_emptystate(p_ring_buff) == 1)
    {
        printf("buff is empty\n");
        return (-2);
    }

    data = p_ring_buff->array[p_ring_buff->R%LEN];
    p_ring_buff->R++;
    return data;
}

/*销毁*/
int ring_buff_destory(struct ring_buff * p_ring_buff)
{
    if(p_ring_buff == NULL)
    {
        printf("p null\n");
        return (-1);
    }

    free(p_ring_buff);

    return (0);
}

int main()
{
    int i = 0;
    int data;

    /*设置种子*/
    srand((int)time(0));
    /*定义一个环形缓冲区*/
    ring pt_ring_buff = fifo_init();

    printf("write:\n");
    /*向环形缓冲区中写入数据*/
    for(i = 0;i<10;i++)
    {
        data = rand()%LEN;
        ring_buff_insert(pt_ring_buff,data);
        printf("%d ",data);
    }

    printf("\nread:\n");
    printf("%d ",ring_buff_get(pt_ring_buff));
    printf("\nread:\n");
    /*从环形缓冲区中读出数据*/
    for(i = 0;i<10;i++)
    {
        printf("%d ",ring_buff_get(pt_ring_buff));
    }

    printf("\n");
    /*销毁一个环形缓冲区*/
    ring_buff_destory(pt_ring_buff);

    return (1);
}

输出结果

weiqifa@bsp-ubuntu1804:~/c$ gcc ringbuffer.c && ./a.out
write:
47 39 31 16 55 25 22 38 41 62
read:
47
read:
39 31 16 55 25 22 38 41 62 buff is empty
-2
weiqifa@bsp-ubuntu1804:~/c$

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值