用Xlib库进行基本图形编程(2)

2.X Window系统的客户和服务器模型
X window系统开发之初有一个最大的目标-灵活性。想法是这样的东西看上如如何一回事,
东西如何工作的又是另外一回事。因而,底层提供在画窗口,处理用户输入,允许使用颜色
画图形(或者黑白屏幕)等动作中需要的工作。就这点决定了把系统分为两个部分。客户决定
作什么,而服务器实际在屏幕上画图并读出用户输入以发给客户进行处理。
这个模型正好和人们在客户和服务器中所习惯的行为相反。在我们的例子,用户坐在由服务
器控制的机器旁边,而客户可能运行于一个远程的机器上。服务器控制屏幕,鼠标和键盘。
客户可能连接到了服务器,发出画一个(或者多个)窗口的请求,并要求服务器把任何用户发
送给这些窗口的输入给他。因而,几个客户可能连接到了同一个X服务器-一个可能在运行e
mail软件,一个可能在运行WWW浏览器,等等。当由用户发送输入给某些窗口时,服务器向
控制这些窗口的客户发送消息以供处理。客户决定对输入作什么,并给服务器发送请求来在
窗口中绘图。
整个会话过程是用X消息协议执行的。该协议最初时由TCP/IP协议包执行的,允许客户运行
于任何和服务器连接在相同网络上的机器上。后来,X服务器被扩展为允许客户运行在本地
机器上更优的访问服务器(注意到X协议消息可能有几百KB那么大),比如使用共享内存,或
者使用Unix域sockets(一个在Unix系统上的两个进程间创建逻辑通道的方法)。
3.GUI编程-同步化的编程模型
不同于包含某种顺序化执行内质的传统的计算机程序。GUI程序通常使用同步化的编程模型
,也被称为“事件驱动编程”。这个意味着程序大部分时候时闲着的,等待由X服务器发送
的事件,然后根据这些事件作出反应。事件可能时”用户在点x,y处按下第一个按钮“,或
者时”你控制的串口需要重画“。为了程序能够响应用户输入以及刷新请求,它需要在一个
相当短的时间内处理每个事件(比如作为一个大体的规则,小于200毫秒)。
这也意味着程序可能不执行在处理过程也许需要很长时间的事件的操作(例如打开一个连接
到远程服务器的网络连接,或者连接到数据库服务器,甚至执行一个大文件的拷贝)。而是
,它需要同步化的执行所有这些操作。这可能通过使用各种同步模型来执行长时间的操作,
或者通过用不同的进程或者线程来执行他们。
因而GUI程序看上去大概是这样:
   1、执行初始化
   2、连接到X服务器
   3、执行X相关的初始化工作
   4、在没有结束之前:
        1、接收来自于X服务器的下一个事件
        2、处理事件,也许向X服务器发送多种绘画请求
        3、如果事件是退出消息,跳出循环
   5、关闭连接到X服务器的连接
   6、执行清理操作
4.基本的Xlib概念
为了消除程序事件实现X协议层的需求,一个称为‘Xlib’的库被创造出来。该库给程序提
供了一个对任何X服务器非常底层的访问。因为协议是标准化的,客户使用Xlib的任何一种
实现都可以和和X服务器通话。这些可能在今天看来没什么大用,但回到使用字符模式终端
和专有方法在屏幕上绘图的日子,这是一个很大的突破。事实上,你将注意到咱爱瘦客户,
窗口终端服务器,等等周围进行的各种虚伪的骗局。他们在今天实现X协议在80年代晚期已
经能够作的事情。另外一方面,X universe在玩一个关于CUA(共通用户感观,一个由IBM
制造的概念,指的是对所有程序使用共通的观感以使得用户能够更加轻松)的catch-up游戏。
没有共通的感观是X window系统创造者的哲学。明显,它有许多在今天看来显然的缺陷。
X Display

使用Xlib的最大的概念是X display。这是一个代表我们和一个给定X服务器打开的连接的结
构体。它隐藏了服务器的消息队列,客户将要发送给服务器的请求队列。在Xlib中,这个结
构体被命名为‘Display’。当我们打开一个连接到X服务器的连接的时候,库返回一个指向
这种结构体的指针。然后,我们把这个指针提供给任何需要发送消息给X服务器或者从这个
服务器接收消息的Xlib函数。
GC -图形上下文

当我们执行各种绘出(图形,文本,等)操作的时候,我们可能要指定几个选项以控制数据怎
么被绘出 - 前景色和背景色是什么,线的边缘如何连接,在绘出文本的时候使用何种字体
,等。为了避免给每个绘出函数提供n多参数,一个类型为‘GC’的图形上下文结构被启用
。我们在这个结构中设置各种绘出选项,并且把指向这个结构的指针传递给每个绘出函数。
这个是相当方便的,因为我们通常需要用相同选项执行好几个绘出请求。因而,我们初始化
图形上下文,设置所需的选项,并把这个GC结构传递给所有的绘出函数。
对象句柄

当各种对象被创造出来给X服务器使用 - 例如窗口,绘画区域和光标 - 相关的函数返回
一个句柄。这是实际存在于X服务器的内存中的对象的标识符。我们能够在后面通过把这些
句柄提供给各种Xlib函数来操纵这些对象。服务器保存了这些句柄和它们管理的对象之间的
映射。Xlib提供各种型别定义给这些对象(窗口,光标,色表等等),它们实际上最终映射为
简单的整数。我们在定义保存这些句柄的变量的时候仍然应当使用这些型别名-为了有更好
的可移植性。

为Xlib结构体分配内存

在Xlib的接口中使用了各种结构型别。他们中的一些直接由用户分配内存。其他的使用Xli
b函数分配。这使得库能够恰当的初始化这些结构。这非常方便,因为这些结构倾向于包含
很多变量,使得对于差劲点的程序员非常难于初始化它们。记住-Xlib尝试着尽可能的了灵
活,而且这意味着他也是尽可能的复杂。由缺省值使得初学X的程序员能够使用这个库,而
不打扰有经验的程序员在n多选项中作调整的可能。对于释放内存,由两种方法完成。在我们
分配内存的情况-我们用相同方法释放它们(也就是使用free()来释放由malloc()分配的内
存)。在我们用某Xlib函数分配的时候,或者我们使用返回动态分配的内存的Xlib查询方法
的时候-我们使用XFree()函数来释放这些内存块。
事件

型别‘XEvent’的结构被用来传递从X服务器接收来的事件。Xlib支持很大数量的事件型别
。XEvent结构包含接收事件的类型,以及与该事件相关的数据(例如事件产生的屏幕位置,
与事件相关的鼠标按钮,和‘redraw’事件相关的屏幕区域,等)。读取事件的数据的方法
和事件类型有关。因而,XEvent结构包含一个C语言对于所有可能事件型别的联合(如果你不
确知C的联合是什么,该是查查你的C语言手册的时候...)。因而,我们能够有一个XExpose
事件,XButton事件,XMotion事件,等。

Posted by 陈着 Nov 04, 2009 10:22:39 AM


用Xlib库进行基本图形编程

用Xlib库进行基本图形编程,从中大bbs的精华版找到
---------------------------------------------------------------------------------------------------------------------------------
目录
1、前言
2、X Window系统的客户服务器模式
3、GUI编程-同步化的编程模型
4、基本的Xlib概念
        1、X Display
        2、GC-图形上下文
        3、对象句柄
        4、Xlib 结构体的内存分配
        5、事件
5、编译给予Xlib的程序
6、打开和关闭一个连接到X服务器的连接
7、检查关于Display的基本信息
8、创建一个简单的窗口-我们的“hello world”程序
9、在窗口中绘画
        1、分配图形上下文(GC)
        2、基本绘图-点,线,框,圆...
10、X事件
        1、使用事件遮罩给事件型别注册
        2、接收事件-撰写事件循环
        3、暴露事件
        4、获得用户输入
                1、鼠标按钮点击和释放事件
                2、鼠标移动事件
                3、鼠标指针进入和离开事件
                4、键盘焦点
                5、键盘按下和释放事件
        5、X事件-完整的例子
11、处理文本和字体
        1、字体结构
        2、载入字体
        3、把字体赋给图形上下文
        4、在窗口中绘出文本
12、窗口阶层
        1、根,父和子窗口
        2、事件传播
13、和窗口管理器交互
        1、窗口属性
        2、设置窗口名和图标名
        3、设置最佳窗口尺寸
        4、设置窗口管理器的杂项
        5、设置应用程序的图标
14、简单窗口操作
        1、映射和取消映射窗口
        2、在屏幕上移动窗口
        3、缩放窗口
        4、改变窗口的堆叠次序-提高会放低
        5、标识会取消标识窗口
        6、获得窗口的信息
15、使用颜色来绘出彩虹
        1、色表
        2、分配和释放色表
        3、分配和释放单个颜色
        4、用颜色绘画
16、X Bitmap和Pixmap
        1、什么是X Bitmap?什么又是X Pixmap?
        2、从文件中载入Bitmap
        3、在窗口中画出Bitmap
        4、创建Pixmap
        5、在窗口中画出Pixmap
        6、释放Pixmap
17、改变鼠标光标
        1、创建和释放鼠标光标
        2、设置窗口的鼠标光标
前言


该教程是“可能会有”的关于在X Window环境下进行图形化编程的教程的第一个系列。其自
身是用处不大的。一个真正的X程序员一般使用一个更高层次的抽象,比如用Motif(或者它
的的免费版本,lesstiff),GTK,QT和类似的库。然而,我们需要从一个地方开始入手。
不仅如此,知道表象之下的事情是如何工作的决不会是坏的主意。

在读完本教程后,你可能能够些简单的图形程序,但是那不会是一个有良好用户界面的程序
。对于这样的程序,也许就要用到上述的库中的某一个。

Posted by 陈着 Nov 02, 2009 09:42:03 AM


(转)编程习惯

 

/Alexey Radul/程显峰

近年来,我对编程艺术有很多体会。过后,我发现有些体会是错的;有些体会我遗忘了但又重新感受到;而另外有些则是必然会发现的。我还完善了一套项目管理的好习惯,这习惯包括我自己的,或者小组的抑或是大的,公司内部的。一方面,这些习惯对软件的成功开发是至关重要的(太小或者纯粹巧合的不算),另一方面,这些习惯也不是什么高深莫测的东西,较小的篇幅就可以说清楚了,第三,这些习惯都没有得到应有的重视。所以我把这些写下来,而你呢,正读着呢。

本文包含很多零散的个人建议,有六大块,各讲一个方面。因为建议很多而且相互联系紧密,所以不太好把他们逐条陈列。这样写还有一个好处就是你可以有所挑选的阅读,把你所知道的部分跳过去,把你想重新思考的部分温习一下,或者只是简略的阅读提纲不深入研究具体内容。


1 版本控制

版本控制是一种在开发的过程中对软件开发的历史系统地跟踪的方法。此项任务由版本控制系统完成,如CVSSubversion。版本系统保持了一个受控编码的历史痕迹,提供很多操作:获得当前版本代码(通常称为检出);提交修改;更新工作拷贝来协调他人的修改。版本控制系统还提供一些其他功能,如用不同的方式检查代码的历史,撤销更改,回滚到软件历史的某个点,解决冲突(两个人同时对相同的代码段进行了不同的修改)。

用过文本编辑器的撤销按 钮,或是用备份文件进行过恢复的人,都能体会到让电脑记住以前的东西的惊奇效果。而代码比普通的文件要复杂的多,自然这种功能在开发中就更加重要了。要是 你没有体会过与别人一起开发的乐趣,记住我的话:潜在的混淆和数据的损失是与参与的人数成正比的。因此,版本控制在开发软件的过程中的作用是不可估量的, 如果使用得当的话还会发挥更大的效力。

那么,怎样才能用好版本控制呢?

1. 选用一个优秀的版本控制系统 我现在非常中意Subversion,它不但高效,而且免费。它现在非常流行,并且会变得更加流行。它几乎不干扰其他开发过程。我并不是说它是完美的,但在你找到更好的之前确实是一个不错的选择。

2. 一开始就要版本化 要是在项目中有些东西要保存在比草稿纸更加正式一点的地方,那么绝对要将其版本化。

推论:要是你已经开始了一个没有版本控制的项目,马上建立一个,并提交项目。

3. 就算你单干也要版本化 三个星期后的你将大大有别于今天的你。努力实践版本控制使得你实现你的意图的时候非常清晰,要是你忘记了手头做的什么事情(相信我,你会忘的),那它可有大用处了。此外,你还免费的做了一个备份,【1】看来这么做一点损失都没有,好处倒不少。

4. 只要是人类的智慧就要版本化 代码(肯定是人类的智慧啦),测试用例,编译脚本,文档,任务列表,解释说明,演讲稿,想法,需求只要是经过人的大脑创造出来的一切,都应该记录在版本控制系统里,除非你有更好的理由将它们放到别处。

5. 计算机生成的文件就不必了这样做只能导致项目出现不一致(如有人提交了源文件,却忘记生成了有依赖关系的衍生文件)。比较好的做法是让版本控制系统忽略这些衍生文件,需要的时候再生成就是了。但是万万要把能衍生文件的人类原创途径记录在案,包括执行生成过程的命令之类的。

6. 做好日志 好的版本控制系统都会在每一次更改的时候让你留下日志,目的是解释一下你对提交的代码都做了些什么。千万别忽略这一步,一定要写,并且好好的写。

i. 不光是为别人而写也是为自己,认真细致记录日志会迫使你梳理你的设计,看清问题所在,认清正在做的事情,也会使得想知道细节的人(同样包括未来某时的你)与代码的作者有个交流。

ii. 做了什么记下来(必要时补充原因),而不是怎么做的。要是怎么做的真的那么令人感兴趣,而且从修改本身很难去理解,当然有必要记一记,但是通常代码本身已经很能说明怎么做的了。要是真的有什么地方不清楚的话,那一定是你的思路。

iii. 描述点滴所做 版本控制系统能帮助你找到你所做的更改,要试着将所有的修改都详细的告诉系统。推论:不要做自己都解释不清的事情。要将其分割成很多比较小的步骤,然后一个一个的来。

7. 不能搞破坏 每 一次你提交了代码,系统应该是好用的。也就是说,其他人此时更新代码后应该能够编译(可能的话),执行测试套件,并通过测试。将错误提交了是对与你协同的 人(还是包括未来的你)极大的不尊重。这会让他们搞不清楚到底是因为自己的问题还是你提交的一些垃圾导致了系统不能运转)。【2

推论:要是你真的搞破坏了,要道歉,并立刻修复。

8. 修改要小到原子 理想的情形下,每一次修改只包含一个意图,每条日志只是说明一个问题的单句段落。这有一条傻瓜法则用以判断两个相互联系的事情到底是一个还是两个原子修改:问问自己会不会有人只想撤销其中一个而保留另一个。如果答案是肯定的,就要分开来提交。

9. 不要改而不交 拖延修改的提交时间越长,你越容易忘了自己都做了什么,越容易产生BUG,也越容易与其它人的相关工作不协调。要是你没有提交修改,其实一天的活儿就不算干完,除非你有更好的理由。


2 构建系统

构 建系统是很多自动完成软件开发任务。其中最常见的就是编译代码。运行测试组件也是构建系统提供的功能。还有很多其他的功用,像根据代码注释生成可浏览的文 档,将组件合并用以发布,等等。将这些工作自动化可以节省很多时间和人力,还会防止有无误操作或是由于懒惰疏忽而产生的错误。

已经有这样的工具了。UNIX工具make已经成为构建自动化的标准好多年了,而且相当好用。AntJava领域内非常流行,我个人喜欢rake3】。现代IDE也有部分这样的功能,或是有调用这些标准工具的接口。无论你最终使用哪一种工具,编译,测试,或是其它什么别的,应该简单到只是按一下按钮。

构建系统建议:

1. 使用真正的构建工具 学习一个全新的工具是有些令人却步,但却很值得。简单的脚本总是能搞定你的项目。不会有项目小到不适合应用这些标准工具,只有极少数特大号的项目不适合。【4

2. 使一切自动化 编译和测试应该从一开始就精心地自动化了。同样当你开始生成文档或者代码,做清理,安装等等的时候,也要使其自动完成。总的来说,要干第二次的任务最好就要自动化。

3. 自动过程要明晰 尤其要注意,当大家提交东西的时候,应该有一个精心定义的构建,一种大家都不应该破坏的构建。通常,这会包括成功的编译结果和测试结果,但是,一定要说清楚哪些命令用来执行,保证其总是能干活。并确保在任意一次执行都能清楚地知道它是否运转正常。


3 自动化的测试套件

测试套件是很多验证代码有效的测试。如果其能够完全由计算机来执行并评估结果,那么就称其为自动化了的。

可以将测试按照测试代码的多少分类:“单元测试检验单一组件的功能,比如一个函数或者一个类。集成测试检验若干组件,也可能是整个系统是否能协同工作。功能测试检验系统(通常是全部或大部分)在一个较高的层次上能否正常运转。后两个概念有些重叠,但是从不同的角度,和我个人的经验来说,还没有一个统一的说法到底如何区分。这两条术语经常用来区别于单元测试。

我不会详细讨论优秀自动测试的好处。当前,自动测试问题分得很细:谁应该负责测试,怎样测试,进行多少测试,以及如何将其自动化,何时创建测试,等等。在Internet上 有很多此项议题的讨论,我就不浪费大家的时间在这里啰嗦了。肯定的讲,我个人觉得良好的测试套件,包括足够的单元测试和集成测试(功能测试),对任何项目 都是非常重要的。这些应该与主要代码同步完成,还必须是同样的人(要是项目很大能请得起专业的测试人员帮助他们那就更好了)。

已经有一些用于编写测试组件的工具了,JavaJUnit,Python PyUnit, RubyTest::Unit, 等等实际上每一种编程语言都有xUnit风格的测试库,绝大部分还提供别的方法。不要被这么多选择吓着,是否使用测试组件比到底用哪种组件重要的多。

那么,如何建立一个优秀的测试组件呢?

1. 要提交组件 要知道测试程序也是代码的一部分,和那些在产品中真正运行的东西一样重要。将其版本化。像其它部分一样,共享,备份,跟踪都要进行。另外,所有人都要使用相同的组件。

2. 测试组件要自动化 应该有一个清晰的按钮(比如说‘make test’命令)可以执行所有的测试,并得出结果报告。

3. 测试组件应该是清晰明确的 运行了测试之后,通没通过应该一目了然,如果没通过,那一部分没通过也应该显而易见。绝对不要参砸任何的人为判定到底是通过了还是没通过。测试也不要产生任何可能会使报告费解的输出。

4. 一定要通过测试 提交未通过测试的东西无异于对构建进行破坏,是要绝对禁止的(如果不巧发生了,要立即修复)。如果你知道代码是正确无误的,是测试出了问题,那就要修改测试。如果你的确要提交,但是有些测试就是毫无道理的通不过,你也没时间立即debug,那么暂时将其从组件中去掉,过后要马上弄好。

5. 经常测试 修改的时候要测,提交前要测,开发期间要测。运行测试套件(至少也是其中的相当的一部分)应该成为你开发周期的一部分。

6. 测试先行 不要惊讶,我是说真的。当修正bug的时候,先要写一个能针对它的测试,然后再修复bug。要是测试通过了,就成了。添加新的功能的时候,先写好针对它的测试。不仅会帮助你理解这个功能到底应该干什么,还会在干成了的时候通知你。

7. 测试是可以执行的文档 与普通文档不同,测试从不说谎,因为人人都运行还总得通过。如果你觉得一段代码难于理解,那么就写一个单元测试。如果你写了实际的文档,写写测试来验证一下其中的陈述。

8. 依靠阅读来测试代码能否工作显然是行不通的,所以还是写测试来检验到底是否正常运转。你将会惊讶地发现找到很多bug,也会惊讶于避免了相当多的bug产生。

9. 只做有效的测试 很容易写如下的测试:当我运行这个程序时给这个输入,就会产生10000行我这个文件里的输出。通常这样做比没有好,但绝对是一个糟糕的测试,因为除了真正起作用的东西外,也测试了许许多多无关的细节(如浮点舍入误差)。改进类似的测试来适应程序中的修改,要么极其痛苦,要么很可能引入bug.


4 代码审核

代码审核是通过阅读别人的代码,来寻找错误,提出改进意见等的过程和实践。使得代码更清晰,设计更合理,综合性能更好是条很漫长的路。另外一双眼睛,另外一种审视问题的角度会极大的促进方案更清楚,更优异。代码审核还会帮助程序员相互传授有用的技术,方法,风格等。只要一个项目的开发者多于一个人,那么立即开始相互审核代码。理想情况下,每一行代码都要被两个人看过,作者和审核人。

对代码审核的 实践有很多东西值得探讨:何时审核,如何整体审核,由谁来审核。在很多全职的程序员共同参与的大工程中,每一段代码都应在提交的时候送评(如果有更好的工 具支持,则应在提交之前)。审核人要清楚与之相关的代码,并要理解其作用(还有,更重要的,可能会发生的错误)。在此过程中,审核要快速的完成(一到两天 内),作者在评论完成之前,不要试图改动这段代码,以避免把事情搞乱。

参 与人数少的小项目则不必如此。要是代码不多的话,进展的速度会很快,要是每一次提交都审核的话耽误不起。但是,代码审核对代码和写代码的人都很有益处(因 为,不说别的,这使得至少两个人都理解这段代码。)用什么样的风格做项目,只要适合就好,但一定要养成审核代码和让人审核代码的好习惯。

如果你是审核人:

1. 及时 如若有人请你审核一段代码,要么马上开始,要么痛快的告诉人家不行(并转交给更有资历的审核人)。千万不要让人家等。

2. 尊重 代码审核的目的是确保代码的质量,不是为了显示出谁比谁高明。作为审核人,你的确有很大的权力,但是不能乱用。

3. 详尽 若是有些东西你搞不清楚,要么是代码写的就不够清楚,要么是注释不够,也可能二者兼有。请作者澄清(不单单是对你私下里的解释,要写进代码里)。要是什么不对劲,这很可能,或者是对的但看上去是错的,或是晦涩的,都要告诉作者使其修改。

4. 执行规定 要是项目中有些规定或规范(代码风格,命名规范,测试等),发现不符的地方一定要让作者修改。有些时候这样做显得吹毛求疵,但是定下这些规定和规范是有缘由的,所以应该遵守。

如果你是被审核代码的作者:

1. 尊重 审核人是你的朋友,给你有价值的建议。如果你有些不同见解,那么这绝对是建设性谈话的好议题。要是审核人对你的代码有误解,那么极有可能是你没有把代码写清楚。

2. 不要以为批评是针对你本人 代码审核是要改进代码,不是要刺伤(或者是提升)你的自尊心。审核之所以一定要关注你做错的地方,是因为那就是要改进的地方。对不良代码的批评都是建设性的(很可能包括积极的建议从某方面深入思考),要是并非如此,也许找审核人礼貌的谈一谈是个不错的选择。

3. 尽早尽量获得审核 审核一大堆刚写的代码是非常非常讨厌的,只有一种例外,审核人发现在整个部分你都在犯傻。这个过程需要一点一点修改提交。即使你认为要多做一点儿,将工作拆分成易于审核的小块,这样会避免很多错误,以使你得到数倍的回报。十个100行的代码审核要比一个900行的来得轻松得多,拆分着做会节省很多寻找修正bug的工作量。


5 重构

重写一段代码保留其运行的外部特性,但在某些方面有所改进,这样的过程称之为重构。通常这是为了代码更加清晰易读,或是为了更易于扩展,也可能是为了执行的更快。这项活动不论规模大小,都可以叫做重构。重新命名变量和函数是重构,在类之间调换功用亦是重构,将一个100000行纠缠在一起的代码分解成一个插件架构和若干相对较小独立的组件。当然,三种目的的重构遇到的问题会有很大的不同,但有一点是不变的:无论哪一种目的的重构都不改变原有代码与外部世界的交互。

重 构对代码,对身心都是有益处的。一定要重构。重写一段代码并不丢人。就当第一版是草稿。这会帮助你勾勒问题的轮廓,是你明白你要从方案中得到些什么。得到 已经正常运转的代码是非常有用的,这样可以制定测试组件,来精确定义你到底要解决什么问题。然后,有了测试组件,你就可以修改你的初始方案做出改进。之 后,修改第二稿,第三稿,直到没有可改的了。相似的,千万不要以为没有对代码的行为做出显式的修改就是浪费时间。相反,通过梳理代码,你可以使其更加易读 便于理解,易于维护和扩展,便于发现和修正bug。要是需求在最后的时刻发生了变化,在这上面花点精力的就会显得太值了,事实上也往往如此。

那么,怎么重构呢:

1. 重构 其价值如何强调都不过分。

2. 不要将重构与实质性的修改混为一谈 代码的行为在你重构前后应该是一致的,相同的。这也是检查与验证你是不是进行重构的极其有效的手段。尽可能将你的重构一个一个的提交,而将实质性的修改分开提交。

3. 测 试重构的代码 除了最简单重构外,值得重构的代码,也只得测试。相应地,由重构的定义可知重构应该在不改变代码行为的框架内进行。若是测试套件已经有了足 够的边界条件测试,很好。要是没有,做一些,并在你开始重构之前弄好它们。这是一个很好的方法可以使你的重构不拖累其它部分。

4. 一小块一小块的来 信誓旦旦地说我要重写这个程序,然后埋头就干,企图一下子把所有都重做一遍。说说很容易,但绝对行不通。这样绝对不会得到看上去一样的程序,你就开始想是个bug呢,还是原本特性如此呢,原来的代码到底对此事如何处理呢,等等。绝对是灾难。在重构过程中,当目标总是停滞不前,我们总是可是把事情分成一小段一小段的,以便查验。

5. 别 害怕丑陋的中间产物 举例来说,如果你想修改一个接口,首先为新的接口提供支持,然后将所有客户端逐个转换,然后去掉旧版本的接口。要是修改过于庞大的 话,不要一蹴而就,将其拆分成几个小部分完成。可以肯定当你修改到一半时,代码看起来会很难看,有些客户端用新方法,有些用旧方式,但是程序依旧正常工 作,测试也正常通过,你好检验你的修改没有引入任何不希望的破坏。重构都可以像这样来拆分。尝试拆分,尽量将工作化成可以度量的小块儿。

6. 不要卡在中间 要是开始了进行重构,那就进行到底。关键是代码要变清晰,但是干了一半儿的活儿通常会是旧的混乱加上新的方案混合在一起。绝对不要让程序长时间出于这种状态。

7. 通过重构为实质性修改铺垫 你是不是经常开始做X的时候发现必须要先改Y才能行,而在改Y的时候,你又发现得先改Z,最后呢你困惑了,结果是一团糟。我们都有过类似的经历,只是相对少一些而已,预先估计都需要改什么,然后先重构,测试,提交Z,接着是Y,最后是X。这样会得到更好的代码,减少错误,即使真的有东西弄乱了,你也更容易让其步入正轨。

8. 别 怕删代码 人们(很可能是你)花了很多的时间去写并不意味着是代码一定能很好地完成既定任务。这些代码帮助定义了任务是什么,完成此项任务会遇到怎样的困 难,这些都对日后的工作很有用,因此其作者的工作很值得尊敬。即使是知道了不用这么做也是很有帮助的,有时候很值得投入全部力量去写代码做这项似乎无意义 的事。另一方面,无效的,废弃的代码只会使程序变得拥挤不堪,难于阅读和理解,所以还是删掉的好。要是后来你又最终决定用了,版本控制系统会将其完好无损 的还给你。

9. 不 要把应删掉的代码注释掉了事 我知道有些人重写提交代码的时候,把旧代码注释掉。千万不要这样。将代码注释掉不仅没有任何帮助而且非常容易造成混乱。如果 你是想有一个备份用来恢复,那么记住版本控制系统做这个更加在行。如果你是想解释新代码应该做什么,那么还是用英语解释更合适。不管你的目的是什么,注释 代码都不是个好主意。


6 代码风格

代码风格是指在写程序的时候我们做出的很多细微,不关紧要不加以思考的选择。一个子块要缩进多少?if语句里的条件加括号了么?只有一行的循环加花括号了么?+号和加数之间有空格么?是把代码用括号圈到一处还是有点缩进?5】一行可以有多长?这些都是微不足道的事情,但是做好这些会使程序变得非常明晰。

已 经有针对这些的工具了。这些工具程序审查代码,验证其是否符合某种风格规范,还能修改源程序以其符合风格规范。我自己从来没有用过这些工具,也没有在项目 中用过,所以我说不好其价值,但是我要是不提及其存在就是我的不对了。不论你使不使用这种工具,下面的建议都有帮助。

1. 找到一种代码风格 不被编译器或解释器重视的微小部分应该至始至终的保持一致。没必要一开始就将其写下来,但是一定要确保每一个人都知道。随着项目的增长,要形成书面的文档。

2. 认同这种代码风格 在项目初期,因为项目太小了一个人就可能改变风格,这时礼貌的和富有建设性的讨论就很有好处了。要是有明显更好的途径,绝对没有理由坚持一开始的老路。另一方面,不要在此方面浪费太多时间。继续做要做的决定,并在项目中实施。

3. 尊重编程语言的代码风格 要是你所使用的编程语言的社区已经有某种代码风格习惯,一定要遵循。这么做会使得项目之间相互收益,就像同一种代码风格会对一个项目本身有好处一样。

4. 遵守这个代码风格 即使你个人偏好在同一行开始一个花括号,要是项目要求其独立成行,还是按着做吧。一致性,和由此而来的稳定性比个人的口味更加重要。

5. 保持局部一致 如果你要修改的文件或组件不符合全局的代码风格,应该有人将其转换,但是如果你当时没有这个时间,那么遵循你要修改代码周围的代码风格。局部一致比全局一致更加重要。


7 结束语

如 果上面说的东西对你来说挺新鲜的,我鼓励你将其融入到你的工作中去。也许要花点时间习惯,但是相信我,很值得。要是你在学习这些,我建议宁火不温。这是因 为从定义上来讲,对某一概念缺乏了解会桎梏实践,所以对一件好事来讲,只有当你做过了头,你才能充分了解整个问题的尺度。只有当你既知道这是个好事并了解 你已经做过头了,你才会对什么是适量做出正确的判断。


8 注释

1. 要是你的工作拷贝完蛋了,你还有版本库,所以你还是能弄到一份工作拷贝;要是你的版本库完蛋了,就算你没有备份它,你还是有一份(或多份)工作拷贝,你还是可以恢复当前状态,并接着干活。不过呢,备份版本库才是好的方式,这样你就可以连同项目的历史都能恢复出来。

2. 许 多编程环境是允许提交试验性代码的,这种代码除了提交者本人外不会影响任何人。这样的代码不属于构建,所以就算提交了残缺的试验性代码也不能破坏构建本 身。当然有些情形下这样做是必须的,但是我必须要警告大家不要滥用:避免构建框架的束缚的同时,也丧失了构建框架的好处。

3. rakemake差不多,它用Ruby作为描述语言,比较新,但有些复杂的功能尚不具备。

4. 要是你的项目增长的比工具的能力还迅速,那么你正应该考虑雇佣一名专门的构建工程师来定制一个适用的系统,不过那是公司的事儿,不是个人的。

5. 这种风格问题是Lisp程序员提出来的,并困扰了很多其他语言程序员很长时间。

Posted by 陈着 Oct 30, 2009 08:21:38 AM


Osd-lyric classical module进展

最近插空就搞搞osd-lyrics,虽然进度慢,但是还是有进展的。相比osd模式,可能有些人喜欢一次可以看多行歌词(但其实已经不是当初设想的on-screen display),所以我们就有了做这个模块的想法了。然后我就肩负起osd-lyrics 0.3版本重要功能的开发。主要的参照对象是TTplayer的歌词显示方式。废话少说,发张图先:

现在歌词已经可以很平滑的滚动了,不会出现刚开始的跳跃现象。不过需要处理的是第一行和最后一行的渐进效果,这样在换歌词的时候就可以很没有让人难受的跳跃感。

下一步就是实现歌词的拖动,等着tigersoldier的接口,:D

ps:.透漏下一步计划,03版本发布的时候可能会加上另一重要功能,大家多多关注啊!

项目地址:http://code.google.com/p/osd-lyrics/

Posted by 陈着 Oct 19, 2009 08:03:50 AM


cairo graphics 手册(2)

Cairo 定义

本章我们将介绍Cairo 图形库中一些比较有用的定义。这将帮助我们更好的理解Cairo绘图模型。

Context

要用Cairo画图,首先得创建一个Cairo 上下文(context)。Cairo 上下文(context)中定义了所有的图形状态变量,它们描述了绘图是怎样完成的。其中包括信息如:线宽(line width),颜色(color)以及要绘制的图面(surface)和其他很多相关的信息。

函数gdk_cairo_create()可以为绘图区创建一个Cairo上下文(context)。

cairo_t *cr;
cr = gdk_cairo_create(widget->window);

这两行代码创建了一个Cairo 上下文(context)。在这个例子中,Cairo上下文(context)绑定到GdkDrawable。cairo_t结构包括了渲染设备的当前状态以及还未画出图形的坐标。从技术上说,cairo_t对象被称为cairo 上下文(context)。

所有与Cario有关的绘图都是由Cario_t对象来完成。Cairo上下文(context)可以被绑定到一个特别的图面(surface)上,如pdf,svg,png,GdkDrawable等等。

GDK没有包装Cairo API。它允许Cairo上下文(context)可以被用来在GDK 绘图区(drawables)上绘图。附加的函数被用来将GDK的矩形和区域转换成Cairo 路径(path),并用pixbufs作为资源进行绘图操作。

Path

路径(Path)由一条或者多条线组成。这些线由两个或者多个锚点(anchor point)连接而成。路径可以由直线和曲线组成。开放路径(Open path)和闭合路径(Closed path)是两种不同的路径(Path)。在开放路径中,起点和终点不相遇。在闭合路径中,起点和终点相遇。

在Cairo中,我们首先定义一个路径(Path),接着通过填充(filling)和描边(storking)使其可见。当调用cairo_stroke()或者cairo_fill()函数的时候如果路径(Path)是空的,必须重新定义一个新的路径(Path)。

路径(Path)可以由子路径(Subpaths)组成。

Source

图源(Source)是我们绘图过程中的画笔。我们可以将图源(Source)比作画笔或者油墨,这样就可以绘制轮廓和填充图形。有四种基本的图源(Sourse)包括颜色(Colors),渐变(gradients),模板(patterns)和图像(images)。

Surface

图面(Surface)是绘图的最终目标。我们可以用PDF或者PostScript图面(surface)来渲染文档。

文档中提及了以下几种图面(Surfaces)

typedef enum _cairo_surface_type {
  CAIRO_SURFACE_TYPE_IMAGE,
  CAIRO_SURFACE_TYPE_PDF,
  CAIRO_SURFACE_TYPE_PS,
  CAIRO_SURFACE_TYPE_XLIB,
  CAIRO_SURFACE_TYPE_XCB,
  CAIRO_SURFACE_TYPE_GLITZ,
  CAIRO_SURFACE_TYPE_QUARTZ,
  CAIRO_SURFACE_TYPE_WIN32,
  CAIRO_SURFACE_TYPE_BEOS,
  CAIRO_SURFACE_TYPE_DIRECTFB,
  CAIRO_SURFACE_TYPE_SVG,
  CAIRO_SURFACE_TYPE_OS2
} cairo_surface_type_t;

Mask

在图源(Source)应用到图面(surface)之前,要进行一次过滤。遮罩(Mask)作为一个过滤器,决定一个图源(Source)哪部分被应用到图面(Surface),哪部分没有。遮罩(Mask)不透明的部分允许将图源(Source)拷贝到图面(surface),透明的部分不允许复制图源(Source)到图面(Surface)。

Pattern

Cairo pattern(模板)代表要绘画到图面(surface)上的source(图源)。在cairo中,你可以读取一个pattern,可以将它作为绘画操作的图源或者遮罩来使用。pattern可以是纯色,一个图面,甚至是渐变。

Posted by 陈着 Oct 10, 2009 10:07:18 AM


(转)深入理解C语言指针的奥秘

貌似评价蛮好的一篇文章,先贴这里!

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

指针是一个特殊的变量,它里面存储的数值被解释成为内存里的一个地址。 要搞清一个指针需要搞清指针的四方面的内容:指针的类型、指针所指向的类型、指针的值或者叫指针所指向的内存区,还有指针本身所占据的内存区。让我们分别说明。

 
先声明几个指针放着做例子:  
例一:  

(1)int *ptr;
(2)char *ptr;
(3)int **ptr;
(4)int (*ptr)[3];
(5)int *(*ptr)[4];  

指针的类型
从语法的角度看,你只要把指针声明语句里的指针名字去掉,剩下的部分就是这个指针的类型。这是指针本身所具有的类型。让我们看看例一中各个指针的类型:  

(1)int *ptr;//指针的类型是int*
(2)char *ptr;//指针的类型是char*
(3)int **ptr;//指针的类型是int**
(4)int (*ptr)[3];//指针的类型是int(*)[3]
(5)int *(*ptr)[4];//指针的类型是int*(*)[4]
怎么样?找出指针的类型的方法是不是很简单?
 
指针所指向的类型
当你通过指针来访问指针所指向的内存区时,指针所指向的类型决定了编译器将把那片内存区里的内容当做什么来看待。从语法上看,你只须把指针声明语句中的指针名字和名字左边的指针声明符*去掉,剩下的就是指针所指向的类型。例如:  

(1)int *ptr;//指针所指向的类型是int
(2)char *ptr;//指针所指向的的类型是char
(3)int **ptr;//指针所指向的的类型是int*
(4)int (*ptr)[3];//指针所指向的的类型是int()[3]
(5)int *(*ptr)[4];//指针所指向的的类型是int*()[4]   
在指针的算术运算中,指针所指向的类型有很大的作用。指针的类型(即指针本身的类型)和指针所指向的类型是两个概念。当你对C越来越熟悉时,你会发现,把与指针搅和在一起的 "类型 "这个概念分成 "指针的类型 "和 "指针所指向的类型 "两个概念,是精通指针的关键点之一。我看了不少书,发现有些写得差的书中,就把指针的这两个概念搅在一起了,所以看起书来前后矛盾,越看越糊涂。


指针的值,或者叫指针所指向的内存区或地址
指针的值是指针本身存储的数值,这个值将被编译器当作一个地址,而不是一个一般的数值。在32位程序里,所有类型的指针的值都是一个32位整数,因为 32位程序里内存地址全都是32位长。   指针所指向的内存区就是从指针的值所代表的那个内存地址开始,长度为si   zeof(指针所指向的类型)的一片内存区。以后,我们说一个指针的值是XX,就相当于说该指针指向了以XX为首地址的一片内存区域;我们说一个指针指向 了某块内存区域,就相当于说该指针的值是这块内存区域的首地址。  
指针所指向的内存区和指针所指向的类型是两个完全不同的概念。在例一中,指针所指向的类型已经有了,但由于指针还未初始化,所以它所指向的内存区是不存在的,或者说是无意义的。  
以后,每遇到一个指针,都应该问问:这个指针的类型是什么?指针指的类型是什么?该指针指向了哪里?  


指针本身所占据的内存区
指针本身占了多大的内存?你只要用函数sizeof(指针的类型)测一下就知道了。在32位平台里,指针本身占据了4个字节的长度。指针本身占据的内存这个概念在判断一个指针表达式是否是左值时很有用。  


指针的算术运算  
指针可以加上或减去一个整数。指针的这种运算的意义和通常的数值的加减运算的意义是不一样的。例如:  
例二:  

(1)char a[20];
(2)int *ptr=a;
    ...
    ...
(3)ptr++;  
在上例中,指针ptr的类型是int*,它指向的类型是int,它被初始化为指向整形变量a。接下来的第3句中,指针ptr被加了1,编译器是这样处 理的:它把指针ptr的值加上了sizeof(int),在32位程序中,是被加上了4。由于地址是用字节做单位的,故ptr所指向的地址由原来的变量a 的地址向高地址方向增加了4个字节。由于char类型的长度是一个字节,所以,原来ptr是指向数组a的第0号单元开始的四个字节,此时指向了数组a中从第4号单元开始的四个字节。  
我们可以用一个指针和一个循环来遍历一个数组,看例子:
例三:  
(1)int array[20];
(2)int *ptr=array;
    ...
    //此处略去为整型数组赋值的代码。
    ...
(3)for(i=0;i <20;i++)
(4){
(5) (*ptr)++;
(6) ptr++;
(7)}
这个例子将整型数组中各个单元的值加1。由于每次循环都将指针ptr加1,所以每次循环都能访问数组的下一个单元。 再看例子:  
例四:  
(1)char a[20];
(2)int *ptr=a;
    ...
    ...
(3)ptr+=5;
在这个例子中,ptr被加上了5,编译器是这样处理的:将指针ptr的值加上5乘sizeof(int),在32位程序中就是加上了5乘4=20。由 于地址的单位是字节,故现在的ptr所指向的地址比起加5后的ptr所指向的地址来说,向高地址方向移动了20个字节。在这个例子中,没加5前的ptr指 向数组a的第0号单元开始的四个字节,加5后,ptr已经指向了数组a的合法范围之外了。虽然这种情况在应用上会出问题,但在语法上却是可以的。这也体现 出了指针的灵活性。

如果上例中,ptr是被减去5,那么处理过程大同小异,只不过ptr的值是被减去5乘sizeof(int),新的ptr指向的地址将比原来的ptr所指向的地址向低地址方向移动了20个字节。 总结一下,一个指针ptrold加上一个整数n后,结果是一个新的指针ptrnew,ptrnew的类型和ptrold的类型相同,ptrnew所指 向的类型和ptrold所指向的类型也相同。ptrnew的值将比ptrold的值增加了n乘sizeof(ptrold所指向的类型)个字节。就是 说,ptrnew所指向的内存区将比ptrold所指向的内存区向高地址方向移动了n乘sizeof(ptrold所指向的类型)个字节。  

一个指针ptrold减去一个整数n后,结果是一个新的指针ptrnew,ptrnew的类型和ptrold的类型相同,ptrnew所指向的类型和 ptrold所指向的类型也相同。ptrnew的值将比ptrold的值减少了n乘sizeof(ptrold所指向的类型)个字节,就是 说,ptrnew所指向的内存区将比ptrold所指向的内存区向低地址方向移动了n乘sizeof(ptrold所指向的类型)个字节。

 
运算符&和*  
这里&是取地址运算符,*是...书上叫做 "间接运算符 "。 &a的运算结果是一个指针,指针的类型是a的类型加个*,指针所指向的类型是a的类型,指针所指向的地址嘛,那就是a的地址。*p的运算结果就五花八门了。总之*p的结果是p所指向的东西,这个东西有这些特点:它的类型是p指向的类型,它所占用的地址是p所指向的地址。  
例五:  

(1)int a=12;
(2)int b;
(3)int *p;
(4)int **ptr;
(5)p=&a;
    //&a的结果是一个指针,类型是int*,指向的类型是int,指向的地址是a的地址
(6)*p=24;
    //*p的结果,在这里它的类型是int,它所占用的地址是p所指向的地址,显然,*p就是变量a。
(7)ptr=&p;
    //&p的结果是个指针,该指针的类型是p的类型加个*,在这里是int   **。该指针所指向的类型是p的类型,这里是int*。该指针所指向的地址就是指针p自己的地址。
(8)*ptr=&b;
    //*ptr是个指针,&b的结果也是个指针,且这两个指针的类型和所指向的类型是一样的,所以用&b来给*ptr赋值就是毫无问题的了。
(9)**ptr=34;
    //*ptr的结果是ptr所指向的东西,在这里是一个指针,对这个指针再做一次*运算,结果就是一个int类型的变量。

指针表达式  
一个表达式的最后结果如果是一个指针,那么这个表达式就叫指针表式。  
下面是一些指针表达式的例子:  
例六: 

(1)int a,b;
(2)int array[10];
(3)int *pa;
(4)pa=&a;//&a是一个指针表达式。
(5)int **ptr=&pa;//&pa也是一个指针表达式。
(6)*ptr=&b;//*ptr和&b都是指针表达式。
(7)pa=array;
(8)pa++;//这也是指针表达式。

例七:

(1)char *arr[20];
(2)char **parr=arr;//如果把arr看作指针的话,arr也是指针表达式
(3)char *str;
(4)str=*parr;//*parr是指针表达式
(5)str=*(parr+1);//*(parr+1)是指针表达式
(6)str=*(parr+2);//*(parr+2)是指针表达式
由于指针表达式的结果是一个指针,所以指针表达式也具有指针所具有的四个要素:指针的类型,指针所指向的类型,指针指向的内存区,指针自身占据的内存。

好了,当一个指针表达式的结果指针已经明确地具有了指针自身占据的内存的话,这个指针表达式就是一个左值,否则就不是一个左值。 在例七中,&a不是一个左值,因为它还没有占据明确的内存。*ptr是一个左值,因为*ptr这个指针已经占据了内存,其实*ptr就是指针pa,既然pa已经在内存中有了自己的位置,那么*ptr当然也有了自己的位置。

数组和指针的关系  
数组的数组名其实可以看作一个指针。看下例:  
例八:  

(1)int array[10]={0,1,2,3,4,5,6,7,8,9},value;
    ...
    ...
(2)value=array[0];//也可写成:value=*array;
(3)value=array[3];//也可写成:value=*(array+3);
(4)value=array[4];//也可写成:value=*(array+4);

上例中,一般而言数组名array代表数组本身,类型是int[10],但如果把array看做指针的话,它指向数组的第0个单元,类型是int*,所指 向的类型是数组单元的类型即int。因此*array等于0就一点也不奇怪了。同理,array+3是一个指向数组第3个单元的指针,所以* (array+3)等于3。其它依此类推。  

例九:  
(1)char *str[3]={
(2)  "Hello,thisisasample! ",
(3)  "Hi,goodmorning. ",
(4)  "Helloworld "
(5)};
(6)chars[80]
(7)strcpy(s,str[0]);//也可写成strcpy(s,*str);
(8)strcpy(s,str[1]);//也可写成strcpy(s,*(str+1));
(9)strcpy(s,str[2]);//也可写成strcpy(s,*(str+2));
上例中,str是一个三单元的数组,该数组的每个单元都是一个指针,这些指针各指向一个字符串。把指针数组名str当作一个指针的话,它指向数组的第0号单元,它的类型是char**,它指向的类型是char*。*str也是一个指针,它的类型是char*,它所指向的类型是char,它指向的地址是字符串 "Hello,thisisasample! "的第一个字符的地址,即 'H '的地址。   str+1也是一个指针,它指向数组的第1号单元,它的类型是char**,它指向的类型是char*。*(str+1)也是一个指针,它的类型是char*,它所指向的类型是char,它指向   "Hi,goodmorning. "的第一个字符 'H ',等等。  

下面总结一下数组的数组名的问题。声明了一个数组TYPEarray[n],则数组名称array就有了两重含义:第一,它代表整个数组,它的类型是 TYPE[n];第二   ,它是一个指针,该指针的类型是TYPE*,该指针指向的类型是TYPE,也就是数组单元的类型,该指针指向的内存区就是数组第0号单元,该指针自己占有 单独的内存区,注意它和数组第0号单元占据的内存区是不同的。该指针的值是不能修改的,即类似array++的表达式是错误的。   


在不同的表达式中数组名array可以扮演不同的角色。  
在表达式sizeof(array)中,数组名array代表数组本身,故这时sizeof函数测出的是整个数组的大小。在表达式*array中,array扮演的是指针,因此这个表达式的结果就是数组第0号单元的值。sizeof(*array)测出的是数组单元的大小。表达式array+n(其中n=0,1,2,....。)中,array扮演的是指针,故array+n的结果是一个指针,它的类型是TYPE*,它指向的类型是TYPE,它指向数组第n号单元。故sizeof(array+n)测出的是指针类型的大小。  
例十

(1)int array[10];
(2)int (*ptr)[10];
(3)ptr=&array;

上例中ptr是一个指针,它的类型是int(*)[10],他指向的类型是int[10]   ,我们用整个数组的首地址来初始化它。在语句ptr=&array中,array代表数组本身。本节中提到了函数sizeof(),那么我来问一问,sizeof(指针名称)测出的究竟是指针自身类型的大小呢还是指针所指向的类型的大小?答案是前者。例如:  

int (*ptr)[10];  

则在32位程序中,有:  
sizeof(int (*)[10])==4
sizeof(int [10])==40
sizeof(ptr)==4
实际上,sizeof(对象)测出的都是对象自身的类型的大小,而不是别的什么类型的大小。 


指针和结构类型的关系  

可以声明一个指向结构类型对象的指针。  
例十一:  

(1)struct MyStruct
(2){
(3) int a;
(4) int b;
(5) int c;
(6)}
(7)MyStruct ss={20,30,40};
//声明了结构对象ss,并把ss的三个成员初始化为20,30和40。
(8)MyStruct *ptr=&ss;
//声明了一个指向结构对象ss的指针。它的类型是MyStruct*,它指向的类型是MyStruct。
(9)int *pstr=(int*)&ss;
//声明了一个指向结构对象ss的指针。但是它的类型和它指向的类型和ptr是不同的。
请问怎样通过指针ptr来访问ss的三个成员变量?  
答案:  
ptr-> a;
ptr-> b;
ptr-> c;   

又请问怎样通过指针pstr来访问ss的三个成员变量?  
答案: 

*pstr;//访问了ss的成员a。
*(pstr+1);//访问了ss的成员b。
*(pstr+2)//访问了ss的成员c。

虽然我在我的MSVC++6.0上调式过上述代码,但是要知道,这样使用pstr来访问结构成员是不正规的,为了说明为什么不正规,让我们看看怎样通过指针来访问数组的各个单元:  
例十二:  

int array[3]={35,56,37};
int *pa=array;
通过指针pa访问数组array的三个单元的方法是:
*pa;//访问了第0号单元
*(pa+1);//访问了第1号单元
*(pa+2);//访问了第2号单元
 

从格式上看倒是与通过指针访问结构成员的不正规方法的格式一样。所有的C/C++编译器在排列数组的单元时,总是把各个数组单元存放在连续的存储区里,单元和单元之间没有空隙。但在存放结构对象的各个成员时,在某 种编译环境下,可能会需要字对齐或双字对齐或者是别的什么对齐,需要在相邻两个成员之间加若干个 "填充字节 ",这就导致各个成员之间可能会有若干个字节的空隙。所以,在例十二中,即使*pstr访问到了结构对象ss的第一个成员变量a,也不能保证*(pstr+1)就一定能访问到结构成员b。因为成员a和成 员b之间可能会有若干填充字节,说不定*(pstr+1)就正好访问到了这些填充字节呢。这也证明了指针的灵活性。要是你的目的就是想看看各个结构成员之 间到底有没有填充字节,嘿,这倒是个不错的方法。过指针访问结构成员的正确方法应该是象例十二中使用指针ptr的方法。

 
指针和函数的关系  
可以把一个指针声明成为一个指向函数的指针。

int fun1(char*,int);
int (*pfun1)(char*,int);
pfun1=fun1;
....
....
int a=(*pfun1)( "abcdefg ",7);//通过函数指针调用函数。  

可以把指针作为函数的形参。在函数调用语句中,可以用指针表达式来作为实参。  
例十三: 

(1)int fun(char*);
(2)int a;
(3)char str[]= "abcdefghijklmn ";
(4)a=fun(str);
   ...
   ...
(5)int fun(char*s)
(6){
(7)  int num=0;
(8)  for(int i=0;i<strlen(s);i++)
(9{
(10)   num+=*s;
(11)   s++;
(12) }
(13) return num;
(14)}
 

这个例子中的函数fun统计一个字符串中各个字符的ASCII码值之和。前面说了,数组的名字也是一个指针。在函数调用中,当把str作为实参传递给 形参s后,实际是把str的值传递给了s,s所指向的地址就和str所指向的地址一致,但是str和s各自占用各自的存储空间。在函数体内对s进行自加1 运算,并不意味着同时对str进行了自加1运算。


指针类型转换  

当我们初始化一个指针或给一个指针赋值时,赋值号的左边是一个指针,赋值号的右边是一个指针表达式。在我们前面所举的例子中,绝大多数情况下,指针的类型和指针表达式的类型是一样的,指针所指向的类型和指针表达式所指向的类型是一样的。  
例十四:  

(1)float f=12.3;
(2)float *fptr=&f;
(3)int *p;  

在上面的例子中,假如我们想让指针p指向实数f,应该怎么搞?是用下面的语句吗?  

p=&f;  

不对。因为指针p的类型是int*,它指向的类型是int。表达式&f的结果是一个指针,指针的类型是float*,它指向的类型是 float。两者不一致,直接赋值的方法是不行的。至少在我的MSVC++6.0上,对指针的赋值语句要求赋值号两边的类型一致,所指向的类型也一致,其 它的编译器上我没试过,大家可以试试。为了实现我们的目的,需要进行 "强制类型转换 ":  

p=(int*)&f;

如果有一个指针p,我们需要把它的类型和所指向的类型改为TYEP*TYPE,   那么语法格式是:  

(TYPE*)p;

这样强制类型转换的结果是一个新指针,该新指针的类型是TYPE*,它指向的类型是TYPE,它指向的地址就是原指针指向的地址。而原来的指针p的一切属性都没有被修改。  

一个函数如果使用了指针作为形参,那么在函数调用语句的实参和形参的结合过程中,也会发生指针类型的转换。  
例十五:

(1)void fun(char*);
(2)int a=125,b;
(3)fun((char*)&a);
   ...
   ...
(4)void fun(char*s)
(5){
(6)  char c;
(7)  c=*(s+3);*(s+3)=*(s+0);*(s+0)=c;
(8)  c=*(s+2);*(s+2)=*(s+1);*(s+1)=c;
(9)}

注意这是一个32位程序,故int类型占了四个字节,char类型占一个字节。函数fun的作用是把一个整数的四个字节的顺序来个颠倒。注意到了吗?在函 数调用语句中,实参&a的结果是一个指针,它的类型是int*,它指向的类型是int。形参这个指针的类型是char*,它指向的类型是 char。这样,在实参和形参的结合过程中,我们必须进行一次从int*类型到char*类型的转换。结合这个例子,我们可以这样来想象编译器进行转换的 过程:编译器先构造一个临时指针char*temp,   然后执行temp=(char*)&a,最后再把temp的值传递给s。所以最后的结果是:s的类型是char*,它指向的类型是char,它指 向的地址就是a的首地址。  

我们已经知道,指针的值就是指针指向的地址,在32位程序中,指针的值其实是一个32位整数。那可不可以把一个整数当作指针的值直接赋给指针呢?就象下面的语句:  

(1)unsigned int a;
(2)TYPE *ptr;//TYPE是int,char或结构类型等等类型。
   ...
   ...
(3)a=20345686;
(4)ptr=20345686;//我们的目的是要使指针ptr指向地址20345686(十进制)
(5)ptr=a;//我们的目的是要使指针ptr指向地址20345686(十进制)
 

编译一下吧。结果发现后面两条语句全是错的。那么我们的目的就不能达到了吗?不,还有办法:  

(1)unsigned int a;
(2)TYPE *ptr;//TYPE是int,char或结构类型等等类型。
   ...
   ...
(3)a=某个数,这个数必须代表一个合法的地址;
(4)ptr=(TYPE*)a;//呵呵,这就可以了。

严格说来这里的(TYPE*)和指针类型转换中的(TYPE*)还不一样。这里的(TYPE*)的意思是把无符号整数a的值当作一个地址来看待。上面强调了a的值必须代表一个合法的地址,否则的话,在你使用ptr的时候,就会出现非法操作错误。  

想想能不能反过来,把指针指向的地址即指针的值当作一个整数取出来。完   全可以。下面的例子演示了把一个指针的值当作一个整数取出来,然后再把这个整数当作一个地址赋给一个指针:  
例十六:  

(1)int a=123,b;
(2)int *ptr=&a;
(3)char *str;
(4)b=(int)ptr;//把指针ptr的值当作一个整数取出来。
(5)str=(char*)b;//把这个整数的值当作一个地址赋给指针str。

现在我们已经知道了,可以把指针的值当作一个整数取出来,也可以把一个整数值当作地址赋给一个指针。  


指针的安全问题  
看下面的例子:  
例十七:

(1)char s= 'a ';
(2)int *ptr;
(3)ptr=(int*)&s;
(4)*ptr=1298; 

指针ptr是一个int*类型的指针,它指向的类型是int。它指向的地址就是s的首地址。在32位程序中,s占一个字节,int类型占四个字节。最 后一条语句不但改变了s所占的一个字节,还把和s相临的高地址方向的三个字节也改变了。这三个字节是干什么的?只有编译程序知道,而写程序的人是不太可能 知道的。也许这三个字节里存储了非常重要的数据,也许这三个字节里正好是程序的一条代码,而由于你对指针的马虎应用,这三个字节的值被改变了!这会造成崩 溃性的错误。  

让我们再来看一例:  
例十八: 

(1)char a;
(2)int *ptr=&a;
   ...
   ...
(3)ptr++;
(4)*ptr=115;  

该例子完全可以通过编译,并能执行。但是看到没有?第3句对指针ptr进行自加1运算后,ptr指向了和整形变量a相邻的高地址方向的一块存储区。这 块存储区里是什么?我们不知道。有可能它是一个非常重要的数据,甚至可能是一条代码。而第4句竟然往这片存储区里写入一个数据!这是严重的错误。所以在使 用指针时,程序员心里必须非常清楚:我的指针究竟指向了哪里。在用指针访问数组的时候,也要注意不要超出数组的低端和高端界限,否则也会造成类似的错误。

 
在指针的强制类型转换:ptr1=(TYPE*)ptr2中,如果sizeof(ptr2的类型)大于sizeof(ptr1的类型),那么在使用指 针ptr1来访问ptr2所指向的存储区时是安全的。如果sizeof(ptr2的类型)小于sizeof(ptr1的类型),那么在使用指针ptr1来 访问ptr2所指向的存储区时是不安全的。至于为什么,读者结合例十七来想一想,应该会明白的。

 

 

Posted by 陈着 Oct 08, 2009 10:13:16 AM


mp3乱码问题

1.安装mid3iconv,该工具来自Python 的 Mutagen 模块,此模块负责处理各种音频文件的元数据,也包括 mp3 文件的各式标签。

$ sudo apt-get install python-mutagen

2.结合find命令,对当前目录下的.mp3格式的文件标签进行编码转换。

find . -iname '*.mp3' -exec mid3iconv -e GBK {}\;

 

 

Posted by 陈着 Oct 08, 2009 12:18:13 AM


osd-lyrics正在不断完善中。。

0sd-lyrics从5月份开始,从无到有,从粗糙简陋到功能逐渐完善,第一个感谢的人是项目老大TigerSoldier,其次是广大的支持者。我真正感受到开源的力量,有网友对我们项目的支持与关注,我们有了更多的需求,我们有了自己的图标,只有大家的支持才能给我们继续开发的信心。

想想自己,这几个月来,虽然对项目的贡献不多,但是却学到了很多东西,更领悟了“我爱linux因为我自由”这句话的真谛。在此谢谢各位曾经支持和帮助我们的人,我“临危受命”接受osd-lyrics 0.3版的主要功能,不过进度有些缓慢,很惭愧,只能不断的加油来弥补之前时间的流失,希望不久后出来的0.3版可以更满足大家的需要,加油加油!!

 

Posted by 陈着 Sep 24, 2009 08:30:07 AM


apt-get 代理设置

 刚从中大搬过华工,今天才有网上。之前的中大内部源显然不能用了,于是找中科大的老源,apt-get update出现错误,貌似不能直接连到某些国外网站。。。华工是教育网阿。。悲

还是解决问题吧。。。设代理:

sudo gedit /etc/apt/apt.conf

加入一行:

Acquire::http::Proxy "http://yourproxyaddress:proxyport";

然后当然是:

sudo apt-get update

试了下,速度210k/s,没办法,用的是代理嘛

ps:华工的网速还是超快的,看视频一点都不卡

 

 

 

Posted by 陈着 Sep 09, 2009 09:39:33 AM


(转)GNU hello学习笔记(1)——autoconf和automake

tigersoldier终于写了第一个Gnu hello心得笔记,赶紧转载学习。


 

什么是 GNU hello

GNU hello 是 GNU 推出的 hello world 软件,就是将入门的 hello world,以正规的 GNU 规范来实现,从而来展示 Unix-like 系统下开发软件的一些常用技术和软件的组织方法。麻雀虽小,五脏俱全,GNU hello 虽然只是一个 hello world,却包含了如下几项技术:

如何学习 GNU hello

最好的方式莫过于自己参照 GNU hello 弄个自己的 hello world 出来,把其中的各项有用技术都自己玩个遍。只看书永远没有实践来得实在,不是么?

程序主要的新东西自己手写,一些体力劳动就直接复制粘贴了。最终出来的内容不一定与 GNU hello 完全相同。

本篇学习内容

基本的程序文件组织方式

  • 基本的 Autoconf 和 Automake 配置文件写法及其原理

好了,现在开始!

程序文件的组织

严格来说,一个程序并没有什么标准的组织规范,任何组织形式都是可以的。但是良好的程序文件组织结构可以让人快速定位开发文件,更好地管理项目。在 长期的实践中,Unix社区对一些常见文件的组织逐渐形成了一些传统,这些传统不仅是经受了时间的考验,也能让别人更好的了解自己的项目文件组织结构。

在 GNU hello 中,文件是这样组织的(设“/”是代码根目录)

  • \:根目录存放automake和autoconf等配置文件,以及程序的一些说明文件(NEWS、Changelg、COPYING、INSTALL、AUTHORS等)
    • build-aux:automake 和 autoconf 自动生成的一些脚本
    • contrib:暂时不明
    • doc:程序文档
    • gnulib:Gnulib 的各文件
    • man:manpage存放地
    • po:gettext 翻译文件
    • src:程序源文件
    • tests:测试脚本

在其他项目中,我见过的有doc、man、po和src,这些应该是约定俗成的命名。另外会生成库文件的项目不少是把库文件放在lib目录下的。

Autoconf 和 Automake

它们是干啥的?

Unix下的软件,编译安装一般来说是要运行如下三条命令:

1../configure

2.make

3.make install

 

make是通过Makefile里指定的命令与目标来进行相应操作的程序,可以简单地理解为一串指令的集合(当然, 它远远不止于此)。用它就不必为编译代码写一条条的指令,只要把相关的指令写在Makefile里,直接make就行了。

然而不同的系统里装的软件不一样。可能一台机子里装了gcc,另一台机子里装的是cc。为了使Makefile能在所有机子里通用,就了有 configure脚本。代码里并不带有Makefile,只有一个模板文件Makefile.in,里面用变量来定义要使用的命令,由 configure脚本将检测到的工具填入Makefile.in生成Makefile。

但是不论是configure脚本还是Makefile,都并不是两句话可以写完的,而其内容多是类似的重复内容,于是就有了autoconf和automake。它们通过一些相对简单的语法来生成标准的configure脚本和Makefile。

准备工作

写一个最简单的hello world,存为src/hello.c:

 
1.#include <stdio.h>
2.
3.int
4.main (int argc, char *argv[])
5.{
6printf ("hello world!");
7return 0;
8.}
 
 

编写configure.ac

configure.ac是autoconf的配置文件,可以通过autoscan来辅助生成。旧版本的autoconf使用configure.in,现在这两个文件名都是通用的

在顶层目录运行autoscan,软件自动生成configure.scan,这是一个根据现有代码生成的configure.ac模板,将它另存为如下的configure.ac文件:

 
01.# 初始化autoconf,这句必须写在其他之前。定义的参数分别为软件名,版本号,维护地址(网站/邮件)
02.AC_INIT([TS Hello], [0.1], [tigersoldi&lt;at&gt;gmail&lt;dot&gt;com])
03.# [可选]定义生成的辅助文件的目录,默认是与configure.ac同目录
04.AC_CONFIG_AUX_DIR([build-aux])
05.# 初始化automake
06.AM_INIT_AUTOMAKE([readme-alpha])
07.# 定义需要的autoconf最低版本,这是autoscan自动生成的,未必需要这么高的版本
08.AC_PREREQ([2.63])
09.# 定义源代码的路径,通过指定源代码目录内一个存在的文件来指定
10.AC_CONFIG_SRCDIR([src/hello.c])
11.# 检测C编译器
12.AC_PROG_CC
13.# 生成配置文件,配置文件都是将`配置文件.in'进行变量替换后得到的
14.AC_CONFIG_FILES([Makefile src/Makefile])
15.# 生成上面要求输出的文件
16.AC_OUTPUT
 
 

 编写Makefile.am

与autoconf类似,automake的配置文件是Makefile.am,它会用此来生成Makefile.in文件,该文件是configure脚本用来生成Makefile文件的模板。

与configure.ac不同,我们要为每个文件夹写一个Makefile.am文件

根目录下的Makefile.am文件很简单,它不用做任何事情, 只要告诉make它有个子目录src,让make去操作src目录:

 
1.#处理如下子文件夹
2.SUBDIRS = src
 

src目录下的Makefile.am指定了我们要编译的程序及其文件:

 
1.# 定义要编译的程序hello,可以有多个,用空白符(空格、TAB、行末加了`\'的换行)分开
2.bin_PROGRAMS = hello
3.# 定义hello的源文件,必须以`<程序名>_SOURCES'的形式来定义,
4.# <程序名>就是bin_PROGRAMS中定义的程序之一,同样可以有多个源程序
5.hello_SOURCES = hello.c
 
 

 编译程序

文件都准备好后,就可以开始编译了。在编译之前,我们先让autoconf和automake生成我们需要的文件(确保在程序根目录):

 

01.# 将configure.ac里所需要的M4宏复制到文件夹中
02.aclocal
03.# 通过configure.ac生成configure脚本
04.autoconf
05.# 创建build-aux文件夹
06.mkdir build-aux
07.# 创建GNU要求的说明文件
08.touch NEWS README AUTHORS ChangeLog
09.# 通过Makefile.am生成Makefile.in模板
10.automake --add-missing --copy
 
 

其中,aclocal是为autoconf进行准备工作的。

automake默认是GNU模式,GNU规范要求程序包含(NEWS README AUTHORS ChangeLog)等说明文件,我们用touch暂创建几个临时文件上去。

automake还需要创建一些辅助脚本,这些脚本由--add-missing选项添加到目录中。由于在configure.ac里用 AC_CONFIG_AUX_DIR定义到了build-aux目录下,我们要先创建这个目录。如果没有指定--copy,那么创建的是指向 automake安装目录相应文件的符号链接,指定后是直接复制一份。

可以看到autoconf创建了configure脚本,而automake则在程序根目录和src子目录下创建了Makefile.in,接下来就用configure脚本配合Makefile.in来生成Makefile:

1../configure

由于我们在configure.ac里用AC_CONFIG_FILES指定了生成Makefile和src/Makefile两个文 件,configure脚本通过相应的.in文件替换到检测到的相应变量后生成。注意生成的文件不仅限于Makefile,任何文件名都是可以的,只要它 有.in模板。

有了Makefile就可以make啦:

1.make all

直接用make也可以。all是指定的目标,也是一个约定俗成的目标(这是Makefile指定的,不同Makefile可以指定不同的目标,只要把它放在第一个就行),它的作用是编译程序。

于是现在可以执行Hello world了:

 
1../src/hello
 

 可以看到我的hello world出现了:-)

换一个目标试试:

1.make install
 

install也是个约定俗成的目标,它把我们的软件安装到系统中。现在可以直接输入hello来运行软件了。

有安装自然也有卸载,卸载的目标是uninstall。

还可以用make clean来清除编译出来的文件。

 

Posted by 陈着 Aug 15, 2009 04:38:28 AM