2015.11.06
读xtcxyczjh(系统程序员成长计划)— 学习程序设计方法。
抄敲的代码练习笔记保存地址:y15m11d06
按前面在双向链表中学习到的方法实现动态数组。
读xtcxyczjh P.55-P.58。
动态数组就是它的长度可以根据存储数据多少自动调整。
[摘作者的话]动态数组的功能和双向链表非常类似,所以它对外的接口也是类似的。借助标准C的内存管理函数realloc,可以轻易改变数组的长度,从而实现动态数组。
函数realloc是比较费时 的,如果每插入/删除一个元素就要realloc一次,不但会带来性能的下降,而且可能造成内存碎片。为了解决这个问题,需要使用一个称为预先分配的惯用手法:
- 在扩展数组时,不是一次扩展一个元素,而是一次扩展多个元素。至于应该扩展多少个,经验数据是扩展为现有元素个数的1.5倍。
- 在删除元素时也不是马上释放空闲空间,而是等到空闲空间高于某个值时才释放它们。这里我们的做法时,空闲空间多于有效空间一倍时,将总空间调整为有效空间的1.5倍。
预先分配实际上是用空间换时间的典型应用。
是时候像做贼(zuwei)一样抄看作者的代码了。之前的双向链表中无销毁函数的概念,趁此时机体验一下。
/* darray.h */
/* 声明darray.c中实现的动态数组接口示例 */
#ifndef DARRAY_H
#define DARRAY_H
#include <stddef.h>
#include "typedef.h"
/* 管理|描述动态数组的数据结构及类型 */
struct _DArrayMT;
typedef struct _DArrayMT DArrayMT;
/* 回调函数类型 */
typedef void (*pDataDestroyFuncT)(void *ctx, void *data);
typedef RetT (*pDataVisitFuncT)(void *ctx, void *data);
/* 动态数组方法原型(声明) */
DArrayMT *create_darry(pDataDestroyFuncT pdata_destroy_func, void *ctx);
void destroy_darray(DArrayMT *pmd);
RetT insert_darray(DArrayMT *pmd, size_t index, void *data);
RetT delete_darray(DArrayMT *pmd, size_t index);
RetT set_darray_by_index(DArrayMT *pmd, size_t index, void *data);
RetT get_darray_by_index(DArrayMT *pmd, size_t index, void **data);
RetT get_darray_length(DArrayMT *pdarray);
RetT foreach_darray(DArrayMT *pmd, pDataVisitFuncT pvisit_func, void *ctx);
#endif
将y15m10d31中的typedef.h文件拷贝到当前目录下。就先声明这几个接口为动态数组的实现服务。
size_t
size_t是标准C库stddef.h中定义的,它是sizeof操作符返回的结果类型,该类型的大小是(可)选择的。它可以存储在理论上是可能的任何类型的数组的最大大小。size_t通常用于循环、数组索引、大小的存储和地址运算。它是一个基本的无符号整数的C类型,比习惯使用无符号类型更加安全。(来自度娘百科)。
[1] 管理|描述动态数组的数据结构
/* darray.c */
/* 动态数组接口示例 */
#include "darray.h"
#include "tk.h"
#include <stdlib.h>
//管理|描述动态数组的数据结构
struct _DArrayMT {
void **ppd; //动态指针数组
size_t size; //动态指针数组元素个数
size_t alloc_size; //为动态指针数组实际分配的元素个数
void *data_destroy_ctx; //销毁动态指针数组-元素-时传入的数据
pDataDestroyFuncT pdata_destroy_func; //销毁动态指针数组-元素-的回调函数
};
pdata_destroy_func为释放动态指针元素的回调函数,此函数由用户实现。比如,用户给ppd[i]的数据是在堆内存中,那么用户就可以实现这个回调函数来释放ppd[i]指向的堆内存。
**[2] 方法实现
创建/释放管理动态指针数据的数据结构的对象。**
/* darray.c */
/* 动态数组接口示例 */
//……
//创建、初始化管理动态数组的数据结构
DArrayMT *create_darry(pDataDestroyFuncT pdata_destroy_func, void *ctx)
{
DArrayMT *pmd = malloc(sizeof(DArrayMT));
if (NULL != pmd) {
pmd->ppd = NULL;
pmd->size = 0;
pmd->alloc_size = 0;
pmd->pdata_destroy_func = pdata_destroy_func;
pmd->data_destroy_ctx = ctx;
}
return pmd;
}
//为每个数组元素释放内存即销毁动态数组元素
static void destroy_darray_data(DArrayMT *pmd, void *data)
{
if (NULL != pmd->pdata_destroy_func) {
pmd->pdata_destroy_func(pmd->data_destroy_ctx, data);
}
}
//释放动态数组的堆内存即销毁动态数组
void destroy_darray(DArrayMT *pmd)
{
size_t i = 0;
size_t size = 0;
if (NULL != pmd) {
size = pmd->size;
for (i = 0; i < size; i++) {
destroy_darray_data(pmd, pmd->ppd[i]); } free(pmd->ppd); pmd->ppd = NULL; free(pmd); } }
通过create_darry动态分配的pmd则来管理-实现动态指针数组。如果ppd[i]指向的内容不需要释放(如在栈中),那么pmd->pdata_destroy_func就可以没有。
向动态指针数组中插入一个指针元素 – 动态数组维数的扩展。
/* darray.c */
/* 动态数组接口示例 */
//……
//扩展动态数组维数
static RetT expand_darray(DArrayMT *pmd, size_t nd_size)
{
void **data = NULL;
size_t alloc_size = 0;
return_val_if_p_invalid(NULL != pmd, RET_PARAMETER_INVALID);
if ((pmd->size + nd_size) > pmd->alloc_size) {
alloc_size = pmd->alloc_size + (pmd->alloc_size >> 1) + MIN_PRE_ALLOCATE_NR;
data = (void **)realloc(pmd->ppd, sizeof(void *) * alloc_size);
if (NULL != data) {
pmd->ppd = data;
pmd->alloc_size = alloc_size;
}
}
return ((pmd->size + nd_size) <= pmd->alloc_size) ? RET_OK : RET_FAIL;
}
//插入动态数组的元素
RetT insert_darray(DArrayMT *pmd, size_t index, void *data)
{
RetT ret = RET_FAIL;
size_t c_index = index;
return_val_if_p_invalid(NULL != pmd, RET_PARAMETER_INVALID);
c_index = c_index < pmd->size ? c_index : pmd->size;
if (expand_darray(pmd, 1) == RET_OK) {
size_t i = 0;
for (i = pmd->size; i > c_index; i--) {
pmd->ppd[i] = pmd->ppd[i-1]; } pmd->ppd[c_index] = data; pmd->size++; ret = RET_OK; } return ret; }
每当插入一个指针元素时,insert_darray会调用expand_darray来判断是否需要对动态指针数组的维数进行扩展。在expand_darray中,如果插入指针元素会超过动态指针数组的维数则扩展动态指针数组的维数,动态指针数组扩展维数的公式为pmd->alloc_size + (pmd->alloc_size >> 1) + MIN_PRE_ALLOCATE_NR([作者说]计算1.5*thiz->alloc_size时,不使用1.5 * thiz->alloc_size,因为这样存在浮点数计算,在大多数嵌入式平台中,都没有硬件浮点数计算的支持,浮点数的计算比定点数的计算要慢上百倍。也不使用thiz->alloc_size+ thiz->alloc_size/2,如果编译器不做优化,除法指令也是比较慢的操作,特别是像在ARM这种没有除法指令的芯片中,需要很多条指令才能实现除法的计算。后面加上MIN_PRE_ALLOCATE_NR的原因是避免thiz->alloc_size为0时存在的错误);否则不扩展动态指针数组维数而直接返回到insert_darray函数中。然后将index后的元素往后移,腾出插入位置。
删除动态指针数组中的指针元素 – 减缩动态数组维数。
/* darray.c */
/* 动态数组接口示例 */
//……
//减缩动态数组维数
static RetT shrink_darray(DArrayMT *pmd)
{
return_val_if_p_invalid(NULL != pmd, RET_PARAMETER_INVALID);
if ((pmd->size < (pmd->alloc_size >> 1)) && (pmd->alloc_size > MIN_PRE_ALLOCATE_NR)) {
void **data = NULL;
size_t alloc_size = 0;
alloc_size = pmd->size + (pmd->size >> 1);
data = (void **)realloc(pmd->ppd, sizeof(void *) * alloc_size);
if (NULL != data) {
pmd->ppd = data;
pmd->alloc_size = alloc_size;
}
}
return RET_OK;
}
//删除动态数组元素
RetT delete_darray(DArrayMT *pmd, size_t index)
{
size_t i = 0;
return_val_if_p_invalid(NULL != pmd && pmd->size > index, RET_PARAMETER_INVALID);
destroy_darray_data(pmd, pmd->ppd[index]); for (i = index;(i+1) < pmd->size; i++) { pmd->ppd[i] = pmd->ppd[i+1]; } pmd->size--; shrink_darray(pmd); return RET_OK; }
每当删除一个动态指针数组的指针元素时,现将指针元素的数据销毁(如释放指向的堆内存)。然后移动数组元素覆盖被删除元素的位置。最后调用shrink_darray检查是否要缩减动态数组维数,当数组元素小于为数组分配个数的一半且为数组分配个数大于MIN_PRE_ALLOCATE_NR个时,就调用reallloc来减小数组的维数使其维数为数组元素个数的1.5倍。
在linux终端使用“man realloc”命令查看realloc函数。
根据索引设置/获取动态数组每个元素的值。
/* darray.c */
/* 动态数组接口示例 */
//……
//根据索引设置动态数组元素值
RetT set_darray_by_index(DArrayMT *pmd, size_t index, void *data)
{
return_val_if_p_invalid(NULL != pmd && pmd->size > index, RET_PARAMETER_INVALID);
pmd->ppd[index] = data;
return RET_OK;
}
//根据索引获取动态元素值
RetT get_darray_by_index(DArrayMT *pmd, size_t index, void **data)
{
return_val_if_p_invalid(NULL != pmd && pmd->size > index, RET_PARAMETER_INVALID);
*data = pmd->ppd[index];
return RET_OK;
}
获取动态数组元素个数。
/* darray.c */
/* 动态数组接口示例 */
//……
//获取动态数组长度
size_t get_darray_length(DArrayMT *pmd)
{
return_val_if_p_invalid(NULL != pmd, 0);
return pmd->size;
}
遍历数组元素。
/* darray.c */
/* 动态数组接口示例 */
//……
//遍历动态数组元素
RetT foreach_darray(DArrayMT *pmd, pDataVisitFuncT pvisit_func, void *ctx)
{
size_t i = 0;
size_t size = 0;
RetT ret = RET_OK;
return_val_if_p_invalid(NULL != pmd && NULL != pvisit_func, RET_PARAMETER_INVALID);
size = pmd->size;
for (i = 0; i < size; i++) {
ret = pvisit_func(ctx, pmd->ppd[i]);
}
return ret;
}
动态数组中的元素所指的值只有用户知道,所以实现回调函数来供用户操作每一个元素。
/* darray.c */
/* 动态数组接口示例 */
//……
#ifdef DAYYAY_TEST
#include <assert.h>
static RetT print_int_darray(void *ctx, void *data)
{
int *expected =(int*)ctx;
printf("%d ", *expected + (int)data);
return RET_OK;
}
static void test_int_darray(void)
{
int i = 0;
int n = 100;
int data = 0;
DArrayMT *pmd = NULL;
pmd = create_darry(NULL, NULL);
for (i = 0; i < n; i++) {
assert(insert_darray(pmd, i, (void *)i) == RET_OK);
assert(get_darray_length(pmd) == (i + 1));
assert(set_darray_by_index(pmd, i, (void *)(2*i)) == RET_OK);
assert(get_darray_by_index(pmd, i, (void **)&data) == RET_OK);
assert(data == 2*i);
}
i = 2 * (n - 1);
assert(foreach_darray(pmd, print_int_darray, &i) == RET_OK);
printf("\n");
for (i = 0; i < n; i++) {
assert(get_darray_length(pmd) == (n-i));
assert(delete_darray(pmd, 0) == RET_OK);
assert(get_darray_length(pmd) == (n-i-1));
}
assert(get_darray_length(pmd) == 0);
destroy_darray(pmd);
}
int main(void)
{
test_int_darray();
return 0;
}
#endif
all:
gcc -Wall -g -DDAYYAY_TEST darray.c -o darry
clean:
rm darry
gcc -DDAYYAY_TEST为程序定义了DAYYAY_TEST,所以整个测试程序才得以编译器继续编译到最终的执行。
在与程序同目录下的linux中端运行make命令后即可生成darry可执行程序。在linux终端会显示回调函数输出的结果:
[作者说,同时我也赞同]
- 动态数组本身占用一块连续的内存,而双向链表的每个结点要占用一块内存。在频繁增删数据的情况下,双向链表更容易造成内存碎片,具体影响与内存管理器的好坏有关。
- 动态数组的增删操作需要移动后面的元素,而双向链表只需要修改前后元素的指针。在存储大量数据的情况下,这种差异更为明显。
- 动态数组支持多种高效的排序算法,像快速排序、归并排序和堆排序等等,而这些算法在双向链表中的表现并不好,甚至不如冒泡排序来得快。
- 排序好的动态数组可以使用二分查找,而排序好的双向链表仍然只能使用顺序查找。主要原因是双向链表不支持随机定位,定位中间结点时只能一个一个的移动指针。
- 对于小量数据,使用动态数组还是双向链表没有多大区别,使用哪个只看个人的喜好了。
抄看代码就是快~` ou…..
[2015.11.06-19:52]