“最早的CPU的体系结构中是没有堆栈的。CPU的处理过程……它是一种从头到尾的处理过程。从打孔机开始,到4位和8位的机器,甚至当时的巨型机也是这样,此时,一个程序就是一个过程,既没有堆栈也没有函数。最早的语言是汇编语言,后来产生了Fortran语言,Fortran语言之后又产生了BASIC语言。早期的语言都没有函数的概念,只有goto语句。”“在8位机的6502的CPU中,就出现了一点点的堆栈,前面有256个字节的堆栈区的空间,后面是主存。……这时候就出现了函数的概念,出现了GOSUB类语句的调用方法。早期堆栈主要是保存返回的地址,堆栈没有存放参数,6502就是这样的。这相当于可以调用较少的递归层次。这时候就引入了递归的概念,因为最开始程序并没有考虑到有这一种情况。当有了堆栈后,就能做递归,所以早期的BASIC是不能进行递归运算的,因为它没有堆栈区。……PC到了16位时代时,堆栈就不受限制了。堆栈是独立的,其空间也是不受限制的。此时,堆栈就可以存储参数。因为有了参数,所以可以做递归了,真正的递归程序才能运算。当时出现的Pascal语言,以至后来的C语言,都能进行递归程序的运算。”又在第159页里的插曲说,“最老的BASIC没有函数的概念,只能用goto语句,也没有return等类型的语句。后来出现了GOSUB等语句,像Fortran这样的语言一开始都是没有类似函数的语句。goto语句是不用堆栈的,自从有了堆栈以后才出现函数,只有有了函数后才会有全局、局部变量的出现。”
从这大段的文字可以看出,梁先生的意思是大抵明白的,就是,早期的CPU没有堆栈(作栈——Stack解,以下同),也就没有函数及其函数的概念,没有堆栈也就没有递归的概念和递归运算;直到8位机的6502这种CPU出现时才有了堆栈,才有了函数的概念和函数,才有GOSUB之类语句的调用方法,(16位PC时代)才有递归的概念和递归运算。而“当时出现的Pascal语言,以至后来的C语言,都能进行递归程序的运算。”
我们知道,6502这种CPU是1975年才出现的,不说别的单是就在它前头的Intel 8080就已经有了堆栈了,那时是1974年(1971年的Intel 4004就有三级深栈)了;而在这之前1970年代出来的小型机NEC PDP-11已经有堆栈了(参看《KA11 Processor Manual》Sep1970,有当前堆栈寄存器SP等,还有操作堆栈的指令如JSR、RTS、RTI、TRAP等)。【1961年 Burroughs 体系结构中设计有栈硬件。《操作系统——并发与分布式软件设计》附录A (英国)Jean.Bacon 2002年】可见,出现堆栈的历史远在6502 CPU之前。而函数的概念更是老早就出现了,前面已经说过,远在Fortran出现之前的1948年就已经有子程序的概念了,而Fortran出现之初就有子程序和子程序库了(参看《Programmer’s reference manual FORTRAN》1956 Backus)。函数并不必然意味着堆栈【的使用】。照作者的说法,(1970年出现的)Pascal和(1972年代出现的)C语言就已经有递归了,而作者一面又说(直到1975年出现的)6502才有堆栈才有函数才有递归,这不是很矛盾吗?作者信口开河的程度不得不令人惊讶,就如同儿戏一般,还以此为我们打好基础、以便步入出神入化的编程高手境界呢。
如果没有弄错的话,BASIC在出现之初就已经有GOSUB语句(与RETURN语句结合使用)了。详情可以参看《A Manual for BASIC, the elementary algebraic language designed for use with the Dartmouth Time Sharing System》(1964年10月,The original Dartmouth BASIC manual,其中3.3 Functions and Subroutines)。
关于递归的概念和实现语言。Pascal语言1970年就出现了,远在6502CPU之前。其初就明确地谈到递归及其运算,在讲到函数时特地举了一些例子如GCD()函数:
function GCD(m,n:integer):integer;
begin if n = 0 then GCD := m else GCD := GCD(n,m mod n)
end
(《The Programming Language Pascal》1970 Wirth)。而1968年出版的《The Art of Computer Programming》(Knuth)就已经明确地讨论了递归及其运算【ALGOL60, 】。递归的实现同样也不必然同堆栈联系着。它还可以用寄存器、普通存储器等实现递归,不管这是否方便、是否有限制,但都能进行一定程度的递归。
“后来就引入了C++的概念。C++是这样一种工作方式,即把数据和对其操作的代码进行捆绑。这样就可以和结构一样,进行内存的分配和使用。在C++中,函数内部用动态的可重入的变量,而C++又是工作在自己独立的数据区和代码区。在C++在CPU中代码是共享的,数据是独立的,它们都是共享这个代码段。在编译器看来,代码是固定的。如果直接用指针的方法,在C++中取某个函数的地址,则不会成功。在这个类中的函数不会取出正确的地址。因为它生成时就是动态地分配一个空间。所以从语言的角度来说,让你不能取类成员函数的地址。但是C可以。因为定义C++的类时,代码和数据都浮动。然而,代码浮动是没有意义的。……对于同一个进程中,代码当然只有一份,因为代码是不会改变自己的。所以,在C++中加了不能取类函数地址的限制。有了这些限制,从C++的类中取一个函数的地址就会很困难。但还是可以做到的,在后面的C++语言特点中我们将会详细地介绍。但这时,C的方法可以取到函数的地址,地址是固定的。C++的设计理念是数据和代码都是浮动的,这样可以把整个对象传入给一个进程或是当成一个数据类型,把这个对象从这个进程传入到另一个进程中去用。在这基础上,发展出发布式对象。......把某个对象从计算机A中传入计算机B中去运算,按照C++的设计理念完全是可行的。把对象放入另一个计算机中去进行运算,运算完成后,只需要返回运算的结果。但这种理论的模型到现在还不可能都做到,就是操作系统也做不到这一点。”
C++中,函数内部不仅用“动态的可重入的变量”,还用动态的不一定可重入的变量如局部静态变量,因为它在首次进入函数时才可能动态生成并初始化【】,不然就根本没有生成它,自然还可以用并非动态的全局变量等。
不知作者根据什么得出C++“工作在自己独立的数据区和代码区”的结论的。不过,作者在谈及Fortran,说它的“程序和代码放在不同的区域中,钉是钉,铆是铆,数据和代码都是很清楚的,……所以,当程序要使用变量时,一开始就有做申明,编译程序会自动为这个变量分配一个对应大小的空间。变量区域后接着的是代码区域,这两块区域在源代码上可以看出来。”而“到了Pascal和C语言中,程序和数据之间的关系就模糊了”。从这些看来,作者大概从源代码来判断的,Fortran语言中变量声明都在前面,后面是代码(可执行语句),“变量区域后接着的是代码区域,这两块区域在源代码上可以看出来。”而C语言函数中,一般情况下,也是声明在前面,代码在后面,如果没有在函数里的块语句内部声明(定义)变量的话,情形也就这样,“这两块区域在源代码上可以看出来”,不过如果在函数内的块语句里声明了变量,此时那就“程序和数据之间的关系就模糊了”。在C++中,作者大概以为private/protect/public分隔的各个部分,数据和代码只有分开的那种情形(要么是变量、声明等数据,要么是函数等代码)。不过后面谈到“C++在CPU中代码是共享的,数据是独立的”时又涉及到C++的对象模型了。
C++“把数据和对其操作的代码进行捆绑。这样就可以和结构一样,进行内存的分配和使用。” “但是C可以。因为定义C++的类时,代码和数据都浮动。”
这里,作者以为C++的类的对象分配是同时分配数据和代码的,以为数据和代码像C结构一样分配的。“在这个类中的函数不会取出正确的地址。因为它生成时就是动态地分配一个空间。所以从语言的角度来说,让你不能取类成员函数的地址。”如果真是这样的话,即“它生成时就是动态地分配一个空间”,那么类的非inline函数代码如果这样繁殖下去的话,C++就绝无效率(空间、时间)可言,这简直是想耗损效率的绝佳办法,好一个开放性思维!如果真是这样,想对MFC不屑一顾恐怕也没有机会了,因为C++早就夭折了,哪里还会轮到梁先生在这里不假思索、如同儿戏般的这些信口开河呢!
C++设计之初,其对象模型就不是数据和代码捆绑(在一起动态分配)的,更不是以此进行分配的。“事实上,这种模型太理想化了”的,不是别的,这种捆绑,梁先生自己一手打造的,尽管十分愚蠢,居然连类的所有函数代码也随对象一同动态分配、动态生成,除了少数情形如inline函数、Thunking等之外,谁敢把这种当作常规机制?
既然你知道代码是要共享的,哪为什么还要生成时动态生成它们(数据和代码)呢?这样一来,不就不是共享的了么。既然你认为“代码是不会自己改变自己的”,那么看着这些动态生成的代码,难道不是表明了代码就可以“自己改变自己了”么。作者陷入了这些糊涂、混乱、矛盾的开放性思维之中而难以自拔。
作者谈到(设计理念)理论上可以在进程间、计算机之间传递C++对象,主要是因为它们是浮动的。其实,这里的根本、关键并不在于“浮动”或是“固定”。以Win32进程为例,就算是固定的,那么先请你把包含地址的指针的结构变量传递到另一进程、另一计算机中的进程试一试,没有问题时再来谈“浮动”的也不迟。这里的关键应是对数据类型(字节顺序等)、地址空间和机器指令等的意义等的解析和处理是否可行(有效)。Win32进程空间是有各自独立的虚拟地址空间。一个进程中的地址在另一个进程中未必就指向同一物理地址(系统各进程共享),那么就不能直接使用,而需要通过其他(间接)方法来处理。而一个进程的指针对另外一个计算机中的进程来说,连它们共同活动的物理地址空间都没有了,甚至另一计算机所用的指令系统也不一样,这样,如果硬要使之通行(直接使用),那么梁先生就可以去修复/建造巴比伦塔了。【C++程序最终是编译成机器代码的。】
梁先生云,因为C++中类的数据和代码都是浮动的,所以不能取成员函数的地址。我们知道至少C++中类的非inline函数代码并不是“浮动”的。而说“不能取成员函数的地址”也并非都这样。且不说如同普通C函数一样的静态成员函数了,就拿普通的成员函数(除了构造函数、析构函数等外)来说,都是可以方便取其地址的,这个地址也是其在内存中的有效地址,在这点上与C函数地址一样的,它们之所以不同,是因为C++普通成员函数还需要(指定)一定的对象才能执行,这个对象只是成员函数操作的内容。
在“汇编的原理”中梁先生谈到了“程序的入口和出口”。还特地抄了一大段代码(大致与MS的general.c样板相当)来说明程序的入口点,并且是“自定义”的,且又在汇编中呢。其实,在汇编中,程序的入口点本来就是必须程序员自定义的、如同家常便饭一般,正无须大书特书的(前后几乎足足用了6页)。为此还把 WinMain()差强人意地拉进来一起示众,其实哪里用得着呢。(作者大抵是想说明自定义C语言程序或Windows程序的入口点,可是硬扯到汇编程序里来,就显得有些不伦不类了。就自定义C语言程序或Windows程序的入口点来说,作者又没有提到哪些需要注意的问题、方面等。两边都想说,结果没有一边说好。)
其中也谈到“汇编中有很多不确定的信息,程序员在编写时要尽量确定这些信息。”至于“这些未确定的因素要怎样才能被确定下了呢?”好像就不是程序员的事情了,“那就是Link进来。Link的过程是按照这个机器的实际情况,把这些未确定的因素最终确定下来,并放入生成的EXE文件中。”Linker竟然能把那些需要在Loading时才能最终确定下来的那些基址重定位(如可重定位的动态库DLL等)都最终确定下来,平常的东西一到我们的高手的手中那威力就非同一般了。
【C++所谓的“重入”问题】
C/C++的原理。“我们知道,在C中有全局、局部(自动变量)和静态变量。”“不论数据是静态的也好,还是全局的也好,所有的变量都是在大括号之内,当在大括号之外是是不可使用的。那么,全局变量就相当于在一个全局的大括号内,如下例:
{
int var1;
void FUC1()
{
int Fvar1;
…
}
static int Svar1;
void main()
{
int localvar;
…
}
}
这样,就相当于整个文件有一个大括号,它是隐含的,不用写到程序里去。例如,我们通常不会在开头和结尾写一对大括号,因为这是没有必要的,在第一个大括号中申明的变量就是全局的。”
如果整个文件相当于有个隐含的大括号的话,那么include进来的文件内容自然在整个文件大括号里面的内层括号里了,它的外面即整个文件大括号内这一层是看不到的,因为“原则是变量的访问权限只能往外看,不能往里看,如果用层次的观点来看的话。”既然看不到,那就不能通过头文件来引入外部变量和类型、函数声明了,此时头文件就像被阉割的太监一般没有用武之地了,多亏了“开放性的思维”!即使在C++里全局名字空间也不能这样来行使其权利,至少要对嵌套的情形等要特别地具有真正的“开放性思维”。
“局部也叫自动变量……变量并不是在程序的某一个地方申明,而是在堆栈里面去实现。局部变量完全在堆栈里面去实现。”
其实,当就自动变量来说,也并不一律“完全在堆栈里面去实现”。在C函数中声明的自动变量如果是一个指针变量的话,那么它的实现就不是完全在堆栈里面的,虽然它的上半身即指针自身是在堆栈里实现,但它的下半身就是在堆里面实现的了。如
void F()
{
int *p;
p=malloc(sizeof(int));
…
}
指针变量p难道不是一个在堆栈上的自动变量?它的下半身实现(malloc()分配)难道也在堆栈上?在C++中也同样不能说一律“局部变量完全在堆栈里面去实现”了,理由如上。
“函数是什么?函数实际上和变量表达有一个相似的地方,就是最终的结果一定是放在内存中的块。函数不过是放在代码段里面的一个个标识符而已。如果从地址的角度来看,例如,这个函数被调用,只不过是代码执行到函数的入口点,函数的调用还会有一个返回的过程。”
函数代码的入口地址和指令序列此时就又被阉割成“代码段里面的一个个标识符而已”了,即只剩下入口地址了。
下面接着谈的是C++的类。“如果把类剖析出来会发现有一堆Vtable的指针,这个指针再jmp到一个函数的地址,如果你继承了很多的函数,Vtable区就会有很多这样的指针,第一部分是函数的指针结构,第二部分是类的成员变量,不管是私有的还是公有的,或是保护都被放到这个类结构的空间,然后,整个这个结构就是一个类。”“事实上,类继承时,函数的结构都会放在Vtable表中,它们时平级的,接着就会把继承类中所有的成员变量一个一个地从上往下排,按先祖先后自身的排序的方法进行摆放。但是,这样有一个问题,就是不论用其基类,还是用它的子类,都会有一个很大的结构,所以,这就是为什么MFC编写出的程序是一个很庞大的文件的原因。如果你用到里面的函数时,并且不是静态分配(静态分配内存会单独地放在一块区域中),而都是动态的分配,当程序做了很多内存的操作后,如果某地方出现问题,就会出现CALL地址错误。”
这里充满着孩童式的天真和幻想,把C仿真C++对象实现的结构当作C++的实现结构了,以为每个类中都有Vtable表,以为每个函数都登入其中。一个没有虚函数的类,无论你怎样去剖析,都不会“发现有Vtable的指针”,更不用说“一堆”了,看来即使汇编级一步一步的调试也有失手的时候。退一步说,即使有虚函数,那也只有这些虚函数才会登入Vtable(作者的Vtable表理解有些混乱)中。继承类的理解也是这样,非虚函数是不会登入Vtable表的。调用类里面的普通非inline函数并不是动态分配的,就如果C函数一样。而调用虚函数,也同样不是动态分配的,那些虚函数表的设置早在调用之前就已经设置好了,调用时就已经是“静态”的了,至于那些虚函数代码也不是动态分配的,也如普通非inline成员函数一样,只是引用方式不一样而已(间接性)。【inline & virtual】
“重载是什么?重载是相同的函数名用不同的参数来进行区分。”
这不过是其表面形式、表面现象。应该从实质上来理解,重载是“将同一个名字用于不同类型上操作的函数的情况”,“在不同类型的对象上执行概念上相同的函数时给它们取相同的名字”。
作者举了两个重载的函数来说明,
int add(int i, int j);
float add(float I, float j);
“类里面的重载……在Vtable表中,可以看到,祖先有一个add的说明,本身也有一个add函数的说明。”
我们不知到梁先生在哪里看到了这两个普通函数居然在Vtable表里了!没有天书的我们也不知到梁先生究竟又深入到那一底层去了……
“用动态的方法来访问函数就会又很多的毛病,这就是为什么C++中很难取类里面的地址的原因。所有类中的函数时不能取地址的,除非用静态的方法。但是,用静态的函数就不是类的成员,编译时,是按普通的函数进行编译的。……但是,如果不是静态函数,则取地址就很困难,主要是编译器不允许,并不是技术上实现不了,而是编译器觉得会导致许多不确定性。”
我们现在终于知道梁先生以前说地,“有了这些限制,从一个C++的类中取一个函数中的地址就会很困难。但还是可以做到的…..”。现在我们知道这个原来大抵是用静态的方法了。其实不光是静态函数可以取地址,就是普通函数(除了取虚函数的地址为虚表中的索引值、不能取构造函数和析构函数等的函数地址外)也一样方便的,并且取的也是有效(真实)的内存地址。
“还有一种像COM那种的继承,事实上是用的一个类结构……这是一种分级指针的结构,所以COM的继承都是分级指针的,因为COM没有给你提供源代码。”
COM组件之间的“继承”,大抵就是COM组件之间的重用(包容和聚合等),与源代码级别如C++的“继承”不一样,是二进制形式上的“继承”。它们不光是“分级指针”,而且还可以“分级”地指向着自己,亦即,既是客户也是服务器,还有似乎不伦不类【有出有进】的“美女勾魂”——出接口,等等。COM组件之间的复杂关系远超出了“分级指针”的范围了。
解释语言的原理。最精彩的就是把从网上下载来的BASIC解析器的“烂代码”给我们抄了一遍又一遍,足足用了近二十页。其次就是这里面的小插曲了,真是一段奇文:“比尔可能比较喜欢它的BASIC,因为这也是他起家的东西。也许就是这个原因,所以BASIC一直没有消亡。”我想,“它的BASIC”中的“它”大概是笔误吧,“比尔”大抵总不至于会成为“它”的。看来“比尔”的喜欢可以令BASIC不消亡,他的喜好真是威力无穷。可惜,“比尔”更喜欢的恐怕不只是BASIC而已,曾经的和将来的,“消亡”的和未消亡的。
C、C++的学习方式。首先给我们讲起了汇编、C和C++的混合语言编程。这是在大多数相应的语言教材里大都可以看到的内容。不过,好文章大家一起来欣赏,还是举一个例子罢。梁先生在“C++的接口方法”中谈到C++与汇编的“接口”并且举例说明,我们就来看看这个例子。
-------------------------------------------------------------------------
现在假设要在XXX类中的Hello函数嵌入汇编指令。有如下几种方法,下面分别述之。
class XXX
{
public:
void Hello(void);
private:
int X;
};
void XXX::Hello(void)
{
// 1.没招,(无法编译通过)
_asm{
mov eax, X
mov [X], ebx
}
// 2.庸招,(可以运行,程序会很复杂)
LPINT lpx;
lpx=&X;
_asm{
mov edx, lpx;
mov eax, [edx]
mov edx, ebx
}
// 3.错招,(运行不正常,甚至出错)
_asm{
mov eax,this.X
mov this.X,ebx
}
// 4.绝招,(既简单,又正确)
_asm{
mov ecx, this
mov eax,[ecx]this.X
mov [ecx]this.X,ebx
}
}
-------------------------------------------------------------------------
【从书中的图4-15 大抵可以知道梁先生是在VC++下来实作的】
梁先生解析第一中方法为什么不行时说,“原来X这个变量是定义在一个类中的私有的变量,所以编译出的程序无法找到对应的变量X”。
如果你认为编译出的程序无法找到对应的变量X是由于X这个变量是类的私有变量的缘故,那么为什么不试一试把它放在类的公有部分里呢?大概试了也不行罢。这样大概就“出现问题时,问题出在哪里就搞不清楚了”罢。其实,在类函数的内嵌汇编代码中要访问成员变量,必须指出其所引对象、并用点号(成员操作符)来引出成员变量,如对象指针在eax的话,那么可以这样来引用[eax]this.X,中间的this在变量名唯一的情况下可以不用,即[eax].X。内嵌汇编代码中直接只用成员变量的名字X是不能引用到成员变量的。具体可以参看MSDN2001中《Visual C++ Programer’s Guide》中“Accessing C or C++ Data in __asm Blocks”一节。
梁先生在谈及第三种方法时列出了其中处理后的汇编代码来,this.X被处理成dword ptr [this]了,看后大声道:“不对!第二条语句不是改变的类中的变量,而把this的指针改变了”,第二条语句即 mov dword ptr [this], ebx。其实,dword ptr [this] 与 this.X在这个例子中是等价的,因为X为类的唯一变量,又没有虚函数,它的起始地址大抵也就是类对象的起始地址,即它们的相对偏移为0。this指针在处理后的汇编代码中似应该更进一步落到实处、不再是原来的this指针形式了,大多数编译器处理类的这种成员函数时都设置好了this指针,如Borland C++一般将其放在esi寄存器中,而VC++则将其放在ecx中。VC++并且把其值存入栈中,就像普通参数一样,统一通过ebp寄存器来引用。就算如此,还是那个this指针形式,这个也只是一个指向对象首地址的指针,在内嵌汇编中我们不能直接通过它来引用对象的成员变量,必须再进一步取得对象的首地址才行,这时才像C的结构指针那样通过它可以引用成员变量。梁先生没有在这里把Y变量加入是明智的,this.Y将会是[this+4](假定int为4字节,且成员间无填充)了,这样就把指针指向了别处、最终离开了原来指向的对象了。
MSDN2001中《Visual C++ Programer’s Guide》中“Accessing C or C++ Data in __asm Blocks”一节中说到,“If a class, structure, or union member has a unique name, an __asm block can refer to it using only the member name, without specifying the variable or typedef name before the period (.) operator. If the member name is not unique, however, you must place a variable or typedef name immediately before the period operator.”亦即梁先生所说的第四种方法。可是梁先生看了X、Y变量的汇编结果分别为dword ptr [ecx]、dword ptr [ecx + 4],就得出结论道:“可以看见,[this+0]指向X变量地址,而[this+4]指向Y变量地址,这样就没有错误了。Y为[this+4],是因为int类型占用了四个字节长度。这就是‘绝招’了。”梁先生没有看到这里的二级间接引用,还以为是等同[this+0]、[this+4],而实际上,[this+0]即[this]亦即属于“错招”的第三种方法的处理结果。
原来弄了半天,梁先生自己还是没有弄懂到底是怎么回事!但这些都丝毫不会妨碍作者一遍一遍地说,“要知道,一个代码是不是完全正确,一定要在汇编指令中看一看”。最后还告诉我们,“最重要的一点是,汇编可以确定程序出错的真正原因。”不要像“很多人遇到不能解决的问题时,总是一遍一遍地去试用各种方法,而不是从代码编译结果出发去解决问题,这样就很容易掉进漩涡中出不来。”
在介绍C、C++的学习方式的一节里,梁先生没有给我们介绍怎么样从C语言学习结构化程序设计,也没有说起怎么样从C++语言里学习数据抽象、面向对象、泛型程序设计等程序设计风格。我们也无缘拜会了。下面就给我们讲起了“挂钩技术”。
Windows上C的挂钩。梁先生为了挂钩应用程序自己的MessageBoxA(这应该是应用程序的一个IAT表项,来自user32.dll),却想去修改user32.dll里MessageBoxA的IAT表项(实际应是Export Table表项)。试了,发现找不到JMP相关代码,说“不好!微软可能是为了高速地调用Windows API,把JMP过程都省去了,程序直接去调用了函数的地址”。猜想替代了对问题的探究。后来就“干脆”拐了个弯——
“是不是可以通过申明全局变量的方法来让它调用Windows API的时候产生JMP的过程呢?”所谓用全局变量的方法,即直接修改当前应用里的MessageBoxA函数的地址,这总算碰对了,自然会找到本应用程序里的IAT表项并进行修改。可是梁先生还是不明白怎么回事,说什么“让它【全局变量】调用Windows API的时候产生JMP的过程”!什么产生什么哦!后面“用PE的方法实现”也大抵基于同样的修改IAT表的方法,只是自己从Image里定位而已。
后面又谈起了“C++的挂钩技术”。并且以DirectX为例,“DirectX是一个COM。而COM是一个C++的结构,这就是说,要把C++的函数进行挂钩,只有这样,才可以随心所欲地控制所显示的画面。”(画面指的是传奇游戏的画面)作者的意思大抵是修改COM接口的vptr值为自己实现的接口的vptr值。作者以IDirectDraw为例讲述如何“挂钩”,最后,“从上面可以看出,对COM的挂钩要比函数的挂钩简单得多。所要知道的只是类的原型,写一个相应的类,然后Vtable指向做的类的Vtable就可以了。”
按照梁先生的意思,要想挂钩一个COM对象的某个接口,挂钩前做到两条就可以了:一,知道接口的各个函数的原型;二,据此实现该接口。且不说第一条当接口为自定义接口时该如何去探究它的函数原型的种种复杂情形;就算你已经知道接口的函数的原型了,我们该如何据此实现它们呢?我们难道不需要知道这些函数参数和返回值的意义?难道不需要知道这些函数本来所要实现的(大致)功能及其限制、约束?难道不需要知道这个接口与其他接口在对象内部是如何交互、协调的?难道不需要知道组件的控制对象生存期的逻辑(对象级?接口级?等)?如果是双接口,在这种情况下又该如何正确实现我们要挂钩的那个接口?难道不需要知道该接口本来是否支持聚合特性?等等,等等。
真是比函数的挂钩简单多了!常看见警匪片中被派去保护关键证人的大大咧咧的警察对证人狠劲地拍着自己的胸脯打包票道:“有我们在,绝对保证你们的安全!”结果那些关键证人就往往轻易地死得特别快。我想我们程序员总不至于成为那样的关键证人的,也就不再放在心上了。
好罢,大胆地往前走——只是走得太远时别忘了回家的路……
本文探讨了编程语言的发展历程,从早期的汇编语言到Fortran、BASIC,再到Pascal和C语言,强调了堆栈、函数和递归概念的演变。文章指出早期CPU并未内置堆栈,而随着技术进步,堆栈和递归运算得以实现。作者还澄清了关于C++中数据和代码分配的误解,并讨论了C++的面向对象特性,如类和继承。此外,文章讨论了C++中取函数地址的限制以及COM对象的实现。最后,文章提醒读者理解编程语言原理的重要性,以避免误解和混淆。
3890

被折叠的 条评论
为什么被折叠?



