09 多态

9.1 虚函数和多态

多态:前面有virtual关键字的成员函数就是虚函数。声明的时候加virtual,定义的时候不用。

构造函数和静态成员函数不能是虚函数。

多态的两种表现形式

  • 通过基类指针调用基类和派生类的同名虚函数时: 1. 如果该指针指向一个基类的对象,那么被调用的是基类的虚函数 2. 如果该指针指向一个派生类的对象,那么被调用的是派生类的虚函数

  • 通过基类引用调用基类和派生类的同名虚函数时: 1. 如果该引用引用的是一个基类的对象,那么被调用的是基类的虚函数 2. 如果该引用引用的是一个派生类的对象,那么被调用的是派生类的虚函数

派生类中和基类中同名同参数表的函数,不加virtual也自动成为虚函数。

注意在成员函数中调用虚函数是多态,但是在构造或析构函数中调用虚函数则不是多态。

9.2 多态的实现原理

  • 多态的关键在于通过基类指针或引用调用一个虚函数时,编译时不确定到底调用的是基类还是派生类的函数,运行时才确定。这叫”动态联编“。

每一个有虚函数的类(或者有虚函数的类的派生类)都有一个虚函数表,该类的任何对象中都放着指向虚函数表的指针。虚函数表中列出了该类的虚函数的地址。由此可见,使用多态机制会有额外的时空开销。

9.3 虚析构函数

通过基类的指针delete派生类的对象时,只会调用基类的析构函数,我们想让派生类的析构函数先调用,然后再调用基类的析构函数。

解决办法:把基类的析构函数声明为虚函数

  • 这时派生类的析构函数不加virtual也是虚的

  • 通过基类的指针delete派生类的对象时,首先调用派生类的析构函数,然后调用基类的析构函数

一般来说,一个类中定义了虚函数,则应该把它的析构函数定义成虚函数。或,一个类打算作为基类,也应该将析构函数定义为虚函数。所以推荐使用虚析构函数。但是注意构造函数不能写成虚的。

class son{
public:
virtual ~son(){cout << "bye son" << endl;}
};
class grandson:public son{
public:
~grandson(){cout << "bye grandson" << endl;}
};
int main(){
son *p;
p = new grandson();
delete p;
return 0;
}

会输出

bye grandson
bye son

如果son的析构函数不是虚函数,则只会输出bye son

9.4 纯虚函数和抽象类

纯虚函数:没有函数体的虚函数。(花括号都没有) 抽象类:包含纯虚函数的类。

  • 抽象类只能作为基类来派生新类使用,不能创建抽象类的对象

  • 抽象类的指针和引用可以指向抽象类派生出来的类的对象

在抽象类的成员函数中可以调用纯虚函数,但是在构造函数或析构函数中不能调用纯虚函数。因为在成员函数中的当前对象一定是派生类的对象。见下面的例子。

如果一个类从抽象类派生而来,那么当且仅当它实现了基类中的所有纯虚函数,它才能成为非抽象类。

class A{
public:
virtual void f() = 0; // 纯虚函数
void g() {
this->f(); // 这里的this必然指向的是派生类的对象
}
};
class B:public A{
public:
void f(){
cout << "B:f()" << endl;
}
};
int main(){
B b;
b.g();
return 0;
}