KeyFansClub

首页 » - 特色讨论区 - » 键社茶餐厅 » [M] Prelude to K.O. (4)
Prz - 2007/5/16 18:08:00
K.O.(KeyFC Open Translation Toolset) 是目前正在开发的“开放版本”汉化工具的代号。
其中的汉化部分工具试验性的采用了代号为Chobits Application Server(CAS)的架构。
这个结构的特点为,程序全部由基于消息传递的模块动态组合而成。

前面几回介绍了CAS架构的理念,内部结构,消息的接口、定义及传递。
至此,各位所了解的CAS还处于"空想模块化"阶段——理论上写出模块化的程序是可行的。
那么具体的实现上呢?这就是今天要谈的内容: CAS模块的结构、封装和程序的组装

--- CAS模块 ---

从理论模块化到实际模块化,CAS选择了一个最直接的方法:一对一"物理"映射。
也就是说,每一个CAS的"功能模块"都映射到一个实际的文件上。
文件的格式自然也就是"动态加载库",在Windows下叫DLL(Dynamic Loading Library),在Linux下叫SO(Shared Object)。

目前CAS仅有Windows的版本;但因为整个CAS除了少数地方使用同步对象外,并没有其他平台相关的代码,因此能够被很容易的移植到Linux下。
其中核心组件Lock-free队列我为了在8CPU环境下验证正确性已经实现在Linux下了(但是由于Linux pthreads的Condition同步对象缺少Timed Wait(带时限的等待)方法,暂时没有写出带锁同步队列作性能比较)。
有时间的话,我会尝试包裹Posix Thread中的同步对象(支持Timed Wait),这样移植CAS应该就没有什么大碍了。
(虽然Interface在CAS的使用是兼容COM规范的,但是其用途并不COMish,因此不影响Linux移植。)

书归正传,那么,也就是说模块的加载需要靠"动态加载库"文件(以下简称DLL)的接口来进行了。
(下面的部分将涉及一些抽象程序代码和具体API)

--- CAS模块封装 ---

因为世界上没有未卜先知,因此CAS模块的接口必须提供一个固定的方法集合才能够被正确的加载。
不过,因为每个人都有自己写加载方法的自由,因此这里讲的只能算"官方推荐",供使用者参考(当然,作为"官方使用者",K.O.系列的程序将遵循这里介绍的接口)


I: 首先要解决的问题是,怎样知道加载的DLL是合法的CAS模块?
因此,我规定每个CAS模块DLL都需要导出(Export)一个函数: ChobitsModuleSignature
这个函数返回一个64字节的记录(没有硬性规定意义,可以是代码的MD5,或者是数码签名等),目的是让加载方能够作选择性的加载。


II: 接下来,需要确定的是加载的模块使用的是兼容的接口和消息定义。
使用动态加载库的一个著名的问题就是"DLL地狱",由于同一个DLL的不同版本可能使用不同的接口,如果管理不善可能导致各种奇怪的问题。
为了解决好这个问题,我规定每个CAS模块DLL都需要导出(Export)另一个函数: ChobitsModuleInfo
这个函数返回多种信息,包括模块名称、GUID、消息队列长度、需要多少工作线程等等;
其中有一条信息(32位整数),称作"兼容总线版本",详细的定义如下:

* 高8位: 总线运行级别
  在前面的介绍中,我有意模糊了总线的具体功能,现在给出更清晰的定义。目前定义的总线有三个运行级:
  0. 静态总线: 仅提供了模块注册、查询和消息传送支持等基本功能;总线内部没有线程调度,因此不执行消息投递。好处是轻载(light weight),适用于消息较少的应用环境。(K.O.系列程序使用的就是这个总线)
  1. 投递总线: 顾名思义,就是增加了消息投递支持。适用于消息较密集的应用环境。(由于涉及到事件顺序,加上前段时间的总线代码重构(code re-factor),目前仍然在编写/调试中...)
  2. 管理总线: 在前一个级别的基础上,增加了一个管理线程,用于执行管理工作(总线统计信息收集、消息队列阻塞检测、失效队列回收;当然最重要的是接受队列注册、注销请求——有了这个功能,就可以使用消息来注册消息队列,这也就意味着,可以由一个模块来加载另一个模块)。这个总线适用于较大型的服务和应用程序,因为它可以自动支持一个动态的执行环境(比如动态的替换一个模块的库文件到新的版本,而不需要重新启动整个应用程序)。
  因为每一个运行级提供的支持都是前一个运行级的超集(Super-set),因此,自然的,兼容低级别总线的模块可以加载到高级别总线上,反过来则不行。

* 中间12位: 总线的主版本号
  前几回说到了,总线和模块之间唯一需要交互的两处就是: 模块消息接口——用于发送和接收消息;基本消息接口——用于处理消息的传递过程。
  同一个主版本号的总线将使用相同的模块消息接口和基本消息接口。换句话说,就是一旦模块消息接口和基本消息接口发生任何改变,主版本号将会提升。
  这也就意味着,兼容不同主版本号总线的模块一定不能被加载,不论谁高谁低。

* 后面12位: 总线的兼容版本号
  如果总线发生了一些细小的修订,但是没有更改模块消息接口或者基本消息接口,那么兼容版本号将会提升。
  因为接口并未改变,所以兼容旧版本总线的模块将可以使用在新的总线上;但是,因为兼容新总版本总线的模块将会期待总线一定程度上的变化,因此不能用于旧版本的总线。

最后举一下例子,以免有人看晕。以下版本号的格式为: [运行级别] [主版本号] [兼容版本号]
如果总线版本为:
2 2 2
模块兼容版本为:
2 2 2 可以加载
1 2 2 可以加载  (期待运行级别低可以)
3 2 2 不可以加载 (期待运行级别高不行)
2 2 1 可以加载  (兼容前一个修订版本可以)
2 2 3 不可以加载 (兼容后一个修订版本不行)
x y z 只要y不是2就不可以加载 (主版本不一致)


III: 接下来就是模块特定导出函数了。
在目前使用的规范中,只有两种不同的模块: 总线模块(是的总线也可以是一个单独的DLL :D) 和 应用模块
总线模块导出两个函数: CMB_Request (请求总线) 和 CMB_Drop (丢弃总线)
服务模块导出三个函数: CAM_Attach (接驳总线),CAM_Detach (脱离总线) 和 CAM_Finalize (结束应用)
(注意,因为目前CAS的应用仅仅在总线运行级别0上的,因此更高级别的总线以及对应服务模块*可能*会有更多的导出函数。但是这里已经列出的函数及接口将保持不变)

* 先谈总线模块 (Chibots Bus Module, CMB):

CMB_Request 需要一个总线ID参数,返回一个总线管理接口(Interface)。(是的,如果你愿意,同一个应用程序里面可以存在多个总线。总线之间是相互独立的,没有内置的交互能力,因此怎么使用嘛就是你的事了=v=)
如果指定ID的总线不存在则创建一个新的;如果不能创建(达到个数上限),则返回空指针。

CMB_Drop 需要一个总线ID参数。其功能是将符合ID的总线的内置记录清除,这样下一次请求同样ID时,将创建一个新的总线。
注意,因为总线是通过管理接口引用的,因此也是引用计数的。调用CMB_Drop和总线是否被释放没有任何关系。(当然,推荐合理的使用方法为即将释放总线前调用CMB_Drop,然后将管理接口置空,总线将被自动释放)

* 然后是服务模块 (Chobits Application Module, CAM):

模块加载程序在调用了前面提到的ChobitsModuleInfo,检查版本符合之后,应该根据同时返回的其他信息为正在加载的模块收集资源。(比如,使用返回的名称和GUID在总线上注册一个消息接口;同时生成要求数量的线程)
准备完毕后,调用服务模块的CAM_Attach,将资源递交给模块。
这时,模块应该做的事情有:
* 初始化内部结构
* 记录传递的消息接口
* 将传入的线程(一般来说,只有一个)分配到指定的任务(一般来说,是消息处理循环)


接下来,将会有很长一段时间DLL的接口不再有动静。因为所有的交互都通过消息接口在总线上进行...


当由于某种原因(一般来说,是程序结束),消息队列将被从总线取下,模块加载程序应当首先调用CAM_Detach。(如果顺序颠倒,服务模块在发送消息时将会受到一个异常: 队列已经从总线脱离)
收到这个调用的时候,服务模块应该做的事情是:
* 将分配的线程从任务脱离 (不需要终结,它们将会被线程池自动回收)
* 如果必要的话,做出适当清理,为下一个接驳总线的请求做好准备

当CAM_Detach返回后,模块加载程序就可以放心的从总线注销(一般来说是)消息队列了。

最后,当模块加载器需要卸载模块前,应该调用CAM_Finalize,这个时候模块要做的事情就是:
* 停止正在执行的操作(本来这个时候不应该有了,但是以防万一...)
* 释放申请的所有资源

当CAM_Finalize返回后,模块加载程序就可以放心的卸载模块DLL了。


在细节里面滚打了大半圈,让我们重新回到高处,俯瞰CAS架构。

--- CAS程序的组装 ---

因为运行级别3的CAS总线的操作和使用环境与前两个有一定不同,我们分开来看。

前两个级别的CAS总线需要由独立于总线的管理操作控制(加载/卸载应用模块),因此CAS应用程序的(建议)组装结构应该像下图:


(图1) CAS应用程序

* 处理应用程序图形界面(UI),同时负责初始化的线程最先被创建(主线程);
  这个线程创建一个管理线程,用于初始化和终止CAS架构,同时负责响应抽象用户请求。

* 初始化开始,总线模块首先载入,各个模块依次载入并在总线上注册,相应的模块线程被创建;
  初始化最后,第二个UI线程被创建(图中与主线程重合),用于将UI事件转换成抽象用户请求,以及将结果反映到UI上。
  (创建第二个UI线程的原因是,Windows有一套自己的窗体消息机制,(主要)用于和用户输入打交道,因此主线程有自己的消息循环需要处理。)

  什么?你问既然有了Window消息为什么还要CAS?晕...两套消息机制的设计目的以及用途非常不同,其他的(比如多线程同步效率)不说,一个很简单的例子就能说明问题:当你的鼠标划过一个窗口时,这个窗口的消息队列就会接受到成百条鼠标位置的信息,如果这个时候另外几个模块正在通过Window消息队列频繁的通讯的话,这个队列就会更长,导致所有的消息都得不到及时处理,这个时候的现象就是程序的窗口响应迟钝同时后台计算也变慢...可见将图形界面的消息和内部运算的消息混在一起的话,两者的处理都会受到影响。

* 回到刚才说的,所有线程都进入自己的消息循环,程序正式开始工作:
  用户的窗体操作被包装成为高级的应用请求消息,传递到管理线程;
  管理线程的程序将高级请求转换成为一系列的模块功能请求消息,然后分发到各个模块;
  (对于有顺序要求的消息,状态机也好,跨模块调用(IMC)也好,根据个人的风格和期望的效果实现,CAS是一个非常自由的架构! :D)
  根据各个模块反馈的结果,管理线程组装一个UI反馈消息,传递给UI线程,然后由其将消息解释成为图形界面的状态,反馈给用户。

* 最后,程序即将结束,管理线程首先通知各个模块队列取下,接下来依次注销消息队列,然后卸载各个功能模块,总线模块,最后返回;
  而主线程则一直等待线程池将所有线程回收,最后清理资源,终止程序。

注意图中有几条虚线,他们表示的意思是,UI线程和管理线程是作为功能模块挂接到总线上的,但是实际上他们并没有一个单独的DLL文件,因此称为"伪模块"(Pseudo-Module)。



级别2的CAS总线(管理总线)一般应用在大型的服务程序中。因为其本身具有一定的管理能力,因此CAS服务程序的(建议)组装结构应该像下图:


(图2) CAS服务程序

* 一般情况下,CAS消息总线将会和主程序形成一个模块;
  主线程创建后,首先创建消息总线(同前一种程序一样,可以创建多个,根据用途而定)
  然后,作为启动(Bootstrap)过程,主线程试图载入预先配置好的一个模块管理模块,注册并挂接;
  接下来,根据需要,主线程可以注册一个伪模块,以便和总线交互。(图中虚线省略了)

* 挂接后,具有管理功能的总线将向模块发出通知消息,模块管理模块捕获这个消息,并开始工作;
  模块管理模块可按照预设定或者某种特定顺序载入各个模块,每个模块独自捕获总线载入通知开始动作。
  注册/注销等消息队列管理操作都将以请求的方式发送给总线,由总线管理线程执行,并且消息队列取下的通知也由总线发出,各个模块应该独立相应。
  因此模块管理模块需要做的操作仅仅是载入,通知挂接,和卸载模块。

* 一般情况下,主线程没有其他事情可做,只需要等待总线的"结束程序"消息(CAS消息或者同步事件);
  然后,首先卸载模块管理模块(在此之前,总线已经通知其队列取下),接下来释放资源,结束程序。

如果需要用户界面,可以将其单独包装在一个模块中予以加载。

可以看得出来,服务程序的流程更加结构化;当然,这主要是因为总线实现了许多附加的功能。


------

现在,坚持跟进的各位应该对CAS架构有了一个初步的了解。如果各位还有什么不清楚的地方,欢迎回复反馈,我在合适的时候将集中解答。

最近正在对底层消息的实现作比较大的改变,主要是软件工程上的,比如统一总线内部对各种消息的处理等;
其中包括前面提到的,将总线对于IMC(跨模块调用)消息的支持通用化,使得基于CAS的开发者能够写出自定义的IMC消息。
当然,这些更改对于前面介绍的抽象概念和高层操作基本没有影响,请各位放心消化。 :P
dwing - 2007/5/16 19:22:00
感觉这目前这4章讲述的越来越乱了.其实只要几张图及少量说明就能了解总体框架了.
我一直认为"simple is best".这个系统现在看起来还是有些复杂,我们经常遇到越复杂的东西越容易出问题,模块开发者也会感到困惑和误用.一个模块可以简洁地只导出一个函数,返回接口指针即可,此时模块就可以做初始化,不必再调用attach.
当然我还没能全面了解,不清楚一些复杂功能的必要性.
COM就是为了跨编程语言而没有内在地使用智能指针,而多出了AddRef/Release的接口.当然C++的广泛应用使得大多数架构不必做到跨编程语言.
想必CAS也是用C++实现的吧.如果能提供一些sample代码,就能更清楚地了解运行的流程.
keakon - 2007/5/17 6:57:00
处理几百条消息也用不了1秒,人感觉不到就行了,又不是设计导弹=。=

基本上现在不是消息处理不过来,是硬盘和内存之间交换数据才会卡的,窗口显示不了看看你的硬盘是不是狂转吧

看样子是不用MFC了,WIN32 SDK写个东西真麻烦……
Prz - 2007/5/17 8:38:00
...果然写得太多会导致人发晕...

1楼的意见都是针对封装的。
但是我一开始就提到,CAS的主要核心是程序的设计和实现方法,至于如何封装,是使用者个人的风格和程序的功能要求问题;各种不同的封装方法、形式都是完全可以支持的。
这一次的谈内容只是建议的方法,并且每一步的作用都有详细的解释,至于是否必要,就看个人对于最终用户体验的要求了。
比如,1楼提到可以直接扔一个接口了事。这种封装方法虽然也不影响正常情况的功能,但是小心我提到的"DLL地狱"——如果总线或者不同的模块编译时使用的接口不一致,轻则出现内存漏洞,或者程序AV(非法访问),更严重的可能导致模块的"非出错性"错误操作(表面看上去工作正常,实际上做的东西是完全错误的)。


2楼对于消息处理的理解也是有问题的。
其中描述的"窗口显示不了看看你的硬盘是不是狂转吧"仅仅是将一些表面现象机械的联系在一起。
我承认很多情况下“窗口显示不了”时硬盘确实在“狂转",但是,出现这个现象的根本原因还是窗口消息队列的阻塞:处理窗体消息的线程因为阻塞在了一些低速IO操作上,导致不能对窗口重绘事件及时响应。
而且,很多情况下这就是因为使用了我所描述的“不良”的消息处理机制——将程序的内部功能的消息与用户界面的消息混合在一个队列。
(是啊,平时写一次硬盘也就几十个毫秒,没什么大不了;但是关键是时刻就会让你等上几秒到几十秒,就等着窗口翻白吧......)

在CAS架构下,就算硬盘“狂转”,用户界面(窗口)依旧是响应灵活的,就是因为消息处理分离——处理窗口消息的线程只管绘制窗口,也就不会阻塞在低速IO上。
(当然,机器内存不足导致系统产生的大量页面交换的情况除外;不过这种情况下,任何用户程序都会响应迟缓,这就是另外一个问题了...)

(另, 1楼的"想必"是错误的... ^^ Windows版本的CAS是用Object Pascal实现的)


------

哦,对了,关于"越来越乱"的解释。
怎么说呢,还是老话:CAS代表的是一个方法、理念,引导使用者根据一定的原则设计程序,同时尽量给予使用者实现的自由。

举个具体的例子:Internet无疑是一个非常成功的架构,我们仔细看看Internet的结构,会发现一个"砂时计" (抽象概念的):

(应用协议)  HTTP FTP SMTP RTSP P2P ......
(传输协议)      TCP UDP ...
(路由协议)        IP
(物理协议)    ETHER ATM FDDI ...
(物理介质) 电话线缆 专用电缆 光纤 无线电 红外线 ......

不论底层还是顶层,看上去都很"乱"(褒义的讲叫"自由"),但是中间有IP坐镇,一切工作就有条不紊。


我设计CAS的时候,也有意的向这个成功的典范看齐:

服务模块 数据库 数学运算 图像处理 文法分析 加密解密 ......
消息分类    请求/回复 限时 功能调用 序列化 ...
通讯机制        消息总线
封装方法      应用程序 服务 ...
支持应用 WEB服务 媒体流传输 个人数据管理 游戏汉化 ......


这个宇宙中最"乱"(自由)的就是人的思想,因此上下两边到底有多少东西会以什么样的形式出现,我根本不企图去猜测,只需要集中精神做好中间的核心就行了。
dwing - 2007/5/17 9:26:00
设计的简单灵活就可以把基本接口确定下来,这样每个模块返回的接口指针都不会有不兼容问题,可以用接口中的一个方法返回版本或是signature,不会有"dll hell"的问题.如果扩展接口,像COM那样提供QueryInterface就解决了.
我不知道Object Pascal的虚函数表是否和C++的虚函数表在二进制上兼容.
但我认为Object Pascal的使用范围非常狭窄,可移植能力也很有限.
要想发展,就必须考虑支持C++编写的模块.

另外,我没说CAS核心是很乱的,我上面说的只是这4次讲述是越来越乱的,
也许您的时间比较宝贵,或者急于奉献自己的宝贵思想.没有很好地整理.这个我可以理解.
dwing - 2007/5/17 9:39:00
其实选择语言是很重要的,没有虚函数/抽象函数的支持,COM和CAS都很难实现.
虽然Object Pascal支持抽象函数,但语言本身貌似还没真正标准化.
其使用者比C++少得多,编译器比C++少得多,优化效果也差些,写核心也用不上delphi的众多GUI控件.
所以我很怀疑用Object Pascal实现的原因.难道只是因为习惯问题?
Prz - 2007/5/17 9:46:00
QueryInterface是提供了的,但是我在开发中曾经遇到过(可能是)因为通讯模块间Interface版本不一致 (编译两个模块中间修改了Interface的定义) 导致的一些问题,于是我设计了版本校验功能。
我不清楚如果两个使用同一名称、GUID的Interface如果定义不一致到底会产生什么样的问题;根据楼上的描述,似乎不会出现问题,但是我持保留态度。

另外,我曾经提到过CAS的Interface是符合COM特性的,楼上也提到过,使用接口就是为了解决使用不同语言/编译器的程序的通讯问题,因此Pascal和C++的代码的二进制兼容性不需要考虑。

每一种语言都有自己的使用人群,每一个人都是处于自己的一些个人喜好选择语言,因此我对楼上对Pascal的认识表示不屑:

虽然我不喜欢Java,但是我不得不承认,整个工业界正在以一种不可阻挡的趋势转向Java,那么楼上想要发展,我建议趁早抛弃C++转向Java..... (或者被Microsoft的这个#、那个.NET逐渐的诱导到一个他们专有的"Java"世界里去....)


------

我上面说的只是这4次讲述是越来越乱的


我明白你的意思,上一个回复也是针对这个意思回答的:
我认为你觉得乱是因为我介绍的内容的分类越来越多;而我的回答是,一开始讲的是核心,后来逐渐到外层,分类变多是正常的,而且我觉得是开放架构应该体现的优势。

当然也有可能是因为我试图在短短几回里面就把一个反复构思、试验、实现了数年的东西完整的摆出来的结果...=v= 没办法,我这是业余工作,不能占用太多时间......

另外...我不知道楼上从什么地方得知Pascal的语言没有标准化...
(我也没有看到什么地方说过Pascal的语言标准化了就是了... =v= 管它的呢,语言就是语言,标准化与否不是太重要,关键是美就行了)

还有,似乎楼上对于虚函数和抽象函数的担心有点多余:
Pascal是第一个真正支持OOP的高级语言,因此这方面的支持不仅不用担心,而且我认为应该比(标准)C++要早一些... (当然,仅仅是猜想,手里没有历史资料,如果错了也不要打我 :D)
dwing - 2007/5/17 10:07:00
我还是认为: 如果设计足够简单并保留一定灵活性和扩展性,就可以把interface确定下来.
现在看起来比较好的pascal编译器只有borland在做,而C++,至少MS,GNU,Intel的编译器都非常完善.
我上面对Pascal的一部分认识应该成为共识了,现在仍用pascal/delphi的看起来只有习惯问题和RAD这两个原因了.如果还有哪些优势,请不吝赐教.
java的运行效率是致命弱点,跨平台能力也不是广告说的那样(我体验过).
.NET在这两方面比java更差,可能好处只是MS的支持和比较好的开发环境.

忽然想起Object Pascal好像不支持C++的模板特性,这样智能指针恐怕是不能实现了,这东西可以和COM很好地搭配,一般情况下不用手动管理对象引用计数,也很大程度上降低了内存无意泄露的问题.
Prz - 2007/5/17 10:43:00
相对于没有跨平台能力来说,Java就是强。还有其代码可维护性要比C++高得多,这就是Java在业界吃香的原因。

---
GNU编译器也有Pascal的版本,而且是Actively Maintained,好不好用没试过就是了。
Free Pascal也是一个很好的编译器,并且与Delphi语法高度兼容。

---
C/C++我也经常用,只不过只在Linux下(因为当初VS给我的印象极差,虽然听说自从从Borland挖人以后有很大改善,但是我都懒得去试 =v=)

C++里面有很多问题让我头痛,比如:

Pascal可以用下面的方法定义一个指向任意一个类符合接口的成员函数:
TFunc = Function (A: DWORD): DWORD of object;

C++就缺少这种形式的定义,只能用以下两种很“丑”的方法实现:
1. 直接将成员函数Cast成为指针然后赋值;
2. 使用Template,定义一个指向Template类的成员函数的指针;

第一种“丑”是因为需要强行Cast,而且不Type Safe,也就是说如果Cast回接口不一致的函数,程序一样高兴的去执行,然后自我崩坏...
第二种“丑”是因为每定义一个对象,都需要在后面加一行,套用Template产生一个只能指向这个对象的成员函数的类型,一点都不简洁,可用性还差...

如果楼上有什么更美观的解决办法,请不吝赐教。


写程序是一种艺术,因此每个人对于作品的审美观都不同,我喜欢Object Pascal是因为它有些地方(比如上面)很"美",是C++达不到的。
当然我也并不是说C++就不美;事实上我从来就没说过我讨厌C++,有些地方它比Pascal要美得多,比如写循环的语法上就比Pascal精练。
Java也是很"美"的,其纯OOP的语法虽然其实际使用中有的地方会造成语言冗余,这些地方是"丑"的,但是总的来说Java的程序结构就像一串漂亮的玻璃项链。

所以我的观点是,每个存在的语言都有自己的优点和存在的意义,因此仅仅因为自己不使用就否定其存在的价值是不正确的。

---
另外,说老实话,我不认为在目前主流OOP语法架构下,Template有什么不可替代的作用。
Template能够做到的事情,通过合理的设计类、处理继承关系,或者重载操作符,都可以做到。
dwing - 2007/5/17 11:35:00
"Pascal可以用下面的方法定义一个指向任意一个类符合接口的成员函数:
TFunc = Function (A: DWORD): DWORD of object;"

真正深入学习编程语言就会理解每句代码是如何实现的.
C++几乎所有的代码我都能了解具体的实现方法.
但这句代码初看起来就有些问题.
C++中,用变量表示函数仅能使用函数指针.
这里的TFunc应该是指针吧.如果不是就会觉得很不可理解TFunc到底是什么结构.
如果是指针,这个值就是不固定的,因为许多类都可能符合这个接口,
而这些接口的地址都是不同的.
C++的函数指针只能指向全局或静态函数,指向某个非虚函数也有方法,
但指向虚函数的指针如何实现是不可想象的.
其实我们为什么只要这个成员函数呢,
C++中没遇到这个问题的原因是我们通常使用基类指针,
不必只要其中特定的某个成员函数.

我也不能说OP语言不优美,但我个人认为从语言本身的角度总体来看,确实与C++有差距.
最大的一点就是C/C++的预编译语言,尽管有人说它已经过时了,
但不得不说它使C/C++的开发适应能力达到极致.
借助预编译指令,C++工程是能够在主流平台编译的,现在许多开源工程都是如此.
C++的复杂和自由,还使人们经常能写出叹为观止的代码,智能指针就是一个例子.
C++不是没有跨平台能力,它是在源代码级跨平台的,而不是java的运行时跨平台.
不过就是因为源代码级跨平台太强了,int的范围竟不是固定的.
导致写一些核心函数时不敢直接用int,而是自己定义统一类型.(看来不可能十全十美)

java语言确实也和C++一样优美,可惜只是C++的一个子集(大致可以这么说).
其中的int是固定大小了,字符完全支持Unicode了.
但有一个致命的缺失--竟然不支持无符号整型,
有些时候就因为这个原因,程序不得不写的很复杂,效率很低.
当然一般只在系统/算法编程上.
所以说java只适合高层应用,不像C++那样应用广泛.
.NET要不是MS大力推广,能力上与C++比起来几乎没有优势.

嗯...感觉有些跑题了...

---
其实我也对Template没什么好印象,多数情况下确实用不到.
但"智能指针"是个特例,好像我只在这个地方用到了Template.
话说回来,"重载操作符"好像只是C++的特性吧-_-||
Prz - 2007/5/17 14:04:00
这里的TFunc应该是指针吧.如果不是就会觉得很不可理解TFunc到底是什么结构.
如果是指针,这个值就是不固定的,因为许多类都可能符合这个接口,
而这些接口的地址都是不同的.
C++的函数指针只能指向全局或静态函数,指向某个非虚函数也有方法,
但指向虚函数的指针如何实现是不可想象的.
其实我们为什么只要这个成员函数呢,
C++中没遇到这个问题的原因是我们通常使用基类指针,


那么,请给出一个C++的实现,满足如下使用方法:
1. 线程被包装在一个类中(可以命名WorkerThread);
2. 任何一个对象中符合" int (WorkerThread* Thread); "接口的方法都可以被分配给这个"线程对象";
3, 一旦分配,该线程自动调用该对象的方法,并将本"线程对象"实例的指针传递给该方法。
如果不用"丑陋"的Cast和啰嗦的Template,你会发现这个简单的任务对于C++就是Mission Impossible...

而这些接口的地址都是不同的......但指向虚函数的指针如何实现是不可想象的

Object Pascal的设计者们应该是超人了 :D 因为我前面描述的那个Pascal的定义不仅是指向虚函数的,而且是指向"虚类"的...
其实仔细想想,编译器编译你的程序的时候是拥有你的所有类的VMT的,实现对于这种架空的指针的支持,仅仅是做一些偏移量运算而已。

其实,你也不能怪C++的设计者们想不到,C++没有这个东西是因为它的设计理念不同。
C/C++不是一个Strong Type的语言,因此对类型的要求很宽松。
起因要追溯历史到C的诞生了。发明C的Unix编程者们都是Programming Guru,他们的信仰是什么?
"Type checking is for weak minds" (类型检查是为"脑容量有限者"准备的) :D
因此,通过前面我提到的Cast的方法能够实现的操作,C++就肯定不会增加另外一种带类型的方法来解决。

其实,M$的C++里面是有这样的架空指针的,不过有足够的理由相信,这个语言扩展出现在Delphi首席设计师被挖去M$之后... :D
因此可以看出语言总是相互借鉴发展的,因此我告诉你下面的事实你也不要感到意外:

话说回来,"重载操作符"好像只是C++的特性吧-_-|


至少2年前,Delphi的Object Pascal中就增加了操作符重载的支持。
dwing - 2007/5/17 14:29:00
大致是这样实现的,增加一个共同的接口即可.

class WorkerThread;

class InterfaceA
{
public:
  virtual int DoSomething(WorkerThread *Thread) = 0;
};

class ModuleA : public InterfaceA
{
  // ...
  virtual int DoSomething(WorkerThread *Thread);
};

int ModuleA::DoSomething()
{
  // ...
}

class ModuleB : public InterfaceA
{
  // ...
  virtual int DoSomething(WorkerThread *Thread);
};

int ModuleB::DoSomething()
{
  // ...
}

class WorkerThread
{
  bool Attach(InterfaceA *one);
  // ...
};

bool WorkerThread::Attach(InterfaceA *one)
{
  // ...
  one->DoSomething(this);
  // ...
}

int main()
{
  // ...
  WorkerThread *workerthread = //...
  ModuleA *moda = //...
  ModuleB *modb = //...
  workerthread->Attach(moda);
  workerthread->Attach(modb);
  // ...
}

Delphi开始支持运算符重载?倒不如连模板一起支持了,
现在很多情况都是模板与运算符重载结合使用的.

无类型的思想最早是源于汇编的,而C语言是与汇编联系最紧密的高级语言.
所以无类型的思想才逐渐保留下来,这是自由的体现,也是指针发展的平台.

C++中不是所有类都有VTABLE的,
也许是OP的每个类都有VTABLE或如果使用了方法指针就自动加了VTABLE.
我认为不是简单地算一个偏移量就能解决的,
不继承基类,谁能保证类中同名方法在VTABLE中都是同样的位置?
Prz - 2007/5/17 15:31:00
赫赫,楼上的实现偷换概念了呢。请注意我说的是"任意类",您的实现仅仅局限在从InterfaceA继承下来的类了。

不继承基类,谁能保证类中同名方法在VTABLE中都是同样的位置?


为什么要保证?有一个好听的术语叫"Compiler Magic"...
你觉得 把一个类的一个方法的地址付给一个指针,这个操作在编译器在处理的时候和静态赋值有什么区别么?
就象我前面说的那样,顶多多计算一下偏移量罢了。
虚函数?没问题,这个虚函数是怎么调用的,这个使用这个函数指针时按同样方法处理就行了。

无类型的思想最早是源于汇编的,

这个是没办法而已吧||||

而C语言是与汇编联系最紧密的高级语言.

赫赫,这又提醒了我,使用Inline汇编,C/C++还有一个做不到的功能Pascal可以。

在一个函数中Inline两段汇编,C/C++没有办法从一段汇编的中间直接跳到另外一段里面。

Pascal Code:

Function A(A: integer): Integer;
Label Jump1;
BEGIN
// 第一段
ASM
  // 一些指令
  JMP Jump1
  // 一些指令
END;

// 一些"高级"代码

// 第二段
ASM
  // 一些指令
Jump1:
  // 一些指令
END;
END;

本来很强大的C Inline汇编在如此关键的地方变得无力,让人觉得很是意外。

(注: "巧合"的是,M$的C++语言在这里也有和Object Pascal相同的扩展...hmm让人觉得意外的亲切呢)
dwing - 2007/5/17 16:20:00
是不是任意类是无意义的.
既然写了DoSomething方法,就是说要为WorkerThread而实现,
那么就要遵守WorkerThread的规则去继承InterfaceA又有何不妥呢?
而且我认为继承InterfaceA显式声明了要为WorkerThread而创建的类才是正途.
否则编译后所有的类的所有方法名称都要在可执行文件中保留.

InterfaceA只有一个虚函数,其他要成为Module的都要继承,
这是很正常的,许多C++大型工程,多继承(尤其是继承接口)是很常见的.
C++里的VTABLE可以说是动态调用方法中效率最高的.
如果像java那样还保留函数名的信息不但有运行效率问题,还容易被反编译.

以下代码编译通过(VC6+sp6):
void main()
{
        __asm
        {
                jmp next_
        }

        int a=123;

        __asm
        {
next_:
                nop
        }
}

不过我承认VC里的_emit指令确实不如Delphi里的db/dw/dd指令好用.
C++的inline函数和C/C++的宏都是OP没法做到的.
Prz - 2007/5/18 0:30:00
以下代码编译通过(VC6+sp6):
void main()
{
        __asm
        {
                jmp next_
        }

        int a=123;

        __asm
        {
next_:
                nop
        }
}


这就是我说的M$的令人熟悉的扩展啊。可惜不是标准化的,没有移植性,拿给GNU编译器就会爆掉。
其实,VC从Delphi学了很多东西才能达到今天的易用程度,比如你敲一个对象,然后加"."旁边出来一个小窗口告诉你所有可用的方法和成员变量。

怎么说呢,学习并不是坏事。昨天翻Google还看到Intel的CPU技术其实很多都是从苏联学来的:
当苏联解体的时候,Intel秘密的收容了苏联国家处理器实验室的一位科学家,于是才有了Pentium I-III系列的CPU——里面用的一些技术(比如 SuperScaler Execution,超标量执行)是苏联80年代就开发出来了的。

------

另外Pascal的开域语句也是特色。

Pascal:

With StructA.StructB.StructC.StructD do
BEGIN
IntA:= 0;
IntB:= 6;
IntC:= 9;
END

C/C++:
StructA.StructB.StructC.StructD.IntA:= 0;
StructA.StructB.StructC.StructD.IntB:= 6;
StructA.StructB.StructC.StructD.IntC:= 9;

丑吧.....当然,如果用非标准化的语法,稍微好一点:
{
// 代码中混合变量声明,C/C++的特色,可惜非标准,现在大多数编译器都会警告
StructD* PtrD = &StructA.StructB.StructC.StructD;
PtrD->IntA:= 0;
PtrD->IntB:= 6;
PtrD->IntC:= 9;
}

还有,Pascal的类型指针自动解析也是很是美的。

Pascal:

// 一个RecordX的实例
StructX: RecordX;
StructX.IntC:= 123;

// 指向一个RecordX的指针
PtrD: ^RecrodX;
PtrD:= @StructX;
PtrD.IntC:= 456; // 一样用"."解析,编译器自动帮忙

C/C++:

RecordX StructX;
StructX.IntC:= 123;

RecordX* PtrD = &StructX;
PtrD->IntC:= 456;

// 一会儿用"."一会儿用"->"多麻烦啊

当要访问一个很复杂的结构中"埋藏"很深的纪录的时候,用C++要交叉写很多"."和"->",如果是在一个并列的条件语句中的话,完全可以把人写晕:
if (((StructD->StructE.StructF->StructG.StructX.IntC > 5) || (StructD->StructE.StructF->StructG.StructX.IntC < 10)) && (StructD->StructE.StructF->StructG.StructX.IntC != 7)) {}
如果用临时变量的话,基本避免不了非标准化(与代码混在一起),因为很多的时候你要先确认深层结构确实存在才能引用。

------

当然C的结构指针运算还是很不错的:
RecordX* PtrD = &StructX;
PtrD++; // 指向下一个邻接的结构

Pascal不能对任意指针作运算,只有一个"丑"一点的解决方案:
// 先定义一个很大的虚序列的指针
Type
StructXArray = Array [0..100000] of StructX;
PStructXArray = ^StructXArray;
Var
PtrD: PStructXArray = @StructX;

// 通过编号来访问下一个邻接的结构
PtrD[1].IntC:= 123;

------

学会了解并欣赏其他语言的优美,这就是我这次和您版聊的目的,我想已经达到了。:D
dwing - 2007/5/18 8:58:00
其实__asm根本就不是C/C++标准里的,
真正的高级语言标准是不允许嵌入汇编的,这影响到源代码的可移植性.
所以讨论inline汇编是不是标准,是不是扩展毫无意义.
VC的编译器基本上是Win32平台事实上的"标准",
GNU的编译器在Win32平台还无法与之抗衡.
所以请不要说GNU不能做的事是标准,VC能做的事是不标准.
很喜欢VC的"naked call",好像其他任何高级语言编译器都不支持这一很底层的特性.
这也是有人说VC"万能"的原因.
我认为您不要再对VC有什么偏见,
我是从VC6开始用的,VC可能也是从这时开始强大起来的,
VC2003更是达到了巅峰,据说对C++标准化的支持比GNU的还好一点,
XP操作系统和XP的补丁大部分都是用VC2003开发的.
累计到目前超过一半的游戏都是用VC开发的,目前使用VC6开发游戏的仍有不少.

"比如你敲一个对象,然后加"."旁边出来一个小窗口告诉你所有可用的方法和成员变量。"
这种特性还有必要说谁学谁吗,
VC有VAX做更高级的辅助功能,输入"b"就能智能提示出"bool",输入"class"就自动给出代码框架.
我不知道Delphi是不是有这些功能.如果没有,Delphi不去学就是优点吗?

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

多层结构体的问题,下面的写法才是正确的:
struct StructD {int id;};
struct StructC {StructD sd;};
struct StructB {StructC sc;};
struct StructA {StructB sb;};

StructA sa;
StructD* psd = &sa.sb.sc.sd;
psd->id=1;
即使打开VC的最高警告级别也没有任何警告提示.
我想知道为什么说这是不标准的.

这与下面的写法看起来没有任何不美观的地方:
With StructA.StructB.StructC.StructD do
BEGIN
IntA:= 0;
END

其实OP实现那段代码,也要取StructD的指针.

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

"一会儿用"."一会儿用"->"多麻烦啊"

晕了,微软提倡的匈牙利命名方法比这更麻烦,怎么还有人大力赞扬?
还有OP的"begin/end"比"{}",":="比"="等等更是麻烦...
其实很难能找到一个比C/C++更简洁的高级语言了.
还有一点值得一提,如果有VAX辅助,应该输入"->"却输入"."时会自动修正成"->".
VAX是我见过代码输入辅助软件中辅助能力最高,智能程度最高的,敲代码如行云流水一般.
比eclipse还强一点.

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

我写过不知多少行代码,很难见到"深层结构",
真是如此,就应该看看结构设计的是否合理了.
Prz - 2007/5/18 10:49:00
其实__asm根本就不是C/C++标准里的,
真正的高级语言标准是不允许嵌入汇编的,这影响到源代码的可移植性.
所以讨论inline汇编是不是标准,是不是扩展毫无意义.


那么,你的意思也就是说,你前面说Object Pascal没有"标准化"也是毫无意义的喽?


即使打开VC的最高警告级别也没有任何警告提示.
我想知道为什么说这是不标准的.

这与下面的写法看起来没有任何不美观的地方


赫赫,果然又在吧M$当成标准了。说老实话,M$的特点就是"不标准",而且非常喜欢把本来标准的东西拿过来乱改。
ISO规范化的C是不提倡将代码和变量声明混合在一起的,因为这样代码可维护性差。你试试用gcc编译就知道了。

前面的帖子我就说过,VC的跨asm段跳转是一个严重不兼容的语言扩展,放在其他编译器下就会爆掉,连这个VC都不会警告,你还期望它警告一个不提倡的使用方法?

不美观是因为多用了一个变量,多写了一行。
如果编译器够聪明,会发现这个变量仅仅是为了展开结构,也就会自动省掉。
但是,这样其实也就相当于把程序运行的效率人为的降低,然后把提升的希望放在编译器上。

VC有VAX做更高级的辅助功能,输入"b"就能智能提示出"bool",输入"class"就自动给出代码框架.
我不知道Delphi是不是有这些功能.如果没有,Delphi不去学就是优点吗?


Delphi上用滥的功能,而且我充分相信也是Delphi首先引入的功能。

或许你应该学学历史,Turbo Pascal从出现到离开主流都是世界首屈一指的IDE开发环境。
C程序员还需要在单独的文本编辑器写程序,然后花数分钟时间编译,最后再链接的时候,TP只需要一个键就可以了。
光标移到一个函数上,一键就可以立刻获得这个函数的接口和相关帮助信息,这个是80年代的事情。那个时候写C的程序员还在来回的翻书吧。
不要忘了,Borland就是靠优秀的开发环境,不断的为快速程序开发创新,才能生存于M$的美元大棒下的。

还有OP的"begin/end"比"{}",":="比"="等等更是麻烦...
其实很难能找到一个比C/C++更简洁的高级语言了.


begin end和{}的区别,大概就是敲键盘上三个键的位置不同罢了。({+} 确实比 b+<回车> 隔的要近一点,你硬要说这个是天大的优势,我也没办法)

至于:=和=嘛,赫赫,请你自己回忆一下,不小心把 == 写成 = 曾经浪费了你多少的时间?


我写过不知多少行代码,很难见到"深层结构",


任何需要处理大量关系数据的场合: 人工智能图像识别, 搜索引擎用的网络爬虫...
我算是对"->"深恶痛绝了..... =v=
LOVEHINA-AVC - 2007/5/18 11:08:00
……寒,居然变成战帖了

粗略的看了一下,感觉大致是COM的简化版加上一个异步消息处理机制。MISHA你也开始玩架构了哦……

个人在ARM及X86上都很喜欢用ASM写万行+代码PROJECT并且全面优化(通常是最小SIZE和最大速度混合)。不为别的,只是好玩而且看起来很COOL而已(不过IA64是不可能的orz)。比起类我还是更喜欢用指针,实用至上主义,搞小东西用BASIC,搞严谨点的用C。OO语言太费劲了,光是设计结构都要想半年,不是老板要求的话一定不用,呵呵
Prz - 2007/5/18 11:21:00
赫赫,是啊,我觉得没有必要"口水战"下去了,原因很简单,这个世界上跟本就没有什么东西是完美,语言也不例外。
从ASM到Perl随便举一种语言我都可以找出一堆比C++好的地方,反之亦然。

最后重申一下我从开始到现在一直的立场:
学会了解并欣赏其他语言的优美


本讨论可以结束了 =v=
wdx04 - 2007/5/18 11:25:00
14楼的程序略作改动就可以在1990年的Microsoft Quick C 2.51上编译通过,说明这个扩展并不是从Delphi学来的。
void main()
{
    int a=123;
        asm
        {
                jmp next_
        }

        a++

        asm
        {
next_:
                nop
        }
}
此外Intel,DigtalMars的C++编译器也支持这种用法,但Borland C++不支持,而GCC的汇编语法就和Intel系的汇编语法都不一样。

C++的typedef可以提供类似Object Pascal开域语句的功能,用它来简化复杂类型是常见的手法:
    struct StructA
    {
        struct StructB
        {
            struct StructC
            {
                struct StructD
                {
                    int id;
                }sd;
            }sc;
        }sb;
    };

    StructA sa;
    typedef StructA::StructB::StructC::StructD StructD;
    StructD& psd = sa.sb.sc.sd;
    psd.id=1;

另外现在C++的编程思想是,能用reference的地方不用pointer;能用template代替virtual function的时候不用virtual function,至于模版怎么用,建议参考Loki和Boost.Spirit这两个library。
虽然C++代码在Size方面不是很好,但Speed不会比C和汇编差。
LOVEHINA-AVC - 2007/5/18 11:38:00

虽然C++代码在Size方面不是很好,但Speed不会比C和汇编差


跟C比的话区别的确是不太大的,慢也只是慢在SIZE上,不过跟ASM比就有点说不过去了。让你这么想的原因大概是没有什么人会去优化非性能关键的代码(除非有特殊癖好,比如我这种),所以平常看上去的那些代码比C要更慢(严格来说,是慢得多)。即便是优化能力最好的INTEL编译器,我还是经常能够发现很多明显不妥当的、影响性能的地方。不说别的,就拿寄存器利用来说,这点是要输给手工优化很长一截距离的。
dwing - 2007/5/18 11:42:00
本来我对标准化就不怎么看重,其实一旦某些东西用得广泛了,就是事实上的"标准化".
我说VC的编译器是事实上的标准并没有错,C++标准委员会里的人都有在MS做VS开发的.

"VC的跨asm段跳转"是很正常的,为什么要"爆掉"呢?

"不美观是因为多用了一个变量,多写了一行。"
实际上明明是需要这个变量的,OP在编译时也不例外,为什么要隐藏这句代码呢?
语言太高级就不是C/C++的特点了.
讨论语言历史,尤其是10年以前的历史没什么意义.
历史上,MacOS的图形界面比Windows的还好,难道为此我们就都去用MacOS?

"不小心把 == 写成 = 曾经浪费了你多少的时间?"
看来你不是成熟的C/C++开发人员,否则根本不会犯这么低级的错误.
而且VC在大多数情况下都能发现此问题,并给出警告.
如果在这个错误上浪费时间,那么在OP中":="和"="上浪费的时间也不会少.
"."和"->"的误用更不会遇到,何况编译时是一定能发现这种错误的.
当我熟悉C/C++时,把"."和"->"合并成"."看起来是很不合理的.
它把变量和指针的含义搞混了.

"任何需要处理大量关系数据的场合"
不必要的复杂设计才是导致深层结构的根本原因.

以上我所有关于语言的讨论宗旨是: Object Pascal语言是优美的, 但与C++相比还有差距.
MS放弃Pascal可能也是如此考虑的吧.
dwing - 2007/5/18 12:01:00
C++在size上的控制的不好的主要原因只是template,inline和未使用的virtual function.
template可以说是C++中最复杂的了,所有C++编译器不能完全符合标准多数都由于template的支持.
此处我现在也只懂些皮毛,不敢深入使用.

现在CPU都足够快了,一般来说,不是critical的代码不必用汇编.
而且不是汇编高手写出的汇编恐怕还不如编译器优化的代码.
LOVEHINA-AVC - 2007/5/18 12:13:00
原帖由 dwing 于 2007-5-18 12:01:00 发表
现在CPU都足够快了,一般来说,不是critical的代码不必用汇编.
而且不是汇编高手写出的汇编恐怕还不如编译器优化的代码.


可以说除了编写特殊指令还有关键代码段的性能优化之外,不是单片机之类的平台都没有必要使用ASM。不过其他语言慢一些终归是事实,比起极限优化的还要慢不少(虽然对于现在的CPU来说是微不足道的),毕竟无论用什么语言编译,最终出来的都是机器代码。
Prz - 2007/5/18 12:23:00
本来不想再无谓的争论一些无聊的东西,不过忍不住最后插一句:

MS放弃Pascal可能也是如此考虑的吧.


我觉得更加可能的是,开发、创新能力上竞争不过对手......
网络上大量的证据都指向90年代Borland与M$签订的"互不侵犯利润条约":
此后的十年内Borland没有出过C和Basic的IDE,M$也没有出过Pascal。

就此封笔,本贴接下来您说什么我都无条件同意。:D
LOVEHINA-AVC - 2007/5/18 12:25:00
封笔就不要了,我还想看(5)哇;P
dwing - 2007/5/18 13:18:00
那个传说的"互不侵犯利润条约"我也早就听说.不过很大程度上是笑谈而已.
我个人认为是:
MS认为(object)pascal和c/c++是同一层次上的,而c/c++更有广泛的应用,所以没必要去搞(object)pascal.而basic/vb是更高级的语言,面向的用户也与c/c++不同,所以不会产生冲突.
Borland认为它的delphi是源自起家的pascal,而且同时考虑vb和c++的优点,所以就不屑再开发basic.但c/c++毕竟有大量用户,所以不得不照着delphi又弄出个带有delphi血统的bcb.
根据近年Borland推出jbuilder可以看出,它不愿搞底层开发工具,所以delphi/bcb不能像vc那样接近开发的最底层(已经贴近Win32Asm的层次,可以开发驱动).也可以从支持"naked call"和"自定义entry"看出(这两点borland貌似一直不支持,所以没看到有人用delphi/bcb写出VC能做到的等用于直接用汇编的1KB的EXE).
dwing - 2007/5/18 13:32:00
原帖由 Prz 于 2007-5-18 12:23:00 发表
就此封笔,本贴接下来您说什么我都无条件同意。


汗~~这话说的真绝,
"说什么我都无条件同意"......;P
Prz - 2007/5/18 13:34:00
原帖由 LOVEHINA-AVC 于 2007-5-18 12:25:00 发表
封笔就不要了,我还想看(5)哇;P


唔,不是说不写了,只是不想就这个问题讨论了。
这是一个很有意义但是很没有讨论价值的东西,就像众神论、一神论和无神论一样,口水积成海,再杀上一堆人,估计把地球炸成两截都解决不了问题... :P

那个(5)是真的没有了,因为我已经用最快的方法把我认为关键的要点介绍出来了。

接下来就是 Prelude 先行预览版了,不过我计划至少等把数据库支持移植到CAS架构下才发布,根据空闲情况,要等几天去了。
wdx04 - 2007/5/18 13:53:00
原帖由 LOVEHINA-AVC 于 2007-5-18 11:38:00 发表

虽然C++代码在Size方面不是很好,但Speed不会比C和汇编差


跟C比的话区别的确是不太大的,慢也只是慢在SIZE上,不过跟ASM比就有点说不过去了。让你这么想的原因大概是没有什么人会去优化非性能......


我对Intel C++还是比较有信心的,要不谁出个题目,固定算法,你用ASM实现,我用Intel C++,看速度能差多少?
12
查看完整版本: [M] Prelude to K.O. (4)