`
fireDragonpzy
  • 浏览: 462640 次
  • 性别: Icon_minigender_1
  • 来自: 济南
社区版块
存档分类
最新评论

c++进阶(二)函数调用约定及函数名称修饰符

c++ 
阅读更多
几乎每种编程语言都有函数的概念,而作为函数,就一定有参数的概念;一般来说,参数的传递是通过堆栈来实现的,堆栈是一种先入后出的数据结构,使用Push()把参数压入栈中,使用Pop()把参数弹出栈,而且Push()和Pop()必须成对出现;
重要的一点:函数调用约定不仅决定了发生函数调用时函数参数的入栈顺序,还决定了是由调用者函数还是被调用者函数负责清除函数栈中的参数并还原栈;
不过,对于函数的参数来说,有两种不同的处理方法:
第一种方法:把参数按照原顺序(函数定义时的顺序)压入栈中,然后使用CALL指令调用函数的地址,而函数使用Pop()把参数从栈中弹出然后处理;由于堆栈的先入后出特性,所以这种方法对调用者有利,因为在实际调用的时候,调用者得到的是正序的参数(编译阶段由编译器负责压栈),而被调函数得到的是反序的参数;
第二种方法:把参数按照反序(函数定义时的顺序的逆序)压入栈中,然后使用CALL指令调用函数地址,而函数使用POP把参数弹出栈然后处理;这种方法对被调用者有利,因为被调函数得到的是正序的参数,而函数的压栈操作时在编译阶段由编译器做的;
C语言和PASCAL语言分别使用者两种方式:C语言使用正序压栈反序出栈的方式,PASCAL语言使用反序压栈正序出栈的方式;而Windows使用的调用方式跟PASCAL语言的调用方式是一样的,所以,以前老的C程序员编写Windows程序时需要使用关键字PASCAL来明确指出使用PASCAL的调用规则,现在一般不再使用PASCAL关键字了,而使用__stdcall说明符,表示使用的是标准调用;
现在称为标准调用的就是PASCAL语言的调用方式:反序压栈,正序出栈;这样做的确有好处,因为压栈行为是编译器编译好的(编译时行为),虽然麻烦,但是只在编译阶段做一次,而出栈行为是程序实际运行时的调用行为(运行时行为),需要反复多次执行,使用正序的参数,对被调函数来说,处理上方便不少;
函数调用约定由很多种方式,常见的有__cdecl、__stdcall、__fastcall,C++的编译器还支持__thiscall方式,不少的C/C++编译器还支持naked call方式;
1.__cdecl方式:
这种方式是编译器默认的函数调用方式,所有非C++成员函数和那些没有使用__stdcall或__fastcall声明的函数默认都是__cdecl方式;__cdecl使用的是C语言的调用方式:函数参数按照从右向左的顺序入栈(反序入栈),并由函数调用者负责在调用时把参数弹出栈,调用结束时清空栈中的参数;由于每次函数调用都要由编译器产生清除/还原堆栈的代码,所以使用__cdecl方式编译出来的程序比使用__stdcall方式编译出来的程序要大很多,但是__cdecl调用方式是由调用者函数负责把参数弹出和清空栈中的函数参数,所以,__cdecl方式支持可变参数,比如printf和Windows的API wsprintf就是__cdecl调用方式;对于C函数,__cdecl方式的名字修饰符约定是在函数名称前面添加一个下划线;对于C++函数,除非特别使用extern "C",C++函数使用不同的名字修饰符;
2.__stdcall方式:
这种方式的函数调用约定将函数参数按照从右向左的顺序压入栈中,函数调用时由被调用函数负责把参数弹出栈,并在调用结束时由被调用函数负责清除函数栈中的参数;除非使用指针或引用类型的参数,其它所有参数的传递都是采用传值的方式传递;对于C函数,__stdcall的名称修饰方式是在函数名称前面添加下划线,在函数后面添加@符号和函数参数的大小;例如: _function_name@number;
3.__fastcall方式:
这种函数调用约定方式在可能的情况下使用寄存器传递参数,通常是前两个DWORD类型的参数或者是较小的参数使用ECX和EDX寄存器传递,其余的参数按照从右向左的顺序压入栈中(反序入栈),由被调用函数负责在调用时弹出栈中的参数,并在调用结束时(函数返回之前)清除函数栈中的参数;编译器使用两个@来修饰函数的名字,后面跟十进制数字表示函数参数列表的大小;比如:@function_name@number;需要注意的是:__fastcall函数调用约定在不同的编译器上可能有不同的实现;比如:16位的编译器和32位的编译器,另外,在使用内嵌汇编代码时,还要注意不能和编译器使用的寄存器有冲突;
4.thiscall方式:
这种方式仅仅用于C++类的成员函数的调用,函数参数按照从右向左的顺序入栈,类实例的指针this通过寄存器ECX传递,组要注意的是,thiscall不是C++的关键字,不能使用thiscall来声明函数,thiscall关键字只能由编译器使用;
5.naked call方式:
在使用调用约定__cdecl、__stdcall、__fastcall和thiscall时,编译器会在需要的时候自动在函数开始处(进入函数时)添加保存ESI、EDI、EBX、EBP寄存器的代码,在退出函数时恢复这些寄存器的内容;而使用naked call方式不会产生这样的代码;这也就是为什么称其为naked的原因吧.naked call不是类型修饰符,故必须与__declspec共同使用;如下:
__declspec(naked) int Func(parameters-list)
{ ... }

注意:VC的编译环境默认使用的是__cdecl的调用约定;程序员也可以自行设定调用约定;比如:在Windows系统上写代码的时候,常常用到WINAPI宏,编译器会根据编译器的设置把这个宏编译成适当的调用约定;在WIN32中,它被编译成__stdcall;
还有的资料上有以下的观点:
C语言调用约定:参数正序入栈,反序出栈;
PASCAL语言调用约定:参数反序入栈,正序出栈;属标准调用;Windows及Windows API就采用这种方式;

函数名称修饰符方式:
函数的名称修饰符就是编译器在编译期间创建的一个字符串,用来指明函数的定义或原型.LINK程序或其它工具有时需要指定函数的名称修饰符来定位函数的正确位置;多数情况下,程序员不需要知道函数的名称修饰符,LINK程序会自动区分它们.当然在某些情况下需要指定函数的名称修饰符,例如在C++程序中为了让LINK程序或其它工具能够匹配到正确的函数名字,就必须为重载函数和一些特殊函数(构造函数或析构函数)指定名称修饰符.另一种需要指定函数名称修饰符的情况是在汇编程序中调用C或C++的函数,如果函数名字、调用约定、返回值类型或函数参数有任何改变,那么原来的名字修饰符就不再有效了,必须指定新的名称修饰符.C和C++程序的函数在内部使用不同的名字修饰符规则:
1、C编译器的函数名称修饰规则:
对于__stdcall调用约定,编译器和链接器会在输出函数的名称前面加上一个下划线,函数名称后面加上一个@符号和一个表示参数大小(字节数)的数字,例如:_FunctionName@number; __cdecl调用约定仅在输出函数名称前面加上一个下划线,例如:_FunctionName; __fastcall调用约定会在输出函数名称前面加上一个@符号,后面加上一个@符号和一个表示参数大小的数字,例如:@FunctionName@number;
2、C++编译器的函数名称修饰规则:
C++的函数名称修饰符规则有点复杂,但是能表示更充足的信息,通过分析函数名称修饰符,能够知道函数的调用方式、返回值类型、参数个数,甚至也可以知道参数类型.
非成员函数的名称修饰符规则:
不管__cdecl、__fastcall还是__stdcall调用方式,函数修饰符都以一个问号"?"开始,后面紧跟函数的名称,再后面是参数表的开始标识和按照参数类型代号拼出的参数表.对于__stdcall方式,参数表的开始标识是"@@YG";对于__cdecl方式,参数表的开始标识是"@@YA";对于__fastcall方式,参数表的开始标识是"@@YI";参数表的拼写代号如下:
X  --- void
D  --- char
E  --- unsigned char
F  --- short
H  --- int
I  --- unsigned int
J  --- long
K  --- unsigned long(DWORD)
M  --- float
N  --- double
_N --- bool
U  --- struct
......
指针的方式有些特别,用PA表示指针,用PB表示const类型的指针;后面的代号表明指针类型,如果相同类型的指针连续出现,则以"O"代替,一个"O"表示重复一次.U表示结构类型,通常后面跟结构体的名称,用"@@"表示结构类型名的结束.函数的返回值不做特殊处理,它的描述方式和函数参数一样,紧跟着参数表的开始标识,也就是说,函数参数表的第一项实际上表示函数的返回值类型;参数表后面以"@Z"标识整个名字的结束,如果该函数没有参数,则以"Z"标识结束.
例1:函数声明: int Function1(char*, unsigned long);
其函数名称修饰符为"?Function1@@YGHPADK@Z"
例2:函数声明: void Function(void);
其函数名称修饰符为"?Function2@@YGXXZ";
成员函数名称修饰符规则:
C++类的成员函数采用thiscall的调用约定,成员函数的名称修饰符规则与非成员函数的名称修饰符规则有所不同;首先就是在函数名称与参数表之间插入以字符"@"引导的类名;其次是参数表的开始标识不同,public成员函数的参数表开始标识是"@@QAE",protected成员函数的参数表的开始标识是"@@IAE",private成员函数的参数表的开始标识是"@@AAE";如果函数声明中使用了const关键字,则成员函数的名称修饰符又有所不同了,此时,public成员函数的参数表的开始标识是"@@QBE",protected成员函数的参数表的开始标识是"@@IBE",private成员函数的参数表的开始标识是"@@ABE";如果参数类型是类实例的引用,则使用"AAV1",对于const类型的引用,则使用"ABV1";例如:
class CTest
{
  private:
    void Function(int);
  protected:
    void CopyInfo(const CTest& src);
  public:
    long DrawText(HDC hdc, long pos, const TCHAR* text, RGBQUAD color, BYTE bUnder, bool bSet);
    long InsightClass(DWORD dwClass) const;
};
成员函数Function()的名称修饰符是"?Function@CTest@@AAEXH@Z",字符串"@@AAE"表示该成员函数是私有成员函数;成员函数CopyInfo()只有一个参数,而且是对CTest类实例的一个常引用,其修饰符名称是"?CopyInfo@CTest@@IAEXABV1@@Z";成员函数DrawText()是个比较复杂的函数,不仅有字符串参数,还有结构体参数和HDC句柄参数,需要指出的是,HDC实际上是一个HDC__结构类型的指针,这个参数的表示就是"PAUHDC__@@",成员函数DrawText的完整的名称修饰符是"?DrawText@CTest@@QAEJPAUHDC__@@JPBDUtagRGBQUAD@@E_N@Z";成员函数InsightClass()是个const类型的共有成员函数,其参数表开始标识是"@@QBE",完整的名称修饰符就是"?InsightClass@CTest@@QBEJK@Z";
总之,无论是C函数名称修饰规则还是C++函数名称修饰规则,均不会改变输出函数名称的大小写,这与PASCAL的函数调用约定不同,PASCAL约定输出函数的名称无任何修饰且全部大写;
查看函数的名称修饰符:
在Windows下,有两种方法查看函数的名称修饰符:使用编译输出列表或者是dumpbin工具;使用/FAc、/FAs或/FAcs命令行参数可以让编译器输出函数或变量名称列表;使用dumpbin.exe /SYMBOLS命令也可以获得obj文件或lib文件中的函数名或变量名列表;此外,还可以使用undname.exe将修饰符名转换成未修饰形式;
如果要在C++程序中调用C语言编译的函数或库,通常要按照下面的方法来声明C语言函数或头文件:
#ifdef __cplusplus
extern "C" {
#endif
long Function(long);
#ifdef __cplusplus
}
#endif
VC的编译器会根据源文件的扩展名来选择编译方式,如果源文件扩展名是.c,则编译器会采用C语言的语法来编译,如果源文件的扩展名是.cpp,则编译器会采用C++的语法来编译;所以,最好的方法就是使用extern "C";
gcc编译器采用的也是__cdecl方式的调用约定;这样的话,就很容易实现可变参数的函数了;函数调用所涉及到的参数传递是通过函数的栈来实现的,子函数从栈中读取传递给它的参数,如果参数是从左向右压栈的话,那么子函数的最后一个参数在栈顶,然后依次是倒数第二个参数,...;如果是从右向左压栈的话,那么子函数的第一个参数在栈顶,然后依次是第二个参数,...;这样的话,参数压栈的顺序决定了子函数读取其参数的位置;对于具有可变参数的函数来说,并不知道具体有几个参数,需要知道某些信息才行,比如:printf()函数,就是从前面的格式化字符串参数fmt中分析出参数的个数;所以,被调函数返回时清理栈的方式,对于可变参数是无法实现的,因为被调函数并不知道需要弹出的参数的数量;而对于调用者函数自己来说,自己传递给被调函数多少个参数(通过把参数压栈时统计得知)当然是一清二楚的,这样被调函数返回之后,栈的清理工作就可以做到准确无误了(不多不少地把栈中的参数清理干净);


摘自:http://bdxnote.blog.163.com/blog/static/84442352010017361476/
分享到:
评论

相关推荐

    函数调用约定与函数名称修饰规则.pdf

    ### 函数调用约定与函数名称修饰规则 #### 调用约定(Calling Convention) 调用约定是指在程序设计语言中为了实现函数调用而建立的一种协议。这种协议规定了该语言的函数中的参数传送方式、参数是否可变以及由谁...

    C++调用C函数实例详解

    首先,为什么要使用extern “C”修饰符? C++调用其它语言的函数,由于编译器生成函数的机制不一样,所以需要经过特殊处理,才可以调用。调用C语言的函数,需要在函数声明的地方语句extern “C”。如果不使用该语句...

    c/c++中函数调用方式

    在C/C++编程语言中,函数调用方式是程序设计中的关键概念之一,它涉及到如何在函数调用过程中处理参数的传递以及栈空间的管理。根据给定的文件信息,我们可以深入探讨C/C++中几种主要的函数调用方式:__cdecl、__...

    DLL中调用约定和名称修饰

    在C++中,为了支持操作符重载和函数重载,编译器通常会对每个函数入口点的符号名进行改写,以便允许相同的函数名拥有不同的参数类型或作用域,这被称为**名称修饰**(Name Decoration) 或**名称改编**(Name Mangling)...

    函数调用约定.docx

    函数调用约定是编程语言中规定函数调用过程的一个重要机制,主要涉及到参数传递和堆栈清理的责任分配。在C和C++中,不同的调用约定适用于不同的场景,特别是涉及到DLL(动态链接库)和Win API函数时,选择正确的调用...

    C++调用的多种形式

    C++调用约定是指在C++中函数调用的多种形式,它们是:_stdcall、_cdecl、_fastcall、thiscall和naked call。每种调用约定都有其特点和应用场景。 _stdcall调用约定 _stdcall调用约定是Pascal程序的缺省调用方式,...

    深度探索C++对象模型及学习c++的50条忠告 c++ 进阶 编程 提高 大师作品 经典力作

    多态性是C++的另一大特点,它使得不同的对象可以响应相同的函数调用。这是通过虚函数和抽象类实现的,为设计灵活、可扩展的系统提供了可能。深入理解虚函数表的工作原理,以及动态绑定的机制,对于编写面向对象的...

    C语言函数调用规定[文].pdf

    在C语言中,函数调用是一项基础且至关重要的概念,涉及到程序执行流程和参数传递机制。...在实际编程中,编译器会根据函数声明和定义中的修饰符来确定使用哪种调用约定,因此正确地声明和定义函数至关重要。

    C++中的函数修饰符深入讲解

    在C++编程语言中,函数修饰符是用来控制函数行为的关键字。这些修饰符可以在函数声明时添加,以改变函数的特性、行为或访问权限。在本文中,我们将深入探讨C++中的函数修饰符,分为函数名前和函数名后两大类别。 ##...

    C++:浅谈修饰符const

    在C++编程语言中,const是一个非常重要的修饰符,它用于声明一个变量为常量。这个修饰符的主要作用是确保变量的值在程序执行期间不会被改变,以增强程序的可读性和安全性。以下是关于const修饰符的详细知识点: 1. ...

    C++程序设计_面向对象进阶_C++_

    C++通过访问控制修饰符(public, private, protected)实现封装。 3. **继承**:继承允许一个类(子类或派生类)从另一个类(父类或基类)继承属性和行为,减少了代码重复,增强了代码的可扩展性。C++支持单继承和...

    C++builder调用VCdll.pdf

    但是,__declspec(dllexport)只是表示这个函数是一个DLL导出函数,而__stdcall是一种函数调用约定,两者应该是没有冲突的。 在C++builder和VC中,函数名的修饰方式不同。在C++builder中,__cdecl的函数输出前会带:...

    C++函数对象的定义及使用

    下面将详细介绍C++函数对象的定义、使用以及其优势。 ### 定义函数对象 1. **创建类模板**:首先,我们需要定义一个类,该类包含一个或多个被重载的`()`操作符。这个操作符是函数对象的核心,它使得对象能够像函数...

    C++构造函数析构函数

    内联函数的定义是在函数原型所在的类体内,编译器会尝试将函数体插入到每个调用处,避免了函数调用带来的开销。在上面的例子中,`mj()`和`sc()`都被声明为内联成员函数,它们直接计算和输出矩形的面积。 在C++中,...

    C#调用C/C++ Dll中函数实例代码

    然后,可以在P/Invoke声明中使用`Out`或`In, Out`修饰符来指定参数的方向: ```csharp [DllImport("MyDll.dll")] public static extern void ProcessStruct([In, Out] MyStruct structure); ``` 在调用时,可以...

    C++函数组成

    ### C++函数组成及其属性详解 #### 一、引言 在C++编程语言中,函数是程序的基本构建块之一,用于实现特定的功能并提高代码的重用性和可维护性。一个函数通常由函数头和函数体两部分组成。本文将详细介绍C++中的...

    C++函数(1)源代码.zip_c++ 函数

    9. **const修饰符** `const`关键字可以用于函数参数,表示该参数不会被函数修改。例如: ```cpp void display(const int num) { // num不能在此处被修改 } ``` 10. **递归函数** 递归函数是指函数在其定义中...

    C++和C#互相调用Demo程序

    这通常涉及使用`ref class`或`value class`,并用`public`修饰符声明成员。 2. 在C#项目中,添加对C++/CLI项目的引用。 3. 在C#代码中,可以直接使用C++/CLI类,就像它们是普通的.NET类一样。 这个Demo程序包含了...

    回调函数举例

    `CALLBACK` 是一个修饰符,表明这个函数可以被Windows API函数调用。尽管在这个示例中它不直接影响函数的行为,但在实际的Windows编程中,这个关键字指示编译器使用标准的调用约定。 ##### 2. 定义回调函数 ```c++...

    函数调用与堆栈

    为了更精确地控制局部变量的存储方式及其生命周期,C++允许使用特定的存储类修饰符。主要有: 1. **auto**:默认情况下,局部变量具有自动生存期。`auto`关键字可以省略,因为它是默认值。 2. **static**:用于声明...

Global site tag (gtag.js) - Google Analytics