C++——函数重载详解

C++知识总结目录索引

1. 什么是重载

  重载,简单说,就是函数或者方法有相同的名称,但是参数列表不相同的情形,这样的同名不同参数的函数或者方法之间,互相称之为重载函数或者方法。

  实际编程当中,有时候我们需要实现几个功能类似的函数,只是有些细节不同。例如Swap函数(用来交换两个变量的值),这两个变量有多种类型,可以是 int、float、char、bool 等,我们需要通过参数把变量的地址传入函数内部。如果是用C语言,我们要设计三个不同的函数,如下:

void Swap1(int *a, int *b);     //交换 int 变量的值
void Swap2(float *a, float *b); //交换 float 变量的值
void Swap3(char *a, char *b);   //交换 char 变量的值

  这样写起来非常麻烦,但在C++中我们完全没必要这么写,C++支持重载,多个函数可以有同一个名字,只要他们的参数列表不同。

参数列表又叫参数签名,包括参数的类型、参数的个数和参数的顺序,只要有一个不同就叫做参数列表不同。


2. 重载的规则

函数的重载必须遵循以下四条规则:

  • 函数名称必须相同。
  • 参数列表必须不同(个数不同、类型不同、参数排列顺序不同,满足其一便可)。
  • 函数的返回类型可以相同也可以不相同。
  • 仅仅返回类型不同不足以成为函数的重载。

3. C++如何实现重载?

  我们知道源代码(.cpp)翻译成可执行程序有四步——预处理、编译、汇编和链接。它们实现的功能是:

  • 预处理:展开头文件、宏替换、去掉注释以及条件编译(#ifndef - #endif)。
  • 编译:检查语法,生成汇编代码。
  • 汇编:汇编代码转换成机器码。
  • 链接:把目标文件和库文件链接成可执行文件。

  C++代码在编译时会根据参数列表对函数进行重命名,下面展示一下在Linux操作系统的g++编译器中函数名的转换。

//交换两个int变量的值
void Swap(int *a, int *b)
{
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

//交换两个float变量的值
void Swap(float *a, float *b)
{
    float tmp = *a;
    *a = *b;
    *b = tmp;
}

int main()
{
    int n1 = 10;
    int n2 = 100;
    Swap(&a, &b);

    float f1 = 1.1;
    float f2 = 2.1;
    Swap(&f1, &f2);

    return 0;
}

  用g++编译上述代码,然后使用objdump -d a.out > log.txt 把它的汇编代码写入到log.txt文件,再用less指令查看。我们来对比一下源代码中的函数名和汇编代码中的函数名:

void Swap(int* a, int* b) –> _Z4SwapPiS
void Swap(float* x, float* y) –> _Z4SwapPfS

通过比较我们推断出汇编代码的含义:作用域+(命名空间)+函数名+参数形式

  在链接阶段,虽然代码中这几个函数名都是相同的,但是在编译系统作用下,这些函数已经根据C++函数名修饰规则重新生成了新的名字。

  编译器会根据传入的实参去逐个匹配,以选择对应的函数,如果匹配失败,编译器就会报错,这叫做重载决议(OverloadResolution)。不同的编译器有不同的重命名方式,这里仅仅举例说明,实际情况可能并非如此。

问:在C++程序中调用被C编译器编译后的函数,为什么要加 extern "C"
答:C++语言支持函数重载,C语言不支持函数重载。函数被C++编译器编译和被C编译器编译后生成的内部名字是不同的。C++提供了C连接交换指定符号 extern "C" 来解决名字匹配问题(即二进制兼容性问题)。

C++之所以支持重载是因为它的函数名修饰规则。


4. 重载函数的调用匹配

  先前说到编译器会根据传入的实参去逐个匹配,以选择对应的函数,那么它是如何匹配的呢?为了匹配到最适合的重载函数,需要依次按照下列规则来判断:

  • 精确匹配:参数匹配而不做转换,或者只是做微不足道的转换,如数组名到指针、函数名到指向函数的指针、T到const T;

  • 提升匹配:即整数提升(如bool 到 int、char到int、short 到int),float到double

  • 使用标准转换匹配:如int 到double、double到int、double到long double、T* 到void*、int到unsigned int;

  • 使用用户自定义匹配;

  • 使用省略号匹配:类似printf中省略号参数

      如果在最高层有多个匹配函数找到,调用将被拒绝(因为有歧义、模凌两可)。看下面的例子:

void func(short);
void func(long);

int main()
{
    int i = 0;
    func(i);

    system("pause");
    return 0;
}

这时候就会报如下错误:

3   IntelliSense:  有多个 重载函数 "func" 实例与参数列表匹配: 
        函数 "func(short)"
        函数 "func(long)"
        参数类型为:  (int)   

错误  2   error C2668: “f2”: 对重载函数的调用不明确

错误  1   error C2668: “f1”: 对重载函数的调用不明确

5. 运算符重载

  在C++中可以用operator关键字加上运算符来表示函数,叫做运算符重载。运算符重载函数形式比较特殊,运算符本身就是函数名。

下面是一个复数相函数
Complex Add(const Complex& n1, const Complex& n2);
使用运算符重定义后:
Complex operator+(const Complex& n1, const Complex& n2);
使用方法:

int main()
{
    Complex a, b, c;
    //...
    c = Add(a, b); //调用普通函数
    c = a + b;     //调用运算符重载函数——“+”

1. 运算符重载规则

运算符 规则
所有一元运算符 建议重载为非静态成员函数
=、()、[]、->、* 只能重载为非静态成员函数
+=、-=、/=、*=、&=、/=、-=、%=、>>=、<<= 建议重载为非静态成员函数
其他所有运算符 建议重载为全局函数

2. 运算符重载和函数重载的区别

  1. 如果重载为成员函数,则 this 对象发起对它的调用。
  2. 如果重载为全局函数,则第一个参数发起对它的调用。
  3. 除了函数调用运算符“()”外,其他运算符重载函数不能有默认参数值。

3. 不能重载的运算符

  • 不能重载.
  • 不能重载反引用类成员指针.*
  • 不能重载作用域解析运算符::
  • 不能重载唯一那个三元运算符——条件运算符? :
  • 不能重载sizeof()typeid()
  • 不能重载###等预处理操作符 。

4. 重载 ++--

  在C++标准中规定:当为一个类型重载++--的前置版本时,不需要参数;当为一个类型重载++--的后置版本时,需要一个int类型的参数作为标志(即哑元,非具名参数),加这个int形参唯一的作用就是和前置版本构成重载。

class AA
{
public:
    AA(int aa1, int aa2);  //构造函数
    AA(const AA& aa);      //拷贝构造函数

    AA& operator++();      //前置++
    AA operator++(int);    //后置++,形参列表不同才能构成重载

    void Show();

private:
    int _a1;
    int _a2;
};

//重载实现
AA& AA::operator++() //前置
{
    _a1 += 1;
    _a2 += 1;

    return *this;
}

AA AA::operator++(int) //后置
{
    AA ret = AA(*this);
    _a1 += 1;
    _a2 += 1;
    return ret;
}

//调用
int main()
{
    AA aa(10, 11);
    aa.operator++();   //调用前置++
    aa.operator++(0);  //调用后置++
    aa.operator++(int());  //也可以这样写,调用后置++

    system("pause");
    return 0;
}

  我们比较上面代码的前置++和后置++实现,前置++实现只要对成员++,然后直接返回就可了;但是后置++不同,后置++必须先保存原来的值,再进行++运算,最后返回,别小看这两者之间只差了一个创建变量,当你类成员非常大的时候,效率上的差距就出来了。

  如果仔细看的话,你会发现前置++的返回值是引用,后置++返回的是值,返回值是要创建一个临时变量的,这样效率差距就更大了,所以如果可以,我们尽量使用前置++


6. 输入输出运算符的重载

  这部分看书时很少提到,提到了也都说输入输出运算符是不可以重载成成员函数的(都是用友元实现的),下面我们试着实现一下。

class AA
{
public: 
    //...
    ostream& operator<<(ostream& os); //输入输出运算符的重载
    istream& operator>>(istream& is);

private:
    int _a1;
    int _a2;
};

//类外实现
ostream& AA::operator<<(ostream& os)
{
    os << _a1 << endl;
    os << _a2 << endl;

    return os;
}
istream& AA::operator>>(istream& is)
{
    is >> _a1;
    is >> _a2;

    return is;
}

int main()
{
    AA bb(1, 1);
    cout << bb; //(1)
    bb << cout; //(2)

    system("pause");
    return 0;
}

  调用输入输出的时候我们发现按照常规写法 (1) ,程序会报错,报错的信息也很燃:错误 1 error C2679: 二进制“<<”: 没有找到接受“AA”类型的右操作数的运算符(或没有可接受的转换)。但当我们写成 (2) 这种形式时,程序是没问题的,可以正常运行。出现这种问题是因为this指针

  我们设计的函数是这样的ostream& operator<<(ostream& os);,但实际上是这样的ostream& operator<<(AA* this, ostream& os); this 指针已经强占了第一个形参的位置,但我们输入的是cout << bb,这样传参就弄反了,所以要写成bb << cout才能正确调用。

  虽然上述代码我们已经基本实现输入输出运算符的重载了,但是这只是一个阉割版本,毕竟bb << cout这种写法是没人认可的,最重要的是这种写法没法实现连续输出的。所以想要完美实现还是得使用友元函数来实现。


你可能感兴趣的:(C++知识总结,重载,函数名修饰规则)