[stretchy_buffer.h]一个简单的类C++ vector 数组的源码阅读

stretchy_buffer.h源码就只有几个宏和一个函数。作者也在里边进行了一些说明和使用介绍,是英文的,而且没有具体的实现原理。当然,老鸟一看就懂了,我是菜鸟,一看再看才懂的。这里我把自己看后的理解记录下来,如果有人正好需要的话,希望能有帮助。


一般的,数组在声明的时候,它的大小就已经确定了,所能存储的元素个数不能大于数组的大小。就算我们可以在堆空间中申请任意(这里的任意是指合理的情况下)大小的空间,也无法做到那种“直接在原来的数组的基础上加多一点空间,然后把元素储存到数组中”,而是多做了一些步骤,让这个数组看起来像是可以储存任意元素那样:往数组中添加一个元素,再添加一个,再添加一个,而不用担心这个数组只能固定存几个元素。这其中的原理是:当我们有一个存了n个元素的数组,我们需要存第n+1个元素,这时数组没有多余的空间了,于是重新分配可以存n+1个元素的空间,把原来空间中的数据拷贝到新分配的0 ~ n-1个的空间中,最后把需要存的第n+1个元素存到第n个空间中。出于效率的考虑,重新分配空间的时候,一般是多分配几个空间,而不是刚好分配所需的空间,这样下次往数组添加元素的时候就不用每次都分配空间了。代码写的不多的同学可能有疑问:为什么不能直接在原来的数组的基础上接着分配空间,然后接着存放元素?非要整个分配空间,还要拷贝数据?原因是数组后面的空间可能被其它数据占用了,而且分配空间的操作是系统控制的,不是我们想在哪分配就在哪分配。我们只是告诉操作系统想要多少空间,然后操作系统会在某处给我们这么多空间。这里写图片描述


stretchy buffer的意思就是这段空间是可变的,并不是固定的。一般要实现一个这样的结构是将相关数据封装在一个结构体里,像这样:

struct stretch_buffer {
    int m; // 总的可存放的个数
    int n; // 已存放的元素个数
    // 为了可以存放任意类型的元素,这里用void*而不是具体的类型指针
    void* a; // 指向存放元素的首地址
};

其实这里涉及到的原理和之前SDS系列博客所说的差不多,不过SDS只能存字符串。这个作者并没有使用结构体,但是在内存中的布局还是一样的:
这里写图片描述


清楚上面说的东西后,就可以理解代码了。代码直接贴原来的,源文件中的说明和License就去掉了。最好看下源文件中的使用介绍比较好。

#ifndef STB_STRETCHY_BUFFER_H_INCLUDED
#define STB_STRETCHY_BUFFER_H_INCLUDED

#ifndef NO_STRETCHY_BUFFER_SHORT_NAMES
#define sb_free   stb_sb_free
#define sb_push   stb_sb_push
#define sb_count  stb_sb_count
#define sb_add    stb_sb_add
#define sb_last   stb_sb_last
#endif

// 这个宏是用来释放整个申请的内存的。因为申请的时候是一起申请的,所以可以一次释放
// 要释放这段空间,肯定要从起始地址开始,可以看上面那张图片。stb_sbraw(a)就是获取整个空间的起始地址的
// 如果a不为空,释放a,最后得到一条语句 0; 。如果a为空,直接得到 0; 。0加个逗号是条合法的语句,虽然平时并不会见到
#define stb_sb_free(a)         ((a) ? free(stb__sbraw(a)),0 : 0)
// 重点看这个宏,向数组a中添加元素v。它做了两件事,首先判断数组的空间够不够大,
// 如果不够大,重新分配空间(原理和开头文字介绍的一样)
// 如果够大,则不变。然后再将元素添加到数组中
// 这个过程有点繁琐,具体是这样的:执行stb__sbmaybegrow(),这条语句可能会为a重新分配空间,也可能不会
// 具体需不需要重新分配空间,通过stb__sbneedgrow()来判断,如果需要,则调用stb__sbgrow()
// 而stb__sbgrow()最终会调用函数stb__sbgrowf()来为a分配新空间,拷贝数据,释放原来的空间
#define stb_sb_push(a,v)       (stb__sbmaybegrow(a,1), (a)[stb__sbn(a)++] = (v))
// 下面这个宏是获取数组元素的个数,看开头给的截图,这个结构中的n就是用来存数组元素个数的
#define stb_sb_count(a)        ((a) ? stb__sbn(a) : 0)
// 为数组a增加n个元素的空间,并且会返回新增的空间的首地址。如果a为空,增加3个元素的空间后,就可以存放3个元素了
#define stb_sb_add(a,n)        (stb__sbmaybegrow(a,n), stb__sbn(a)+=(n), &(a)[stb__sbn(a)-(n)])
// 获取数组最后一个元素,n-1就是最后一个元素的下标
#define stb_sb_last(a)         ((a)[stb__sbn(a)-1])

/* 下面这3个宏是跟整个结构中开头部分的m,n有关的 */
// a是指向数组的首地址的,将其强制转换成int*并向后偏移两个单位,就是整个结构的首地址了
#define stb__sbraw(a) ((int *) (a) - 2)
// 有了首地址后,像数组那样使用,第0个是m,第1个是n
#define stb__sbm(a)   stb__sbraw(a)[0] // 整个数组可存放的个数
#define stb__sbn(a)   stb__sbraw(a)[1] // 已存放的数组元素个数

// 判断数组a是否需要重新分配空间。n代表新添加的元素个数,m代表数组总共可存放的元素个数。如果a为空,肯定需要分配空间
// 如果a不为空,但是原来的数组元素个数加上新添加的元素个数大于数组可存放的个数,当然需要为a重新分配一块更大的空间
#define stb__sbneedgrow(a,n)  ((a)==0 || stb__sbn(a)+(n) >= stb__sbm(a))
// 如果a需要重新分配空间,则重新分配空间,否则得到一条语句 0; 
#define stb__sbmaybegrow(a,n) (stb__sbneedgrow(a,(n)) ? stb__sbgrow(a,n) : 0)
// 取a的地址,强制转换成void**,再取这个地址的内容。如果a为一级指针,则分配的空间
// 用来存普通变量,如果a为二级指针,则分配的空间用来存放一级指针(每个一级指针又指向普通变量)
#define stb__sbgrow(a,n)      (*((void **)&(a)) = stb__sbgrowf((a), (n), sizeof(*(a))))

#include <stdlib.h>

// 唯一的一个函数,arr为原来的数组地址,increment为数组新增的元素个数,itemsize为每个数组元素的大小
static void * stb__sbgrowf(void *arr, int increment, int itemsize)
{
   // 如果arr不为空,将原来的数组的大小扩大为两倍
   int dbl_cur = arr ? 2*stb__sbm(arr) : 0;
   // 原来的数组元素个数加上新增的元素个数,为至少需要的空间个数
   int min_needed = stb_sb_count(arr) + increment;
   // 当新添加的元素较少时,dbl_cur会大于min_needed,较多时dbl_cur会小于min_needed
   int m = dbl_cur > min_needed ? dbl_cur : min_needed;
   // realloc()在满足条件的情况下会为arr新分配一块空间并将原来的数据拷贝到新空间中,然后释放原来的空间
   // 元素的大小itemsize乘以个数就是数组的空间大小了,再加上两个int大小的空间,用来存放m,n
   int *p = (int *) realloc(arr ? stb__sbraw(arr) : 0, itemsize * m + sizeof(int)*2);
   // 空间分配成功
   if (p) {
      // 原来的数组为空
      if (!arr)
         // 数组已存元素个数为0
         p[1] = 0;
      // 设置整个数组的个数
      p[0] = m;
      // 返回数组的首地址,去掉两个int的空间
      return p+2;
   } else { // 分配空间失败后的处理
      #ifdef STRETCHY_BUFFER_OUT_OF_MEMORY
      STRETCHY_BUFFER_OUT_OF_MEMORY ;
      #endif
      return (void *) (2*sizeof(int)); // try to force a NULL pointer exception later
   }
}
#endif // STB_STRETCHY_BUFFER_H_INCLUDED

要看懂上面的解释(难免有误,发现后再更正),需要理解整个stretchy buffer的原理是怎样的,不然只是知道每条语句的意思,却不知道这些语句组合起来实现了什么功能。代码中用到了很多逗号运算符,如果用函数来描述这些步骤而不是用宏来描述的话,会清晰很多。作者用简单的几行代码就实现了类似C++中的vector,用起来挺方便的,不过效率有点低,因为多次新分配空间和数据的拷贝开销挺大的,用链表可以避免这个问题,但是有时链表又没有数组好用。

猜你喜欢

转载自blog.csdn.net/alonwol/article/details/80564273