动态数组

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);

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

[编程珠玑第二章]啊哈,算法

3

Posted by conan | Posted in 读书笔记 | Posted on 16-09-2009

看完编程珠玑第二章,发现确实很好玩。第二章用三道题目引出三个基本操作,然后再将这三个操作推广到更一般的情形。

  1. 二分搜索:将线性搜索的O(n)时间降到O(log2n) ,要求单调
    1. 看这句话似乎很平凡无奇,但是这句话确实是二分搜索适用范围的精确表达,关键在于线性搜索四个字。你明确知道什么样的是线性搜索么?我不敢说我很明确,但是在这里我把它总结为在线性解空间里寻找解。曾经有人给我讲过二分枚举法,一个奇形怪状的容器,问水面多高的时候水是总容积的一半。这就是个线性有序的解空间,可以进行二分搜索。如果你可以求得高与体积的关系式,那么直接根据一半体积求高的做法就更像是hash查找。
    2. 二分搜索的核心问题:1.有序2.有个比较“大小”的方法来将解空间每次减半(定义一个范围,在该范围内表示元素的方式,确定哪一半范围存在缺失整数的方法)
    3. A问题,有一个连续的文件里面有40亿个32位整数,找出至少一个不在这个文件里的32位整数。朴素的二分思想,每次找出存在缺失整数的那一堆。
  2. 向量旋转:问题B:用几十个字节的额外空间将一个n元向量x在正比于n的时间内左旋i个位置。对应于实际问题中交换相邻的不同大小的内存块。
    可用方法:

    1. “杂技”算法。以“元素”为单位(元素不仅可以为数组元素,还可以为一切旋转单位),循环移动该向量中的元素,若元素个数为n,移动距离为d,那么需要移动n与d的最大公约数趟。
    2. 等大小内存块移动法。目的:ab->ba,假设b比a长,那么取b后面的与a等长的一段br,与a交换。于是效果就是ablbr->brbla,a已经到了最终位置,剩下的目标就是交换bl和br。递归操作。
    3. 求逆法。(a’b')’=ba
      reverse(0, i-1);     //abcdefgh->cbadefgh
      reverse(i-1, n);     //cbadefgh->cbahgfed
      reverse(0, n);       //cbahgfed->defghabc

    直观上比较,杂技算法对每个元素仅存取一次,而求逆要两次,那么杂技算法算法的速度要两倍于求逆。实际上当n比较大而旋转距离变长的时候,由于杂技算法的高速缓存性能比较差,而求逆大部分操作为顺序读取,由于缓存局部性的影响,杂技算法的实际表现并不如求逆算法。

  3. 标识:当使用等价关系来定义类别时,定义一种标识,使得类中每一项都具有相同的标识,而该类以外的其他项没有,这样就可以将一个感觉无从下手的问题变成一个用已知数据结构和算法解决的问题。(ps:后续还可以根据标识进行排序,二分等等操作)
      问题C,挑选出一个词典中所有的变位词,就是如deposit和topside这样关系的词。如果要考虑单词字母的所有排列,那是注定要失败的方法。因为算法复杂度实在太大了。注意描述,deposit和topside是一类,他们是变位词,也就是说他们存在着某种等价关系。只要给所有变位词定义上同样的标识,然后将其按照标识排序,这个问题就变成了一个很容易下手的问题。

后记:本章主题是寻找合适的算法,绝对不要有了想法就急不可耐的去实现,就是要找到自己的灵机一动,用巧妙的方法来实现,要“建设性的懒惰”。

c语言宏处理器哈希表 & Python字典类

12

Posted by conan | Posted in 读书笔记 | Posted on 10-09-2009

哈希算法,用一个算式result=hash(key)就可以大致确定所需数据的位置,大大简化了数据查找的复杂度。此算法的关键在于建立一种两类数据间的映射,就是那个hash函数的选定。另外有个很重要的问题就是hash冲突,就是不同的key得到了同样的result。常用冲突解决法有拉链法(我认为叫做链表法更容易理解)和平面地址法两种,后面有描述。

c语言宏处理器(出自C程序设计语言)

“这段代码很典型,可以在宏处理器或编译器的符号表管理例程中找到。”

例如考虑define语句。
#define IN 1
需要把名字IN和替换文本1存入到某个表中,此后当名字IN出现在某些语句中时,就必须用1来替换IN。
以下两个函数用来处理名字和替换文本。install(s, t)函数将名字s和替换文本t记录到某个表中,其中s和他仅仅是字符串。lookup(s)函数在表中查找s,若找到,则返回指向该处的指针;若没找到,返回NULL。
该算法采用的是散列查找方法,将输入的名字转换为一个小的非负整数,该整数随后将作为一个指针数组的下标。数组的每个元素指向某个链表的表头,链表中的各个块用于描述具有该散列值的名字。如果没有名字散列到该值,则数组元素值为NULL。
链表块结构:
struct nlist {                /*链表项*/
struct nlist *next;    /*链表中下一表项*/
char *name;            /*定义的名字*/
char *defn;            /*替换文本*/
}
指针数组:
#define HASHSIZE 101
static struct nlist *hashtab[HASHSIZE];    /*指针表*/
散列函数hash,通过for循环将上次计算的结果值变换(乘以31)得到的值加上当前字符值(*s + 31 * hashval),然后对数组长度取模。这并不是最好的散列函数,但比较简短有效。
unsigned hash(char *s)
{
unsigned hashval;
for (hashval = 0; *s  != ”; s++)
hashval = *s + 31 * hashval;
return hashval % HASHSIZE;
}
strdup函数将通过参数传入的字符串复制到某个安全的位置
char *strdup(char *s)
{
char *p;
p = (char *) malloc(strlen(s) + 1);
if (p != NULL)
strcpy(p, s);
return p;
}
lookup函数发现表项存在,则返回指向该表项的指针,否则返回NULL。散列过程生成了在数组hashtab中执行查找的起始下标,若该字符串可以找到,则它一定位于该起始下标指向的链表的某个块中。

struct nlist *lookup(char *s)

{
struct nlist *np;
for (np = hashtab[hash(s)]; np != NULL; np = np-> next)
if (strcmp(s, np->name == 0))
return np;    /*found*/
return NULL;        /*not found*/
}

install函数借助lookup判断待加入的名字是否已经存在。若存在则用新的定义取而代之;否则创建新表项。若无足够空间创建新表项,则install函数返回NULL

struct nlist *lookup(char *);
char *strdup(char *);
struct nlist *install(char *name, char *defn)
{
struct nlist *np;
unsigned hashval;
if ((np = lookup(name)) == NULL){    /*not found*/
np = (struct nlist *) malloc(sizeof(*np));
if (np == NULL || (np->name = strdup(name)) == NULL)
return NULL;
hashval = hash(name);
np->next = hashtab[hashval];
hashtab[hashval] = np;
} else                                 /*already exist*/
free((void *) np->defn);    /*free previous defn*/
if ((np->defn = strdup(defn)) == NULL)
return NULL;
return np;
}
三点疑问:
1.hash函数如何选的?
2.hashsize如何选定?
3.作为宏处理器,又用了宏定义?

Python字典类(代码之美 第18章)

“字典是python语言的基本数据类型,它的作用就像awk里的关联数组或者perl里的哈希表”
python的字典类实现中一般有以下内容:
int ma_fill        13
int ma_used    13
int ma_mask    31
PyDictEntry ma_table[]:
[0]: aa, 1        hash(aa) == -1549758592, -1549758592 & 31 = 0
.
.
.
域成员的名字前缀ma_的意思是映射,在Python里它专指那些提供关键字/数据元素匹配功能的数据结构。域成员:
ma_used    有关键字的存储单元数量。加入关键字增1,删除减一。
ma_fill        有关键字存储单元和哑存储单元的总数。删除关键字的时候不变化
ma_mask    对应哈希表大小的比特掩码。哈希表有ma_mask+1个存储单元。规定存储单元数永远是2的幂,那么ma_mask就是2**n – 1
ma_table    指向PyDictEntry结构的数组。这个结构包含几个指针,分别指向关键字对象,数据元素对象,保存关键字哈希值的缓存。缓存哈希值是为了提高速度,如字典大小变化的时候。
为小哈希表而做的特别优化
PyDictObject还包括一个拥有8个存储单元 的哈希表。不多于5个元素的小字典可以直接放在这张表内,从而免去了调用malloc()的额外开销。同时这样做还提高了缓存的局部性,拿x86 GCC编译的PyDictObject来说,它的大小是124字节,刚好可以放进两个大小为64字节的缓存行。因为一般参数关键字用到的字典大部分都只包 含1到3个关键字,所以小哈希表的这种优化措施也提高了函数调用的性能。
为字符串关键字优化
一个字典里可以包括多种数据类型的关键字。不过在大多数Python程序的类实例和模块中,它们内部字典只用字符串做关键字。
首先,字符串比较时不会抛出异常,所以可以掠过一些不必要的错误检查。其次,比较两个字符串是比较简单的事,不像一般Python对象可能会有<, >, <=, >=, ==和!=这样的比较操作。
Java版优化:
Python的Java版本,Jython有一个专用字符串的字典类型:org.python.org.PyStringMap。而用 户编程时所用的字典类实际上是另外一个叫做org.python.core.PyDictionary的类型,内部使用 java.util.Hashtable类来保存内容,并加了一个简介层来提供子类化能力。Python不允许用户用其它数据类型替换内建的 __dict__字典类,从而免去了子类化的麻烦(有什么麻烦?不明白)。对Jython来说,有一个专门的字符串类型字典还是有意义的。
C版优化:动态选择存储方法
一开始,字典调用之处理字符串的方法,当用户需要检索非字符串数据类型时,就调用能够处理通用数据类型的方法。 PyDictObject只有一个域成员ma_lookup,他是一个函数指针
struct PyDictObject{
PyDictEntry *(*ma_lookup)(PyDictObject *mp, PyObject *key, long hash);
}
字典类通过(dict->ma_lookup)(dict, key, hash)来查找关键字,里面的参数key是一个指针,它指向代表关键字的PyObject,参数hash是根据关键字算出来的哈希值。刚开始 时,ma_lookup指向函数lookdict_string,这个函数假定字典里所包含的关键字和待检索的关键字都是字符串类型的,用结构 PyStringObject表示。如果字典里有非字符串类型的关键字,或是用户想查找这样的关键字时,ma_lookup就会改成指向更加通用的查表函 数。当lookdict_string函数发现其参数的实际类型不是字符串时,它就会修改ma_lookup,并调用新的函数来进行查找。(这就是说,如 果你对一个纯字符串版本的字典进行非字符串检索时,比如d.get(1),整体检索速度会比通用的情况要慢,哪怕这是一次失败查询。接下来字典的检索函数 就会切换到通用版本。这时候就算查的还是字符串类型的关键字,查询速度已经不能跟纯字符串版本同日而语了。)PyStringObject类型的子类应该 被认为时非字符串类型,因为子类里可能会定义相等比较操作。
冲突处理
拉链法:由于建立链表需要为每个链表元素申请内存空间,而内存分配比较慢。同时,遍历链表有可能会降低缓存局部性。此法被Python弃用。
开放地址法:如果第一次在存储单元i中没有找到待查关键字,那就按照某种固定的模式尝试其他存储单元。
/* 起始存储单元 */
slot = hash;
/* 初始干扰值 */
perturb = hash;
while (<存储单元非空> && <单元内的值不等于关键字值>){
slot = (5*slot) + 1 + perturb;
perturb >>= 5;
}
在c语言的代码里,5*slot可以用位运算和加法来替代:(slot<<2) + slot 。一开始,干扰因子perturb直接取成哈希值;接下来,他的二进制值在每次循环的时候右移5位。移位操作可以让哈希值的每个比特都能够迅速地参与到探 索尝试的计算中。经过若干次移位操作,干扰因子最终会变为0,此时探索模式就退化为slot=(5*slot)+1,它生成的数值可以使0和 ma_mask之间的任何一个数。因此,这种尝试模式可以确保:要么是找到关键字(如果是在检索字典),要么是找到一个空的存储单元(如果是准备向字典里 插入数据)。
每次偏移量5比特是根据试验得出来的。 跟4和6相比,5可以把冲突率再降低一点(Objects/dictobject.c里有大段的注释详细讨论了这种优化的前世今生。)
调整大小
关于关键字数量,现在的做法是保证哈希表最多有三分之二是满的。这个比例是折中的结果:太满就会导致更多的冲突,太稀疏就会浪费太多内存,同时也不利于缓存处理。
确定新尺寸
不超过5万个关键字的中小规模哈希表,新的尺寸应该是ma_used*4。大多数使用大字典的Python程序都会在初始阶段构建字 典,接下来做的就只是检索关键字或者是遍历字典。这时4倍会让字典显得有点稀疏(1/4的存储单元有数据),好处是在初始阶段构建字典时减少了字典调整的 次数。对于超过5w个关键字的大型字典,规定新尺寸是ma_used*2,可以避免浪费过多的内存空间。
一个合算的内存折中方案:空闲列表
Python在用参数关键字作函数调用时,会创建字典实例,然后在函数返回时销毁它们。这种操作发生的很频繁,字典实例的生命周期也很短。这种情况的一个有效优化措施是回收那些不再使用的数据结构。
Python用一个叫做free_dicts的数组来保存不再使用的字典。在Python 2.5中,这个数组长度是80.当需要创建PyDictObject时,Python会设法从free_dicts里找到一个指针,重用它指向的数据结 构。当字典被删除时, 它们就会被加到free_dicts数组中。如果此时数组已经满了,那就直接释放这个字典对象。
迭代和动态变化
在遍历迭代的过程中,Python不允许增加或者删除字典的存储单元。 当iter*()系列方法第一次被调用时,迭代器会记住字典中所有元素的个数。如果在遍历迭代过程中,字典大小发生变化,迭代器就会抛出 RuntimeError异常,告诉用户:“迭代过程中字典的大小发生了变化”。

第四章. 模块性:清晰,简洁

1

Posted by conan | Posted in 读书笔记 | Posted on 08-09-2009

半年前看的《unix编程艺术》,笔记电子化。红色为重点部分,蓝色为个人废话。

模块化原则:模块间通过应用程序编程接口(API)──一组严密,定义良好的程序调用和数据结构来通信。

API标准:若试着用纯人类语言来描述设计(不摘录源代码),能否将事情说清楚?在编码前为API编写一段非正式书面描述是个好习惯。一些最有能力的开发者,一开始总是在定义接口,然后编写简要注释,对其进行描述,最后才编写代码──因为编写注释的过程就阐明了代码必须达到的目的,这种描述能够帮助组织思路,本身就是十分有用的模块说明。P85(编写简要注释这个习惯半年了还没有养成)

Hatton的经验数据表明,最佳模块长度为200~400逻辑行,大概400~800物理行。P87

紧凑性:若有经验的用户不需要操作手册,那么着个设计满足紧凑性。评测API的经验:若编程者要记忆的条目大于,则不大可能算是严格紧凑的。合理对待紧凑性,设计中尽量考虑,绝不随意抛弃。(七,人类短期记忆的极限)

正交性:每一个动作(无论是API调用,宏调用,还是语言运算)只改变一件事,不会影响其它。无论控制什么系统,改变一个属性的方法有且只有一个。

重构:改变代码的结构和组织,而不改变其外在行为,目的在于提高正交性。

SPOT:真理的单点性(Single Point of Truth)。不要重复自身。任何一个知识点在系统内部都应有一个唯一,明确,权威的表述。数据结构中也存在类似原则,“无垃圾,无混淆”。无垃圾是指数据结构(模型)应该最小化,无混淆是指在真实世界中绝对明确清晰的状态在模型中也应该同样明确清晰(不要重复自身这条基本在每本编程方法的书里都有)

自顶向下和自底向上:考量差异的方法。问一下设计是围绕主事件循环(常常具备与其非常接近的高级应用逻辑)组织,还是围绕主循环可能调用的所有操作的服务库组织代码。P95

模块化编码需要考虑的问题:

  • 有多少全局变量?全局变量是模块化的毒药,很容易使各模块轻率、混乱地互相泄露信息。
  • 模块大小是否在最佳范围?多了会产生长期的维护问题。若不知自己的最佳范围,则最好保守,坚持下限。
  • 模块内的单个函数是否太大?内部不要太复杂。
  • 代码是否有内部API?即可作为单元向其它人描述的函数调用集和数据结构集。且每个单元都封装了某一个层次的函数,不受其它代码影响。
  • API的入口点是否超过七个?哪个类有七个以上的方法?数据结构成员是否超过七个?
  • 每个模块的入口点数量如何分布?是否不均?有很多入口点的模块真的需要那么多入口点么?模块的复杂性往往和入口点数量的平房成正比。

Unix哲学基础

0

Posted by conan | Posted in 读书笔记 | Posted on 08-09-2009

半年前看的《unix编程艺术》,笔记电子化。

By Doug Mcllorg:

  1. 让每个程序就做好一件事,有新任务就重新开始,不要往原程序中加入新功能而搞的复杂。
  2. 假定每个程序的输出都会成为另一个程序的输入,哪怕那个程序还是未知的,输出中不要有无关信息干扰。避免使用严格的分栏格式和二进制格式输入。不要坚持使用交互式输入。
  3. 尽可能早的将设计和编译的软件投入试用,哪怕是操作系统。理想状况应该是几个星期内。对拙劣的代码别犹豫,扔掉重写。
  4. 优先使用工具而不是拙劣的帮助来减轻编程任务的负担。

重点:一个程序只做一件事,并做好,程序要能协作,程序要能处理文本流。

By Rob Pike:

  1. 程序耗费运行时间的地方无法判定,瓶颈经常出现在想不到的地方。证实瓶颈之前切勿急于找地方改代码。
  2. 估量:没有估量代码,尤其未找到最耗时的部分前不要去优化速度。
  3. 花哨算法在n很小的时候通常很慢,而n通常很小。花哨算法的常数复杂度很大,除非n一直很大,否则不要用。(拿不准就穷举)
  4. 花哨算法比简单算法更容易出现bug,难以实现。尽量简单算法配合简单数据结构。
  5. 数据压倒一切。编程的核心是数据结构,而不是算法。

Unix系统的十七个原则

  1. 模块原则:使用简单的结构拼合简单的部件。
  2. 清晰原则:清晰胜于机巧。优雅清晰的代码不仅不容易崩溃,而且更易于让后来的修改者立刻理解。
  3. 组合原则:设计时考虑拼接组合,互相通信。
  4. 分离原则:策略同机制分离,接口同引擎分离。如:前端策略,后端机制,设计分层。
  5. 简洁原则:设计要简洁,复杂度能低则低。
  6. 吝啬原则:除非确无他法,不要编写庞大的程序。(体积大复杂度高)
  7. 透明性原则:设计要可见,以便审查和调试。
  8. 健壮原则:健壮源于透明与简洁。(避免在代码中出现特例)
  9. 表示原则:把知识叠入数据以求逻辑质朴而健壮。(在设计中应主动将代码的复杂度转移到数据中去)
  10. 通俗原则:接口设计避免标新立异。
  11. 缄默原则:如果一个程序没什么好说的,就保持沉默。
  12. 补救原则:出现异常时马上退出并给出足量错误信息。宁为玉碎,不为瓦全。
  13. 经济原则:宁花程序一分,不花程序员一秒。(人少花时间,交给机器)
  14. 生成原则:避免手工hack,尽量编写程序去生成程序。如makefile生成器。
  15. 优化原则:雕琢前先得有原型,跑之前先学会走。(先制作原型系统,再精雕细琢,优化前先确保能用。 | 先求运行,再求正确,最后求快。)
  16. 多样原则:绝不相信所谓“不二法门”的断言。
  17. 扩展原则:设计着眼未来,未来总比预想快。

应用实例:

  • 只要可行,一切都应该作成与来源和目的无关的过滤器。
  • 数据应尽可能文本化。
  • 数据库部署和应用协议应尽可能文本化。
  • 复杂的前端(用户界面)和后端应该泾渭分明。
  • 如果有可能,用c语言编写前先用解释性语言搭建原型。
  • 当且仅当只用一门语言编程会提高程序复杂度时,混合语言编程比单一语言编程来的好。
  • 宽收严发。
  • 过滤时,不需要丢弃的信息决不丢。
  • 小就是美,在确保完成任务的基础上,程序的功能尽可能少。