【C++学习】函数重载

【C++学习】函数重载


对比于 C 语言的函数,C++增加了 重载(overloaded)内联(inline)constvirtual 四种新机制。其中重载和内联机制既可用于全局函数也可用于类的成员函数, constvirtual 机制仅用于类的成员函数。

函数重载

在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)当一个形参类型有constvolatile修饰时,如果形参是按值传递方式定义,在识别函数声明是否相同时,并不考虑constvolatile修饰符。

当一个形参类型有const或volatile修饰时,如果形参定义指针或引用时,在识别函数声明是否相同时,就要考虑constvolatile修饰符。

(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)// 错误,缺省值出现在函数的定义体中 
    {}
    
    //函数的实现(定义)本来就与参数是否有缺省值无关,所以没有必要让缺省值出现在函数的定义体中。
    //参数的缺省值可能会改动,显然修改函数的声明比修改函数的定义要方便。
    

使用参数的缺省值并没有赋予函数新的功能,仅仅是使书写变得简洁一些。 它可能会提高函数的易用性,但是也可能会降低函数的可理解性。所以我们只能适当地 使用参数的缺省值,要防止使用不当产生负面效果。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值