在C++的继承体系里,子类对父类成员的重新定义会分为覆盖和隐藏两种情况,两者的行为逻辑完全不同,很多开发者在使用继承时经常因为混淆两者导致程序运行结果不符合预期。下面先通过具体的代码示例来直观感受两者的差异。
什么是覆盖(Override)
覆盖是指子类重新定义父类中声明为virtual的虚函数,要求子类函数的函数名、参数列表、返回值类型都和父类虚函数完全一致,此时子类的函数会覆盖父类的虚函数实现,当通过父类指针或引用调用该函数时,会根据对象的实际类型动态调用子类的实现,也就是运行时多态的体现。
覆盖的代码示例
#include <iostream>
using namespace std;
// 父类
class Base {
public:
// 声明为虚函数
virtual void show() {
cout << "Base show" << endl;
}
// 虚析构函数,保证删除子类对象时正确调用析构函数
virtual ~Base() {}
};
// 子类
class Derived : public Base {
public:
// 覆盖父类的show虚函数
void show() override { // override关键字是C++11引入的,用于显式标记覆盖,避免写错函数签名
cout << "Derived show" << endl;
}
};
int main() {
Base* basePtr = new Derived();
basePtr->show(); // 调用的是子类的show,输出Derived show
delete basePtr;
return 0;
}
什么是隐藏(Hiding)
隐藏指的是子类中定义了和父类同名的成员(可以是函数也可以是变量),此时子类的同名成员会遮蔽父类的同名成员,不管父类的成员是否为虚函数,只要名字相同就会触发隐藏。如果通过子类对象直接调用该成员,只会访问到子类的版本,父类的版本会被隐藏,除非显式指定父类作用域。
隐藏的代码示例
#include <iostream>
using namespace std;
class Base {
public:
void print() {
cout << "Base print" << endl;
}
// 重载的print函数,带参数
void print(int num) {
cout << "Base print num: " << num << endl;
}
int value = 10;
};
class Derived : public Base {
public:
// 隐藏父类的print函数,即使参数不同也会隐藏所有父类同名函数
void print() {
cout << "Derived print" << endl;
}
int value = 20;
};
int main() {
Derived d;
d.print(); // 调用子类的print,输出Derived print
// d.print(10); // 编译错误,父类的带参print被隐藏了
d.Base::print(); // 显式指定父类作用域,调用父类的无参print,输出Base print
d.Base::print(10); // 显式指定父类作用域,调用父类的带参print,输出Base print num: 10
cout << d.value << endl; // 输出子类的value,20
cout << d.Base::value << endl; // 显式指定父类作用域,输出父类的value,10
return 0;
}
覆盖与隐藏的核心区别
我们可以从以下几个维度来区分覆盖和隐藏:
- 触发条件不同:覆盖要求父类函数必须是虚函数,且子类函数的函数名、参数列表、返回值类型完全一致;隐藏只需要子类和父类的成员同名即可,和是否为虚函数、参数是否相同无关。
- 调用逻辑不同:覆盖通过父类指针或引用调用时,会根据对象实际类型动态绑定到子类实现;隐藏无论通过什么方式调用,只要不显式指定父类作用域,都只会访问子类的版本。
- 作用不同:覆盖是实现运行时多态的核心机制,用于让不同子类对同一行为有不同的实现;隐藏只是子类成员对父类同名成员的遮蔽,没有多态特性。
快速区分两者的方法
在实际开发中可以通过以下方式快速判断是覆盖还是隐藏:
- 先看父类的同名函数是否声明了
virtual关键字,如果没有,那子类的定义一定是隐藏。 - 如果父类函数是虚函数,再看子类函数的函数名、参数列表、返回值是否和父类完全一致,若一致则为覆盖,否则为隐藏。
- 尝试通过父类指针指向子类对象,调用该成员,如果调用的是子类实现则是覆盖,否则是隐藏。
另外C++11引入了override关键字,在子类函数声明后加上该关键字,编译器会检查是否真的覆盖了父类的虚函数,如果没有覆盖会直接报错,能有效避免把覆盖写成隐藏的问题。