2006年06月28日

Writen by interma@BMY ( interma@stu.xjtu.edu.cn )
2004.12 初稿
2005.6修订
声明: 以下所有信息都是本人自我感悟,因此难免有疏漏和错误之处,欢迎大家指正。
同时这也是面向DotNet新手的,各位大牛们感到太肤浅的话,不看也罢。
一. 什么是DotNet,为什么要学习DotNet:
[1] 什么是DotNet:
首先我先给出微软对DotNet定义:
“Microsoft.NET 是 Microsoft.NET XML Web Services 平台。XML Web Services 允许应用程序通过 Internet 进行通讯和共享数据,而不管采用的是哪种操作系统,设备或编程语言。Microsoft.NET 平台提供XML Web Services 并将这些服务集成在一起,为个人用户的好处是无缝的,吸引人的体验。”
但这只是官方的定义,对于我们来说DotNet只是一个为了实现上述目标的技术集,其中包含未来几年内可能应用到的先进技术,它的出现会使开发人员更容易更好地开发面对未来的复杂应用。
[2] 为什么学习DotNet:
在这里首先打一个总的基调,那就是:DotNet经过近4年(出现于2000年)的发展,它已经不再是那个稚气未脱的孩子,人们已经不再关注是否要学DotNet,而是关注于什么时候开始学习DotNet!
下面我将给出我的学习原因:
(1) 它本身的魅力:
一个技术对大家影响最深,令大家印象最深刻的应该在语言层面,这本小节中,我主要说明DotNet中的语言魅力。
DotNet强调的是多语言单平台。它其中的语言种类繁多(C#,VB,C++,J#,Delphi, Python等),这便给了程序员们极大的自由,由于这几乎覆盖了所有的流行语言,因此无论你以前是使用的语言,你都可以很快的过渡到DotNet上来,让你以前的辛苦没有白费。
DotNet中的语言是面向构件(组件)编程的典范。在这里我看到了语言层次的进步,C那代语言是面向过程,C++那代是面向对象,而到了DotNet则是面向构件。
在这一代又一代语言发展中无不体现出一个特点(包括近几年流行的动态语言):那就是对问题域的抽象越来越容易,越来越精确,越来越以人为本。随着计算机硬件性能的提升,性能已不是唯一的关注点了。
DotNet中的语言具有优美性。这在里我主要是指C# ———- 这个没有历史包袱的语言。在看C#的语言特性中我看到了美丽,它没有了那些面向过程的乌七八糟的东西,模式和重构都能随心应手的使用,轻量级的开发过程和相应的工具也能做到有的放矢。这无疑和C# 优良的语言特性不无关系,而且我还看到了它的轻快的发展脚步,C# 2.0中的范型(引自C++)和匿名方法(引自Java)等语言特性的出现,无不体现它的美好明天。
DotNet中的语言和基类库(BCL)简单易学。这个我在版上同很多人交流过,但是结果我感到非常遗憾,大家对它的这个特点很不认可,多数表出“DotNet太简单了,没什么可学的”的态度,关于这个问题我在[FAQ]中会进行说明,在这里先给出一个观点:简单易学决不是缺点,只能是优点。
(2) 未来的形势:
因为未来的不确定性,因此在这里我不多说,只谈2点:
DotNet中的技术都是当前或未来几年内的技术热点,这些技术在未来几年内有很大的用处。
Windows的下一代平台Longhorn将用DotNet Framework作为基础来取代现在的Win32 API,也就是说托管API在将来将大行其道,Win32API只会变成2等公民,将来Longhorn下的应用大多都将用托管代码写成。(修订注:最新的MS路线图表明Longhorn不会完全采用托管API,但是托管API一定会比现在的XP中的那个要强得多)
顺便说一个不可不提的事情,那就是微软在将来一定会大力推进DotNet的应用,从最近的Imagine Cup中你看到了什么?(提示一下: 那就是所有编程语言都必须使用DotNet系列语言之一)
(3) 更好的解决问题:
在这里我只强调一个问题,这个是编程的根本:就是为了解决实际问题。
无论某个技术是多先进,还是多么烂,只要能解决实践问题就very well。我们无需卖弄自己采用的技术有多先进,也无需阐明有过高深,多奇妙,我们要的只是解决问题 ————— This is our final target。
而恰恰我看到了DotNet的解决实际问题的强大能力:从桌面系统到企业应用,无所不能。(当然,我不是说无所不精)这个我从很多朋友们那都有所了解,他们不是搞DotNet的,但是他们拿DotNet做过项目!
它让我们能高效地解决实际问题,这就是我们选择它的原因。
[3] 小结:
不知道看了上边的文章,你对DotNet产生了学习兴趣没有。
如果有的话,那我非常高兴,因为你在学习它的过程中一定会找到编程的快乐。
如果没有,也没关系,那应该是我的表达能力不好,没有说出DotNet的迷人之处,你可以在其他开发社区中,看看其他人的观点,再作出你的决定。
如果你已经有了兴趣,那让我们看看应该怎样学习它吧。进入第2部分。
二. 学习路线:
在下边的每个环节中我都会推荐几本书,如果是我没看过的,我会说明的。
因此大家不用担心书的质量。(我以下都未说这些书的作者和出版社,因为这些书我们学校的图书馆一般都有,不会弄混,如果你未找到,请和我联系,我会告诉你它的详细信息)
[1]初始阶段:
目标:首先你应精通一门DotNet语言(包括Asp.net开发人员,我总感觉Asp.net开发人员的基础好象不扎实)
具体实现:建议使用C#,因为这是为DotNet量身定做的语言,它没有历史包袱,最能体现出CLI的特性。如果熟悉VB6同时特别喜欢VB的话,可以选择VB.net。(不过我猜你最后肯定也会C#了)
在这里,你的第一本DotNet语言学习书籍没有过多的限制,中国的外国的都行,不过记得别选太厚的。
之后你将看到一本必读书:《.NET Framework 程序设计》(修订版),不多介绍了,这本书必读,否则你就不叫懂DotNet,这本书的有些内容你可能有点看不懂,不过没关系,随着你以后的深入,你就会明白了。
成果:你这时只需比较了解语言特性,不必编出任何程序。
[2]提高阶段:
目标:可以开发程序了,You are a programmer now!
具体实现:在这里我们要区分一下,分为桌面开发人员和网站开发人员(Asp.net)。
对于网站开发人员(Asp.net),我在这里向你们推荐的Asp.net入门书是:
《Asp.net 揭秘》(电力出版社)和《Asp.net权威编程》(电力出版社),这2本书非常不错,很系统的介绍了Asp.net 1.1的种种特性,只不过有点厚。 唉,在这我要说一句很遗憾的话,“对于Asp.net开发人员,我能对你的帮助到此为止了”,我不熟悉这个领域,但是在这阶段之后,你要多做一些项目,这会对你的开发水平有极大的提高,记住,一定要做项目,边作边学。在这之后,你的水平会有显著的提高,你那时应该可以知道自己的下一步该怎么走了。(顺便说一句:yboys@BMY是个牛人,去问他吧。)
对于桌面开发人员,在本阶段,你们要做的和网站开发人员差不多。先推荐一本关于Winform的书籍,这个有很多,建议看《Winform 高级编程》和《Windows forms 程序设计》(此本我未看过),这2本都有点厚,时间不多的话,选若干章节看就行了。当然选其它关于Winform书也行,不过推荐外国人写的。
之后就去开发桌面程序吧,从小到大,逐个深入,在这里你主要是熟悉DotNet的基类库和各种可视化控件(你的课程设计就可以用DotNet搞), 为将来打好基础。
成果:你已经能开发出具体的小项目了,呵呵,在同学的眼里,你已经算半个编程高手了。
[3] 提高阶段:
目标:你要变得professional 。不过这个阶段有点漫长。
具体实现:我们要对一些关于DotNet的基础知识和开发经验进行一下复习与总结了,同时也可以搞懂你在《.NET Framework 程序设计》中不懂的问题。遗憾的是,DotNet现在还没有这种书籍,不过没关系,我们要到Java那里找些帮助:《effective java》和
《practical java》,呵呵,放心,你绝对看得懂。
接下来,你要了解一些开发准则了,首先Clear你的代码,让它变得美丽,这是一个Programmer的基本素质,推荐那本《高质量程序设计指南—-C/C++语言》,相信这本书你不会陌生,这里边讲到了一些作为programmer的最基本素质,无论是编码还是做人。
下面2本重量级的书出场了,《设计模式》(我未读过)和《重构》,软工双杰的称号不是吹出来的,呵呵,开始吧。不过,我推荐看《C#设计模式》,因为那本《设计模式》据说太过抽象,很难看懂,并且里边还是用C++描述的,不适合初学者阅读。看完之后还推荐一本《设计模式精解》,书如其名,相信可以给你带来新的感受。
在这同时你还应了解一下软件工程的知识轻量级的软件过程方法(如:XP,TDD等敏捷开发方法),注意:只是了解,不求深入。
至于传统软工的冬冬,随便找一本介绍那些的书看看就行了,只要看完可以基本看懂一些简单的UML图就可以了,之后推荐看一本《java程序员UML手册》就行了,这本写得很好。
看到这里,你可能会疑惑,难道对软工的知识如此轻视吗?就看这几本的书就行了?
当然不行!但是你要明白软工知识必须要和具体的开发能力和项目经验相结合才有好的效果。对于我们学生来说,根本没经过大的,规范的软件项目的熏陶,因此根本无法深入软工这个领域,因此软工的知识等你工作了或上研了,在深入也来得及,因此现在只要求有所了解,不求深入。
归根结底,本阶段是要提高你的Programmer素质和代码控制能力。不过,在学完这些之后别忘了开发一个大一点的程序检验一下你的所学阿。
成果:你已经变得专业了,可以写出几千行(<5000)高质量的代码了。(顺便说一句,如果你现在刚好本科毕业,应该很抢手的。)
[4]深入阶段:
目标:学习DotNet的高级技术
具体实现:关于DotNet的高级技术,有很多方面(太多了),其中有很多我们可能到工作阶段才能够深入了解。但是现在了解一些不无好处, 我在下面只列出了很少的几个,你可以从中选择几个学习。其中应该会有你已经熟悉的技术了,这是很正常的,本阶段不像开始的那几个阶段,比较灵活,它的开始和结束具有灵活性,由你进行控制。
(1)线程操作:
这是提高软件并发性的基础,无论是桌面开发还是网站编程,相信你都会遇到它。强烈建议打好基础。在这里DotNet下还没有本专题的好书,只能推荐这本《C#/VB.net线程手册》,不过此书正如其名,它只是带着你将托管线程的类库浏览了一番,关键的东西都未提到,因此你只有到网上看一些有关线程的资料了。不过在这里,我又要拿Java出来了,《Java线程编程》(我未看过),这本Java线程方面的经典之著应该不会让你失望。
(2)网络编程:
这个重要性不说了吧,就是套接字那套东西。不过这可不是令人头疼的Winsock了,DotNet对Winsock进行了包装,使其变得好用很多。强烈推荐:《C# 网络编程》,它非
常系统的介绍了网络编程的原理和方法,看完之后,编个QQ或Serv-U应该不成问题,总之不容错过。
(3)数据库编程:
这个好没意思阿,我也不大懂,又不能帮你了。基本的就是一些数据库sql 语句和存储过程,同时还要对常见的DBMS有一些了解就行了。如果想在这个领域深入的话,那就要熟悉某种DBMS的性能特点,同时提高数据库分析和设计的能力。
(4)CLR和DotNet底层知识:
学了之后短期内应该没用,不过如果像我一样待着没事的话,看看DotNet的内部运作机理还是挺有意思的。推荐:《高级.NET程序设计》和《.NET本质论》(此本有难度)。
(5)分布式:
它就是.NET Remoting,这是非常有用的一种技术,我也在学习中。推荐《.NET remoting 技术手册》,不过还要到网上多看别人的例子。(呵呵,让我们忘掉复杂的COBRA吧)
(6)Web Services:
这是将来的基础应用平台,现在应用的也很广泛。我对这个知识了解一些基础的知识。
没法推荐高深的,只推荐一本O’Reilly的《.Net Web服务》,是本很好的入门教材。
(7)Pocker PC和Smartphone:
热门领域,不过同样也没什么书籍可以参考,同样我也没法帮你,到网上找找吧。
成果:你现在对这几个DotNet的高级技术都有了了解,将来(应当是工作时了)你应该会精通其中的某几项技术,这是你就是DotNet技术专家了。当然,别忘了,学习完这些技术后,编几个东西熟悉一下啊。
[5]superman阶段:
目标:提高系统分析和设计的能力,提高OOA/OOD的能力,还有着众多的项目经验。
同时还对DotNet中各项技术都有了新的认识,不再拘泥于技术细节,能做到在最恰当的场合应用最合理的技术(不一定是最先进的技术),同时还能对将来的技术趋势做出自己的分析和把握。
过程:。。。。。。
成果:我该怎么称呼你呢?我亲爱的DotNet架构师。
[6]小结:
我已经将我认为的DotNet开发人员的成长路线展示给你了,你一定会有自己的主意了,希望你能在学习它的过程中找到快乐。
在第3部分中,我将列出网上的一些参考资源,供你参考之用。
三. 参考资源:
在这里我只推荐很少的一部分资源,但因为我们的时间和精力都有限,这些已经足够你
成为高手中的高手了。
[1]开发工具及相关下载:
(1) 我的ftp :
202.200.238.199 user: guest pass: guest
在这里有绝大多数的开发工具(当然包括DotNet), 我同时也会经常更新。在你找某个开发工具时,可先来这里看一下。
(2) MSDN下载:
MSDN开发中心下载(中文):
http://www.microsoft.com/downloads/search.aspx?displaylang=zh-cn&categoryid=10
MSDN Download and Code Center (英文):
http://msdn.microsoft.com/downloads/
这是微软的官方站点,英文的资源多一点,如果在中文站点没找到,可去英文的看看。
[2]开发社区及学习资料:
(1) CSDN:
首页:
http://www.csdn.net/
CSDN技术社区[C#]: http://community.csdn.net/expert/forum.asp?url=/Expert/ForumList.asp?roomid=5201&typenum=1&xmlsrc=&whichpage=1
CSDN技术中心[DotNet文档列表]:
http://dev.csdn.net/articlelist.aspx?c=14
CSDN就啥都不说了,号称亚洲最大,其实啥人都有,别看花眼啊。
(2) MSDN [中文网站]:
http://www.microsoft.com/china/msdn/
基本都是精品,包含DotNet的各方面信息。
(3) 微软新闻组:
news.bentium.com (用OE上)
通过奔腾转信,进入: Microsoft.public.cn.dotnet.language.相应的语言。
其实就是对应微软社区。有很多的MVP帮你,感觉比CSDN强多了,也许还能碰
到clear和imcc呢。
(4)
博客堂:
http://blog.joycode.com/
博客园:
http://www.cnblogs.com/
这2个有关于DotNet的新闻和技术动态,以较为实用的工程技术知识为主。
[3]其它:
(1) SourceForge:
http://sourceforge.net/
这个应该不必说了吧?大名鼎鼎的sf.net 。
现在DotNet的开源项目也有很多了, 学习他们优秀的代码设计也是必不可少的一项课程。
进入其中的Software Map 之后,选择某种DotNet语言,就能看到相应的项目了。
(2) ME:
在你没成为高手之前,我也许能帮上你的忙,呵呵,欢迎你与我交流。(QQ: 165194384)
四. 其他:(FAQ):
这些FAQ,都是我曾经扪心自问的,其中不涉及到任何技术问题,都是一些我这几年对开发程序的思考。这些大多与DotNet无关,但是我希望你能从我这里找到问题的答案。如果你有不同的看法,十分欢迎你与我交流。
[1]为什么要开发程序?为什么要当程序员?
Only one reason,我们喜欢,这就是我们的生活方式。我们不是为了享受众人羡慕的眼光,也不是为了去挣大把大把的金钱。我们对技术的态度就像恶魔猎人对力量的无限渴求一样。我们可以把手放在胸前,对天发誓:我们真心喜欢编程。
[2]不喜欢编程能当好程序员吗?
你应该已经能猜到我的观点了,那就是:不能,就算能你也不快乐。(没人愿意干自己不感兴趣的事情,过得不快乐,你这辈子就算白活了)
[3]开发水平低怎么办?成为不了高手怎么办?
难道成为高手你就快乐了吗?我看不出来。
[4]快速提高水平的秘诀是什么?
唯有刻苦+勤奋。
[5]中国软件业的未来怎么办?
我还在思考。。。。。。
[6]关于语言的选择?
语言只是为了解决问题的,最终的要点是要解决问题,因此不同的语言有它擅长的应用领域。
[7]C++的阴影?
这个其实和上个问题一样,有这样一种观点,DotNet的出现把程序员们都弄傻了,只会用那些控件(like vb)。其实没有傻的技术,只有傻的人,没有人会因为使用DotNet而变得愚蠢。CPPer们攻击DotNet的原因无非就是DotNet太简单了,白痴都会,做不了底层应用,代码效率低。我想不通,一个技术容易上手有什么不好,在同样的应用产出下,你的学习成本的降低,开发效率的提高难道是坏事?至于做不了底层系统和代码效率的问题,这是因为DotNet不是面向这个领域,这就是C++将来的地盘,DotNet有它的领域,一个企业3层应用平台,全部用C++开发怎能快速,灵活的搞定。过于偏激的排外或爱好一种技术,只会给自己带来灾难性的后果。最后要说一个历史趋势,那就是随着计算机性能和问题的逻辑复杂度的提高,高效率的代码已经不再是唯一的衡量开发的标准了,从汇编,C,C++,JAVA,DotNet的发展,你看到了什么?那就是抽象的程度越来越高,程序员们的编码难度逐步降低(也就是次要复杂度的降低),但是系统分析和设计的难度则在逐层提升,所面对的问题的逻辑复杂度在逐渐升高(主要复杂度的升高),这正是DotNet(Java)出现的契机,它降低我们开发代码的次要复杂度,使我们可以集中我们的全部精力投入到主要复杂度的解决上来,这也正是MDA出现的原因。
[8]程序员的基本素质?
正直,善良,刻苦,勤奋,平和,坦然。
[9]最后来各搞笑点的话题,程序员真的不好找mm吗?
确实不好找。。。呜呜~~~
为了我们的程序,认了吧。
五. 最后的话:
非常感谢你能有耐心读完我的文章,希望看完之后你能找到自己的方向,期待你与我的进一步交流,bye~~~~。

2006年06月20日

一、前言

  自从微软推出16位的Windows操作系统起,此后每种版本的Windows操作系统都非常依赖于动态链接库(DLL)中的函数和数据,实际上Windows操作系统中几乎所有的内容都由DLL以一种或另外一种形式代表着,例如显示的字体和图标存储在GDI DLL中、显示Windows桌面和处理用户的输入所需要的代码被存储在一个User DLL中、Windows编程所需要的大量的API函数也被包含在Kernel DLL中。

  在Windows操作系统中使用DLL有很多优点,最主要的一点是多个应用程序、甚至是不同语言编写的应用程序可以共享一个DLL文件,真正实现了资源"共享",大大缩小了应用程序的执行代码,更加有效的利用了内存;使用DLL的另一个优点是DLL文件作为一个单独的程序模块,封装性、独立性好,在软件需要升级的时候,开发人员只需要修改相应的DLL文件就可以了,而且,当DLL中的函数改变后,只要不是参数的改变,程序代码并不需要重新编译。这在编程时十分有用,大大提高了软件开发和维护的效率。

  既然DLL那么重要,所以搞清楚什么是DLL、如何在Windows操作系统中开发使用DLL是程序开发人员不得不解决的一个问题。本文针对这些问题,通过一个简单的例子,即在一个DLL中实现比较最大、最小整数这两个简单函数,全面地解析了在Visual C++编译环境下编程实现DLL的过程,文章中所用到的程序代码在Windows98系统、Visual C++6.0编译环境下通过。

  二、DLL的概念

  DLL是建立在客户/服务器通信的概念上,包含若干函数、类或资源的库文件,函数和数据被存储在一个DLL(服务器)上并由一个或多个客户导出而使用,这些客户可以是应用程序或者是其它的DLL。DLL库不同于静态库,在静态库情况下,函数和数据被编译进一个二进制文件(通常扩展名为*.LIB),Visual C++的编译器在处理程序代码时将从静态库中恢复这些函数和数据并把他们和应用程序中的其他模块组合在一起生成可执行文件。这个过程称为"静态链接",此时因为应用程序所需的全部内容都是从库中复制了出来,所以静态库本身并不需要与可执行文件一起发行。

  在动态库的情况下,有两个文件,一个是引入库(.LIB)文件,一个是DLL文件,引入库文件包含被DLL导出的函数的名称和位置,DLL包含实际的函数和数据,应用程序使用LIB文件链接到所需要使用的DLL文件,库中的函数和数据并不复制到可执行文件中,因此在应用程序的可执行文件中,存放的不是被调用的函数代码,而是DLL中所要调用的函数的内存地址,这样当一个或多个应用程序运行是再把程序代码和被调用的函数代码链接起来,从而节省了内存资源。从上面的说明可以看出,DLL和.LIB文件必须随应用程序一起发行,否则应用程序将会产生错误。

  微软的Visual C++支持三种DLL,它们分别是Non-MFC Dll(非MFC动态库)、Regular Dll(常规DLL)、Extension Dll(扩展DLL)。Non-MFC DLL指的是不用MFC的类库结构,直接用C语言写的DLL,其导出的函数是标准的C接口,能被非MFC或MFC编写的应用程序所调用。Regular DLL:和下述的Extension Dlls一样,是用MFC类库编写的,它的一个明显的特点是在源文件里有一个继承CWinApp的类(注意:此类DLL虽然从CWinApp派生,但没有消息循环),被导出的函数是C函数、C++类或者C++成员函数(注意不要把术语C++类与MFC的微软基础C++类相混淆),调用常规DLL的应用程序不必是MFC应用程序,只要是能调用类C函数的应用程序就可以,它们可以是在Visual C++、Dephi、Visual Basic、Borland C等编译环境下利用DLL开发应用程序。

  常规DLL又可细分成静态链接到MFC和动态链接到MFC上的,这两种常规DLL的区别将在下面介绍。与常规DLL相比,使用扩展DLL用于导出增强MFC基础类的函数或子类,用这种类型的动态链接库,可以用来输出一个从MFC所继承下来的类。

  扩展DLL是使用MFC的动态链接版本所创建的,并且它只被用MFC类库所编写的应用程序所调用。例如你已经创建了一个从MFC的CtoolBar类的派生类用于创建一个新的工具栏,为了导出这个类,你必须把它放到一个MFC扩展的DLL中。扩展DLL 和常规DLL不一样,它没有一个从CWinApp继承而来的类的对象,所以,开发人员必须在DLL中的DllMain函数添加初始化代码和结束代码。

  三、动态链接库的创建

  在Visual C++6.0开发环境下,打开File\New\Project选项,可以选择Win32 Dynamic-Link Library或MFC AppWizard[dll]来以不同的方式来创建Non-MFC Dll、Regular Dll、Extension Dll等不同种类的动态链接库。

  1. Win32 Dynamic-Link Library方式创建Non-MFC DLL动态链接库

  每一个DLL必须有一个入口点,这就象我们用C编写的应用程序一样,必须有一个WINMAIN函数一样。在Non-MFC DLL中DllMain是一个缺省的入口函数,你不需要编写自己的DLL入口函数,用这个缺省的入口函数就能使动态链接库被调用时得到正确的初始化。如果应用程序的DLL需要分配额外的内存或资源时,或者说需要对每个进程或线程初始化和清除操作时,需要在相应的DLL工程的.CPP文件中对DllMain()函数按照下面的格式书写。


BOOL APIENTRY DllMain(HANDLE hModule,DWORD ul_reason_for_call,LPVOID lpReserved)
{
switch( ul_reason_for_call )
{
case DLL_PROCESS_ATTACH:
…….
case DLL_THREAD_ATTACH:
…….
case DLL_THREAD_DETACH:
…….
case DLL_PROCESS_DETACH:
…….
}
return TRUE;
}

  参数中,hMoudle是动态库被调用时所传递来的一个指向自己的句柄(实际上,它是指向_DGROUP段的一个选择符);ul_reason_for_call是一个说明动态库被调原因的标志,当进程或线程装入或卸载动态链接库的时候,操作系统调用入口函数,并说明动态链接库被调用的原因,它所有的可能值为:DLL_PROCESS_ATTACH: 进程被调用、DLL_THREAD_ATTACH: 线程被调用、DLL_PROCESS_DETACH: 进程被停止、DLL_THREAD_DETACH: 线程被停止;lpReserved为保留参数。到此为止,DLL的入口函数已经写了,剩下部分的实现也不难,你可以在DLL工程中加入你所想要输出的函数或变量了。

  我们已经知道DLL是包含若干个函数的库文件,应用程序使用DLL中的函数之前,应该先导出这些函数,以便供给应用程序使用。要导出这些函数有两种方法,一是在定义函数时使用导出关键字_declspec(dllexport),另外一种方法是在创建DLL文件时使用模块定义文件.Def。需要读者注意的是在使用第一种方法的时候,不能使用DEF文件。下面通过两个例子来说明如何使用这两种方法创建DLL文件。

  1)使用导出函数关键字_declspec(dllexport)创建MyDll.dll,该动态链接库中有两个函数,分别用来实现得到两个数的最大和最小数。在MyDll.h和MyDLL.cpp文件中分别输入如下原代码:


//MyDLL.h
extern "C" _declspec(dllexport) int Max(int a, int b);
extern "C" _declspec(dllexport) int Min(int a, int b);
//MyDll.cpp
#include
#include"MyDll.h"
int Max(int a, int b)
{
if(a>=b)return a;
else
return b;
}
int Min(int a, int b)
{
if(a>=b)return b;
else
return a;
}

  该动态链接库编译成功后,打开MyDll工程中的debug目录,可以看到MyDll.dll、MyDll.lib两个文件。LIB文件中包含DLL文件名和DLL文件中的函数名等,该LIB文件只是对应该DLL文件的"映像文件",与DLL文件中,LIB文件的长度要小的多,在进行隐式链接DLL时要用到它。读者可能已经注意到在MyDll.h中有关键字"extern C",它可以使其他编程语言访问你编写的DLL中的函数。

  2)用.def文件创建工程MyDll

  为了用.def文件创建DLL,请先删除上个例子创建的工程中的MyDll.h文件,保留MyDll.cpp并在该文件头删除#include MyDll.h语句,同时往该工程中加入一个文本文件,命名为MyDll.def,再在该文件中加入如下代码:

LIBRARY MyDll
EXPORTS
Max
Min

  其中LIBRARY语句说明该def文件是属于相应DLL的,EXPORTS语句下列出要导出的函数名称。我们可以在.def文件中的导出函数后加@n,如Max@1,Min@2,表示要导出的函数顺序号,在进行显式连时可以用到它。该DLL编译成功后,打开工程中的Debug目录,同样也会看到MyDll.dll和MyDll.lib文件。

  2.MFC AppWizard[dll]方式生成常规/扩展DLL

  在MFC AppWizard[dll]下生成DLL文件又有三种方式,在创建DLL是,要根据实际情况选择创建DLL的方式。一种是常规DLL静态链接到MFC,另一种是常规DLL动态链接到MFC。两者的区别是:前者使用的是MFC的静态链接库,生成的DLL文件长度大,一般不使用这种方式,后者使用MFC的动态链接库,生成的DLL文件长度小;动态链接到MFC的规则DLL所有输出的函数应该以如下语句开始:


AFX_MANAGE_STATE(AfxGetStaticModuleState( )) //此语句用来正确地切换MFC模块状态

  最后一种是MFC扩展DLL,这种DLL特点是用来建立MFC的派生类,Dll只被用MFC类库所编写的应用程序所调用。前面我们已经介绍过,Extension DLLs 和Regular DLLs不一样,它没有一个从CWinApp继承而来的类的对象,编译器默认了一个DLL入口函数DLLMain()作为对DLL的初始化,你可以在此函数中实现初始化,代码如下:


BOOL WINAPI APIENTRY DLLMain(HINSTANCE hinstDll,DWORD reason ,LPVOID flmpload)
{
switch(reason)
{
……………//初始化代码;
}
return true;
}

  参数hinstDll存放DLL的句柄,参数reason指明调用函数的原因,lpReserved是一个被系统所保留的参数。对于隐式链接是一个非零值,对于显式链接值是零。

  在MFC下建立DLL文件,会自动生成def文件框架,其它与建立传统的Non-MFC DLL没有什么区别,只要在相应的头文件写入关键字_declspec(dllexport)函数类型和函数名等,或在生成的def文件中EXPORTS下输入函数名就可以了。需要注意的是在向其它开发人员分发MFC扩展DLL 时,不要忘记提供描述DLL中类的头文件以及相应的.LIB文件和DLL本身,此后开发人员就能充分利用你开发的扩展DLL了。

  四、动态链接库DLL的链接

  应用程序使用DLL可以采用两种方式:一种是隐式链接,另一种是显式链接。在使用DLL之前首先要知道DLL中函数的结构信息。Visual C++6.0在VC\bin目录下提供了一个名为Dumpbin.exe的小程序,用它可以查看DLL文件中的函数结构。另外,Windows系统将遵循下面的搜索顺序来定位DLL: 1.包含EXE文件的目录,2.进程的当前工作目录, 3.Windows系统目录, 4.Windows目录,5.列在Path环境变量中的一系列目录。

  1.隐式链接

  隐式链接就是在程序开始执行时就将DLL文件加载到应用程序当中。实现隐式链接很容易,只要将导入函数关键字_declspec(dllimport)函数名等写到应用程序相应的头文件中就可以了。下面的例子通过隐式链接调用MyDll.dll库中的Min函数。首先生成一个项目为TestDll,在DllTest.h、DllTest.cpp文件中分别输入如下代码:


//Dlltest.h
#pragma comment(lib,"MyDll.lib")
extern "C"_declspec(dllimport) int Max(int a,int b);
extern "C"_declspec(dllimport) int Min(int a,int b);
//TestDll.cpp
#include
#include"Dlltest.h"
void main()
{int a;
a=min(8,10)
printf("比较的结果为%d\n",a);
}

  在创建DllTest.exe文件之前,要先将MyDll.dll和MyDll.lib拷贝到当前工程所在的目录下面,也可以拷贝到windows的System目录下。如果DLL使用的是def文件,要删除TestDll.h文件中关键字extern "C"。TestDll.h文件中的关键字Progam commit是要Visual C+的编译器在link时,链接到MyDll.lib文件,当然,开发人员也可以不使用#pragma comment(lib,"MyDll.lib")语句,而直接在工程的Setting->Link页的Object/Moduls栏填入MyDll.lib既可。

  2.显式链接

  显式链接是应用程序在执行过程中随时可以加载DLL文件,也可以随时卸载DLL文件,这是隐式链接所无法作到的,所以显式链接具有更好的灵活性,对于解释性语言更为合适。不过实现显式链接要麻烦一些。在应用程序中用LoadLibrary或MFC提供的AfxLoadLibrary显式的将自己所做的动态链接库调进来,动态链接库的文件名即是上述两个函数的参数,此后再用GetProcAddress()获取想要引入的函数。自此,你就可以象使用如同在应用程序自定义的函数一样来调用此引入函数了。在应用程序退出之前,应该用FreeLibrary或MFC提供的AfxFreeLibrary释放动态链接库。下面是通过显式链接调用DLL中的Max函数的例子。


#include
#include
void main(void)
{
typedef int(*pMax)(int a,int b);
typedef int(*pMin)(int a,int b);
HINSTANCE hDLL;
PMax Max
HDLL=LoadLibrary("MyDll.dll");//加载动态链接库MyDll.dll文件;
Max=(pMax)GetProcAddress(hDLL,"Max");
A=Max(5,8);
Printf("比较的结果为%d\n",a);
FreeLibrary(hDLL);//卸载MyDll.dll文件;
}

  在上例中使用类型定义关键字typedef,定义指向和DLL中相同的函数原型指针,然后通过LoadLibray()将DLL加载到当前的应用程序中并返回当前DLL文件的句柄,然后通过GetProcAddress()函数获取导入到应用程序中的函数指针,函数调用完毕后,使用FreeLibrary()卸载DLL文件。在编译程序之前,首先要将DLL文件拷贝到工程所在的目录或Windows系统目录下。

  使用显式链接应用程序编译时不需要使用相应的Lib文件。另外,使用GetProcAddress()函数时,可以利用MAKEINTRESOURCE()函数直接使用DLL中函数出现的顺序号,如将GetProcAddress(hDLL,"Min")改为GetProcAddress(hDLL, MAKEINTRESOURCE(2))(函数Min()在DLL中的顺序号是2),这样调用DLL中的函数速度很快,但是要记住函数的使用序号,否则会发生错误。

  本文通过通俗易懂的方式,全面介绍了动态链接库的概念、动态链接库的创建和动态链接库的链接,并给出个简单明了的例子,相信读者看了本文后,能够创建自己的动态链接库并应用到后续的软件开发当中去了,当然,读者要熟练操作DLL,还需要在大量的实践中不断摸索,希望本文能起到抛砖引玉的作用。

2006年06月19日

C语言测试是招聘嵌入式系统程序员过程中必须而且有效的方法。这些年,我既参加也组织了许多这种测试,在这过程中我意识到这些测试能为面试者和被面试者提供许多有用信息,此外,撇开面试的压力不谈,这种测试也是相当有趣的。
        从被面试者的角度来讲,你能了解许多关于出题者或监考者的情况。这个测试只是出题者为显示其对ANSI标准细节的知识而不是技术技巧而设计吗?这是个愚蠢的问题吗?如要你答出某个字符的ASCII值。这些问题着重考察你的系统调用和内存分配策略方面的能力吗?这标志着出题者也许花时间在微机上而不是在嵌入式系统上。如果上述任何问题的答案是"是"的话,那么我知道我得认真考虑我是否应该去做这份工作。
        从面试者的角度来讲,一个测试也许能从多方面揭示应试者的素质:最基本的,你能了解应试者C语言的水平。不管怎么样,看一下这人如何回答他不会的问题也是满有趣。应试者是以好的直觉做出明智的选择,还是只是瞎蒙呢?当应试者在某个问题上卡住时是找借口呢,还是表现出对问题的真正的好奇心,把这看成学习的机会呢?我发现这些信息与他们的测试成绩一样有用。
        有了这些想法,我决定出一些真正针对嵌入式系统的考题,希望这些令人头痛的考题能给正在找工作的人一点帮助。这些问题都是我这些年实际碰到的。其中有些题很难,但它们应该都能给你一点启迪。
        这个测试适于不同水平的应试者,大多数初级水平的应试者的成绩会很差,经验丰富的程序员应该有很好的成绩。为了让你能自己决定某些问题的偏好,每个问题没有分配分数,如果选择这些考题为你所用,请自行按你的意思分配分数。

预处理器(Preprocessor)

1 . 用预处理指令#define 声明一个常数,用以表明1年中有多少秒(忽略闰年问题)
         #define SECONDS_PER_YEAR (60 * 60 * 24 * 365)UL
我在这想看到几件事情:
1) #define 语法的基本知识(例如:不能以分号结束,括号的使用,等等)
2)懂得预处理器将为你计算常数表达式的值,因此,直接写出你是如何计算一年中有多少秒而不是计算出实际的值,是更清晰而没有代价的。
3) 意识到这个表达式将使一个16位机的整型数溢出-因此要用到长整型符号L,告诉编译器这个常数是的长整型数。
4) 如果你在你的表达式中用到UL(表示无符号长整型),那么你有了一个好的起点。记住,第一印象很重要。

2 . 写一个"标准"宏MIN ,这个宏输入两个参数并返回较小的一个。
        #define MIN(A,B) ((A) <= (B) ? (A) : (B))
这个测试是为下面的目的而设的:
1) 标识#define在宏中应用的基本知识。这是很重要的。因为在  嵌入(inline)操作符 变为标准C的一部分之前,宏是方便产生嵌入代码的唯一方法,对于嵌入式系统来说,为了能达到要求的性能,嵌入代码经常是必须的方法。
2)三重条件操作符的知识。这个操作符存在C语言中的原因是它使得编译器能产生比if-then-else更优化的代码,了解这个用法是很重要的。
3) 懂得在宏中小心地把参数用括号括起来
4) 我也用这个问题开始讨论宏的副作用,例如:当你写下面的代码时会发生什么事?
        least = MIN(*p++, b);

3. 预处理器标识#error的目的是什么?
如果你不知道答案,请看参考文献1。这问题对区分一个正常的伙计和一个书呆子是很有用的。只有书呆子才会读C语言课本的附录去找出象这种问题的答案。当然如果你不是在找一个书呆子,那么应试者最好希望自己不要知道答案。


死循环(Infinite loops)

4. 嵌入式系统中经常要用到无限循环,你怎么样用C编写死循环呢?
这个问题用几个解决方案。我首选的方案是:

while(1)
{

}

一些程序员更喜欢如下方案:

for(;;)
{

}

这个实现方式让我为难,因为这个语法没有确切表达到底怎么回事。如果一个应试者给出这个作为方案,我将用这个作为一个机会去探究他们这样做的基本原理。如果他们的基本答案是:"我被教着这样做,但从没有想到过为什么。"这会给我留下一个坏印象。

第三个方案是用 goto
Loop:

goto Loop;
应试者如给出上面的方案,这说明或者他是一个汇编语言程序员(这也许是好事)或者他是一个想进入新领域的BASIC/FORTRAN程序员。


数据声明(Data declarations)

5. 用变量a给出下面的定义
a) 一个整型数(An integer)
b)一个指向整型数的指针( A pointer to an integer)
c)一个指向指针的的指针,它指向的指针是指向一个整型数( A pointer to a pointer to an intege)r
d)一个有10个整型数的数组( An array of 10 integers)
e) 一个有10个指针的数组,该指针是指向一个整型数的。(An array of 10 pointers to integers)
f) 一个指向有10个整型数数组的指针( A pointer to an array of 10 integers)
g) 一个指向函数的指针,该函数有一个整型参数并返回一个整型数(A pointer to a function that takes an integer as an argument and returns an integer)
h) 一个有10个指针的数组,该指针指向一个函数,该函数有一个整型参数并返回一个整型数( An array of ten pointers to functions that take an integer argument and return an integer )

答案是:
a) int a; // An integer
b) int *a; // A pointer to an integer
c) int **a; // A pointer to a pointer to an integer
d) int a[10]; // An array of 10 integers
e) int *a[10]; // An array of 10 pointers to integers
f) int (*a)[10]; // A pointer to an array of 10 integers
g) int (*a)(int); // A pointer to a function a that takes an integer argument and returns an integer
h) int (*a[10])(int); // An array of 10 pointers to functions that take an integer argument and return an integer

人们经常声称这里有几个问题是那种要翻一下书才能回答的问题,我同意这种说法。当我写这篇文章时,为了确定语法的正确性,我的确查了一下书。但是当我被面试的时候,我期望被问到这个问题(或者相近的问题)。因为在被面试的这段时间里,我确定我知道这个问题的答案。应试者如果不知道所有的答案(或至少大部分答案),那么也就没有为这次面试做准备,如果该面试者没有为这次面试做准备,那么他又能为什么出准备呢?

Static

6. 关键字static的作用是什么?
这个简单的问题很少有人能回答完全。在C语言中,关键字static有三个明显的作用:
1)在函数体,一个被声明为静态的变量在这一函数被调用过程中维持其值不变。
2) 在模块内(但在函数体外),一个被声明为静态的变量可以被模块内所用函数访问,但不能被模块外其它函数访问。它是一个本地的全局变量。
3) 在模块内,一个被声明为静态的函数只可被这一模块内的其它函数调用。那就是,这个函数被限制在声明它的模块的本地范围内使用。

大多数应试者能正确回答第一部分,一部分能正确回答第二部分,同是很少的人能懂得第三部分。这是一个应试者的严重的缺点,因为他显然不懂得本地化数据和代码范围的好处和重要性。


Const

7.关键字const有什么含意?
我只要一听到被面试者说:"const意味着常数",我就知道我正在和一个业余者打交道。去年Dan Saks已经在他的文章里完全概括了const的所有用法,因此ESP(译者:Embedded Systems Programming)的每一位读者应该非常熟悉const能做什么和不能做什么.如果你从没有读到那篇文章,只要能说出const意味着"只读"就可以了。尽管这个答案不是完全的答案,但我接受它作为一个正确的答案。(如果你想知道更详细的答案,仔细读一下Saks的文章吧。)
如果应试者能正确回答这个问题,我将问他一个附加的问题:
下面的声明都是什么意思?

const int a;
int const a;
const int *a;
int * const a;
int const * a const;

/******/
前两个的作用是一样,a是一个常整型数。第三个意味着a是一个指向常整型数的指针(也就是,整型数是不可修改的,但指针可以)。第四个意思a是一个指向整型数的常指针(也就是说,指针指向的整型数是可以修改的,但指针是不可修改的)。最后一个意味着a是一个指向常整型数的常指针(也就是说,指针指向的整型数是不可修改的,同时指针也是不可修改的)。如果应试者能正确回答这些问题,那么他就给我留下了一个好印象。顺带提一句,也许你可能会问,即使不用关键字 const,也还是能很容易写出功能正确的程序,那么我为什么还要如此看重关键字const呢?我也如下的几下理由:
1) 关键字const的作用是为给读你代码的人传达非常有用的信息,实际上,声明一个参数为常量是为了告诉了用户这个参数的应用目的。如果你曾花很多时间清理其它人留下的垃圾,你就会很快学会感谢这点多余的信息。(当然,懂得用const的程序员很少会留下的垃圾让别人来清理的。)
2) 通过给优化器一些附加的信息,使用关键字const也许能产生更紧凑的代码。
3) 合理地使用关键字const可以使编译器很自然地保护那些不希望被改变的参数,防止其被无意的代码修改。简而言之,这样可以减少bug的出现。


Volatile

8. 关键字volatile有什么含意?并给出三个不同的例子。
一个定义为volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。精确地说就是,优化器在用到这个变量时必须每次都小心地重新读取这个变量的值,而不是使用保存在寄存器里的备份。下面是volatile变量的几个例子:
1) 并行设备的硬件寄存器(如:状态寄存器)
2) 一个中断服务子程序中会访问到的非自动变量(Non-automatic variables)
3) 多线程应用中被几个任务共享的变量

回答不出这个问题的人是不会被雇佣的。我认为这是区分C程序员和嵌入式系统程序员的最基本的问题。搞嵌入式的家伙们经常同硬件、中断、RTOS等等打交道,所有这些都要求用到volatile变量。不懂得volatile的内容将会带来灾难。
假设被面试者正确地回答了这是问题(嗯,怀疑是否会是这样),我将稍微深究一下,看一下这家伙是不是直正懂得volatile完全的重要性。
1)一个参数既可以是const还可以是volatile吗?解释为什么。
2); 一个指针可以是volatile 吗?解释为什么。
3); 下面的函数有什么错误:

int square(volatile int *ptr)
{
        return *ptr * *ptr;
}

下面是答案:
1)是的。一个例子是只读的状态寄存器。它是volatile因为它可能被意想不到地改变。它是const因为程序不应该试图去修改它。
2); 是的。尽管这并不很常见。一个例子是当一个中服务子程序修该一个指向一个buffer的指针时。
3) 这段代码有点变态。这段代码的目的是用来返指针*ptr指向值的平方,但是,由于*ptr指向一个volatile型参数,编译器将产生类似下面的代码:

int square(volatile int *ptr)
{
    int a,b;
    a = *ptr;
    b = *ptr;
    return a * b;
}

由于*ptr的值可能被意想不到地该变,因此a和b可能是不同的。结果,这段代码可能返不是你所期望的平方值!正确的代码如下:

long square(volatile int *ptr)
{
    int a;
    a = *ptr;
    return a * a;
}

位操作(Bit manipulation)

9. 嵌入式系统总是要用户对变量或寄存器进行位操作。给定一个整型变量a,写两段代码,第一个设置a的bit 3,第二个清除a 的bit 3。在以上两个操作中,要保持其它位不变。
对这个问题有三种基本的反应
1)不知道如何下手。该被面者从没做过任何嵌入式系统的工作。
2) 用bit fields。Bit fields是被扔到C语言死角的东西,它保证你的代码在不同编译器之间是不可移植的,同时也保证了的你的代码是不可重用的。我最近不幸看到 Infineon为其较复杂的通信芯片写的驱动程序,它用到了bit fields因此完全对我无用,因为我的编译器用其它的方式来实现bit fields的。从道德讲:永远不要让一个非嵌入式的家伙粘实际硬件的边。
3) 用 #defines 和 bit masks 操作。这是一个有极高可移植性的方法,是应该被用到的方法。最佳的解决方案如下:

#define BIT3 (0×1 << 3)
static int a;

void set_bit3(void)
{
    a |= BIT3;
}
void clear_bit3(void)
{
    a &= ~BIT3;
}

一些人喜欢为设置和清除值而定义一个掩码同时定义一些说明常数,这也是可以接受的。我希望看到几个要点:说明常数、|=和&=~操作。


访问固定的内存位置(Accessing fixed memory locations)

10. 嵌入式系统经常具有要求程序员去访问某特定的内存位置的特点。在某工程中,要求设置一绝对地址为0×67a9的整型变量的值为0xaa66。编译器是一个纯粹的ANSI编译器。写代码去完成这一任务。
这一问题测试你是否知道为了访问一绝对地址把一个整型数强制转换(typecast)为一指针是合法的。这一问题的实现方式随着个人风格不同而不同。典型的类似代码如下:
    int *ptr;
    ptr = (int *)0×67a9;
    *ptr = 0xaa55;

 A more obscure approach is:
一个较晦涩的方法是:

    *(int * const)(0×67a9) = 0xaa55;

即使你的品味更接近第二种方案,但我建议你在面试时使用第一种方案。

中断(Interrupts)

11. 中断是嵌入式系统中重要的组成部分,这导致了很多编译开发商提供一种扩展—让标准C支持中断。具代表事实是,产生了一个新的关键字 __interrupt。下面的代码就使用了__interrupt关键字去定义了一个中断服务子程序(ISR),请评论一下这段代码的。

__interrupt double compute_area (double radius)
{
    double area = PI * radius * radius;
    printf("\nArea = %f", area);
    return area;
}

这个函数有太多的错误了,以至让人不知从何说起了:
1)ISR 不能返回一个值。如果你不懂这个,那么你不会被雇用的。
2) ISR 不能传递参数。如果你没有看到这一点,你被雇用的机会等同第一项。
3) 在许多的处理器/编译器中,浮点一般都是不可重入的。有些处理器/编译器需要让额处的寄存器入栈,有些处理器/编译器就是不允许在ISR中做浮点运算。此外,ISR应该是短而有效率的,在ISR中做浮点运算是不明智的。
4) 与第三点一脉相承,printf()经常有重入和性能上的问题。如果你丢掉了第三和第四点,我不会太为难你的。不用说,如果你能得到后两点,那么你的被雇用前景越来越光明了。


代码例子(Code examples)

12 . 下面的代码输出是什么,为什么?

void foo(void)
{
    unsigned int a = 6;
    int b = -20;
    (a+b > 6) ? puts("> 6") : puts("<= 6");
}
这个问题测试你是否懂得C语言中的整数自动转换原则,我发现有些开发者懂得极少这些东西。不管如何,这无符号整型问题的答案是输出是 ">6"。原因是当表达式中存在有符号类型和无符号类型时所有的操作数都自动转换为无符号类型。因此-20变成了一个非常大的正整数,所以该表达式计算出的结果大于6。这一点对于应当频繁用到无符号数据类型的嵌入式系统来说是丰常重要的。如果你答错了这个问题,你也就到了得不到这份工作的边缘。

13. 评价下面的代码片断:

unsigned int zero = 0;
unsigned int compzero = 0xFFFF;
/*1’s complement of zero */

对于一个int型不是16位的处理器为说,上面的代码是不正确的。应编写如下:

unsigned int compzero = ~0;

这一问题真正能揭露出应试者是否懂得处理器字长的重要性。在我的经验里,好的嵌入式程序员非常准确地明白硬件的细节和它的局限,然而PC机程序往往把硬件作为一个无法避免的烦恼。
到了这个阶段,应试者或者完全垂头丧气了或者信心满满志在必得。如果显然应试者不是很好,那么这个测试就在这里结束了。但如果显然应试者做得不错,那么我就扔出下面的追加问题,这些问题是比较难的,我想仅仅非常优秀的应试者能做得不错。提出这些问题,我希望更多看到应试者应付问题的方法,而不是答案。不管如何,你就当是这个娱乐吧…


动态内存分配(Dynamic memory allocation)

14. 尽管不像非嵌入式计算机那么常见,嵌入式系统还是有从堆(heap)中动态分配内存的过程的。那么嵌入式系统中,动态分配内存可能发生的问题是什么?
这里,我期望应试者能提到内存碎片,碎片收集的问题,变量的持行时间等等。这个主题已经在ESP杂志中被广泛地讨论过了(主要是 P.J. Plauger, 他的解释远远超过我这里能提到的任何解释),所有回过头看一下这些杂志吧!让应试者进入一种虚假的安全感觉后,我拿出这么一个小节目:
下面的代码片段的输出是什么,为什么?

char *ptr;
if ((ptr = (char *)malloc(0)) == NULL)
    puts("Got a null pointer");
else
    puts("Got a valid pointer");

这是一个有趣的问题。最近在我的一个同事不经意把0值传给了函数malloc,得到了一个合法的指针之后,我才想到这个问题。这就是上面的代码,该代码的输出是"Got a valid pointer"。我用这个来开始讨论这样的一问题,看看被面试者是否想到库例程这样做是正确。得到正确的答案固然重要,但解决问题的方法和你做决定的基本原理更重要些。

Typedef
 
15 Typedef 在C语言中频繁用以声明一个已经存在的数据类型的同义字。也可以用预处理器做类似的事。例如,思考一下下面的例子:

#define dPS struct s *
typedef struct s * tPS;

以上两种情况的意图都是要定义dPS 和 tPS 作为一个指向结构s指针。哪种方法更好呢?(如果有的话)为什么?
这是一个非常微妙的问题,任何人答对这个问题(正当的原因)是应当被恭喜的。答案是:typedef更好。思考下面的例子:

dPS p1,p2;
tPS p3,p4;

第一个扩展为

struct s * p1, p2;
.
上面的代码定义p1为一个指向结构的指,p2为一个实际的结构,这也许不是你想要的。第二个例子正确地定义了p3 和p4 两个指针。



晦涩的语法

16 . C语言同意一些令人震惊的结构,下面的结构是合法的吗,如果是它做些什么?

int a = 5, b = 7, c;
c = a+++b;

这个问题将做为这个测验的一个愉快的结尾。不管你相不相信,上面的例子是完全合乎语法的。问题是编译器如何处理它?水平不高的编译作者实际上会争论这个问题,根据最处理原则,编译器应当能处理尽可能所有合法的用法。因此,上面的代码被处理成:

c = a++ + b;

因此, 这段代码持行后a = 6, b = 7, c = 12。
如果你知道答案,或猜出正确答案,做得好。如果你不知道答案,我也不把这个当作问题。我发现这个问题的最大好处是这是一个关于代码编写风格,代码的可读性,代码的可修改性的好的话题。


好了,伙计们,你现在已经做完所有的测试了。这就是我出的C语言测试题,我怀着愉快的心情写完它,希望你以同样的心情读完它。如果是认为这是一个好的测试,那么尽量都用到你的找工作的过程中去吧。天知道也许过个一两年,我就不做现在的工作,也需要找一个。

作者介绍:
        Nigel Jones 是一个顾问,现在住在Maryland,当他不在水下时,你能在多个范围的嵌入项目中找到他。 他很高兴能收到读者的来信,他的email地址是: NAJones@compuserve.com

参考文献
1) Jones, Nigel, "In Praise of the #error directive," Embedded Systems Programming, September 1999, p. 114.
2) Jones, Nigel, " Efficient C Code for Eight-bit MCUs ," Embedded Systems Programming, November 1998, p. 66.

关于c++的编译器现在有很多,各有所长。不可能每个人都全部用过。  
我想统计一下现在的主流编译器都有什么。linux,windows,dos下的什么编译器最流行,也最好!请各位发表意见并给出你所用的编译器的好处。  
—————————————————————  
我主要从事windows下的编程,所以我一般用vc编译,毕竟是当今软件霸主的产品,性能和功能自然不必问了。就是编译速度有点慢。所以我在写小程序的时候有lcc编译器。性能也不错,而且编译速度极快。  
还有就是我觉得vc对于初学者来说可能不是好的编译器,因为mfc把所有的程序执行机制都封装在它的类里面。初学者看到其代码可能不明所以然。但是lcc不错,很适合初学者。它是标准的sdk编译器。程序的运行机制和消息处理机制写的非常清楚。  
大家见仁见智吧!  
我的选择:
WINDOWS首选VC  初学者,小项目用LCC  

我主要用这两种编译器,其他的很少用,所以不敢评价。请大家评论。  
—————————————————————  
我这儿有数十种C/C++编译器。  
如下:  
GCC家族有  
   Cygwin  
   Mingw32  
   DJGPP  
   Dev-C++(Mingw32)  
   还有正宗的GNU  GCC  2.95.5~3.0.0.4版本  
MS家族有  
   MSC  5.0、6.0、7.0  
   MSQC  1.0、2.5  
   MSVC  1.0、4.2、6.0、7.0  
Borland家族有  
   TC  1.0、2.0  
   TC++  1.01、3.0  
   BC  3.0、3.1、4.0、4.5、5.0、5.02  
   BCB  3.0、5.0、6.0  
其它有  
   Intel  C/C++  5.0  
   Watcom  C/C++  11.0、11.0c  
   VectorC  1.3.3  
   IBM  VisualAge  for  C++  
   DigitalMars  C/C++  
   KAI  C/C++  4.03f  for  RedHat  7.2  
   Lcc4.1  
   LCC-WIN32  2001-09-25~2002-04-28日版  
   Small  C  
   CC386  
   Pacific  C  
另外还有C的解释器  
   Quincy  
   Eic  
   CINT  
     
   上面提到的编译器/解释器,大部分我都使用过。现在固定使用VC7.0  Cygwin  Mingw32  VectorC和LCC-WIN32这五种编译器。

   在GCC家族中GNU  GCC是根本,其它的编译器版本都是从它导出的。其中,Cygwin和Mingw32都是WIN32平台下的编译器,DJGPP是DOS下的32位编译器。大家所熟知的DEV-C++充其量只是GCC的一个外壳,它所自带的编译器就是Mingw32的一个版本。这些GCC的版本中,Cygwin是最大的,它与其说是一个编译器,倒不如说是一套编程工具。它不仅有编译器,还有其它很多的工具。其实,它就是一个UNIX系统在WIN32平台上的实现。实现了大多常用的UNIX工具,最近的版本中连Apache这样的“工具”都集成进来的。不过,Cygwin虽然功能强大,但它却不是很易用(和UNIX相似,熟悉UNIX的人用它可以很快上手),因为太多其它的工具分散了人们的注意力。相比之下Mingw32就要好用得多,它只有最基本的几个编程工具(只可惜它不自带GDB)。GCC中并不只是C/C++编译器,其中还有很多其它的编译器如JAVA,Fortran,ADA等。它是一个编译器集合,不过有些编译器只能在UNIX系统上用。MS家族的编译器就不用说了,大家对它们都很熟悉。VC  7.0(VC.NET)是它的最新产品。Borland家族也不用说,大家也是耳熟能详。最近它才推出了BCB 6.0。
  
   其它的编译器如:Intel  C/C++大家一看名称就知道是Intel的东西,它和VC6完全兼容,不过要挂在VC6下才能用。Watcom  C/C++是早先编译器四国大战中的一员,原本是很不错的东西,可惜战略不对,现在已不见声息了。倒是以它为基础的一个OpenWatcom现在还在奋战。VectorC是我近日才发现的一个好东东,它是个纯C的编译器。IBM的VisualAge for  C++原本是IBM想用来淌C++编译器这片浑水的东西,不过IBM的战略改了,它就被放弃了。DigitalMars  C/C++的前身的Symantec C++(它也是编译器四国大战中的一员),不过现在Symantec不做了,于是它的作者就把它改成了DigitalMars  C/C++开放给大家使用。以上这些都是WIN32平台上的东西。KAI  C/C++是个很强大的C/C++编译器,它是个多平台的编译器。不过现在被INTEL收购了,已经停止开发了。Lcc4.1是个纯C的编译器它是开放源代码的。不过不怎么好用。LCC-WIN32是一个在LCC基础上开发的C语言的集成开发环境,很好用,而且有很详细的资料,FREE!Pacific  C是一个纯DOS的C的集成开发环境,就不多说了。Small  C  CC386都是开放源代码的编译器,它们都很简单,应用来给大家学习编译器的。Quincy  Eic  CINT都是C的解释器,是用来让大家学习C语言的其中CINT的功能很强大,还支持一些C++的特性。  
   当然还有很多其它的编译器,这里我给出的编译器都是可以在WIN32或DOS平台上用的(除KAI外)。UNIX平台上的编译器还是以GNU的为主,其它的我就不是很清楚了。

   在以上的编译器中,最特别的就是VectorC这个东西只支持纯C。但它却号称是最快的编译器,不过经过我的试验,它的确在有些情况下强过其它编译器很多!而且它还有个交互式的优化器,可以让你直接看到C代码对映的汇编代码。Cygwin和Mingw32为一母所生,其运行效果相差不大。它们生成的代码效率都很不错,编译的速度也很快,最值得一提的是它们对C++的特性的支持算是所有编译器中最完全的,而且它们还支持C99的大部分特性。这一点很是不错!大家对MS的VC已经很熟悉了,本不用我多说。不过在它的最新的产品VC7.0中,有很大的改进。它对C++的特性的支持比6.0有了很大的提高,是我所用的编译器中是仅次于GCC的。而且它编译出的程序,运行速度很快!仅有少数时候次于VectorC与GCC,其它情况都是最快的!其平均运行速度是最快的。对Borland的产品我也无需多说。它的TC2.0与BC3.1都是我最喜欢的东西。可是现在的BCB却大不如前了,编译的速度和VC6一样慢!IDE还有较多的BUG。最令人想不通的是它生成的代码的运行速度很慢,比LCC-WIN32还慢!它唯一值得一提的就是它的RAD做的比MS的好。Intel的编译器大家可能不熟,它太贵了!还要有VC的支持,很不划算,而且编译速度比VC6还慢。不过它的代码质量很不错。DigitalMars  C/C++没有什么亮点,编译速度较快,代码执行速度适中,对C++特性支持还算不错。LCC-WIN32是个很不错的集成开发环境,它只支持纯C。它的编译速度极快!代码执行速度较慢。不过它的最大亮点在于它的IDE,在所有的FREE编程工具中,它的IDE是最专业的,有很强大的代码分析,管理功能。而且它提供了大量的编程资料。  
   我曾对一些编译器的代码执行效率做过一些测试,以下是概况:  
   1.  VectorC、VC  7.0  (极快)  
   2.  Intel  C/C++、VC  6.0、GCC  (很快)  
   3.  DigitalMars  C/C++  (一般)  
   4.  LCC-WIN32、BCB、BC5.02  (较慢)  
当然,我所做的测试比较片面。不过在很大程度上已能反映其大概状况。  
(我也曾看到过一个测试,其中Intel的编译器是最快的,比VC  6.0和GCC要快10%~50%,而我所测的结果中,GCC  >=VC6  >Intel C/C++ )  
(以上评论都是个人观点)