前言
本篇继续类继承的学习,虚函数
基类,派生类,指针
基类与派生类具有非常特殊的关系:
- 可以用派生类对象为基类对象赋值;
- 可以用派生类对象为基类对象初始化;
- 基类指针可以指向派生类对象;
- 基类引用可以绑定派生类对象。
上面四个关系反过来不成立。
#include <cstdio>
#include <iostream>
#include <cstring>
using std::ostream;
class Base{
private:
int a_;
double b_;
protected:
int extra_ = -1;
public:
Base();
Base(int, double);
Base(const Base&);
Base& operator= (const Base&);
~Base();
void print();
};
Base::Base(): a_(0), b_(0){
std::cout << "Base default constructor\n";
}
Base::Base(int a, double b): a_(a), b_(b){
std::cout << "Base constructor\n";
}
Base::Base(const Base& s): a_(s.a_), b_(s.b_){
std::cout << "Base copy constructor\n";
}
Base& Base::operator= (const Base& s){
std::cout << "Base overload = \n";
}
void Base::print(){
std::cout << a_ << " " << b_ << std::endl;
}
Base::~Base(){
std::cout << "Base destructor\n";
std::cout << a_ << " " << b_ << std::endl;
}
class Derived : public Base{
private:
int a_;
double c_;
public:
Derived();
Derived(int, double, int, double);
void print();
~Derived();
};
Derived::Derived(): a_(1), c_(1.){
std::cout <<"Derived default constructor\n";
}
Derived::Derived(int a0, double b0, int a1, double c)
:Base(a0, b0), a_(a1), c_(c) {
std::cout <<"Derived constructor\n";
}
void Derived::print(){
Base::print();
std::cout <<a_ << " " << c_ << " "
<< Base::extra_ << std::endl;
}
Derived::~Derived(){
std::cout << "Derived destructor\n";
}
用派生类对象为基类对象赋值,将调用基类的重载赋值运算符Base& Base::operator= (const Base&);
,派生类对象内嵌的基类对象作为参数。如果不手动提供,编译器会提供一个隐式的基类重载赋值运算符。
用派生类对象为基类对象初始化,将调用基类的复制构造函数Base::Base(const Base&);
,派生类对象内嵌的基类对象作为参数。如果不手动提供,编译器会提供一个隐式的基类复制构造函数。
Derived de;
Base ba = de; // ok!
de = ba; // error!
基类指针和引用可以关联派生类对象,但只能调用基类的方法。
Derived* pde = &ba; // error!
Base* pba = &de; // ok!
pba->print(); // ok!
pba->Base::print(); // error!
pba->Derived::print();
将报error: 'Derived' is not a base of 'Base'
,也就是说基类指针不能使用派生类的方法。
上面这些关系的核心在于派生类对象内嵌了一个基类对象,因此实际上是基类对象与派生类对象内嵌的基类对象的关系。
正因为上面这些关系,派生类对象可以作为以基类对象为形参的函数实参。
虚函数与多态
基类指针和引用可以关联派生类对象,但不能使用派生类的方法。
如果基类和派生类有一个同名同参数列表的函数,调用如下:
pba->print(); // use Base::print()
(*pba).print(); // use Base::print()
基类指针pba
虽然指向的是派生类对象,但通过基类指针使用的仍然是基类方法。
虚函数可以打破基类指针的限制,使得基类指针能够使用派生类方法。
虚函数,virtual关键字
类声明中,virtual
关键字用于声明一个类方法是虚函数:
virtual 返回类型 函数名(参数列表);
基类中声明虚函数后,派生类声明的同名同参函数也成为虚函数,派生类的虚函数可以不使用virtual
声明,但最好也用virtual
指出它是一个虚函数。
关键字virtual
和explicit
一样,只能在类声明中使用。
指针和引用调用普通成员函数时,程序将根据引用类型和指针类型选择方法;指针和引用调用虚函数时,程序将根据关联的对象的类型来选择方法。
典型例子是虚析构函数。上篇提到,派生类的构造函数先调用基类构造函数创建内嵌基类对象,再创建派生类对象;派生类的析构函数先销毁派生类对象,再调用基类的析构函数销毁基类对象。
void main(){
Base* pba = new Derived(1, 1.1, 2, 2.2);
delete pba;
}
/*
Base constructor
Derived constructor
Base destructor
*/
上面的示例中,析构函数不是虚函数,通过new
创建了派生类Derived
对象并与基类指针pba
关联,delete销毁派生类对象时,只调用了基类的构造函数,没有调用派生类的析构函数,出现了内存泄漏。
现在把析构函数声明为虚函数:
// class Base
virtual ~Base();
// class Derived
virtual ~Derived(); // can omit 'virtual'
// main
void main(){
Base* pba = new Derived(1, 1.1, 2, 2.2);
delete pba;
}
/*
Base constructor
Derived constructor
Derived destructor
Base destructor
*/
声明虚析构函数后,可以看到程序先调用派生类的析构函数,再调用基类的析构函数。
注意:尽量将基类和派生类的析构函数声明为虚析构函数。
多态
虚函数的一项重要目的是实现多态:调用的方法取决于调用该方法的对象。
我们知道,数组成员必须是同一类型的,但基类指针可以指向派生类对象,因此创建一个基类指针数组,数组成员就可以指向不同类型的对象:
Base** pbaa = new Base* [2];
*pbaa = new Base();
*(pbaa + 1) = new Derived();
for(int i=0; i<2; i++){
(*(pbaa + i))->print(); // 0 for Base::print(), 1 for Derived::print()
}
于是,程序在一个循环中根据指针指向的对象类型的不同而使用了不同的函数。
上面这个例子还不能完全体现多态的作用。
假如有一个基类Device
,派生出两个子类Computer
,MobilePhone
,派生类和子类都有同名函数power()
,但内部实现不同。
现在在外部有一个函数button(Device*)
,内部调用了power()
函数,目标功能是在按下按钮时传入的对象开机。
如果power()
不是虚函数,则对于Computer
, MobilePhone
,还需要重载button(Computer*)
, button(MobilePhone*)
来分别实现开机功能。因为power()
不是虚函数,即使给button(Device*)
传入派生类对象,调用的仍然是基类Device::power()
。
如果power()
是虚函数,则button(Device*)
将根据传入的对象类型调用对应的虚函数。
这就是多态性。虚函数可以实现动态多重继承的多态性。
后记
本篇学习了类继承中的虚函数与多态,下一篇将继续学习类继承中的动态内存使用。