【cplusplus教程翻译】多态(Polymorphism)

发布时间 2023-05-25 15:02:18作者: xiaoweing

多态(Polymorphism)

学习本章之前,需要正确理解指针和继承,如果忘记下面表达式的含义,需要回顾之前的章节

基类指针(Pointers to base class)

继承的一个关键特性就是派生类的指针可以类型安全地转换成基类指针,多态就是利用这个简单通用特性的艺术

// pointers to base class
#include <iostream>
using namespace std;

class Polygon {
  protected:
    int width, height;
  public:
    void set_values (int a, int b)
      { width=a; height=b; }
};

class Rectangle: public Polygon {
  public:
    int area()
      { return width*height; }
};

class Triangle: public Polygon {
  public:
    int area()
      { return width*height/2; }
};

int main () {
  Rectangle rect;
  Triangle trgl;
  Polygon * ppoly1 = &rect;
  Polygon * ppoly2 = &trgl;
  ppoly1->set_values (4,5);
  ppoly2->set_values (4,5);
  cout << rect.area() << '\n';
  cout << trgl.area() << '\n';
  return 0;
}

main函数声明了两个基类指针,但是却被赋值成派生类对象的地址,这样的赋值是有效的,因为派生关系。
解引用也是有效的,获得的是指向的对象,上面的调用等效于ppoly1->set_values (4,5); rect.set_values (4,5);
但是由于指针的类型是基类类型(不是派生类类型),只有从基类继承的成员能够被访问,而不是派生类的成员,这也就是为什么上面的程序直接访问area,因为基类的指针访问不了area
如果area是基类的成员,而不是派生类的,那么就可以访问,但是问题是派生类的area有不同的实现所以没办法在基类里加一个公共版本。

虚成员

虚函数是指派生类可以重定义的成员函数,但是通过引用调用的属性还是保留?虚函数的声明语法就是在函数定义前加virtual关键字

// virtual members
#include <iostream>
using namespace std;

class Polygon {
  protected:
    int width, height;
  public:
    void set_values (int a, int b)
      { width=a; height=b; }
    virtual int area ()
      { return 0; }
};

class Rectangle: public Polygon {
  public:
    int area ()
      { return width * height; }
};

class Triangle: public Polygon {
  public:
    int area ()
      { return (width * height / 2); }
};

int main () {
  Rectangle rect;
  Triangle trgl;
  Polygon poly;
  Polygon * ppoly1 = &rect;
  Polygon * ppoly2 = &trgl;
  Polygon * ppoly3 = &poly;
  ppoly1->set_values (4,5);
  ppoly2->set_values (4,5);
  ppoly3->set_values (4,5);
  cout << ppoly1->area() << '\n';
  cout << ppoly2->area() << '\n';
  cout << ppoly3->area() << '\n';
  return 0;
}

在这个例子里,三个类都有同样的成员函数和成员变量
area成员函数在基类里被声明成虚函数,然后在每一个派生类里被重定义了,非虚成员函数也可以在派生类里被重定义,但是非虚函数不能通过基类的指针被访问到,例如:如果virtual关键字被移除,那么三个函数调用都会返回0,因为对于这个情况,调用的都是基类版本
因此,本质上来说,virtual关键字的作用是:允许派生类的成员和基类成员同名,但是可以通过基类的指针访问(注意C++都是通过类型进行调用的),更准确地说,就是上面这个例子提到的,指针的类型是基类,但是对象的类型是派生类
声明或者继承了一个虚函数的类就被称为多态类
尽管声明了一个虚函数成员,基类还是一个正常类,可以用来实例化对象

抽象基类(Abstract base classes)

抽象基类和上面例子里的基类很像,虚函数变成了纯虚函数,本质上是不提供函数定义,语法上是函数定义赋值为0

// abstract class CPolygon
class Polygon {
  protected:
    int width, height;
  public:
    void set_values (int a, int b)
      { width=a; height=b; }
    virtual int area () =0;
};
请注意,现在area没有任何定义,也就是纯虚函数,包含一个以上纯虚函数的类被称为抽象基类
抽象基类不能被用来实例化对象(**没有函数定义,没办法分配内存**),因此这个用法是不对的`Polygon mypolygon;   // not working if Polygon is abstract base class`
但是抽象基类也不是完全没用,可以用来定义指针类型发挥多态功能```c++
Polygon * ppoly1;
Polygon * ppoly2

当然也可以用来解引用,显然指针的对象一定是派生类,因为基类没办法实例化对象

// abstract base class
#include <iostream>
using namespace std;

class Polygon {
  protected:
    int width, height;
  public:
    void set_values (int a, int b)
      { width=a; height=b; }
    virtual int area (void) =0;
};

class Rectangle: public Polygon {
  public:
    int area (void)
      { return (width * height); }
};

class Triangle: public Polygon {
  public:
    int area (void)
      { return (width * height / 2); }
};

int main () {
  Rectangle rect;
  Triangle trgl;
  Polygon * ppoly1 = &rect;  // 本质上这里发生了类型转换,&rect是由无名对象返回值的
  Polygon * ppoly2 = &trgl;
  ppoly1->set_values (4,5);
  ppoly2->set_values (4,5);
  cout << ppoly1->area() << '\n';
  cout << ppoly2->area() << '\n';
  return 0;
}

这个例子里,类型不同但是相关的对象通过统一的指针类型被引用,由于虚函数的关系,对应的成员函数被调用,这个特性在某些情况下非常有用,例如抽象基类还可以使用this指针

// pure virtual members can be called
// from the abstract base class
#include <iostream>
using namespace std;

class Polygon {
  protected:
    int width, height;
  public:
    void set_values (int a, int b)
      { width=a; height=b; }
    virtual int area() =0;
    void printarea()
      { cout << this->area() << '\n'; }
};

class Rectangle: public Polygon {
  public:
    int area (void)
      { return (width * height); }
};

class Triangle: public Polygon {
  public:
    int area (void)
      { return (width * height / 2); }
};

int main () {
  Rectangle rect;
  Triangle trgl;
  Polygon * ppoly1 = &rect;
  Polygon * ppoly2 = &trgl;
  ppoly1->set_values (4,5);
  ppoly2->set_values (4,5);
  ppoly1->printarea();
  ppoly2->printarea();
  return 0;
}

多态对面向对象编程十分有用,当然,上面这个例子很简单,不一定要是局部对象,对象数组和new出来的对象也可以
下面是一个非常完善的例子

// dynamic allocation and polymorphism
#include <iostream>
using namespace std;

class Polygon {
  protected:
    int width, height;
  public:
    Polygon (int a, int b) : width(a), height(b) {}
    virtual int area (void) =0;
    void printarea()
      { cout << this->area() << '\n'; }
};

class Rectangle: public Polygon {
  public:
    Rectangle(int a,int b) : Polygon(a,b) {}
    int area()
      { return width*height; }
};

class Triangle: public Polygon {
  public:
    Triangle(int a,int b) : Polygon(a,b) {}
    int area()
      { return width*height/2; }
};

int main () {
  Polygon * ppoly1 = new Rectangle (4,5);
  Polygon * ppoly2 = new Triangle (4,5);
  ppoly1->printarea();
  ppoly2->printarea();
  delete ppoly1;
  delete ppoly2;
  return 0;
}

注意new那段代码,虽然声明的是基类指针,但是对象是派生类类型

总结

OOP三大特点:封装、继承和多态,靠的是private protect public virtaul关键字
封装通过private public实现,类外只能访问public成员,不能访问private和protected,
继承通过protected丰富了访问权限控制,考虑访问点位置:类内、派生类内、类外,考虑派生权限:private、protected、public,在考虑基类数据成员属性private、protected、public,结合多次派生,就能明白为什么需要三个层级
多态可以避免编译器只通过类型进行访问