加入VIP
  • 专属下载特权
  • 现金文档折扣购买
  • VIP免费专区
  • 千万文档免费下载

上传资料

关闭

关闭

关闭

封号提示

内容

首页 系统程序员成长计划

系统程序员成长计划.pdf

系统程序员成长计划

ssagui
2010-06-25 0人阅读 举报 0 0 暂无简介

简介:本文档为《系统程序员成长计划pdf》,可适用于IT/计算机领域

写作背景在经历过几个大型的失败的项目之后我终于明白没有什么比高素质的程序员更能决定项目的成功了无论什么过程什么编程语言和开发工具离开了高素质的程序员什么都是白费力气。毫无疑问人是软件开发中最重要的因素但不是每个人都重要不是什么样的人都重要只有那些高素质的程序员和那些对项目有突出贡献的人才是重要的。不过高素质的程序员并不多见所以从我开始带人之际就在思考团队成员培养的问题。我做过很多尝试从小组内学习到整个部门一起上大课最后又回到对个人单独的辅导从通过codreview做现场教育到制定过一个宏伟的培训计划最后又回到一个朴素的培训过程。其中遇到了很多问题开始是培训不够系统效果不甚理想后来又因为计划过于”宏伟”而无法实施直到最后行成一个朴素的切实可行的培训方案中间经过了好几年时间直到去年整个计划才趋于完善。我把这个培训计划称为系统程序员成长计划这就是我在这个系列中要写的。培训内容不是来源于某本书毕业八年来我坚持不断的看书家中放了大储物箱有多本不同类型的书籍其中囊括了大部分经典的IT图书。当然也不是全部内容都来源书本这几年我在开源软件吸取了大量的营养一些思想和经验在broncho项目中也有充分的发挥可以说是理论经验和实践的结合。但我不尝试阐述什么高深的道理相反我是针对应届毕业生和业余爱好者写的目的是要让初学者进阶为一个专业的程序员。为什么叫系统程序员成长计划?程序员的范围太广了,虽然软件开发有很多相似之处但是隔行如隔山比如对于目前炙手可热的WEB开发我完全是外行。想什么都讲一点结果是什么都没有讲清楚所以我得把培训计划限定在我熟悉的范围之内。先定义一下系统程序员:从事操作系统内核、DBMS、GUI系统基础函数库应用程序框架编译器和虚拟机等基础软件开发的程序员。这些培训同样适用于桌面软件和智能手机软件开发我想对其它软件开发也会有一些启发作用。草莓酱定律与果酱定律。第一次在咨询的奥秘中看到草莓酱定律时我觉得非常有意思。当然这个系列也无法脱离草莓酱定律的魔法利用这个系列中的内容我手把手的教了十多个同事收到了良好的效果。当有数百个读者读这些文章时我不敢期望有同样的效果。不过在果酱定律的鼓励下我相信这个系列中至少有部分内容的价值不会因为读者群的增大而消失所以最终决定写出来。中心思想软件开发的困难在哪里?对于这个问题不同的人有不同的答案同一个人在不同职业阶段也会有不同的答案。作为一个系统程序员来说我认为软件开发有两大难点:一是控制软件的复杂度。软件的复杂度越来越高而人类的智力基本保持不变如何以有限的智力去控制无限膨胀的复杂度?我经历过几个大型项目也分析过不少现有的开源软件我得出一个结论:没有单个难题和技术细节是我们无法搞定的而所有这些问题出现在一个项目中时其呈指数增长的复杂度往往让我们束手无策。二是隔离变化。用户需求在变化应用环境在变化新技术不断涌现所有这些都要求软件开发能够射中移动的目标。即使是开发基础平台软件在超过几年时间的开发周期之后需求的变化也是相当惊人的。需求变化并不可怕关键在于变化对系统的影响如果牵一发而动全身一点小小的变化可能对系统造成致命的影响。为了解决这两个问题方法学家们几十年来不断努力他们发明或改进软件的开发过程和设计方法。系统程序员面对的基础软件通常都是大型的复杂的软件其通用性也要求能容纳更多变化解决这两个问题也是系统程序员成长计划的主要目标。文章特色以引导读者思考为主。培训可以制造合格的程序员却无法造就一流的高手。培训是一个被动的过程我们都知道在大学里听课的效果我不希望本系列文章的成为单纯的培训教材相反我们要变被动为主动最大限度的提高学习的效果。大多数情况下我先提出问题让读者去思考让读者自己尝试去解决能不能解决这个问题不重要重要的是在思考中提升自己。如果读者在一定时间无法找到解决问题的方法后面会有专业程序员的参考做法(或许不是最优的)。以简单的例子讲述复杂的设计方法。我曾经制定一个宏伟的培训计划结果失败了原因是我忘记了我在走路之前也艰难的爬行过。这次我吸取了教训用简单的示例讲述复杂的设计方法而且不对读者的背景太多假设。里面不会出现复杂的数据结构和算法也不会引入大型软件来唬人。即有足够的挑战不会让读者感到乏味又一切尽在掌握之中不会因为挫折而打击积极性。技术能力与工作态度并重。古人说德才兼备者才是君子。同样做一流的程序员也要德才兼修才行。当我手把手的教别人的时候我不但希望他能学会我讲的知识点更希望他能学习我的工作态度和作为程序员的道德素养。当然有些东西只可意会不可言传未必能用文字表达出来不管怎样这是我的初衷之一。读者群这个系列完全是针对初学者写的初学者包括在校学生、应届毕业生和其他业余爱好者。我面试过很多应届毕业生他们大多数都不具备编程能力唯一的优势就是对基本理论有一知半解的了解。本系列文章就是为他们量身定制的经历十几个人的实践取得令人满意的效果大多数人在开始一行代码都写不出到培训结束时一般都能独立开发维护一些几千行的小模块。本系列文章并不是金钥匙最终学习的效果与个人的悟性和努力密切相关但不管怎样只要读完这些文章你都会有不小收获的。如何使用温伯格说过医生的药方包括药物和服药的方法两者缺一不可。同样的教材不同的学习方法效果也有很大差别。对于本系列文章的服用方法我的建议是对于文中提出的问题先自己想办法去解决可以查资料至少经过两三个小时的思考之后再继续阅读最后再按学到的方法独立写一遍学习编程一定要多写多练否则效果会大打折扣。Enjoyit!对于是否写这样一章我犹豫了很久最后考虑到这个系列是针对新手而写的不应该对读者做过多假设这些基础知识是必须掌握的不能不介绍一下。如果你已经了解它们可以放心的跳过本章。如果你是新手请认真学习本章提到的内容。基础知识C语言。千万不要认为C语言过时了它始终是开源社区特别是系统软件和嵌入式系统中的王者在可以预见的未来C语言将持续焕发出生命力。有些外行认为C语言不适合开发大型软件这是大错特错了操作系统内核虚拟机数据库管理系统图形引擎和WEB服务器等大型软件几乎都是用C语言开发的。相反C语言不适合开发小程序这时候脚本语言更能显出威力。C语言能经久不衰自有它的道理:C语言是最简单的语言之一大部分编程语言在出现时都以其简单而获得好评几乎全部都随着时间的推移变得越来越复杂C语言经过数十年的发展却始终保持其简洁和优美。初学者认为C语言难学其实主要是对计算机本身不理解花点时间去学习一下计算机组成原理和操作系统原理再来学习C语言就很简单了。一旦掌握了它你会发现C语言的每项特性都是必须常用的根本不需要记忆任何不必要的东西它的特性真是减无可减了。C语言是运行时效率最高的编程语言之一。同样的算法C语言通常比其它语言更高效这也它作为系统软件主流编程语言的原因之一。有些动态语言号称比较C语言更快那都是骗人的拿一个特定算法作为例子不足为证。选择是高效的算法是根本但C语言更能把高效发挥到极致。C语言是最直观的语言之一。C语言能够直观的表达程序员的想法不像其它一些语言一行简单的代码你不清楚里面到底做了什么不清楚它将花多少时间执行。C语言的直观性很好的满足了程序员好奇心使用C语言你更能感觉编程是一种艺术。一切尽在掌握之中更能满足你的成就感。在系统程序员炼成计划中前面部分都是使用C语言作为示例读者应该找本C语言入门书籍看看可以先通读一遍不求甚解都可以随着后面的课程而深入的学习。数据结构与算法。不管使用什么设计方法和开发过程数据结构与算法都是软件开发的基石。打好基础在以后的工作中会事半功倍。后继课程也都是这些基本数据结构和算法为中心讲述如何用这些基本的材料构建大型系统。读者暂时无需精通数据结构和算法先找本书看看了解一下像双向链表、动态数组、队列、堆、栈、hash表、排序和查找的基本原理就行了后面我们会以这些数据结构题材反复的练习。开发环境本系列文章重点讲解软件开发的基础知识这些知识不依赖于特定的平台和开发环境读者可以根据自己喜好来选择我们推荐读者使用下列开发环境:操作系统使Linux。Linux是最适合程序员使用的操作系统它是开源的有多种不同的发行版可以免费使用这些发行版默认安装就带了开发工具。学习Linux本身就需要一本书如果你从来没接触过Linux也不用惊慌花几个小时学会十来个常用的命令就够了其它的以后慢慢再学。编辑器使用VIM。编辑器的功能是创建源文件也就是把我们编写的代码输入到电脑中。vim和emacs是Linux下最流行的代码编辑器vim入门更简单功能也很强大。它支持查找剪切替换等基本编辑功能也支持符号跳转和代码补全等高级编辑特性。vimtutor是最好的入门教材初学者跟着这个tutor学习一遍就可以用它来编程了等用得比较熟练之后再去掌握那些高级功能。你掌握得越熟练你就能更高效的工作这个投资是值得的。编译器使用gcc。编译器的功能是把源代码翻译成计算机可以“读懂”的机器语言。在Linux下可用的C编译器有好几个gcc是其中最流行的大多数发行版都默认安装了gcc。gcc的参数很多看起来很复杂我们只掌握最简单的用法就好了大概像这样的:gccgtestcotest。调试器使用gdb。调试器的功能是帮助程序员定位错误这是最后一招也是最不期望的一招使用调试器越多通常说明你的水平越差不过对初学者来说掌握这个工具必不要可少的。gdb的功能强大推荐读者使用命令行的gdb它更灵活更方便。读者先掌握如何设置断点、显示变量和继续执行等基本操作就行了。工程管理使用make。make是Linux下最流行的工程管理工具Makefile是make的输入文件它本身就相当于一种编程语言执行make相当于调用其中的函数。编写Makefile是一件繁琐无趣的工作幸好我们不用学习它后面我们会讲解make的改进版automake现在你能写出下面这种简单的Makefile就行了:all:gccgtestcotestclean:rmftest在这里你可以把all看作一个函数名gccgtestcotest是函数体(前面加tab)它的功能是编译testc成test在命令行运行makeall就相当于调用这个函数。clean是另外一个函数它的功能是删除test。如果你有时间学习一下Makefile当然更好如果没有时间了解这么多也够了。我在培训初学者时如果他从来没用过Linux没有用C语言写过程序我会给两到四周时间学习上述内容。如果读者处于类似的水平也不急着看后面的课程好好学习一下这里提到的内容。需求简述用C语言编写一个双向链表。如果你有一定的C语言编程经验这自然是小菜一碟。有的读者可能连一个小程序都没有写过那也不用害怕可以参考任何一本《数据结构》和C语言的书籍。先弄明白基本概念把书上的代码看明白再把代码抄到电脑里保证编译过去调试它到正常运行。反复这个过程直到你能独立完成它为止。写第一行代码是很痛苦的我培训过好几个同事他们不是计算机系毕业的开始在电脑前坐一整天一行代码都敲不出来我最早写程序时的情况也好不了多少不过没有关系迈出这一步就好了。花天时间完成这个任务后再继续往下阅读。当你读到这里的时候相信你已经独立写出了一个双向链表。恭喜你!迈出这一步可是值得庆祝的现在你已经走在通往程序员的光明大道上了。不过你还是个业余程序员那当然了你才写出第一个程序呢!什么时候才能成为一个专业程序员呢?三年还是五年工作经验?其实不用的你马上就可以了我没有骗你因为专业程序员与业余程序员之分主要在于一种态度如果缺乏这种态度拥有十年工作经验也还是业余的。什么态度?专业态度!也就是星爷常说的专业精神。专业态度有多种表现形式以后我们会一一介绍的。这里先介绍一下有关形象的态度专业的程序员是很注重自己的形象的当然程序员的形象不是表现在衣着和言谈上而是表现在代码风格上代码就是程序员的社交工具代码风格可是攸关形象的大事。有人说过傻瓜都可以写出机器能读懂的代码但只有专业程序员才能写出人能读懂的代码。作为专业程序员每当写下一行代码时要记得程序首先是给人读的其次才是给机器读的。你要从一个业余程序员转向专业程序员就要先从代码风格开始并从此养成一种严谨的工作态度生活上的不拘小节可不能带到编程中来。代码风格有很多种Windows和Linux都有自己主流的代码风格每个团队每个公司也可能有自己的代码风格争论哪种风格好那种风格坏没有什么意义。只要有助于其他程序员理解的代码风格都是可以接受的因为遵循特定代码风格的目的就是为了便于交流。这里介绍一下作者本人喜欢的代码风格这种代码风格也在作者所在团队中使用。这里的命名风格与GTK代码相近排版风格Linux内核代码相近。命名要展示对象的功能。文件名:单词小写多个单词用下划线分隔。如:dlistc(这里d代表double是通用的缩写方法)注意:文件名一定要能传达文件的内容信息别人一看到文件名就是知道文件中放的是什么内容。只把一个类或者一类的代码放在一起是好的习惯这样就很容易给文件取一个直观的名字。业余爱好者常常把很多没关系的代码糅到一个文件中结果造成代码杂乱无章也很难给它取一个恰当的名字。函数名:单词小写多个单词用下划线分隔。如:findnode注意:同样一个函数只完成单一功能不要用代码的长度来衡量是不是要把一段代码独立成一个函数。即使只有几行代码只要它完成的是一项独立的功能都应该提为一个单独的函数而函数名可以直观的反应出它的功能。如果在给函数起名时遇到了困难通常是函数设计不合理应该仔细思考一下。结构枚举联合名:首字母大写多个单词连写。如:structDListNode宏名:单词大写多个单词下划线分隔如:#defineMAXPATH变量名:单词小写多个单词下划线分隔。如:DListNode*node=面向对象的命名方式:以对象为中心采用主语(对象)谓语(动作)取代传统的谓语(动作)宾语(目标)。如:dlistappend第一个参数为对象并用thiz命名。如:dlistappend(DList*thiz,void*value)对象有自己的生命周期都有create和destroy函数。排版布局要美观大方。合理使用空行:函数体之间用空行分隔。结构联合枚举声明空行分隔。不同功能的代码块之间用空行分隔。类似的代码放在一起和其它部分用空行分隔。比如宏定义类型定义函数声明和全局变量放在一起。使用空行时一行就够了不要使用连续多个空行那样让人感觉空荡荡。合理使用空格:等号两边用空格。如:如:inta=参数之间用空格。如:如:test(inta,intb,intc)语句末的分号与前面内容不要加空格。如:test(a,b,c)其它有助让代码更美观的地方。合理使用括号:用括号分隔子表达式不要只靠默认优先级来判断。如:((ab)||(cd))用括号分隔ifwhilefor等语句的代码块那怕代码只有一行。如:if(a>b){returnc}合理的缩进方式:每一级都正常缩进用tab缩进取代空格缩进(Linuxkernel也遵循此规则)。用空格缩进的目的是防止代码因编辑器的tab宽度不同而变乱这个担心现在是多余的了代码编辑器都支持tab宽度设置了。如果缩进的居次太多(比如超过三层)可能是代码设计上出了问题。如:if(a>b){for(i=i<i){…}}遵从团队的习惯。这个是最重要的一个团队就要像一个团队的样子不管你的水平有多高遵循团队的规则是一个程序员的基本素养。如果团队的规则确实不好大家应该一起完善它。做到这一点你已经走近专业程序员了重新做一遍练习吧。随着后面的学习你就可以真正走进专业程序员这个行列了。需求简述:或许你还在欣赏用良好代码风格重新编写的双向链表看起来不错不是吗?不过这还远远不够专业程序员要有精益求精的精神。至于要精到什么程度与具体需求有关如果只是写个小程序验证一下某个想法那完成需要的功能就行了如果是开发一个基础程序库那就要考虑更多了。侯捷先生说过学从难处学用从易处用。这里我们是学习就要精得不能再精为止精到钻牛角尖为止。在后面的几章中我们将对这个双向链表进行持续的重构这个过程是循序渐近的请读者不要着急稳扎稳打的学习是才最好的。在这一节中我们要学习的是程序的封装性请读者思考下面几个问题:.什么是封装?.为什么要封装?.如何实现封装?花上半小时去思考尝试回答上述问题然后按你的想法重写双向链表(多写多练不要偷懒)。什么封装?人有隐私程序也有隐私。有隐私不是什么坏事没有隐私人就不是人了程序也不成其为程序了。问题是隐私不应该让别人知道否则伤害的不仅仅是自己相关人物也会跟着倒霉“艳照门”就是个典型的例子。程序隐私的暴露造成的伤害不一定有“艳照门”大也不一定比它小反正不要小看它就行了。封装就是要保护好程序的隐私不该让调用者知道的事就坚决不要暴露出来。为什么要封装?总体来说封装主要有以下两大好处(具体影响后面再说):隔离变化。程序的隐私通常是程序最容易变化的部分比如内部数据结构内部使用的函数和全局变量等等把这些代码封装起来它们的变化不会影响系统的其它部分。降低复杂度。接口最小化是软件设计的基本原则之一最小化接口容易被理解和使用。封装内部实现细节只暴露最小的接口会让系统变得简单明了在一定程度上降低了系统的复杂度。如何封装?隐藏数据结构暴露内部数据结构会使头文件看起来杂乱无章让调用者发蒙。其次是如果调用者图方便直接访问这些数据结构的成员会造成模块之间紧密耦合给以后的修改带来困难。隐藏数据结构的方法很简单如果是内部数据结构外面完全不会引用则直接放在C文件中就好了千万不要放在头文件里。如果该数据结构在内外都要使用则可以对外暴露结构的名字而封装结构的实现细节做法如下:在头文件中声明该数据结构。如:structLrcPooltypedefstructLrcPoolLrcPool在C文件中定义该数据结构。structLrcPool{sizetunitsizesizetnpreallocunits}提供操作该数据结构的函数哪怕只是存取数据结构的成员也要包装成相应的函数。如:void*lrcpoolalloc(LrcPool*thiz)voidlrcpoolfree(LrcPool*thiz,void*p)提供创建和销毁函数。因为只是暴露了结构的名字编译器不知道它的大小(所占内存空间)外部可以访问结构的指针(指针的大小的固定的)但不能直接声明结构的变量所以有必要提供创建和销毁函数。如:这样是非法的:LrcPoollrcpool应该对外提供创建和销毁函数。LrcPool*lrcpoolnew(sizetunitsize,sizetnpreallocunits)voidlrcpooldestroy(LrcPool*thiz)任何规则都有例外。有些数据结构纯粹是社交型的为了提高性能和方便起见常常不需要对它们进行封装比如点(Point)和矩形(Rect)等。当然封装也不是坏事MFC就对它们作了封装是否需要封装要根据具体情况而定。隐藏内部函数内部函数通常实现一些特定的算法(如果具有通用性应该放到一个公共函数库里)对调用者没有多大用处但它的暴露会干扰调用者的思路让系统看起来比实际的复杂。函数名也会污染全局名字空间造成重名问题。它还会诱导调用者绕过正规接口走捷径造成不必要的耦合。隐藏内部函数的做法很简单:在头文件中只放最小接口函数的声明。在C文件上所有内部函数都加上static关键字。禁止全局变量除了为使用单件模式(只允许一个实例存在)的情况外任何时候都要禁止使用全局变量。这一点我反复的强调但发现初学者还是屡禁不止为了贪图方便而使用全局变量。请读者从现在开始就记住这一准则。全局变量始终都会占用内存空间共享库的全局变量是按页分配的那怕只有一个字节的全局变量也占用一个page所以这会造成不必要空间浪费。全局变量也会给程序并发造成困难想把程序从单线程改为多线程将会遇到麻烦。重要的是如果调用者直接访问这些全局变量会造成调用者和实现者之间的耦合。在整个系统程序员成长计划中我们都是以面向对象的方式来设计和实现的(封装就是面向对象的主要特点之一)。为了避免不必要的概念混淆这里先解释一下对象和类:关于对象:对象就是某一具体的事物比如一个苹果,一台电脑都是一个对象。每个对象都是唯一的实例两个苹果无论它们的外观有多么相像内部成分有多么相似两个苹果毕竟是两个苹果它们是两个不同的对象。对象可以是一个实物也可以是一个概念比如一个苹果对象是实物而一项政策就是一个概念。在软件中对象是一个运行时概念它只存在于运行环境中比如:代码中并不存在窗口对象这样的东西要创建一个窗口对象一定要运行起来才行。关于类:对象可能是一个无穷的集合用枚举的方式来表示对象集合不太现实。抽象出对象的特征和功能按此标准将对象进行分类这就引入类的概念。类就是一类事物的统称类实际上就是一个分类的标准符合这个分类标准的对象都属于这个类。当然为了方便起见通常只需要抽取那些对当前应用来说是有用的特征和功能。在软件中类是一个设计时概念它只存在于代码中运行时并不存在某个类和某个类之间的交互。我们说编写一个双向链表实际上指的是双向链表这个类。C语言里并没有类这个概念我也不想因为引入这个概念让读者感到迷惑。在后面的讲述中我不会刻意区分类和对象我们说对象可能是指单个对象也可能是指对象所属的类要根据上下文进行区分(这种区分通常是很直观的)。我并不是这种做法的首创者见过好几本书都是这样做的希望挑剔的读者不要在这个概念问题上纠缠。好了如果你实现的双向链表没有达到这个标准请重做一遍练习吧。需求简述WriteOnce,DebugEverywhere。据说这是流传于JAVA程序员中间的一句笑话Sun公司用来形容JAVA的跨平台性的原话是Writeonce,runanywhere(WORA)。后者是理想的前者才是现实。如果我们的双向链表可以到处运行那就太好了。Writeonce,runanywhere(WORA)是我们的目标不过我们先要面对现实回到双向链表上请读者思考下列问题:专用双向链表和通用双向链表各自的特点与适用范围。如何编写一个通用的双向链表?花点时间思考一下再尝试编写一个通用的双向链表或许实现得不是那么优美但是我们迈出了走向通用性的第一步。专用链表和通用链表各自的特点与适用范围。专用链表在这里是指它的实现和调用耦合在一起只能被一个调用者使用而不能单独在其它地方被重用。通用链表则相反它具有通用性可以在多处被重复使用。尽管通用链表相对专用链表来说有很多优越之处不过简单的断定通用链表比专用链表好也是不公正的因为它们都有自己的优点和适用范围:专用链表的优点:更高性能。专用链表的实现和调用在一起可以直接访问数据成员省去了包装函数带来的性能开销可以提高时间性能。专用链表无需实现完整的接口只要满足自己的需要就行了生成的代码更小因此可以提高空间性能。更少依赖。自己实现不用依赖于别人。有时候你要写一个规模不大的跨平台程序比如想在展讯手机平台和MTK手机平台上运行虽然有现存的库可用但你又不想把整个库移植过去那么实现一个专用链表是不错的选择。实现简单。实现专用链表时不需要考虑在各种复杂应用情况下的特殊要求也不需要提供完整的接口所以实现起来比通用链表更为简单。通用链表的优点(从全局来看):可靠性更高。通用链表的实现要复杂得多复杂的东西意味着不可靠。但它是可以重复使用的其存在的问题会随每一次重用而被发现和改正慢慢的就行成一个可靠的函数库。开发效率更高。通用链表的实现要复杂得多复杂的东西也意味着更高的开发成本。同样因为它是可以重复使用的开发成本会随每一次重用而降低从整个项目来看会大大提高开发效率。考虑到链表是最常用的数据结构之一很多地方都会用到它实现通用的链表会更有价值。接下来我们要实现一个通用的链表不过请大家记住实现通用的链表并不是我们的目标而是我们学习软件设计方法的手段。前面我许诺过要以简单的数据结构讲述复杂的软件设计方法链表就是其中的载体之一。如何编写一个通用的链表?编写通用链表是一项复杂的任务不可能在这一节中把它阐述清楚这里我们先考虑三个问题:存值还是存指针通用链表首先是要做能够存放任何数据类型的数据新手常见的做法是定义一个抽象数据类型需要什么存放什么就定义成什么。如:typedefintTypetypedefstructDListNode{structDListNode*prevstructDListNode*nextTypedata}DListNode这样的链表算不上是通用的因为你存放整数时编译一次存放字符串时重义Type再编译一次存放其它类型同样要重复这个过程。麻烦不说关键是没有办法同时使用多个数据类型。我们要找到一种同时可以表示不同数据类型的类型才行有人说可以用union但是数据类型是无穷无尽的不可能在union中表示它们的全部。可行的办法有两种:存值:typedefstructDListNode{structDListNode*prevstructDListNode*nextvoid*datasizetlength}DListNode存入时拷贝一份数据保存数据的指针和长度。考虑到拷贝数据会带来性能开销不合符C语言的风格而且C语言中没有构造函数实现深拷贝比较麻烦所以在C语言中以这种方式实现的链表很少见。存指针:typedefstructDListNode{structDListNode*prevstructDListNode*nextvoid*data}DListNode只是保存指向对象的指针存取效率高是C语言中常见的做法。在存放整数时可以把void*强制转换成整数使用以避免内存分配(在现实中以上的情况链表都是存放结构的)。让C可以调用这不是一个重要的话题只是顺便提一下。C中允许同名函数存在所以编译器会对函数名重新编码。C代码包含C语言的头文件时重新编码名字与C语言库中的原函数名不一致结果造成找不到函数的情况。为了让C语言实现的函数在C中可以调用需要在头文件中加点东西才行:#ifdefcplusplusextern"C"{#endif…#ifdefcplusplus}#endif它表示如果在C中调用这里的函数编译器不能对函数名进行重新编码。完整的接口作为一个通用的链表接口要比较完整才行否则无法满足各种情况的需要(提供完整的接口并不违背最小接口原则)。实现具有完整接口的链表不是件容易的事读者先实现插入删除等基本操作就行了后面我们会慢慢扩展它的功能。(为了避免读起来拗口本文把双向链表简写成链表了希望读者不要介意)需求简述大部分初学者在编写双向链表时为了验证相关函数工作是否正常都会编写一个dlistprint的函数它的功能是在屏幕上打印出整个双向链表中的数据。从客观上讲用dlistprint输出的信息来判断dlist的正确性不是最好的办法不过脑袋里有质量概念总是值得表扬的。当把专用的双向链表演化成通用的双向链表时编写一个dlistprint已经不那么简单了。这里我们请读者写一个dlistprintf函数看看会遇到什么问题。在专用双向链表中dlistprintf的实现非常简单如果里面存放的是整数用”d”打印存放的字符串用”s”打印。现在的麻烦在于双向链表是通用的我们无法预知其中存在的数据类型也就是我们要面对数据类型的变化。怎么办呢?初学者常见的做法有:实现多个函数需要哪个就用哪个。比如实现的有dlistprintint用来打印存放整数的双向链表dlistprintstring用来打印存放字符串的双向链表如此等等其它类型都有自己的打印函数。这种做法的缺点有:一是每个函数的实现方式类似造成大量重复的代码。二是数据类型的种类不确定每种数据类型都要写一个print函数当要存放新的数据类型时需要修改dlist的实现。传入一个附加参数来决定如何打印。比如传入表示按整数方式打印传入表示按字符串方式打印以此类推。这种做法比第一种好一点至少不会造成大量重复的代码。但是同样存在增加新类型时要修改dlistprint函数的问题。调用dlist的接口函数获取每一个位置的数据并打印出来。它可以避免前面两种方法的缺点而且是一种很直观的方式。奇怪的是偏偏很少有人这样去做原因可能有两个其一是太拘泥于传统的实现方式而没有想到这一种。其二是担心性能问题因为通过索引取值每一次都从头开始定位其性能开销为O(n*n)。其实这种方法是可以接受的dlistprint是用于辅助测试我们并不在乎它的性能开销而且很少在链表中存放成千上万的数据它带来的性能影响也没有想的那样严重。不过在这里我们要介绍一种新的方法:dlistprint的大体框架为:DListNode*iter=thiz>firstwhile(iter!=){print(iter>data)iter=iter>next}在上面代码中我们主要是不知道如何实现print(iter>data)这行代码。可是谁知道呢?很明显调用者知道因为调用者知道里面存放的数据类型。OK那让调用者来做好了调用者调用dlistprint时提供一个函数给dlistprint调用这种回调调用者提供的函数的方法我们可以称它为回调函数法。调用者如何提供函数给dlistprint呢?当然是通过函数指针了。变量指针指向的是一块数据指针指向不同的变量则取到的是不同的数据。函数指针指向的是一段代码(即函数)指针指向不同的函数则具有不同的行为。函数指针是实现多态的手段多态就是隔离变化的秘诀这里只是一个开端后面我们会逐步的深入学习。回到正题上我们看如何实现dlistprint:定义函数指针类型:typedefDListRet(*DListDataPrintFunc)(void*data)声明dlistprint函数:DListRetdlistprint(DList*thiz,DListDataPrintFuncprint)实现dlistprint函数:DListRetdlistprint(DList*thiz,DListDataPrintFuncprint){DListRetret=DLISTRETOKDListNode*iter=thiz>firstwhile(iter!=){print(iter>data)iter=iter>next}returnret}调用方法staticDListRetprintint(void*data){printf("d",(int)data)returnDLISTRETOK}…dlistprint(dlist,printint)所有问题都解决了是不是很简单我以前写过一篇关于函数指针的BLOG文中声称不懂函数指针就不要自称是C语言高手现在我仍然坚持这个观点。函数指针的概念本身很简单关键在于灵活应用这里是一个最简单的应用希望读者仔细体会一下后面将会有大量篇幅介绍。我写了一个简单的示例它的实现并不完善不过用来演示我们到目前为止学到的内容已经够了。有兴趣的读者请到这里下载。需求简述这里我们请读者实现下列功能:对一个存放整数的双向链表找出链表中的最大值。对一个存放整数的双向链表累加链表中所有整数。多写多练不要偷懒写完之后请仔细思考一下有无改进的余地。实现这两个函数并不是件难事但真正写好的人并不多。初学者通常的做法有两种:各写一个独立的函数。dlistfindmax用来找出最大值dlistsum用来求和。这种做法和前面写dlistprint时所犯的错误一样会造成重复的代码让dlist的实现随着应用环境的变化而变化。采用回调函数法。细心的初学者会发现这两个函数的实现与dlistprint的实现很类似无非是print那行代码要换成别的功能。能想到这一点很好不过在真正动手时发现每个回调函数都要保存一些中间数据。大部分人选择了用全局变量来保存这可以实现要求的功能但违背了禁用全局变量原则。这两个函数没有什么实用价值但是通过它们我们可以学习几点:不要编写重复的代码按传统的方法写出dlistfindmax之后每个人都知道这个函数与dlistprint很类似在写出dlistsum之后那种感觉就更明显了。在这个时候不应该停下来而是要想办法把这些重复的代码抽出来。即使因为经验所限也要极力去想思考和查资料。写重复的代码很简单甚至凭本能都可以写出来。但要想成为优秀的程序员你一定要克服自己的惰情因为重复的代码造成很多问题:重复的代码更容易出错。在写类似代码的时候几乎所有人(包括我)都会选择CopyPaste的方法这种方法很容易犯一些细节上的错误如果某个地方修改不完整那就留下了”不定时”的炸弹说不定什么时候会暴露出来。重复的代码经不起变化。无论是修改BUG还是增加新特性往往你要修改很多地方如果忘掉其中之一你同样得为此付出代价。请记住古惑仔的话出来混迟早是要还的。大师们说过在软件中欠下的BUG你会为此还得更多。去除重复代码往往不是件简单的事情需要更多思考和更多精力不过事实证明这是最值得的投资。在这里我们要怎么抽取这些重复的代码呢?这三个函数无非是要遍历双向链表并做一些事情遍历双向链表我们可以提供一个dlistforeach函数至于要做什么这是千变万化的行为可以通过回调函数让调用者去做。任何回调函数都要有上下文大部分初学者都选择了回调函数法不过都无一例外的选择了用全局变量来保存中间数据这里我不想再强调全局变量的坏处了记性不好的读者可以看看前面的内容。我们要说的是在这种情况下如何避免使用全局变量。很简单给回调函数传递额外的参数就行了。这个参数我们称为回调函数的上下文变量名用ctx(context的缩写)。要在这个上下文中存放什么东西呢?那得根据具体的回调函数而定为了能保存任何数据类型我们选择void*表示这个上下文。下面我们看看怎么实现这个dlistforeach:DListRetdlistforeach(DList*thiz,DListVisitFuncvisit,void*ctx){DListRetret=DLISTRETOKDListNode*iter=thiz>firstwhile(iter!=ret!=DLISTRETSTOP){ret=visit(ctx,iter>data)iter=iter>next}returnret}visit是回调函数ctx就是我们说的上下文。要特别强调的一点是ctx应该作为回调函数的第一个参数。为什么呢?在前面我们讲过的面向对象的函数命名规则中我们以thiz作为函数的第一个参数而thiz通常也就是函数的上下文。如果在这里恰好ctx==thiz就不需要因为参数顺序不同而做转换了。实现求和的回调函数:staticDListRetsumcb(void*ctx,void*data){longlong*result=ctx*result=(int)datareturnDLISTRETOK}调用foreach:longlongsum=dlistforeach(thiz,sumcb,sum)是不是很简单?以后在使用回调函数时记得多加一个ctx参数即使暂时用不着留着方便以后扩展。好了请读者用类似的方法实现查找最大值的功能吧。只做份内的事我见到不少任劳任怨的程序员别人让他做什么他就做什么不管是不是份内的事不管是上司要求的还是同事要求的都来者不拒。别人说需要一个XXX功能的函数他就写一个函数在他的模块里日积月累后他的模块变得乱七八糟的成了大杂烩。我亲眼见过在系统设置和桌面两个模块里提供很多毫不相干的函数这些函数造成不必要的耦合和复杂度。在这里也是一样的求和和求最大值不是dlist应该提供的功能放在dlist里面实现是不应该的。为了能实现这些功能我们提供一种满足这些需求的机制就好了。热心肠是好的但一定不能违背原则否则就费力不讨好了。本节的示例请到这里下载。需求简述这里我们请读者实现下列功能:对一个存放字符串的双向链表把存放在其中的字符串转换成大写字母。对于初学者来说这道题有点难度很少有人能完全做对的。不过没关系我们的目标并不是要难倒读者而是要刺激读者去思考加深学习的印象。有了前面两次的经验我想没有人再去写一个dlisttoupper的函数了大家都会调用dlistforeach来实现。不过新的问题又出现了初学者常犯的错误有以下几种:转换大写的方法不对。char*p=strif(p!=){while(*p!=''){if('a'<=*p*p<='z'){*p=*p('a''A')}p}}这是我们在课本里学到的写法但在工程中是不能这样做的。因为大小写字母在不同语言中的定义是不一样的’a’是一个字符常量它的值在任何时候都是但在不同语言中却不一定代表’a’。我们不能简单的认为在(‘a’)(‘z’)之间的字符就是小写字母而是应该调用标准C函数islower来判断同样转换为大写应该调用toupper而不是减去一个常量。在双向链表中存放常量字符串转换时出现段错误。DList*dlist=dlistcreate()dlistappend(dlist,"It")dlistappend(dlist,"is")dlistappend(dlist,"OK")dlistappend(dlist,"!")dlistforeach(dlist,strtoupper,)dlistdestroy(dlist)运行时会出现Segmentationfault错误。原因是”It”等字符串是常量常量是不能修改的。在双向链表中存放的是临时变量转换后发现所有字符串都一样。charstr={}DList*dlist=dlistcreate()strcpy(str,"It")dlistappend(dlist,str)strcpy(str,"is")dlistappend(dlist,str)strcpy(str,"OK")dlistappend(dlist,str)strcpy(str,"!")dlistappend(dlist,str)dlistforeach(dlist,strtoupper,)dlistforeach(dlist,strprint,)dlistdestroy(dlist)运行时发现打印出几个感叹号。原因是dlistappend时没有拷贝一份所以在dlist中存放的是同一个地址。而且这个dlist在当前函数返回后里面保存的数据都无效了因为这些数据指向的是临时变量。存放时拷贝了数据但没有free分配的内存。DList*dlist=dlistcreate()dlistappend(dlist,strdup("It"))dlistappend(dlist,strdup("is"))dlistappend(dlist,strdup("OK"))dlistappend(dlist,strdup("!"))dlistforeach(dlist,strtoupper,)dlistforeach(dlist,strprint,)dlistdestroy(dlist)这里看起来工作正常了但存在内存泄露的BUG。strdup调用malloc分配了内存但没有地方去free它们。初学者对内存和指针只有一知半解的认识常常犯一些连自己都莫名其妙的错误。为了避免这些不必要的错误今天我们要学习各种数据存放的位置以及它们的特性让初学者对编程有更进一步的认识。在程序中数据存放的位置主要有以下几个:未初始化的全局变量(bss段)已经记不清bss代表BlockStorageStart还是BlockStartedbySymbol。像我这种没有用过那些史前计算机的人终究无法明白这样怪异的名字记不住也是不足为奇的。不过没有关系重要的是我们要清楚什么数据是存放在bss段中的这些数据有什么样的特点以及如何使用它们。通俗的说bss段是用来存放那些没有初始化的和初始化为的全局变量的。它有什么特点呢让我们来看看一个小程序的表现。intbssarray*intmain(intargc,char*argv){return}#gccgbsscobssexe#lslbssexerwxrwxrxrootrootNov:bssexe#objdumphbssexe|grepbssbsseee**变量bssarray的大小为M而可执行文件的大小只有K。由此可见bss类型的全局变量只占运行时的内存空间而不占用文件空间。现代大多数操作系统在加载程序时会把所有的bss全局变量清零。但为保证程序的可移植性手工把

用户评价(2)

  • 我要笑遍世界618 谢谢 支持 该书不错的~~ 这个pdf资料 最后缺少第11/12章 不过也算很完整了 谢谢

    2011-02-27 08:40:14

  • Looro 实在.

    2010-11-09 00:08:54

关闭

新课改视野下建构高中语文教学实验成果报告(32KB)

抱歉,积分不足下载失败,请稍后再试!

提示

试读已结束,如需要继续阅读或者下载,敬请购买!

文档小程序码

使用微信“扫一扫”扫码寻找文档

1

打开微信

2

扫描小程序码

3

发布寻找信息

4

等待寻找结果

我知道了
评分:

/43

系统程序员成长计划

仅供在线阅读

VIP

在线
客服

免费
邮箱

爱问共享资料服务号

扫描关注领取更多福利