虚函数是动态绑定的基础。虚函数必须是非静态的成员函数。虚函数经过派生之后,在类族中就可以实现运行过程的多态。
根据类型兼容规则,可以使用派生类的对象代替基类的对象。如果基类类型的指针指向派生类对象,就可以通过这个指针来访问该对象,但是访问到的只是从基类继承来的同名的函数成员。如果需要通过基类的指针指向派生类的对象,并访问某个与基类同名的成员,首先在基类中将这个同名函数声明为虚函数。这样,通过基类类型的指针,就可以使属于不同派生类的不同对象产生不同行为,从而实现运行过程的多态。
1.一般虚函数成员
(1)一般虚函数成员的声明语法是:
virtual 函数类型 函数名(参数表);
这实际上就是在类的定义中使用virtual
关键字来限定成员函数,虚函数声明只能出现在类定义中的函数原型声明中,不能出现在成员函数实现的时候。
运行过程中的多态需要满足3个条件:
(1)类之间满足类型兼容规则
(2)要声明虚函数
(3)要由成员函数来调用或者通过指针、引用来访问虚函数
【注意】虚函数一般不声明为内联函数,因为对虚函数的调用需要动态绑定,而对内联函数的处理是静态的,所以虚函数一般不能以内联函数来处理。但将虚函数声明为内联函数也不会引起错误,因为编译器会自动忽略。
(2)普通函数成员与虚函数成员的比较
①普通函数成员
#include<iostream>
using namespace std;
class A//基类A定义
{
public:
void display()const//声明基类A中的成员函数为普通函数
{
cout << "显示类A" << endl;
}
};
class B :public A//公有派生类B定义
{
public:
void display()const
{
cout << "显示类B" << endl;
}
};
class C :public B//公有派生类C定义
{
public:
void display()const
{
cout << "显示类C" << endl;
}
};
void fun(A* p)//参数为指向基类A的对象的指针
{
p->display();//"对象指针->成员名"
}
int main()
{
A a;//定义基类对象A
B b;//定义直接基类为A类的派生类B的对象
C c;//定义直接基类为B类的派生类C的对象
fun(&a);//用基类A的对象的指针调用fun函数
fun(&b);//用直接基类为A类的派生类B对象的指针调用fun函数
fun(&c);//用直接基类为B类的派生类C对象的指针调用fun函数
return 0;
}
运行结果:
分析:
上述程序中,虽然基类A的指针指向了派生类B,C的对象,但是fun函数运行时,通过这个指针只能访问到派生类B和C中从基类A继承下来的成员函数display,而不是派生类B和C中自身的的同名函数display。
②虚函数成员
class A//基类A定义
{
public:
virtual void display()const//声明基类A中的成员函数为虚函数
{
cout << "显示类A" << endl;
}
};
class B :public A//公有派生类B定义
{
public:
void display()const//覆盖基类的虚函数
{
cout << "显示类B" << endl;
}
};
class C :public B//公有派生类C定义
{
public:
void display()const//覆盖基类的虚函数
{
cout << "显示类C" << endl;
}
};
void fun(A* p)//参数为指向基类A的对象的指针
{
p->display();//"对象指针->成员名"
}
int main()
{
A a;//定义基类对象A
B b;//定义直接基类为A类的派生类B的对象
C c;//定义直接基类为B类的派生类C的对象
fun(&a);//用基类A的对象的指针调用fun函数
fun(&b);//用直接基类为A类的派生类B对象的指针调用fun函数
fun(&c);//用直接基类为B类的派生类C对象的指针调用fun函数
return 0;
}
运行结果:
分析:
程序中的A,B和C属于同一个类族,而且是通过公有派生而来的,因此满足类型兼容规则。同时基类A的函数成员声明为虚函数,程序中使用对象指针来访问函数成员。这样绑定过程就在运行过程中完成,实现了运行中的多态。通过基类类型的指针就可以访问到正在指向的对象的成员,这样就能够对同一类族中的对象进行统一处理,抽象程序更高,程序更加简洁、高效。
在本程序中派生类并没有显式给出虚函数的声明,这时系统就会遵循以下规则来判断一个函数成员是不是虚函数:
(1)该函数是否与基类的虚函数具有相同的名称
(2)该函数是否与基类的虚函数具有相同的参数个数及相同的对应参数类型
(3)该函数是否与基类的虚函数有相同的返回值或者满足赋值兼容规则的指针、引用型的返回值
如果从名称、参数、返回值3个方面检查之后,派生类的函数满足以上条件,就会自动确定为虚函数。这时,派生类的虚函数便覆盖了类的虚函数。不仅如此,派生类中的虚函数还会隐藏基类中同名函数的其他所有重载形式。
【注意】
①用指向派生类对象的指针仍然可以调用基类中被派生类覆盖的成员函数,方法是使用“::”进行限定。例如如果把上例中的fun函数改为以下形式,其他部分不改动:
void fun(A* p)//参数为指向基类A的对象的指针
{
p->A::display();//"对象指针->成员名"
}
运行结果:
可以看出,使用“::”进行限定之后,无论p所指向的对象的多态类型是什么,最终被调用的总是A类的display函数。在派生类的函数中,有时需要先调用基类被覆盖的函数,再执行派生类特有的操作,这时就可以使用“基类名::函数名(…)”来调用基类中被覆盖的函数。
②派生类覆盖基类成员函数时,既可以用virtual关键字,也可以不使用,二者没有差别。只要在基类中声明某成员函数是虚函数即可,派生类中的同名成员函数可以不声明为虚函数。有时候习惯于在派生类函数中也使用virtual关键字,因为这样可以清楚提示这是一个虚函数。
(3)基类的构造函数和析构函数对虚函数的调用
①当基类的构造函数调用虚函数时,不会调用派生类的虚函数。
假设有基类A和派生类B,两个类中有虚成员函数fun(),在执行派生类B的构造函数时,需要首先调用基类A的构造函数。如果A::A()调用了虚函数fun(),则被调用的是A::fun(),而不是B::fun()。这是因为当基类被构造时,对象还不是一个派生类对象。
同样,当基类被析构时,对象以及不再是一个派生类对象了,所以如果A::~A()调用了fun(),则被调用的时A::fun(),而不是B::fun()。
class A//基类A定义
{
public:
A()
{
fun();
cout << "调用基类A的默认构造函数" << endl;
}
A(int a):x(a)
{
fun();
cout << "调用基类A的构造函数" << endl;
}
virtual void fun()const//声明基类A中的成员函数为虚函数
{
cout << "显示类A" << endl;
}
~A()
{
cout << endl;
fun();
cout << "调用基类A的析构函数" << endl;
}
private:
int x;
};
class B :public A//公有派生类B定义
{
public:
B(){}
B(int b) :y(b)
{
cout << "调用派生类B的构造函数" << endl;
}
virtual void fun()const//覆盖基类的虚函数
{
cout << "显示类B" << endl;
}
~B()
{
cout << endl;
fun();
cout << "调用派生类B的析构函数" << endl;
}
private:
int y;
};
int main()
{
A a(5);
cout << endl;
B b(3);
return 0;
}
运行结果:
分析:
在主函数中,定义了一个基类A的对象a并进行初始化,初始化时调用基类A的构造函数,基类A的构造函数中调用虚函数fun(),虽然在基类A和派生类B中都有虚函数fun(),但是在基类A的构造函数中调用的fun()函数是基类A中的fun函数,而不是派生类B中的fun()函数。又定义了一个派生类对象b并进行初始化,初始化派生类对象b时先调用基类A的默认构造函数,再调用B类的构造函数进行初始化b对象,在调用A类默认构造函数时,A类默认构造函数中调用了虚函数fun,这里调用的虚函数fun仍然不是B类中的虚函数fun,而是A类中的虚函数fun。
这是因为当基类A被被构造时,对象还不是一个派生类对象。
②只有虚函数是多态绑定的,如果派生类需要修改基类的行为(即重写与基类函数同名的函数),就应该在基类中将相应的函数声明为虚函数。而基类中声明的非虚函数,通常代表那些不希望被派生类改变的功能,也就是不能实现多态的。一般不要重写继承而来的非虚函数,因为会导致通过基类的指针和派生类的指针会对象调用同名函数时,会产生不同的结果而引起混乱。
【注意】在重写继承来的虚函数时,如果函数有默认值形参值,不要重新定义不同的值。因为,虽然虚函数是多态绑定的,但是默认形参是静态绑定的。也就是说,通过一个指向派生类对象的基类指针,可以访问到派生类的虚函数,但是默认形参值却只能来自基类定义。例如:
class A//基类A定义
{
public:
virtual void display()const//声明基类A中的成员函数为虚函数
{
cout << "显示类A" << endl;
}
};
class B :public A//公有派生类B定义
{
public:
virtual void display()const//覆盖基类的虚函数
{
cout << "显示类B" << endl;
}
};
class C :public B//公有派生类C定义
{
public:
virtual void display()const//覆盖基类的虚函数
{
cout << "显示类C" << endl;
}
};
void fun(A* p)//参数为指向基类A的对象的指针
{
p->A::display();//"对象指针->成员名"
}
int main()
{
C c;//定义派生类对象
A* p = &c;//基类指针p可以指向派生类对象
A& r = c;//基类引用r可以作为派生类对象的别名
A a = c;//调用基类A的拷贝构造函数用c构造a,a的类型是A而非C
return 0;
}
这里,A a = c;
会用C类型的对象c为A类型的对象a初始化,初始化时使用的是A类的拷贝构造函数。由于拷贝构造函数接收的是A类型的常引用,C类型的c符合类型兼容规则,可以作为参数传递给它。由于执行的是A类的拷贝构造函数,只有A类型的成员会被拷贝,C类中新增的数据成员不会被拷贝,也没有空间去存储,因此生成的对象是基类A的对象。这种用派生类对象拷贝构造基类对象的行为叫做对象切片。这时,如果用a去调用基类A的虚函数,调用的目的对象是对象切片后得到的A类对象,与C类型的c对象毫无关系,对象的类型很明确,因此无须多态绑定。