【C++学习】函数重载
文章目录
对比于 C 语言的函数,C++增加了 重载(overloaded)、 内联(inline)、 const 和 virtual 四种新机制。其中重载和内联机制既可用于全局函数也可用于类的成员函数,
const
与
virtual
机制仅用于类的成员函数。
函数重载
在C++中可以为两个或两个以上的函数提供相同的函数名称,只要参数类型不同,或参数类型相同而参数的个数不同,称为函数重载。
重载函数的意义在于减少了函数名的数量,避免了名字空间的污染,提高了程序的可读性。
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(char a,char b){ return a >b?a:b}
int main()
{
int ix=my_max(12,23);
double dx=my_max(12.23,34.45);
char chx=my_max(‘a’,'b');
return 0;
}
这些函数都执行了相同的一般性动作——都返回两个形参中的最大值;从用户的角度来看,只有一种操作,就是判断最大值。
这种词汇上的复杂性不是”判断参数中的最大值“问题本身固有的,而是反映了程序设计环境的一种局限性:在同一个域中出现的名字必须指向一个唯实体(函数体)。
函数重载把程序员从这种词汇复杂性中解放出来。
判断函数重载的规则
(1)如果两个函数的参数表相同,但是返回类型不同, 会被标记为编译错误:函数的重复声明。
(2)参数表的比较过程与形参名无关。
(3)如果在两个函数的参数表中,只有缺省实参不同,则第二个被视为第一个的重复声明。
(4)typedef
为现有的数据类型提供了一个替换名,它并没有创建一个新类型 ,因此如果两个函数参数表的区别只在于一个使用了 typedef
,而另一个使用了与typedef
相应的类型。则该参数表被视为相同的参数列表。
(5)当一个形参类型有const
或volatile
修饰时,如果形参是按值传递方式定义,在识别函数声明是否相同时,并不考虑const
和volatile
修饰符。
当一个形参类型有const
或volatile修饰时,如果形参定义指针或引用时,在识别函数声明是否相同时,就要考虑const
和volatile
修饰符。
(6)如果在两个函数的参数表中,形参类型相同,而形参个数不同,形参默认值将会影响函数的重载。(二义性)
函数重载的解析步骤
(1)确定函数调用考虑的重载函数的集合,确定函数调用中实参表的属性。
(2)从重载函数集合中选择函数,该函数可以在(给出实参个数和类型)的情况下可以调用函数。
(3)选择与调用最匹配的函数。
C++ 如何做到函数重载
C++代码在编译时会根据参数列表对函数进行重命名,当发生函数调用时,编译器会根据传入的实参去逐个匹配,以选择对应的函数,如果匹配失败,编译器就会报错。
名字粉碎(名字修饰)
在C/C++中,一个程序要运行起来,需要经历以下几个阶段:预编译(预处理)、编译、汇编、链 接。Name Mangling是一种在编译过程中,将函数名、变量名的名字重新命名的机制。
“C"或者"C++”函数在内部(编译和链接)通过修饰名识别。修饰名是编译器在编译函数定义或者原型时生成的字符串
修饰名由函数名、类名、调用约定、返回类型、参数等共同决定。
C++编译时函数修饰约定规则
_cdecl
调用约定:
(1)以”?”标识函数名的开始,后跟函数名;
(2)函数名后面以"@@YA"标识参数表的开始,后跟参数表;
(3)参数表以代号表示:
X——void,
D——char,
E——unsigned char,
F——short,
H——int,
|——unsigned int,
J——long,
K—— unsigned long,
M ——float,
N——double,
_N——bool,
PA——表示指针,后面的代号表明指针类型,如果相同类型的指针连续出现,以”0”代 替,一个”0”代表一次重复。
(4)参数表的第一项为该函数的返回值类型,其后依次为参数的数据类型,指针标识在其所指数据类型前。
(5)参数表后以"@Z”标识整个名字的结束,如果该函数无参数,则以"Z"标识结束。
extern关键字
extern放在变量和函数声明之前,表示该变量或者函数在别的文件中已经定义,提示编译器在编译时要从别的文件中寻找。
extern ”C“:函数名以C的方式修饰约定规则
extern ”C++“:函数名以C++的方式修饰约定规则
如果 C++程序要调用已经被编译后的 C 函数,该怎么办?
假设某个 C 函数的声明如下:
void foo(int x, int y);
该函数被 C 编译器编译后在库中的名字为_foo,而 C++编译器则会产生像_foo_int_int之类的名字用来支持函数重载和类型安全连接。由于编译后的名字不同,C++程序不能直接调用 C 函数。C++提供了一个 C 连接交换指定符号 extern“C”来解决这个问题。
例如:
extern “C”
{
void foo(int x, int y);
… // 其它函数
}
或者写成
extern “C”
{
#include “myheader.h”
… // 其它 C 头文件
}
这就告诉 C++编译译器,函数 foo 是个 C 连接,应该到库中找名字_foo 而不是找_foo_int_int。C++编译器开发商已经对 C 标准库的头文件作了 extern“C”处理,所以我们可以用#include 直接引用这些头文件
作用:
(1)声明外部变量
在声明全局变量时,不同的文件在编译器编译时是不透明的,比如在A.c中定义int i,同时在B.c中定义int i,编译器编译时是不会报错的,但是当链接linking时会报错重复定义。当需要使用同一全局变量时,如:在A.c中定义了int i,在B.c中需要调用i,只需要在B.c中声明extern int i,表示该变量在别的文件中已经定义,编译时便不会出错,在linking…的时候会自动去查找定义过的变量i。
(2)extern函数声明
extern void fun()暗示该函数可能在别的文件中定义过,它和定义为void fun(),没什么区别,其用处在于复杂的项目用通过在函数前添加extern声明来取代用include" *.h"来声明函数。
(3)单方面修改函数原型
当声明extern void fun(int i,int j,int k)时,在之后的调用中如果按照是fun(x,y,z)的原型调用时是没有问题的,但是如果要对该函数进行修改比如输入参数,调用时为fun(x,y)此时编译器就会报错了,解决办法就是去掉extern,该头文件中声明void fun(int i,int j),并对该函数进行修改,之后在调用的文件中包含该函数所在的头文件" *.h"即可。
补充:
并不是两个函数的名字相同就能构成重载。全局函数和类的成员函数同名不算重载,因为函数的作用域不同。
void Print(…); // 全局函数
class A
{…
void Print(…); // 成员函数
}
::Print(…); // 表示 Print 是全局函数而非成员函数
//如果类的某个成员函数要调用全局函数
Print,为了与成员函数 Print 区别,全局函数被调用时应加‘::’标志
隐式类型转换导致重载函数产生二义性
#include <iostream.h>
void output( int x); // 函数声明
void output( float x); // 函数声明
void output( int x)
{
cout << " output int " << x << endl ;
}
void output( float x)
{
cout << " output float " << x << endl ;
}
void main(void)
{
int x = 1;
float y = 1.0;
output(x); // output int 1
output(y); // output float 1
output(1); // output int 1
// output(0.5); // error! ambiguous call, 因为自动类型转换
output(int(0.5)); // output int 0
output(float(0.5)); // output float 0.5
}
由于数字本身没有类型,将数字当作参数时将自动进行类型转换(称为隐式类型转换)。语句 output(0.5)将产生编译错误,因为编译器不知道该将 0.5 转换成 int 还是 float 类型的参数。
成员函数的重载、覆盖与隐藏
重载与覆盖
成员函数被重载的特征:
- 相同的范围(在同一个类中);
- 函数名字相同;
- 参数不同;
- virtual 关键字可有可无。
覆盖是指派生类函数覆盖基类函数,特征是:
- 不同的范围(分别位于派生类与基类);
- 函数名字相同;
- 参数相同;
- 基类函数必须有 virtual 关键字。
//函数 Base::f(int)与 Base::f(float)相互重载,而 Base::g(void)被 Derived::g(void)覆盖。
#include <iostream.h>
class Base
{
public:
void f(int x){ cout << "Base::f(int) " << x << endl; }
void f(float x){ cout << "Base::f(float) " << x << endl; }
virtual void g(void){ cout << "Base::g(void)" << endl;}
};
class Derived : public Base
{
public:
virtual void g(void){ cout << "Derived::g(void)" << endl;}
};
void main(void)
{
Derived d;
Base *pb = &d;
pb->f(42); // Base::f(int) 42
pb->f(3.14f); // Base::f(float) 3.14
pb->g(); // Derived::g(void)
}
隐藏
“隐藏”是指派生类的函数屏蔽了与其同名的基类函数。
- 如果派生类的函数与基类的函数同名,但是参数不同。此时,不论有无 virtual 关键字,基类的函数将被隐藏(注意别与重载混淆)。
- 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有 virtual 关键字。此时,基类的函数被隐藏(注意别与覆盖混淆)。
#include <iostream.h>
class Base
{
public:
virtual void f(float x){ cout << "Base::f(float) " << x << endl; }
void g(float x){ cout << "Base::g(float) " << x << endl; }
void h(float x){ cout << "Base::h(float) " << x << endl; }
};
class Derived : public Base
{
public:
virtual void f(float x){ cout << "Derived::f(float) " << x << endl; }
void g(int x){ cout << "Derived::g(int) " << x << endl; }
void h(float x){ cout << "Derived::h(float) " << x << endl; }
};
函数 Derived::f(float)覆盖了 Base::f(float)。 有virtual
函数 Derived::g(int)隐藏了 Base::g(float),而不是重载。
函数 Derived::h(float)隐藏了 Base::h(float),而不是覆盖。无virtual
隐藏作用
- 写语句 pd->f(10)的人可能真的想调用 Derived::f(char *)函数,只是他误将参数写错了。有了隐藏规则,编译器就可以明确指出错误。否则,编译器会静悄悄地将错就错,程序员将很难发现这个错误,流下祸根。
- 假如类 Derived 有多个基类(多重继承),有时搞不清楚哪些基类定义了函数 f。如果没有隐藏规则,那么 pd->f(10)可能会调用一个出乎意料的基类函数 f。尽管隐藏规则看起来不怎么有道理,但它的确能消灭这些意外。
class Base
{
public:
void f(int x);
};
class Derived : public Base
{
public:
void f(char *str);
};
语句 pd->f(10)的本意是想调用函数 Base::f(int),但是 Base::f(int)不幸被 Derived::f(char *)隐藏了。由于数字 10不能被隐式地转化为字符串,所以在编译时出错。
void Test(void)
{
Derived *pd = new Derived;
pd->f(10); // error
}
参数的缺省值
有一些参数的值在每次函数调用时都相同,书写这样的语句会使人厌烦。C++语言采用参数的缺省值使书写变得简洁(在编译时,缺省值由编译器自动插入)。
-
参数缺省值只能出现在函数的声明中,而不能出现在定义体中。
void Foo(int x=0, int y=0); // 正确,缺省值出现在函数的声明中 void Foo(int x=0, int y=0)// 错误,缺省值出现在函数的定义体中 { … } //函数的实现(定义)本来就与参数是否有缺省值无关,所以没有必要让缺省值出现在函数的定义体中。 //参数的缺省值可能会改动,显然修改函数的声明比修改函数的定义要方便。
使用参数的缺省值并没有赋予函数新的功能,仅仅是使书写变得简洁一些。 它可能会提高函数的易用性,但是也可能会降低函数的可理解性。所以我们只能适当地 使用参数的缺省值,要防止使用不当产生负面效果。