|

楼主 |
发表于 2003-10-31 13:10:03
|
显示全部楼层
续
4.3.3 接口的建立方法
很多程序员对C++和C之间有什么联系和差别,总是搞不清,只是知道C++好,但遇到C和C++之间的接口,就会不知所措。往往花了很长的时间还是没有结果,这是为什么呢?
这就是因为不知道C和C++语言的本质是什么。所以,学习任何东西不追根求源,是不可能成为真正的高手的。在学习时,一定要了解事物的本质是什么,它是怎么被运行的。
为了方便大家的学习,本书建立了一个工程,可以从光碟的第四章\Demo目录下找到。
可以看到,在当前的工程中有两个文件:
(1)C语言的文件Demo.c。
(2)以CPP为后缀的C++语言的API.CPP文件。
1. C接口的方法
主程序main函数中要调用API.CPP中的int API(int A)函数。
现在可以在main函数中,输入调用的语句。
Demo.c
main(int argc,char *argv[])
{
char Buffer[512];
if(argc!=2)
{
printf("Usgae:Test xxx\n");
return 0;
}
Buffer[0]=API(3); //调用API.CPP中的API函数
printf("Test !\n");
return 0;
}
API.CPP
#include <windows.h>
int API(int A)
{
Hello();
return ~A;
}
接着对程序分别进行编译,一切没有问题。但当进行链接的时候,就会出现如下的错误:
------------------Configuration: Demo - Win32 Release------------------
Linking...
Demo.obj : error LNK2001: unresolved external symbol _API
..\BIN/Demo.exe : fatal error LNK1120: 1 unresolved externals
Error executing link.exe.
Demo.exe - 2 error(s), 0 warning(s)
奇怪!?我们明明定义的是名称为API的函数,但为什么编译器说没有定义_API呢?把它反过来,在CPP中调用C的函数,还是会出现同样的问题(读者可以试一试)。
为了搞清这个问题,就必须搞清楚到底C的函数和CPP的函数在编译之后,会变成什么样子。
因为C++的一个很重要的属性是可以重载,那什么是重载呢?我们可以从函数的角度来解释,重载就是一个函数名,在一个类中可以出现多次,并且每个函数体还不同。也就是C++所说的相同的形式,不同的操作。
这样就有一个问题,C++是怎么分清一个函数的?究竟需要调用哪个函数呢?C++会通过不同的参数来选择不同的、具有相同名称的不同的实现函数的调用(有点绕口令的味道,其实是要找到自己的运行体)。C++语言会解决这个问题。
还有一个问题,C++的函数被编译后,怎么样才能被正确地找到呢?
CPP是这样做的。在生成C++的函数时,函数附带了一些附加的参数信息。
现在我们可以在API.CPP的文件中再增加一个API的同名函数,这个API的函数就被重载了。
float API(float A)
{
return -A;
}
接下来,重新编译这个文件。没有问题,一切正常。
但这样在C中就有问题了。因为C分不清楚到底哪一个才是自己要调用的。
为了探索这个问题,就需要输出其map文件。
只要在“Project Settings”的“Link”页面中,选取“Generate mapfile”。重新编译,就会在工程目录处生成一个.map为后缀的文件,如图4.15所示。
图4.15 输出map文件
重新编译此工程,就可以在release目录找到demo.map的文件名。
用文本工具打开,但什么内容也看不见,这时怀疑可能是连接没有成功。先停止demo.c中的API函数的调用。再编译一次,这次成功通过。再打开demo.map,将出现很多函数的说明,可以用“查找”工具找到API函数的定义,如下:
Address Publics by Value Rva+Base Lib:Object
0001:00000000 ?API@@YAHH@Z
00401000 f Api.obj
0001:0000000f ?API@@YAMM@Z
0040100f f Api.obj
0001:00000019 ?Hello@XXX@@QAEXXZ
00401019 f Api.obj
0001:00000040 _Hello 00401040 f Demo.obj
0001:00000056 _main 00401056 f Demo.obj
可以看到,在CPP中:
Ø int API(int A) 变成了 ?API@@YAHH@Z
Ø float API(float A) 变成了 ?API@@YAMM@Z
Ø void main() 变成了 _main
这时可能大家已明白,为什么在调用API.CPP中的函数API中,编译说不能找到_API,原来是在C语言被编译的过程中,自动地加上了下划线。例如,main的函数在编译之后就变成了_main,而函数API就在编译时变成了_API。
在C++中,函数的参数就被扩展了。可以看到,两个API名的函数前面加了一个问号,紧跟着是两个“@”符号,其作用就像左括号,而右边的“@”符号就相当于右括号,而中间的字母就是用来表示参数的。
可以看见,两重载的函数使用了不同的参数,也就用不同的字母表示了。一个是整数,一个是浮点数,当这两个函数进行连接时,在相应的C++的连接过程中,编译器也会以编译出来的名字到对应的OBJ文件中进行查找。其实,在C++在编译过程中,会把参数的类型、名字和函数的名字组合成一个函数,这样就和程序中定义的函数名字相一致。
这样,两个同名函数在OBJ中就是不相同的,对编译器来说,它们是不同的。对可执行代码来说也是不同的,只从源程序来看它们是相同的。所以,当程序很复杂时,如果大量地使用重载的话,很有可能带来灾难性的后果,可能即使出现错误也无法找到错误源,因为它本身就被混淆了。
现在我们回过头来看出错信息,编译器报告的是找不到这个_API函数。我们在影像输出文件中也没有找到对应的函数,在输出中有的只是?API@@YAHH@Z和?API@@YAMM@Z,所以,这个程序基本就不可能连接上。
原因知道了,那怎么解决这个问题呢?
想到的最直接的方法就是修改C++文件的编译结果,让它去产生一个对应C的函数名,也就是按C的方法生成对应的函数名。所以,我们可以在C++文件的头部加上如下的代码段:
extern "C"
{
int API(int A);
}
extern "C" 语句的作用就是把C++的函数按C的约定编译。接下来继续编译,链接,一切正确。运行,没问题!看一看影像文件,就会变成如下:
Address Publics by Value Rva+Base Lib:Object
0001:00000000 _API 00401000 f Api.obj
0001:0000000f ?API@@YAMM@Z
0040100f f Api.obj
0001:00000019 ?Hello@XXX@@QAEXXZ
00401019 f Api.obj
0001:00000040 _Hello 00401040 f Demo.obj
0001:00000056 _main 00401056 f Demo.obj
可以看见,int API(int A)函数被编译成了_API,而float API(float A)还是以前的格式。如果想在函数中调用float API(float A)怎么办呢?那就需要把两个API函数设置成不同的函数名。例如
int API(int A) 变成 int iAPI(int A);
float API(float A) 变成 float fAPI(float A);
并且在C++文件的头部添加这两个函数的定义:
extern "C"
{
int iAPI(int A);
float fAPI(int A);
}
再在需要调用的函数中写上要使用的函数的名称。这样就可以运行了。
注意:同名函数实际使用时很容易引起误解,本人认为还是不用为妙。因为从表面上看,它好像是解放了“生产力”,其实这两个参数的名字是含糊不清的。对于调用者来说,经常是搞不清楚到底需要调用哪个函数,有时程序出现错误还不知道错误是在什么地方引起的。本人认为,这种所谓C++的特点对程序员来说,不见得是一件好事。
但有一种情况可以使用,那就是你很清楚将要调用哪个函数。并为某种特殊的目的—为函数或类的内部去使用。如果你把它作为API去使用,则最好必须避免。因为别人不可能特别清楚被重载函数之间的差别。
重载在实际中用处不大,但在教科书中介绍很多。
2. C++接口的方法
我们已经知道在C中调用C++的函数,那么,在C++中怎么调用C的函数呢?
其实,和以上在C中调用C++的函数一样,需要在C++的开始部分说明C函数的调用方式,这样就可以正常地使用了。具体实现可参考例子。
可以在所有的Windows的头文件中,看见如下的代码:
#ifdef __cplusplus //开始
extern "C" {
#endif
…………
#ifdef __cplusplus //结束
}
#endif
因为所有定义的API都是标准的API的函数,是通过C的函数来实现的,所以,当在C的环境中编程时,这一对条件宏就不起作用。在C++中,因为定义了__cplusplus,所以C++中就会按C的格式输出函数名,这样,无论是在C中还是在C++中,定义的函数都能被正确地使用。
你是否看过这些头文件?是否想过微软为什么要这么写这些?做程序不能不求甚解,好像只要知道用API,就万事大吉,无所不能。做程序一定要多问多想,求本求源的治学方法才是成为真正高手的必由之路。
微软的代码中有很多精华,只要真正地理解了,对自己的水平提高是很有帮助的。
大家知道,在C中嵌入一段汇编是很简单的,例如,如下的C函数代码,在其中嵌入汇编。
static int X;
void Hello(void)
{
_asm {
mov eax,X
mov [X],ebx
}
}
我们看到,在C中,只要在_asm的括号内输入对应的汇编代码就可以编译运行了。
现在假设要在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
}
}
(1)第一种方法是直接在Hello的函数中嵌入汇编指令,通过再编译,会被告之:
error C2420: 'X' : illegal symbol in second operand
error C2420: 'X' : illegal symbol in first operand
error C2415: improper operand type
Error executing cl.exe.
Api.obj - 3 error(s), 0 warning(s)
这是为什么呢?原来X这个变量是定义在一个类中的私有的变量,所以编译出的程序无法找到对应的变量X,有什么办法解决呢?我们是不是可以把变量定义到类的外部,这样,变量就不随类的变化而变化,所有类都只会使用一个X变量,就可以通过类中引用汇编来获得变量的值了。编译运行通过没问题!
这样做好像是成功了,可X变量没有定义在类中,也就是没有被封装起来,我们把程序的原义给改变了。我们就不能完整使用这个类了。看来这种方法还不行。
(2)第二种方法:是不是可以通过一个指针来取得值呢?
LPINT lpX;
lpX=&X;
_asm {
mov edx,lpX;
mov eax,[edx]
mov [edx],ebx
}
在这个段中,定义了一个指针lpX,让它指向X的地址。通过编译运行可以发现,这种方法是可行的。但又有问题,如果嵌入的汇编指令使用了很多C++中的变量,那么每一个变量都要定义一个指针,随后再通过指针来引用类中的变量,这样程序就会很复杂,也很麻烦,不利于程序的维护,看来这是一个“庸招”。
(3)第三种方法:我们都知道,在每个类中都有this这个指针,指向自己,能不能直接在汇编中用this来引用类中的对象呢?在宏汇编中有一种结构的用法,通过定义一个结构,然后能通过“点”的方法来用结构中的变量,那是不是可以在嵌入的汇编中用“点”的方法来操作类中的对象呢?于是,接下来实验如下代码:
_asm {
mov eax,this.X
mov this.X,ebx
}
编译一下,好像没问题,可运行时不对。是不是this的指向有问题呢?通过分析编译程序的汇编代码发现编译通过。要知道,一个代码是不是完全正确,一定要在汇编指令中看一看。接下来在调试环境中,查看上段嵌入汇编的真实的意义,语句如下:
66: _asm {
67: //X变量的使用
68:
69: mov eax,this.X
00401023 mov eax,dword ptr [this]
70: mov this.X,ebx
00401026 mov dword ptr [this],ebx
不对!第二条语句不是改变的类中的变量,而把this的指针改变了,这样做一定会出现问题。当程序运行这个汇编后,类的this指针发生了改变,这个类一定会不正确。有什么方法可以解决以上问题呢?那是不是可以用一个寄存器存放this的指针,而用this.X的方法来表示X在这个类的堆栈中的偏移呢?于是产生了第四种方法。
(4)第四种方法;我们能不能用一个寄存器间接寻址的方法来访问这个类中的变量呢?有了以上思想的指导,可以试一下以下代码。
_asm {
mov ecx,this;
mov eax,[ecx]this.X
mov [ecx]this.X,ebx
}
编译运行通过,好像没什么问题。是不是这样做就行呢?因为一个代码是不是完全正确,一定要在汇编指令中看一看。接下来在调试环境中,查看上段嵌入汇编的真实意义,语句如下:
52: _asm {
53: //X变量的使用
54: mov ecx,this;
00401023 mov ecx,dword ptr [this]
55: mov eax,[ecx]this.X
00401026 mov eax,dword ptr [ecx]
56: mov [ecx]this.X,ebx
00401028 mov dword ptr [ecx],ebx
57:
}
这段程序好像是没有什么问题,但只有一个变量,不知道在多个变量中是否能正确地产生偏移位置,所以,可以多加一个或多个变量试一下运行的结果。于是可以增加一个变量:
int X,Y;
然后在汇编代码段中加入对应的对Y变量的使用方法,增加如下的代码段:
//Y变量的使用
mov ecx,this;
mov eax,[ecx]this.Y
mov [ecx]this.Y,ebx
接下来,我们看一下生成汇编的代码段:
52: _asm {
53: //X变量的使用
54: mov ecx,this;
00401023 mov ecx,dword ptr [this]
55: mov eax,[ecx]this.X
00401026 mov eax,dword ptr [ecx]
56: mov [ecx]this.X,ebx
00401028 mov dword ptr [ecx],ebx
57:
58: //Y变量的使用
59: mov ecx,this;
0040102A mov ecx,dword ptr [this]
60: mov eax,[ecx]this.Y
0040102D mov eax,dword ptr [ecx+4]
61: mov [ecx]this.Y,ebx
00401030 mov dword ptr [ecx+4],ebx
62: }
可以看见,[this + 0 ]指向X变量地址,而[this+4]指向Y变量地址,这样就没有错误了。Y为[this+4],是因为int类型占用了四个字节长度。这就是“绝招”了。
通过以上的探索和学习,我们已经能在C++的代码中嵌入汇编指令,但也许有人会问,这是什么年代了,现在都用Java、COM等高级抽象的语言和工具了,汇编还有用吗?
很有用,汇编最大的好处就是可以直接地控制CPU的运算,这样就能很方便地进行I/O和高速的运算,最大地发挥CPU的运算能力。例如,汇编在科学运算中的矩阵运算、游戏和多媒体的处理很有用。
最重要的一点是,汇编可以确定程序出错的真正原因。
最近我在DVD 3的声音的解码中,做了音频处理类,用
它来控制音频的均衡,而对这个类的运算是通过重载运算符来实现的。
这样,DVD中各段音频能通过此类的对象进行操作。
我按以上的方法实现后,发现这样做速度极慢。后来,我把类的重载运算用汇编实现之后,速度一下就提高了上百倍。
通过以上分析的过程,给我们两点启示:
(1)做程序一定要用程序的方法去思考问题,假如以主观的方法来分析问题,C的方法和C++的方法就应该一样,可实际运行时,机器是不会按你的想像去运行程序的,它只会根据自己的机理去运行程序。当遇到这种问题时,最好的方法是看一看机器编译出的实际代码是什么,机器实际是怎么执行的,在运行时干了什么。以上通过输出map文件来查看汇编代码就是这种方法。
当看了map文件,就会知道输出的结果,有的有下划线,有的改变了输出名称,变成以问号开始。回过头来分析在C++中嵌入汇编时,如果光凭主观的想像,则怎么也解决不了这个问题,但如果打开代码研究一下,不就可以轻易地解决了吗?
(2)通过对以上代码的分析,可以发现,当遇到问题时,不能只求表面。很多人遇到不能解决的问题时,总是一遍一遍地去试用各种方法,而不是从代码编译结果出发去解决问题,这样就很容易掉进漩涡中出不来。
我当时解决这个问题时,也是通过分析编译的结果代码,来得到以上方法的。分析问题和解决问题要从本质上去研究。
很多人学习编程时,一下就想编写出很高级的程序,一些VB,PB高手好像什么都能做,但因为没有对程序的本质进行深入的学习,所以有时遇见问题会不知所措。所以,如果希望用一两个月就成为高手的人永远成不了高手,一定要有踏实的根底,一步一步地磨练。所以,本书的很多章节,希望广大读者能深入下去学习,只有深入进去,才能真正地学到一些知识。 |
|