C++虚函数小结

来源:互联网 发布:移动公司网络监控软件 编辑:程序博客网 时间:2024/05/16 05:14

一、虚函数的定义
virtual关键字声明的函数叫虚函数。虚函数必须是非静态成员函数(且非构造函数),其访问权限是public(可以定义为private or proteceted, 但是对于多态来说,没有意义),在基类的类定义中定义虚函数的一般形式
  virtual 函数返回值类型 虚函数名(形参表)
  函数体 }

二、虚函数的作用

实现动态联编(或动态关联),即编译程序编译阶段不确定具体调用的函数而是在运行阶段依据对象的类型来确定将要调用的函数,也就是在程序的运行阶段动态地选择合适的成员函数,这种能力也叫做C++的多态性,这种方式的实现技术也叫迟绑定(late binding)技术。

在定义了虚函数后,可以在基类的派生类中对虚函数重新定义,其一般形式
  virtual 函数返回值类型 虚函数名(形参表)
  函数体 }
说明:在派生类中重新定义的函数应与虚函数具有相同的形参个数和形参类型。以实现统一的接口,不同定义过程。如果在派生类中没有对虚函数重新定义,则它继承其基类的虚函数。当程序发现虚函数名前的关键字virtual后,会自动将其作为动态联编处理,即在程序运行时动态地选择合适的成员函数。这也是C++多态性的实现机制。

实现动态联编需要三个条件:
1、 必须把需要动态联编的行为定义为类的公共属性虚函数
2、 类之间存在子类型关系,一般表现为一个类从另一个类公有派生而来。
3、 必须先使用基类指针指向子类型的对象,然后直接或间接使用基类指针调用虚函数。

关于C++的多态性:在基类的函数前加上visual关键字,在派生类中重写该函数,运行时将会根据对象的实际类型来调用相应的函数。如果对象类型是派生类,就调用派生类的函数,如果对象类型是基类,就调用基类函数。


三、定义虚函数的限制:

  (1非类的成员函数不能定义为虚函数,类的成员函数中静态成员函数和构造函数也不能定义为虚函数,但可以将析构函数定义为虚函数。实际上,优秀的程序员常常把基类的析构函数定义为虚函数。因为,将基类的析构函数定义为虚函数后,当利用delete删除一个指向派生类定义的对象指针时,系统会调用相应的类的析构函数。而不将析构函数定义为虚函数时,只调用基类的析构函数。
  (2)只需要在声明函数的类体中使用关键字“virtual”将函数声明为虚函数,而定义函数时不需要使用关键字“virtual”。
  (3)如果声明了某个成员函数为虚函数,则在该类中不能出现和这个成员函数同名并且返回值、参数个数、参数类型都相同的非虚函数。在以该类为基类的派生类中,也不能出现这种非虚的同名同返回值同参数个数同参数类型函数。 

虚函数必须是类的成员函数:

引入虚函数的目的就是为了实现多态,在类外定义虚函数没有实际用处。 

类的静态成员函数 不能 为虚函数:

如果定义为虚函数,那么它就是动态绑定的,也就是在派生类中可以被覆盖的,这与静态成员函数的定义(:在内存中只有一份拷贝;通过类名或对象引用访问静态成员)本身就是相矛盾的。另外,静态成员函数 不能被继承,只属于该类。

 构造函数 不能 为虚函数:

因为如果构造函数为虚函数的话,它将在执行期间被构造,而执行期则需要对象已经建立,构造函数所完成的工作就是为了建立合适的对象,因此在没有构建好的对象上不可能执行多态(虚函数的目的就在于实现多态性)的工作。

在继承体系中,构造的顺序就是从基类到派生类,其目的就在于确保对象能够成功地构建。构造函数同时承担着虚函数表的建立,如果它本身都是虚函数的话,如何确保vtbl的构建成功呢?

注意:当基类的构造函数内部有虚函数时,会出现什么情况呢?结果是在构造函数中,虚函数机制不起作用了,调用虚函数如同调用一般的成员函数一样。当基类的析构函数内部有虚函数时,又如何工作呢?与构造函数相同,只有“局部”的版本被调用。但是,行为相同,原因是不一样的。构造函数只能调用“局部”版本,是因为调用时还没有派生类版本的信息。析构函数则是因为派生类版本的信息已经不可靠了。我们知道,析构函数的调用顺序与构造函数相反,是从派生类的析构函数到基类的析构函数。当某个类的析构函数被调用时,其派生类的析构函数已经被调用了,相应的数据也已被丢失,如果再调用虚函数的派生类的版本,就相当于对一些不可靠的数据进行操作,这是非常危险的。因此,在析构函数中,虚函数机制也是不起作用的。

友元函数不能声明为虚函数

 因为C++不支持友元函数的继承,对于没有继承特性的函数没有虚函数的说法。友元函数不属于类的成员函数,不能被继承。

内联成员函数不能声明为虚函数 

内联函数是为了在代码中直接展开,减少函数调用花费的代价;虚函数是为了在继承后对象能够准确的执行自己的动作,这是不可能统一的(inline函数在编译时被展开,虚函数在运行时才能动态的邦定函数)

inline函数和virtual函数有着本质的区别,inline函数是在程序被编译时就展开,在函数调用处用整个函数体去替换,而virtual函数是在运行期才能够确定如何去调用的,因而inline函数体现的是一种编译期机制,virtual函数体现的是一种运行期机制。此外,一切virtual函数都不可能是inline函数。

C++中的虚函数的作用主要是实现了多态的机制。关于多态,简而言之就是用基类类别的指针指向其派生类的实例,然后通过类的指针调用实际派生类类的成员函数。这种技术可以让父类的指针有“多种形态”,这是一种泛型技术。所谓泛型技术,说白了就是试图使用不变的代码(Or 不变的 接口)来实现可变的算法。比如:模板技术,RTTI技术,虚函数技术,要么是试图做到在编译时决议,要么试图做到运行时决议。

四、虚函数的实现机制(C++多态性

虚函数在c++中的实现机制就是用虚表和虚指针,每个类用了一个虚表,每个类的对象用了一个虚指针。具体的用法如下:

class A
{
public:
    virtual void f();
    virtual void g();
private:
    int a
};

class B : public A
{
public:
    void g();
private:
    int b;
};
//A,B的实现省略

因为A有virtual void f(),和g(),所以编译器为A类准备了一个虚表vtableA,内容如下:

A::f 的地址

A::g 的地址


B因为继承了A,所以编译器也为B准备了一个虚表vtableB,内容如下:

A::f 的地址

B::g 的地址


注意:因为B::g是重写了的,所以B的虚表的g放的是B::g的入口地址,但是f是从上面的A继承下来的,所以f的地址是A::f的入口地址。

然后某处有语句 B bB;的时候,编译器分配空间时,除了A的int a,B的成员int b;以外,还分配了一个虚指针vptr,指向B的虚表vtableB,bB的布局如下:

vptr : 指向B的虚表vtableB

int a: 继承A的成员

int b: B成员


当如下语句的时候:
A *pa = &bB;
pa的结构就是A的布局(就是说用pa只能访问的到bB对象的前两项,访问不到第三项int b)那么pa->g()中,编译器知道的是,g是一个声明为virtual的成员函数,而且其入口地址放在表格(无论是vtalbeA表还是vtalbeB表)的第2项,那么编译器编译这条语句的时候就如是转换:call *(pa->vptr)[1]。
这一项放的是B::g()的入口地址,则就实现了多态。(注意bB的vptr指向的是B的虚表vtableB)另外要注意的是,如上的实现并不是唯一的,C++标准只要求用这种机制实现多态,至于虚指针vptr到底放在一个对象布局的哪里,标准没有要求,每个编译器自己决定。 

五、纯虚函数

定义:纯虚函数是指被标明为不具体实现的虚成员函数(纯虚函数可以有函数体但是这种用法很少见)。纯虚函数可以让类先具有一个操作名称,而没有操作内容,让派生类在继承时再去具体地给出定义。

声明格式

virtual 返回类型 函数名(参数列表)=0;

引入原因

1、为了方便使用多态特性,我们常常需要在基类中定义虚拟函数。

2、在很多情况下,基类本身生成对象是不合情理的。例如,动物作为一个基类可以派生出老虎、孔雀等子类,但动物本身生成对象明显不合常理。

为了解决上述问题,引入了纯虚函数的概念,将函数定义为纯虚函数(方法:virtual ReturnType Function()= 0;),则编译器要求在派生类中必须予以重写以实现多态性。同时含有纯虚拟函数的类称为抽象类,它不能生成对象。这样就很好地解决了上述两个问题。

相似概念

1、多态性

指相同对象收到不同消息或不同对象收到相同消息时产生不同的实现动作。C++支持两种多态性:编译时多态性,运行时多态性。

a.编译时多态性:通过重载函数实现

运行时多态性:通过虚函数实现

2、虚函数

虚函数是在基类中被声明为virtual,并在派生类中重新定义的成员函数,可实现成员函数的动态重载

3、抽象类

包含纯虚函数的类称为抽象类。由于抽象类包含了没有定义的纯虚函数,所以不能定义抽象类的对象。 

六、常见问题

构造函数不会被继承,但是子类的构造函数会显示的调用父类的构造函数或隐式的调用父类的默认的构造函数进行父类部分的初始化,析构函数也一样。

关于虚函数,需要总结的方面还有很多,待补充。

待完善 

 

0 0