🐱作者:一只大喵咪1201
🐱专栏:《C++学习》
🔥格言:你只管努力,剩下的交给时间!
多态
- 多态概念:去完成某个行为,当不同的对象去完成时会产生出不同的状态。
拿生活中买火车票的例子来说,买票的人分别是普通人,学生,军人。
同样是买票这个行为,不同人群得到的结果,行为都不同,这就是多态。体现在代码中就是:
class Person
{
public:
virtual void BuyTicket()
{
cout << "Person : 全价票" << endl;
}
};
class Student : public Person
{
public:
virtual void BuyTicket()
{
cout << "Student : 半价票" << endl;
}
};
class Solider : public Person
{
public:
virtual void BuyTicket()
{
cout << "Solider : 优先买票" << endl;
}
};
void Func(Person& p)
{
p.BuyTicket();
}

可以看到,不同的人做买票这件事时,状态不一样。这里的状态包括结果行为等等。
有人会觉得,这不是不同的类对象调用各自的成员函数吗?这有什么特殊的。

也就是说,无论外面给还是传什么类型,是子类都会发生切片,但是函数的形参都不会变,一直都是父类的引用,也就意味着在Func内调用的成员函数始终都是父类Person的成员函数。这是我们之前学习到的内容。
但是此时产生的结果却是三种,而不是都是调用父类Person成员函数的结果,这和我们之前所认识的矛盾。
- 站在Func函数的角度去看,它只知道自己是父类的引用,调用的函数是BuyTicket()。
但是父类的引用对象调用的并不是同一个成员函数,这就是多态。
虚函数:
- 被virtual修饰的函数就叫做虚函数。

上面类中的成员函数就是虚函数。
重写/覆盖:
- 虚函数的重写(覆盖):子类中有一个跟父类完全相同的虚函数(函数名相同,返回类型相同,形参相同)。
- 重写的前提:父类和子类中必须都是虚函数。

上图中,父类中的BuyTicket就和子类中的BuyTicket构成了重新关系。

它和继承中的隐藏/重定义有点相似:

它们的关系就像上图所示,重写是在隐藏的基础上多加两个条件,条件更加苛刻。
- 但是符合重写就是重写,而不是隐藏。
虚函数重写的两个例外:

父类虚函数有virtual,子类没有,此时仍然构成重写。
- 因为子类会继承父类的虚函数,所以认为在继承之后,子类的虚函数同样有了virtual,这是从父类继承下来的。
但是一般建议父子类的虚函数都加上virtual。

如上图,此时构成重写的虚函数返回类型就不相同,但是属于父子关系的指针,当然也可以是引用。

- 返回类型可以是其他类的父子关系的指针或引用类型。
这种重写的两个虚函数的返回类型不同的例外情况叫做协变。
多态的条件:
必须符合这两个条件才能构成多态。

再看最开始的代码。
完全符合多态的两个条件,所以结果也是多态的,调用了三个不同的成员函数。
多态调用和普通调用有什么区别呢?
普通调用时调用的时对象所对应的成员函数,多态调用时调用的是指针或者引用指向的那个对象的成员函数。
父类的析构函数最好用virtual修饰:

父类和子类都会在堆区开辟动态空间。

创建两个父类的指针ptr1指向堆区的父类对象,ptr2指向堆区的子类对象。然后使用delete释放堆上的两个对象。
- new一个对象时,会调用它的构造函数。
- delete一个对象时,会调用它的析构函数。
此时如果是多态调用析构函数就好了,delete指针ptr2的时候就可以调用子类的析构函数,就会将子类中属于自己的和父类的堆区数据全部释放掉。

再看代码,可以注意到,在delete时,
- ptr1和ptr2都是父类的指针
- 符合了多态的一个条件,还缺一个构成重写的条件。
所以在父类和子类的析构函数前都加上virtual,此时就都是虚函数了。但是父类和子类的析构函数名不同。

最终形成如上图所示的样子,此时父类和子类的析构函数就可以构成重写关系了。(语法上不能这样写)
这也解释了在学习继承的时候,父子类的析构函数为什么会构成隐藏关系。

回想构成重写虚构函数的第一个例外,子类的虚函数可以不加virtual。
- 此时构成多态的两个条件,构成重写关系,又父类的指针调用虚函数都具备了。

由于是多态调用,此时ptr2调用的就是子类的析构函数,而不是父类的析构函数,所以会先调用子类的析构函数清理子类的资源,再调用父类的析构函数清理从父类继承下来的资源(继承中析构函数的另一怪)。
为了防止出现子类资源析构不彻底的情况,最好在父类的析构函数前加上virtual。
final:
这里对继承部分的知识进行补充。
如何设计出不能被继承的类呢?

创建子类对象的时候,子类的构造函数会先调用父类的构造函数将从父类继承下来的成员进行初始化,再初始化子类自己的成员。
这种类也被叫做最终类,顾名思义就是最后的类,意味着无法继承。

在父类被final修饰以后,创建子类对象时会直接报错,子类无法继承。
如果需要设计无法继承的父类时,最好使用第二种方式,也就是用final修饰父类。
回归正题:

父类和子类中虽然都是虚函数,并且符合三同的条件,但是由于父类中的虚函数被final修饰了,所以在创建子类对象的时候报错父类的虚函数无法重写。
override:

| 概念 | 条件1 | 条件2 | 条件3 |
|---|---|---|---|
| 重载 | 两个函数在同一个作用域 | 函数名相同 | 参数不同 |
| 重定义(隐藏) | 两个函数分别在父类和子类的作用域 | 函数名相同 | |
| 重写(覆盖) | 两个函数分别在父类和子类的作用域 | 函数名,返回类型,形参相同(协变除外) | 两个函数必须是虚函数 |
两个分别在父类和子类中的同名函数,不是构成隐藏就构成重写。
- 概念:在虚函数后面加上=0,这个虚函数就称为纯虚函数,含有纯虚函数的类就叫做抽象类(也叫做接口类),抽象类不能实例化出对象。

如上图,在父类的虚函数后加上了=0,此时就成立纯虚函数,所以class Car就是一个抽象类。在创建父类对象的时候,报错误无法实例化对象。

当父类是抽象类的时候,子类只会继承它的接口,具体实现会重写,换言之,抽象类会强制子类重写纯虚函数。
接口继承和实现继承:
不实现多态,不要把函数定义成虚函数。
抽象类就是典型的接口继承,当父类在现实中没有具体的事物对照,但是它的方法框架已经有了,只是没有实现,此时就可以使用抽象类。
当子类继承了父类以后,就会在现实中有事物的对照,此时再将继承下来的纯虚函数接口进行重写,这样一个完整的类就有了。
class Base
{
public:
virtual void func1()
{
cout << "Base::func1()" << endl;
}
protected:
int _b = 1;
};
这样一个类有多大,占据多少个字节的内存?
按照之前的认识:
- 成员变量只有一个int类型,占4个字节。
- 成员函数在代码段,所以计算大小时不考虑。
- 所以这个类的大小是4字节。

结果却是8,不是4,这是为什么呢?

通过监视窗口可以看到,在Base对象创建后,不仅包含成员变量,还有一个vdptr的指针变量,这个指针叫做虚函数表指针。
包含虚函数的类对象中,存在一个虚函数表指针。
那么,这个虚函数表指针指向的虚函数表中放的是什么呢?顾名思义是虚函数,下面本喵来给大家验证一下。


父类Base对象b:
子类Drive对象d:
再来画一下它们的对象结构图:

虚函数表是一个指针数组,里面放的都是函数指针。
那么,虚函数表是存放在哪里的呢?栈区吗?其实根据数组名就是首元素地址,也就是第一个函数指针的地址,可以推断出虚函数表是存放在代码段的。
下面本来进行一个不太严谨的验证:

将我们知道存储位置的变量的地址打印出来,分别是栈区,堆区,数据段,代码段。
复习一下指针:

- 取类对象的地址,此时该指针指向的是整个对象。
- 由于虚表指针在类对象的第一个位置,并且大小是4个字节的,所以将类指针强转成int*的,此时指针指向的就是vfptr。
- 然后进行解引用,得到vfptr中的内容,也就是虚表名,相当于虚表首元素的地址。
- 由于解引用得到的是int类型的数据,所以需要再强转位void*后,才会按照指针类型将地址打印出来,而不是整数。
思考:在32位机器上,需要将类指针强转为int*,那么64位机器呢?如何让它自适应不同的机器呢?
*(void**)(&be)
有人可能觉得为什么要这么麻烦,直接访问vfptr不就行了吗?

根本就无法直接访问vfptr虚表指针。

父类中有两个虚函数func1和func2,子类中有两个虚函数func1和func3,其中func1对子类虚函数进行了重写,还有一个普通函数func4。

由于监视窗口VS2019会做一些不合理的优化,所以用内存窗口来看。
Base b:
Drive d:
从这里可以看出,子类继承父类的虚表指针以及虚表是在设计层面的。
接下来本喵给大家看一下虚表中的内容,也就是看一下虚表中函数的地址:
typedef void(*VFPtr)();
void PrintVFTable(VFPtr vft[])
{
for (int i = 0; vft[i]; ++i)
{
printf("[%d]:0x%p-->", i, vft[i]);
vft[i]();//调用对应虚函数
}
cout << endl;
}
封装一个打印虚函数表的函数,指针的应用,本喵就不解释了。

此时各自打印出了自己的虚表内容并且还调用了相应的函数。
说了这么多,原理到底是什么呢?肯定是和虚表指针以及虚表息息相关。
- 通过不同对象的虚表指针去调用虚表中对应的函数。

在上面的多态调用中:

当ptr指向的是父类时,通过父类的虚表指针和虚表调用的就是父类的虚函数,着很容易理解。

当ptr指向的是子类时:
- 静态绑定:又称为前期绑定(早绑定),在程序编译期间确定了程序的行为,也称为静态多态,比如:函数重载。
- 动态绑定:又称后期绑定(晚绑定),是在程序运行期间,根据具体拿到的类型确定程序的具体行为,调用具体的函数,也称为动态多态。

我们来看上面程序的汇编代码来感受一下:
静态绑定:

静态绑定时,要调用的函数地址已经确定好了,直接调用就行。
动态绑定:

动态绑定时,要调用函数的地址并不是确定好的,而是需要在执行过程中计算得到,并且放在eax寄存器中。
反思:多态的两个条件中,为什么必须是父类的指针或者引用取调用虚函数?父类的对象不可以吗?

先说答案,不可以,可以看到,上图代码中并没有实现多态调用,两次调用都是掉的父类中的虚函数。

- 相同类型的对象,共用一个虚表。
- 如上图中的Base b和Base func,它们的虚表都是同一个,地址是00409b34。
由于普通子类对象在赋值给普通父类对象时,子类对象的虚表并不会给过去,而所有父类对象都共用一个虚表,所以普通父类对象无法实现多态,必须是父类的指针或者引用。

现在我们要看的是子类对象的虚函数表是什么样的。
- 子类继承自父类Base1和Base2,所以子类中有两个虚函数表指针。指向两个虚表。
typedef void(*VFPtr)();
void PrintVFTable(VFPtr vft[])
{
cout << "虚表地址:" << vft << endl;
for (int i = 0; vft[i]; ++i)
{
printf("[%d]:0x%p-->", i, vft[i]);
vft[i]();//调用对应虚函数
}
cout << endl;
}
int main()
{
Drive d;
//从Base1继承的虚表指针
VFPtr* VFTBase1 = (VFPtr*)(*(int*)(&d));
PrintVFTable(VFTBase1);
//从Base2继承的虚表指针
VFPtr* VFTBase2 = (VFPtr*)(*(int*)((char*)(&d) + sizeof(Base1)));
PrintVFTable(VFTBase2);
return 0;
}
使用上面代码来查看虚表中的内容。

从Base1继承下来的虚表:
从Base2继承下来的虚表:
多继承中,有多少个父类,子类中就会继承多少个虚表,每个虚表中防着的是各自父类的虚函数,若父类中的虚函数和子类构造重写,则将对应虚表中的位置进行覆盖。子类特有的虚函数会放在第一张虚表中。
多继承对象的内存模型:

class A
{
public:
virtual void func1()
{
cout << "A::func1()" << endl;
}
int _a;
};
class B : public A
{
public:
virtual void func1()
{
cout << "B::func1()" << endl;
}
virtual void func2()
{
cout << "B::func2()" << endl;
}
int _b;
};
class C : public A
{
public:
virtual void func1()
{
cout << "C::func1()" << endl;
}
virtual void func3()
{
cout << "C::func3()" << endl;
}
int _c;
};
class D : public B, public C
{
public:
virtual void func1()
{
cout << "D::func1()" << endl;
}
int _d;
};

菱形继承中的虚表和多继承一样,子类会继承父类的虚表指针和虚表,如果发生重写就去对应的虚表中覆盖,如果子类自己有虚函数则放入第一个虚表中。
其他和菱形继承一样,会导致数据冗余和二义性。

将两个虚表打印出来发现并不是同一个表。虽然两个虚表最终都是来自于A,但是在B和C继承后并不是同一个,得得到了重新维护。
代码就不放了,只需要在菱形的腰部使用虚拟继承就行。

冗余的变量和菱形虚拟继承一样,都是放在d对象的最下面。
可以看到,在d对象的下边不仅有冗余的成员变量,还有一个指针一样的东西。
注意:如果B和C都对A中的虚函数进行了重写,那么D必须得重新,因为它们是共用一个的,否则不知道该用B重写后的还是C重写后的。
对象模型:

虚拟菱形继承中,来自B和C的虚表指针和虚表正常存在,只是虚表中没有了从A继承下来的虚函数。从A继承下来的虚函数和冗余的变量一样,放在了d对象的后边,进行共用。
再次表达继承中谈到的观点,强烈不建议设计菱形继承,不仅数据冗余和二义性复杂,虚表的处理也复杂。
inline函数可以是虚函数吗?
答:不可以,因为内联函数没有地址,但是虚函数地址要被填入到虚表中。不过是可以编译通过的,因为inline只是个建议,到底有没有展开要视情况而定:若调用时不构成多态,保持inline属性;若构成多态,则没有inline属性。
静态成员可以是虚函数吗?
答:不能,因为静态成员函数没有this指针,使用类型::成员函数的调用方式无法访问虚函数表,所以静态成员函数无法放进虚函数表。
构造函数可以是虚函数吗?
答:不能,因为对象中的虚函数表指针是在构造函数初始化列表阶段才初始化的。
析构函数可以是虚函数吗? 什么场景下析构函数是虚函数?
答:可以,并且最好把基类的析构函数定义成虚函数。具体例子文章中有提到。
对象访问普通函数快还是虚函数更快?
答:虚函数不构成多态就一样快,虚函数构成多态的调用,普通函数快,因为多态调用时进行动态绑定。
虚函数表是在什么阶段生成的,存在哪的?
答:虚函数表是在编译阶段就生成的,一般情况下存在代码段(常量区)的。
C++菱形继承的问题?虚拟菱形继承的原理?
答:注意虚表和虚基表是两个东西,不要混为一谈。
class A
{
public:
virtual void func(int val = 1) { std::cout << "A->" << val << std::endl; }
virtual void test() { func(); }
};
class B : public A
{
public:
void func(int val = 0) { std::cout << "B->" << val << std::endl; }
};
int main(int argc, char* argv[])
{
B* p = new B;
p->test();
return 0;
}
上面代码输出结果是什么?
A: A->0 B: B->1 C: A->1 D: B->0 E: 编译出错 F: 以上都不正确

所以结果应该是B。该题考查了接口继承和多态调用,非常坑。
class A {
public:
A(char* s) { cout << s << endl; }
~A() {}
};
class B :virtual public A
{
public:
B(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
class C :virtual public A
{
public:
C(char* s1, char* s2) :A(s1) { cout << s2 << endl; }
};
class D :public B, public C
{
public:
D(char* s1, char* s2, char* s3, char* s4) :B(s1, s2), C(s1, s3), A(s1)
{
cout << s4 << endl;
}
};
int main() {
D* p = new D("class A", "class B", "class C", "class D");
delete p;
return 0;
}
A:class A class B class C class D B:class D class B class C class A
C:class D class C class B class A D:class A class C class B class D
答案选哪个?
所以,它们的初始化顺序是A,B,C,D。
继承关系中,谁先声明就先初始化谁。
多态离不开继承,所以多态和继承必须都理解透彻,同时切记不要设计菱形继承以及虚拟继承。
目录前言滤波电路科普主要分类实际情况单位的概念常用评价参数函数型滤波器简单分析滤波电路构成低通滤波器RC低通滤波器RL低通滤波器高通滤波器RC高通滤波器RL高通滤波器部分摘自《LC滤波器设计与制作》,侵权删。前言最近需要学习放大电路和滤波电路,但是由于只在之前做音乐频谱分析仪的时候简单了解过一点点运放,所以也是相当从零开始学习了。滤波电路科普主要分类滤波器:主要是从不同频率的成分中提取出特定频率的信号。有源滤波器:由RC元件与运算放大器组成的滤波器。可滤除某一次或多次谐波,最普通易于采用的无源滤波器结构是将电感与电容串联,可对主要次谐波(3、5、7)构成低阻抗旁路。无源滤波器:无源滤波器,又称
最近在学习CAN,记录一下,也供大家参考交流。推荐几个我觉得很好的CAN学习,本文也是在看了他们的好文之后做的笔记首先是瑞萨的CAN入门,真的通透;秀!靠这篇我竟然2天理解了CAN协议!实战STM32F4CAN!原文链接:https://blog.csdn.net/XiaoXiaoPengBo/article/details/116206252CAN详解(小白教程)原文链接:https://blog.csdn.net/xwwwj/article/details/105372234一篇易懂的CAN通讯协议指南1一篇易懂的CAN通讯协议指南1-知乎(zhihu.com)视频推荐CAN总线个人知识总
深度学习部署:Windows安装pycocotools报错解决方法1.pycocotools库的简介2.pycocotools安装的坑3.解决办法更多Ai资讯:公主号AiCharm本系列是作者在跑一些深度学习实例时,遇到的各种各样的问题及解决办法,希望能够帮助到大家。ERROR:Commanderroredoutwithexitstatus1:'D:\Anaconda3\python.exe'-u-c'importsys,setuptools,tokenize;sys.argv[0]='"'"'C:\\Users\\46653\\AppData\\Local\\Temp\\pip-instal
我完全不是程序员,正在学习使用Ruby和Rails框架进行编程。我目前正在使用Ruby1.8.7和Rails3.0.3,但我想知道我是否应该升级到Ruby1.9,因为我真的没有任何升级的“遗留”成本。缺点是什么?我是否会遇到与普通gem的兼容性问题,或者甚至其他我不太了解甚至无法预料的问题? 最佳答案 你应该升级。不要坚持从1.8.7开始。如果您发现不支持1.9.2的gem,请避免使用它们(因为它们很可能不被维护)。如果您对gem是否兼容1.9.2有任何疑问,您可以在以下位置查看:http://www.railsplugins.or
我使用的是遗留数据库,所以我无法控制数据模型。他们使用了很多多态链接/连接表,就像这样createtableperson(per_ident,name,...)createtableperson_links(per_ident,obj_name,obj_r_ident)createtablereport(rep_ident,name,...)其中obj_name是表名,obj_r_ident是标识符。因此链接的报告将按如下方式插入:insertintoperson(1,...)insertintoreport(1,...)insertintoreport(2,...)insertint
如何学习ruby的正则表达式?(对于假人) 最佳答案 http://www.rubular.com/在Ruby中使用正则表达式时是一个很棒的工具,因为它可以立即将结果可视化。 关于ruby-我如何学习ruby的正则表达式?,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/questions/1881231/
深度学习12.CNN经典网络VGG16一、简介1.VGG来源2.VGG分类3.不同模型的参数数量4.3x3卷积核的好处5.关于学习率调度6.批归一化二、VGG16层分析1.层划分2.参数展开过程图解3.参数传递示例4.VGG16各层参数数量三、代码分析1.VGG16模型定义2.训练3.测试一、简介1.VGG来源VGG(VisualGeometryGroup)是一个视觉几何组在2014年提出的深度卷积神经网络架构。VGG在2014年ImageNet图像分类竞赛亚军,定位竞赛冠军;VGG网络采用连续的小卷积核(3x3)和池化层构建深度神经网络,网络深度可以达到16层或19层,其中VGG16和VGG
文章目录1、自相关函数ACF2、偏自相关函数PACF3、ARIMA(p,d,q)的阶数判断4、代码实现1、引入所需依赖2、数据读取与处理3、一阶差分与绘图4、ACF5、PACF1、自相关函数ACF自相关函数反映了同一序列在不同时序的取值之间的相关性。公式:ACF(k)=ρk=Cov(yt,yt−k)Var(yt)ACF(k)=\rho_{k}=\frac{Cov(y_{t},y_{t-k})}{Var(y_{t})}ACF(k)=ρk=Var(yt)Cov(yt,yt−k)其中分子用于求协方差矩阵,分母用于计算样本方差。求出的ACF值为[-1,1]。但对于一个平稳的AR模型,求出其滞
写在之前Shader变体、Shader属性定义技巧、自定义材质面板,这三个知识点任何一个单拿出来都是一套知识体系,不能一概而论,本文章目的在于将学习和实际工作中遇见的问题进行总结,类似于网络笔记之用,方便后续回顾查看,如有以偏概全、不祥不尽之处,还望海涵。1、Shader变体先看一段代码......Properties{ [KeywordEnum(on,off)]USL_USE_COL("IsUseColorMixTex?",int)=0 [Toggle(IS_RED_ON)]_IsRed("IsRed?",int)=0}......//中间省略,后续会有完整代码 #pragmamulti_c
按照目前的情况,这个问题不适合我们的问答形式。我们希望答案得到事实、引用或专业知识的支持,但这个问题可能会引发辩论、争论、投票或扩展讨论。如果您觉得这个问题可以改进并可能重新打开,visitthehelpcenter指导。关闭9年前。我来自C、php和bash背景,很容易学习,因为它们都有相同的C结构,我可以将其与我已经知道的联系起来。然后2年前我学了Python并且学得很好,Python对我来说比Ruby更容易学。然后从去年开始,我一直在尝试学习Ruby,然后是Rails,我承认,直到现在我还是学不会,讽刺的是那些打着简单易学的烙印,但是对于我这样一个老练的程序员来说,我只是无法将它