•  

    我们学校的计算机学院从去年起开始组织学生参加世界上最具权威性的大学生程序设计竞赛——ACM/ICPC。从这学期开始,学院计划有组织地进行训练和讲座,以帮助大家在有限的时间内尽可能多地提高自己的能力,这对有兴趣投入数据结构与算法研究的同学来说无疑是一件好事。但是,刚刚接触信息学领域的同学往往存在很多困惑,不知道从何入手学习,在这篇文章里,我希望能将自己不多的经验与大家分享,希望对各位有所帮助。

    一、语言是最重要的基本功

        无论侧重于什么方面,只要是通过计算机程序去最终实现的竞赛,语言都是大家要过的第一道关。亚洲赛区的比赛支持的语言包括C/C++JAVA。笔者首先说说JAVA,众所周知,作为面向对象的王牌语言,JAVA在大型工程的组织与安全性方面有着自己独特的优势,但是对于信息学比赛的具体场合,JAVA则显得不那么合适,它对于输入输出流的操作相比于C++要繁杂很多,更为重要的是JAVA程序的运行速度要比C++10倍以上,而竞赛中对于JAVA程序的运行时限却往往得不到同等比例的放宽,这无疑对算法设计提出了更高的要求,是相当不利的。其实,笔者并不主张大家在这种场合过多地运用面向对象的程序设计思维,因为对于小程序来说这不旦需要花费更多的时间去编写代码,也会降低程序的执行效率。

        接着说CC++。许多现在参加讲座的同学还在上大一,C的基础知识刚刚学完,还没有接触过C++,其实在赛场上使用纯C的选手还是大有人在的,它们主要是看重了纯C在效率上的优势,所以这部分同学如果时间有限,并不需要急着去学习新的语言,只要提高了自己在算法设计上的造诣,纯C一样能发挥巨大的威力。

        C++相对于C,在输入输出流上的封装大大方便了我们的操作,同时降低了出错的可能性,并且能够很好地实现标准流与文件流的切换,方便了调试的工作。如果有些同学比较在意这点,可以尝试CC++的混编,毕竟仅仅学习C++的流操作还是不花什么时间的。

        C++的另一个支持来源于标准模版库(STL),库中提供的对于基本数据结构的统一接口操作和基本算法的实现可以缩减我们编写代码的长度,这可以节省一些时间。但是,与此相对的,使用STL要在效率上做出一些牺牲,对于输入规模很大的题目,有时候必须放弃STL,这意味着我们不能存在“有了STL就可以不去管基本算法的实现”的想法;另外,熟练和恰当地使用STL必须经过一定时间的积累,准确地了解各种操作的时间复杂度,切忌对STL中不熟悉的部分滥用,因为这其中蕴涵着许多初学者不易发现的陷阱。

        通过以上的分析,我们可以看出仅就信息学竞赛而言,对语言的掌握并不要求十分全面,但是对于经常用到的部分,必须十分熟练,不允许有半点不清楚的地方,下面我举个真实的例子来说明这个道理——即使是一点很细微的语言障碍,都有可能酿成错误:

        在去年清华的赛区上,有一个队在做F题的时候使用了coutprintf的混合输出,由于一个带缓冲一个不带,所以输出一长就混乱了。只是因为当时judge team中负责F题的人眼睛尖,看出答案没错只是顺序不对(答案有一页多,是所有题目中最长的一个输出),又看了看程序发现只是输出问题就给了个Presentation error(格式错)。如果审题的人不是这样而是直接给一个 Wrong Answer,相信这个队是很难查到自己错在什么地方的。

        现在我们转入第二个方面的讨论,基础学科知识的积累。

    二、以数学为主的基础知识十分重要

        虽然被定性为程序设计竞赛,但是参赛选手所遇到的问题更多的是没有解决问题的思路,而不是有了思路却死活不能实现,这就是平时积累的基础知识不够。今年World Final的总冠军是波兰华沙大学,其成员出自于数学系而非计算机系,这就是一个鲜活的例子。竞赛中对于基础学科的涉及主要集中于数学,此外对于物理、电路等等也可能有一定应用,但是不多。因此,大一的同学也不必为自己还没学数据结构而感到不知从何入手提高,把数学捡起来吧!下面我来谈谈在竞赛中应用的数学的主要分支。

        1、离散数学——作为计算机学科的基础,离散数学是竞赛中涉及最多的数学分支,其重中之重又在于图论和组合数学,尤其是图论。

        图论之所以运用最多是因为它的变化最多,而且可以轻易地结合基本数据结构和许多算法的基本思想,较多用到的知识包括连通性判断、DFSBFS,关节点和关键路径、欧拉回路、最小生成树、最短路径、二部图匹配和网络流等等。虽然这部分的比重很大,但是往往也是竞赛中的难题所在,如果有初学者对于这部分的某些具体内容暂时感到力不从心,也不必着急,可以慢慢积累。

        竞赛中设计的组合计数问题大都需要用组合数学来解决,组合数学中的知识相比于图论要简单一些,很多知识对于小学上过奥校的同学来说已经十分熟悉,但是也有一些部分需要先对代数结构中的群论有初步了解才能进行学习。组合数学在竞赛中很少以难题的形式出现,但是如果积累不够,任何一道这方面的题目却都有可能成为难题。

        2、数论——以素数判断和同余为模型构造出来的题目往往需要较多的数论知识来解决,这部分在竞赛中的比重并不大,但只要来上一道,也足以使知识不足的人冥思苦想上一阵时间。素数判断和同余最常见的是在以密码学为背景的题目中出现,在运用密码学常识确定大概的过程之后,核心算法往往要涉及数论的内容。

        3、计算几何——计算几何相比于其它部分来说是比较独立的,就是说它和其它的知识点很少有过多的结合,较常用到的部分包括——线段相交的判断、多边形面积的计算、内点外点的判断、凸包等等。计算几何的题目难度不会很大,但也永远不会成为最弱的题。

        4、线性代数——对线性代数的应用都是围绕矩阵展开的,一些表面上是模拟的题目往往可以借助于矩阵来找到更好的算法。

        5、概率论——竞赛是以黑箱来判卷的,这就是说你几乎不能动使用概率算法的念头,但这也并不是说概率就没有用。关于这一点,只有通过一定的练习才能体会。

        6、初等数学与解析几何——这主要就是中学的知识了,用的不多,但是至少比高等数学多,我觉得熟悉一下数学手册上的相关内容,至少要知道在哪儿能查到,还是必要的。

        7、高等数学——纯粹运用高等数学来解决的题目我接触的只有一道,但是一些题目的叙述背景往往需要和这部分有一定联系,掌握得牢固一些总归没有坏处。

        以上就是竞赛所涉及的数学领域,可以说范围是相当广的。我认识的许多人去搞信息学的竞赛就是为了逼着自己多学一点数学,因为数学是一切一切的基础。

    三、数据结构与算法是真正的核心

        虽然数学十分十分重要,但是如果让三个只会数学的人参加比赛,我相信多数情况下会比三个只会数据结构与算法的人得到更为悲惨的结局。

        先说说数据结构。掌握队列、堆栈和图的基本表达与操作是必需的,至于树,我个人觉得需要建树的问题有但是并不多。(但是树往往是很重要的分析工具)除此之外,排序和查找并不需要对所有方式都能很熟练的掌握,但你必须保证自己对于各种情况都有一个在时间复杂度上满足最低要求的解决方案。说到时间复杂度,就又该说说哈希表了,竞赛时对时间的限制远远多于对空间的限制,这要求大家尽快掌握“以空间换时间”的原则策略,能用哈希表来存储的数据一定不要到时候再去查找,如果实在不能建哈希表,再看看能否建二叉查找树等等——这都是争取时间的策略,掌握这些技巧需要大家对数据结构尤其是算法复杂度有比较全面的理性和感性认识。

        接着说说算法。算法中最基本和常用的是搜索,主要是回溯和分支限界法的使用。这里要说的是,有些初学者在学习这些搜索基本算法是不太注意剪枝,这是十分不可取的,因为所有搜索的题目给你的测试用例都不会有很大的规模,你往往察觉不出程序运行的时间问题,但是真正的测试数据一定能过滤出那些没有剪枝的算法。实际上参赛选手基本上都会使用常用的搜索算法,题目的区分度往往就是建立在诸如剪枝之类的优化上了。

        常用算法中的另一类是以“相似或相同子问题”为核心的,包括递推、递归、贪心法和动态规划。这其中比较难于掌握的就是动态规划,如何抽象出重复的子问题是很多题目的难点所在,笔者建议初学者仔细理解图论中一些以动态规划为基本思想所建立起来的基本算法(比如Floyd-Warshall算法),并且多阅读一些定理的证明,这虽然不能有什么直接的帮助,但是长期坚持就会对思维很有帮助。

    四、团队配合

        通过以上的介绍大家也可以看出,信息学竞赛对于知识面覆盖的非常广,想凭一己之力全部消化这些东西实在是相当困难的,这就要求我们尽可能地发挥团队协作的精神。同组成员之间的熟练配合和默契的形成需要时间,具体的情况因成员的组成不同而不同,这里我就不再多说了。

    五、练习、练习、再练习

        知识的积累固然重要,但是信息学终究不是看出来的,而是练出来的,这是多少前人最深的一点体会,只有通过具体题目的分析和实践,才能真正掌握数学的使用和算法的应用,并在不断的练习中增加编程经验和技巧,提高对时间复杂度的感性认识,优化时间的分配,加强团队的配合。总之,在这里光有纸上谈兵是绝对不行的,必须要通过实战来锻炼自己。

        大家一定要问,我们去哪里找题做,又如何检验程序是否正确呢?这大可不必担心,现在已经有了很多网上做题的站点,这些站点提供了大量的题库并支持在线判卷,你只需要把程序源码提交上去,马上就可以知道自己的程序是否正确,运行所使用的时间以及消耗的内存等等状况。下面我给大家推荐几个站点,笔者不建议大家在所有这些站点上做题,选择一个就可以了,因为每个站点的题都有一定的难易比例,系统地做一套题库可以使你对各种难度、各种类型的题都有所认识。

        1Ural

        Ural是中国学生对俄罗斯的Ural州立大学的简称 ,那里设立了一个Ural Online Problem Set,并且支持Online JudgeUral的不少题目算法性和趣闻性都很强,得到了国内广大学生的厚爱。根据“信息学初学者之家”网站的统计,Ural的题目类型大概呈如下的分布:

     

    题型

    搜索

    动态规划

    贪心

    构造

    图论

    计算几何

    纯数学问题

    数据结构

    其它

    所占比例

    10%

    15%

    5%

    5%

    10%

    5%

    20%

    5%

    25%

        这和实际比赛中的题型分布也是大体相当的。有兴趣的朋友可以去看看。

        2UVA

        UVA代表西班牙Valladolid大学(University de Valladolid)。该大学有一个那里设立了一个PROBLEM SET ARCHIVE with ONLINE JUDGE ,并且支持ONLINE JUDGE,形式和Ural大学的题库类似。不过和Ural不同的是,UVA题目多的多,而且比较杂,而且有些题目的测试数据比较刁钻。这使得刚到那里做题的朋友往往感觉到无所适从,要么难以找到合适的题目,要么Wrong Answer了很多次以后仍然不知道错在那里。 如果说做Ural题目主要是为了训练算法,那么UVA题目可以训练全方位的基本功和一些必要的编程素质。UVA和许多世界知名大学联合办有同步网上比赛,因此那里强人无数,不过你先要使自己具有听懂他们在说什么的素质:)

        3ZOJ

        ZOJ是浙江大学建立的ONLINE JUDGE,是中国大学建立的第一个同类站点,也是最好和人气最高的一个,笔者和许多班里的同学就是在这里练习。ZOJ虽然也定位为一个英文网站,但是这里的中国学生比较多,因此让人觉得很亲切。这里目前有500多道题目,难易分配适中,且涵盖了各大洲的题目类型并配有索引,除此之外,ZOJJUDGE系统是几个网站中表现得比较好的一个,很少出现Wrong AnswerPresentation error混淆的情况。这里每月也办有一次网上比赛,只要是注册的用户都可以参加。

            说起中国的ONLINE JUDGE,去年才开始参加ACM竞赛的北京大学现在也建立了自己的提交系统;而我们学校也是去年开始参加比赛,现在也有可能推出自己的提交系统,如果能够做成,到时候大家就可以去上面做题了。同类网站的飞速发展标志着有越来越多的同学有兴趣进入信息学的领域探索,这是一件好事,同时也意味着更激烈的竞争,希望大家都能通过竞争锻炼自己、提高自己,并争取成为胜利者。

  • 转自 火狐技术联盟

    网络最经典命令行-网络安全工作者的必杀技
    作者:未知     来源:未知


      1.最基本,最常用的,测试物理网络的

      ping 192.168.0.8 -t ,参数-t是等待用户去中断测试

      2.查看DNS、IP、Mac等

      A.Win98:winipcfg

      B.Win2000以上:Ipconfig/all

      C.NSLOOKUP:如查看河北的DNS

      C:\>nslookup

      Default Server: ns.hesjptt.net.cn

      Address: 202.99.160.68

      >server 202.99.41.2 则将DNS改为了41.2

      > pop.pcpop.com

      Server: ns.hesjptt.net.cn

      Address: 202.99.160.68

      Non-authoritative answer:

      Name: pop.pcpop.com

      Address: 202.99.160.212

       3.网络信使

      Net send 计算机名/IP * (广播) 传送内容,注意不能跨网段

      net stop messenger 停止信使服务,也可以在面板-服务修改

      net start messenger 开始信使服务

       4.探测对方对方计算机名,所在的组、域及当前用户名 (追捕的工作原理)

      ping -a IP -t ,只显示NetBios名

      nbtstat -a 192.168.10.146 比较全的

       5.netstat -a 显示出你的计算机当前所开放的所有端口

      netstat -s -e 比较详细的显示你的网络资料,包括TCP、UDP、ICMP 和 IP的统计等

       6.探测arp绑定(动态和静态)列表,显示所有连接了我的计算机,显示对方IP和MAC地址

      arp -a

       7.在代理服务器端

      捆绑IP和MAC地址,解决局域网内盗用IP!:

      ARP -s 192.168.10.59 00 -50-ff-6c-08-75

      解除网卡的IP与MAC地址的绑定:

      arp -d 网卡IP

       8.在网络邻居上隐藏你的计算机

      net config server /hidden:yes

      net config server /hidden:no 则为开启

       9.几个net命令

      A.显示当前工作组服务器列表 net view,当不带选项使用本命令时,它就会显示当前域或网络上的计算机上的列表。

      比如:查看这个IP上的共享资源,就可以

      C:\>net view 192.168.10.8

      在 192.168.10.8 的共享资源

      资源共享名 类型 用途 注释

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

      网站服务 Disk

      命令成功完成。

      B.查看计算机上的用户帐号列表 net user

      C.查看网络链接 net use

      例如:net use z: \192.168.10.8\movie 将这个IP的movie共享目录映射为本地的Z盘

      D.记录链接 net session

      例如: C:\>net session

      计算机 用户名 客户类型 打开空闲时间

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

      \192.168.10.110 ROME Windows 2000 2195 0 00:03:12

      \192.168.10.51 ROME Windows 2000 2195 0 00:00:39

      命令成功完成。

       10.路由跟踪命令

      A.tracert pop.pcpop.com

      B.pathping pop.pcpop.com 除了显示路由外,还提供325S的分析,计算丢失包的%

       11.关于共享安全的几个命令

      A.查看你机器的共享资源 net share

      B.手工删除共享(可以编个bat文件,开机自运行,把共享都删了!)

      net share c$ /d

      net share d$ /d

      net share ipc$ /d

      net share admin$ /d

      注意$后有空格。

      C.增加一个共享:

      c:\net share mymovie=e:\downloads\movie /users:1

      mymovie 共享成功。

      同时限制链接用户数为1人。

       12.在DOS行下设置静态IP

      A.设置静态IP

      CMD

      netsh

      netsh>int

      interface>ip

      interface ip>set add "本地链接" static IP地址 mask gateway

      B.查看IP设置

      interface ip>show address

      Arp

      显示和修改“地址解析协议 (ARP)”缓存中的项目。ARP 缓存中包含一个或多个表,它们用于存储 IP 地址及其经过解析的以太网或令牌环物理地址。计算机上安装的每一个以太网或令牌环网络适配器都有自己单独的表。如果在没有参数的情况下使用,则 arp 命令将显示帮助信息。

      语法

      arp [-a [InetAddr] [-N IfaceAddr]] [-g [InetAddr] [-N IfaceAddr]] [-d InetAddr [IfaceAddr]] [-s InetAddr EtherAddr [IfaceAddr]]

      参数

      -a [InetAddr] [-N IfaceAddr]

      显示所有接口的当前 ARP 缓存表。要显示指定 IP 地址的 ARP 缓存项,请使用带有 InetAddr 参数的 arp -a,此处的 InetAddr 代表指定的 IP 地址。要显示指定接口的 ARP 缓存表,请使用 -N IfaceAddr 参数,此处的 IfaceAddr 代表分配给指定接口的 IP 地址。-N 参数区分大小写。

      -g [InetAddr] [-N IfaceAddr]

      与 -a 相同。

      -d InetAddr [IfaceAddr]

      删除指定的 IP 地址项,此处的 InetAddr 代表 IP 地址。对于指定的接口,要删除表中的某项,请使用 IfaceAddr 参数,此处的 IfaceAddr 代表分配给该接口的 IP 地址。要删除所有项,请使用星号 (*) 通配符代替 InetAddr。

      -s InetAddr EtherAddr [IfaceAddr]

      向 ARP 缓存添加可将 IP 地址 InetAddr 解析成物理地址 EtherAddr 的静态项。要向指定接口的表添加静态 ARP 缓存项,请使用 IfaceAddr 参数,此处的 IfaceAddr 代表分配给该接口的 IP 地址。

       在命令提示符显示帮助。

      注释

      InetAddr 和 IfaceAddr 的 IP 地址用带圆点的十进制记数法表示。

      物理地址 EtherAddr 由六个字节组成,这些字节用十六进制记数法表示并且用连字符隔开(比如,00-AA-00-4F-2 A-9C)。

      通过 -s 参数添加的项属于静态项,它们不会 ARP 缓存中超时。如果终止 TCP/IP 协议后再启动,这些项会被删除。要创建永久的静态 ARP 缓存项,请在批处理文件中使用适当的 arp 命令并通过“计划任务程序”在启动时运行该批处理文件。

      只有当网际协议 (TCP/IP) 协议在 网络连接中安装为网络适配器属性的组件时,该命令才可用。

      范例

      要显示所有接口的 ARP 缓存表,可键入:

      arp -a

      对于指派的 IP 地址为 10.0.0.99 的接口,要显示其 ARP 缓存表,可键入:

      arp -a -N 10.0.0.99

      要添加将 IP 地址 10.0.0.80 解析成物理地址 00-AA-00-4F-2 A-9C 的静态 ARP 缓存项,可键入:

      arp -s 10.0.0.80 00-AA-00-4F-2 A-9C

      At

      计划在指定时间和日期在计算机上运行命令和程序。at 命令只能在“计划”服务运行时使用。如果在没有参数的情况下使用,则 at 列出已计划的命令。

      语法

      at [\ComputerName] [{[ID] [/delete] /delete [/yes]}]

      at [[\ComputerName] hours:minutes [/interactive] [{/every:date[,...] /next:date[,...]}] command]

      参数

       \computername

      指定远程计算机。如果省略该参数,则 at 计划本地计算机上的命令和程序。

      ID

      指定指派给已计划命令的识别码。

      /delete

      取消已计划的命令。如果省略了 ID,则计算机中所有已计划的命令将被取消。

      /yes

      删除已计划的事件时,对来自系统的所有询问都回答“是”。

      hours:minutes

      指定命令运行的时间。该时间用 24 小时制(即从 00:00 [午夜] 到 23:59)的 小时: 分钟格式表示。

      /interactive

      对于在运行 command 时登录的用户,允许 command 与该用户的桌面进行交互。

      /every:

      在每个星期或月的指定日期(例如,每个星期四,或每月的第三天)运行 command 命令。

      date

      指定运行命令的日期。可以指定一周的某日或多日(即,键入 M、T、W、Th、F、S、Su)或一个月中的某日或多日(即,键入从 1 到31 之间的数字)。用逗号分隔多个日期项。如果省略了 date,则 at 使用该月的当前日。

      /next:

      在下一个指定日期(比如,下一个星期四)到来时运行 command。

      command

      指定要运行的 Windows 命令、程序(.exe 或 .com 文件)或批处理程序(.bat 或 .cmd 文件)。当命令需要路径作为参数时,请使用绝对路径,也就是从驱动器号开始的整个路径。如果命令在远程计算机上,请指定服务器和共享名的通用命名协定 (UNC) 符号,而不是远程驱动器号。

      /?

      在命令提示符显示帮助。

      注释

      Schtasks 是功能更为强大的超集命令行计划工具,它含有 at 命令行工具中的所有功能。对于所有的命令行计划任务,都可以使用 schtasks 来替代 at。有关 schtasks 的详细信息,请参阅“相关主题”。

      使用 at

      使用 at 命令时,要求您必须是本地 Administrators 组的成员。

  •  

    在DOS下的Microsoft Network Client和Windows 9x的DOS窗口等环境中,有许多很有用的但不包含在DOS自带的命令中的网络命令。那么,有哪些这样的命令呢?下面将它们中常用的命令介绍一下。

    Arp

    显示和修改“地址解析协议”(ARP) 所使用的到以太网的 IP 或令牌环物理地址翻译表。该命令只有在安装了 TCP/IP 协议之后才可用。
    arp -a [inet_addr] [-N [if_addr]]
    arp -d inet_addr [if_addr]
    arp -s inet_addr ether_addr [if_addr]
    参数
    -a
    通过询问 TCP/IP 显示当前 ARP 项。如果指定了 inet_addr,则只显示指定计算机的 IP 和物理地址。
    -g
    与 -a 相同。
    inet_addr
    以加点的十进制标记指定 IP 地址。
    -N
    显示由 if_addr 指定的网络界面 ARP 项。
    if_addr
    指定需要修改其地址转换表接口的 IP 地址(如果有的话)。如果不存在,将使用第一个可适用的接口。
    -d
    删除由 inet_addr 指定的项。
    -s
    在 ARP 缓存中添加项,将 IP 地址 inet_addr 和物理地址 ether_addr 关联。物理地址由以连字符分隔的 6 个十六进制字节给定。使用带点的十进制标记指定 IP 地址。项是永久性的,即在超时到期后项自动从缓存删除。
    ether_addr
    指定物理地址。

    Finger

    在运行 Finger 服务的指定系统上显示有关用户的信息。根据远程系统输出不同的变量。该命令只有在安装了 TCP/IP 协议之后才可用。
    finger [-l] [user]@computer[...]
    参数
    -l
    以长列表格式显示信息。
    user
    指定要获得相关信息的用户。省略用户参数以显示指定计算机上所有用户的信息:
    @computer

    Ftp

    将文件传送到正在运行 FTP 服务的远程计算机或从正在运行 FTP 服务的远程计算机传送文件(有时称作 daemon)。Ftp 可以交互使用。单击“相关主题”列表中的“ftp 命令”以获得可用的“ftp”子命令描述。该命令只有在安装了 TCP/IP 协议之后才可用。Ftp 是一种服务,一旦启动,将创建在其中可以使用 ftp 命令的子环境,通过键入 quit 子命令可以从子环境返回到 Windows 2000 命令提示符。
         当 ftp 子环境运行时,它由 ftp 命令提示符代表。
    ftp [-v] [-n] [-i] [-d] [-g] [-s:filename] [-a] [-w:windowsize] [computer]
    参数
    -v
    禁止显示远程服务器响应。
    -n
    禁止自动登录到初始连接。
    -i
    多个文件传送时关闭交互提示。
    -d
    启用调试、显示在客户端和服务器之间传递的所有 ftp 命令。
    -g
    禁用文件名组,它允许在本地文件和路径名中使用通配符字符(* 和 ?)。(请参阅联机“命令参考”中的 glob 命令。)
    -s: filename
    指定包含 ftp 命令的文本文件;当 ftp 启动后,这些命令将自动运行。该参数中不允许有空格。使用该开关而不是重定向 (> )。
    -a
    在捆绑数据连接时使用任何本地接口。
    -w:windowsize
    替代默认大小为 4096 的传送缓冲区。
    computer
    指定要连接到远程计算机的计算机名或 IP 地址。如果指定,计算机必须是行的最后一个参数。

    Nbtstat

    该诊断命令使用 NBT(TCP/IP 上的 NetBIOS)显示协议统计和当前 TCP/IP 连接。该命令只有在安装了 TCP/IP 协议之后才可用。
    nbtstat [-a remotename] [-A IP address] [-c] [-n] [-R] [-r] [-S] [-s] [interval]
    参数
    -a remotename
    使用远程计算机的名称列出其名称表。
    -A IP address
    使用远程计算机的 IP 地址并列出名称表。
    -c
    给定每个名称的 IP 地址并列出 NetBIOS 名称缓存的内容。
    -n
    列出本地 NetBIOS 名称。“已注册”表明该名称已被广播 (Bnode) 或者 WINS(其他节点类型)注册。
    -R
    清除 NetBIOS 名称缓存中的所有名称后,重新装入 Lmhosts 文件。
    -r
    列出 Windows 网络名称解析的名称解析统计。在配置使用 WINS 的 Windows 2000 计算机上,此选项返回要通过广播或 WINS 来解析和注册的名称数。
    -S
    显示客户端和服务器会话,只通过 IP 地址列出远程计算机。
    -s
    显示客户端和服务器会话。尝试将远程计算机 IP 地址转换成使用主机文件的名称。
    interval
    重新显示选中的统计,在每个显示之间暂停 interval 秒。按 CTRL+C 停止重新显示统计信息。如果省略该参数,nbtstat 打印一次当前的配置信息。

    Netstat

    显示协议统计和当前的 TCP/IP 网络连接。该命令只有在安装了 TCP/IP 协议后才可以使用。
    netstat [-a] [-e] [-n] [-s] [-p protocol] [-r] [interval]
    参数
    -a
    显示所有连接和侦听端口。服务器连接通常不显示。
    -e
    显示以太网统计。该参数可以与 -s 选项结合使用。
    -n
    以数字格式显示地址和端口号(而不是尝试查找名称)。
    -s
    显示每个协议的统计。默认情况下,显示 TCP、UDP、ICMP 和 IP 的统计。-p 选项可以用来指定默认的子集。
    -p protocol
    显示由 protocol 指定的协议的连接;protocol 可以是 tcp 或 udp。如果与 -s 选项一同使用显示每个协议的统计,protocol 可以是 tcp、udp、icmp 或 ip。
    -r
    显示路由表的内容。
    interval
    重新显示所选的统计,在每次显示之间暂停 interval 秒。按 CTRL+B 停止重新显示统计。如果省略该参数,netstat 将打印一次当前的配置信息。

    Ping

    验证与远程计算机的连接。该命令只有在安装了 TCP/IP 协议后才可以使用。
    ping [-t] [-a] [-n count] [-l length] [-f] [-i ttl] [-v tos] [-r count] [-s count] [[-j computer-list] | [-k computer-list]] [-w timeout] destination-list
    参数
    -t
    Ping 指定的计算机直到中断。
    -a
    将地址解析为计算机名。
    -n count
    发送 count 指定的 ECHO 数据包数。默认值为 4。
    -l length
    发送包含由 length 指定的数据量的 ECHO 数据包。默认为 32 字节;最大值是 65,527。
    -f
    在数据包中发送“不要分段”标志。数据包就不会被路由上的网关分段。
    -i ttl
    将“生存时间”字段设置为 ttl 指定的值。
    -v tos
    将“服务类型”字段设置为 tos 指定的值。
    -r count
    在“记录路由”字段中记录传出和返回数据包的路由。count 可以指定最少 1 台,最多 台计算机。
    指定要 ping 的远程计算机。

    Rcp

    在 Windows 2000 计算机和运行远程外壳端口监控程序 rshd 的系统之间复制文件。rcp 命令是一个连接命令,从 Windows 2000 计算机发出该命令时,也可以用于其他传输在两台运行 rshd 的计算机之间复制文件。rshd 端口监控程序可以在 UNIX 计算机上使用,而在 Windows 2000 上不能使用,所以 Windows 2000 计算机仅可以作为发出命令的系统参与。远程计算机必须也通过运行 rshd 提供 rcp 实用程序。
    rcp [-a | -b] [-h] [-r] source1 source2 ... sourceN destination
    参数
    -a
    指定 ASCII 传输模式。此模式在传出文件上将回车/换行符转换为回车符,在传入文件中将换行符转换为回车/换行符。该模式为默认的传输模式。
    -b
    指定二进制图像传输模式。没有执行回车/换行符转换。
    -h
    传输 Windows 2000 计算机上标记为隐藏属性的源文件。如果没有该选项,在 rcp 命令行上指定隐藏文件的效果与文件不存在一样。
    -r
    将源的所有子目录内容递归复制到目标。source 和 destination 都必须是目录,虽然即使源不是目录,使用 -r 也能够工作。但将没有递归。
    source 和 destination
    格式必须为 [computer[.user]:]filename。如果忽略了 [computer[.user]:] 部分,计算机将假定为本地计算机。如果省略了 [.user] 部分,将使用当前登录的 Windows 2000 用户名。如果使用了完全合格的计算机名,其中包含句点 (.) 分隔符,则必须包含 [.user]。否则,计算机名的最后部分将解释为用户名。如果指定了多个源文件,则 destination 必须是目录。
    如果文件名不是以 UNIX 的正斜杠 (/) 或 Windows 2000 系统的反斜杠 (\) 打头,则假定相对于当前的工作目录。在 Windows 2000 中,这是发出命令的目录。在远程系统中,这是远程用户的登录目录。句点 (.) 表示当前的目录。在远程路径中使用转义字符(\、" 或 '),以便在远程计算机中使用通配符。

    Rexec

    在运行 REXEC 服务的远程计算机上运行命令。rexec 命令在执行指定命令前,验证远程计算机上的用户名,只有安装了 TCP/IP 协议后才可以使用该命令。
    rexec computer [-l username] [-n] command
    参数
    computer
    指定要运行 command 的远程计算机。
    -l username
    指定远程计算机上的用户名。
    -r
    将 rexec 的输入重定向到 NULL。
    command
    指定要运行的命令。

    Route

    控制网络路由表。该命令只有在安装了 TCP/IP 协议后才可以使用。
    route [-f] [-p] [command [destination] [mask subnetmask] [gateway] [metric costmetric]]
    参数
    -f
    清除所有网关入口的路由表。如果该参数与某个命令组合使用,路由表将在运行命令前清除。
    -p
    该参数与 add 命令一起使用时,将使路由在系统引导程序之间持久存在。默认情况下,系统重新启动时不保留路由。与 print 命令一起使用时,显示已注册的持久路由列表。忽略其他所有总是影响相应持久路由的命令。
    command
    指定下列的一个命令。
    命令 目的
    print 打印路由
    add 添加路由
    delete 删除路由
    change 更改现存路由
    destination
    指定发送 command 的计算机。
    mask subnetmask
    指定与该路由条目关联的子网掩码。如果没有指定,将使用 255.255.255.255。
    gateway
    指定网关。
    名为 Networks 的网络数据库文件和名为 Hosts 的计算机名数据库文件中均引用全部 destination 或 gateway 使用的符号名称。如果命令是 print 或 delete,目标和网关还可以使用通配符,也可以省略网关参数。
    metric costmetric
    指派整数跃点数(从 1 到 9999)在计算最快速、最可靠和(或)最便宜的路由时使用。
    Rsh
    在运行 RSH 服务的远程计算机上运行命令。该命令只有在安装了 TCP/IP 协议后才可以使用。
    rsh computer [-l username] [-n] command
    参数
    computer
    指定运行 command 的远程计算机。
    -l username
    指定远程计算机上使用的用户名。如果省略,则使用登录的用户名。
    -n
    将 rsh 的输入重定向到 NULL。
    command
    指定要运行的命令。

    Tftp

    将文件传输到正在运行 TFTP 服务的远程计算机或从正在运行 TFTP 服务的远程计算机传输文件。该命令只有在安装了 TCP/IP 协议后才可以使用。
    tftp [-i] computer [get | put] source [destination]
    参数
    -i
    指定二进制图像传送模式(也称为“八位字节”)。在二进制图像模式中,文件一个字节接一个字节地逐字移动。在传送二进制文件时使用该模式。
    如果省略了 -i,文件将以 ASCII 模式传送。这是默认的传送模式。此模式将 EOL 字符转换为 UNIX 的回车符和个人计算机的回车符/换行符。在传送文本文件时应使用此模式。如果文件传送成功,将显示数据传输率。
    computer
    指定本地或远程计算机。
    put
    将本地计算机上的文件 destination 传送到远程计算机上的文件 source。
    get
    将远程计算机上的文件 destination 传送到本地计算机上的文件 source。
    如果将本地计算机上的文件 file-two 传送到远程计算机上的文件 file-one,请指定 put。如果将远程计算机上的文件 file-two 传送到远程计算机上的文件 file-one,请指定 get。
    因为 tftp 协议不支持用户身份验证,所以用户必须登录,并且文件在远程计算机上必须可以写入。
    source
    指定要传送的文件。如果本地文件指定为 -,则远程文件在 stdout 上打印出来(如果获取),或从 stdin(如果放置)读取。
    destination
    指定将文件传送到的位置。如果省略了 destination,将假定与 source 同名。

    Tracert

    该诊断实用程序将包含不同生存时间 (TTL) 值的 Internet 控制消息协议 (ICMP) 回显数据包发送到目标,以决定到达目标采用的路由。要在转发数据包上的 TTL 之前至少递减 1,必需路径上的每个路由器,所以 TTL 是有效的跃点计数。数据包上的 TTL 到达 0 时,路由器应该将“ICMP 已超时”的消息发送回源系统。Tracert 先发送 TTL 为 1 的回显数据包,并在随后的每次发送过程将 TTL 递增 1,直到目标响应或 TTL 达到最大值,从而确定路由。路由通过检查中级路由器发送回的“ICMP 已超时”的消息来确定路由。不过,有些路由器悄悄地下传包含过期 TTL 值的数据包,而 tracert 看不到。
    tracert [-d] [-h maximum_hops] [-j computer-list] [-w timeout] target_name
    参数
    /d
    指定不将地址解析为计算机名。
    -h maximum_hops
    指定搜索目标的最大跃点数。
    -j computer-list
    指定沿 computer-list 的稀疏源路由。
    -w timeout
    每次应答等待 timeout 指定的微秒数。
    target_name
    目标计算机的名称。

    此外,还有IPCONFIG,DNR等命令,这里就不再一一介绍了。

  •  

    XP有一个很无敌的命令,用来替换文件的replace,连正在使用的文件也能替换。非常无敌。 比如:在C:下建一个目录,c:aaa ,然后复制一首mp3到c:aaa并命名为c:aaaa.mp3 ,然后再复制另一首歌到C:a.mp3 ,然后用media player 播放c:aaaa.mp3 ,在命令提示符下输入:replace c:a.mp3 c:aaa ,过一会,是不是播放的歌已变为另一首。

    用这个命令来替换系统文件真是太爽了,并且XP的系统文件保护也对它无效。 再也不用到安全模式下去替换文件了

    格式

    REPLACE [drive1:][path1]filename [drive2:][path2] [/A] [/P] [/R] [/W]

    REPLACE [drive1:][path1]filename [drive2:][path2] [/P] [/R] [/S] [/W]

    [drive1:][path1]filename 指定源文件。

    [drive2:][path2] 指定要替换文件的

    目录

    /A 把新文件加入目标目录。不能和/S 或 /U 命令行开关搭配使用。 /P 替换文件或加入源文件之前会先提示您进行确认。 /R 替换只读文件以及未受保护的文件。 /S 替换目标目录中所有子目录的文件。 不能与 /A 命令选项搭配使用。

    /W 等您插入磁盘以后再运行。

    /U 只会替换或更新比源文件日期早的文件。 不能与 /A 命令行开关搭配使用。

    ps:要是病毒或者黑客也用这个命令那就危险了


  •  

    方法2。重载TControl的WndProc方法
      还是先谈谈VCL的继承策略。VCL中的继承链的顶部是TObject基类。一切的VCL组件和对象都继承自TObject。

      打开BCB帮助查看TControl的继承关系:

      TObject->TPersistent->TComponent->TControl

      呵呵,原来TControl是从TPersistent类的子类TComponent类继承而来的。TPersistent抽象基类具有使用流stream来存取类的属性的能力。

      TComponent类则是所有VCL组件的父类。

      这就是所有的VCL组件包括您的自定义组件可以使用dfm文件存取属性的原因『当然要是TPersistent的子类,我想您很少需要直接从TObject类来派生您的自定义组件吧』。 TControl类的重要性并不亚于它的父类们。在BCB的继承关系中,TControl类的是所有VCL可视化组件的父类。实际上就是控件的意思吧。所谓可视化是指您可以在运行期间看到和操纵的控件。这类控件所具有的一些基本属性和方法都在TControl类中进行定义。

      TControl的实现在\Borland\CBuilder5\Source\Vcl\control.pas中可以找到。『可能会有朋友问你怎么知道在那里?使用BCB提供的Search -> Find in files很容易找到。或者使用第三方插件的grep功能。』

      好了,进入VCL的源码吧。说到这里免不了要抱怨一下Borland。哎,为什么要用pascal实现这一切.....:-(

      TControl继承但并没有重写TObject的Dispatch()方法。反而提供了一个新的方法就是xycleo提到的WndProc()。一起来看看Borland的工程师们是怎么写的吧。

    procedure TControl.WndProc(var Message: TMessage);
    var
    Form: TCustomForm;
    begin
    //由拥有control的窗体来处理设计期间的消息
    if (csDesigning in ComponentState) then
    begin
    Form := GetParentForm(Self);
    if (Form <> nil) and (Form.Designer <> nil) and
    Form.Designer.IsDesignMsg(Self, Message) then Exit;
    end
    //如果需要,键盘消息交由拥有control的窗体来处理
    else if (Message.Msg >= WM_KEYFIRST) and (Message.Msg <= WM_KEYLAST) then
    begin
    Form := GetParentForm(Self);
    if (Form <> nil) and Form.WantChildKey(Self, Message) then Exit;
    end
    //处理鼠标消息
    else if (Message.Msg >= WM_MOUSEFIRST) and (Message.Msg <= WM_MOUSELAST) then
    begin
    if not (csDoubleClicks in ControlStyle) then
    case Message.Msg of
    WM_LBUTTONDBLCLK, WM_RBUTTONDBLCLK, WM_MBUTTONDBLCLK:
    Dec(Message.Msg, WM_LBUTTONDBLCLK - WM_LBUTTONDOWN);
    end;
    case Message.Msg of
    WM_MOUSEMOVE: Application.HintMouseMessage(Self, Message);
    WM_LBUTTONDOWN, WM_LBUTTONDBLCLK:
    begin
    if FDragMode = dmAutomatic then
    begin
    BeginAutoDrag;
    Exit;
    end;
    Include(FControlState, csLButtonDown);
    end;
    WM_LBUTTONUP:
    Exclude(FControlState, csLButtonDown);
    end;
    end
    // 下面一行有点特别。如果您仔细的话会看到这个消息是CM_VISIBLECHANGED.
    // 而不是我们熟悉的WM_开头的标准Windows消息.
    // 尽管Borland没有在它的帮助中提到有这一类的CM消息存在。但很显然这是BCB的
    // 自定义消息。呵呵,如果您对此有兴趣可以在VCL源码中查找相关的内容。一定会有不小的收获。
    else if Message.Msg = CM_VISIBLECHANGED then
    with Message do
    SendDockNotification(Msg, WParam, LParam);
    // 最后调用dispatch方法。
    Dispatch(Message);
    end;

      看完这段代码,你会发现TControl类实际上只处理了鼠标消息,没有处理的消息最后都转入Dispatch()来处理。

     但这里需要强调指出的是TControl自己并没有获得焦点Focus的能力。TControl的子类TWinControl才具有这样的能力。我凭什么这样讲?呵呵,还是打开BCB的帮助。很多朋友抱怨BCB的帮助实在不如VC的MSDN。毋庸讳言,的确差远了。而且这个帮助还经常有问题。但有总比没有好啊。

      言归正传,在帮助的The TWinControl Branch 分支下,您可以看到关于TWinControl类的简介。指出TWinControl类是所有窗体类控件的基类。所谓窗体类控件指的是这样一类控件:

      1. 可以在程序运行时取得焦点的控件。

      2. 其他的控件可以显示数据,但只有窗体类控件才能和用户发生键盘交互。

      3. 窗体类控件能够包含其他控件(容器)。

      4. 包含其他控件的控件又称做父控件。只有窗体类控件才能够作为其他控件的父控件。

      5. 窗体类控件拥有句柄。

      除了能够接受焦点之外,TWinControl的一切都跟TControl没什么分别。这一点意味着TwinControl可以对许多的标准事件作出响应,Windows也必须为它分配一个句柄。并且与这个主题相关的最重要的是,这里提到是由BCB负责来对控件进行重画以及消息处理。这就是说,TwinControl封装了这一切。

      似乎扯的太远了。但我要提出来的问题是TControl类的WndProc方法中处理了鼠标消息。但这个消息只有它的子类TwinControl才能够得到啊!?

      这怎么可以呢... Borland是如何实现这一切的呢?这个问题实在很奥妙。为了看个究竟,再次深入VCL吧。

      还是在control.pas中,TWinControl继承了TControl的WndProc方法。源码如下:

    procedure TWinControl.WndProc(var Message: TMessage);
    var
    Form: TCustomForm;
    KeyState: TKeyboardState;
    WheelMsg: TCMMouseWheel;
    begin
    case Message.Msg of
    WM_SETFOCUS:
    begin
    Form := GetParentForm(Self);
    if (Form <> nil) and not Form.SetFocusedControl(Self) then Exit;
    end;
    WM_KILLFOCUS:
    if csFocusing in ControlState then Exit;
    WM_NCHITTEST:
    begin
    inherited WndProc(Message);
    if (Message.Result = HTTRANSPARENT) and (ControlAtPos(ScreenToClient(
    SmallPointToPoint(TWMNCHitTest(Message).Pos)), False) <> nil) then
    Message.Result := HTCLIENT;
    Exit;
    end;
    WM_MOUSEFIRST..WM_MOUSELAST:
    //下面这一句话指出,鼠标消息实际上转入IsControlMouseMsg方法来处理了。
    if IsControlMouseMsg(TWMMouse(Message)) then
    begin
    if Message.Result = 0 then
    DefWindowProc(Handle, Message.Msg, Message.wParam, Message.lParam);
    Exit;
    end;
    WM_KEYFIRST..WM_KEYLAST:
    if Dragging then Exit;
    WM_CANCELMODE:
    if (GetCapture = Handle) and (CaptureControl <> nil) and
    (CaptureControl.Parent = Self) then
    CaptureControl.Perform(WM_CANCELMODE, 0, 0);
    else
    with Mouse do
    if WheelPresent and (RegWheelMessage <> 0) and
    (Message.Msg = RegWheelMessage) then
    begin
    GetKeyboardState(KeyState);
    with WheelMsg do
    begin
    Msg := Message.Msg;
    ShiftState := KeyboardStateToShiftState(KeyState);
    WheelDelta := Message.WParam;
    Pos := TSmallPoint(Message.LParam);
    end;
    MouseWheelHandler(TMessage(WheelMsg));
    Exit;
    end;
    end;
    inherited WndProc(Message);
    end;
    鼠标消息是由IsControlMouseMsg方法来处理的。只有再跟到IsControlMouseMsg去看看啦。源码如下:

    function TWinControl.IsControlMouseMsg(var Message: TWMMouse): Boolean;
    var
    //TControl出现啦
    Control: TControl;
    P: TPoint;
    begin
    if GetCapture = Handle then
    begin
    Control := nil;
    if (CaptureControl <> nil) and (CaptureControl.Parent = Self) then
    Control := CaptureControl;
    end else
    Control := ControlAtPos(SmallPointToPoint(Message.Pos), False);
    Result := False;
    if Control <> nil then
    begin
    P.X := Message.XPos - Control.Left;
    P.Y := Message.YPos - Control.Top;
    file://TControl的Perform方法将消息交由WndProc处理。
    Message.Result := Control.Perform(Message.Msg, Message.Keys, Longint(PointToSmallPoint(P)));
    Result := True;
    end;
    end;



      原来如此,TWinControl最后还是将鼠标消息交给TControl的WndProc来处理了。这里出现的Perform方法在BCB的帮助里可以查到是TControl类中开始出现的方法。它的作用就是将指定的消息传递给TControl的WndProc过程。

      结论就是TControl类的WndProc方法的消息是由TwinControl类在其重载的WndProc方法中调用IsControlMouseMsg方法后使用Peform方法传递得到的。

      由于这个原因,BCB和Delphi中的TControl类及其所有的派生类都有一个先天的而且是必须的限制。那就是所有的TControl类及其派生类的Owner必须是TwinControl类或者TWinControl的派生类。Owner属性最早可以在TComponent中找到,一个组件或者控件是由它的Owner拥有并负责释放其内存的。这就是说,当Owner从内存中释放的时候,它所拥有的所有控件占用的内存也都被释放了。Owner最好的例子就是Form。Owner同时也负责消息的分派,当Owner接收到消息的时候,它负责将应该传递给其所拥有的控件的消息传递给它们。这样这些控件就能够取得处理消息的能力。TImage就是个例子:你可以发现Borland并没有让TImage重载TControl的WndProc方法,所以TImage也只有处理鼠标消息的能力,而这种能力正是来自TControl的。

      唧唧崴崴的说了一大堆。终于可以说处理消息的第二种方法就是重载TControl的WndProc方法了。例程如下:

    void __fastcall TForm1::WndProc(TMessage &Message)
    {
    switch (Message.Msg)
    {
    case WM_CLOSE:
    OnCLOSE(Message); // 处理WM_CLOSE消息的方法
    break;
    }
    TForm::WndProc(Message);
    }

      乍看起来,这和上次讲的重载Dispatch方法好象差不多。但实际上还是有差别的。差别就在先后次序上,从前面TControl的WndProc可以看到,消息是先交给WndProc来处理,最后才调用Dispatch方法的啦。

      这样,重载WndProc方法可以比重载Dispatch方法更早一点点得到消息并处理消息。

  • 时至今日,学习Windows编程的兄弟们都知道消息机制的重要性。所以理解消息机制也成了不可或缺的功课。

      大家都知道,Borland的C++ Builder以及Delphi的核心是VCL。作为Win32平台上的开发工具,封装Windows的消息机制当然也是必不可少的。

      那么,在C++ Builder中处理消息的方法有哪些呢?它们之间的区别又在哪里?如果您很清楚这些,呵呵,对不起啦,请关掉这个窗口。 如果不清楚那就和我一起深入VCL的源码看个究竟吧。『注:BCB只有Professional和Enterprise版本才带有VCL源码。当然,大伙的版本都有源码的。我没猜错吧 :-)』

      方法1。使用消息映射(Message Map)重载TObject的Dispatch虚成员函数

      这个方法大家用的很多。形式如下

    BEGIN_MESSAGE_MAP

    VCL_MESSAGE_HANDLER( … …)

    END_MESSAGE_MAP( …)


      但这几句话实在太突兀,C++标准中没有这样的定义。不用讲,这显然又是宏定义。它们到底怎么来的呢?CKER第一次见到它们的时候,百思不得其解。嘿嘿,不深入VCL,怎么可能理解?

    在\Borland\CBuilder5\Include\Vcl找到sysmac.h,其中有如下的预编译宏定义:

    #define BEGIN_MESSAGE_MAP virtual void __fastcall Dispatch(void *Message) \
    { \
    switch (((PMessage)Message)->Msg) \
    {

    #define VCL_MESSAGE_HANDLER(msg,type,meth) \
    case msg: \
    meth(*((type *)Message)); \
    break;

    // NOTE: ATL defines a MESSAGE_HANDLER macro which conflicts with VCL's macro. The
    // VCL macro has been renamed to VCL_MESSAGE_HANDLER. If you are not using ATL,
    // MESSAGE_HANDLER is defined as in previous versions of BCB.
    file://
    #if !defined(USING_ATL) && !defined(USING_ATLVCL) && !defined(INC_ATL_HEADERS)
    #define MESSAGE_HANDLER VCL_MESSAGE_HANDLER
    #endif // ATL_COMPAT

    #define END_MESSAGE_MAP(base)
    default: \
    base::Dispatch(Message); \
    break; \
    } \
    }


      这样对如下的例子:

    BEGIN_MESSAGE_MAP

    VCL_MESSAGE_HANDLER(WM_PAINT,TMessage,OnPaint)

    END_MESSAGE_MAP(TForm1)


      在预编译时,就被展开成如下的代码

    virtual void __fastcall Dispatch(void *Message)
    {
    switch (((PMessage)Message)->Msg)
    {
    case WM_PAINT:
    OnPaint(*((TMessage *)Message)); //消息响应句柄,也就是响应消息的成员函数,在Form1中定义
    break;

    default:
    Form1::Dispatch(Message);
    break;
    }
    }


      这样就很顺眼了,对吧。对这种方法有两点要解释一下:

      1。virtual void __fastcall Dispatch(void *Message) 这个虚方法的定义最早可以在TObject的定义中找到。打开BCB的帮助,查找TForm的Method(方法),你会发现这里很清楚的写着Dispatch方法继承自TObject。如果您关心VCL的继承机制的话,您会发现TObject是所有VCL对象的基类。TObject的抽象凝聚了Borland的工程师们的心血。如果有兴趣。您应该好好查看一下TObject的定义。

      很显然,所有Tobject的子类都可以重载基类的Dispatch方法,来实现自己的消息调用。如果Dispatch方法找不到此消息的定义,会将此消息交由TObject::DefaultHandler方法来处理。抽象基类TObject的DefaultHandler方法实际上是空的。同样要由继承子类重载实现它们自己的消息处理过程。

      2。很多时候,我见到的第二行是这样写的:

      MESSAGE_HANDLER(WM_PAINT,TMessage,OnPaint)

      在这里,您可以很清楚的看到几行注解,意思是ATL中同样包含了一个MESSAGE_HANDLER的宏定义,这与VCL发生了冲突。为了解决这个问题,Borland改用VCL_MESSAGE_HANDLER这样的写法。

      当您没有使用ATL的时候,MESSAGE_HANDLER将转换成VCL_MESSAGE_HANDLER。但如果您使用了ATL的话,就会有问题。所以我建议您始终使用VCL_MESSAGE_HANDLER的写法,以免出现问题。

  •  

     还在Dos时代,人们就在寻求一种多任务的实现。于是出现了TSR类型的后台驻留程序,比较有代表性的有Side Kick、Vsafe等优秀的TSR程序,这类程序的出现和应用确实给用户使用计算机带来了极大的方便,比如Side Kick,我们编程可以在不用进编辑程序的状态下,一边编辑源程序,一边编译运行,非常方便。但是,Dos单任务操作系统的致命缺陷注定了在Dos下不可能开发出真正的多任务程序。进入Windows3.1时代,这种情况依然没有根本的改变,一次应用只能做一件事。比如数据库查询,除非应用编得很好,在查询期间整个系统将不响应用户的输入。

      进入了Windows NT和Windows 9x时代,情况就有了彻底的改观,操作系统从真正意义上实现了多任务(严格地说,Win9x还算不上)。一个应用程序,在需要的时候可以有许多个执行线程,每个线程就是一个小的执行程序,操作系统自动使各个线程共享CPU资源,确保任一线程都不能使系统死锁。这样,在编程的时候,可以把费时间的任务移到后台,在前台用另一个线程接受用户的输入。对那些对实时性要求比较高的编程任务,如网络客户服务、串行通信等应用时,多线程的实现无疑大大地增强了程序的可用性和稳固性。

      在Windows NT和Windows 9x中,多线程的编程实现需要调用一系列的API函数,如CreateThread、ResumeThread等,比较麻烦而且容易出错。我们使用Inprise公司的新一代RAD开发工具C++Builder,可以方便地实现多线程的编程。与老牌RAD工具Visual Basic和Delphi比,C++Builer不仅功能非常强大,而且它的编程语言是C++,对于系统开发语言是C的Windows系列操作系统,它具有其它编程语言无可比拟的优势。利用C++Builder提供的TThread对象,多线程的编程变得非常简便易用。那么,如何实现呢?且待我慢慢道来,让你体会一下多线程的强大功能。

      1. 创建多线程程序:

      首先,先介绍一下实现多线程的具体步骤。在C++Builder中虽然用Tthread对象说明了线程的概念,但是Tthread对象本身并不完整,需要在TThread下新建其子类,并重载Execute方法来使用线程对象。在C++Builder下可以很方便地实现这一点。

      在C++Builder IDE环境下选择菜单File|New,在New栏中选中Thread Object,按OK,接下来弹出输入框,输入TThread对象子类的名字MyThread,这样C++Builder自动为你创建了一个名为TMyThread的TThread子类。同时编辑器中多了一个名为Unit2.cpp的单元,这就是我们创建的TMyThread子类的原码,如下:

      #include
      #pragma hdrstop
      
      #include “Unit2.h”
      #pragma package(smart_init)
      //---------------------
      // Important: Methods and properties of objects in VCL can only be
      // used in a method called using Synchronize, for example:
      //
      // Synchronize(UpdateCaption);
      //
      // where UpdateCaption could look like:
      //
      // void __fastcall MyThread::UpdateCaption()
      // {
      // Form1->Caption = “Updated in a thread”;
      // }
      //--------------------
      __fastcall MyThread::MyThread(bool CreateSuspended)
       : TThread(CreateSuspended)
      {
      }
      //--------------------
      void __fastcall MyThread::Execute()
      {
       //---- Place thread code here ----
      }
      //---------------------

      其中的Execute()函数就是我们要在线程中实现的任务的代码所在处。在原代码中包含Unit2.cpp,这个由我们创建的TMyThread对象就可以使用了。使用时,动态创建一个TMyThread 对象,在构造函数中使用Resume()方法,那么程序中就增加了一个新的我们自己定义的线程TMyThread,具体执行的代码就是Execute()方法重载的代码。要加载更多的线程,没关系,只要继续创建需要数量的TMyThread 对象就成。

      以上我们初步地实现了在程序中创建一个自定义的线程,并使程序实现了多线程应用。但是,多线程应用的实现,并不是一件简单的工作,还需要考虑很多使多个线程能在系统中共存、互不影响的因素。比如,程序中公共变量的访问、资源的分配,如果处理不当,不仅线程会死锁陷入混乱,甚至可能会造成系统崩溃。总的来讲,在多线程编程中要注意共享对象和数据的处理,不能忽视。因此,下面我们要讲的就是多线程中常见问题:

     2. 多线程中VCL对象的使用

      我们都知道,C++Builder编程是建立在VCL类库的基础上的。在程序中经常需要访问VCL对象的属性和方法。不幸的是,VCL类库并不保证其中对象的属性和方法是线程访问安全的(Thread_safe),访问VCL对象的属性或调用其方法可能会访问到不被别的线程所保护的内存区域而产生错误。因此,TThread对象提供了一个Synchronize方法,当需要在线程中访问VCL对象属性或调用方法时,通过Synchronize方法来访问属性或调用方法就能避免冲突,使各个线程之间协调而不会产生意外的错误。如下所示:

      void __fastcall TMyThread::PushTheButton(void)
      
      {
       Button1->Click();
      }
      
      void __fastcall TMyThread::Execute()
      {
       ...
       Synchronize((TThreadMethod)PushTheButton);
       ...
      }

      对Button1-〉Click()方法的调用就是通过Synchronize()方法来实现的,它可以自动避免发生多线程访问冲突。在C++Builder中,虽然有一些VCL对象也是线程访问安全的(如TFont、TPen、TBrush等),可以不用Sychronize()方法对它们的属性方法进行访问调用以提高程序性能,但是,对于更多的无法确定的VCL对象,还是强烈建议使用Synchronize()方法确保程序的可靠性。

      3. 多线程中公共数据的使用

      程序设计中难免要在多个线程中共享数据或者对象。为了避免在多线程中因为同时访问了公共数据块而造成灾难性的后果,我们需要对公共数据块进行保护,直到一个线程对它的访问结束为止。这可以通过临界区域(Critical Section)的使用来实现,所幸的是在C++Builder中,给我们提供了一个TCriticalSection对象来进行临界区域的划定。该对象有两个方法,Acquire()和Release()。它设定的临界区域可以保证一次只有一个线程对该区域进行访问。如下例所示:

      class MyThread : public TThread
      {
       ...
      private:
      TCriticalSection pLockX;
      int x;
      float y;
      ...
      };
      void __fastcall MyThread::Execute()
      {
      ...
      pLockX->Acquire();//Here pLockX is a Global CriticalSection variable.
      x++;
      y=sin(x);
      pLockX->Release();
      ...
      }

      这样,对公共变量x,y的访问就通过全局TCriticalSection 对象保护起来,避免了多个线程同时访问的冲突。

      4. 多线程间的同步

      当程序中多个线程同时运行,难免要遇到使用同一系统资源,或者一个线程的运行要依赖另一个线程的完成等等,这样需要在线程间进行同步的问题。由于线程同时运行,无法从程序本身来决定运行的先后快慢,使得线程的同步看起来很难实现。所幸的是Windows系统是多任务操作系统,系统内核为我们提供了事件(Event)、Mutex、信号灯(semaphore)和计时器4种对象来控制线程间的同步。在C++Builder中,为我们提供了用于创建Event的TEvent 对象供我们使用。

      当程序中一个线程的运行要等待一项特定的操作的完成而不是等待一个特定的线程完成时,我们就可以很方便地用TEvent对象来实现这个目标。首先创建一个全局的TEvent对象作为所有线程可监测的标志。当一个线程完成某项特定的操作时,调用TEvent对象的SetEvent()方法,这样将设置这个标志,其他的线程可以通过监测这个标志获知操作的完成。相反,要取消这个标志,可以调用ResetEvent()方法。在需要等待操作完成的线程中使用WaitFor()方法,将一直等待这个标志被设置为止。注意WaitFor()方法的参数是等待标志设置的时间,一般用INFINITE表示无限等待事件的发生,如果其它线程运行有误,很容易使这个线程死住(等待一个永不发生的事件)。

      其实直接用Windows API函数也可以很方便地实现事件(Event)、信号灯(semaphore)控制技术。尤其是C++Builder,在调用Windows API方面有着其它语言无可比拟的优势。所用的函数主要有:CreateSemaphore()、CreateEvent()、WaitForSingleObject()、ReleaseSemaphore()、SetEvent()等等,这里就不赘述了。

      本文结合Inprise(Borland)公司开发的强大的RAD工具C++Builder的编程,对Windows下的多线程编程作了比较全面的介绍。其实多线程的实现并不神秘,看了本文,你也可以编出自己的多线程程序,真正体会多任务操作系统的威力。

      附:本文是本人在使用C++Builder一年来的一些实践体会。在完成自己的项目的同时,发现对多线程的编程一般的书籍都介绍得比较少,而实际应用中,多线程编程又是如此的重要,因此,本文通过对多线程编程比较全面的介绍,愿能达到抛砖引玉之效。

  • 要求: 算法中使用的内存数量是一个常数, 即不能因为链表长度的增减使用的内存也增减. 

    解析:其实就好象两个人在环形的跑道上跑步,速度快的必定可以超过速度慢的一圈最后相遇。(这个模型可以扩展为一个数组中只有两个相同的数,如何快速找出)


     flw 回复于:2004-09-20 13:18:10
    用两个指针,一个的步长为 1,另外一个的为 2,从表头开始一起往前走,如果相遇,表明有环路,否则就是没有了。


    win_hate 回复于:2004-09-20 18:50:29
    这个问题应该有很长历史了,《C 专家编程》的附录,程序员工作面试的秘密就提到过这个问题。在这

    里我试图对这个问题做一个数学的分析。

    如果单链表里存在重复的点,则该链表中包含一个环,事实上,可以用下面的图来表示

    这使我想起 Pollard 的 "rho" 算法,事实上本问题与 "rho" 算法有一个共同点,寻找一个碰撞。

    从这个图我们可以看到,如果一个单链表里出现了重复的点,则从表头开始走,无论以什么步调,必定

    会落到环中。所以我们可以肯定,如果以某个步调走,碰到了NULL,则该链表无重合点。

    尝试用两个指针,以不同的步调前进,如果他们能相遇,必定是在环中。假定指针 p 以步调 f 前进,q

    以步调 g 前进,g>f。则 q 先进入链表的环。有一种情况很特殊,就是:在 p 刚进入环的时候就与 q 

    相遇。这是一个小概率事件,我们排除它,不考虑这种情况。可以认为:

    p 进入环的时候,偏移为 a, 而同时 q 的偏移为 b, 环的长度为 n。(参考下面的图)


    往后, p, q 就在圈内打转,它们在x 步后重合的条件为:

    fx + a = gx + b (mod n)
    (f-g)x = b-a (mod n)

    上式有解等价于  (f-g, n) | (b-a)。 

    但是,我们在事前不知道 n, 不知道 b-a, 所以唯一能确保 (f-g, n) | (b-a) 成立的是 f-g =1。

    只要 f-g = 1, 我们就能一定能检测出重合的情况,这是一个充分条件。

    而一旦 p 刚进入环时与 q 不等,(f-g, n) | (b-a) 就成为检测重合的必要条件。前面一些朋友说 f,g 

    互素或 f, g 不同即可的观点是错误的。从 (f-g, n) | (b-a) 这个条件应该能找到反例。但这个我就

    留给有兴趣的朋友了。

    f = 1, g = 2 未必是最好的,因为如果 "rho" 的尾巴很长,p 要花费很多工夫才能进入环。此外,虽

    然步调大的时候,可能要跑好几个圈才能覆盖整个环,甚至在很多情况下不能覆盖整个环,但它跑一圈

    的时间也相应减少,足以抵消。可惜的是,分析最优的选择,超出了我的能力范围。









     yangtou 回复于:2004-09-20 19:28:07
    不需要考虑细节情况,就可以判断可能性
    x=pn/(a-b) > m/b
    只要改变p的值就可以找到合适的x,x>m/b所以只要ab不等就必定相遇

    另外如果m较小比如0,由x=pn/(a-b)(不考虑m/b)
    则x只决定于a-b(只要改变p就可以得到x的合适的最小值,而p是决定于a-b的)
    T=(au+bu+av+w)x,在(a-b)一定的情况下,ab取值越小T越小

  • “独乐乐不如众乐乐”,玩玩博客(blog)已经不够过瘾,于是玩Wiki开始成为时尚。如今,有人将Wiki翻译为“维客”,在形声义方面,也算得上一个合格的中文译名。与大多数网络亚文化一样,Wiki也同样是体现开放,合作,平等,共享的网络文化!那么wiki究竟是什么玩意?你肯定不知道,我也才刚刚明白。但是,不要着急,让我们慢慢看下去,你就知道wiki很可能是互联网奉献的又一个让你疯狂的新生事物。



    何为Wiki(维客)?

    首先,我们要搞清楚:wiki概念的发明人是Ward Cunningham。wiki这个字到底是什幺意思呢?根据FAQ的说法,WikiWiki一词来源于夏威夷语的“wee kee wee kee”,原本是“快点快点” (quick)的意思。实际上 wiki 也真的是既简单又快速,你可以看到 wiki 每天都在成长。

    新概念的定义总是让人有点摸不着头脑,wiki 也不例外。先看看简单解释:Wiki——一种多人协作的写作工具。Wiki站点可以有多人(甚至任何访问者)维护,每个人都可以发表自己的意见,或者对共同的主题进行扩展或者探讨。

    还不明白,那就给你看看更复杂、更晕眩的解释:Wiki指一种超文本系统。这种超文本系统支持面向社群的协作式写作,同时也包括一组支持这种写作的辅助工具。我们可以在Web的基础上对Wiki文本进行浏览、创建、更改,而且创建、更改、发布的代价远比HTML文本为小;同时Wiki系统还支持面向社群的协作式写作,为协作式写作提供必要帮助;最后,Wiki的写作者自然构成了一个社群,Wiki系统为这个社群提供简单的交流工具。与其它超文本系统相比,Wiki有使用方便及开放的特点,所以Wiki系统可以帮助我们在一个社群内共享某领域的知识。

    Wiki概念的通俗解说
    还是有点云里雾里?那就通俗一点:根据 wiki 社群的定义,wiki 是一种提供「共同创作(collaborative)」环境的网站,也就是说,每个人都可以任意修改网站上的页面资料。这听起来挺疯狂的,万一有陌生人来网站上乱搞怎幺办?别担心,所有的 wiki 都有「版本控制(Version Control)」的概念,你随时都可以找回之前的正确版本;更何况你可不是单打独斗,社群的力量是非常惊人的。就在这种相信人性本善的概念下,整个 wiki 社群迅速地成长茁壮。如果你还是很担心,更可以帮 wiki 加入权限管理(Access Control)的机制,保证万无一失。除了版本控制之外,值得一题的是「格式化语法(Formating Rule)」。因为对一般人来说,HTML 语法实在是个恶梦,所以 wiki 创造了一套更简单的写作语法,让大家可以专注在写作上。

    你还是不明白,那也不要紧,我们就先不要咬文嚼字,还是以一种看热闹的从容心态,先慢慢看下去。

    Wiki发展历史
    Wiki的历史还不长,无论是Wiki概念自身,还是相关软件系统的特性,还都在热烈的讨论中;所以怎样的一个站点才能称得上是一个Wiki系统还是有争议的。与Wiki相关最近出现的技术还有blog,它们都降低了超文本写作和发布的难度。这两者都与内容管理系统关系紧密。第一个 Wiki 网站诞生于 1995 年,Ward Cunningham 创建的,作为波特兰的模式仓库的模式定义和讨论的交互性场所: http://c2.com/ppr/;而其根源可以上述到 1972 年卡耐基-梅隆大学的 ZOG 数据库系统。

    1995年Ward Cunningham为了方便模式社群的交流建立了一个工具-波特兰模式知识库(Portland Pattern Repository)。在建立这个系统的过程中,Ward Cunningham创造了Wiki的概念和名称,并且实现了支持这些概念的服务系统。这个系统是最早的Wiki系统。从1996年至2000年间,波特兰模式知识库围绕着面向社群的协作式写作,不断发展出一些支持这种写作的辅助工具,从而使Wiki的概念不断得到丰富。同时Wiki的概念也得到了传播,出现了许多类似的网站和软件系统。

    历史资料:http://c2.com/cgi/wiki?WikiHistory
  • Web_2.0入门到精通
    2005-8-9 9:47:36    klogs/KMCenter 

    历史很重要。对一个技术的学习也应当从历史出发,通过其在时间形成历史的流变,得以知晓现状,甚至能够预知未来。

      那Web 1.0是什么呢?

      他们说,记得静态HTML的WWW时代么?

      (那个时代的WWW应用、人们的Web体验、对社会的影响如何?)

      那么动态HTML和静态HTML下的Web相比,是多少版本?1.5?对了,他们是真这么叫的。

      (在效果和影响上,与1.0相比,扩展和加深多少?)

      要呈现的数据存储在数据库中,通过Web服务端的程序,应用户的请求,取出数据,加上事先设计的模板,动态的生成Html代码,发送到用户的浏览器那里。

      他是1.0系列,应为用户在浏览器中所见和Web 1.0一样,它有0.5的升级,因为数据不是事先制作并发布,而是动态生成,和用户的需要交互生成。

      那好,在加0.5,到Web 2.0,变化是在哪里呢?

      (看到了正在崛起的和改变的,会继续朝着什么方向改变互联网和社会呢?)

      更新:关于各个版本的差别,看看亚马逊的例子。

      事情没有那么幸运,Web 2.0并不是一个具体的事物,而是一个阶段,是促成这个阶段的各种技术和相关的产品服务的一个称呼。所以,我们无法说,Web 2.0是什么,但是可以说,那些是Web 2.0。

      WikiPedia的Web 2.0条目下列出了这些条件:

      * CSS 和语义相关的 XHTML 标记

      * AJAX 技术

      * Syndication of data in RSS/ATOM

      * Aggregation of RSS/ATOM data

      * 简洁而有意义的 URLs

      * 支持发布为 weblog

      * RESTian (preferred) 或者 XML Webservice APIs

      * 一些社会性网络元素

      必须具备的要素有:

      * 网站应该能够让用户把数据在网站系统内外倒腾。

      * 用户在网站系统内拥有自己的数据

      * 完全基于Web,所有的功能都能透过浏览器完成。

      (以上内容引用自英文版维基百科)

      虽然这只是一家之言,不过,对于其中谈到的几个要素,大家还是公认的。

      - 基于RSS/ATOM/RDF/FOAF等XML数据的同步、聚合和迁移。

      数据不再和页面和网站混粘在一起,它独立了,它跟着用户走。这是Web 2.0的很重要特征。这也是为什么Blog是Web 2.0的代表的原因。在网志上,常主角的是相互独立的一则则的网志。

      独立,然后有物理表现。现在,就能让他们活跃起来。透过对XML数据的处理,这些内容能被自由的组合,被各种应用程序,不论是Web程序还是桌面程序等呈现和处理。

      当然,最重要的是背后的人。

      - 社会性因素。

      内容跟着人走,内容又能够被用户自由的组合,也就是说,用户能够自由的借助内容媒介,创建起一个个的社群,发生各种社会性的(网络)行为。

      此外还有标签以及建立在开放标签系统之上的Folksonomy。

      - 第三个公认的因素是开放API,这个技术性稍强些,得另花时间研习,可以先看看例子:amazon、flickr、google map等。

      从Web应用的产品/服务生产者角度来说,该如何创建Web 2.0的产品呢?

      重要的是要抓住这么几点,一个是微内容(这里有定义),一个是用户个体。除了这两个最基本的之外,还可以考虑社群内的分享以及提供API。

      微内容:英文是microcontent。用户所生产的任何数据都算是微内容,比如一则网志,评论,图片,收藏的书签,喜好的音乐列表、想要做的事情,想要去的地方、新的朋友等等。这些微内容,充斥着我们的生活、工作和学习,它的数量、重要性,还有我们对它的依赖,并不亚于那些道貌岸然、西装革履的正统文章、论文、书籍。

      对微内容的重新发现和利用,是互联网所开创的平等、民主、自由风气的自然衍生,也是互联网相关技术消减信息管理成本之后的一个成果。

      我们每天都生产众多的微内容,也消费着同样多的微内容。对于Web 2.0来说,如何帮助用户管理、维护、存储、分享、转移微内容,就成了关键。

      用户个体。对于Web 1.0的典型产品/服务来说,用户没有具体的面貌、个性,它只是一个模糊的群体的代名词而已。但是对于Web2.0的产品和服务来说,用户是个实实在在的人。Web 2.0所服务的,是具体的人,而不是一个如同幽灵般的概念。并且,这个人的具体性,会因为服务本身而不断地充实起来。

      如何为这个具体的个体服务,是Web 2.0设计的起点。

      因此,一类可以被称作Web 2.0的产品/服务将是这样:

      服务于用户个体的微内容的收集、创建、发布、管理、分享、合作、维护等的平台。

      其他的呢?恐怕就设计到好些人提到的,微内容的XML表现;微内容的聚合;微内容的迁移;社会性关系的维护;界面的易用性等等。

      以及是否就是开源、参与、个人价值、草根、合作等等?

      Web2.0是许多方面起头并进又相互牵连的一个新的阶段的到来。因此,不同的人,有着不同的看法。那么,对于Web开发人员来说,Web2.0意味着什么呢?

      他们说Web2.0阶段,Web是一个平台,或者说,Web正在变得可编程,可以执行的Web应用。野心家们设想这个它的终极目标是Web OS。

      Web 1.0时候,Web只是一个针对人的阅读的发布平台,Web由一个个的超文本链接而成。现在的趋势发生了变化,Web不仅仅是Html文档的天下,它成了交互的场所。

      Web 2.0 Conference网站的横幅引用Jeff Bezos的话说“Web 1.0 is making the internet for people,web 2.0 is making the internet better for computers”。

      具体来讲,他们说Web成为一个开发环境,借助Web服务提供的编程接口,网站成了软件构件。

      这些,就是Web Service的目标吧,信息孤岛通过这些Web Service的对话,能够被自由构建成适合不同应用的建筑来。

      一些例子:del.icio.us、flickr、a9、amazon、yahoo、google、msn等提供的编程接口衍生出的各种应用。

      为什么要开放APIs,这涉及到集市中的商业方面的技术策略。当然,还有更深层的原因,那是什么呢?

      这种交互不仅体现在不同的网站服务之间,同时还体现在用户和Web之间在浏览器上的交互。这也是为什么在美味书签的收藏中Web2.0和AJAX如此相关的一个原因。

      在Web页面上使用桌面程序有的那些便利,真的是很享受的事情。这恐怕也是Web可编程的一个方面,Web页面不再是标记和内容混合那样的简单,它就是一个可以编程的地方(是这样理解吧?)

      有人反对说,AJAX的使用对搜索引擎不友好,只有Web 1.0的站长才关心这个事情吧,在Web 2.0时候,站长应该关心的是用户参与的便利、用户的自由度,至于搜索,有RSS/ATOM/RDF等,更本用不着操心,Google不是已经顺应这个趋势,让大家主动提交了么?

      可编程的第三个方面,是否在于Web应用和桌面应用之间的无缝连接趋势的出现?类似这里说的“从工具上,是由互联网浏览器向各类浏览器、rss阅读器等内容发展”

      编程的一个重要目的是对数据的操作,因此,对于网站来说,除了Web Service接口之外,最近为简便方式就是将内容以RSS/RDF/ATOM格式,或者有意义的XHTML格式输出,同时实现内容和表现的分离。

      [Web 2.0是个历史学的概念,而非是个技术性的概念,它是对Web发展历史断代的成果。对这个概念的梳理,能帮助我更好的把握互联网正在发生的技术与文化。]

      中文网志圈谈论的Web 2.0内容摘要:

      - “Web 2.0是用来研究现象、发现规律的东西,不是用来招商引资、搭台唱戏的东西。当越来越多的互联网应用采取与用户互动的方式,越来越多的内容是由用户产生,越来越多的用户参与到互联网创造的过程中的时候,其实它代表了一种新的思潮。在这种思潮之下,一些新的技术开始出现,一些古老技术重新焕发了生机。随便你怎么表述这样一种现象,但现象本身是实实在在存在的,不管是叫它Web 2.0,还是社会化互联网。”[Keso:老冒给Web 2.0浇了一桶冷水]

      - “我觉得最有价值的一个是, web应用的数据格式开始逐渐出现了交换“标准”...这些标准...更加容易被机器自动化处理...能帮助人更好地过滤和定制化信息。其次,更多的服务将以web service的形式来提供,...这使得web 服务可以被互相集成, 从而诞生更多新的服务...人的重要性被提高了。过去web更多注重在信息提供, 而现在的越来越多的应用更加关注人,也就是所谓“社会性”。此外web的可用性改进正在被越来越重视...”[老冒:朝web 2.0泼点冷水]

      - “我认为Web x.x是人们为了区别不同时代Web的发展而使用的,而这些概念也是经过归纳出来的结果。抓住对方向,如Wikipedia中所提到的朝向互动及社会网络的方向发展,不论应用何种技术,只要能达到目的都是很好的。甚至作为一般的使用者,都可以不去理会Web x.x的讨论,因为我们都已经在使用这些技术或网站了。”[图书馆观点:Web 2.0]

      - “RSS逐渐成为在线内容提供服务的标准发行平台。Blog以及user-generated内容的兴起。My Yahoo提供的RSS整合型服务。同时提出了值得密切关注的一些发展中领域,其中包括搜索技术,个性化,User-Generated内容(包括 blog,评论,图像和声音),音乐,短视频和Accessibility(易访问性)”[Owen:Mary Meeker新作 - 关于Digital World的发展报告的摘取]

      - “我们谈论的Web2.0带给我们的是一种可读写的网络,这种可读写的网络表现于用户是一种双通道的交流模式,也就是说网页与用户之间的互动关系由传统的“Push”模式演变成双向交流的“Two- Way Communication”的模式。而对于Web服务的开发者来说,Web2.0带来的理念是服务的亲和力,可操作性,用户体验以及可用性。”[Owen:BaCKpACK-体验可读写的Web服务]

      - “web 2.0是一种可以被分发的信息概述,web文档被格式化成了web数据。我们不会再看到不同旧地信息,现在我们所注意到是一种聚合、再混合内容的工具。”[songzhen:也说Web 2.0的翻译]

      - “从这些应用中可以看到:如果基于传统的HTML,同样的功能实现将变得非常复杂和不稳定,数据的再生产和交换成本是很高的。所以:RSS这个标准最终要的贡献就是使得互联网的大部分网站变得可编程:类似的例子还有Blog中的:TrackBack Ping等机制,这些机制都是依赖XML/RPC实现的。当初为Lucene设计一个RSS/XML的接口也是为了这个初衷,它使得全文检索服务可以轻松的嵌入到各种应用中,通过关键词将各种内容之间实现更丰富的关联(Well Referenced)。”[车东:RSS,简单协议使得互联网可编程]

      - “聚合的可能性以及如何更好地聚合(通常来说,更好的聚合应该基于个人知识管理和人际关系管理)很显然应该成为新一代或者说web2.0架构的核心之一。还有,你会重新发现,恰好是分散带动了聚合,聚合促进了分散,通过聚合的思维,互联网的网络状变得越来越丰富和密集,web2.0就变得越来越有趣味,它将web1.0时代的硕大节点即门户网站不断消解,去努力创造一个更加和谐的自然网络图谱。”[Horse:rss,聚合的无数可能]

      - “新的web2.0网站都依赖于用户参与、用户主导、用户建设”。[Horse:Web 2.0这个词]

      - Keso:Web 1.0与Web 2.0的区别

      - “表面上看,Bloglines取代了门户,成为一个新的中心,但这里有一个重大的区别。门户是只读的,它带有某种锁定的性质。你可以离开门户,但你无法带走门户的内容。Bloglines则完全不同,你觉得它好用,就会继续使用,有一天你不再喜欢Bloglines,你完全可以导出你的OPML,到另一个 RSS订阅网站,或者干脆用客户端软件浏览同样的内容。所以,像Bloglines这样的网站,是可写的,你可以导入,也可以导出。就像你对信息拥有选择权,对服务提供商也同样拥有选择权,没有人可以锁定你,主动权在你自己手上。”[Keso:再说信息选择权]

      - “Flickr、del.icio.us、Bloglines等Web 2.0服务,通过开放API获得了很多有趣、有用的想法,并借助外部的力量,让用户获得了更好的体验。更多大公司也加入到开放API的潮流中,Google、Yahoo!、Amazon、Skype。Google桌面搜索今年3月才开放API,很快就产生了大量的创造,大大扩充了可搜索的文件格式。”[Keso:开放API]

      - “归纳:web1.0天天谈门户,web2.0谈个人化;web1.0谈内容,web2.0谈应用;web1.0商业模式,web2.0谈服务;web1.0谈密闭、大而全,web2.0大家谈开放、谈联合;web1.0网站中心化,web2.0谈个人中心化;web1.0一对一,web2.0谈社会性网络;web1.0不知道你是狗,web2.0你去年夏天干了什么我一清二楚甚至想要干什么呢。。。”[van_wuchanghua:发现了N.HOOLYWOOD,我还知道你今年夏天要干什么]

      - “我认为Web2.0有下面几个方面的特性: 个性化的传播方式. 读与写并存的表达方式. 社会化的联合方式.标准化的创作方式. 便捷化的体验方式. 高密度的媒体方式.”[飞戈:Web2.0与未来的网络]

      - “用RAILS写的网站带有典型的读写网络的特征:RAILS创建的三个架构中的ACTIVE RECORD这个模块中,如果你读读它最重要的基类 ActiveRecord::Base,你会发现有CREAT,EDIT,SAVE,DESTROY这些方法已经天然包含在内了,这让实现一个数据库的CRUDS行为变得如此简单。由于这些类的方法直接和网页的名称映射到一起,这使得网页本身就像一个可以编缉的数据库的数据项。”[Blogdriver:RUBY ON RAILS,wEB2.0世界新生的创造力]

      - “Greasemonkey一定名列前茅。这个通过User Script就能修改任何网页输出效果的插件极大的提高了用户阅读的自主性,一推出就引起了轰动,同时也引来了不少争议。”[Webleon:platypus,完全可写的互联网]

      - “Web1.0到Web2.0的转变,具体的说,从模式上是单纯的“读”向“写”、“共同建设”发展;从基本构成单元上,是由“网页”向“发表/记录的信息”发展;从工具上,是由互联网浏览器向各类浏览器、rss阅读器等内容发展;运行机制上,由“Client Server”向“Web Services”转变;作者由程序员等专业人士向全部普通用户发展;应用上由初级的“滑稽”的应用向全面大量应用发展。 ”[Don:Web 2.0概念阐释]

      Web 2.0阶段的一个重要特征是开放,和Web初期的开放有很大不同,有以下几种突出的表现:

      内容方面。

      - 内容的创作共用授权。它的广谱和可选择性,让它具有了足够的生命力。CC先是在网志圈中广泛采用,后来许多商业公司也纷纷采用CC方式(比如BBC);先是文本世界采用,后来逐渐推广到了多媒体世界,比如音频、视频、Flash动画等等。一场自由的文化(free culture)运动在各个方面悄然铺开。

      - 内容来源方面的开放。和早期的Web阶段相比,由于使用相关设备的成本降低,利用相关技术的门槛减低,人们可以自由生产并发布各种内容,比如文本信息,比如语音记录,比如视频录制等。信息的生产和传播不再仅仅是商业资本或者技术精英的特权。在Web的新阶段,原来在商业、技术与大众之间的信息生产和传播的落差被削平。消除信息垄断和去中心化已经成为可能。不仅如此,信息的生产和消费的模式也发生了变化,从原来的生产/消费的对立,变成了参与式的信息集市。

      Web主体方面。

      - 商业网站也渐渐采取了开放的、参与的模式。除了内容上的CC授权出现之外。原来并不外露的内容,也随着blog、podcasting等的兴盛而对外开放。一些网站还在技术层面开放,比如开放源代码,比如开放APIs(编程接口),让自己成为一个平台,让用户可以参与衍生产品的创造,用户本身也是产品的生产者。不仅是内容、技术层面,在鼓励用户的参与上,也有相应的开放出现,比如一些新闻网站的RSS源的输出、引用通告(trackback)功能的采纳、blogthis便利的提供,无一不是让用户参与到内容生产、传播的各个环节。

      - 个人信息层面的开放。有开放,才有交流,才有社会行为和形态产生。个人内容的开放是与一类Web 2.0服务的兴起有关。它涵盖了内容(文本、声音、影像、视频)、关系、行为等等。

  • 2005-10-12

    健康的价值

    健康包括心理健康,身体健康,对社会的认识能力,道德的健康.当我们的身体没有感觉的时候,那就是健康.而当身体有疾病的时候,那就是不健康的感觉.

    当今社会压力倍增,越来越多的人感觉身体不舒适.专家调查,中国有15%的人身上有疾病,80%的人处于亚健康状态,而只有5%的人身体健康.实在是很惊人的一个数字.

    我对比了一下,我就是处于亚健康状态,时常会头晕,发困,睡不醒.严重啊.

    听了一个营养老师的课后,学到一个小小但很实在的办法.

    第一就是:一日三餐要吃好.早饭占到一天消耗的40%-50%,所以有个说法,早餐吃的要象皇帝.中午饭和晚饭占50%-60%,也就是中午吃的象大臣,晚上象平民.

    第二,每天要做十到二十次深呼吸:吸如闻香,呼如吐丝.

    恩,健康是自己的,好好保养,人在阵地在!

  • C++中extern “C”含义深层探索


    作者:宋宝华              出处:PConline

      1.引言

      C++语言的创建初衷是“a better C”,但是这并不意味着C++中类似C语言的全局变量和函数所采用的编译和连接方式与C语言完全相同。作为一种欲与C兼容的语言,C++保留了一部分过程式语言的特点(被世人称为“不彻底地面向对象”),因而它可以定义不属于任何类的全局变量和函数。但是,C++毕竟是一种面向对象的程序设计语言,为了支持函数的重载,C++对全局函数的处理方式与C有明显的不同。

      2.从标准头文件说起

      某企业曾经给出如下的一道面试题:

      面试题
      为什么标准头文件都有类似以下的结构?




    #ifndef __INCvxWorksh
    #define __INCvxWorksh
    #ifdef __cplusplus
    extern "C" {
    #endif
    /*...*/
    #ifdef __cplusplus
    }
    #endif
    #endif /* __INCvxWorksh */

      分析
      显然,头文件中的编译宏“#ifndef __INCvxWorksh、#define __INCvxWorksh、#endif” 的作用是防止该头文件被重复引用。

      那么
    #ifdef __cplusplus
    extern "C" {
    #endif
    #ifdef __cplusplus
    }
    #endif

      的作用又是什么呢?我们将在下文一一道来。
      3.深层揭密extern "C"

      extern "C" 包含双重含义,从字面上即可得到:首先,被它修饰的目标是“extern”的;其次,被它修饰的目标是“C”的。让我们来详细解读这两重含义。

      被extern "C"限定的函数或变量是extern类型的;

      extern是C/C++语言中表明函数和全局变量作用范围(可见性)的关键字,该关键字告诉编译器,其声明的函数和变量可以在本模块或其它模块中使用。记住,下列语句:
      extern int a;

      仅仅是一个变量的声明,其并不是在定义变量a,并未为a分配内存空间。变量a在所有模块中作为一种全局变量只能被定义一次,否则会出现连接错误。

      通常,在模块的头文件中对本模块提供给其它模块引用的函数和全局变量以关键字extern声明。例如,如果模块B欲引用该模块A中定义的全局变量和函数时只需包含模块A的头文件即可。这样,模块B中调用模块A中的函数时,在编译阶段,模块B虽然找不到该函数,但是并不会报错;它会在连接阶段中从模块A编译生成的目标代码中找到此函数。

      与extern对应的关键字是static,被它修饰的全局变量和函数只能在本模块中使用。因此,一个函数或变量只可能被本模块使用时,其不可能被extern “C”修饰。

      被extern "C"修饰的变量和函数是按照C语言方式编译和连接的;

      未加extern “C”声明时的编译方式

      首先看看C++中对类似C的函数是怎样编译的。

      作为一种面向对象的语言,C++支持函数重载,而过程式语言C则不支持。函数被C++编译后在符号库中的名字与C语言的不同。例如,假设某个函数的原型为:
    void foo( int x, int y );

      该函数被C编译器编译后在符号库中的名字为_foo,而C++编译器则会产生像_foo_int_int之类的名字(不同的编译器可能生成的名字不同,但是都采用了相同的机制,生成的新名字称为“mangled name”)。

      _foo_int_int这样的名字包含了函数名、函数参数数量及类型信息,C++就是靠这种机制来实现函数重载的。例如,在C++中,函数void foo( int x, int y )与void foo( int x, float y )编译生成的符号是不相同的,后者为_foo_int_float。
      同样地,C++中的变量除支持局部变量外,还支持类成员变量和全局变量。用户所编写程序的类成员变量可能与全局变量同名,我们以"."来区分。而本质上,编译器在进行编译时,与函数的处理相似,也为类中的变量取了一个独一无二的名字,这个名字与用户程序中同名的全局变量名字不同。

      未加extern "C"声明时的连接方式

      假设在C++中,模块A的头文件如下:
    // 模块A头文件 moduleA.h
    #ifndef MODULE_A_H
    #define MODULE_A_H
    int foo( int x, int y );
    #endif

      在模块B中引用该函数:
    // 模块B实现文件 moduleB.cpp
    #include "moduleA.h"
    foo(2,3);

      实际上,在连接阶段,连接器会从模块A生成的目标文件moduleA.obj中寻找_foo_int_int这样的符号!

      加extern "C"声明后的编译和连接方式

      加extern "C"声明后,模块A的头文件变为:
    // 模块A头文件 moduleA.h
    #ifndef MODULE_A_H
    #define MODULE_A_H
    extern "C" int foo( int x, int y );
    #endif

      在模块B的实现文件中仍然调用foo( 2,3 ),其结果是:

      (1)模块A编译生成foo的目标代码时,没有对其名字进行特殊处理,采用了C语言的方式;

      (2)连接器在为模块B的目标代码寻找foo(2,3)调用时,寻找的是未经修改的符号名_foo。

      如果在模块A中函数声明了foo为extern "C"类型,而模块B中包含的是extern int foo( int x, int y ) ,则模块B找不到模块A中的函数;反之亦然。

      所以,可以用一句话概括extern “C”这个声明的真实目的(任何语言中的任何语法特性的诞生都不是随意而为的,来源于真实世界的需求驱动。我们在思考问题时,不能只停留在这个语言是怎么做的,还要问一问它为什么要这么做,动机是什么,这样我们可以更深入地理解许多问题):
      实现C++与C及其它语言的混合编程。
      明白了C++中extern "C"的设立动机,我们下面来具体分析extern "C"通常的使用技巧。
      4.extern "C"的惯用法

      (1)在C++中引用C语言中的函数和变量,在包含C语言头文件(假设为cExample.h)时,需进行下列处理:
    extern "C"
    {
    #include "cExample.h"
    }

      而在C语言的头文件中,对其外部函数只能指定为extern类型,C语言中不支持extern "C"声明,在.c文件中包含了extern "C"时会出现编译语法错误。

      笔者编写的C++引用C函数例子工程中包含的三个文件的源代码如下:
    /* c语言头文件:cExample.h */
    #ifndef C_EXAMPLE_H
    #define C_EXAMPLE_H
    extern int add(int x,int y);
    #endif
    /* c语言实现文件:cExample.c */
    #include "cExample.h"
    int add( int x, int y )
    {
    return x + y;
    }
    // c++实现文件,调用add:cppFile.cpp
    extern "C"
    {
    #include "cExample.h"
    }
    int main(int argc, char* argv[])
    {
    add(2,3);
    return 0;
    }

      如果C++调用一个C语言编写的.DLL时,当包括.DLL的头文件或声明接口函数时,应加extern "C" { }。

      (2)在C中引用C++语言中的函数和变量时,C++的头文件需添加extern "C",但是在C语言中不能直接引用声明了extern "C"的该头文件,应该仅将C文件中将C++中定义的extern "C"函数声明为extern类型。
      笔者编写的C引用C++函数例子工程中包含的三个文件的源代码如下:
    //C++头文件 cppExample.h
    #ifndef CPP_EXAMPLE_H
    #define CPP_EXAMPLE_H
    extern "C" int add( int x, int y );
    #endif
    //C++实现文件 cppExample.cpp
    #include "cppExample.h"
    int add( int x, int y )
    {
    return x + y;
    }
    /* C实现文件 cFile.c
    /* 这样会编译出错:#include "cExample.h" */
    extern int add( int x, int y );
    int main( int argc, char* argv[] )
    {
    add( 2, 3 );
    return 0;
    }

      如果深入理解了第3节中所阐述的extern "C"在编译和连接阶段发挥的作用,就能真正理解本节所阐述的从C++引用C函数和C引用C++函数的惯用法。对第4节给出的示例代码,需要特别留意各个细节。

      欢迎与作者联系沟通。联系方式:
      Email: 21cnbao@21cn.com
      MSN: barrysong80@hotmail.com

  • 2005-10-11

    内存管理内幕

    内存管理内幕[转载]- -

                                          

    动态分配的选择、折衷和实现

    级别: 中级

    Jonathan Bartlett
    技术总监, New Media Worx
    2004 年 11 月 29 日

    本文将对 Linux™ 程序员可以使用的内存管理技术进行概述,虽然关注的重点是 C 语言,但同样也适用于其他语言。文中将为您提供如何管理内存的细节,然后将进一步展示如何手工管理内存,如何使用引用计数或者内存池来半手工地管理内存,以及如何使用垃圾收集自动管理内存。

    为什么必须管理内存
    内存管理是计算机编程最为基本的领域之一。在很多脚本语言中,您不必担心内存是如何管理的,这并不能使得内存管理的重要性有一点点降低。对实际编程来说,理解您的内存管理器的能力与局限性至关重要。在大部分系统语言中,比如 C 和 C++,您必须进行内存管理。本文将介绍手工的、半手工的以及自动的内存管理实践的基本概念。

    追溯到在 Apple II 上进行汇编语言编程的时代,那时内存管理还不是个大问题。您实际上在运行整个系统。系统有多少内存,您就有多少内存。您甚至不必费心思去弄明白它有多少内存,因为每一台机器的内存数量都相同。所以,如果内存需要非常固定,那么您只需要选择一个内存范围并使用它即可。

    不过,即使是在这样一个简单的计算机中,您也会有问题,尤其是当您不知道程序的每个部分将需要多少内存时。如果您的空间有限,而内存需求是变化的,那么您需要一些方法来满足这些需求:

    • 确定您是否有足够的内存来处理数据。
    • 从可用的内存中获取一部分内存。
    • 向可用内存池(pool)中返回部分内存,以使其可以由程序的其他部分或者其他程序使用。

    实现这些需求的程序库称为 分配程序(allocators),因为它们负责分配和回收内存。程序的动态性越强,内存管理就越重要,您的内存分配程序的选择也就更重要。让我们来了解可用于内存管理的不同方法,它们的好处与不足,以及它们最适用的情形。

    C 风格的内存分配程序
    C 编程语言提供了两个函数来满足我们的三个需求:

    • malloc:该函数分配给定的字节数,并返回一个指向它们的指针。如果没有足够的可用内存,那么它返回一个空指针。
    • free:该函数获得指向由 malloc 分配的内存片段的指针,并将其释放,以便以后的程序或操作系统使用(实际上,一些 malloc 实现只能将内存归还给程序,而无法将内存归还给操作系统)。

    物理内存和虚拟内存
    要理解内存在程序中是如何分配的,首先需要理解如何将内存从操作系统分配给程序。计算机上的每一个进程都认为自己可以访问所有的物理内存。显然,由于同时在运行多个程序,所以每个进程不可能拥有全部内存。实际上,这些进程使用的是 虚拟内存

    只是作为一个例子,让我们假定您的程序正在访问地址为 629 的内存。不过,虚拟内存系统不需要将其存储在位置为 629 的 RAM 中。实际上,它甚至可以不在 RAM 中 —— 如果物理 RAM 已经满了,它甚至可能已经被转移到硬盘上!由于这类地址不必反映内存所在的物理位置,所以它们被称为虚拟内存。操作系统维持着一个虚拟地址到物理地址的转换的表,以便计算机硬件可以正确地响应地址请求。并且,如果地址在硬盘上而不是在 RAM 中,那么操作系统将暂时停止您的进程,将其他内存转存到硬盘中,从硬盘上加载被请求的内存,然后再重新启动您的进程。这样,每个进程都获得了自己可以使用的地址空间,可以访问比您物理上安装的内存更多的内存。

    在 32-位 x86 系统上,每一个进程可以访问 4 GB 内存。现在,大部分人的系统上并没有 4 GB 内存,即使您将 swap 也算上, 每个进程所使用的内存也肯定少于 4 GB。因此,当加载一个进程时,它会得到一个取决于某个称为 系统中断点(system break)的特定地址的初始内存分配。该地址之后是未被映射的内存 —— 用于在 RAM 或者硬盘中没有分配相应物理位置的内存。因此,如果一个进程运行超出了它初始分配的内存,那么它必须请求操作系统“映射进来(map in)”更多的内存。(映射是一个表示一一对应关系的数学术语 —— 当内存的虚拟地址有一个对应的物理地址来存储内存内容时,该内存将被映射。)

    基于 UNIX 的系统有两个可映射到附加内存中的基本系统调用:

    • brk: brk() 是一个非常简单的系统调用。还记得系统中断点吗?该位置是进程映射的内存边界。 brk() 只是简单地将这个位置向前或者向后移动,就可以向进程添加内存或者从进程取走内存。
    • mmap: mmap(),或者说是“内存映像”,类似于 brk(),但是更为灵活。首先,它可以映射任何位置的内存,而不单单只局限于进程。其次,它不仅可以将虚拟地址映射到物理的 RAM 或者 swap,它还可以将它们映射到文件和文件位置,这样,读写内存将对文件中的数据进行读写。不过,在这里,我们只关心 mmap 向进程添加被映射的内存的能力。 munmap() 所做的事情与 mmap() 相反。

    如您所见, brk() 或者 mmap() 都可以用来向我们的进程添加额外的虚拟内存。在我们的例子中将使用 brk(),因为它更简单,更通用。

    实现一个简单的分配程序
    如果您曾经编写过很多 C 程序,那么您可能曾多次使用过 malloc()free()。不过,您可能没有用一些时间去思考它们在您的操作系统中是如何实现的。本节将向您展示 mallocfree 的一个最简化实现的代码,来帮助说明管理内存时都涉及到了哪些事情。

    要试着运行这些示例,需要先 复制本代码清单,并将其粘贴到一个名为 malloc.c 的文件中。接下来,我将一次一个部分地对该清单进行解释。

    在大部分操作系统中,内存分配由以下两个简单的函数来处理:

    • void *malloc(long numbytes):该函数负责分配 numbytes 大小的内存,并返回指向第一个字节的指针。
    • void free(void *firstbyte):如果给定一个由先前的 malloc 返回的指针,那么该函数会将分配的空间归还给进程的“空闲空间”。

    malloc_init 将是初始化内存分配程序的函数。它要完成以下三件事:将分配程序标识为已经初始化,找到系统中最后一个有效内存地址,然后建立起指向我们管理的内存的指针。这三个变量都是全局变量:

    清单 1. 我们的简单分配程序的全局变量
    
    
            
    int has_initialized = 0;
    
    void *managed_memory_start;
    
    void *last_valid_address;
    
          

    如前所述,被映射的内存的边界(最后一个有效地址)常被称为系统中断点或者 当前中断点。在很多 UNIX® 系统中,为了指出当前系统中断点,必须使用 sbrk(0) 函数。 sbrk 根据参数中给出的字节数移动当前系统中断点,然后返回新的系统中断点。使用参数 0 只是返回当前中断点。这里是我们的 malloc 初始化代码,它将找到当前中断点并初始化我们的变量:

    清单 2. 分配程序初始化函数
    
    
            
    /* Include the sbrk function */
    
    #include <unistd.h>
    
    void malloc_init()
    
    {
    
    	/* grab the last valid address from the OS */
    
    	last_valid_address = sbrk(0);
    
    
    	/* we don't have any memory to manage yet, so
    	 *just set the beginning to be last_valid_address
    	 */
    
    	managed_memory_start = last_valid_address;
    
    	/* Okay, we're initialized and ready to go */
    
     	has_initialized = 1;
    
    }
    
          

    现在,为了完全地管理内存,我们需要能够追踪要分配和回收哪些内存。在对内存块进行了 free 调用之后,我们需要做的是诸如将它们标记为未被使用的等事情,并且,在调用 malloc 时,我们要能够定位未被使用的内存块。因此, malloc 返回的每块内存的起始处首先要有这个结构:

    清单 3. 内存控制块结构定义
    
    
            
    struct mem_control_block {
    
    	int is_available;
    
    	int size;
    
    };
    
          

    现在,您可能会认为当程序调用 malloc 时这会引发问题 —— 它们如何知道这个结构?答案是它们不必知道;在返回指针之前,我们会将其移动到这个结构之后,把它隐藏起来。这使得返回的指针指向没有用于任何其他用途的内存。那样,从调用程序的角度来看,它们所得到的全部是空闲的、开放的内存。然后,当通过 free() 将该指针传递回来时,我们只需要倒退几个内存字节就可以再次找到这个结构。

    在讨论分配内存之前,我们将先讨论释放,因为它更简单。为了释放内存,我们必须要做的惟一一件事情就是,获得我们给出的指针,回退 sizeof(struct mem_control_block) 个字节,并将其标记为可用的。这里是对应的代码:

    清单 4. 解除分配函数
    
    
            
    void free(void *firstbyte) {
    
    	struct mem_control_block *mcb;
    
    	/* Backup from the given pointer to find the
    	 * mem_control_block
    	 */
    
    	mcb = firstbyte - sizeof(struct mem_control_block);
    
    	/* Mark the block as being available */
    
    	mcb->is_available = 1;
    
    	/* That's It!  We're done. */
    
    	return;
    }
    
          

    如您所见,在这个分配程序中,内存的释放使用了一个非常简单的机制,在固定时间内完成内存释放。分配内存稍微困难一些。以下是该算法的略述:

    清单 5. 主分配程序的伪代码
    
    
            
    
    1. If our allocator has not been initialized, initialize it.
    
    2. Add sizeof(struct mem_control_block) to the size requested.
    
    3. start at managed_memory_start.
    
    4. Are we at last_valid address?
    
    5. If we are:
    
       A. We didn't find any existing space that was large enough
          -- ask the operating system for more and return that.
    
    6. Otherwise:
    
       A. Is the current space available (check is_available from
          the mem_control_block)?
    
       B. If it is:
    
          i)   Is it large enough (check "size" from the
               mem_control_block)?
    
          ii)  If so:
    
               a. Mark it as unavailable
    
               b. Move past mem_control_block and return the
                  pointer
    
          iii) Otherwise:
    
               a. Move forward "size" bytes
    
               b. Go back go step 4
    
       C. Otherwise:
    
          i)   Move forward "size" bytes
    
          ii)  Go back to step 4
    
          

    我们主要使用连接的指针遍历内存来寻找开放的内存块。这里是代码:

    清单 6. 主分配程序
    
    
            
    void *malloc(long numbytes) {
    
    	/* Holds where we are looking in memory */
    
    	void *current_location;
    
    	/* This is the same as current_location, but cast to a
    	 * memory_control_block
    	 */
    
    	struct mem_control_block *current_location_mcb;
    
    	/* This is the memory location we will return.  It will
    	 * be set to 0 until we find something suitable
    	 */
    
    	void *memory_location;
    
    	/* Initialize if we haven't already done so */
    
    	if(! has_initialized) 	{
    
    		malloc_init();
    
    	}
    
    	/* The memory we search for has to include the memory
    	 * control block, but the users of malloc don't need
    	 * to know this, so we'll just add it in for them.
    	 */
    
    	numbytes = numbytes + sizeof(struct mem_control_block);
    
    	/* Set memory_location to 0 until we find a suitable
    	 * location
    	 */
    
    	memory_location = 0;
    
    	/* Begin searching at the start of managed memory */
    
    	current_location = managed_memory_start;
    
    	/* Keep going until we have searched all allocated space */
    
    	while(current_location != last_valid_address)
    
    	{
    
    		/* current_location and current_location_mcb point
    		 * to the same address.  However, current_location_mcb
    		 * is of the correct type, so we can use it as a struct.
    		 * current_location is a void pointer so we can use it
    		 * to calculate addresses.
    		 */
    
    		current_location_mcb =
    
    			(struct mem_control_block *)current_location;
    
    		if(current_location_mcb->is_available)
    
    		{
    
    			if(current_location_mcb->size >= numbytes)
    
    			{
    
    				/* Woohoo!  We've found an open,
    				 * appropriately-size location.
    				 */
    
    				/* It is no longer available */
    
    				current_location_mcb->is_available = 0;
    
    				/* We own it */
    
    				memory_location = current_location;
    
    				/* Leave the loop */
    
    				break;
    
    			}
    
    		}
    
    		/* If we made it here, it's because the Current memory
    		 * block not suitable; move to the next one
    		 */
    
    		current_location = current_location +
    
    			current_location_mcb->size;
    
    	}
    
    	/* If we still don't have a valid location, we'll
    	 * have to ask the operating system for more memory
    	 */
    
    	if(! memory_location)
    
    	{
    
    		/* Move the program break numbytes further */
    
    		sbrk(numbytes);
    
    		/* The new memory will be where the last valid
    		 * address left off
    		 */
    
    		memory_location = last_valid_address;
    
    		/* We'll move the last valid address forward
    		 * numbytes
    		 */
    
    		last_valid_address = last_valid_address + numbytes;
    
    		/* We need to initialize the mem_control_block */
    
    		current_location_mcb = memory_location;
    
    		current_location_mcb->is_available = 0;
    
    		current_location_mcb->size = numbytes;
    
    	}
    
    	/* Now, no matter what (well, except for error conditions),
    	 * memory_location has the address of the memory, including
    	 * the mem_control_block
    	 */
    
    	/* Move the pointer past the mem_control_block */
    
    	memory_location = memory_location + sizeof(struct mem_control_block);
    
    	/* Return the pointer */
    
    	return memory_location;
    
     }
    
          

    这就是我们的内存管理器。现在,我们只需要构建它,并在程序中使用它即可。

    运行下面的命令来构建 malloc 兼容的分配程序(实际上,我们忽略了 realloc() 等一些函数,不过, malloc()free() 才是最主要的函数):

    清单 7. 编译分配程序
    
    
    
            
    gcc -shared -fpic malloc.c -o malloc.so
    
          

    该程序将生成一个名为 malloc.so 的文件,它是一个包含有我们的代码的共享库。

    在 UNIX 系统中,现在您可以用您的分配程序来取代系统的 malloc(),做法如下:

    清单 8. 替换您的标准的 malloc
    
    
            
    LD_PRELOAD=/path/to/malloc.so
    
    export LD_PRELOAD
    
          

    LD_PRELOAD 环境变量使动态链接器在加载任何可执行程序之前,先加载给定的共享库的符号。它还为特定库中的符号赋予优先权。因此,从现在起,该会话中的任何应用程序都将使用我们的 malloc(),而不是只有系统的应用程序能够使用。有一些应用程序不使用 malloc(),不过它们是例外。其他使用 realloc() 等其他内存管理函数的应用程序,或者错误地假定 malloc() 内部行为的那些应用程序,很可能会崩溃。ash shell 似乎可以使用我们的新 malloc() 很好地工作。

    如果您想确保 malloc() 正在被使用,那么您应该通过向函数的入口点添加 write() 调用来进行测试。

    我们的内存管理器在很多方面都还存在欠缺,但它可以有效地展示内存管理需要做什么事情。它的某些缺点包括:

    • 由于它对系统中断点(一个全局变量)进行操作,所以它不能与其他分配程序或者 mmap 一起使用。
    • 当分配内存时,在最坏的情形下,它将不得不遍历 全部进程内存;其中可能包括位于硬盘上的很多内存,这意味着操作系统将不得不花时间去向硬盘移入数据和从硬盘中移出数据。
    • 没有很好的内存不足处理方案( malloc 只假定内存分配是成功的)。
    • 它没有实现很多其他的内存函数,比如 realloc()
    • 由于 sbrk() 可能会交回比我们请求的更多的内存,所以在堆(heap)的末端会遗漏一些内存。
    • 虽然 is_available 标记只包含一位信息,但它要使用完整的 4-字节 的字。
    • 分配程序不是线程安全的。
    • 分配程序不能将空闲空间拼合为更大的内存块。
    • 分配程序的过于简单的匹配算法会导致产生很多潜在的内存碎片。
    • 我确信还有很多其他问题。这就是为什么它只是一个例子!

    其他 malloc 实现
    malloc() 的实现有很多,这些实现各有优点与缺点。在设计一个分配程序时,要面临许多需要折衷的选择,其中包括:

    • 分配的速度。
    • 回收的速度。
    • 有线程的环境的行为。
    • 内存将要被用光时的行为。
    • 局部缓存。
    • 簿记(Bookkeeping)内存开销。
    • 虚拟内存环境中的行为。
    • 小的或者大的对象。
    • 实时保证。

    每一个实现都有其自身的优缺点集合。在我们的简单的分配程序中,分配非常慢,而回收非常快。另外,由于它在使用虚拟内存系统方面较差,所以它最适于处理大的对象。

    还有其他许多分配程序可以使用。其中包括:

    • Doug Lea Malloc:Doug Lea Malloc 实际上是完整的一组分配程序,其中包括 Doug Lea 的原始分配程序,GNU libc 分配程序和 ptmalloc。 Doug Lea 的分配程序有着与我们的版本非常类似的基本结构,但是它加入了索引,这使得搜索速度更快,并且可以将多个没有被使用的块组合为一个大的块。它还支持缓存,以便更快地再次使用最近释放的内存。 ptmalloc 是 Doug Lea Malloc 的一个扩展版本,支持多线程。在本文后面的 参考资料部分中,有一篇描述 Doug Lea 的 Malloc 实现的文章。
    • BSD Malloc:BSD Malloc 是随 4.2 BSD 发行的实现,包含在 FreeBSD 之中,这个分配程序可以从预先确实大小的对象构成的池中分配对象。它有一些用于对象大小的 size 类,这些对象的大小为 2 的若干次幂减去某一常数。所以,如果您请求给定大小的一个对象,它就简单地分配一个与之匹配的 size 类。这样就提供了一个快速的实现,但是可能会浪费内存。在 参考资料部分中,有一篇描述该实现的文章。
    • Hoard:编写 Hoard 的目标是使内存分配在多线程环境中进行得非常快。因此,它的构造以锁的使用为中心,从而使所有进程不必等待分配内存。它可以显著地加快那些进行很多分配和回收的多线程进程的速度。在 参考资料部分中,有一篇描述该实现的文章。

    众多可用的分配程序中最有名的就是上述这些分配程序。如果您的程序有特别的分配需求,那么您可能更愿意编写一个定制的能匹配您的程序内存分配方式的分配程序。不过,如果不熟悉分配程序的设计,那么定制分配程序通常会带来比它们解决的问题更多的问题。要获得关于该主题的适当的介绍,请参阅 Donald Knuth 撰写的 The Art of Computer Programming Volume 1: Fundamental Algorithms 中的第 2.5 节“Dynamic Storage Allocation”(请参阅 参考资料中的链接)。它有点过时,因为它没有考虑虚拟内存环境,不过大部分算法都是基于前面给出的函数。

    在 C++ 中,通过重载 operator new(),您可以以每个类或者每个模板为单位实现自己的分配程序。在 Andrei Alexandrescu 撰写的 Modern C++ Design 的第 4 章(“Small Object Allocation”)中,描述了一个小对象分配程序(请参阅 参考资料中的链接)。

    基于 malloc() 的内存管理的缺点
    不只是我们的内存管理器有缺点,基于 malloc() 的内存管理器仍然也有很多缺点,不管您使用的是哪个分配程序。对于那些需要保持长期存储的程序使用 malloc() 来管理内存可能会非常令人失望。如果您有大量的不固定的内存引用,经常难以知道它们何时被释放。生存期局限于当前函数的内存非常容易管理,但是对于生存期超出该范围的内存来说,管理内存则困难得多。而且,关于内存管理是由进行调用的程序还是由被调用的函数来负责这一问题,很多 API 都不是很明确。

    因为管理内存的问题,很多程序倾向于使用它们自己的内存管理规则。C++ 的异常处理使得这项任务更成问题。有时好像致力于管理内存分配和清理的代码比实际完成计算任务的代码还要多!因此,我们将研究内存管理的其他选择。

    半自动内存管理策略

    引用计数
    引用计数是一种 半自动(semi-automated)的内存管理技术,这表示它需要一些编程支持,但是它不需要您确切知道某一对象何时不再被使用。引用计数机制为您完成内存管理任务。

    在引用计数中,所有共享的数据结构都有一个域来包含当前活动“引用”结构的次数。当向一个程序传递一个指向某个数据结构指针时,该程序会将引用计数增加 1。实质上,您是在告诉数据结构,它正在被存储在多少个位置上。然后,当您的进程完成对它的使用后,该程序就会将引用计数减少 1。结束这个动作之后,它还会检查计数是否已经减到零。如果是,那么它将释放内存。

    这样做的好处是,您不必追踪程序中某个给定的数据结构可能会遵循的每一条路径。每次对其局部的引用,都将导致计数的适当增加或减少。这样可以防止在使用数据结构时释放该结构。不过,当您使用某个采用引用计数的数据结构时,您必须记得运行引用计数函数。另外,内置函数和第三方的库不会知道或者可以使用您的引用计数机制。引用计数也难以处理发生循环引用的数据结构。

    要实现引用计数,您只需要两个函数 —— 一个增加引用计数,一个减少引用计数并当计数减少到零时释放内存。

    一个示例引用计数函数集可能看起来如下所示:

    清单 9. 基本的引用计数函数
    
    
            
    /* Structure Definitions*/
    
    /* Base structure that holds a refcount */
    
    struct refcountedstruct
    
    {
    
    	int refcount;
    
    }
    
    /* All refcounted structures must mirror struct
     * refcountedstruct for their first variables
     */
    
    /* Refcount maintenance functions */
    
    /* Increase reference count */
    
    void REF(void *data)
    
    {
    
    	struct refcountedstruct *rstruct;
    
    	rstruct = (struct refcountedstruct *) data;
    
    	rstruct->refcount++;
    
    }
    
    /* Decrease reference count */
    
    void UNREF(void *data)
    
    {
    
    	struct refcountedstruct *rstruct;
    
    	rstruct = (struct refcountedstruct *) data;
    
    	rstruct->refcount--;
    
    	/* Free the structure if there are no more users */
    
    	if(rstruct->refcount == 0)
    
    	{
    
    		free(rstruct);
    
    	}
    
    }
    
          

    REFUNREF 可能会更复杂,这取决于您想要做的事情。例如,您可能想要为多线程程序增加锁,那么您可能想扩展 refcountedstruct,使它同样包含一个指向某个在释放内存之前要调用的函数的指针(类似于面向对象语言中的析构函数 —— 如果您的结构中包含这些指针,那么这是 必需的)。

    当使用 REFUNREF 时,您需要遵守这些指针的分配规则:

    • UNREF 分配前左端指针(left-hand-side pointer)指向的值。
    • REF 分配后左端指针(left-hand-side pointer)指向的值。

    在传递使用引用计数的结构的函数中,函数需要遵循以下这些规则:

    • 在函数的起始处 REF 每一个指针。
    • 在函数的结束处 UNREF 第一个指针。

    以下是一个使用引用计数的生动的代码示例:

    清单 10. 使用引用计数的示例
    
    
            
    /* EXAMPLES OF USAGE */
    
    
    /* Data type to be refcounted */
    
    struct mydata
    
    {
    
    	int refcount; /* same as refcountedstruct */
    
    	int datafield1; /* Fields specific to this struct */
    
    	int datafield2;
    
    	/* other declarations would go here as appropriate */
    
    };
    
    
    /* Use the functions in code */
    
    void dosomething(struct mydata *data)
    
    {
    
    	REF(data);
    
    	/* Process data */
    
    	/* when we are through */
    
    	UNREF(data);
    
    }
    
    
    struct mydata *globalvar1;
    
    /* Note that in this one, we don't decrease the
     * refcount since we are maintaining the reference
     * past the end of the function call through the
     * global variable
     */
    
    void storesomething(struct mydata *data)
    
    {
    
    	REF(data); /* passed as a parameter */
    
    	globalvar1 = data;
    
    	REF(data); /* ref because of Assignment */
    
    	UNREF(data); /* Function finished */
    
    }
    
          

    由于引用计数是如此简单,大部分程序员都自已去实现它,而不是使用库。不过,它们依赖于 mallocfree 等低层的分配程序来实际地分配和释放它们的内存。

    在 Perl 等高级语言中,进行内存管理时使用引用计数非常广泛。在这些语言中,引用计数由语言自动地处理,所以您根本不必担心它,除非要编写扩展模块。由于所有内容都必须进行引用计数,所以这会对速度产生一些影响,但它极大地提高了编程的安全性和方便性。以下是引用计数的益处:

    • 实现简单。
    • 易于使用。
    • 由于引用是数据结构的一部分,所以它有一个好的缓存位置。

    不过,它也有其不足之处:

    • 要求您永远不要忘记调用引用计数函数。
    • 无法释放作为循环数据结构的一部分的结构。
    • 减缓几乎每一个指针的分配。
    • 尽管所使用的对象采用了引用计数,但是当使用异常处理(比如 trysetjmp()/ longjmp())时,您必须采取其他方法。
    • 需要额外的内存来处理引用。
    • 引用计数占用了结构中的第一个位置,在大部分机器中最快可以访问到的就是这个位置。
    • 在多线程环境中更慢也更难以使用。

    C++ 可以通过使用 智能指针(smart pointers)来容忍程序员所犯的一些错误,智能指针可以为您处理引用计数等指针处理细节。不过,如果不得不使用任何先前的不能处理智能指针的代码(比如对 C 库的联接),实际上,使用它们的后果通实比不使用它们更为困难和复杂。因此,它通常只是有益于纯 C++ 项目。如果您想使用智能指针,那么您实在应该去阅读 Alexandrescu 撰写的 Modern C++ Design 一书中的“Smart Pointers”那一章。

    内存池
    内存池是另一种半自动内存管理方法。内存池帮助某些程序进行自动内存管理,这些程序会经历一些特定的阶段,而且每个阶段中都有分配给进程的特定阶段的内存。例如,很多网络服务器进程都会分配很多针对每个连接的内存 —— 内存的最大生存期限为当前连接的存在期。Apache 使用了池式内存(pooled memory),将其连接拆分为各个阶段,每个阶段都有自己的内存池。在结束每个阶段时,会一次释放所有内存。

    在池式内存管理中,每次内存分配都会指定内存池,从中分配内存。每个内存池都有不同的生存期限。在 Apache 中,有一个持续时间为服务器存在期的内存池,还有一个持续时间为连接的存在期的内存池,以及一个持续时间为请求的存在期的池,另外还有其他一些内存池。因此,如果我的一系列函数不会生成比连接持续时间更长的数据,那么我就可以完全从连接池中分配内存,并知道在连接结束时,这些内存会被自动释放。另外,有一些实现允许注册 清除函数(cleanup functions),在清除内存池之前,恰好可以调用它,来完成在内存被清理前需要完成的其他所有任务(类似于面向对象中的析构函数)。

    要在自己的程序中使用池,您既可以使用 GNU libc 的 obstack 实现,也可以使用 Apache 的 Apache Portable Runtime。GNU obstack 的好处在于,基于 GNU 的 Linux 发行版本中默认会包括它们。Apache Portable Runtime 的好处在于它有很多其他工具,可以处理编写多平台服务器软件所有方面的事情。要深入了解 GNU obstack 和 Apache 的池式内存实现,请参阅 参考资料部分中指向这些实现的文档的链接。

    下面的假想代码列表展示了如何使用 obstack:

    清单 11. obstack 的示例代码
    
    
            
    #include <obstack.h>
    
    #include <stdlib.h>
    
    /* Example code listing for using obstacks */
    
    /* Used for obstack macros (xmalloc is
       a malloc function that exits if memory
       is exhausted */
    
    #define obstack_chunk_alloc xmalloc
    
    #define obstack_chunk_free free
    
    /* Pools */
    
    /* Only permanent allocations should go in this pool */
    
    struct obstack *global_pool;
    
    /* This pool is for per-connection data */
    
    struct obstack *connection_pool;
    
    /* This pool is for per-request data */
    
    struct obstack *request_pool;
    
    void allocation_failed()
    
    {
    
    	exit(1);
    
    }
    
    int main()
    
    {
    
    	/* Initialize Pools */
    
    	global_pool = (struct obstack *)
    
    		xmalloc (sizeof (struct obstack));
    
    	obstack_init(global_pool);
    
    	connection_pool = (struct obstack *)
    
    		xmalloc (sizeof (struct obstack));
    
    	obstack_init(connection_pool);
    
    	request_pool = (struct obstack *)
    
    		xmalloc (sizeof (struct obstack));
    
    	obstack_init(request_pool);
    
    	/* Set the error handling function */
    
    	obstack_alloc_failed_handler = &allocation_failed;
    
    	/* Server main loop */
    
    	while(1)
    
    	{
    
    		wait_for_connection();
    
    		/* We are in a connection */
    
    		while(more_requests_available())
    
    		{
    
    			/* Handle request */
    
    			handle_request();
    
    			/* Free all of the memory allocated
    
    			 * in the request pool
    
    			 */
    
    			obstack_free(request_pool, NULL);
    
    		}
    
    		/* We're finished with the connection, time
    
    		 * to free that pool
    
    		 */
    
    		obstack_free(connection_pool, NULL);
    
    	}
    
    }
    
    int handle_request()
    
    {
    
    	/* Be sure that all object allocations are allocated
    	 * from the request pool
    	 */
    
    	int bytes_i_need = 400;
    
    	void *data1 = obstack_alloc(request_pool, bytes_i_need);
    
    	/* Do stuff to process the request */
    
    	/* return */
    
    	return 0;
    
    }
    
          

    基本上,在操作的每一个主要阶段结束之后,这个阶段的 obstack 会被释放。不过,要注意的是,如果一个过程需要分配持续时间比当前阶段更长的内存,那么它也可以使用更长期限的 obstack,比如连接或者全局内存。传递给 obstack_free()NULL 指出它应该释放 obstack 的全部内容。可以用其他的值,但是它们通常不怎么实用。

    使用池式内存分配的益处如下所示:

    • 应用程序可以简单地管理内存。
    • 内存分配和回收更快,因为每次都是在一个池中完成的。分配可以在 O(1) 时间内完成,释放内存池所需时间也差不多(实际上是 O(n) 时间,不过在大部分情况下会除以一个大的因数,使其变成 O(1))。
    • 可以预先分配错误处理池(Error-handling pools),以便程序在常规内存被耗尽时仍可以恢复。
    • 有非常易于使用的标准实现。

    池式内存的缺点是:

    • 内存池只适用于操作可以分阶段的程序。
    • 内存池通常不能与第三方库很好地合作。
    • 如果程序的结构发生变化,则不得不修改内存池,这可能会导致内存管理系统的重新设计。
    • 您必须记住需要从哪个池进行分配。另外,如果在这里出错,就很难捕获该内存池。

    垃圾收集
    垃圾收集(Garbage collection)是全自动地检测并移除不再使用的数据对象。垃圾收集器通常会在当可用内存减少到少于一个具体的阈值时运行。通常,它们以程序所知的可用的一组“基本”数据 —— 栈数据、全局变量、寄存器 —— 作为出发点。然后它们尝试去追踪通过这些数据连接到每一块数据。收集器找到的都是有用的数据;它没有找到的就是垃圾,可以被销毁并重新使用这些无用的数据。为了有效地管理内存,很多类型的垃圾收集器都需要知道数据结构内部指针的规划,所以,为了正确运行垃圾收集器,它们必须是语言本身的一部分。

    收集器的类型

    • 复制(copying): 这些收集器将内存存储器分为两部分,只允许数据驻留在其中一部分上。它们定时地从“基本”的元素开始将数据从一部分复制到另一部分。内存新近被占用的部分现在成为活动的,另一部分上的所有内容都认为是垃圾。另外,当进行这项复制操作时,所有指针都必须被更新为指向每个内存条目的新位置。因此,为使用这种垃圾收集方法,垃圾收集器必须与编程语言集成在一起。
    • 标记并清理(Mark and sweep):每一块数据都被加上一个标签。不定期的,所有标签都被设置为 0,收集器从“基本”的元素开始遍历数据。当它遇到内存时,就将标签标记为 1。最后没有被标记为 1 的所有内容都认为是垃圾,以后分配内存时会重新使用它们。
    • 增量的(Incremental):增量垃圾收集器不需要遍历全部数据对象。因为在收集期间的突然等待,也因为与访问所有当前数据相关的缓存问题(所有内容都不得不被页入(page-in)),遍历所有内存会引发问题。增量收集器避免了这些问题。
    • 保守的(Conservative):保守的垃圾收集器在管理内存时不需要知道与数据结构相关的任何信息。它们只查看所有数据类型,并假定它们 可以全部都是指针。所以,如果一个字节序列可以是一个指向一块被分配的内存的指针,那么收集器就将其标记为正在被引用。有时没有被引用的内存会被收集,这样会引发问题,例如,如果一个整数域中包含一个值,该值是已分配内存的地址。不过,这种情况极少发生,而且它只会浪费少量内存。保守的收集器的优势是,它们可以与任何编程语言相集成。

    Hans Boehm 的保守垃圾收集器是可用的最流行的垃圾收集器之一,因为它是免费的,而且既是保守的又是增量的,可以使用 --enable-redirect-malloc 选项来构建它,并且可以将它用作系统分配程序的简易替代者(drop-in replacement)(用 malloc/ free 代替它自己的 API)。实际上,如果这样做,您就可以使用与我们在示例分配程序中所使用的相同的 LD_PRELOAD 技巧,在系统上的几乎任何程序中启用垃圾收集。如果您怀疑某个程序正在泄漏内存,那么您可以使用这个垃圾收集器来控制进程。在早期,当 Mozilla 严重地泄漏内存时,很多人在其中使用了这项技术。这种垃圾收集器既可以在 Windows® 下运行,也可以在 UNIX 下运行。

    垃圾收集的一些优点:

    • 您永远不必担心内存的双重释放或者对象的生命周期。
    • 使用某些收集器,您可以使用与常规分配相同的 API。

    其缺点包括:

    • 使用大部分收集器时,您都无法干涉何时释放内存。
    • 在多数情况下,垃圾收集比其他形式的内存管理更慢。
    • 垃圾收集错误引发的缺陷难于调试。
    • 如果您忘记将不再使用的指针设置为 null,那么仍然会有内存泄漏。

    结束语
    一切都需要折衷:性能、易用、易于实现、支持线程的能力等,这里只列出了其中的一些。为了满足项目的要求,有很多内存管理模式可以供您使用。每种模式都有大量的实现,各有其优缺点。对很多项目来说,使用编程环境默认的技术就足够了,不过,当您的项目有特殊的需要时,了解可用的选择将会有帮助。下表对比了本文中涉及的内存管理策略。

    表 1. 内存分配策略的对比

    策略 分配速度 回收速度 局部缓存 易用性 通用性 实时可用 SMP 线程友好
    定制分配程序 取决于实现 取决于实现 取决于实现 很难 取决于实现 取决于实现
    简单分配程序内存使用少时较快很快容易
    GNU malloc 容易
    Hoard 容易
    引用计数 N/A N/A 非常好 是(取决于 malloc 实现) 取决于实现
    非常快 极好 是(取决于 malloc 实现) 取决于实现
    垃圾收集 中(进行收集时慢) 几乎不
    增量垃圾收集 几乎不
    增量保守垃圾收集 容易 几乎不

  • 2005-10-10

    malloc实现

    /* Take from the IBM development formum*/

    /* Include the sbrk function */
    #include <unistd.h>

    int has_initialized = 0;
    void *managed_memory_start;
    void *last_valid_address;

    void malloc_init()
    {
    /* grab the last valid address from the OS */ 
    last_valid_address = sbrk(0);    

    /* we don't have any memory to manage yet, so
      *just set the beginning to be last_valid_address
      */ 
    managed_memory_start = last_valid_address;    

    /* Okay, we're initialized and ready to go */
      has_initialized = 1;  
    }

    struct mem_control_block {
    int is_available;
    int size;
    };

    void free(void *firstbyte) {
    struct mem_control_block *mcb; 

    /* Backup from the given pointer to find the
      * mem_control_block
      */
    mcb = firstbyte - sizeof(struct mem_control_block);  
    /* Mark the block as being available */
    mcb->is_available = 1;   
    /* That's It!  We're done. */
    return;  


    void *malloc(long numbytes) {
    /* Holds where we are looking in memory */
    void *current_location;

    /* This is the same as current_location, but cast to a
      * memory_control_block
      */
    struct mem_control_block *current_location_mcb; 

    /* This is the memory location we will return.  It will
      * be set to 0 until we find something suitable
      */ 
    void *memory_location; 

    /* Initialize if we haven't already done so */
    if(! has_initialized)  {
      malloc_init();
    }

    /* The memory we search for has to include the memory
      * control block, but the user of malloc doesn't need
      * to know this, so we'll just add it in for them.
      */
    numbytes = numbytes + sizeof(struct mem_control_block); 

    /* Set memory_location to 0 until we find a suitable
      * location
      */
    memory_location = 0; 

    /* Begin searching at the start of managed memory */
    current_location = managed_memory_start; 

    /* Keep going until we have searched all allocated space */
    while(current_location != last_valid_address) 
    {
      /* current_location and current_location_mcb point
       * to the same address.  However, current_location_mcb
       * is of the correct type so we can use it as a struct.
       * current_location is a void pointer so we can use it
       * to calculate addresses.
       */
      current_location_mcb =
       (struct mem_control_block *)current_location;

      if(current_location_mcb->is_available)
      {
       if(current_location_mcb->size >= numbytes)
       {
        /* Woohoo!  We've found an open,
         * appropriately-size location. 
         */

        /* It is no longer available */
        current_location_mcb->is_available = 0;

        /* We own it */
        memory_location = current_location;

        /* Leave the loop */
        break;
       }
      }

      /* If we made it here, it's because the Current memory
       * block not suitable, move to the next one
       */
      current_location = current_location +
       current_location_mcb->size;
    }

    /* If we still don't have a valid location, we'll
      * have to ask the operating system for more memory
      */
    if(! memory_location)
    {
      /* Move the program break numbytes further */
      sbrk(numbytes);

      /* The new memory will be where the last valid
       * address left off
       */
      memory_location = last_valid_address;

      /* We'll move the last valid address forward
       * numbytes
       */
      last_valid_address = last_valid_address + numbytes;

      /* We need to initialize the mem_control_block */
      current_location_mcb = memory_location;
      current_location_mcb->is_available = 0;
      current_location_mcb->size = numbytes;
    }

    /* Now, no matter what (well, except for error conditions),
      * memory_location has the address of the memory, including
      * the mem_control_block
      */

    /* Move the pointer past the mem_control_block */
    memory_location = memory_location + sizeof(struct mem_control_block);

    /* Return the pointer */
    return memory_location;
    }

  •  上一章费那么多唇舌讨论C语言的声明,其实目的都是为了这一章,期望读者通过对C语言声明形式的详细了解,树立声明嵌套的观念,因为C语言所有复杂的指针声明,都是由各种声明嵌套构成的。如何解读复杂指针声明呢?右左法则是一个既著名又常用的方法。不过,右左法则其实并不是C标准里面的内容,它是从C标准的声明规定中归纳出来的方法。C标准的声明规则,是用来解决如何创建声明的,而右左法则是用来解决如何辩识一个声明的,两者可以说是相反的。右左法则的英文原文是这样说的:

    The right-left rule: Start reading the declaration from the innermost parentheses, go right, and then go left. When you encounter parentheses, the direction should be reversed. Once everything in the parentheses has been parsed, jump out of it. Continue till the whole declaration has been parsed.


    这段英文的翻译如下:

    右左法则:首先从最里面的圆括号看起,然后往右看,再往左看。每当遇到圆括号时,就应该掉转阅读方向。一旦解析完圆括号里面所有的东西,就跳出圆括号。重复这个过程直到整个声明解析完毕。

            笔者要对这个法则进行一个小小的修正,应该是从未定义的标识符开始阅读,而不是从括号读起,之所以是未定义的标识符,是因为一个声明里面可能有多个标识符,但未定义的标识符只会有一个。

            现在通过一些例子来讨论右左法则的应用,先从最简单的开始,逐步加深:

    int (*func)(int *p);

    首先找到那个未定义的标识符,就是func,它的外面有一对圆括号,而且左边是一个*号,这说明func是一个指针,然后跳出这个圆括号,先看右边,也是一个圆括号,这说明(*func)是一个函数,而func是一个指向这类函数的指针,就是一个函数指针,这类函数具有int*类型的形参,返回值类型是int。

    int (*func)(int *p, int (*f)(int*));

    func被一对括号包含,且左边有一个*号,说明func是一个指针,跳出括号,右边也有个括号,那么func是一个指向函数的指针,这类函数具有int *和int (*)(int*)这样的形参,返回值为int类型。再来看一看func的形参int (*f)(int*),类似前面的解释,f也是一个函数指针,指向的函数具有int*类型的形参,返回值为int。

    int (*func[5])(int *p);

    func右边是一个[]运算符,说明func是一个具有5个元素的数组,func的左边有一个*,说明func的元素是指针,要注意这里的*不是修饰func的,而是修饰func[5]的,原因是[]运算符优先级比*高,func先跟[]结合,因此*修饰的是func[5]。跳出这个括号,看右边,也是一对圆括号,说明func数组的元素是函数类型的指针,它所指向的函数具有int*类型的形参,返回值类型为int。


    int (*(*func)[5])(int *p);

    func被一个圆括号包含,左边又有一个*,那么func是一个指针,跳出括号,右边是一个[]运算符号,说明func是一个指向数组的指针,现在往左看,左边有一个*号,说明这个数组的元素是指针,再跳出括号,右边又有一个括号,说明这个数组的元素是指向函数的指针。总结一下,就是:func是一个指向数组的指针,这个数组的元素是函数指针,这些指针指向具有int*形参,返回值为int类型的函数。

    int (*(*func)(int *p))[5];

    func是一个函数指针,这类函数具有int*类型的形参,返回值是指向数组的指针,所指向的数组的元素是具有5个int元素的数组。

    要注意有些复杂指针声明是非法的,例如:

    int func(void) [5];

    func是一个返回值为具有5个int元素的数组的函数。但C语言的函数返回值不能为数组,这是因为如果允许函数返回值为数组,那么接收这个数组的内容的东西,也必须是一个数组,但C语言的数组名是一个右值,它不能作为左值来接收另一个数组,因此函数返回值不能为数组。

    int func[5](void);

    func是一个具有5个元素的数组,这个数组的元素都是函数。这也是非法的,因为数组的元素除了类型必须一样外,每个元素所占用的内存空间也必须相同,显然函数是无法达到这个要求的,即使函数的类型一样,但函数所占用的空间通常是不相同的。

            作为练习,下面列几个复杂指针声明给读者自己来解析,答案放在第十章里。

    int (*(*func)[5][6])[7][8];

    int (*(*(*func)(int *))[5])(int *);

    int (*(*func[7][8][9])(int*))[5];

            实际当中,需要声明一个复杂指针时,如果把整个声明写成上面所示的形式,对程序可读性是一大损害。应该用typedef来对声明逐层分解,增强可读性,例如对于声明:

    int (*(*func)(int *p))[5];

    可以这样分解:

    typedef  int (*PARA)[5];
    typedef PARA (*func)(int *);

    这样就容易看得多了。
  •                    来源:天极网  作者网易郑兰

            概述

      C语言中有一种长度不确定的参数,形如:"…",它主要用在参数个数不确定的函数中,我们最容易想到的例子是printf函数。

      原型:

    int printf( const char *format [, argument]... );

      使用例:

    printf("Enjoy yourself everyday!\n");
    printf("The value is %d!\n", value);

      这种可变参数可以说是C语言一个比较难理解的部分,这里会由几个问题引发一些对它的分析。

      注意:在C++中有函数重载(overload)可以用来区别不同函数参数的调用,但它还是不能表示任意数量的函数参数。

      问题:printf的实现

      请问,如何自己实现printf函数,如何处理其中的可变参数问题? 答案与分析:

      在标准C语言中定义了一个头文件<stdarg.h>专门用来对付可变参数列表,它包含了一组宏,和一个va_list的typedef声明。一个典型实现如下:

    typedef char* va_list;
    #define va_start(list) list = (char*)&va_alist
    #define va_end(list)
    #define va_arg(list, mode)\
    ((mode*) (list += sizeof(mode)))[-1]
    自己实现printf:
    #include <stdarg.h>
    int printf(char* format, …)
    {
    va_list ap;
    va_start(ap, format);
    int n = vprintf(format, ap);
    va_end(ap);
    return n;
    }

      问题:运行时才确定的参数

      有没有办法写一个函数,这个函数参数的具体形式可以在运行时才确定?

      答案与分析:

      目前没有"正规"的解决办法,不过独门偏方倒是有一个,因为有一个函数已经给我们做出了这方面的榜样,那就是main(),它的原型是:

    int main(int argc,char *argv[]);

      函数的参数是argc和argv。

      深入想一下,"只能在运行时确定参数形式",也就是说你没办法从声明中看到所接受的参数,也即是参数根本就没有固定的形式。常用的办法是你可以通过定义一个void *类型的参数,用它来指向实际的参数区,然后在函数中根据根据需要任意解释它们的含义。这就是main函数中argv的含义,而argc,则用来表明实际的参数个数,这为我们使用提供了进一步的方便,当然,这个参数不是必需的。

      虽然参数没有固定形式,但我们必然要在函数中解析参数的意义,因此,理所当然会有一个要求,就是调用者和被调者之间要对参数区内容的格式,大小,有效性等所有方面达成一致,否则南辕北辙各说各话就惨了。

      问题:可变长参数的传递

      有时候,需要编写一个函数,将它的可变长参数直接传递给另外的函数,请问,这个要求能否实现?

      答案与分析:

      目前,你尚无办法直接做到这一点,但是我们可以迂回前进,首先,我们定义被调用函数的参数为va_list类型,同时在调用函数中将可变长参数列表转换为va_list,这样就可以进行变长参数的传递了。看如下所示:

    void subfunc (char *fmt, va_list argp)
    {
    ...
    arg = va_arg (fmt, argp); /* 从argp中逐一取出所要的参数 */
    ...
    }

    void mainfunc (char *fmt, ...)
    {
    va_list argp;
    va_start (argp, fmt); /* 将可变长参数转换为va_list */
    subfunc (fmt, argp); /* 将va_list传递给子函数 */
    va_end (argp);
    ...
    }

      问题:可变长参数中类型为函数指针

      我想使用va_arg来提取出可变长参数中类型为函数指针的参数,结果却总是不正确,为什么?

      答案与分析:

      这个与va_arg的实现有关。一个简单的、演示版的va_arg实现如下:

    #define va_arg(argp, type) \
    (*(type *)(((argp) += sizeof(type)) - sizeof(type)))

      其中,argp的类型是char *。

      如果你想用va_arg从可变参数列表中提取出函数指针类型的参数,例如

    int (*)(),则va_arg(argp, int (*)())被扩展为:
    (*(int (*)() *)(((argp) += sizeof (int (*)())) -sizeof (int (*)())))

      显然,(int (*)() *)是无意义的。

      解决这个问题的办法是将函数指针用typedef定义成一个独立的数据类型,例如:

    typedef int (*funcptr)();

      这时候再调用va_arg(argp, funcptr)将被扩展为:

    (* (funcptr *)(((argp) += sizeof (funcptr)) - sizeof (funcptr)))

      这样就可以通过编译检查了。

      问题:可变长参数的获取

      有这样一个具有可变长参数的函数,其中有下列代码用来获取类型为float的实参:

    va_arg (argp, float);

      这样做可以吗?

      答案与分析:

      不可以。在可变长参数中,应用的是"加宽"原则。也就是float类型被扩展成double;char, short被扩展成int。因此,如果你要去可变长参数列表中原来为float类型的参数,需要用va_arg(argp, double)。对char和short类型的则用va_arg(argp, int)。

      问题:定义可变长参数的一个限制

      为什么我的编译器不允许我定义如下的函数,也就是可变长参数,但是没有任何的固定参数?

    int f (...)
    {
    ...
    }

      答案与分析:

      不可以。这是ANSI C 所要求的,你至少得定义一个固定参数。

      这个参数将被传递给va_start(),然后用va_arg()和va_end()来确定所有实际调用时可变长参数的类型和值。

  • [转帖]可变参数学习笔记
    前言:本文在很大程度上改编自网友kevintz的"C语言中可变参数的用法"一文,在行文之前先向这位前辈表示真诚的敬意和感谢。
    一、什么是可变参数
    我们在C语言编程中有时会遇到一些参数个数可变的函数,例如printf()函数,其函数原型为: 
    int printf( const char* format, ...); 
    它除了有一个参数format固定以外,后面跟的参数的个数和类型是可变的(用三个点"…"做参数占位符),实际调用时可以有以下的形式: 
        printf("%d",i); 
        printf("%s",s); 
        printf("the number is %d ,string is:%s", i, s);    
    以上这些东西已为大家所熟悉。但是究竟如何写可变参数的C函数以及这些可变参数的函数编译器是如何实现,这个问题却一直困扰了我好久。本文就这个问题进行一些探讨,希望能对大家有些帮助.

    二、写一个简单的可变参数的C函数 
    先看例子程序。该函数至少有一个整数参数,其后占位符…,表示后面参数的个数不定. 在这个例子里,所有的输入参数必须都是整数,函数的功能只是打印所有参数的值.
    函数代码如下:
    //示例代码1:可变参数函数的使用
    #include "stdio.h"
    #include "stdarg.h"
    void simple_va_fun(int start, ...) 

        va_list arg_ptr; 
        int nArgValue =start;
        int nArgCout=0;     //可变参数的数目
        va_start(arg_ptr,start); //以固定参数的地址为起点确定变参的内存起始地址。
        do {
           ++nArgCout;
            printf("the %d th arg: %d\n",nArgCout,nArgValue);     //输出各参数的值
            nArgValue = va_arg(arg_ptr,int);                    //得到下一个可变参数的值
        } while(nArgValue != -1);                
        return; 
    }
    int main(int argc, char* argv[])
    {
        simple_va_fun(100,-1); 
        simple_va_fun(100,200,-1); 
        return 0;
    }

    下面解释一下这些代码

    从这个函数的实现可以看到,我们使用可变参数应该有以下步骤: 

    ⑴由于在程序中将用到以下这些宏: 
        void va_start( va_list arg_ptr, prev_param ); 
        type va_arg( va_list arg_ptr, type ); 
        void va_end( va_list arg_ptr ); 
    va在这里是variable-argument(可变参数)的意思. 
    这些宏定义在stdarg.h中,所以用到可变参数的程序应该包含这个头文件.

    ⑵函数里首先定义一个va_list型的变量,这里是arg_ptr,这个变量是存储参数地址的指针.因为得到参数的地址之后,再结合参数的类型,才能得到参数的值。

    ⑶然后用va_start宏初始化⑵中定义的变量arg_ptr,这个宏的第二个参数是可变参数列表的前一个参数,即最后一个固定参数. 

    ⑷然后依次用va_arg宏使arg_ptr返回可变参数的地址,得到这个地址之后,结合参数的类型,就可以得到参数的值。

    ⑸设定结束条件,这里的条件就是判断参数值是否为-1。注意被调的函数在调用时是不知道可变参数的正确数目的,程序员必须自己在代码中指明结束条件。至于为什么它不会知道参数的数目,读者在看完这几个宏的内部实现机制后,自然就会明白。

    (二)可变参数在编译器中的处理 
    我们知道va_start,va_arg,va_end是在stdarg.h中被定义成宏的, 由于1)硬件平台的不同 2)编译器的不同,所以定义的宏也有所不同,下面看一下VC++6.0中stdarg.h里的代码(文件的路径为VC安装目录下的\vc98\include\stdarg.h)
        typedef char *  va_list;
        #define _INTSIZEOF(n) ((sizeof(n) + sizeof(int) - 1) & ~(sizeof(int) - 1) )
        #define va_start(ap,v)  ( ap = (va_list)&v + _INTSIZEOF(v) )
        #define va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
        #define va_end(ap)      ( ap = (va_list)0 )

    下面我们解释这些代码的含义:

    1、首先把va_list被定义成char*,这是因为在我们目前所用的PC机上,字符指针类型可以用来存储内存单元地址。而在有的机器上va_list是被定义成void*的

    2、定义_INTSIZEOF(n)主要是为了某些需要内存的对齐的系统.这个宏的目的是为了得到最后一个固定参数的实际内存大小。在我的机器上直接用sizeof运算符来代替,对程序的运行结构也没有影响。(后文将看到我自己的实现)。

    3、va_start的定义为 &v+_INTSIZEOF(v) ,这里&v是最后一个固定参数的起始地址,再加上其实际占用大小后,就得到了第一个可变参数的起始内存地址。所以我们运行va_start(ap, v)以后,ap指向第一个可变参数在的内存地址,有了这个地址,以后的事情就简单了。 

    这里要知道两个事情:
        ⑴在intel+windows的机器上,函数栈的方向是向下的,栈顶指针的内存地址低于栈底指针,所以先进栈的数据是存放在内存的高地址处。
        (2)在VC等绝大多数C编译器中,默认情况下,参数进栈的顺序是由右向左的,因此,参数进栈以后的内存模型如下图所示:最后一个固定参数的地址位于第一个可变参数之下,并且是连续存储的。
    |--------------------------|
    |  最后一个可变参数             |   ->高内存地址处
    |--------------------------|
    |--------------------------|
    |  第N个可变参数              |     ->va_arg(arg_ptr,int)后arg_ptr所指的地方,
    |                               |     即第N个可变参数的地址。
    |--------------- |     
    |--------------------------|
    |  第一个可变参数               |     ->va_start(arg_ptr,start)后arg_ptr所指的地方
    |                               |     即第一个可变参数的地址
    |--------------- |     
    |------------------------ --|
    |                               |
    |  最后一个固定参数             |    -> start的起始地址
    |-------------- -|       .................
    |-------------------------- |
    |                               |  
    |--------------- |  -> 低内存地址处

    (4) va_arg():有了va_start的良好基础,我们取得了第一个可变参数的地址,在va_arg()里的任务就是根据指定的参数类型取得本参数的值,并且把指针调到下一个参数的起始地址。
    因此,现在再来看va_arg()的实现就应该心中有数了:
        #define va_arg(ap,t)    ( *(t *)((ap += _INTSIZEOF(t)) - _INTSIZEOF(t)) )
    这个宏做了两个事情,
       ①用用户输入的类型名对参数地址进行强制类型转换,得到用户所需要的值
       ②计算出本参数的实际大小,将指针调到本参数的结尾,也就是下一个参数的首地址,以便后续处理。

    (5)va_end宏的解释:x86平台定义为ap=(char*)0;使ap不再 指向堆栈,而是跟NULL一样.有些直接定义为((void*)0),这样编译器不会为va_end产生代码,例如gcc在linux的x86平台就是这样定义的. 在这里大家要注意一个问题:由于参数的地址用于va_start宏,所以参数不能声明为寄存器变量或作为函数或数组类型. 关于va_start, va_arg, va_end的描述就是这些了,我们要注意的 是不同的操作系统和硬件平台的定义有些不同,但原理却是相似的.

    (三)可变参数在编程中要注意的问题 
    因为va_start, va_arg, va_end等定义成宏,所以它显得很愚蠢, 可变参数的类型和个数完全在该函数中由程序代码控制,它并不能智能 地识别不同参数的个数和类型. 有人会问:那么printf中不是实现了智能识别参数吗?那是因为函数 printf是从固定参数format字符串来分析出参数的类型,再调用va_arg 的来获取可变参数的.也就是说,你想实现智能识别可变参数的话是要通过在自己的程序里作判断来实现的. 例如,在C的经典教材《the c programming language》的7.3节中就给出了一个printf的可能实现方式,由于篇幅原因这里不再叙述。

    (四)小结: 
    1、标准C库的中的三个宏的作用只是用来确定可变参数列表中每个参数的内存地址,编译器是不知道参数的实际数目的。
    2、在实际应用的代码中,程序员必须自己考虑确定参数数目的办法,如
    ⑴在固定参数中设标志-- printf函数就是用这个办法。后面也有例子。
    ⑵在预先设定一个特殊的结束标记,就是说多输入一个可变参数,调用时要将最后一个可变参数的值设置成这个特殊的值,在函数体中根据这个值判断是否达到参数的结尾。本文前面的代码就是采用这个办法.
    无论采用哪种办法,程序员都应该在文档中告诉调用者自己的约定。
    3、实现可变参数的要点就是想办法取得每个参数的地址,取得地址的办法由以下几个因素决定:
    ①函数栈的生长方向
    ②参数的入栈顺序
    ③CPU的对齐方式
    ④内存地址的表达方式
    结合源代码,我们可以看出va_list的实现是由④决定的,_INTSIZEOF(n)的引入则是由③决定的,他和①②又一起决定了va_start的实现,最后va_end的存在则是良好编程风格的体现,将不再使用的指针设为NULL,这样可以防止以后的误操作。
    4、取得地址后,再结合参数的类型,程序员就可以正确的处理参数了。理解了以上要点,相信稍有经验的读者就可以写出适合于自己机器的实现来。下面就是一个例子

    (五)扩展--自己实现简单的可变参数的函数。
    下面是一个简单的printf函数的实现,参考了<The C Programming Language>中的156页的例子,读者可以结合书上的代码与本文参照。
    #include "stdio.h"
    #include "stdlib.h"
    void myprintf(char* fmt, ...)        //一个简单的类似于printf的实现,//参数必须都是int 类型

        char* pArg=NULL;               //等价于原来的va_list 
        char c;
        pArg = (char*) &fmt;    //注意不要写成p = fmt !!因为这里要对//参数取址,而不是取值
        pArg += sizeof(fmt);         //等价于原来的va_start          
       do
        {
            c =*fmt;
            if (c != '%')
            {
                putchar(c);            //照原样输出字符
            }
            else
    {
    //按格式字符输出数据
                switch(*++fmt) 
    {
                case 'd':
                    printf("%d",*((int*)pArg));           
                    break;
                case 'x':
                    printf("%#x",*((int*)pArg));
                    break;
                default:
                    break;
                } 
                pArg += sizeof(int);               //等价于原来的va_arg
            }
            ++fmt;
        }while (*fmt != '\0'); 
        pArg = NULL;                               //等价于va_end
        return; }
    int main(int argc, char* argv[])
    {
        int i = 1234;
        int j = 5678;
        myprintf("the first test:i=%d\n",i,j); 
        myprintf("the secend test:i=%d; %x;j=%d;\n",i,0xabcd,j); 
        system("pause");
        return 0;
    }
    在intel+win2k+vc6的机器执行结果如下:
    the first test:i=1234
    the secend test:i=1234; 0xabcd;j=5678;
  • 选择一个博客服务器

    作者: Builder.com

    Wednesday, November 17 2004 11:29 AM
    想知道如何建立完全属于自己的博客服务器吗?在本系列文章的第一篇,David McAmis带您浏览各种可能的选择方案,以及在建服务器前需要考虑的关键因素。

    博客(Blogging)在最近几年已经变得非常流行,这些天来,甚至每个人都会发表博客日志,从软件开发者到政治家,受折磨的小孩,以及他们之间 的所有人。在这篇文章里,我们要去看看打造你自己的网志服务器可能的选择方案。在我这系列的下篇文章中,我将会贯穿如何设置服务器的内容,使用一种支持 PHP 和 PERL 的开源软件。

    首先,可以考虑一系列方案选择blog服务器的平台。你需要问的第一个问题是你想要单一blog还是多作者的多重blog。然后,你需要查看你能实现什么样的服务器环境来部署你的blog服务器。你会用自己的服务器,还是使用商业 Web 主机?如果你在使用商业 Web 主机,那么他们支持什么样的平台和语言?我们发现大多数的blog服务器是基于 Linux/MySQL的,但有些也提供了其他平台的选择,包括 .NET。


    选择一个blog平台

    如果你仅仅想发布单个blog,那么就有不少快速简单的方案,且不需要你建立自己的blog服务器。这些包括了Blogger, Radio 等等服务商。大体上说,这些服务工作的机制是需要你有自己的网站或网络主机来运行你的blog——这些服务也提供了前端程序供你提交blog,或是将 blog文件上传至 Web 服务器。这通常是最简单的解决方案,也有最多人选择此路。

    如果你需要更多的灵活性,或者是想要完全控制自己的blog,同时又已经有自己的 Web 服务器或是已托管于某处,那你就已经准备好设置自己的blog服务器了。现在有两种不同类型的blog服务器,取决于你将blog内容存在数据库中还是作为文件放在服务器上。

    一个基于文件的blog服务器是最容易部署的,除了对存放你数据文件的目录读写权限的设置,不需要任何其他的设置步骤。

    如果你有一个复杂些的主机环境,包含了对数据库如MySQL的访问,你可以想考虑部署一个使用数据库来维护后端blog存储的blog服务器。这种 类型的实现稍微复杂一些,同时需要你对数据库平台的基本了解,以及使各个组件正常工作需要的额外设置步骤。记住,我们在这篇文章中涉及到的多数blog服 务器也是开源或自由软件,因此有时候安装过程不好理解,相关文档通常也很少。

    当你决定了是使用基于文件的还是数据库的解决方案后,下一步该考虑的就是平台的部署。Blog服务器往往是用跨平台的语言写成的(如 PHP, Perl 等等),但应当留心,这些服务器软件可能只是为特定平台创建的(通常是 Linux)。

    所以,比如说,如果你选择在 Windows 平台上部署blog服务器,你可能因为其安装步骤是针对 Linux的而在安装上多花些时间。这并不意味着在 Windows 平台上部署这些方案是不可行的,只是说可能会多花一些时间或步骤。


    几种主要的Blog服务器

    Blosxom是一个轻量级的,多平台的blog服务器,可以用来维护多重blog而不需要繁杂的步骤。

    环境需求:Web 服务器(Apache, IIS 等等),Perl

    优势:容易安装和配置,不需要数据库服务器支持。

    劣势:是面向小型开发团队或开发者的。

    访问http://www.blosxom.com/ 获取更多信息。

    Pivot

    Pivot 是一个基于 Web 的blog服务器,非常容易安装和使用,可以维护多重blog,是避免使用后端数据库的良好替换方案。

    环境需求:Web Server(Apache 或 IIS)以及 PHP

    优势:智能化的特性,没有数据库后端。

    劣势:大型blog的重建过程可能十分冗长。

    访问http://www.pivotlog.net/ 以获得更多信息。

    http://www.gotdotnet.com/workspaces/workspace.aspx?ID=E99FCCB3-1A8C-42B5-90EE-348F6B77C407

    .Text(Beta)

    使用 C# 和 ASP.NET 在 .NET 平台建立的blog服务器。

    环境需求:IIS,.NET 框架,SQL Server 或 MSDE。

    优势:在 Microsoft 框架中运行。

    劣势:很难安装设置,缺少文档支持。

    更多信息:

    http://www.gotdotnet.com/workspaces/workspace.aspx?ID=E99FCCB3-1A8C-42B5-90EE-348F6B77C407


    LiveJournal

    Blog网站 LiveJournal.com 后台使用的流行的开源框架。

    环境需求:Perl, MySQL, Apache, mod_perl


    优势:为用户和开发者设置完好,灵活的框架。

    劣势:很难定制,代码量大。

    更多信息:http://www.livejournal.com/developer/

    Nucleus

    是一个blog以及新闻发布站点框架,提供了很多插件以扩展其功能(包括聊天,日志跟踪,等等)。

    环境需求:Web 服务器 (Apache, IIS 等),PHP,MySQL

    优势:很多特性可设置并很容易定制;

    劣势:插件可能很难安装和配置。

    更多信息:http://nucleuscms.org/

     WordPress

    WordPress发布系统是一个建立在 PHP 和MySQL上,基于 GPL 协议的自由软件。

    环境需求:Web 服务器,PHP,MySQL

    优势:流行的框架,强有力的开发者群体

    劣势:可能难于安装和运行。

    更多信息:http://wordpress.org/

  • http://www.embeddedcore.com介绍嵌入式和DSP相关很多技术,非常不错
  • 虽然一直比较懒于关注网络方面的东西,但是最近上网的时候怎么躲也躲不开这些信息了。web2.0的介绍,应用越来越广泛,这几天草草的google了几把,发现它是个好宝贝,几乎已经快成了新一代网络的方向了。未来终将是网络的时代。不管是人际网络还是虚拟网络,它们的能量都是很可怕的,一但集合在一起,就可以发挥出几千上万的力量。

    我给自己列了个要学习的清单,督促自己快快加油:

    wiki技术

    blog技术: movable type, wordpress,textPattern,instiki

    content management 技术

    Ruby,perl 相关技术

  • 2005-10-08

    小弟

    小弟是和我一起实习的小孩,他是来自山东,有着传统山东大汉的形象,身高1.8,体重160斤。咋一看的时候,觉得威风凛凛,不过仔细一瞅,却发现他长得十分可爱:小小的眼睛,圆圆的脸,傻傻的表情。除了长相可爱以外,他讲话也很有意思,很多小小的故事不时的挂在他的嘴边,还有很多让人意料不到的笑话,他自己没有什么感觉,而周围的人都已经开心的不行。也许他的幽默是天生的。除此以外,最让我欣赏的还是他处事做人的态度。对于生活,他总是乐观的。不管发生什么事情,他都是乐呵呵的,“总会有解决的办法的”,“没关系”。对于身边的人,不管是谁,都很认真的对待,当作朋友来对待。所以现在他在这个陌生的城市已经有了很多朋友,真是让我很佩服。

  • 2005-09-25

    mm红红的脸

    mm昨天在太阳下面做了一天的 培训,脸上,两只手和脖子的皮肤都被晒伤了。开始没有注意,后来在灯光下才发现,红的吓人,又火辣辣的痛,我一下没了注意。还好有A牌的护肤霜,不管三七二十一,mm摸了半天,然后自己对自己说,好多了。我看着她,我小声嘀咕:可以当红灯了。。。啪的一声,然后是某人的惨叫。。。阿门,我一晚上不敢再提.

  • 2005-09-24

    朋友

    今天是周末,无事可干,也不想做什么事情。不知道这算不算浪费生命。尽管自己还有毕业设计论文,保鲜万字决心书,还有简历需要去完成。

    无意中点到中小弟的网页www.herorob.com ,心情好了不少。这家伙一如既往写着他的流水帐,记录着他生活的影子。什么时候变的这么丰富多彩了?呵呵。他的心就象阳光一样。不晓得上天有没有把他的另一半带给他啊?真想见见,然后进行打击...

    还有那个nesta,(汗,一直搞个这样的nickname),整体为两个mm消的人憔悴,真是有失大哥风范啊。可惜我们相距太远,要不然兄弟陪你喝酒,醉了就一切都忘了。没有忘不了的爱,只有你放不放的下。不过现在好了,好好待你的peipei哈。嘿嘿

    哈哈,想这些坏家伙了。这里连个踢球的据点都没有,英雄也只能仰天而叹了。上次去交大踢球,竟然还被管理的老头驱赶,说是只有交大的学生才能来这里,否则交钱。不管这样,好心情被破坏的一干二净。

  •     FileSet是一个文件组.这些文件可在以根目录开始的目录树里找到,符合PatternSet和Selector设定的模式.
    FileSet在支持这种特性的任务里出现,或者与目标同一级出现,就像项目的子元素.
       PatternSet可以在内嵌元素<patternset>定义.另外,FileSet包含一个隐性PatternSet,并且直接支持PatternSet
    内嵌的<include>,<includesfile>,<exclude>和<excludesfile>元素.
       Selectors像FileSet的内嵌元素一样在FileSet里出现.如果在FileSet里的任意一个selectors都没有选择文件,那么该文件
    就不被认为是FileSet的一部分.这使得FileSet与<and>的selector容器是一样的功能.
       FileSet有8个属性
       1)dir 2)defaultexcludes 3)includes  4)includesfile  5)excludes  6)excludesfile  7)casesensitive  8)followsymlinks
    注意:

    例子
      <fileset dir="${server.src}" casesensitive="yes">
         <include name="**/*.java"/>
         <exclude name="**/*Test*"/>
      </fileset>
      集合在${server.src}目录下所有的JAVA源文件,但排除文件名包含"Test"的文件.
      <fileset dir="${server.src}" casesensitive="yes">
         <patternset id="non.test.sources">
            <include name="**/*.java"/>
            <exclude name=**/*Test*/>
         </patternset>
      </fileset>
      集合在${server.src}目录下所有的JAVA源文件,但也创建了可以在其他<fileset>元素(在不同的目录里)里引用的PatternSet
      <fileset dir="${client.src}">
         <patternset refid="non.test.sources">
      </fileset>
      集合在${client.src}目录下所有文件,使用与上面例子一样的模式.
      <fileset dir="${server.src}" casesensitive="yes">
         <filename name="**/*.java"/>
         <filename name="**/*Test*" negate="true"/>
      </fileset>
      集合在${server.src}目录下所有文件,但使用了<filename> selector.
      <fileset dir="${server.src}" casesensitive="yes">
         <filename name="**/*.java"/>
         <not>
            <filename name="**/*Test*"/>
         <not>
      </fileset>
      集合在${server.src}目录下所有文件,这个动作使用<filename>selector和<not>selector容器.
  • 2005-09-23

    准备找工作啦

    时间过的真是快,研究生三年就已经快结束了,我也该振作精神,开始人生的另一个旅程。从小学开始到现在,略略算了下,竟然有18年在读书。古人十年寒窗苦读和我们比较起来,也就稀松平常了。但是转念一想,人家古人十年可以考到举人,进士甚至中个榜眼撞个状元什么的,我们苦读十几年出来找个工作却急的满头是包。我想起小学时,老师说考上大学就自由了,就相当于进士了。等读了大学发现老师说的是一个善意的谎言。那年少说也有100万兄弟姐们和我一起在大学中奋斗,我想最多我们就算个秀才了。

    稀里糊涂的混到大肆,发现自己大学没有什么成就感,于是赶上时候考了一把研究生,我的学生生涯就这样又延续了三年。这三年在课堂里度过的时间真的不算太多,可惜了我的英语口语课,因为在美女众多的课堂上,我大部分时候都在mute。没有表现,美女怎么可能注意你嘛。最悲惨的是,最后上课结束以后,竟然一个美女也没有认识。失败啊。

    不光是英语课遗憾,整个研究生阶段都很郁闷,我就常想研究生怎么就这么难认识女生啊...还有半年,加油了,恩。

    晕,怎么一下blog就天马行空了,思路跳跃的太快了。我不是主要在思考如何找工作么。汗~~

    我思考,思考,再思考~如何让自己的人生实现价值。但是什么是人生价值呢?人生又是什么了?这些问题一个个在我脑袋里打转。我试图解释一个问题的时候,发现另一个问题又需要先解释,最后自己也无可奈何。人生也许就是这样,你可以考虑,但没有选择。

    休息,明天再想。

  • 2005-09-16

    java step one

    因为工作的原因需要使用java构建一个系统,于是打算最近一段时间全部用来学习java. 完全从零起步哟.

    简单翻了一下java的语法,就是一般的控制流和一些oo的东西,先不管它.不过发现写程序的时候需要理解其他的东西,比如package,classpath,import,它的名字空间管理,已经常用的类库.汗,东西比我想象的多多了.还是我的c好,简洁高效.

    不过还是要硬着头皮上啊,今天目标完成HelloWorld,呵呵

  • btw: 改了一个问题,估计是网页的原因:

    在jsp调用中:String ip=" www.sina.com.cn"; //待Ping的地址

    www前面有个空格,原来好像没有。

    利用Java调用可执行命令实例

    package test;


    /**
    * Add one sentence class summary here.
    * Add class description here.
    *
    * @author lxx
    * @version 1.0, 2004-11-16
    */
    public class TestCmd {
    public TestCmd(){}
    /* public void main(String args[]){
    try {
    Process process = Runtime.getRuntime().exec("cmd.exe /c start http://www.csdn.net"); //登录网站
    Process process = Runtime.getRuntime().exec("cmd.exe /c start ping 10.144.98.100"); //调用Ping命令
    }catch (Exception e)
    {
    e.printStackTrace();
    }

    }
    } */

    //在项目下建立一个名为hello的文件夹
    public static void main(String[] args) {
    System.out.println(System.getProperty("user.dir"));
    createFolder("hello");
    }

    private static void createFolder(String folderName) {
    String temp = System.getProperty("user.dir") + java.io.File.separator+ folderName;
    java.io.File f = new java.io.File(temp);
    f.mkdirs();
    }

    }



    在Java程序中获取当前运行程序的路径

    import java.io.*;

    public class Test {

    public static void main(String[] args) {

    File directory = new File(".");

    try {

    File newPath = new File(directory.getCanonicalPath()+"NewFolder");

    newPath.mkdir();

    }catch(Exception exp)

    {

    exp.printStackTrace();

    }

    }

    }

    //File directory = new File(".");

    //directory.getCanonicalPath();取得当前路径



    在Jsp页面中调用Ping命令---PingIP.jsp
    <%@ page language="java" contentType="text/html; charset=gb2312" import="java.io.*" %>
    <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
    <html>
    <head>
    <title>Ping IP测试页面</title>
    </head>

    <body>
    <div align="center">
    <h2>Ping IP测试页面</h2>
    </div>

    <%
    Runtime runtime = Runtime.getRuntime();
    Process process =null;
    String line=null;
    InputStream is =null;
    InputStreamReader isr=null;
    BufferedReader br =null;
    String ip=" www.sina.com.cn"; //待Ping的地址
    try
    {
    process =runtime.exec("ping "+ip);
    is = process.getInputStream();
    isr=new InputStreamReader(is);
    br =new BufferedReader(isr);
    out.println("<pre>");
    while( (line = br.readLine()) != null )
    {
    out.println(line);
    out.flush();
    }
    out.println("</pre>");
    is.close();
    isr.close();
    br.close();
    }
    catch(IOException e )
    {
    out.println(e);
    runtime.exit(1);
    }
    %>

    </body>
    </html>