动态数组

4

Posted by conan | Posted in 读书笔记 | Posted on 10-04-2011

自己感触,有错的还请大家指出

动态数组:
动态数组只写了三遍,发现一些问题总结如下。

设计的动态数组和之前的双向链表一样,也是浅拷贝的数据结构。

  • 第一遍用malloc,free,memmove等函数来完成的(注:memcpy和memmove的区别是memcpy没有考虑内存重叠的情况)。
  • 第二遍发现memmove的操作并不怎么直观,所移动的size每次都要写的比较长(sizeof(void *) * n),于是实现了一个elem_move的函数,使用index进行移动。同时替换malloc和free所进行的内存扩展为realloc,在使用realloc的过程中没有仔细阅读realloc的函数原型,第一次使用时居然认为肯定原地扩展了,虽然运行起来没发现错误,还好被valgrind查出来了。在此严重推荐一下valgrind,检查内存泄漏的一个很不错的工具,帮我查出很多次错误了;
  • 写第三遍是因为发现了浅拷贝的一个问题。如果get了其中一个elem然后set到不同的index下,那么原来在此index下的elem会发生内存泄漏,而被set过来的elem则会在销毁的时候被free两遍。为了改善这个这个问题,在数组内引入了reference count的概念。我在数组初始化的时候引入了ref_ch和ref_eq函数,调用者自己完成这些函数,并且在自己的数据结构里面加上reference count,根据自己的elem结构对reference count进行操作,并且在insert和set_by_index的时候进行reference count加一,elem_destroy的时候先减一,判断为0了再free。

最终结构是这样的:
数据部分:

struct _DArray{
	void **data;//用来存放动态数组的首指针
	size_t length;//数组当前长度
	size_t size;//数组目前的可用大小

	void *ctx;//进行free的上下文
	DArrayFreeFunc free;//自己的free函数
	DArrayVisitFunc ref_ch;//修改reference count的函数
	DArrayCmpFunc ref_eq;//判断reference count是否等于一个数的函数
};

内部方法:

//销毁一个元素,data为所要销毁的elem的指针。具体操作为调用
//ref_ch((void *)-1, data)减少其ref count,然后检查是否为0,
//是则free掉,否则只是减一
static void darray_elem_destroy(DArray *thiz, void *data)
static size_t darray_get_size(DArray *thiz)
static Ret darray_set_size(DArray *thiz, size_t size)
static Ret darray_set_length(DArray *thiz, size_t length)
//扩展当前数组,函数判断若inc+length>size了就扩展
static Ret darray_expand(DArray *thiz, int inc)
//收缩,若数组长度已减少到一半又少了rand(),则缩减数组尺寸为一半
static Ret darray_shrink(DArray *thiz)
//元素移动,用于中间的插入和删除。n为需要移动的元素个数。
static Ret darray_elem_move(DArray *thiz, size_t des_index, size_t src_index, size_t n)

外部方法:

//创建动态数组实例,返回此实例的指针,参数为该数组用的元素free函数,
DArray *darray_create(DArrayFreeFunc free,
		DArrayVisitFunc ref_ch,
		DArrayCmpFunc ref_eq,
		void *ctx);
//把data插入对应index,具体操作为调用expand(thiz, 1),elem_move, 设置值,
//ref_count++, set_length
Ret darray_insert(DArray *thiz, size_t index, void *data);
Ret darray_append(DArray *thiz, void *data);
Ret darray_prepend(DArray *thiz, void *data);
//删除某个元素,具体操作为elem_destroy, elem_move, set_length, shrink
Ret darray_delete(DArray *thiz, size_t index);
//把某个元素的内容设置为data
Ret darray_set_by_index(DArray *thiz, size_t index, void *data);
//获取某个index对应的元素,将其指针赋到data上
Ret darray_get_by_index(DArray *thiz, size_t index, void **data);

size_t darray_length(DArray *thiz);
Ret darray_foreach(DArray *thiz, DArrayVisitFunc visit, void *ctx);

//顺序调用elem_destroy, free掉所有元素,并把指针全部置为NULL
void darray_destroy(DArray *thiz);

在这个过程中同时折腾了好多的东西,感觉也很有意思。

  • 关于malloc来的内存没有初始化,原因是这样的:我原来认为malloc和free是c用来提供内存和释放内存的函数,其实更准确的说它们是管理内存的函数,它所做的事就是管理内存。malloc是找到一块没有其他人使用的内存,free是把这块内存标记为没有使用,至于内存中的内容?抱歉,这两个函数是不管里面的内容的,free掉的内存中甚至还可能有内存控制块的信息。这样的话所谓的“未初始化”的内存含义就是可能有人用过,可能没有人用过,至于里面的内容,谁知道是什么,可能刚刚有人拿它来存整数,也可能存指针了,也可能真就是空的。(此处提一嘴,强烈推荐K&R的C程序设计语言,这本书中居然还有malloc和free的实现!)
  • 关于struct,猜测内存对齐的原因及结构体访问的方式。新建一个struct实例的操作很简单,malloc一块空间,地址赋给struct类型的指针。就这么简单,这是怎么回事呢?从刚才的说法可以了解malloc来的内存其实是未初始化的,里面什么东西都有可能,那么我们调用其成员的时候系统是如何知道什么是它的成员呢?猜测:其实系统不知道,系统做的只是访问某块内存,对其读取或者写入,系统所了解到的“成员”只不过是这个“成员”相对于结构体指针地址的偏移量,为了方便管理和计算偏移,在编译的时候把它们的偏移量都统一确定了。(暂时不好查阅资料,内存对齐的话题看来要留到以后学习了)。

排序算法
这章中重新写了三个排序算法,冒泡,快速,归并。事实证明,我对这三个算法的熟悉程度并没有到“技能”的程度,平均一个算法写了一个半小时。这里将三个排序算法的关键点记录一下。

  • 冒泡排序:我采取了改进的冒泡法,即每次冒泡都进行到上次交换过的最后一个。此方法主要有三个变量,一个遍历用的变量,一个记录单轮冒泡过程中做交换的元素的位置,一个用来记录已经有序有序的元素的位置(它等于上一轮最后那做交换的元素的位置)。
  • 快速排序:主要思想是每轮都把一个元素放到最终位置上,并且保证比它大的和比它小的元素分开两边。这个算法有两个特殊情况需要处理,选取的标准元素是这一堆中的最小值,或者是其中的最大值,写条件的时候主意这两种情况外加不要越过左右边界,快速排序就不会有什么问题了。
  • 归并排序:它主要是两个动作,首先是递归,然后是合并有序数据。递归出口是函数参数包含的数据段合并完成,初始递归出口是数据段内只有一个元素。每个数据段都是闭区间,所以不要把某个元素包含到两个数据段中去。用来遍历的变量i超过边界是没有问题的,只要超过之后不会再次被使用。