动态数组

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超过边界是没有问题的,只要超过之后不会再次被使用。

走近专业程序员

0

Posted by conan | Posted in 读书笔记 | Posted on 19-03-2011

最近开始看李先静的《系统程序员成长计划》,算是对长久以来缺乏编码的一种补偿把。使用这本书顺便还为了养成编码的习惯,免得见了代码就头疼。养成一个习惯有三个阶段,第一个阶段是需要一直提醒自己,而且觉得很别扭,第二个阶段是需要一直提醒自己,但是已经比较习惯了,第三个阶段是不再需要提醒自己,而且习惯了。纵观以往的努力,中断于第二阶段的情况太多了,因为已经比较习惯了,所以总会去放松,不小心就中断了。这个时候中断太可惜了,既浪费了前面的努力,也浪费了来之不易的良好状态。这次引以为戒,坚决不能再次中断。
目前进度,两周,第一章结束,通用双向链表写了五遍,虽说还不能将双向链表烂熟于心,也算有了一定的经验了。

专业程序员最需要的是什么?是专业态度。

专业态度一:风格态度。良好的代码风格是严谨的一种表现,“傻瓜都能写出机器能读懂的代码,但只有专业程序员才能写出人能读懂的代码。”

稳定的代码风格:
文件名:单词小写,多个单词用下划线分隔。如:dlist.c
函数名:单词小写,多个单词用下划线分隔。如:node_find。一个函数只完成单一功能。一个函数最好在80*24的范围内完成,函数内的临时变量不要超过10个(此句来自linux kernel coding style)。
结构/枚举/联合名:下划线开头,首字母大写,多个单词连写。如:struct _DListNode;
宏名:单词大写,多个单词下划线分隔。如:#define MAX_PATH 260
变量名:单词小写,多个单词下划线分隔。如:DList *node = NULL(此处习惯*与node相连沿用于K&R C,表明*node是DList,这种写法有利于同时定义多个变量。)

面向对象的命名方式:
1.以对象为中心,采用主谓形式(对象+动作)来命名,取代传统的动宾形式,如:dlist_append
2.第一个参数为对象,并用thiz命名,如:dlist_append(DList *thiz, void *value)
3.对象有自己的声明周期,所以有对应的创建和销毁函数。

排版:
使用空行:1.函数体之间。2.结构/联合/枚举声明。3.不同功能的代码块之间。4.功能类似的代码放在一起,和其他部分用可能空行分隔。5.用一行就够了。
使用空格:1.等号两边。2.运算符两边。3.参数之间(for,函数等)
使用括号:
1.分隔子表达式,让人更容易看明白。如:((a && b) || (c && d))。
2.条件判断和循环,即使是一句也要加括号。如:

1
2
3
4
5
if (a > b) {
return c;
} else if (a == b) {
...
}

缩进:使用tab。缩进超过三层的话说明设计有问题

保护隐私:封装。可以隔离变化,降低复杂度。
隐藏数据结构:若为内部数据结构,则直接放在C文件中,不要放在头文件里。若为外部也要使用,则对外暴露名字而封装实现细节。做法为:
在头文件中声明该数据结构。
在C文件中定义该数据结构。
提供操作改数据结构的函数,避免直接访问其成员。
提供创建和销毁函数。
不过是否封装还是要看具体情况。
隐藏内部函数:在头文件中只放最少的接口函数声明,在C文件中所有内部函数都加上static关键字(将函数private化)。
禁用全局变量。

通用化
通用链表的实现方式有两种:
深拷贝,存值,以void *来保存数据首地址,size_t length来保存数据长度。进行深拷贝的时候使用memcpy。C语言没有构造函数,实现深拷贝比较复杂,且复制数据带来性能开销,不符合C语言的风格,较少见。
浅拷贝,存指针,只用void *保存对象的指针。存放整数时,可以把void *强制转换成整数使用。
(注:是否要进行深拷贝要根据实际情况来决定,在需要的时候,比如防火墙处理数据包,还是需要做深拷贝的。另外,据说公司前辈说自己实现的拷贝函数性能比memcpy好。)
让C++可以调用:在头文件中防止编译器对函数名重新编码,加入如下内容:

1
2
3
4
5
6
7
#ifdef __cplusplus
extern "C" {
#endif
/* add your code here */
#ifdef __cplusplus
}
#endif

通用链表面向特定数据的操作
面向特定数据的操作会很多,但是若将其一一实现的话会产生大量重复的代码。将遍历和对数据的操作分离开,使用函数指针将对数据的操作传入就是一个良好的实现框架,称为回调函数法。使用回调函数来进行数据操作的时候会产生一些中间数据,对此引入一个上下文context(缩写为ctx)的概念,用它来做参数负责传入传出数据和保存中间变量。框架如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
 //定义函数指针别名
typedef STAT (*DListVisitFunc)(void *ctx, void *data);
//实现遍历
STAT dlist_foreach(DList *thiz, DListVisitFunc visit, void *ctx)
{
/* some code */
stat = visit(ctx, some_data);
/* some code */
}
//实际回调函数
static STAT sum_cb(void *ctx, void *data)
{
/* some code */
}
//调用方法
long long sum = 0;
stat = dlist_foreach(pdlist, sum_cb, &sum);

面向特定数据的操作不应该杂糅到通用链表中,否则会造成不必要的耦合度和复杂度。