C++中的virtual关键字
虚函数与运行多态
多态:
多态按字面的意思就是多种形态。当类之间存在层次结构,并且类之间是通过继承关联时,就会用到多态。C++ 多态意味着调用成员函数时,会根据调用函数的对象的类型来执行不同的函数。
先看最简单的情况,也就是最普通形式的继承,且父类和子类的方法都是一般成员方法:
class Car{
public:
Car(){cout<<"Car consstructor"<<endl;}
~Car(){cout<<"Car destructor"<<endl;}
// 若将成员成员函数声明为const,则该函数不允许修改类的数据成员
void start() const{cout<<"car start"<<endl;}
void stop() const{cout<<"cat stop"<<endl;}
};
//Benz类,单一继承自Car
class Benz : public Car{
public:
Benz(){cout<<"Benz constructor"<<endl;}
~Benz(){cout<<"Benz destructor"<<endl;}
void start() const{cout<<"Benz start"<<endl;}
void stop() const{cout<<"Benz stop"<<endl;}
};
// Baoma类,单一继承自Car
class Baoma:public Car{
public:
Baoma(){cout<<"Baoma constructor"<<endl;}
~Baoma(){cout<<"Baoma destructor"<<endl;
}
void start() const{cout<<"Baoma constructor"<<endl;}
void stop() const{cout<<"Baoma destructor"<<endl;}
private:
int speed;
};
//以上三个类均具有start和stop的同名成员函数
//调用成员函数start和stop
void carFunction(Car *car){
car->start();
car->stop();
}
int main(int argc,char *argv[]){
Car *benz = new Benz();
cout<<sizeof(Benz)<<endl;
carFunction(benz);
Car *baoma = new Baoma();
cout<<sizeof(Baoma)<<endl;
carFunction(baoma);
delete benz;
delete baoma;
return 0;
}
输出结果如下:
Car consstructor
Benz constructor
1 //内部没有成员变量,因此只有一个字节的空间
car start
cat stop
Car consstructor
Baoma constructor
4 //函数是不占用内存的,baoma中有一个int类型.所以sizeof为4
car start
cat stop
Car destructor
Car destructor
首先,为什么Benz类内部明明没有任何变量,还具有一个字节的大小?这是因为C++编译器不允许对象为零长度(试想一个长度为0的对象在内存中怎么存放?怎么获取它的地址?)。为了避免这种情况,C++强制给这种类插入一个缺省成员,长度为1。如果有自定义的变量,那么变量将取代这个缺省成员。
其次,Benz和Baoma都是继承自Car类,根据 里氏替换原则 ,父类能够出现的地方,那么子类也一定能出现。依赖抽象而不去依赖具体,在上述的函数调用过程中,我们传进去的是benz和baoma指针.但是在调用函数的时候,它并没有去调用子类的方法,这也就是一般成员函数的局限性,就是在编译的时候,一般性的函数已经被静态的编译进去,所以在调用的时候不能去选择动态调用.
里氏替换原则:派生类(子类)对象可以在程式中代替其基类(超类)对象
加入vitural关键字修饰的函数,将父类函数变为虚函数,看看变化:
//和上面几乎一样,都是一般的成员方法,只不过加上了virtual关键字
#include<iostream>
using namespace::std;
class Car{
public:
Car(){
cout<<"Car consstructor"<<endl;
}
~Car(){
cout<<"Car destructor"<<endl;
}
virtual void start() {
cout<<"car start"<<endl;
}
virtual void stop() {
cout<<"cat stop"<<endl;
}
};
class Benz : public Car{
public:
Benz(){
cout<<"Benz constructor"<<endl;
}
~Benz(){
cout<<"Benz destructor"<<endl;
}
//子类继承父类,如果是虚函数,可以写上vitural也可以不写
virtual void start() {
cout<<"Benz start"<<endl;
}
void stop() {
cout<<"Benz stop"<<endl;
}
};
class Baoma:public Car{
public:
Baoma(){
cout<<"Baoma constructor"<<endl;
}
~Baoma(){
cout<<"Baoma destructor"<<endl;
}
void start() {
cout<<"Baoma start"<<endl;
}
void stop() {
cout<<"Baoma stop"<<endl;
}
private:
int speed;
};
void carFunction(Car *car){
car->start();
car->stop();
}
int main(int argc,char *argv[]){
Car *benz = new Benz();
cout<<sizeof(Benz)<<endl;
carFunction(benz);
Car *baoma = new Baoma();
cout<<sizeof(Baoma)<<endl;
carFunction(baoma);
delete benz;
delete baoma;
return 0;
}
输出结果如下:
Car consstructor
Benz constructor
8
Benz start
Benz stop
Car consstructor
Baoma constructor
16
Baoma start
Baoma stop
Car destructor
Car destructor
从上面的输出结果中可以看到,加入了虚函数之后,调用不同指针对象指定函数的时候,这个时候都是去自动调用当前对象类中的具体函数形式,而不是像一般函数的调用一样,只是去调用父类的函数.这就是virtural关键字的作用,因为一般函数调用编译的时候是静态编译的时候就已经决定了,加入了virtural的函数,一个类中函数的调用并不是在编译的时候决定下来的,而是在运行时候被确定的,这也就是虚函数.
虚函数就是由于在由于编写代码的时候并不能确定被调用的是基类的函数还是哪个派生类的函数,所以被 为“虚”函数。 虚函数只能借助于指针或者引用来达到多态的效果, 直接声明的类对象无法达到多态目的。
总结: 虚函数的调用取决于指向或者引用的对象的类型,而不是指针或者引用自身的类型。
注意:
- C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用父类型别的指针指向其子类的实例,然后通过父类的指针调用实际子类的成员函数。
- 对C++ 了解的人都应该知道虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主是要一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其容真实反应实际的函数。这样,在有虚函数的类的实例中这个表被分配在了这个实例的内存中,所以,当我们用父类的指针来操作一个子类的时候,这张虚函数表就显得由为重要了,它就像一个地图一样,指明了实际所应该调用的函数
- 带有虚函数的对象自身确实插入了一些指针信息,而且这个指针信息并不随着虚函数的增加而增大,这也就是为什么上述增加了虚函数后,出现了size变大的现象
虚函数控制下的运行多态有什么用?
假如我们在公司的人事管理系统中定义了一个基类 Employee(员工),里面包含了升职、加薪等虚函数。 由于Manager(管理人员)和Engineer(工程人员)的加薪和晋升流程是不一样的,因此我们需要实现一些继承类并重写这些函数。
有了上面这些以后,到了一年一度每个人都要加薪的时候,我们只需要一个简单的操作就可以完成,如下所示
void globalRaiseSalary(Employee *emp[], int n){
for (int i = 0; i < n; i++)
emp[i]->raiseSalary(); // 会根据emp具体指向的对象类型,来选择合适的函数行为
// Polymorphic Call: Calls raiseSalary()
// according to the actual object, not according to the type of pointer
}
虚函数使得我们可以创建一个统一的基类指针,并且调用不同子类的函数而无需知道子类对象究竟是什么
虚函数表与虚函数表指针
C++中虚函数这种多态的性质是通过虚函数指针和一张虚函数表来实现的:
- vtable(虚函数表):每一个含有虚函数的类都会维护一个虚函数表,里面按照声明顺序记录了虚函数的地址
- vptr(虚函数表指针):一个指向虚函数表的指针,每个对象都会拥有这样的一个指针
先看看下面这个简单的例子:
class A
{
public:
virtual void fun();
};
class B
{
public:
void fun();
};
sizeof(A) > sizeof(B) // true,因为A比B多了一个虚函数指针
下面再来看看刚刚那个加薪的例子,其多态调用的形式如下图:
通常情况下,编译器在下面两处地方添加额外的代码来维护和使用vptr:
- 在每个构造函数中。此处添加的代码会设置被创建对象的虚函数表指针指向对应类的虚函数表
- 在每次进行多态函数调用时。 无论合适调用了多态函数,编译器都会首先查找vptr指向的地址(也就是指向对象对应的类的虚函数表),一旦找到后,就会使用该地址内存储的函数(而不是基类的函数)。
虚函数中的默认参数
先看下面的代码
#include <iostream>
using namespace std;
class Base
{
public:
virtual void fun ( int x = 0 )
{
cout << "Base::fun(), x = " << x << endl;
}
};
class Derived : public Base
{
public:
// 这里的virtual关键字可以省略,因为只要基类里面被声明为虚函数,那么在子类中默认都是虚的
virtual void fun ( int x )// 或者定义为 virtual void fun ( int x = 10)
{
cout << "Derived::fun(), x = " << x << endl;
}
};
int main()
{
Derived d1;
Base *bp = &d1;
bp->fun();
return 0;
}
上面的代码输出始终为:
Derived::fun(), x = 0
解释:
- 首先,参数的默认值是不算做函数签名的,因此,即使基类有默认值,子类没有,这两个函数的函数签名仍然被认为是相同的,所以在调用
bp->fun();
,仍然调用了子类的fun
函数,但是因为没有给出x
的值,所以采用了基类函数给出的默认值0
. - 当基类给出默认值0,子类给出默认值10时,返回结果仍然是默认值
0
,这是因为,参数的默认值是静态绑定的,而虚函数是动态绑定的,因此, 默认参数的使用需要看指针或者引用本身的类型,而不是指向对象的类型。
小结:根据上面的分析,在虚函数中最好不要使用默认参数,否则很容易引起误会!
静态函数可以被声明为虚函数吗
静态函数不可以声明为虚函数,同时也不能被const和volatile关键字修饰。如下面的声明都是错误的:
virtual static void fun(){}
static void fun() const {} // 函数不能被const修饰,但是返回值可以
原因主要有两个方面:
- static成员函数不属于任何类对象或类实例,所以即使给此函数加上virtual也是没有意义的
- 虚函数依靠vptr和vtable来处理,vptr是一个指针,在类的构造函数中创建生成,并且智能用this指针来访问它,静态成员函数没有this指针,所以无法访问vptr。
构造函数可以为虚函数吗
构造函数不可以声明为虚函数。同时除了inline
之外,构造函数不允许使用其他任何关键字,原因如下:
- 尽管虚函数表vtable是在编译阶段就已经建立的,但指向虚函数表的指针vptr是在运行阶段实例化对象时才产生的。 如果类含有虚函数,编译器会在构造函数中添加代码来创建vptr。 问题来了,如果构造函数是虚的,那么它需要vptr来访问vtable,可这个时候vptr还没产生。 因此,构造函数不可以为虚函数。
- 我们之所以使用虚函数,是因为需要在信息不全的情况下进行多态运行。而构造函数是用来初始化实例的,实例的类型必须是明确的。 因此,构造函数没有必要被声明为虚函数。
析构函数可以为虚函数吗
析构函数可以声明为虚函数。如果我们需要删除一个指向派生类的基类指针时,应该把析构函数声明为虚函数。事实上,只要一个类有可能会被其他类所继承,就应该声明虚析构函数(哪怕该析构函数不执行任何操作)。原因可以见先秒的代码:
#include<iostream>
using namespace std;
class base {
public:
base()
{ cout<<"Constructing base \n"; }
// virtual ~base()
~base()
{ cout<<"Destructing base \n"; }
};
class derived: public base {
public:
derived()
{ cout<<"Constructing derived \n"; }
~derived()
{ cout<<"Destructing derived \n"; }
};
int main(void)
{
derived *d = new derived();
base *b = d;
delete b;
return 0;
}
以上代码输出:
Constructing base
Constructing derived
Destructing base
可见,继承类的析构函数没有被调用,delete时只根据指针类型调用了基类的析构函数。 正确的操作是,基类和继承类的析构函数都应该被调用,解决方法是将基类的析构函数声明为虚函数。
虚函数可以为私有函数吗
虚函数可以被私有化,但有一些细节需要注意
#include<iostream>
using namespace std;
class Derived;
class Base {
private:
virtual void fun() { cout << "Base Fun"; }
friend int main();
};
class Derived: public Base {
public:
void fun() { cout << "Derived Fun"; }
};
int main()
{
Base *ptr = new Derived;
ptr->fun();
return 0;
}
输出结果为:
Derived fun()
- 基类指针指向继承类对象,则调用继承类对象的函数
- int main()必须声明为Base类的友元,否则编译失败。编译器报错:ptr无法访问私有函数。当然,把基类声明为public,继承类为private,该问题就不存在了。
虚函数可以被内联吗
通常类成员函数都会被编译器考虑是否进行内联。但通过基类指针或者引用调用的虚函数必定不能被内联。当然,实体对象调用虚函数或者静态调用时可以被内联,虚析构函数的静态调用也一定会被内联展开。
纯虚函数与抽象类
纯虚函数:在基类中只声明不定义的虚函数,同时要求任何派生类都要实现该虚函数。在基类中实现纯虚函数的方法是在函数原型后加“=0”。
抽象类:含有纯虚函数的类为抽象类
纯虚函数的特点以及用途总结如下:
- 如果不在继承类中实现该函数,则继承类仍为抽象类;
- 派生类仅仅只是继承纯虚函数的接口,因此使用纯虚函数可以规范接口形式
- 抽象类无法实例化对象
- 抽象类可以有构造函数
- 析构函数被声明为纯虚函数是一种特例,允许其有具体实现。(有些时候,想要使一个类称为抽象类,但刚好有没有任何合适的纯虚函数,最简单的方法就是声明一个纯虚的析构函数)