目录
1、函数重载
1.1定义
在C++中可以为两个或以上的函数提供相同的函数名称,只要参数类型不同或者参数类型相同而参数个数不同。
int my_max(int a,int b)
{
return a>b?a:b;
}
char my_max(char a,char b)
{
return a>b?a:b;
}
double my_max(double a,double b)
{
return a>b?a:b;
}
int main()
{
int x=my_max(12,23);//调用第一个函数
char ch=my_max('a','b');//调用第二个函数
double dx=my_max(12.23,23.34);//调用第三个函数
return 0;
}
如果两个函数的函数名相同,而参数表中参数的个数或类型或顺序不同,则认为这两个函数是重载
//重载函数
void print(int a,char b);
void print(char b,int a);
//但尽量不要这样进行重载,字符型可以转成整型,整型也可以转成字符型,这样就会出现二义性
int main()
{
printf('a','b');//error
printf(12,23);//error
return 0;
}
1.2函数重载的规则
1、如果两个函数的参数表相同,但是返回值类型不同,会标记为编译错误:函数的重复声明。
//无法重载
unsigned int my_max(int a,int b);
int my_max(int a,int b);
原因:在主函数中调用无法区分两个函数
2、参数表的比较过程与形参名无关
//无法重载
void print(int* b,int n);
void print(int* s,int len);
3、如果在两个函数的参数表中,只有缺省实参不同,则第二个声明被视为第一个声明的重复声明。
//无法重载
void print(int* b,int n);
void print(int* s,int len=10);
4、typedef名为现有的数据类型提供了一个替换名,并没有创建新的类型,因此,如果两个函数参数表的区别只在于一个使用了typedef,另一个使用与其相应的类型,则该参数表被视为相同的参数列表。
//无法重载
typedef unsigned int u_int;
void func(u_int a);
void func(unsigned int b);
5、当一个形参类型有 const 或 volatile 修饰时,如果形参是按值传递方式定义,在识别函数声明是否相同时,并不考虑 const 和 volatile 修饰符,不能作为重载的依据。
//无法重载
void fun(int a){}
void fun(const int a){}
6、当一个形参类型有 const 或 volatile 修饰时,如果形参定义指针或引用时,在识别函数声明是否相同时,就要考虑 const 和 volatile 修饰符,可以作为重载的依据。
重载函数
void fun(int* p){}
void fun(const int* p){}
int main()
{
int a=10;
const int b=20;
//可以进行区分&a优先调用第一个函数&b只能调用第二个函数
fun(&a);
fun(&b);
return 0;
}
void fun(int& a){}
void fun(const int& a){}
int main()
{
int x=10;
//可以进行区分x优先调用第一个函数10只能调用第二个函数
fun(x);
fun(10);
return 0;
}
7、注意函数调用的二义性:
如果在两个函数的参数表中,形参类型相同,而形参个数不同,形参默认值将会影响函数的重载
void fun(int a){}
void fun(int a,int b=10){}
//两个函数依然是重载的,但是在主函数中调用会产生二义性,导致编译错误
int main()
{
func(12);//error
func(12,23);//ok
return 0;
}
8、函数重载解析的步骤如下:
1.确定函数调用考虑的重载函数的集合,确定函数调用中实参表的属性
2.从重载函数集合中选择函数,该函数可以在(给出实个数和类型 的情况下可以调用函数
3.选择与调用最匹配的函数
1.3重载与优先匹配
引用
//在主函数中不调用时可以进行重载
void print(int a){}
void print(int& b){}
void print(const int& c){}
int main()
{
int x=10;
print(x);//error,会产生二义性
return 0;
}
//当定义了引用类型时不要去定义值类型
void print(int& b){}
void print(const int& c){}
int main()
{
int x=10;
const int y=20;
print(x);//ok
print(y);//ok
return 0;
}
//依然可以编译通过
void print(const int& c){}
int main()
{
int x=10;
const int y=20;
print(x);//ok,最佳匹配为值类型或者普通引用类型,如果没有普通引用后者值类型才会匹配常引用类型。
print(y);//ok
return 0;
}
void print(int& c){}
int main()
{
int x=10;
const int y=20;
print(x);//ok,匹配普通引用类型
print(y);//error,只能匹配常引用类型
return 0;
}
指针
void func(int* p){}
void func(const int* s){}
int main()
{
int x=10;
const int y=20;
func(&x);//ok,优先匹配第一个函数
func(&y);//ok,只能匹配第二个函数
return 0;
}
void func(const int* s){}
int main()
{
int x=10;
const int y=20;
func(&x);//ok,优先匹配普通指针类型,没有则匹配常指针类型
func(&y);//ok
return 0;
}
void func(int* p){}
int main()
{
int x=10;
const int y=20;
func(&x);//ok,优先匹配普通指针类型
func(&y);//error,只能匹配常指针类型
return 0;
}
void fun(int* p){}
void fun(const int* s){}
void fun(int* const r){}
int mian()
{
int x=10;
fun(&x);//error,第一个函数和第三个函数无法重载,原因:x的地址为常性无法改变,第一个和第三个函数都可以进行匹配,这样就存在调用的二义性。
return 0;
}
指针使用const 修饰时,如果指针所指之物为常性,则认为与普通指针是不同的参数,可以作为重载的依据;如果指针本身为常性,则认为与普通指针是相同的参数,不能作为重载依据。
1.4C++函数能够重载的原因->名字粉碎(名字修饰)
C或者C++函数在内部(编译和链接)通过修饰名识别。修饰名是编译器在编译函数定义或者原型时生成的字符串。
修饰名由函数名,类名,调用约定,返回类型,参数共同决定。
调用约定:
cdecl(默认方式),C的调用约定按从右到左的顺序压参数入栈(参数的入栈方式),由调用者把参数弹出栈。对于传递参数的内存栈是由调用者来维护的(实现可变参数的函数只能使用该调用约定)。栈的平衡由主函数(调用者)进行维护。关键字为:__cdecl
从右到左的顺序压参数入栈:在调用函数时,从右向左将实参的值赋值给形参。
stdcall(标准方式),回调调用约定是Pascal程序的缺省调用方式,通常用于Win32Api中,函数采用从右到左的压栈方式(参数的入栈方式),自己在退出时清空堆栈。栈的平衡由自己进行维护。关键字为:__stdcall
fastcall(快速方式)特点是快速,因为它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送从左向右的前两个双字(DWORD)或更小的参数,剩下的参数仍然自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈),在函数名修饰约定方面,它和前两者不同。栈的平衡由自己进行维护。关键字为:__fastcall
在函数名前不加关键字均是以默认方式进行的。
thiscall (不能直接修饰,C++默认缺省函数调用约定)仅仅应用于C++类的成员函数。函数的堆栈平衡操作对于参数确定的函数由函数自己执行,不确定参数的函数是由调用函数执行的。__ thiscall 是C++成员函数的默认调用约定,__ thiscall不是关键字,不能进行显示指定。参数是从右向左压栈,而且使用ecx寄存器来传递this指针。(注意:并不是所有的成员函数调用都是通过ecx来实现的,得看具体的编译器)
在C/C++中,一个程序要运行起来,需要经历以下几个阶段: 预编译(预处理)、编译、汇编、链接。名字修饰是一种在编译过程中,将函数名、变量名的名字重新命名的机制。
函数使用cdecl调用约定(默认调用约定)的过程
cdecl,由调用者(该处为主函数)进行栈平衡:
stdcall,自己进行栈平衡:
fastcall,自己进行栈平衡:
参数不多于双字时:没有栈平衡操作,因为只使用了寄存器,没有进行压栈操作,所以不需要栈平衡。
当参数多于余双字时:__fastcall 只给了两个寄存器ECX EDX 用来传参,多余的参数需要进行压栈,也就只需对多余的参数的字节数进行栈平衡即12个字节(0ch)
1.5C语言编译时修饰约定规则:
C语言的名字修饰规则非常简单,__cdecl是C/C++的缺省调用方式,调用约定函数名字前面添加了下划线前缀
stdcali调用约定在输出函数名前加上一个下划线前缀,后面加上一个“@”符号和其参数的字节数
fastcall 调用约定在输出函数名前加上一个”@”符号,函数名后面也是一个 @"符号和其参数的字节数
1.6C++编译时修饰约定规则:
cdecl调用约定:
1、以 ? 标识函数名的开始,后跟函数名;
2、函数名后面以 @@YA 标识参数表的开始,后跟参数表;
3、参数表以代号表示:
X -> void D -> char E ->unsigned char F -> short H -> int I -> unsigned int J -> long K ->unsigned long M -> float N -> double _N -> bool .....
PA 表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以 0 代替,一个 0 代表一次重复;
4、参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前;
5、参数表后以 @z 标识整个名字的结束,如果该函数无参数,则以 Z 标识结束。
//(?printf_int@@YAXH@Z)
void _cdecl printf_int(int a)
{
printf("%d",a);
}
stdcall调用约定:
与cdecl相同,只有第二条不一样:函数名后面以 @@YG 标识参数表的开始
//(?my_add_int@@YGHHH@Z)
int _stdcall my_add_int(int a,int b)
{
return a+b;
}
fastcall调用约定:
与cdecl相同,只有第二条不一样:函数名后面以 @@YI 标识参数表的开始
//(?fun@@YIXXZ)
void _fastcall fun()
{
}
int main()
{
my_add_int(12,23);
printf(12);
fun();
return 0;
}
如果想要在.cpp文件中以C的方式进行名字粉碎:
extern "C"
//在花括号内的函数均以C的方式进行名字粉碎
{
//不加关键字,均以_cdecl(默认方式)调用约定进行名字粉碎
void fun(int a)//_fun
{
}
void print()//_printf
{
}
}
重载的本质
C语言无法进行函数重载的原因:C语言在编译时,只是简单对函数名进行修改,这样就无法进行区分重载的函数。
C++:能够重载
//(?my_max@@YAHHH@Z)
int my_max(int a, int b);
//(?my_max@@YADDD@Z)
char my_max(char a, char b);
//(?my_max@@YANNN@Z)
double my_max(double a, double b);
int main()
{
my_max(12,23);
my_max('a' ,'b') ;
my_max (12.23,34.45);
return 0;
}
但C++能够重载不是以修饰后的名字不同而作为依据的,例如:
//?fun@@YAHH@Z
int fun(int a)
{
}
//?fun@@YADH@Z
char fun(int a)
{
}
int main()
{
fun(12);//error
return 0;
}
很明显两函数修饰后的名字不相同,但是两函数无法进行重载。主要原因在于主函数在调用时无法区分两函数,产生了歧义,所以能够重载在于参数表中参数的类型不同或者个数不同或者顺序不同。
关键字:
extern "C" : 函数名以C的方式修饰约定规则;
extern "C++" :函数名以C++的方式修饰约定规则;
__cdecl
__stdcall
__fastcall
__thiscall
1.7调用约定总结
C语言中的调用约定有三种,默认调用约定(C调用约定)、回调调用约定、快速调用约定,三者参数的压栈方式都是从右向左,快速调用约定可以通过寄存器传递参数,但是只能传递最左边的两个双字的参数,多余的参数仍旧按照从右向左的方式压栈。
对于回调调用约定和快速调用约定是通过自己完成栈的平衡,而C调用约定是由调用者来进行栈平衡,就是调用者知道给该函数多少形参,就清楚栈平衡的字节数,从而进行栈平衡。
C/C++中,没有关键字能影响函数的入参方式,即没有从左向右的压栈方式。(不确定,pascal是从左向右压栈)