欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

23. 多态性(虚函数)

程序员文章站 2022-04-21 23:30:34
...

画三种图:Circle(圆形)、 Rectangle(矩形)、 Ellipse(椭圆形)
共同的操作:render、move、resize
共同的数据:center
23. 多态性(虚函数)

shape是父类,
Rectangle是shape的子类, Square是Rectangle的子类
Ellipse是shape的子类, Circle是Ellipse的子类.
每个子类的render()和shape的render有每种联系.
代码大概的书写方式:

class XYPos { ... }; //x,y point

class Shape
{
  public:
    Shape();
    virtual ~Shape();
    virtual void render(); //加关键字virtual,表示将来Shape类的所有子类,如果重新写了render,名称一样参数表也相同,那么这个render()和子类里的render()就是有联系的. 
    void move(const XYPos&);
    virtual void resize();
  protected:
    XYPos center;
};

class Ellipse:public Shape
{
   public:
     Ellipse(float maj, float minr);
     virtual void render();//这个virtual可加可不加,若不加,仍然表示virtual.
   protected:
     float major_axis, minor_axis;  //长轴短轴
};

class Circle:public Ellipse
{
  public:
    Circle(float radius):Ellipse(radius,radius) {}
    virutal void render();
};

void render(Shape* p) //p是多态的
                      //绑定:调用一个函数的时候,究竟该调哪一个函数?叫做绑定.
                      //动态绑定:运行的时候才知道该调用哪一个函数,根据指针所指向的哪个对象来决定.
                      //静态绑定:调用的函数是确定的,编译的时候是确定的
                      //p的类型是Shape的指针,所以p有一个静态类型,就是Shape的指针. 同时还有一个动态类型, 当时指的那个对象的类型是什么.如果render函数是virtul,就表示是动态绑定,就要看他的动态类型. 如果不是virtual的,发生的就是静态绑定. 
{
  p->render();
}

void fun()
{
  Ellipse ell(10,20);
  ell.render();
  Circle circ(40);
  circ.render();
  render(&ell);

  //由于render是virtual,取决于render()动态绑定,动态绑定:运行的时候才知道该调用哪一个函数,根据指针所指向的哪个对象来决定.

  //virtual:对这个函数的调用,若是通过指针或引用,不能相信是什么类型,需要到运行的时候才知道。这个指针所指的对象是什么类型,再调那个类型.
  render(&circ);
}

拿一个子类的对象,当做父类来看待,叫做upcasting.

多态:相同对象收到不同消息或不同对象收到相同消息时产生的不同动作.

多态分为静态多态和动态多态.

//静态多态
class Rect
{
  public:
    int calcArea(int width);
    int calcArea(int width, int height); //他们函数名相同,参数个数不同,一看就是互为重载的两个函数.
};

int main()
{
   Rect rect;
   rect.calcArea(10);
   rect.calcArea(10,20);
   return 0;
}
//程序在编译阶段根据参数个数确定调用哪个函数,这种情况叫做静态多态(早绑定).

//动态多态(晚绑定)
为何要使用动态绑定?
eg: 比如计算面积。当给圆形计算面积时使用圆形面积的计算公式,给矩形计算面积时使用矩形面积的计算公式. 也就是说有一个计算面积的形状基类,圆形和矩形派生自形状类,圆形与矩形的类各有自己的计算面积的方法。可见动态多态是以封装和继承为基础的.

class Shape //形状类
{
  public:
    double calcArea()
    {
       cout << "calcArea" << endl;
    }
};

class Circle:public Shape //公有继承自形状类的圆形类
{
   public:
     Circle(double r);
     double calcArea(); 
   private:
     double m_dR;
};

class Circle:calcArea()
{
   return 3.14*m_dR*m_dR;
};

class Rect:public Shape //公有继承自形状类的矩形类
{
  public:
    Rect(double width, double height);
    double calArea();
   private:
     double m_dWidth;
     double m_dHeight;
};

double Rect::calcArea()
{
   return m_dWidth*m_dHeight;
}

int main()
{
   Shape *shape1 = new Circle(4.0);
   Shape *shape2 = new Rect(3.0,5.0);
   shape1->calcArea(); //父类的calcArea
   shape2->calcArea(); //父类的calcArea
   ......
   return 0;
}
//如果打印结果的话,以上程序结果会打印两行"calcArea", 因为调用的都是父类的calcArea函数,并不是我们想要的那样去分别调用各自的计算面积的函数. 如果想要实现动态多态则必须使用虚函数
//使用虚函数如下:
class Shape
{
  public:
    virtual double calcArea() { ... } //虚函数
    ... //其它部分
  private:
    ...
};

...

class Circle:public Shape
{
  public:
    Circle(double r);
    virtual double calcArea(); //此处的virtual不是必须的,如果不加,系统会自动加上。如果加上则会在后续的时候看的比较明显(推荐加上)
    ...
  private:
    ...
};

...

class Rect:public Shape
{
   Rect(double width, double height);
   virtual double calcArea();
   private:
     ...
};

关键字virtual->析构函数
之前使用virtual去修饰成员函数,这里使用virtual去修饰析构函数.

class Shape
{
  public:
    ...
    virtual ~Shape();//virtual去修饰析构函数
  private:
    ...
};

class Circle:public Shape
{
  public:
    virtual ~Circle();//与虚函数相同,此处virtual可以不写,系统会自动添加,建议写上
    ...
};
//父类指针指向的是哪个对象,哪个对象的构造函数就会先执行,然后执行父类的构造函数. 销毁的时候子类的构造函数也会执行.

多态的实现原理

虚函数表指针:类中除了定义的函数成员,还有一个成员是虚函数表指针(占四个基本内存单元), 这个指针指向一个虚函数表的起始位置, 这个表会与类的定义同时出现,这个表存放着该类的虚函数指针,调用的时候可以找到该类的虚函数表指针,通过虚函数表指针找到虚函数表,通过虚函数表的偏移找到函数的入口地址,从而找到要使用的虚函数.
当实例化一个该类的子类对象的时候,如果该类的子类并没有定义虚函数,但是却从父类中继承了虚函数,所以在实例化该类子类对象的时候也会产生一个虚函数表,这个虚函数表示子类的虚函数表,但是记录的子类的虚函数地址却是与父类是一样的。所以通过子类对象的虚函数表指针找到自己的虚函数表,在自己的虚函数表找到的要执行的函数指针也是父类的相应函数入口的地址.

多态的原理

如果我们在子类中定义了从父类继承来的虚函数,对于父类来说情况是不变的,对于子类来说它的虚函数表与之前的虚函数表是一样的,但是此时子类定义了自己的(从父类那继承来的)相应函数,所以它的虚函数表当中关于这个函数的指针就会覆盖掉原有的指向父类函数的指针的值,换句话就是指向了自己定义的相应函数。这样如果用父类的指针,指向子类的对象,就会通过子类对象当中的虚函数表指针找到子类的虚函数表,从而通过子类的虚函数表找到子类的相应虚函数地址,而此时的地址已经是该函数自己定义的虚函数入口地址,而不是父类的相应虚函数入口地址,所以执行的将会是子类当中的虚函数. 这就是多态的原理.

虚析构函数的实现原理

当我们在父类中通过virtual修饰析构函数之后,通过父类指针指向子类对象,通过delete接父类指针就可以释放掉子类对象.

执行完子类的析构函数就会执行父类的析构函数.

如果父类当中定义了虚析构函数,那么父类的虚函数表当中就会有一个父类的虚析构函数的入口指针,指向的是父类的虚析构函数,子类虚函数表当中也会产生一个子类的虚析构函数的入口指针,指向的是子类的虚析构函数,这个时候使用父类的指针指向子类的对象,delete接父类指针,就会通过指向的子类的对象找到子类的虚函数表指针,从而找到虚函数表,在虚函数表中找到子类的虚析构函数,从而得到子类的析构函数得以执行,子类的析构函数执行之后,系统就会自动执行父类的虚析构函数。这就是虚析构函数的实现原理.

class Flyable //会飞
{
  public:
   virtual void takeoff() = 0;//起飞
   virtual void land() = 0;//降落
};

class Bird:public Flyable
{
  public:
    ...
    virtual void takeoff() { ... }
    virtual void land() { ... }
  private:
    ...
};

void flyMatch(Flyable *a, Flyable *b) //飞行比赛
//要求传入一个会飞对象的指针,此时鸟类的对象指针可以传入进来
{
   ...
   a->takeoff();
   b->takeoff();
   a->land();
   b->land();
}