动态数组

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

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

sed总结笔记

0

Posted by conan | Posted in 总结 | Posted on 23-06-2009

前言:其实是寒假在家看的,一直总结到现在才发上来。这些就是我看到的sed的精华部分了。

替换:
命令格式:地址范围s/匹配模式/替换字符/选项
/为分界符,可以用别的字符。s后的第一个字符会被识别为分隔符,要求不能是空格或制表符,且能将三段内容分开。
地址范围:数字或模式,如 1 1,4 /A/ /A/,/E/
匹配模式:可用基本元字符,另外可以用( )
替换字符:可写正常文字,也可有特殊字符:
&:代表匹配模式中正则表达式配的字符串。例:s/ab/&cd/
\n:与模式中第n(数字)个\( \)制定的字符串匹配。有点类似子式
\:转义替换特殊字符,如&,\,替换命令的分界符
选项:n:指定第n次出现时替换。(1~512)
g:所有目标均替换。
p:输出模式空间的内容。
w:写模式空间的内容。

删除: 地址范围d 整行删除
附加: 定位地址a\
文本
在定位到的行下面加一行,内容为命令中的文本
插入: 定位地址i\
文本
在定位到的行上面加一行,内容为命令中的文本
变换: 定位地址c\
文本
把定位到的行整行替换掉
*插入和附加不可与地址范围同用,交换可以

sed控制步骤:
1.预处理步骤。将下一行读入模式空间。
2.处理步骤。在模式空间中直接处理所有命令。
3.后处理步骤。输出模式空间并返回第一步骤。
*.删除命令出现后立刻停止处理步骤,并且跳过后处理步骤。
**.插入,附加,变换均为后处理步骤操作。变换先删除模式空间,然后返回后处理模式。

列表: l 前面也可加地址范围,一般用法是用-n参数禁止缺省输出再用 它来处理想要输出的部分。另外它会将不可打印字符输出。
输出: p 同列表命令相似,区别大概是不输出不可打印字符。
= 输出行号,不过在对应行的上一行(?)
转换: 行地址y/abc/ABC/ 将每一个a变为A,b变为B…对应位替换。
下一命令: 行地址n 向模式空间读入下一行,直接转入后处理步骤。
读入文件: 行地址r 文件
写文件: 行地址w 文件

多行下一行: N 将输入文件的下一行附加到模式空间的当前行之后,两端内容
间用换行符\n分开,可配合s将换行替换为空格。
多行删除: D 删除内容只第一个换行符(然后直接转到后处理步骤)
多行打印: P 输出模式空间至第一个换行符

驻留空间:除模式空间外的另一块临时空间。
驻留: 行地址h 将匹配的行覆盖驻留空间内的内容
H 附加到驻留空间,并加入换行符
获取: 行地址g 用驻留空间内容覆盖模式空间的内容
G 将驻留空间的内容附加到模式空间后面,并用换行符分隔
交换: 行地址x 交换驻留空间与模式空间的内容

附:ibm的sed教程(比我的详细很多)
http://www.ibm.com/developerworks/cn/linux/shell/sed/sed-2/index.html

天气预报改进之一

0

Posted by conan | Posted in 思考 | Posted on 08-06-2009

由于google天气预报总是月末让我手机访问某个网址,让没有开手机上网的我很不爽,于是找到linuxtoy上的飞信天气预报来尝试了一下
由于群发不怎么方便,现参考如下两个地址
http://blog.solrex.cn/articles/diy-free-weather-forecast-sms.html
做了一些改进
方案:
使用Solrex的命令行飞信工具作为最终发送工具
使用linuxtoy上 fangvv的方法抓取天气
参考数据与代码分离的思想,设计一个文本用来存放数据,使得增加群发对象和城市的时候不用修改代码
第一版文本格式:

[user]
13*********
[pass]
passwd
[city]
54161               长春
[to]
FetionId_1        自己
FetionId_2        nick
…..

[city]
59493           深圳
[to]
FetionId_1        nick

这个数据结构看起来很清晰,但是写起代码来判断比较多,最终awk脚本大概50多行代码。扫了一下unix编程艺术之后,参考.netrc做了如下改进:

第二版文本格式:

user    13*********
pass    passwd
city    54161           长春
to      FetionId_1        nick
to      FetionId_2        nick
…..
send    Yes

city    59493           深圳
to      FetionId_1        nick
…..
send    Yes

使用这个格式进行编码,awk脚本用了30行多点,代码如下

#!/usr/bin/awk -f
BEGIN{
SmsPath = “/home/conan/bin/weather/”
}
#支持注释
{
if (substr($1,1,1) == “#”)
next
}
#跳过空行
NF < 2{ next }
#设置用户名密码等信息
$1 != “to”{
Data[$1] = $2
}
#格式化群发列表
$1 == “to”{
if (Data["to"] == 0 )
Data["to"] = $2
else
Data["to"] = Data["to"] “,” $2
}
#获取天气并发送
Data["send"] == “Yes”{
Data["send"] = “No”
system(“wget -qnv -O ” Data["city"] ” http://wap.weather.com.cn/wap/” Data["city"] “/h24/”)
system(“sed -i -n ’15,31p’ ” Data["city"])
system(“sed -i ‘s/<[^<]*>//g’ ” Data["city"])
system(“sed -i /^$/d ” Data["city"])
#下面一行可以加上你要的内容
system(“sed -i ’1 i\**气象台为你预报’ ” Data["city"])
system(“sed -i ‘:a;N;s/\\n/ /g;ta’ ” Data["city"])
print (SmsPath “sendsms -vl -f ” Data["user"] ” -p ” Data["pass"] ” -t ” Data["to"] ” \”`cat ” Data["city"] “`\”")
system (“sleep 1″)
system (SmsPath “sendsms -vl -f ” Data["user"] ” -p ” Data["pass"] ” -t ” Data["to"] ” \”`cat ” Data["city"] “`\”")
print “——————–”
delete Data["to"]
delete Data["city"]
}

这版个人评价:不如上一版清晰明了,但是在人可以接收的情况下比较有利于编码。
由于google天气预报总是月末让我手机访问某个网址,让没有开手机上网的我很不爽,于是找到linuxtoy上的飞信天气预报来尝试了一下
由于群发不怎么方便,现参考如下两个地址
http://blog.solrex.cn/articles/diy-free-weather-forecast-sms.html
做了一些改进
方案:
使用Solrex的命令行飞信工具作为最终发送工具
使用linuxtoy上 fangvv的方法抓取天气
参考数据与代码分离的思想,设计一个文本用来存放数据,使得增加群发对象和城市的时候不用修改代码
第一版文本格式:

[user]
13*********
[pass]
passwd
[city]
54161               长春
[to]
FetionId_1        自己
FetionId_2        nick
…..

[city]
59493           深圳
[to]
FetionId_1        nick

这个数据结构看起来很清晰,但是写起代码来判断比较多,最终awk脚本大概50多行代码。扫了一下unix编程艺术之后,参考.netrc做了如下改进:

第二版文本格式:

user    13*********
pass    passwd
city    54161           长春
to      FetionId_1        nick
to      FetionId_2        nick
…..
send    Yes

city    59493           深圳
to      FetionId_1        nick
…..
send    Yes

使用这个格式进行编码,awk脚本用了30行多点,代码如下

#!/usr/bin/awk -f
BEGIN{
SmsPath = “/home/conan/bin/weather/”
}
#支持注释
{
if (substr($1,1,1) == “#”)
next
}
#跳过空行
NF < 2{ next }
#设置用户名密码等信息
$1 != “to”{
Data[$1] = $2
}
#格式化群发列表
$1 == “to”{
if (Data["to"] == 0 )
Data["to"] = $2
else
Data["to"] = Data["to"] “,” $2
}
#获取天气并发送
Data["send"] == “Yes”{
Data["send"] = “No”
system(“wget -qnv -O ” Data["city"] ” http://wap.weather.com.cn/wap/” Data["city"] “/h24/”)
system(“sed -i -n ’15,31p’ ” Data["city"])
system(“sed -i ‘s/<[^<]*>//g’ ” Data["city"])
system(“sed -i /^$/d ” Data["city"])
#下面一行可以加上你要的内容
system(“sed -i ’1 i\**气象台为你预报’ ” Data["city"])
system(“sed -i ‘:a;N;s/\\n/ /g;ta’ ” Data["city"])
print (SmsPath “sendsms -vl -f ” Data["user"] ” -p ” Data["pass"] ” -t ” Data["to"] ” \”`cat ” Data["city"] “`\”")
system (“sleep 1″)
system (SmsPath “sendsms -vl -f ” Data["user"] ” -p ” Data["pass"] ” -t ” Data["to"] ” \”`cat ” Data["city"] “`\”")
print “——————–”
delete Data["to"]
delete Data["city"]
}

这版个人评价:不如上一版清晰明了,但是在人可以接收的情况下比较有利于编码。