重写(override)、重载(overload)和隐藏(overwrite)在C++中是3个完全不同的概念。我们这里对其进行详细的说明
1、重写(override)是指派生类覆盖了基类的虚函数,这里的覆盖必须满足有相同的函数签名和返回类型,也就是说有相同的函数名、形参列表以及返回类型。
2、重载(overload)是指C++允许在同一作用域中声明几个功能类似的同名函数,这些函数的函数名相同,但是函数签名不同,也就是说有不同的形参。
3、隐藏(overwrite)是指基类成员函数,无论它是否为虚函数,当派生类出现同名函数时,如果派生类函数签名不同于基类函数,则基类函数会被隐藏。如果派生类函数签名与基类函数相同,则需要确定基类函数是否为虚函数,如果是虚函数,则这里的概念就是重写;否则基类函数也会被隐藏。另外,如果还想使用基类函数,可以使用
using
关键字将其引入派生类。
一、重写
函数重写的基本原则:在基类中,通过使用关键字 virtual 来声明一个虚函数,派生类可以通过重新定义基类中的虚函数来实现函数重写。
例如:
cpp
class Father {
public:
virtual void func() {
cout << "Father" << endl;
}
};
class Child: public Father {
public:
void func() {
cout << "Child" << endl;
}
};
int main() {
Father f;
Child c;
f.func();
c.func();
}
以上代码实现了子类重写父类中的函数。所以父类子类调用同一个函数会有不同的实现。仿真如下:
1.1、重写引发的问题
重写虚函数很容易出现错误,原因是C++语法对重写的要求很高,稍不注意就会无法重写基类虚函数。且这些错误不易被发现,编译器可能也不会提示:
cpp
class Base {
public:
virtual void some_func() {}
virtual void foo(int x) {}
virtual void bar() const {}
void baz() {}
};
class Derived : public Base {
public:
virtual void sone_func() {}
virtual void foo(int &x) {}
virtual void bar() {}
virtual void baz() {}
};
Derived
的4个函数都没有触发重写操作。第一个派生类虚函数sone_func
的函数名与基类虚函数some_func
不同,所以它不是重写。第二个派生类虚函数foo(int &x)
的形参列表与基类虚函数foo(int x)
不同,所以同样不是重写。第三个派生类虚函数bar()
相对于基类虚函数少了常量属性,所以不是重写。最后的基类成员函数baz
根本不是虚函数,所以派生类的baz
函数也不是重写。
1.2、使用override说明符
重写容易出错,尤其继承关系非常复杂的时候。所以C++11标准提供了一个非常实用的override说明符,明确告诉编译器这个虚函数需要覆盖基类的虚函数,一旦编译器发现该虚函数不符合重写规则,就会给出错误提示。
cpp
class Derived : public Base {
public:
virtual void sone_func() override {}
virtual void foo(int &x) override {}
virtual void bar() override {}
virtual void baz() override {}
};
如果没有override说明符,则修改基类虚函数将面临很大的风险,因为编译器不会给出错误提示,我们只能靠测试来检查问题所在。
1.3、使用final说明符
可以为基类声明纯虚函数来迫使派生类继承并且重写这个纯虚函数。但是一直以来,C++标准并没有提供一种方法来阻止派生类去继承基类的虚函数。C++11标准引入final说明符解决了上述问题,它告诉编译器该虚函数不能被派生类重写。final说明符也需要声明在虚函数的尾部。
cpp
class Father {
public:
virtual void func() final {
cout << "Father" << endl;
}
};
class Child: public Father {
public:
// 报错,不能重写final修饰的函数
void func() {
cout << "Child" << endl;
}
};
最后要说明的是,final说明符不仅能声明虚函数,还可以声明类。如果在类定义的时候声明了final,那么这个类将不能作为基类被其他类继承
cpp
class Base final {
public:
virtual void foo(int x) {}
};
// 报错,不能继承final修饰的类
class Derived : public Base {
public:
void foo(int x) {};
};
1.4、override和final的特别之处
在C++11标准中,override和final并没有被作为保留的关键字,其中override只有在虚函数尾部才有意义,而final只有在虚函数尾部以及类声明的时候才有意义,因此以下代码仍然可以编译通过:
cpp
void override() {}
void final() {}
二、重载
函数重载的条件:
1、参数个数不同
2、参数类型不同
3、参数顺序不同
cpp
void fun(int i) {
cout << "打印整数: " << i << endl;
}
void fun(int i, int j) {
cout << "打印两个整数: " << i << " 和 " << j << endl;
}
void fun(float f) {
cout << "打印浮点数: " << f <<endl;
}
fun(4); // 调用第一个 fun 函数
fun(2, 3); // 调用第二个 fun 函数
fun(1.5f); // 调用第三个 fun 函数
**注意:**返回值不同不是函数重载的判断标准
cpp
void fun(int i) {
cout << "打印整数: " << i << endl;
}
int fun(int i) {
cout << "打印整数: " << i << endl;
return 0;
}
fun(4); // 报错,并不知道调用哪个函数
2.1、函数重载的底层原理
为什么C++支持函数重载而C不支持?C语言中同名函数编译完还是同名的,两个重名函数的地址都是有效值,所以在重定位的时候就会产生冲突和歧义。而C++会对函数名进行修饰,例如:
cpp
void f(int a, double b) { printf("%d %lld", a, b) }
函数名 f
会被修正为 _Z1fid
,Linux下的命名规则为
函数名被修饰为:_Z + 函数名长度 + 函数名 + 各个形参类型首字母的小写
这也就解释了为什么函数重载和返回值无关,为什么和参数个数,类型,顺序不同就可以重载,因为他们修饰完后的函数名就是不同的。
三、隐藏
隐藏指在某些情况下,派生类 中的函数屏蔽了基类中的同名函数:
1、两个函数参数相同,但是基类不是虚函数。和重写的区别在于基类函数是否是虚函数
2、两个函数参数列表不同,无论基类函数是否虚函数,基类函数都将被覆盖。和重载的区别在于两个函数不在同一个类中
下面举例说明:
cpp
class Base {
public:
void funA(){cout<<"funA()"<<endl;}
virtual void funB(){cout<<"funB()"<<endl;}
};
class Heri:public Base {
public:
// 隐藏,基类中同名函数不是虚函数
void funA(){cout<<"funA():Heri"<<endl;}
// 隐藏,参数列表不同,无论基类是否是虚函数,基类函数都将被覆盖
void funA(int a){cout<<"funA(int a):heri"<<a<<endl;}
// 重写,基类是虚函数
void funB(){cout<<"funB():heri"<<endl;}
};
隐藏使用的时候记住一句,派生类的指针或引用,对象调用子类和父类同名的函数,父类的同名函数被子类隐藏,调用的是子类的函数。
看下面代码:
cpp
class Base {
public:
void fun1() { cout<<"base:fun1()"<<endl; fun(); }
virtual void fun() { cout<<"base:fun()"<<endl; }
};
class Deriverd:public Base {
public:
virtual void fun1() { cout<<"deriverd:fun1()"<<endl; }
void fun() { cout<<"deriverd:fun()"<<endl; }
};
int main() {
Base *pb = new Deriverd;
pb->fun1();
return 0;
}
输出结果为:
main
函数中创建了父类的指针指向了子类的对象,然后通过父类的指针调用具有隐藏关系的fun1()
函数,我们会以为pb->fun1()
调用的是子类的函数fun1()
,实际并不是,隐藏关系的函数,谁调用就用谁的函数,按照正常的函数调用使用便可得正确的结果,这里是父类指针调用,就用父类的函数fun1()
。这就是隐藏和重写的区别。
四、重写与隐藏的区别
看下面的代码:
cpp
class Base {
public:
virtual void foo(int x) { cout << "Base: " << x << endl;}
void foo(int x, int y) { cout << "Base: " << x << ' ' << y << endl; }
};
// 报错,不能继承final修饰的类
class Derived : public Base {
public:
void foo(int x) { cout << x << endl; };
void foo(int x, int y) {cout << x << ' ' << y << endl; }
};
int main() {
Base *pb = new Derived;
pb->foo(1);
pb->foo(1, 2);
return 0;
}
其中 foo(int x, int y)
函数发生了隐藏,void foo(int x)
函数发生了重写, Base *pb = new Derived;
发生了父类指针指向子类对象,隐藏由于是父类指针,所以调用了父类的实现,重写由于是子类对象,所以调用了子类实现。