前言
之所以写这篇文章,一方面是想自己总结,彻底搞懂重载、隐藏和重写三者之间的关系,另一方面就是想帮助大家更清楚的认识这三者之间的关系。我在写这篇文章之前也大概搜索了一些关于这三者的博客,但是总感觉那些文章写的并不是我想要的,也并没有表达清楚这三者之间的关系,因此决定写下这篇博客。
我相信在看这篇文章之前肯定有很多小伙伴和我一样对这三者关系不是很清楚,总感觉是似懂非懂,总是差那么一点火候!有时候觉得自己懂了,可是换个时间,又感觉迷迷糊糊的,那么我相信你看了这篇文章后,这种感觉将不复存在!你将对重载、隐藏和重写有一个更深层次的认识!
我们一步一步来,同样,先看下这三者的定义,虽然说看完这个定义你还是迷迷糊糊。但是不急,温水煮青蛙,我们慢慢来,看到最后,拿下重载,隐藏和重写!
定义
重载:重载是C++继C而来特有的性质,相比于之前的C语言来讲,一个函数名只能出现一次。然而在C++中,函数名可以多次出现,但是想要发生重载必须要求函数名相同,参数列表不同(参数列表不同包括:参数个数不同,参数类型不同、参数出现的顺序不同)。
隐藏:隐藏,顾名思义就是被隐藏了导致我们看不到。那么隐藏的对象是什么?为什么会发生隐藏?隐藏只会发生在继承的关系中,当派生类(也叫子类)继承基类(也叫父类)时,如果派生类重新定义了基类中的同名函数,那么基类中重新定义的同名函数称作重写,未被重新定义的函数将被隐藏。
重写:重写也叫覆盖,是C++继承特性的另一种呈现,重写和隐藏很像,重写多被用于虚函数。
看望上面定义,我觉得大多仍然还是晕头转向的,这就对了!,想彻底搞懂这三者,那么我们接着往下看。
光看定义就想彻底搞懂重载、隐藏和重写?
没门!下面我们从具体代码实现来搞懂他们!我们依次对重载、隐藏和重写进行分析,先来看最简单的函数重载
。
重载
对于函数重载来说,他不看重继承关系。也就是有没有继承关系,只要我想,我都可以发生重载。
无继承关系重载
代码如下
#include <iostream>
using namespace std;
class Basic
{
public:
Basic()
{
cout << "Basic:call construct\n";
}
~Basic()
{
cout << "Basic:call destroy\n";
}
public:
void Show()
{
cout << "Basic:Show(void)\n";
}
void Show(int a)
{
cout << "Basic:Show(int):" << a << endl;
}
virtual void Show(double b)
{
cout << "Basic:virtual Show(double)" << b << endl;
}
virtual void Show(char c)
{
cout << "Basic:virtual Show(char)" << c << endl;
}
};
int main(void)
{
{
Basic b;
b.Show(); //void Show()
b.Show(3); //void Show(int a)
b.Show(5.1); //virtual void Show(double b)
b.Show('a'); //virtual void Show(char c)
}
system("pause");
return 0;
}
运行结果如下
从上面我们可以看出,该程序先后调用了Basic类的构造函数、四个show方法和析构函数,可能有人会疑惑为什么main函数中会套一个大括号,其实这个目的是为了让我们能够显示的看到Basic类的析构函数被调用(即使不加大括号,Basic类的析构函数也会被调用,只是说不会被显示出来),加了大括号涉及到变量的生命周期问题,即此时b对象的生命周期只在大括号里,因此大括号结束即调用对象的析构函数,如果没有大括号,则调用析构函数会在main函数调用,不利于我们观察,因此这里加了大括号。
我们继续分析上述结果看能发生什么,首先我们能看到重载函数被正确调用。另外,这些重载函数中有虚函数和非虚函数,我们可以得出结论,可以对虚函数和非虚函数进行重载。
下面我们进一步分析:如果重载函数的返回值类型不一样呢?虚函数和非虚函数除了virtual外其余声明可以完全相同吗?
代码如下
#include <iostream>
using namespace std;
class Basic
{
public:
Basic()
{
cout << "Basic:call construct\n";
}
~Basic()
{
cout << "Basic:call destroy\n";
}
public:
void Show()
{
cout << "Basic:Show(void)\n";
}
int Show(int a)
{
cout << "Basic:Show(int):" << a << endl;
return 1;
}
void Show(int a)
{
cout << "Basic:Show(int):" << a << endl;
}
};
显示结果
从结果,我们可以看到,编译器直接报错了
“无法重载仅按返回类型区分的函数”
,即仅按照返回类型来区分重载不可行。
那么虚函数可以与非虚函数重载只相差一个virtual吗?
代码如下
void Show(double b)
{
cout << "Basic:virtual Show(double)" << b << endl;
}
virtual void Show(double b)
{
cout << "Basic:virtual Show(double)" << b << endl;
}
virtual void Show(char c)
{
cout << "Basic:virtual Show(char)" << c << endl;
}
结果如下
这里结果显示,同样不能进行重载,和上面原因呢基本相同。
其实,对于这种结果,我们稍微想一想就可以明白,如果参数类型相同和个数,那么我们调用的时候怎么区分?我们调用的时候有用到返回类型吗?我们调用的时候有显示的写virtual了吗?如果没有,那么编译器怎么知道你想调用哪个函数?他是你肚子里的蛔虫吗?这些结论都显而易见,你们自己思考吧!
上面介绍晚了非继承关系的重载,其实继承关系的重载和非继承关系的重载相同,下面将简单介绍继承关系的重载。
继承关系重载
我们下面这段代码很有意思,代码中分别在基类和派生类中定义了特有的方法。首先,我们在基类中定义了Show方法,并对其进行了重载;然后,我们又定义了Basicshow方法,并且也对其进行了重载。接下来,我们使用Derive派生类来继承基类Basic,并且,在派生类中我们重新定义了两个Show方法(virtual void Show(double,int) 方法未在派生类中定义,这里有没有virtual都一样);然后,我们又在派生类中定义了派生类特有的方法Deriveshow,并进行了重载,下面我们看代码实现
代码实现
#include <iostream>
using namespace std;
class Basic
{
public:
Basic()
{
cout << "Basic:call construct\n";
}
~Basic()
{
cout << "Basic:call destroy\n";
}
public:
void Show()
{
cout << "Basic:Show(void)\n";
}
void Show(int a)
{
cout << "Basic:Show(int):" << a << endl;
}
virtual void Show(double b, int a)
{
cout << "Basic:virtual Show(double,int):" << b << "," << a << endl;
}
void BasicShow()
{
cout << "Basic:BasicShow(void)\n";
}
void BasicShow(int a)
{
cout << "Basic:BasicShow(int)" << a << endl;
}
};
class Derive : public Basic
{
public:
Derive()
{
cout << "Derive:call construct\n";
}
~Derive()
{
cout << "Derive:call destroy\n";
}
public:
void Show()
{
cout << "Derive:void Show(void)\n";
}
void Show(int a)
{
cout << "Derive:void Show(int):" << a << endl;
}
void DeriveShow()
{
cout << "Derive:DeriveShow(void)\n";
}
void DeriveShow(int a)
{
cout << "Derive:DeriveShow(int)" << a << endl;
}
};
int main(void)
{
{
//定义派生类对象
Derive d;
//基类函数,未在派生类中重定义
d.BasicShow(); //Basic::void BasicShow()
d.BasicShow(5); //Basic::void BasicShow(int a)
//派生类特有的单独定义方法
d.DeriveShow(); //Derive::void DeriveShow()
d.DeriveShow(8); //Derive::void DeriveShow(int a)
//派生类重新定义的基类方法(基类和派生类都有的)
d.Show(); //Derive::void Show()
d.Show(9); //Derive::void Show(int a)
// d.Show(3.4,7);//error:派生类中未重新定义此类型
}
system("pause");
return 0;
}
结果显示
我们对输出结果进行分析,首先从代码中我们可以看到在最后对函数进行调用时,我把d.Show(3.4,7)
这个函数调用给注释掉了,之所以注释掉他,是因为调用他时代码会发生错误,至于为什么会发生错误,我们接下来会进行详细分析。我们继续看上面输出结果,输出结果中第三和第四行调用基类的方法BasicShow,然而这两个方法我们并未在派生类中定义,之所以父类能够调用这两个方法,就是因为派生类的继承特性,我相信大家对继承会有所了解。然后接下来两行又调用了派生类特有的方法DeriveShow,这里我相信大家也没什么疑问。再接下来,我们又调用了Show方法,不过我们如果深入思考一下就会发现,这个Show方法我们在基类和派生类中都有定义,但是,我们在派生类中只定义了基类中的两个Show方法,而我们定义的这两个Show方法恰恰可以在派生类中成功调用,而未在派生类中定义的这个Show方法却不能使用派生类对象调用。接着,我们再深入思考一下,程序中的输出结果显示了基类中的BasicShow方法可以使用派生类对象调用,并且这个BasicShow方法没有在派生类中定义;然而基类中的Show方法有三个,可是派生类只能调用其中的两个,并且这两个还都是在派生类中定义的。大家思考到这里,可能就会有点头绪了,为什么调用不了Show(double,int),其实根本目的就是派生类的Show方法隐藏了基类的Show方法。
下面我们来详细探究一下C++的隐藏特性!
隐藏
何时发生隐藏?隐藏的结果是什么?
对于第一个问题,何时会发生隐藏呢,其实发生隐藏很简单,必须满足以下两个条件:
- 有继承关系
- 派生类中重新定义了基类的方法
对于第二个问题,隐藏的结果也很简单,就是派生类对象访问不了基类方法(只针对与隐藏的方法,不发生隐藏的仍然可以访问)
从上面那个代码就可以知道,派生类中重新定义了Show方法导致发生了隐藏,从而使得派生类对象访问不了基类中的Show方法。下面我们同样从代码分析隐藏,我们在分析之前可以先思考几个问题:
- 发生隐藏的结果?
- 虚函数会影响隐藏结果吗?
代码如下
#include <iostream>
using namespace std;
class Basic
{
public:
Basic()
{
cout << "Basic:call construct\n";
}
~Basic()
{
cout << "Basic:call destroy\n";
}
public:
void Show()
{
cout << "Basic:Show(void)\n";
}
void Show(int a)
{
cout << "Basic:Show(int):" << a << endl;
}
void Show(double b, int a)
{
cout << "Basic:virtual Show(double,int):" << b << "," << a << endl;
}
void Show(int a, int b, int c)
{
cout << "Basic:void Show(int , int , int ):" << a << "," << b << "," << c << endl;
}
};
class Derive : public Basic
{
public:
Derive()
{
cout << "Derive:call construct\n";
}
~Derive()
{
cout << "Derive:call destroy\n";
}
public:
void Show()
{
cout << "Derive:void Show(void)\n";
}
};
int main(void)
{
{
//定义派生类对象
Derive d;
//派生类重新定义的基类方法(基类和派生类都有的)
d.Show(); //Derive::void Show()
//d.Show(9); //error:发生了隐藏
// d.Show(3.4,7);//error:发生了隐藏
//d.Show(2,3,4);//error:发生了隐藏
}
system("pause");
return 0;
}
结果如下
从结果可以看出,带派生类中只定义一个基类中的同名函数时,那么将只能调用这一个同名函数,剩余的几个将全部被隐藏。
如果基类定义的函数是虚方法并且派生类指定义了一个基类的同名方法呢?
这个结果和上面一样会隐藏,这里不再细说。
代码如下
#include <iostream>
using namespace std;
class Basic
{
public:
Basic()
{
cout << "Basic:call construct\n";
}
~Basic()
{
cout << "Basic:call destroy\n";
}
public:
void Show()
{
cout << "Basic:Show(void)\n";
}
void Show(int a)
{
cout << "Basic:Show(int):" << a << endl;
}
virtual void Show(double b, int a)
{
cout << "Basic:virtual Show(double,int):" << b << "," << a << endl;
}
virtual void Show(int a, int b, int c)
{
cout << "Basic:void Show(int , int , int ):" << a << "," << b << "," << c << endl;
}
};
class Derive : public Basic
{
public:
Derive()
{
cout << "Derive:call construct\n";
}
~Derive()
{
cout << "Derive:call destroy\n";
}
public:
void Show()
{
cout << "Derive:Show(void)\n";
}
};
int main(void)
{
{
Derive d;
d.Show();
d.Show(3);
d.Show(3.5,6);
d.Show(2,3,4);
}
system("pause");
return 0;
}
结果如下
如果基类中的是虚方法并且派生类完全只定义了部分基类方法还会全部隐藏吗?
代码如下
#include <iostream>
using namespace std;
class Basic
{
public:
Basic()
{
cout << "Basic:call construct\n";
}
~Basic()
{
cout << "Basic:call destroy\n";
}
public:
virtual void Show()
{
cout << "Basic:Show(void)\n";
}
virtual void Show(int a)
{
cout << "Basic:Show(int):" << a << endl;
}
virtual void Show(double b, int a)
{
cout << "Basic:virtual Show(double,int):" << b << "," << a << endl;
}
virtual void Show(int a, int b, int c)
{
cout << "Basic:void Show(int , int , int ):" << a << "," << b << "," << c << endl;
}
};
class Derive : public Basic
{
public:
Derive()
{
cout << "Derive:call construct\n";
}
~Derive()
{
cout << "Derive:call destroy\n";
}
public:
void Show()
{
cout << "Derive:Show(void)\n";
}
};
int main(void)
{
{
Derive d;
d.Show();
d.Show(3);
d.Show(3.5, 6);
d.Show(2, 3, 4);
}
system("pause");
return 0;
}
结果如下
我们看到只有在派生类中重新定义的Show方法才能成功调用,而其他方法都被隐藏。另外,这里重新定义的Show方法我们有了一个新的名称(上面也相同,如果派生类中的函数和基类中的函数完全相同,则说明派生类重写了基类的函数,那些未被重写的函数将被隐藏),叫做重写,而其他的未被重写的函数都被隐藏了。
下面我么来看一下重写
重写
重写,重写同样也是发生在继承关系中,在派生类中,对于在派生类中重新定义的与基类完全相同的同名函数被称作重写,而那些未被重写的同名函数将被隐藏,导致派生类不能访问,重写多出现于虚函数中,不是虚函数也可以发生重写。
下面我们通过代码来深入了解重写
代码如下
#include <iostream>
using namespace std;
class Basic
{
public:
Basic()
{
cout << "Basic:call construct\n";
}
~Basic()
{
cout << "Basic:call destroy\n";
}
public:
void Show()
{
cout << "Basic:Show(void)\n";
}
void Show(int a)
{
cout << "Basic:Show(int):" << a << endl;
}
void Show(double a, double b)
{
cout << "Basic:void Show(double,double)\n";
}
virtual void Show(double b, int a)
{
cout << "Basic:virtual Show(double,int):" << b << "," << a << endl;
}
virtual void Show(int a, int b, int c)
{
cout << "Basic:void Show(int , int , int ):" << a << "," << b << "," << c << endl;
}
};
class Derive : public Basic
{
public:
Derive()
{
cout << "Derive:call construct\n";
}
~Derive()
{
cout << "Derive:call destroy\n";
}
public:
void Show()
{
cout << "Derive:Show(void)\n";
}
void Show(int a)
{
cout << "Derive:Show(int):" << a << endl;
}
virtual void Show(double b, int a)
{
cout << "Derive:virtual Show(double,int):" << b << "," << a << endl;
}
virtual void Show(int a, int b, int c)
{
cout << "Derive:void Show(int , int , int ):" << a << "," << b << "," << c << endl;
}
};
int main(void)
{
{
//父类指正指向子类对象
Basic *p = new Derive;
//派生类重新定义的基类方法(基类和派生类都有的)
p->Show();
p->Show(9);
p->Show(3.4, 7);
p->Show(2, 3, 4);
}
system("pause");
return 0;
}
结果如下
在这段代码中,我们在基类中定义了五个Show方法,而派生类中只重写了其中四个基类方法(两个普通方法,两个虚方法)。我们在主函数中使用了父类指针指向子类的对象,然后通过这个父类指针来调用这四个方法,结果显示前两个方法调用的是基类的方法,后两个方法调用的是派生类的方法。我们观察一下可以看出,在父类中,前两个Show方法被定义成普通函数,而后两个方法被定义成虚函数,这就是为什么后两个方法调用的是派生类方法。
从上面我们知道当我们在基类中定义的方法为普通方法,在派生类中又重新定义了同名方法,基类中的同名方法会被隐藏,那么如果基类中的同名方法全都是虚方法,我在派生类中只定义了部分基类的方法,会发生什么呢?
代码如下
#include <iostream>
using namespace std;
class Basic
{
public:
Basic()
{
cout << "Basic:call construct\n";
}
~Basic()
{
cout << "Basic:call destroy\n";
}
public:
virtual void Show()
{
cout << "Basic:Show(void)\n";
}
virtual void Show(int a)
{
cout << "Basic:Show(int):" << a << endl;
}
virtual void Show(double a, double b)
{
cout << "Basic:void Show(double,double)\n";
}
virtual void Show(double b, int a)
{
cout << "Basic:virtual Show(double,int):" << b << "," << a << endl;
}
virtual void Show(int a, int b, int c)
{
cout << "Basic:void Show(int , int , int ):" << a << "," << b << "," << c << endl;
}
};
class Derive : public Basic
{
public:
Derive()
{
cout << "Derive:call construct\n";
}
~Derive()
{
cout << "Derive:call destroy\n";
}
public:
virtual void Show(double b, int a)
{
cout << "Derive:virtual Show(double,int):" << b << "," << a << endl;
}
virtual void Show(int a, int b, int c)
{
cout << "Derive:void Show(int , int , int ):" << a << "," << b << "," << c << endl;
}
};
int main(void)
{
{
//父类指正指向子类对象
Basic *p = new Derive;
//派生类重新定义的基类方法(基类和派生类都有的)
p->Show();
p->Show(9);
p->Show(2.3, 2.8);
p->Show(3.4, 7);
p->Show(2, 3, 4);
}
system("pause");
return 0;
}
结果如下
我们对结果进行分析,首先,我们应该知道,我们在基类中定义了5个Show方法,并且这5个Show方法都是虚方法;然后,我们又在派生类中重新定义了其中两个虚方法。然而,从结果显示,其他未被重新定义的虚方法并没用被隐藏,只是说在调用方法时如果调用的是重新定义的方法,则调用的是派生类中重新定义的方法,如果调用的是未重新定义的方法,则调用的是原本基类中的方法。
如果我们细心的观察其实会发现一些问题,其实派生类的这些方法也是被隐藏了,而输出结果之所以没有隐藏,那是因为问题出现在我们的调用方法上。在之前的代码我们都是使用派生类对象去调用的方法,而此处我们调用方法是使用的父类指针指向子类对象来调用方法,如果你在这里重新使用派生对象去调用,那么你同样会发现那些未被重写的函数同样被隐藏了。而这里之所以用父类指针指向子类对象来调用方法,目的是为了引出虚方法的一个特性:虚函数表!
。
我相信,到这里,大家基本上都能动重载,隐藏和重写这三者的关系了,下面我们再来简单聊一聊虚函数表!
虚函数表
虚函数表从名字我们就可以听出来他是专门为虚函数来准备的,那么虚函数表的作用是什么?
其实我们从上面那个代码就可以看出来,为什么当父类指针指向子类对象时父类指针调用的就是派生类的方法,而且派生类方法只重写了部分的基类方法,其余的方法却没有被隐藏?
这其实都是虚函数表的功劳,当类中有虚函数出现时,类中会自动床架一个虚函数表,虚函数表中存储的就是虚函数的地址,当使用父类指针指向子类对象时,就从虚函数表中查找函数地址,来调用虚方法。每当有一个虚方法,就会将虚方法添加进虚函数表,每一个类只要有虚函数,就会有属于自己的虚函数表。当父类指针有虚函数时,便有了一个虚函数表,子类继承父类时,同样将父类的虚函数表继承了下来,如果子类中重写了父类的虚函数,那么只将对应的虚函数表中的函数地址更新,而其他的函数地址不变,这就是为什么当子类只重写类部分的父类方法,其他的父类方法仍然不会被隐藏的原因。
结论
- 重载至于函数的参数列表有关(类型,个数,顺序),而与返回值类型、是否是虚函数无关
- 隐藏和重写只发生在继承关系中
- 当派生类重写了基类的同名方法,其他未被重写的同名方法将被隐藏
- 对于基类中的虚方法,当调用方式为父类指针指向子类对象时,即使子类只重写部分父类的方法,同样可以使用父类指针调用那些未被重写的方法
(虚函数表、多态)
- 对于基类中的虚方法,当调用方式为派生类对象调用时,那些未被重写的方法同样会发生隐藏。
结束语:哎呀,终于写完了!写这么多字不容易啊!如果大家觉得文章对你有帮助,欢迎大家关注,评论,点赞,转发,您的支持就是我创作的最大动力!