一、C++基础之类的多态
1.1、多态的含义
如果有几个上似而不完全相同的对象,有时人们要求在向它们发出同一个消息时, 它们的反应各不相同,分别执行不同的操作。这种情况就是多态现象。
C++中所谓的多态(polymorphism)是指,由继承而产生的相关的不同的类,其对象对同一消息会作出不同的响应。
多态性是面向对象程序设计的一个重要特征,能增加程序的灵活性。可以减轻系统 升级,维护,调试的工作量和复杂度。
这才是面向对象真正实用的东西,其实前面类的封装、和继承在C语言中都可以实现,但是多态是属于面向对象的一个特点。
1.2、多态发生的条件
我们先来看一段代码:
#include <iostream>
using namespace std;
class Student
{
public:
void doing(void)
{
cout << "学习数学中" << endl;
}
private:
};
class Sporter :public Student
{
public:
void doing(void)
{
cout << "打球" << endl;
}
private:
};
void doing(Student &s)
{
s.doing();
}
int main(void)
{
Student s1;
Sporter t1;
doing(s1);
cout << "-----------------------" << endl;
doing(t1);
return 0;
}
这段代码中,我们定义一个学生类和一个体育生类,体育生继承学生,然后打印他们此刻正在做的事情。
前面我们学习继承的时候可以知道,父类的引用可以传递子类的对象,我们来看一下运行结果。
我们想要输出的是这两个学生都在干嘛,但是我们发现最终都是输出学生正在学习,也就是父类的doing函数,那么这时候我们因该如何改变来达到我们的目的呢?
在父类中想要执行的函数加上关键字virtual,例如:
class Student
{
public:
virtual void doing(void)
{
cout << "学习数学中" << endl;
}
private:
};
但是为了方便阅读,建议在子类中的需要执行的函数中也添加这个关键字,这个关键字在子类中是否添加都不会影响到,重点是要在父类中关键函数添加virtual。
class Sporter :public Student
{
public:
virtual void doing(void)
{
cout << "打球" << endl;
}
private:
};
之后我们在运行这个代码。
发现得到我们想要的结果了,这就是多态,我可以传递不同的对象去执行不同的函数,来达到我想要的目的。
通过这个,总结一下发生多态的三个条件:
1、需要有继承。
2、需要有虚函数(添加virtual)。
3、父类指针或者引用指向子类对象。
使用多态的意义:
可以一劳永逸,无线拓展。
1.3、虚析构函数
构造函数不能是虚函数。建立一个派生类对象时,必须从类层次的根开始,沿着继承路径逐个调用基类的构造函数。
析构函数可以是虚的。虚析构函数用于指引delete 运算符正确析构动态对象 。
例如:
#include <iostream>
using namespace std;
class Student
{
public:
virtual void doing(void)
{
cout << "学习数学中" << endl;
}
virtual ~Student()
{
cout << "~Student()" << endl;
}
private:
};
class Sporter :public Student
{
public:
virtual void doing(void)
{
cout << "打球" << endl;
}
~Sporter()
{
cout << "~Sporter()" << endl;
}
private:
};
void doing(Student *s)
{
delete s;
}
int main(void)
{
Sporter *s1 = new Sporter;
doing(s1);
return 0;
}
这个是在析构函数前面加上virtual关键字的运行结果:
没有添加virtual关键字:
class Student
{
public:
virtual void doing(void)
{
cout << "学习数学中" << endl;
}
~Student()
{
cout << "~Student()" << endl;
}
private:
};
这个是没有在父类的析构函数加上virtual的运行结果:
从这里我们就能对比出来,如果在父类中的析构函数中没有添加上虚函数virtual这个关键字,在调用父类指针指向的子类对象时候,他并没有调用子类对象的析构函数,那么这就有可能导致内存泄漏,就是没法调用子类析构函数将子类申请的内存释放,所以一般都建议父类的析构函数都添加上virtual成为虚函数。
1.4、重载、重写、重定义
1、重载(添加):
a 相同的范围(在同一个类中)
b 函数名字相同
c 参数不同
d virtual关键字可有可无
2、重写(覆盖) 是指派生类函数覆盖基类函数,特征是:
a 不同的范围,分别位于基类和派生类中
b 函数的名字相同
c 参数相同
d 基类函数必须有virtual关键字
3、重定义(隐藏) 是指派生类的函数屏蔽了与其同名的基类函数,规则如下:
a 如果派生类的函数和基类的函数同名,但是参数不同,此时,不管有无virtual,基类的函数被隐藏。
b 如果派生类的函数与基类的函数同名,并且参数也相同,但是基类函数没有vitual关键字,此时,基类的函数被隐藏。
1.5、多态实现的原理
1、虚函数表和vptr指针
a 当类中声明虚函数时,编译器会在类中生成一个虚函数表;
b 虚函数表是一个存储类成员函数指针的数据结构;
c 虚函数表是由编译器自动生成与维护的;
d virtual成员函数会被编译器放入虚函数表中;
e 存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr指针)。
其实说白一点,就是如果类中定义有虚函数,那么编译器会开辟一个虚函数表,然后将vptr指针指向这个表,父类有自己的虚函数表,子类也有自己的虚函数表,当我们调用最终的时候是调用vptr这个表里面的函数,实际上十指向虚函数表里面的函数,这样就可以达到不同对象调用不同的函数了。
2、如何证明vptr的存在
我们看一下这段代码:
#include <iostream>
using namespace std;
class Student
{
public:
void doing(void)
{
cout << "学习数学中" << endl;
}
private:
int a;
};
class Student2
{
public:
virtual void doing(void)
{
cout << "学习数学中" << endl;
}
private:
int a;
};
int main(void)
{
cout << "student 大小:" << sizeof(Student) << endl;
cout << "Student2 大小:" << sizeof(Student2) << endl;
return 0;
}
这里定义了两个类,其中一个类是存在虚函数的,另外一个类是不存在虚函数的,前面我们证明过函数是不在类空间内的,如果类中没有vptr指针,那么理论上应该只有一个整型变量a,也就是大小为4.
我们看一下运行结果:
但是我们发现存在虚函数的类大小是8多了4个字节,而这个4个字节就是vptr的大小。
3、vptr指针的分布初始化
讲到这里我们就会有一个疑惑,能否在父类的构造函数里面调用虚函数,我们写个代码测试一下:
#include <iostream>
using namespace std;
class Student
{
public:
Student()
{
doing();
}
virtual void doing(void)
{
cout << "学习数学中" << endl;
}
private:
};
class spoerter :public Student
{
public:
spoerter()
{
doing();
}
virtual void doing(void)
{
cout << "运动中" << endl;
}
private:
};
int main(void)
{
spoerter s1;
return 0;
}
运行结果:
我们只创建spoerter这个类的对象,那么为什么会输出父类的构造函数呢?
因为在我们创建子类的时候,也会调用父类的构造函数,但是这里父类的构造函数输出的却是自己的doing这个函数,后面那个才是在子类构造函数中的doing,他不是虚函数么?为什么没法实现呢?
我们来看这一张图:
首先子类创建vptr指针的时候,在调用父类构造函数,他是指向父类的虚函数表,只有到了子类的构造函数执行的时候,才会把vptr指针指向子类的虚函数表,所以里有个vptr分布初始化的过程。
所以我们一般不建议在构造函数里面进行一些任务的执行,我们只需要在里面初始化成员变量即可。
1.6、纯虚函数和抽象类
在java(我没学过java,听老师讲的)中,存在接口这么一说,但是C++中并没有接口,它存在的是一个叫做 纯虚函数 的概念。
1、基本概念
纯虚函数是一个在基类中说明的虚函数,在基类中没有定义,要求任何派生类都定义自己的版本,纯虚函数为个派生类提供一个公共界面(接口的封装和设计、软件的模块功能划分)。
2、纯虚函数的格式
我们举个例子:
#include <iostream>
using namespace std;
class Graphics
{
public:
virtual void get_area(void) = 0;
private:
};
int main(void)
{
Graphics g1;
return 0;
}
我们并没实现纯虚函数的定义,所以实际上编译器给我们提示出错了。
他在这个过程中只是占用一个位置,但是因为没有实现,当你通过类创建对象时,就可以调用这个函数,可是实际上这个函数还没有意义,所以编译器报错了。
我们来看下面这个例子。
#include <iostream>
using namespace std;
class Graphics
{
public:
virtual void get_area(void) = 0;
private:
};
//三角形
class Triangle :public Graphics
{
public:
Triangle(int w,int h)
{
this->w = w;
this->h = h;
}
virtual void get_area(void)
{
cout << "三角形面积:" << w*h / 2 << endl;
}
private:
int w;
int h;
};
//长方体
class Cuboid :public Graphics
{
public:
Cuboid(int w, int h)
{
this->w = w;
this->h = h;
}
virtual void get_area(void)
{
cout << "长方体面积:" << w*h << endl;
}
private:
int w;
int h;
};
int main(void)
{
Graphics *pg = NULL;
Triangle t1(10,20);
Cuboid c1(10, 20);
pg = &t1;
pg->get_area();
pg = &c1;
pg->get_area();
return 0;
}
我们定义一个图形总类,定义一个获取面积的纯虚函数。
运行结果:
通过虚函数,只要我父类的纯虚函数未改变,那么我可以创建无数个子类,这样我就可以达到同一个功能,不同子类的实现,这就相当于抽象出来一个接口,我不管你如何实现,我最终调用那个函数,就能执行到我想要的结果。
这样的好处就是我做开发的时候,我可以不用动我之前写过的代码,只需要重新写一个类,按照父类的定义来写,这样就可以扩充我代码框架的内容,维护起来也很容易。
1.7、自己的一些总结
写到这一篇,C++的基础就已经告一段落了,这是篇文章我是参考这个文件来写的《轻松搞定C++语言.pdf》这个文档适合有C语言基础的同学,然后扩充来写,学习C++会很快入手。
有兴趣的同学可以去下载来看看,这篇文章总结的很到位,学习起来相对容易,下载地址:
链接:https://pan.baidu.com/s/17Ael6VXhLxfxfLm8lv_Fcw
提取码:scd3