Polymorphism
You are a shape. You know how to draw yourself. So do it!
子类型
对于 OOP 语言,子类 (Subclass) 和子类型 (Subtype) 是一样的。
任何需要父类参数的时刻,子类都可以被当作父类。这个时候将舍弃子类的独有方法和独有成员。
class Item{};
class DVD: public Item{};
class CD: public Item{};
void addDVD(DVD& v);
void addCD(CD& v);
void addItem(Item& v);
int main(){
DVD myDVD;
addItem(DVD);
}
造型
向上造型 (up-casting) 是指将一个对象当成其父类使用。这一般是通过用父类的指针 / 引用指向子类对象实现的。
造型不改变对象的内容和值,这使得它区别于类型转换。
Example
我们想实现一个画图的程序,包括圆形、长方形、正方形和椭圆。
先考虑公共部分,
方法:render, move, resize
数据: center
层次上,圆继承于椭圆,正方形继承于长方形。
为什么不是椭圆继承于圆?
在数学概念上,圆是椭圆,更特殊;在实现上,椭圆可以由圆添加一个参数得到,很方便。
这种情况下,对椭圆成立的性质对圆也成立,而我们应该在父类中维护公共的性质与方法,
所以椭圆应该作为父类,圆作为子类。
#include <algorithm>
#include <iostream>
using namespace std;
class XYpos{
public:
double x, y;
XYpos(double _x = 0, double _y = 0):
x(_x), y(_y){}
};
class Shape{
protected:
XYpos center;
public:
virtual void render(){cout << "Shape::render()" << i << endl;}
};
class Rectangle: public Shape{
private:
double r, c;
public:
void render(){cout << "Rectangle::render()" << i << endl;}
};
class Ellipse: public Shape{
private:
double a, b;
public:
void render(){cout << "Ellipse::render()" << i << endl;}
};
void draw(Shape *p){
p->render();
}
signed main(){
Rectangle r;
Ellipse e;
draw(&r); r.render();
draw(&e);
Shape *p = &e;
p->render();
}
多态变量
一个变量被声明的类型,就称为它的静态类型。对于编译器而言,只能检查变量静态类型的方法。
对于指针 / 引用,它指向的对象的类型称为它的动态类型。
这时它可能指向一个其静态类型的子类,所以指针和引用被称为多态变量(polymorphic variable)。
绑定
静态绑定
一般的函数调用都是静态绑定,使用时直接调用。只有 C++ 默认为静态绑定,这一点与其他编程语言不同。
动态绑定
virtual
关键字:告诉编译器,子类中会有该函数的别的版本,而调用哪个版本的决定推迟到运行时。
这种不确定的调用就是动态绑定。
使用动态绑定的两个条件:
- 指针 / 引用的对象是 polymoriphic variable。
- 调用的方法有
virtual
。
但如果 Shape
类型没有 render()
方法,检查 p->render()
时会报错。这是由于指针只检查其静态类型。
如果函数的返回值是对象,那么派生类不能重写为派生类对象;如果函数的返回值是引用或指针,那么派生类可以重写为派生类对象的引用或指针。
虚函数
virtual
声明的函数称为虚函数,它可以被子类中同名同参数的函数重写(override)。
如何工作:
对于非虚函数:只生成一条指令,调用唯一的版本。
对于虚函数:要决定调用的函数版本,引入了一个由对象确定的虚函数表。
一个父类的成员函数被声明为虚函数后,所有子类的同名函数都被隐式地声明为虚函数。
代码覆盖上面的 main()
signed main(){
Rectangle r;//define an object
long long **vptr = (long long**)(&r);//take its head address
//it works on the assumption that we know it's a pointer to somewhere
void (*fp)() = (void(*)())(**vptr);//convert the element on 'somewhere' to a function pointer
fp();
}
实测发现,fp()
调用了 Rectangle::render()
。
一旦类中有虚函数,其每一个对象的开头都会有一个指针 vptr
,指向该类的虚函数表。
对于子类的函数表,子类函数会覆盖父类函数的位置,这样在调用虚函数时就可以准确地调用。
如果用子类对象赋值父类,vptr
会一并修改吗?答案是否定的。
vptr
在构造的时候被确定。
重载函数
对于基类中的重载函数,在派生类中应该全部重载,否则未被重载的变体将不能调用。
在子类内部实现中,可以使用 using
语句来简化对父类成员的调用。
class Base { public: void f();};
class Child : public Base {
public:
using Base::f;
void func(){ f(); f();} // Base::f
};
虚析构函数
Shape *p = new Ellipse(100.0F, 200.0F);
delete p;
此时将执行两步:1. 调用 p->~Shape()
2. 调用 free(p)
。
为了能调用子类的析构,我们应该将父类的析构函数声明为虚函数。
从后续维护和拓展的角度考虑,所有的析构函数都应该声明为虚函数。
进一步,所有的类都应该存在 vptr
,这是 RTTI(RunTime Type Identification) 的基础。
构造函数
在构造函数中,仍然使用动态绑定。但是因为 vptr
在构造时确定,所以子类在执行父类构造函数时,vptr
指向父类的虚函数表,会默认调用父类的成员函数。
class A {
public:
A() {
f();
}
virtual void f() {
cout << "A::f()";
}
};
class B : public A {
public:
B() {
f();
}
void f() {
cout << "B::f()";
}
};
B temp;
输出可以发现,先调用了 A::f()
后调用了 B::f()
。
纯虚函数
对于公共、抽象的父类(例子中的 Shape
类),我们既不想费脑筋完成其某些功能 render()
,也不希望能够生成其对象。
那么我们就需要将成员函数定义为纯虚函数,这时候这个类就成为了抽象类。
抽象类不能制造对象。
class Shape{
protected:
XYpos center;
public:
virtual void render() = 0;
};
语句的实际意义:将虚函数表中对应的函数指针赋为 0。
多继承
C++ 是唯一的支持多继承的语言。原因:没有实现单根结构(BS 设计)
多继承的目的:实现容器(Ceil 容器)
菱形继承
A
同时被 B
、C
继承,D
继承 B
和 C
,那么 D
相当于继承了两次 A
。
那么当我们把 A
的指针指向 D
的对象时,就不知道应该指向 B::A
还是 C::A
,出现冲突。
同理,如果 D
访问 A
的成员时,不知道应该访问 B::A
还是 C::A
。
虚继承
通过在继承时添加 virtual
关键字实现。
虚继承时子类中不存在父类的对象,而是保有父类的指针。
class A {public: int m_i;};
class B: virtual public A {};
class C: virtual public A {};
class D: public B, public C{};
int main(){
D temp;
temp.m_i++; // Ok, only one A in temp
A* p = new D; // Ok
}
唯一应用方案:多继承时只有一个类是实际类(有成员变量的类),其他类只提供接口。
最优建议:别用
最终方案:模板