多态的使用
1.多态的概念
同一行为对不同的对象有不同的效果;多态针对的是函数
2.多态的条件
两个基本条件
1.父类和子类构成多态的函数的函数名,返回类型,参数(不包括缺省参数)
2.父类的指针或引用去调用虚函数
解释:对于条件的第二点,如果不用父类的指针或引用去调用,实际上并没有语法错误,只不过是不符合你的预期
cpp
class A
{
public:
virtual void func1()
{
cout << "class A" << endl;
}
protected:
int _a = 1;
};
class B : public A
{
public:
virtual void func1()
{
cout << "class B" << endl;
}
protected:
int _b = 2;
};
void func(A& x)
{
x.func1();
}
void func_ptr(A* x)
{
x->func1();
}
int main()
{
A a;
B b;
//引用调用
func(a);
func(b);
cout << endl;
//指针调用
func_ptr(&a);
func_ptr(&b);
}
三个例外
1.协变
父类和子类函数的返回值可以是父子关系的指针或者引用
引用:
指针:
2.派生类可以不写virtual
3.析构函数可以定义为虚函数
这个差的更多了,函数名都不一样;但是在声明析构函数为虚函数时,编译器会处理它成"destructor"(不确定)
看似没有区别,是否定义为多态的析构函数,都可以正常的析构
但是如果被切片了,或者是用父类的指针去析构,就会出现内存泄露
不构成多态的效果:
指针:
切片:
注意:这个是不对的(上),a在栈上,不能析构
所以说:实现析构函数的多态是为了更好的释放资源,同时防止内存泄露
两个关键字
1.override
目的:用于检查子类是否重写了父类的虚函数,以及能否被重写,即父类的那个函数不是虚函数
用法:加在派生类的末尾
2.final
C++11引入,一样的,加在类的后面或者,虚函数的后面
补充:
在C++98,可以通过私有化父类构造,使得子类构造无法生成和实现,导致子类构造无法实例化
不同继承下虚表的模型
1.单继承
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class A
{
public:
virtual void func1()
{
cout << "A::func1()" << endl;
}
virtual void func2()
{
cout << "A::func2()" << endl;
}
protected:
int _a = 1;
};
class B : public A
{
public:
B()
{}
virtual void func1()
{
cout << "B::func1()" << endl;
}
virtual void func3()
{
cout << "B::func3()" << endl;
}
virtual void func4()
{
cout << "B::func4()" << endl;
}
protected:
int _b = 2;
};
class C : public B
{
public:
virtual void func1()
{
cout << "C::func1()" << endl;
}
virtual void func2()
{
cout << "C::func2()" << endl;
}
virtual void func5()
{
cout << "C::func5()" << endl;
}
protected:
int _b = 2;
};
typedef void(*vfptr)();
void print_vfptr(vfptr* vfptr)
{
for (int i = 0; vfptr[i] != nullptr; i++)
{
(*vfptr[i])();
}
cout << endl;
}
int main()
{
A a;
vfptr* ptr_a = (vfptr*)(*((int*)(&a)));
B b;
vfptr* ptr_b = (vfptr*)(*((int*)(&b)));
C c;
vfptr* ptr_c = (vfptr*)(*((int*)(&c)));
print_vfptr(ptr_a);
print_vfptr(ptr_b);
print_vfptr(ptr_c);
}
对于监视窗口只显示爷爷的
2.菱形继承
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class A
{
public:
virtual void func1()
{
cout << "A::func1()" << endl;
}
virtual void func2()
{
cout << "A::func2()" << endl;
}
protected:
int _a = 1;
};
class B : public A
{
public:
B()
{}
virtual void func1()
{
cout << "B::func1()" << endl;
}
virtual void func3()
{
cout << "B::func3()" << endl;
}
virtual void func4()
{
cout << "B::func4()" << endl;
}
protected:
int _b = 2;
};
class C : public A
{
public:
virtual void func1()
{
cout << "C::func1()" << endl;
}
virtual void func2()
{
cout << "C::func2()" << endl;
}
virtual void func5()
{
cout << "C::func5()" << endl;
}
virtual void func3()
{
cout << "C::func3()" << endl;
}
protected:
int _c = 2;
};
class D : public B, public C
{
public:
virtual void func1()
{
cout << "D::func1()" << endl;
}
virtual void func3()
{
cout << "D::func3()" << endl;
}
virtual void func5()
{
cout << "D::func5()" << endl;
}
int _d = 0;
};
typedef void(*vfptr)();
void print_vfptr(vfptr* vfptr)
{
for (int i = 0; vfptr[i] != nullptr; i++)
{
(*vfptr[i])();
}
cout << endl;
}
int main()
{
A a;
vfptr* ptr_a = (vfptr*)(*((int*)(&a)));
B b;
vfptr* ptr_b = (vfptr*)(*((int*)(&b)));
C c;
vfptr* ptr_c = (vfptr*)(*((int*)(&c)));
D d;
vfptr* ptr_d = (vfptr*)(*((int*)(&d)));
print_vfptr(ptr_a);
print_vfptr(ptr_b);
print_vfptr(ptr_c);
print_vfptr(ptr_d);
}
奇怪:d为什么没有func5
改了好多遍得到的结论:D类会先继承B类的,然后覆盖,对于D类有,B类没有的,会先去C类里看,如果C类有,比如这里就是C类有func5,那么会去掉D类的func5虚函数;试过了,普通的多继承也一样,这样做的目的可能地为了让C类有参与感吧(下面更正)
3.菱形虚拟继承
cpp
#define _CRT_SECURE_NO_WARNINGS
#include <iostream>
using namespace std;
class A
{
public:
virtual void func1()
{
cout << "A::func1()" << endl;
}
virtual void func2()
{
cout << "A::func2()" << endl;
}
int _a = 1;
};
class B : virtual public A
{
public:
B(){}
virtual void func1()
{
cout << "B::func1()" << endl;
}
virtual void func3()
{
cout << "B::func3()" << endl;
}
virtual void func4()
{
cout << "B::func4()" << endl;
}
int _b = 2;
};
class C : virtual public A
{
public:
virtual void func1()
{
cout << "C::func1()" << endl;
}
virtual void func5()
{
cout << "C::func5()" << endl;
}
virtual void func3()
{
cout << "C::func3()" << endl;
}
int _c = 3;
};
class D : public B, public C
{
public:
virtual void func1()
{
cout << "D::func1()" << endl;
}
virtual void func3()
{
cout << "D::func3()" << endl;
}
virtual void func5()
{
cout << "D::func5()" << endl;
}
virtual void func6()
{
cout << "D::func6()" << endl;
}
int _d = 4;
};
typedef void(*vfptr)();
void print_vfptr(vfptr* vfptr)
{
for (int i = 0; vfptr[i] != nullptr; i++)
{
(*vfptr[i])();
}
cout << endl;
}
int main()
{
A a;
vfptr* ptr_a = (vfptr*)(*((int*)(&a)));
B b;
vfptr* ptr_b = (vfptr*)(*((int*)(&b)));
C c;
vfptr* ptr_c = (vfptr*)(*((int*)(&c)));
D d;
vfptr* ptr_d = (vfptr*)(*((int*)(&d)));
//cout << sizeof(a) << endl;
//cout << sizeof(b) << endl;
//cout << sizeof(c) << endl;
cout << sizeof(d) << endl;
//print_vfptr(ptr_a);
//print_vfptr(ptr_b);
//print_vfptr(ptr_c);
ptr_d = (vfptr*)(*((int*)(A*)(&d)));
print_vfptr(ptr_d);
ptr_d = (vfptr*)(*((int*)(B*)(&d)));
print_vfptr(ptr_d);
ptr_d = (vfptr*)(*((int*)(C*)(&d)));
print_vfptr(ptr_d);
ptr_d = (vfptr*)(*((int*)(D*)(&d)));
print_vfptr(ptr_d);
}
可是加上构造,模型就变了,这个不对:
纠正如下:
也能看出一点虚表在虚基表上面,虚基表的的第一个偏移量是相对于虚表的(我试了2019也是这样的,就是因为B或者C的地方多了一个构造,就会多出来4个字节)
终于是知道,他是怎么继承的了
结论:先去和先继承的比较,函数构成多态,他就会B虚表(这个虚表仍然是D的,因为D是由3段虚表构成的)里面覆盖上D的函数指针;比对完之后,然后去比对C的,有的话进行覆盖;如果C也没有,那么他就会在B那段虚表后面加上D的虚函数指针
D会和B,C都去比对,满足多态那就覆盖(两段虚表都覆盖),都没有那就放到先继承的B里;A是虚基类,放在最下面
纠正:多继承中一个类会有有多个虚表
解释
1.什么时候形成了多态
在进入初始化列表的那一刻就形成了
验证:
这时候b还没有被初始化,但是虚表指针已经出来了
其实能够形成多态,就是因为虚函数被子类覆盖了
2.为什么有声音说是运行时实现了多态
运行时,顾名思义指的是程序运行的时候,其实我觉得不能说是运行时实现了多态,更准确的是说运行时体现多态;
我们在使用父类的指针或者引用去调用实现了多态的虚函数,在调试的时候发现,都会发现箭头都是先走到父类的函数声明,然后依据父类的指针或者引用,走不同的函数定义;这个过程中(运行时)体现了多态
3.每一个类都只有一个虚表
一个类一个虚表是非常合理的,虚表是在常量区,虚表里存放的都是函数的地址,即函数的指针;函数在汇编的时候,将编译形成的汇编指令,转换成二进制文件,储存在代码段中
子类虚表的构建
是先继承父类的虚表,然后添加新的虚函数指针。对于派生类覆盖了父类虚函数的情况,派生类的虚函数表中会替换相应的虚函数指针为新的虚函数地址
注:图中框出来的是,虚表的地址;
4.为什么一定要是父类的指针或者引用
一个类一个虚表,其实就已经说明了,必须使用父类的指针或者引用,只有这样我们的在【内置类型转换(切片)<- 这个说法不对】调用的时候,能够使用到你所期望的虚表里的构成多态的函数
更正部分名词的理解
也很好理解,如果单单用父类的对象去调用,当然用的是父类的虚表
比如这样:这是发生了切片
这样就可以,因为引用仍然是对象本身,那就是取父类的虚表里找
5.虚表的打印(语法问题)
cpp
class A
{
public:
virtual void func1()
{
cout << "A::func1()" << endl;
}
virtual void func2()
{
cout << "A::func2()" << endl;
}
protected:
int _a = 1;
};
class B : public A
{
public:
B()
{}
virtual void func1()
{
cout << "B::func1()" << endl;
}
virtual void func3()
{
cout << "B::func3()" << endl;
}
virtual void func4()
{
cout << "B::func4()" << endl;
}
protected:
int _b = 2;
};
typedef void(*vfptr)();
void print_vfptr(vfptr* vfptr)
{
for (int i = 0; vfptr[i] != nullptr; i++)
{
printf("%p\n", vfptr[i]);
//vfptr f = vfptr[i]; //不知道为什么不对
//f();
//(*vfptr[i])();
}
}
int main()
{
A a;
B b;
vfptr* ptr1 = (vfptr*)((int*)(&b));
//print_vfptr(ptr1);
vfptr* ptr2 = (vfptr*)(*((int*)(&b)));
print_vfptr(ptr2);
}
还c语言指针的账,ptr1拿出来的是b的地址;函数指针的指针,就是函数指针数组
6.虚表位置
发现加在常量区之间
补充
内存结构的分布
栈,常量区,堆,静态区
名词解释
1.动态绑定
动态绑定发生在运行时期,也称为晚期绑定,动态多态
动态绑定通常发生在虚函数的调用中,其中虚函数表用于确定要调用的函数
2.静态绑定
静态绑定发生在编译时期,也称为早期绑定,静态多态
静态绑定通常发生在非虚函数或对象的静态成员函数的调用中
3.切片
用子类构造父类的时候发生切片,基类部分的成员被赋值到新父类对象中,而派生类对象的额外部分被丢弃的过程;只取到父类的成员,不会取到子类的虚表指针
4.类型转换
指针类型转换数与指针之间的转换,指针与指针之间,内置类型转换:数与数之间
5.赋值兼容
一个类型的对象赋值给另一个类型的对象的时候不需要显示类型的转换;也就是说一个类型可以隐式转换为另一个类型
6.重载
指的是在同一作用域内,多个函数的函数名相同,参数不同;对返回类型是没有要求的,因为我们是要去调用这个函数,没法通过返回值来区分调用的函数
7.重写
也叫覆盖,指的是在继承关系中的父类和子类两个作用域中,两个函数的函数名,参数,返回类型相同,且都是虚函数;这不就是构成多态的条件吗
8.重定义
也叫隐藏,指的是在继承关系中的父类和子类两个作用域中,两个函数的函数名相同
GPT
https://chat.openai.com/share/73eca83a-31cb-4fe7-81c4-a381dd80173d
用多态实现析构函数的好处