一.函数重载
C语言实现int,double,char类型的比较大小函数。
int my_max_i(int a,int b){return a>b?a:b;}
double my_max_d(double a,double b){return a>b?a:b;}
char my_max_c(double a,double b){return a>b?a:b;}
这些函数都执行了相同的动作,返回两个形参中的最大值;从用户的角度来看,只有一种操作,就是判断最大值。
这种词汇上的复杂性不是“判断参数最大值”问题固有的,而是反映了程序设计环境 的一种局限性:在同一个域中出现的名字必须指向一个唯实体(函数体)。
这种复杂性给程序员带来了一个实际问题,他们必须记住每一个函数的名字。
函数重载把程序员从这种词汇的复杂性中解放出来。
1.1函数重载的概念
在C++中可以为两个或两个以上的函数提供相同的函数名称,只要参数类型不同,或者参数类型相同而参数的个数不同,称为函数重载。(函数名相同)
int max(int a,int b)
{
return a>b?a:b;
}
double max(double a,double b)
{
return a>b?a:b;
}
char max(char a,char b)
{
return a>b?a:b;
}
//每个同名函数的参数表是唯一
int main()
{
cout<<max(12,23)<<endl;
cout<<max('a','b')<<endl;//''是 字符的定界符""是字符串的定界符
cout<<max(12.23,34.45)<<endl;
}
如果两个函数的参数表中参数的个数或类型或顺序不同,则认为这两个函数是重载。
void print(int a,char b);
void print(char a,int b);
函数原型:返回值,函数名,形参表。
这两个从函数原型上是不同的。
1.2判断函数重载的规则
1.2.1函数重载不用返回值作为重载的依据
如果两个函数的参数表相同,但是返回类型不同,会被标记为编译错误:函数的重复声明。
int Max(int a,int b)
{
return a>b?a:b;
}
char Max(int a,int b)//error
{
return a>b?a:b;
}
1.2.2参数表的比较过程与形参名无关
//同一个函数
int add(int a,int b);
int add(int x,int y);
1.2.3如果在两个函数的 参数表中,只有缺省实参不同,则第二个声明被视为第一个重复声明
void print(int *br,int n);
void print(int *br,int len=10);
这两个是同一个函数
1.2.4typedef
用typedef做了替换名,但并没有创建一个新类型,也是相同的函数。
typedef unsigned int u_int;
int print(u_int a);
int print(unsigned int b);
1.2.5const volatile(多线程)
int func(int a)
{}
int func(const int b)
{}
int main()
{
func(12);
}
这种情况下会出现二义性的问题,所以在这种情况下,形参表中的const并不作为区分的标志。
但是常性左值引用的左值引用可以最为区分的标志。
int func(int &a)
{}
int func(const int &b)
{}
int func(int &&c)
{}
int main()
{
func(12);
}
同理,在左值引用,右值引用和右值常引用之间,如果是一个左值,优先关联左值引用,左值常引用退而求其次,右值同理。
void func(int *ap)
{}
void func(const int *bp)
{}
int main()
{
int a=10;
func(&a);
}
在这种情况下,指针中的const也作为函数重载的标志。
void func(int *ap)
{}
void func(int * const bp)
{}
int main()
{
int a=10;
func(&a);
}
但当const是修饰指针自身的时候,忽略const,这个还是有二义性的。
1.2.6注意函数调用的二义性
如果两个函数的参数表中,形参类型相同,而个数不同,形参默认值将会影响函数重载。
void func(int a){};
void func(int a,int b=10){};
int main()
{
func(12);
func(12,23);
}
这样就会出现二义性的问题。
二、名字粉碎(名字修饰)
C或者C++函数在 内部(编译和链接)通过修饰名 识别。修饰名是编译器在编译函数定义或者原型时生成的字符串。
修饰名由函数名,类名,调用约定,返回值类型,参数表共同决定。
调用约定:
__stdcall是Pascal程序的缺省调用方式,通常用于Win32 Api中,函数采用从右到左的压栈方式,自己在退出时清空堆栈。
什么是从右到左的压栈方式呢?
int Add(int x,int y)
{
int z=x+y;
return z;
}
int main()
{
int a=10,b=20;
int c=0;
c=Add(a,b);
printf("c=%d",c);
return 0;
}
在调用函数的时候,是先用b对y赋值再用a对x赋值
EIP指令地址寄存器,就是关联到代码执行的位置。
C调用约定(即用__cddecl关键字说明)按照从右至左的顺序压参数入栈,由调用者把参数弹出栈。对于传送参数的内存栈是由调用者来维护的(正因为如此,实现可变参数的函数只能使用该调用约定)。
__fastcall调用约定,它的主要特点就是快,因为它是通过寄存器来传送参数的(实际上,它用ECX和EDX传送前两个双字(DWORD)或更小的参数,剩下的参数仍自右向左压栈传送,被调用的函数在返回前清理传送参数的内存栈),在函数名修饰约定方面,它和两者均不同。
thiscall仅仅应用于“C++”类的成员函数。this指针存放于ECX寄存器,参数从右向左压。thiscall不是关键词,因此不能被程序员指定。
在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预编译(预处理),编译,汇编,链接。
2.1C语言编译时函数名修饰约定规则
C语言的名字修饰规则非常简单,_cdecl是C/C++的缺省调用方式,调用约定函数名字前面添加了下划线前缀。
格式:_functioname
_stdcall调用约定在输出函数名前面加上一个 下划线前缀,后面加上 一个“@”符号 和其参数的字节数。
格式:_functioname@number
__fastcall调用约定在输出函数名前面加上“@”符号,函数后面也是一个“@”符号和其参数的字节数。
格式:@func@number
2.2C++编译时函数修饰约定规则
PA——表示指针,后面的代号表明指针类型,如果相同 类型的指针连续出现,以“0”代替,一个“0”代表重复一次。
4、参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前。
5、参数表后以“@Z”标识整个名字的结束,如果该函数无参数,则以“Z”标识结束。
所以在这种情况下,返回值不能作为重载的依据,因为在底层区别不出来
这也是c++可以进行函数重载但是C语言不行的原因。因为名字粉碎技术不同。所以在编译过程中C++可以区分出来不同而C语言不可以。
外部关键字的用法:
extern其实就是一个声明,明确指出一个语句是声明,比如extern int i
;这是声明变量i,而不是定义i,定义i则是int i
(声明不开辟内存空间,而定义是要开辟内存空间的)
两者 extern 和 extern"C"有点区别:
extren 在C和C++中,都表示声明语句的意思;
而 extern"C"是按C语言方式去编译文件;
extern"C"只能在C++文件使用,extern在C和C++都可以使用
在 函数前面加上
extern"C":函数名以C的方式进行修饰约定规则
extern"C++":函数名以C++的方式进行修饰约定规则
三、函数模板
为了代码重用,代码就必须是通用的;通用的代码就必须不受数据类型的限制。那么我们可以把数据类型改为一个设计参数。这种类型的程序设计称为参数化程序设计。
软件模块由模板(template)构造。包括函数模板和类模板。
函数模板可以用来创造一个通用功能的函数。以支持多种不同形参,简化重载函数的设计。、
函数模板的定义如下:
template<模板参数表>
返回类型 函数名(形式参数表)
{
...;//函数体
}
当编写了这样一个函数时,在编译过程中会变成这样:
实质上还是函数的重载,只不过是编译器自己生成。
函数模板根据一组实际类型或值构造出独立的函数的过程通常是隐式发生的,称为模板实参推演。
函数模板的重载(特化)