C++中虚函数

来源:互联网 发布:linux arp刷新命令 编辑:程序博客网 时间:2024/06/05 10:19

一句话总结:当基类和派生类有相同的函数的时候,虚函数可以保证调用到派生类的函数。


内存模型描述的是程序中各变量(实例域、静态域和数组元素)之间的关系,以及在实际计算机系统中将变量存储到内存和从内存取出变量这样的低层细节.不同平台间的处理器架构将直接影响内存模型的结构.

  首先介绍一下C++中有继承关系的类对象内存的布局:

  在C++中,如果类中有虚函数,那么它就会有一个虚函数表的指针__vfptr,在类对象最开始的内存数据中。之后是类中的成员变量的内存数据。

  对于子类,最开始的内存数据记录着父类对象的拷贝(包括父类虚函数表指针和成员变量)。之后是子类自己的成员变量数据。

  对于子类的子类,也是同样的原理。但是无论继承了多少个子类,对象中始终只有一个虚函数表指针。

  为了探讨C++类对象的内存布局,先来写几个类和函数

  首先写一个基类:

  1. class Base   
  2. {   
  3. public:   
  4. virtual void f() { cout << "Base::f" << endl; }   
  5. virtual void g() { cout << "Base::g" << endl; }   
  6. virtual void h() { cout << "Base::h" << endl; }   
  7. int base;   
  8. protected:   
  9. private:   
  10. };

  然后,我们多种不同的继承情况来研究子类的内存对象结构。

  1. 无虚函数集继承

  1. //子类1,无虚函数重载  
  2. class Child1 : public Base   
  3. {   
  4. public:   
  5. virtual void f1() { cout << "Child1::f1" << endl; }   
  6. virtual void g1() { cout << "Child1::g1" << endl; }   
  7. virtual void h1() { cout << "Child1::h1" << endl; }   
  8. int child1;   
  9. protected:   
  10. private:   
  11. };

  这个子类Child1没有继承任何一个基类的虚函数,因此它的虚函数表如下图:

  我们可以看出,子类的虚函数表中,先存放基类的虚函数,在存放子类自己的虚函数。

 2. 有一个虚函数继承

  1. //子类2,有1个虚函数重载   
  2. class Child2 : public Base   
  3. {   
  4. public:   
  5. virtual void f() { cout << "Child2::f" << endl; }   
  6. virtual void g2() { cout << "Child2::g2" << endl; }   
  7. virtual void h2() { cout << "Child2::h2" << endl; }   
  8. int child2;   
  9. protected:   
  10. private:   
  11. };

  当子类重载了父类的虚函数,则编译器会将子类虚函数表中对应的父类的虚函数替换成子类的函数。

  3. 全部虚函数都继承

  1. //子类3,全部虚函数重载   
  2. class Child3 : public Base   
  3. {   
  4. public:   
  5. virtual void f() { cout << "Child3::f" << endl; }   
  6. virtual void g() { cout << "Child3::g" << endl; }   
  7. virtual void h() { cout << "Child3::h" << endl; }   
  8. protected:   
  9. int x;   
  10. private:   
  11. };

  4. 多重继承

  多重继承,即类有多个父类,这种情况下的子类的内存结构和单一继承有所不同。

我们可以看到,当子类继承了多个父类,那么子类的内存结构是这样的:

  子类的内存中,顺序

  5. 菱形继承

  6. 单一虚拟继承

  虚拟继承的子类的内存结构,和普通继承完全不同。虚拟继承的子类,有单独的虚函数表, 另外也单独保存一份父类的虚函数表,两部分之间用一个四个字节的0x00000000来作为分界。子类的内存中,首先是自己的虚函数表,然后是子类的数据成员,然后是0x0,之后就是父类的虚函数表,之后是父类的数据成员。

 如果子类没有自己的虚函数,那么子类就不会有虚函数表,但是子类数据和父类数据之间,还是需要0x0来间隔。

  因此,在虚拟继承中,子类和父类的数据,是完全间隔的,先存放子类自己的虚函数表和数据,中间以0x分界,最后保存父类的虚函数和数据。如果子类重载了父类的虚函数,那么则将子类内存中父类虚函数表的相应函数替换。

  7. 菱形虚拟继承

  结论:

  (1)对于基类,如果有虚函数,那么先存放虚函数表指针,然后存放自己的数据成员;如果没有虚函数,那么直接存放数据成员。

  (2)对于单一继承的类对象,先存放父类的数据拷贝(包括虚函数表指针),然后是本类的数据。

  (3)虚函数表中,先存放父类的虚函数,再存放子类的虚函数

  (4)如果重载了父类的某些虚函数,那么新的虚函数将虚函数表中父类的这些虚函数覆盖。

  (5)对于多重继承,先存放第一个父类的数据拷贝,在存放第二个父类的数据拷贝,一次类推,最后存放自己的数据成员。其中每一个父类拷贝都包含一个虚函数表指针。如果子类重载了某个父类的某个虚函数,那么该将该父类虚函数表的函数覆盖。另外,子类自己的虚函数,存储于第一个父类的虚函数表后边部分。

  (6)当对象的虚函数被调用是,编译器去查询对象的虚函数表,找到该函数,然后调用。

  到这c++类对象的内存模型就介绍完了,希望对大家有帮助。


纯虚函数在基类中是没有定义的,必须在子类中加以实现,很像java中的接口函数!

虚函数

引入原因:为了方便使用多态特性,我们常常需要在基类中定义虚函数。

class Cman{public:virtual void Eat(){……};void Move();private:};class CChild : public CMan{public:virtual void Eat(){……};private:};CMan m_man;CChild m_child;//这才是使用的精髓,如果不定义基类的指针去使用,没有太大的意义CMan *p ;p = &m_man ;p->Eat(); //始终调用CMan的Eat成员函数,不会调用 CChild 的p = &m_child;p->Eat(); //如果子类实现(覆盖)了该方法,则始终调用CChild的Eat函数//不会调用CMan 的 Eat 方法;如果子类没有实现该函数,则调用CMan的Eat函数p->Move(); //子类中没有该成员函数,所以调用的是基类中的

纯虚函数

引入原因:

1、同“虚函数”;

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

//纯虚函数就是基类只定义了函数体,没有实现过程定义方法如下

// virtual void Eat() = 0; 直接=0 不要 cpp中定义就可以了

//纯虚函数相当于接口,不能直接实例话,需要派生类来实现函数定义

//有的人可能在想,定义这些有什么用啊 ,我觉得很有用

//比如你想描述一些事物的属性给别人,而自己不想去实现,就可以定

//义为纯虚函数。说的再透彻一些。比如盖楼房,你是老板,你给建筑公司

//描述清楚你的楼房的特性,多少层,楼顶要有个花园什么的

//建筑公司就可以按照你的方法去实现了,如果你不说清楚这些,可能建筑

//公司不太了解你需要楼房的特性。用纯需函数就可以很好的分工合作了

虚函数和纯虚函数区别

观点一:

类里声明为虚函数的话,这个函数是实现的,哪怕是空实现,它的作用就是为了能让这个函数在它的子类里面可以被重载,这样的话,这样编译器就可以使用后期绑定来达到多态了

纯虚函数只是一个接口,是个函数的声明而已,它要留到子类里去实现。

class A{protected:void foo();//普通类函数virtual void foo1();//虚函数virtual void foo2() = 0;//纯虚函数}

观点二:

虚函数在子类里面也可以不重载的;但纯虚必须在子类去实现,这就像Java的接口一样。通常我们把很多函数加上virtual,是一个好的习惯,虽然牺牲了一些性能,但是增加了面向对象的多态性,因为你很难预料到父类里面的这个函数不在子类里面不去修改它的实现

观点三:

虚函数的类用于“实作继承”,继承接口的同时也继承了父类的实现。当然我们也可以完成自己的实现。纯虚函数的类用于“介面继承”,主要用于通信协议方面。关注的是接口的统一性,实现由子类完成。一般来说,介面类中只有纯虚函数的。

观点四:

错误:带纯虚函数的类叫虚基类,这种基类不能直接生成对象,而只有被继承,并重写其虚函数后,才能使用。这样的类也叫抽象类。

虚函数是为了继承接口和默认行为

纯虚函数只是继承接口,行为必须重新定义

////////////////////////////////////////////////////////////////////////////////////

虚基类的初始化  虚基类的初始化与一般多继承的初始化在语法上是一样的,但构造函数的调用次序不同.

  派生类构造函数的调用次序有三个原则:  

(1)虚基类的构造函数在非虚基类之前调用;  

(2)若同一层次中包含多个虚基类,这些虚基类的构造函

(3)若虚基类由非虚基类派生而来,则仍先调用基类构造函数,再调用派生类的构造函数.
在派生类继承基类时,加上一个virtual关键词则为虚拟基类继承,如:
class derive:virtual public base
{
}
虚基类主要解决在多重继承时,基类可能被多次继承,虚基类主要提供一个基类给派生类,如:
class B
{
}
class D2:public B
{
}
这里C在D1,D2上继承,但有两个基类,造成混乱。因而使用虚基类,即:
classB
  {
  };
  class D1:virtual public B
  {
  };
  class D2:virtual publicB
  {
  };
  class C:public D1,public D2
在使用虚基类时要注意:  (1) 一个类可以在一个类族中既被用作虚基类,也被用作非虚基类。
(2) 在派生类的对象中,同名的虚基类只产生一个虚基类子对象,而某个非虚基类产生各自的子对象。  (3) 虚基类子对象是由最远派生类的构造函数通过调用虚基类的构造函数进行初始化的。  (4) 最远派生类是指在继承结构中建立对象时所指定的类。  (5) 派生类的构造函数的成员初始化列表中必须列出对虚基类构造函数的调用;如果未列出,则表示使用该虚基类的缺省构造函数。  (6) 从虚基类直接或间接派生的派生类中的构造函数的成员初始化列表中都要列出对虚基类构造函数的调用。但仅仅用建立对象的最远派生类的构造函数调用虚基类的构造函数,而该派生类的所有基类中列出的对虚基类的构造函数的调用在执行中被忽略,从而保证对虚基类子对象只初始化一次。  (7) 在一个成员初始化列表中同时出现对虚基类和非虚基类构造函数的调用时,虚基类的构造函数先于非虚基类的构造函数执行。

静态联编:在程序链接阶段就可以确定的调用。

动态联编:在程序执行时才能确定的调用。


0 0
原创粉丝点击