Assembly 重载函数和名字改编

2018-12-04 17:43 更新

C++编程语言是C语言的一种扩展形式。许多C语言和汇编语言接口的基本规则同样适用于C++。但是,有一些规则需要修正。同样,拥有一些汇编语言的知识,你能很容易理解C++中的一些扩展部分。这一节假定你已经有一定的C++基础知识。


两个名为f()的函数


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++使用的头文件。


引用

引用的例子

引用是C++的另一个新特性。它允许你传递参数给函数,而不需要明确 使用指针。例如,考虑图 7.11中的代码。事实上,引用参数是非常简单, 实际上它们就是指针。只是编译器对程序员隐藏它而已(正如Pascal编译器 把var参数当作指针来执行)。当编译器产生此函数调用的第7行代码的汇编语句时,它将y的地址传递给函数。如果有人是用汇编语言书写的f函数, 那么他们操作的情况,就好像原型如下似的:

void f( int ∗ xp);

引用是非常方便的,特别是对于运算符重载来说是非常有用的。运算符 重载又是C++的另一个特性,它允许你在对结构体或类类型进行操作时赋 予普通运算符另一种功能。例如,一个普遍的使用是赋予加号(+)运算符能 将字符串对象连接起来的功能。因此,如果a和b是字符串,那么a + b将得 到a和b连接后的字符串。实际上,C++可以调用一个函数来做这件事(事实 上,上面的表达式可以用函数的表示法来重写为:operator +(a,b))。为 了提高效率,有人可能会希望传递字符串的地址来代替传递他们的值。若 没有引用,那么将需要这样做:operator +(&a,&b),但是若要求你以运 算符的语法来书写应为:&a + &b。这是非常笨拙而且混乱的。但是,通过 使用引用,你可以像这样书写:a + b,这样就看起来非常自然。

内联函数

到目前为止,内联函数 又是C++的另一个特性。内联函数照道理应该 可以取代容易犯错误的,携带参数的,基于预处理程序的宏。回想一下 在C中,书写一个求数的平方的宏可以是这样的: 

#define SQR(x) ((x)∗(x)) 

因为预处理程序不能理解C而采用简单的替换操作,在大多数情况下, 圆括号里要求是能正确计算出来的值。但是,即使是这个版本也不能给 出SQR(x++)的正确答案。 宏之所以被使用是因为它除去了进行一个简单函数的函数调用的额外 时间开支。就像子程序那一章描述的,执行一个函数调用包括好几步。

内联函数

对于一个非常简单的函数来说,用来进行函数调用的时间可能比实际上 执行函数里的操作的时间还要多!内联函数是一个更为友好的用来书写代 码的方法,让代码看起来象一个标准的函数,但是它并 不是 CALL指令能调 用的普通代码块。出现内联函数的调用表达式的地方将被执行函数的代码 替换。C++允许通过在函数定义前加上inline关键字来使函数成为内联函 数。如果,考虑在图 7.12中声明的函数。第10行对f的调用将执行一个标准的函数调用(在汇编语言中,假定x的地址为ebp-8而y地址为ebp-4):

示例

这种情况下,使用内联函数有两个优点。首先,内联函数更快。没有 参数需要压入栈中,也不需要创建和毁坏堆栈帧,也不需要进行分支。其 次,内联函数调用使用的代码是非常少!后面一点对这个例子来说是正确的,但是并不是在所有情况下都是正确的。 

内联函数的主要优点是内联代码不需要连接,所以对于使用内联函数的 所有 源文件来说,内联函数的代码都必须有效。前面的汇编代码的例子展示了这一点。对于非内联函数的调用,只要求知道参数,返回值类型,调 用约定和函数的函数名。所有的这些信息都可以从函数的原型中得到。但 是,使用内联函数调用,就必须知道这个函数的所有代码。这就意味着如 果改变了一个内联函数中的任何部分,那么 所有 使用了这个函数的源文件 必须重新编译。回想一下对于非内联函数,如果函数原型没有改变,通常 使用这个函数的源文件就不需要重新编译。由于所有的这些原因,内联函 数的代码通常放置在头文件中。这样做违反了在C语言中标准的稳定和快速 准则:执行的代码语句 决不能 放置在头文件中。
以上内容是否对您有帮助:
在线笔记
App下载
App下载

扫描二维码

下载编程狮App

公众号
微信公众号

编程狮公众号