回顾cpp-多态-虚函数

封装->继承->多态

  • 普通虚函数
  • 虚析构函数 check
  • 纯虚函数
  • 抽象类
  • 接口类
  • RTTI
  • 异常处理
  • 隐藏 覆盖
  • 早绑定 晚绑定 check
  • 虚函数表

静态多态(早绑定)

函数重构

1
2
3
4
5
6
7
8
9
10
11
12
class Rect{
public:
int calcArea(int width);
int calcAera(int width, int height);
};

int main(){
Rect rect;
rect.calcAera(10);
rect.calcAera(10, 20);
return 0;
}

程序在运行之前,在编译阶段就已经确定下来要使用哪个calsAera()函数了。很早地就将函数编译进去了。此情况叫做早绑定,即静态多态

动态多态(晚绑定)

不同的对象下达相同的指令,做着不同的操作,为动态多态。有前提的:它必须以封装,继承为基础。

也就是说,有了封装继承之后,才能谈动态多态。动态多态至少由两个类,父子类,三个类时动态多态才表现的更加明显。

注意只有当一个类需要有多态的的性质时,才体现多态。此时的base classes需要将其析构函数声明为virtual。当没有声明virtual时,看下例:

父类:

1
2
3
4
5
6
7
8
9
10
11
class Shape{
public:
Shape(){}
~Shape(){
std::cout<<"~Shape()"<<std::endl;
}
double calcArea(){ // 不是虚函数
std::cout<<"Shape->calc area"<<std::endl;
return 0;
}
};

子类Circle:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Circle : public Shape{
public:
Circle(double r){
m_dR = r;
}
~Circle(){
std::cout<<"~Circle()"<<std::endl;
}
double calcArea(){ // 不是虚函数
return 3.14*m_dR*m_dR;
}
private:
double m_dR;
};

子类Rect:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Rect : public Shape{
public:
Rect(double width, double height){
m_dWidth = width;
m_dHeight = height;
}
~Rect(){
std::cout<<"~Rect()"<<std::endl;
}
double calcAera(){ // 不是虚函数
return m_dHeight*m_dWidth;
}
private:
double m_dWidth;
double m_dHeight;
};

含有多态性质的base class的设计目的是为了通过base class的接口来使用derived class,所以,如果用两个base class指针分别指向两个不同derived class,后分别调用两个derived class的计算面积方法:

1
2
3
4
5
6
7
8
9
10
Shape *shape1 = new Circle(1.0);     // 父类指针指向子类对象
Shape *shape2 = new Rect(3.0, 5.0); // 父类指针指向子类对象

cout<<shape1->calcArea()<<endl;
cout<<shape2->calcArea()<<endl;

delete shape1;
shape1 = NULL;
delete shape2;
shape2 = NULL;

结果并不是我们想要的:

1
2
3
4
5
6
Shape->calc area    //  调用父类方法
0
Shape->calc area // 调用父类方法
0
~Shape() // 调用父类析构函数
~Shape() // 调用父类析构函数

都调用了父类的calcArea()方法。且都调用了父类的析构函数。此现象称为隐藏.

WHY:我们不希望这样,我们希望通过父类指针可以调用子类的方法。如何解决?使用virtual 关键字。


解决方法:在父类中想要实现多态的函数前加virtual,同时在子类的相同函数前也加上virtual

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class Shape{
public:
...
virtual double calcArea(){ // 虚函数
std::cout<<"calc area..."<<std::endl;
}
...
};

class Circle : public Shape{
public:
...
double calcArea(){ // 虚函数
return 3.14*m_dR*m_dR;
}
...
};

class Rect : public Shape{
public:
...
double calcArea(){ // 虚函数
return m_dHeight*m_dWidth;
}
...
};

此时的结果:

1
2
3
4
3.14     // 调用Circle 的方法
15 // 调用Rect 的方法
~Shape() // 只调用父类析构函数
~Shape()

只有base class的同名函数被声明为virtual。

很自然的情况是:当使用derived class指针指向derived class对象时,不管是不是virtual,都会由正确执行结果:

1
2
3
4
5
6
7
8
...
Circle *circle1 = new Circle(1.0);
Rect *rect1 = new Rect(3.0, 5.0);

delete circle1;
circle1 = NULL;
delete rect1;
rect1 = NULL;

输出:

1
2
3
4
5
6
3.14
15
~Circle() // 既调用子类析构函数
~Shape() // 又调用父类析构函数
~Rect()
~Shape()

敲黑板

  • 父类子类要实现多态的方法前,父类要声明为virtual
  • 当父类的析构函数不是虚函数时,类指针指向子类对象时,只调用父类析构函数。
  • 当父类的析构函数不是虚函数时,delete 后跟父类指针,只执行父类析构函数,如果delete 后跟子类指针,执行父类和子类析构函数。
  • 只将要作为父类的类的析构函数声明为virtual

虚析构函数

为啥要有虚析构函数?如果要将这个class作为基类而且是有多态性质的基类,应该把其析构函数定义成virtual!看以下例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Circle : public Shape{
public:
Circle(double r, int x, int y){
m_pCenter = new Coordinate(x, y) // 2.实例化一个坐标对象
m_dR = r;
}
~Circle(){
delete m_pCenter; // 3.释放坐标对象
m_pCenter = NULL;
}
double calcArea(){
return 3.14*m_dR*m_dR;
}
private:
double m_dR;
Coordinate* m_pCenter; // 1.多一个坐标类属性
};

在析构函数中释放了坐标对象,不会内存泄露。可是!
在多态时:

1
2
3
4
5
Shape *shape1 = new Circle(1.0);    
cout<<shape1->calcArea()<<endl;

delete shape1; // 使用父类指针销毁子类对象
shape1 = NULL;

使用父类指针销毁子类对象时,只调用了父类析构函数~Shape(),而子类~Circle()未被调用。坐标对象不会被释放,此时内存泄漏。这就不合理了,虚构函数设计就是要被执行的

如何解决?
虚析构函数!在父类的析构函数前加virtual

1
2
3
4
5
6
7
8
virtual ~Shape(){            // 虚析构
...
}

~Circle(){
delete m_pCenter; // 释放坐标对象
m_pCenter = NULL;
}

此时,使用父类指针销毁子类对象时,既调用了父类析构函数~Shape(),又调用子类~Circle()。父类子类的析构函数都被执行。情理上就对了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Shape{
public:
virtual ~Shape(){ // 虚
std::cout<<"~Shape()"<<std::endl;
}
};

class Circle : public Shape{
public:
~Circle(){
std::cout<<"~Circle()"<<std::endl;
}
};

class Rect : public Shape{
public:
~Rect(){
std::cout<<"~Rect()"<<std::endl;
}
};

父类子类的析构函数都被执行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
    Shape *shape1 = new Circle(1.0);
Shape *shape2 = new Rect(3.0, 5.0);

delete shape1;
shape1 = NULL;
delete shape2;
shape2 = NULL;

OUTPUT:
3.14
15
~Circle()
~Shape()
~Rect()
~Shape()

执行完子类虚析构函数,再执行父类虚析构函数。

virtual使用限制

  • 普通函数不能是虚函数

  • 不能修饰静态成员函数

    1
    2
    3
    4
    class Animal{
    public:
    virtual static int func();
    };
  • 不能修饰inline函数,如果这么做了,计算机会忽略inline关键字,使之成为虚函数。

  • 不能修饰构造函数。

敲黑板
任何时候都应该为含有多态性质的基类(父类)声明virtual析构函数。如果一个class含有任何virual函数,就一定要有一个virtual析构函数。