先自我介绍一下,小编浙江大学毕业,去过华为、字节跳动等大厂,目前阿里P7
深知大多数程序员,想要提升技能,往往是自己摸索成长,但自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!
因此收集整理了一份《2024年最新大数据全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友。
既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,涵盖了95%以上大数据知识点,真正体系化!
由于文件比较多,这里只是将部分目录截图出来,全套包含大厂面经、学习笔记、源码讲义、实战项目、大纲路线、讲解视频,并且后续会持续更新
如果你需要这些资料,可以添加V获取:vip204888 (备注大数据)
正文
如图,有个Person类里面实现了一个BuyTicket函数,还有个student类里面也实现了一个BuyTicket函数。此时student类继承了Person类,两者的BuyTicket函数都是虚函数,满足函数名、参数、返回值相同,并且在fun函数里参数是用的父类的引用去调用。此时Person和student的BuyTicket就构成了多态。通过不同的对象去调用同一个函数产生了不同的行为!
另外,子类的函数可以不是虚函数,但父类的函数必须是虚函数
协变(父类和子类的返回值类型不同)
三同中,返回值类型可以不同,但要求返回值是父子类关系的一个引用或者指针
就算用的别的类型也可以。这里我创建了一个父子类关系类型A,类型B用来做返回值
函数隐藏和虚函数重写的比较
我们知道,父类和子类的函数名相同就构成了函数隐藏或者重定义。而多态的要求比隐藏更严格,虚函数的重写必须满足三同(函数名、参数、返回值类型相同),其中一个不相同即为函数隐藏。
函数重载 | 函数隐藏/重定义 | 函数重写/覆盖 |
---|---|---|
同一作用域;函数名相同;参数列表不同(参数类型、个数、顺序);返回值不影响 | 不同作用域(父类和子类);函数名相同;参数列表不同时,基类有无virtual修饰都是;参数列表相同时,基类没有virtual修饰是;返回值可以不同 | 不同作用域(父类和子类);函数名相同;参数列表相同;返回值类型相同;基类必须要有virtual修饰;必须是由父类的引用或者指针调用虚函数;返回值类型不同时,返回值类型也必须是父子类关系的指针或者引用—协变 |
由此可见,多态调用与调用的对象有关,普通调用与调用的对象类型有关
析构函数的重写
这里是普通调用析构函数,目前没什么问题
然而当有父类的指针指向或者父类的引用时,子类的析构函数没有执行,产生了内存泄漏。原因:子类的切片,指针或者引用指向父类那部分,所以子类就只调用了父类的析构函数。
这时候就需要用到函数的重写。
只需要给父类的析构函数加上virtual修饰即可。编译后,编译器对父类和子类的析构函数名称都统一处理成destructor
关键字final和override
前面都介绍的是如何实现函数的重写,那么一个虚函数不想被重写呢?
给虚函数加上关键词final加以修饰表示虚函数不能被重写
那一个类不想被继承呢?
一是构造函数私有
二是用final修饰,即可理解为最后的类
override修饰子类函数可用来在编译期间检查子类函数是否对父类函数完成了重写
抽象类
定义:在虚函数后面写=0,则这个函数为纯虚函数。包括纯虚函数的类称抽象类或接口类。抽象类不能实例化出对象,其子类也不能实例化出对象,除非子类重写了纯虚函数。纯虚函数规定了子类必须重写,即接口继承。
虚函数继承通过与普通函数的继承对比,普通函数继承为实现继承,派生类继承了基类,可以用基类的函数。而虚函数虚函数是一种接口继承。派生类继承的是接口,目的是为了重写,达到多态。
多态的原理
接下来看一个含有虚函数的类的大小
类A里有一个int类型和一个char类型,合计5个字节,加上虚函数dave,虚函数里有虚表指针4个字节(32位系统下),合计9个字节,内存对齐后是12个字节
我们打开调试窗口可以看到有个指针_vfptr
那如果类里多几个虚函数呢??
class A
{
public:
virtual void dave1(){}
virtual void dave2(){}
virtual void dave3(){}
private:
int _a;
char _b;
};
int main()
{
A aa;
cout << "带有虚函数的类的大小:" << sizeof(aa) << endl;
return 0;
}
类里再多的虚函数也只有一个虚表指针,指针指向一个虚函数表,表里存放着指向各个虚函数的指针,该虚表本质上是函数指针数组。
class A
{
public:
virtual void Func1()
{
cout << "A::Func1()" << endl;
}
virtual void Func2()
{
cout << "A::Func2()" << endl;
}
void Func3()
{
cout << "A::Func3()" << endl;
}
private:
int _a = 1;
};
class B : public A
{
public:
virtual void Func1()
{
cout << "B::Func1()" << endl;
}
private:
int _b = 2;
};
int main()
{
A a;
B b;
return 0;
}
这里B类继承了A类,并完成了对虚函数fun1的重写,而没有对A类的虚函数fun2重写。可以看到两个虚函数都继承了下来,但fun1的地址该变了,而fun2的地址没有改变。可以猜测:子类在对父类函数的重写时,是先把父类的虚函数表拷贝一份,然后对要重写的函数进行覆盖。
那普通调用和多态调用的原理有差别吗?
这里ptr指针对fun3函数调用为普通调用,而对fun1函数调用为多态调用
调试时转到反汇编,可以看到普通调用是直接call函数,而多态调用则步骤很多,还用到了各种寄存器。
这里更加应证了普通调用为编译时绑定,即在编译期间就确定了程序的行为,也称静态多态,比如函数重载。
而多态调用为运行时绑定,在程序运行期间根据具体的类型确定程序的行为,调用具体的函数,也称动态多态。
实际上,普通调用时,是根据指针指向的类型进行调用。ptr指向b对象的fun3是A类fun3的切片,跟ptr指向a对象的fun3无异。所以是直接call A类的fun3函数。
而多态调用是根据指针指向对象的类型有关。ptr指向b对象的fun1,**由于fun1是虚函数,该指向虚函数的指针进入了虚数表,那么指针就进入虚数表里找,找到的是类型B对类型A重写的fun1虚函数的指针,那么调用的就是重写的fun1函数,注意该切片部分是被重写的!**而ptr指向a对象的fun1也是进入虚数表里找,找到的调用的即是fun1虚函数本身。
而多态能完成指向谁调用谁其根本就是由于虚数表。
那虚表在哪里呢?
找到虚表存放的第一个指针的地址就能找到虚表的位置。
int main()
{
int a = 0;
cout << "栈:" << &a << endl;
int\* p1 = new int;
cout << "堆:" << p1 << endl;
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注大数据)**
![img](https://img-blog.csdnimg.cn/img_convert/609af386fb4e889a72c7010dc520d5b6.png)
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**
out << "堆:" << p1 << endl;
**网上学习资料一大堆,但如果学到的知识不成体系,遇到问题时只是浅尝辄止,不再深入研究,那么很难做到真正的技术提升。**
**需要这份系统化的资料的朋友,可以添加V获取:vip204888 (备注大数据)**
[外链图片转存中...(img-U8zdXbCy-1713388503987)]
**一个人可以走的很快,但一群人才能走的更远!不论你是正从事IT行业的老鸟或是对IT行业感兴趣的新人,都欢迎加入我们的的圈子(技术交流、学习资源、职场吐槽、大厂内推、面试辅导),让我们一起学习成长!**