什么是多态性?
多态:相同对象收到不同消息或不同对象收到相同消息时产生不同的动作。C++支持两种多态性:编译时多态性,运行时多态性。
- 编译时多态性:通过重载函数实现
- 运行时多态性:通过虚函数实现。
早绑定 vs 晚绑定
多态与非多态的实质区别就是函数地址是早绑定(静态多态)还是晚绑定(动态多态)。如果函数的调用,在编译器编译期间就可以确定函数的调用地址,并生产代码,是静态的,就是说地址是早绑定的。而如果函数调用的地址不能在编译器期间确定,需要在运行时才确定,这就属于晚绑定。
当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。
C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。简单概括为“一个接口,多个方法”。
对于同一条命名,不同对象接到后所做出的动作是不同的,这就叫多态。
#include<iostream>
#include<stdlib.h>
#include "Circle.h"
#include "Rect.h"
using namespace std;
int main()
{
Shape *shape1 = new Rect(3, 6);
Shape *shape2 = new Circle(5);
shape1->calcArea();
shape2->calcArea();
delete shape1;
shape1 = NULL;
delete shape2;
shape2 = NULL;
system("pause");
return 0;
}
普通虚函数 vs 虚析构函数
普通虚函数:在类的成员函数前加virtual关键字,并在派生类中重新定义的成员函数,其作用是:当父类和子类定义有相同的成员函数,用父类的指针指向子类的对象时能够调用子类的该成员函数。因为若不加virtual关键字,那么调用的就是父类中与之同名的成员函数。
(与之区别的是隐藏,隐藏发生在子类的对象调用子类中的成员函数时而隐藏掉其父类中同名的成员函数。)
虚析构函数:在析构函数前加virtual关键字,其作用是:用父类的指针指向子类的对象并在子类中开辟一段内存时,在销毁内存时由于delete后面跟的是父类的对象,故只调用父类的析构函数而不调用子类的析构函数,从而使得子类中的内存无法释放。而虚析构函数就解决了这种内存泄漏问题。
#ifndef SHAPE_H
#define SHAPE_H
class Shape
{
public:
Shape();
virtual ~Shape();//虚析构函数
virtual double calcArea();//加virtual关键字,虚函数
};
#endif
多态中容易出现的一个问题就是:内存泄漏
若构造函数里没有从堆中申请内存,那么也就不需要在析构函数中进行释放内存操作,也就是说若构造函数什么也不做(只是打印刷存在感)的话,调用与不调用都一样
class Circle :public Shape
{
public:
Circle(double r );
~Circle();
virtual double calcArea();
protected:
double m_dR;
Coordinate m_pCenter;//在Circle类中定义一个指向圆心坐标的指针
};
在Circle类中定义一个指向圆心坐标的指针,则需要在Circle类的构造函数中申请内存,并且在析构函数中释放内存,从而保证内存不泄漏。
当用父类指针指向子类对象并对操作子类对象的虚函数时,是没问题的,但想借助父类指针去销毁子类对象的内存时就会出现问题,因为delete后面如果跟的是父类的指针,则只会调用父类的析构函数,若跟的是子类的指针,则既调用子类的析构函数也调用父类的析构函数。
而对于多态问题,就是用父类的指针去指向子类对象,并对子类对象进行操作,但释放的是父类的指针,此时并不调用子类的析构函数,也就无法释放子类中申请的内存,造成内存泄漏。
为解决此问题就要用到虚析构函数
即用virtual去修饰析构函数(注意是在父类的析构函数前加virtual,子类析构函数可加可不加,不加时系统会自动加上,但推荐都加上,以防子类被其他类继承变成新的父类)
class Shape
{
public:
Shape();
virtual ~Shape();//虚析构函数
virtual double calcArea();
};
Circle::Circle(double r)
{
m_pCenter = new Coordinate(x, y);
m_dR = r;
cout << "Circle()" << endl;
}
Circle::~Circle()
{
delete m_pCenter;
m_pCenter = NULL;
cout << "~Circle()" << endl;
}
virtual在修饰函数时的一些限制:
- 不能修饰普通函数(全局函数),也即该函数必须是某个类的成员函数,否则编译出错
- 不能修饰静态函数,
- 不能修饰内联函数,若virtual修饰inline,则系统会忽略掉inline关键字,使内联函数变成虚函数,
- 不能修饰构造函数
函数指针
函数的本质就是一段二进制的代码写进内存中。可以通过一个指针指向这段代码的开头,计算机就会从开头一直执行下去,直到结尾。
先定义一个Shape类
class Shape
{
public:
virtual double calcArea()
{
return 0;
}
protected:
int m_iEdge;
};
再定义一个Circle子类,其中并没有定义计算面积的函数,而是利用Shape中的虚函数来计算面积。那么此时虚函数是如何来实现的呢?为了弄懂这个问题,先看一个概念:虚函数表
class Circle :public Shape
{
public:
Circle(double r);
private :
double m_dR;
};
虚函数表
C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术等。
覆盖 vs 隐藏 vs 重载
- 隐藏:当父类与子类出现同名的函数,实例化子类对象,并去调用该函数时只会调用子类中定义的函数而不会去调用父类中的同名函数,这就叫函数的隐藏(即指派生类的函数屏蔽了与其同名的基类函数)。当然,若一定要调用父类中的同名函数只需要在函数名前加上父类名就可以了。
- 覆盖:C++多态性是通过虚函数来实现的,虚函数允许子类重新定义成员函数,而子类重新定义父类的做法称为覆盖(override),或者称为重写。重写的话可以有两种,直接重写成员函数(比如隐藏就是这种情况)和重写虚函数(虚函数一般定义在父类中,子类中virtual关键字可加可不加,一般加上为好,以防子类被其他类继承),只有重写了虚函数的才能算作是体现了C++多态性。
如何区分覆盖和隐藏:
如果基类中的函数和派生类中的两个名字一样的函数f
满足下面的两个条件
(a)在基类中函数声明的时候有virtual关键字
(b)基类中的函数和派生类中的函数一模一样,函数名,参数,返回类型都一样。
那么这就是叫做覆盖(override),这也就是虚函数,多态的性质
那么其他的情况呢??只要名字一样,不满足上面覆盖的条件,就是隐藏了。
- 重载:重载则是同一个域中允许有多个同名的函数,而这些函数的参数列表不同,允许参数个数不同,参数类型不同,或者两者都不同。编译器会根据这些函数的不同列表,将同名的函数的名称做修饰,从而生成一些不同名称的预处理函数,来实现同名函数调用时的重载问题。但这并没有体现多态性。
所以,相同的函数名的函数,在基类和派生类中的关系只能是覆盖或者隐藏。
RTTI
运行时类型识别(Run-Time Type Identification),它提供了运行时确定对象类型的方法。
其中有两个重要的运算符:typeid 和 dynamic_cast
- typeid的name函数用于查看对象类型
typeid(*pHuman) == typeid(Japanese)
typeid注意事项:
1 返回一个type-info对象的引用
2 若想通过基类指针获得派生类的数据类型,基类必须带有虚函数
3 只能获取对象的实际类型 - dynamic_cast 常用于从多态编程基类指针向派生类指针的向下类型转换。它有两个参数:一个是类型名;另一个是多态对象的指针或引用。其功能是在运行时将对象强制转换为目标类型并返回布尔型结果。(即编译器无法验证是否发生正确的转换)
dynamic_cast<Japanese*>(pHuman)
dynamic_cast注意事项:
1 转换的类型只能是指针或引用,而不能是对象本身
2 要转换的类型中必须包含虚函数
3 转换成功返回子类地址,失败返回NULL。
纯虚函数
纯虚函数:没有函数体,且在虚函数名后面加=0
抽象类:包含纯虚函数的类,由于抽象类包含了没有定义的纯虚函数,所以不能定义抽象类的对象。
接口类:仅包含纯虚函数的类。即无数据成员只有成员函数且成员函数均为纯虚函数。
接口类更多的表达的是一直能力或协议。