记得向前看,别烂在过去和梦里
@目录
什么是多态
概念:
指的是为不同数据类型的实体提供统一的接口
目的:
实现接口重用;
虚函数:
virtual修饰的成员函数叫虚函数;
虚函数实现多态;
虚函数限制
A、非类的成员函数不能定义为虚函数
B、类的静态成员函数不能定义为虚函数
C、构造函数不能定义为虚函数,但可以将析构函数定义为虚函数
D、只需要在声明函数的类体中使用关键字“virtual”将函数声明为虚函数,而定义函数时不需要使用关键字“virtual”。
E、当将基类中的某一成员函数声明为虚函数后,派生类中的同名函数(函数名相同、参数列表完全一致、返回值类型相关)自动成为虚函数。
虚函数语法
class 类名{
public:
virtual 返回值类型 函数名(参数列表);
....
protected:
virtual 返回值类型 函数名(参数列表);
....
private:
virtual 返回值类型 函数名(参数列表);
....
};
静态链接与动态链接
#include <iostream>
using namespace std;
class Base{
public :
Base(int val=0):value(val)
{
cout << __func__ << ":" << __LINE__ << endl;
}
~Base()
{
cout << __func__ << ":" << __LINE__ << endl;
}
public :
void setval(int val)
{
this->value = val;
}
int getval() const
{
return this->value;
}
void prnmsg()
{
cout << __func__ << ":" << __LINE__ << endl;
}
private:
int value;
};
class Inherit : public Base
{
public:
Inherit(int val = 0):myval(val), Base(val)
{
cout << __func__ << ":" << __LINE__ << endl;
}
~Inherit()
{
cout << __func__ << ":" << __LINE__ << endl;
}
public:
void prnmsg()
{
cout << __func__ << ":" << __LINE__ << endl;
}
private:
int myval;
};
void test(Base &obj)
{
obj.prnmsg();//是静态连接,即在编译时根据类型决定调用的,这里的类型是Base,所以他调用的是基类里面的prnmsg()
}
int main()
{
Base a;
Inherit b;
test(a);
test(b);
return 0;
}
//运行结果
Base:7
Base:7
Inherit:37
prnmsg:24
prnmsg:24
~Inherit:41
~Base:11
~Base:11
结论:
它并没有得到我们想要结果
Base a;
Inherit b;
test(a);//得到的是Base里面的prnmsg
test(b);//得到的也是Base里面的prnmsg。被隐式类型转换 Inherit -> Base
我们想要的是
test(a);//得到的是Base里面的prnmsg
test(b);//得到的是Inherit里面的prnmsg
所以我们引入了虚函数
在基类里面添加void prnmsg()成员函数前面添加virtual,它就变成了虚函数。
virtual void prnmsg() //虚函数
{
cout << __func__ <<":" << __LINE__ << endl;
}
运行结果
Base:7
Base:7
Inherit:37
prnmsg:24
prnmsg:47//我们可以清楚的看见与静态链接相比这里发生了变化,得到的是Inherit里面的prnmsg()函数。
~Inherit:41
~Base:11
~Base:11
为什么可以实现动态链接呢?
1.静态链接
在编译阶段将函数的定义和函数的调用关联;
2.动态链接
在程序运行时将函数的定义和函数的调用关联;
3.虚函数表
为什么可以实现动态链接:虚函数表,定义虚函数就会给虚函数表开空间,8bit(64).
这是因为,在父类和子类的内部结构中,有一个东西叫做虚函数表指针vfptr,即 virtual function pointer的意思。这个虚函数表指针是用于指向虚函数表vftable,而虚函数表的作用就是记录虚函数的函数入口地址。
覆盖、重载、隐藏
1.重载
1)概念
函数名相同但参数不同(个数、类型、个数类型都不同)
2)特点
A、相同的范围(在同一个作用域中)
B、函数名字相同
C、参数不同
D、virtual 关键字可有可无
E、返回值可以不同
2.覆盖(重写)
1)概念
子类重写基类的接口
2)特点
A、不同的作用域(分别位于派生类与基类)
B、函数名字相同
C、参数相同
D、基类函数必须有virtual关键字,不能有static
E、返回值相同 F、重写函数的权限访问限定符可以不同
3.隐藏
A、不在同一个作用域(分别位于派生类与基类)
B、函数名字相同
C、返回值可以不同
D、参数不同。此时,不论有无 virtual
关键字,基类的函数将被隐藏(注意与重载的区别)
E、参数相同,但是基类函数没有
virtual关键字。此时,基类的函数被隐藏(注意与覆盖的区别)
注意:
我们知道虚函数之后,建议基类的析构函数写成这样virtual ~Base()
我们比较一下虚析构函数与析构函数跟直观的了解
#include <iostream>
using namespace std;
class Base{
public :
Base(int val=0):value(val)
{
cout << __func__ << ":" << __LINE__ << endl;
}
virtual ~Base()
{
cout << __func__ << ":" << __LINE__ << endl;
}
public :
void setval(int val)
{
this->value = val;
}
int getval() const
{
return this->value;
}
void prnmsg()
{
cout << __func__ << ":" << __LINE__ << endl;
}
private:
int value;
};
class Inherit : public Base
{
public:
Inherit(int val = 0):myval(val), Base(val)
{
cout << __func__ << ":" << __LINE__ << endl;
}
~Inherit()//默认为虚析构
{
cout << __func__ << ":" << __LINE__ << endl;
}
public:
void prnmsg()
{
cout << __func__ << ":" << __LINE__ << endl;
}
private:
int myval;
};
void test(Base &obj)
{
obj.prnmsg();//是静态连接,即在编译时根据类型决定调用的
}
int main()
{
Base a;
//因为此语句是静态联编的,编译到此处时,
//编译器无法获知指针base实际指向的对象究竟为哪个类型,
//它只会根据base的类型是Base*来决定调用Base类的析构函数。
//所以我们给基类的析构函数声明为虚函数即可解决。
Base *p = new Inherit;
delete p;//根据对象决定调用Inherit的析构函数,子函数时会自动析构基类对象
//Inherit b;
//test(a);
// test(b);
return 0;
}
虚析构函数的运行结果:
Base:7
Base:7
Inherit:37
~Inherit:41
~Base:11
~Base:11
析构函数的结果:
Base:7
Base:7
Inherit:37
~Base:11
~Base:11
所以要记得用虚析构函数。析构函数容易造成内存泄露。
注意:
下面三点来自CSDN博主「司徒若寒」原文链接:https://blog.csdn.net/qq_26501341/article/details/1160713941.只要基类的析构函数是虚函数,那么派生类的析构函数不论是否用virtual关键字声明,都自动成为虚析构函数,即使派生类的析构函数与基类的析构函数名字不相同。
2.一般来说,一个类如果定义了虚函数,则最好将析构函数也定义为虚函数。
3.析构函数可以是虚函数,但构造函数不能声明为虚函数,这是因为在执行构造函数时类对象还未完成建立过程,当然谈不上函数与类对象的绑定。