Assembly 重载函数和名字改编
C++编程语言是C语言的一种扩展形式。许多C语言和汇编语言接口的基本规则同样适用于C++。但是,有一些规则需要修正。同样,拥有一些汇编语言的知识,你能很容易理解C++中的一些扩展部分。这一节假定你已经有一定的C++基础知识。
C++允许不同的函数(和类成员函数)使用同样的函数名来定义。当不止一个函数共享同一个函数名时,这些函数就称为重载函数。在C语言中,如果定义的两个函数使用的函数名是一样,那么连接器将产生一个错误,因为在它连接的目标文件中,一个符号它将找到两个定义。例如,考虑图7.10中的代码。等价的汇编代码将定义两个名为_f的标号,而这明显是错误的。
C++使用和C一样的连接过程,但是通过执行名字改编或修改用来标记函数的符号来避免这个错误。在某种程序上,C也早已经使用了名字改编。当创建函数的标号时,它在C函数名上增加了一条下划线。但是,C语言将以同样的方法来改编图7.10中的两个函数名,那么将会产生一个错误。C++使用一个更高级的改编过程:为这些函数产生两个不同的标号。例如:图7.10中的第一个函数将由DJGPP指定为标号_f_ _Fi,而第二个函数,指定为_f __Fd。这样就避免了任何的连接错误。
不幸的是,关于在C++中如何改编名字并没有一个标准,而且不同的编译器改编的名字也不一样。例如,Borland C++将使用标号@f$qi和@f$qd来表示图7.10中的两个函数。但是,规则并不是完全任意的。改编后的名字编码成函数的签名。一个函数的签名是通过它携带的参数的顺序和类型来定义的。注意,,携带了一个int参数的函数在它的改编名字的末尾将有
一个i(对于DJGPP 和Borland都是一样),而携带了一个double参数的函数在它的改编名字的末尾将有一个d。如果有一个名为f的函数,它的原型如下:
void f ( int x, int y, double z );
DJGPP将会把它的名字改编成_f_ _Fiid而Borland将会把它改编成@f$qiid。函数的返回类型并不是函数签名的一部分,因此它也不会编码到它的改编名字中。这个事实解释了在C++中的一个重载规则。只有签名唯一的函数才能重载。就如你能看到的,如果在C++中定义了两个名字和签名都一样的函数,那么它们将得到同样的签名,而这将产生一个连接错误。
缺省情况下,所有的C++函数都会进行名字改编,甚至是那些没有重载的函数。它编译一个文件时,编译器并没有方法知道一个特定的函数重载与否,所以它将所有的名字改编。事实上,和函数签名的方法一样,编译器同样通过编码变量的类型来改编全局变量的变量名。因此,如果你在一个文件中定义了一个全局变量为某一类型然后试图在另一个文件中用一个错误的类型来使用它,那么将产生一个连接错误。C++这个特性被称为类型安全连接。它同样暴露出另一种类型的错误:原型不一致。当在一个模块中函数的定义和在另一个模块使用的函数原型不一致时,就发生这种错误。在C中,这是一个非常难调试出来的问题。C并不能捕捉到这种错误。程序将被编译和连接,但是将会有未定义的操作发生,就像调用的代码会将和函数期望不一样的类型压入栈中一样。在C++中,它将产生一个连接错误。
当C++编译器语法分析一个函数调用时,它通过查看传递给函数的参数 的类型来寻找匹配的函数。如果它找到了一个匹配的函数,那么通过使用 编译器的名字改编规则,它将创建一个CALL来调用正确的函数。 因为不同的编译器使用不同的名字改编规则,所以不同编译器编译 的C++代码可能不可以连接到一起。当考虑使用一个预编译的C++库时, 这个事实是非常重要的!如果有人想写出一个能在C++代码中使用的汇编 程序,那么他必须知道要使用的C++编译器使用的名字改编规则(或使用下 面将解释的技术)。 机敏的学生可能会询问在图 7.10中的代码到底能不能如预期般工作。 因为C++改编了所有函数的函数名,那么printf将被改编,而编译器将不 会产生一个到标号 printf处的CALL调用。这是一个非常正确的担忧!如 果printf的原型被简单地放置在文件的开始部分,那么这就将发生。原型为:
int printf ( const char ∗, ...);
DJGPP将会把它改编为_printf_ _FPCce。(F表示function ,函数 ,P表示pointer, 指针 ,C表示const ,常量 ,c表示char而e表示省略号。)那么它将不会调用 正规C库中的printf函数!当然,必须有一种方法让C++代码用来调用C代 码。这是非常重要的,因为到处都有 许多 非常有用的旧的C代码。除了允许 你调用遗留的C代码外,C++同样允许你调用使用了正规的C改编约定的汇 编代码。 C++扩展了extern关键字,允许它用来指定它修饰的函数或全局变量 使用的是正规C约定。在C++术语中,称这些函数或全局变量使用了C 链 接 。例如,为了声明printf为C链接,需使用下面的原型:
extern ”C” int printf ( const char ∗, ... );
这就告诉编译器不要在这个函数上使用C++的名字改编规则,而使用C规 则来替代。但是,如果这样做了,那么printf将不可以重载。这就提供了 一个简易的方法用在C++和汇编程序接口上:使用C链接定义一个函数, 然后再使用C调用约定。 为了方便,C++同样允许定义函数或全局变量块的C链接。通常函数或 全局变量块用卷曲花括号表示。
extern ”C” {
/∗ C链接的全局变量和函数原型 ∗/
}
如果你检查了当今的C/C++编译器中的ANSI C头文件,你会发现在每 个头文件上面都有下面这个东西:
#ifdef _ _cplusplus
extern ”C” {
#endif
而且在底部有一个包含闭卷曲花括号的同样的结构。C++编译器定义了 宏 cplusplus(有 两条 领头的下划线)。上面的代码片断如果用C++来编 译,那么整个头文件就被一个extern "C"块围起来了,但是如果使用C来 编译,就不会执行任何操作(因为对于extern "C",C编译器将产生一个 语法错误)。程序员可以使用同样的技术用来在汇编程序中创建一个能 被C或C++使用的头文件。