假期 2020.02.06
学习资源来源于中国MOOC以及c语言中文网
前言
c++之所以是面向对象的是因为该语言既支持类也支持多态,而多态是什么呢?
定义
多态就是同一个操作作用于不同的对象会产生不同的结果。这个操作一般指的是函数的调用等等。
标准定义是对于通过基类指针调用基类和派生类中都有的同名、同参数表的虚函数的语句,编译时并不确定要执行的是基类还是派生类的虚函数;而当程序运行到该语句时,如果基类指针指向的是一个基类对象,则基类的虚函数被调用,如果基类指针指向的是一个派生类对象,则派生类的虚函数被调用。这种机制就叫作“多态(polymorphism)”。
虚函数:在成员函数前加virtual
即可生成虚函数。注意虚函数只能在类的内部定义成员函数时使用,不能在类外部写成员函数时使用,构造函数与静态成员都不能是虚函数。
多态类:包含虚函数的类称为“多态类”。
如何构成多态
常判断条件是
- 有继承关系
- 继承关系中有同名的虚函数,并且是覆盖关系
- 存在基类的指针,指向派生类的虚函数,或者引用
那么什么时候声明虚函数呢?
- 成员函数所在的类是否是基类
- 成员函数在继承后是否会被新定义或者更改,是的话,需要;否则,不需要。
多态应用
举一个指针类型的例子:
注意:多态的语句调用哪个类的成员函数是在运行时才能确定的,编译时不能确定。多态的函数调用语句被称为是“动态联编”,而普通的函数调用语句是“静态联编”。
上述程序是通过指针调用的虚函数,而引用也可以实现该功能。如果引用的是基类的对象,则调用的是基类的虚函数,若引用的是派生类的对象,那么调用的是派生类的虚函数。还需注意的是通过基类的指针只能访问从基类继承过去的成员,不能访问派生类新增的成员,即另外添加的函数或者变量成员。
再看一个引用类型的例子
在基类中没有添加virtual
会发现调用的时候,两个都是基类的成员函数,根本没有访问派生类的成员函数。显然,通过基类指针只能访问派生类的成员变量,但是不能访问派生类的成员函数。为了消除这种困境,让基类指针能够访问派生类的成员函数,C++ 增加了虚函数。
再看修改后的引用结果
这里用到一个简便方式,如果派生类中出现覆盖函数,那么只需要在基类中将被覆盖函数定义为虚函数即可,那么编译时会默认派生类中的函数也为虚函数。
注意:
- 当基类中有虚函数,但是派生类没有定义覆盖函数,那么调用该函数时,将使用基类的虚函数。
- 构造函数不能是虚函数。对于基类的构造函数,它仅仅是在派生类构造函数中被调用,这种机制不同于继承。
- 析构函数可以声明为虚函数。
对于为什么构造函数不能设置成虚函数的原因,本贾尼·斯特劳斯特卢普(C++语言之父)说道:虚函数调用是在部分信息下完成工作的机制,允许我们只知道接口而不知道对象的确切类型。 要创建一个对象,你需要知道对象的完整信息。 特别是,你需要知道你想要创建的确切类型。 因此,构造函数不应该被定义为虚函数。
多态中的特殊例子:
以上程序中,因为class B中没有fun1,因此调用基类函数fun1,但fun1函数调用fun2函数使用的是B中的覆盖函数,与前面B的指针有关。
纯虚函数与抽象类
纯虚函数不含有函数实体,只有声明。在虚函数声明的结尾加上=0,表明此函数为纯虚函数。包含纯虚函数的类成为抽象类,因为他无法实例化,无法创建对象。
例如:
virtual 返回值类型 函数名(函数参数) = 0;
程序
#include<iostream>
using namespace std;
class A {
protected:
int a;
public:
virtual int aa() = 0;//纯虚函数
virtual void ab() = 0;
};
class B :public A {
protected:
int b1;
int b2;
public:
B(int _i,int _n):b1(_i),b2(_n){ }
int aa() {//定义两个虚函数的实体
return b1 * b2;
}
void ab() {
cout << "第二个虚函数被调用" << endl;
}
};
class C :public A {
protected:
int c;
public:
C(int _i):c(_i){ }
int aa() {//定义两个虚函数的实体
return c;
}
void ab() {
cout << "第二个虚函数被调用" << endl;
}
};
int main()
{
C t(2);
cout << t.aa() << endl;
t.ab();
B tq(2, 3);
cout << tq.aa() << endl;
tq.ab();
return 0;
}
执行结果
分析:A 类不需要被实例化,但也不能直接用来定义类的对象,但是它为派生类提供了“约束条件”,派生类必须要实现这两个函数,才能使用这两个函数。即如果一个类从抽象类派生而来,那么当且仅当它实现了基类中的所有纯虚函 数,它才能成为非抽象类。
简单说来就是基类只是提供一个简要的框架,具体内容需要派生类自己去完善自己需要的部分。
注意:
- 只要包含纯虚函数的类就都是抽象类
- 虚函数只能在类中才能被声明为纯虚函数
- 在抽象类的成员函数内可以调用纯虚函数,但是在构造函数或析构函数内部 不能调用纯虚函数。
几何体处理程序
#include<iostream>
using namespace std;
constexpr auto PAI = 3.14159265;
int MyCompare(const void* s1, const void* s2);
class shape {
public:
virtual double aera() = 0;//虚函数
virtual void print() = 0;
};
class retangle :public shape {
public:
double width, length;
double aera() { return width * length; }
void print() { cout << "retangle:" << aera() << endl; }
};
class circle :public shape {
public:
int r;
double aera() { return PAI* r* r; }
void print() { cout << "circle:" << aera() << endl; }
};
class triangle :public shape {
public:
int a, b, c;
double aera() {
double temp = (a + b + c) / 2;
return sqrt(temp * (temp - a) * (temp - b) * (temp - c));
}
void print() { cout << "triangle:" << aera() << endl; }
};
int MyCompare(const void* s1, const void* s2) {
double a1, a2;
shape** p1 = (shape**)s1; //s1,s2指向shape数组中的元素,数组元素的类型是shape *
shape** p2 = (shape**)s2; // 故 p1,p2都是指向指针的指针,类型为 shape **
a1 = (*p1)->aera(); // * p1 的类型是 shape * ,是基类指针,故此句为多态
a2 = (*p2)->aera();
if( a1 < a2 )
return -1;
else
return 1;
}
int main()
{
shape* sh[100];
retangle* ptr;
circle* pc;
triangle* pt;
int n;
cin >> n;
for(int i = 0; i < n; i++)
{
char c;
cin >> c;
switch (c) {
case 'R':
ptr = new retangle;
cin >> ptr->width >> ptr->length;
sh[i] = ptr;
break;
case 'C':
pc = new circle;
cin >> pc->r;
sh[i] = pc;
break;
case 'T':
pt = new triangle;
cin >> pt->a >> pt->b >> pt->c;
sh[i] = pt;
break;
}
}
qsort(sh, n, sizeof(shape*), MyCompare);
for (int i = 0; i < n; i++)
sh[i]->print();
return 0;
}
执行效果
分析:用基类指针数组存放指向各种派生类对象的指 针,然后遍历该数组,就能对各个派生类对象 做各种操作,是很常用的做法。
比较好的一个处理是:派生类中和基类中虚函数同名同参数表的函数,不加virtual也自动成为虚函数。
虚函数的访问权限
分析:存在编译错误的原因是私有成员不可访问,即使我们访问的是该函数的覆盖函数,因为语法检查是不考虑运行结果的。解决方案是将private
换成public
即可通过。
多态的实现原理
动态联编定义:“多态”的关键在于通过基类指针或引用调用 一个虚函数时,编译时不确定到底调用的是基类还 是派生类的函数,运行时才确定。
示例
我们会发现每一个输出的字节数中都比原来的大4(32位时),其实这是因为每一个虚函数类都有一个虚函数边,该类的任何对象中都放着虚函数边的指针,即存着表的地址。这列的虚函数暂时不用考虑内存空间,因为还没有进行实体定义。
多态的函数调用语句被 编译成一系列根据基类指 针所指向的(或基类引用 所引用的)对象中存放的 虚函数表的地址,在虚函 数表中查找虚函数地址,
并调用虚函数的指令。
虚析构函数
- 通过基类的指针删除派生类对象时,通常情况下只调用基类的析构 函数 。 但是,删除一个派生类的对象时,应该先调用派生类的析构函 数,然后调用基类的析构函数。
- 解决办法:把基类的析构函数声明为virtual 派生类的析构函数可以virtual不进行声明。通过基类的指针删除派生类对象时,首先调用派生类的析构函 数,然后调用基类的析构函数
- 一般来说,一个类如果定义了虚函数,则应该将析构函数也定义成 虚函数。或者,一个类打算作为基类使用,也应该将析构函数定义 成虚函数。
如果是一个基类,那通常情况下需要将析构函数定义成虚函数,这样子,他会先调用派生函数的析构函数,然后在调用基类析构函数,达到释放两者的目的。
来源:CSDN
作者:德林恩宝
链接:https://blog.csdn.net/qq_44116998/article/details/104194902