C++中的重载和覆盖,还有(隐藏)

本文详细介绍了C++中的函数重载(overload)、覆盖(override)以及所谓的“隐藏”概念。重载允许在同一个作用域内函数名相同但形参列表不同,而覆盖是在继承中子类函数对基类同名虚函数的重新定义。文中通过实例阐述了如何判断重载、覆盖,并澄清了“隐藏”并非C++的正式规则。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

前言

重载(overload)和覆盖(override)是C++中关于函数的两个基础概念,但是如果让你说出他们具体的描述和区别,一下子还真是不太容易说的很清楚和全面,这里简单把记录一下,作为备忘。关于隐藏我觉得是个误解,C++中根本没有隐藏的说法和规则。

重载

重载(overload)是指同一个作用域内几个函数名字相同,但形参列表不同(包括参数个数、类型、参数顺序)。注意:函数返回值不是作为区分重载函数的标志。

定义重载函数

定义以下几个函数:

void print(const char *cp);
void print(const int *beg, const int *end);
void print(const int ia[], size_t size);

这些函数的名字一样,形参个数和类型不一样。当调用这些函数时,编译器会根据传递的实参类型推断调用的是哪个函数:

int ary[2] = {0, 1};
print("Hello world");           // 调用print(const char *)
print(j, end(ary) - begin(ary));// 调用print(const int, size_t)
print(begin(ary), end(ary));    // 调用print(const int *, const int *)

两个函数除了返回类型不同,其他所有要素都相同的话,第二个函数的声明是错误的,例如:

void print(const char *cp);
bool print(const char *cp); // 错误
判断两个形参的类型是否相异

有些函数的形参列表看起来不一样,实际上是一样的:

Record lookup(const Account &acct);
Record lookup(const Account&);

typedef Phone Telno;
Record lookup(const Phone&);
Record lookup(const Telno&);

第一对声明中,在函数的声明中形参没有实质的含义,有没有它不会影响形参列表的内容。
第二对声明,typedef是已存在的类型提供了另外一个名字,并不创建新的类型,因此,第二对中两个函数本质上没有不同。

重载和const形参
顶层const(top-level const) 底层const(low-level const)
-- 顶层const和底层const的概念参见《C++ Primer》第五版 2.4.3小节

顶层const不影响传入函数的对象。一个拥有顶层const的形参无法和另一个没有顶层const的形参区分开来。

Record lookup(Phone);
Record lookup(const Phone);  // 重复声明

Record lookup(Phone*);
Record lookup(Phone* const); // 重复声明

上面两组函数声明,每一组的两个都是等价的。

Record lookup(Accout&);
Record lookup(const Accout&);  // 新函数

Record lookup(Accout&*);
Record lookup(const Accout&); // 新函数

上面两组例子中,编译器可以通过实参是否是常量来推断应该调用哪个函数。因为const不能转换成其他类型,所以我们只能把const对象(或指向const的指针)传递给const形参。相反,非常量可以转换成const,所以上面的4个函数都能作用于非常量对象或者指向非常量对象的指针。当我们传递一个非常量对象或者非常量对象的指针时,编译器会优先选用非常量版本的函数。

cosnt_cast和重载

const_cast可以用在重载函数中。例如下面这个函数:

const string &shorterString(const string &s1, const string &s2)
{
    return s1.size() <= s2.size() ? s1 : s2;
}

这个函数的参数和返回类型都是const string的引用。我们可以对两个非常量的string实参调用这个函数,但返回的结果仍然是const string的引用。因此我们需要一种新的shorterString函数,当它 的实参不是常量时,得到的结果是一个普通的引用,使用const_cast可以做到这一点:

string &shorterString(string &s1, string &s2)
{
    auto &r = shorterString(const_cast<const string&>(s1),
                            const_cast<const string&>(s2));
    return const_cast<string&>(r);
}

这里使用两次const_cast转换,使用第一个shorterString函数定义了一个非常量版本的shorterString。

调用重载的函数

重载函数调用在编译的时候会有函数匹配(function matching)的一个过程,在这个过程中把特定的函数调用与一组重载函数中的某一个关联起来,函数匹配也叫做重载确定(overload resolution)。编译器会将调用的实参和重载函数集合中每一个函数的形参作比较,然后根据比较的结果决定到底调用哪个函数。

重载函数有三种可能的结果:
- 编译器找到一个与实参最佳匹配的函数,并生成调用该函数的代码。
- 找不到任何一个函数与调用的实参匹配,编译器发出无匹配的错误信息。
- 有多于一个函数可以匹配,但是每一个都不是明显的最佳选择,此时也将发生错误,成为二义性调用。

重载和作用域

我们知道,在同一个作用域中,内层作用域的声明会覆盖外层声明,对于重载也是一样的。如果我们在内层作用域中声明名字,它将隐藏外层作用域中声明个同名实体。在不同的作用域中无法重载函数名:

string read();
void print(const string &);
void print(double);
void fooBar(int ival)
{
    bool read = false; // 新作用域:隐藏了外层的read
    string s = read(); // 错误:read是一个布尔值,而非函数
    // 不好的习惯:通常来说,在局部作用域中声明函数不是一个好的选择
    void print(int);   // 新作用域:隐藏了之前的print
    print("value");    // 错误:print(const string&)被隐藏掉了
    print(ival);       // 正确:当前print可见
    print(3.14);       // 正确:调用print(int); print(double)被隐藏掉了
}

调用print函数时,编译器首先寻找该函数名的声明,找到的是print(int)这个局部声明,一旦在当前作用域中找到了所需的名字,编译器就会忽略掉外层作用域的同名实体,所以print(“value”)这个调用是错误的。调用print(3.14)也是同样的,double型转换成int型,打印出整数值3。

我们把print(int)的声明放在调用函数的外部,print(int)就成为了重载函数:

void print(const string &);
void print(double);
void print(int);
void fooBar(int ival)
{
    print("value");    // 调用 print(const string&)
    print(ival);       // 调用 print(int)
    print(3.14);       // 调用 print(double)

覆盖

覆盖(override)又称为重写,是指派生类中存在重新定义的函数,其函数名,参数列表,返回值类型,和基类中被重写的函数一致。只有函数体不同(花括号内),派生类调用时会调用派生类的重写函数,不会调用被重写函数。基类中被重写的函数必须有virtual修饰。

#include <iostream>

class Base {
public:
    virtual void foo() { std::cout << "Base class" << std::endl; }
};

class Derived : public Base {
public:
    void foo() override { std::cout << "Derived class" << std::endl; }
};

int main() {
    Base b;
    b.foo();      // ①
    Derived d;
    d.foo();      // ②
    Base &b1 = d;
    d.foo();      // ③
    Base *pb = &d;
    pb->foo();    // ④
    pb = &b;
    pb->foo();    // ⑤
}

输出:

Base classDerived classDerived classDerived classBase class

派生类中foo函数后面的override说明该函数重写了基类中的相同函数。示例程序中同时简单演示了虚函数实现的多态。

隐藏

这个没什么好说的。有人说是派生类中定义了和基类名字相同的函数,就把派生类中的函数隐藏了,不考虑形参类型、形参个数和返回值。这个……我表示无法理解。

#include <iostream>

class Base {
public:
    void foo() { std::cout << "Base class" << std::endl; }
    int sum(int a, int b) { foo(); return a + b; }
};

class Derived : public Base {
public:
    void foo() { std::cout << "Derived class" << std::endl; }
    int sum(int b, int a) { foo(); return 1 + b + a;}
};

int main() {
    Base b;
    std::cout << b.sum(1, 2) << std::endl;
    Derived d;
    std::cout << d.sum(1, 2) << std::endl;
    Base &b1 = d;
    std::cout << b1.sum(1, 2) << std::endl;  // ③
    Base *pb = &d;
    std::cout << pb->sum(1, 2) << std::endl; // ④
    pb = &b;
    std::cout << pb->sum(1, 2) << std::endl;
}

上面这段代码的执行结果如下:

Base class
3
Derived class
4
Base class
3
Base class
3
Base class
3

从执行的结果来看,因为是普通成员函数,③和④两处语句的输出和虚函数表现的多态性输出是不一样的,这里没有表现出隐藏,把派生类sum函数的形参改成1个也不改变函数的匹配结果。

C++中没有隐藏的相关规则,不知道为什么把隐藏和重载和覆盖放在一起说,以讹传讹吧。

重载和覆盖的区别

简单总结一下两者的区别:

  • (1)作用域区别:重写和被重写的函数在具有继承关系的类中,重载和被重载的函数在同一作用域中。

  • (2)参数区别:重写与被重写的函数参数列表一定相同,重载和被重载的函数参数列表一定不同。

  • (3)virtual的区别:重写的基类必须要有virtual修饰,重载函数和被重载函数可以被virtual修饰,也可以没有。

参考资料:
C++ Primer 第五版
重载、重写(覆盖)和隐藏的区别

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值