目录
多态的定义
多态构成的条件
析构函数的重写
抽象类
多态的作用
多态的原理
多态的定义
多态是在不同继承关系的类对象,去调用同一函数,产生了不同的行为。
1、必须通过基类的指针或者引用调用虚函数。
注意:对象不行。
2、被调用的函数必须是虚函数,且派生类的虚函数必须对基类的虚函数进行重写,注意:基类的的虚函数必须得要有virtual修饰,派生类可以不用virtual修饰,但是不建议。
重载、重定义与重写三者的区别。
这三个定义很容易混淆。
重载的条件
1、两个函数在同一作用域
2、函数名相同、参数不同
重定义的条件
1、两个函数在基类和派生类各自的作用域类
2、只需要函数名相同
3、两个基类的和派生类的同名函数不构成重写就是重定义
重写的条件
1、基类的函数必须是virtual修饰
2、重写函数必须由相同的类型、名称和参数列表(缺省值可以不同)
这个体现出了接口的继承,一旦构成多态缺省值取决于基类
参考下面这篇文章
C++继承中重载、重写、重定义的区别: - A-祥子 - 博客园 (cnblogs.com)
class Person {
public:
virtual void Print()//虚函数
{
cout << "我是基类" << endl;
}
};
class Student:public Person {
public:
virtual void Print()//重写
{
cout << "我是派生类" << endl;
}
};
void fuc(Person p)
{
p.Print();
}
int main()
{
Student s;
fuc(s);//不构成多态,直接传的对象
Person* p = new Student();//基类的指针指向派生类的对象
p->Print();//构成多态
return 0;
}
先将Print函数进行重写,fuc函数传参时不构成多态,不满足多态的条件中的必须是基类的指针或者引用去调用虚函数。
而下面的基类的指针指向派生类的对象,此时构成了多态。
析构函数的重写
这个比较特殊
先举个例子
class Person {
public:
~Person()
{
cout << "~Person()" << endl;
}
};
class Student:public Person {
public:
~Student()
{
cout << "~Student()" << endl;
}
};
int main()
{
Person* p = new Student();//基类的指针指向派生类的对象
delete p;
return 0;
}
发现只调用了Person的析构函数,没有调用Student的析构函数,很显然,存在内存泄漏的危险。
原因很简单,因为p是基类的指针,释放的时候当然调用他自己的析构函数。
如果是Student类型的指针,释放时当然会调用Student类型的析构,而且还会自动调用基类的析构函数。
那么现在的问题是,之前构成的多态岂不是会造成内存泄漏?
这个问题其实析构函数也可以重写,虽然不同名,但是编译器统一处理成destructor。
class Person {
public:
virtual ~Person()//将析构函数也变成虚函数
{
cout << "~Person()" << endl;
}
};
class Student:public Person {
public:
virtual ~Student()
{
cout << "~Student()" << endl;
此时再次运行就不会存在内存泄漏了。
抽象类
纯虚函数
虚函数不给函数体,在虚函数的后面写上 =0 ,则这个函数为纯虚函数。
包含纯虚函数的类叫做抽象类(也叫接口类)。
抽象类不能实例化出对象。
派生类继承后也不能实例化出对象,只有重写纯虚函数,派生类才能实例化出对象
class Person {
public:
virtual void func() = 0;//纯虚函数
};
class Student:public Person {
public:
};
int main()
{
Person* p = new Student();//报错
p->func();
delete p;
return 0;
}
纯虚函数的作用
1、强制派生类完成重写
2、体现出接口的继承
多态的作用虚函数的继承就是一种接口继承,派生类继承的是虚函数的接口,目的是为了重写,达成多态。
看一个例子
class Person { public: virtual void func(int val = 1) { cout << "Person " << val<
func(); delete p; return 0; } 上面代码最终输出的是 Student 1,而不是Student 0,充分func函数是把整个接口给继承过来了,但是呢,并不继承函数体。
所以,如果不实现多态,不要把函数定义成虚函数
接口重用!!!
相同的接口实现不同的方法,有点和函数重载、模板相似(静态多态),我们现在所说的多态是动态的多态,举个例子,IO设备,通过系统的接口找到他们自己的方法(驱动),而在我们用户层,只需要用一个接口就可以对不同的设备进行 *** 作,非常方便,其实就是多态的思想(Linux系统层面是用C写的,没有多态,其实是用的函数指针)。
既然这样有好处,很方便,C++就引入多态。
C++多态的两种形式 - 云+社区 - 腾讯云 (tencent.com)
多态的原理虚函数表指针
class Person {
public:
virtual void func()
{}
private:
int _num = 0;
};
int main()
{
Person p;
cout << sizeof(p) << endl;//输出8
return 0;
}
可以看到大小为8,原因是有一个虚函数表指针_vfptr
虚函数表是一个指针数组,里面存着虚函数的地址,而虚函数表指针 _vfptr就是指向这个指针数组
虚函数表里的最后一个元素为nullptr,这样就可以知道虚函数表里有几个虚函数地址。
class Person {
public:
virtual void func1()
{}
virtual void func2()
{}
void func3()
{}
private:
int _num = 0;
};
class Student:public Person {
public:
virtual void func2()
{}
};
int main()
{
Person p;
Student s;
return 0;
}
func2被重写了
可以看到func2的地址发生了变化,由于没有重写func1,func1的地址直接继承过来了,所以func1的地址是不变的,func3不是虚函数,所以不再虚函数表里面。
所以说,多态的原理就是,在运行过程中找到指向对象的虚表中查看需要调用的虚函数的地址,并进行调用。
vs下的调试窗口会屏蔽掉派生类自己的虚函数的地址
例如
class Person {
public:
virtual void func1()
{
cout << "func1()" << endl;
}
virtual void func2()
{
cout << "func2()" << endl;
}
void func3()
{}
private:
int _num = 0;
};
class Student:public Person {
public:
virtual void func2()
{
cout << "func2()" << endl;
}
virtual void func4()//派生类自己的虚函数
{
cout << "func4()" << endl;
}
};
注意:
如果基类有虚函数,派生类不会生成自己的虚表,会继承基类的虚表,如果基类没有虚函数,派生类有,派生类自己生成虚表。
这里派生类的虚表只出现两个虚函数地址,但是实际上虚表里面应该存储三个虚函数地址,即派生类自己的虚函数地址监视窗口不显示。
打印虚表内虚函数地址的函数
typedef void(*VFPTR)();//函数指针
void PrintVfptr(VFPTR* vfptr)
{
int index = 0;
while (vfptr[index])
{
printf("vfptr[%d] = %p -> ", index, vfptr[index]);
vfptr[index]();
index++;
}
}
int main()
{
Person p;
Student s;
PrintVfptr((VFPTR*)(*(int*)&p));//取出前四个字节并强转成指向指针数组的指针,注意是二级指针,且类型是函数指针类型
cout << endl;
PrintVfptr((VFPTR*)(*(int*)&s));
return 0;
}
在vs下,虚函数表的指针在对象的地址空间的前四个字节。
只需要取出这前四个字节,然后进行类型转换。
可以看到,其实虚函数表中有func4的地址。
派生类的大小
派生类的大小如何计算呢?
派生类的大小 = 基类的大小 + 自己本身的大小。
注意注意:这里是分别单独计算,然后直接求和,单独计算的时候需要考虑内存对齐,求和之后不用考虑对齐。
举个例子:
class Person {
public:
virtual void func1()
{}
virtual void func2()
{}
void func3()
{}
private:
char_num = 0;
};
class Student:public Person {
public:
virtual void func2()
{}
virtual void func4()
{}
private:
int c;
};
Person的大小为8字节,包括虚函数表指针4字节+char类型一个字节,最终内存对齐一下就是8字节,关于内存对齐的知识看这篇文章
看了这篇自定义数据类型讲解还不会,可以放下你手中的键盘了k
而单独的Student的大小为 int类型4个字节,此时千万别把虚函数表指针再算一次,上面已经说过一次了,因为基类有虚函数,派生类不用自己生成虚函数表指针,所以大小就为4
总的大小就为8+4 = 12字节。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)