C++:虚函数理解

【重要】文章是自己看了以后思考了以后全文手打的,但理解学习过程中有参考以下内容:
动态编联和静态编联部分
内存分区部分
C++用new和不用new创建类对象部分


一、用new创建对象和不用new创建对象的区别

假设前提:已有定义:

class A{
    ......;//无任何成员定义
}

1.new创建类对象:A *a = new A;

  • ①语句创建过程
    A *a=NULL;//此时只是在栈中定义了一个A型指针变量指向NULL
    a=new A;//此时在中分配内存,生成A型对象(同时因为我是在堆上,生存周期无线延长,可以通过指针控制它,并且一般不需要进行拷贝)
  • ②内存释放:需要指针接收,使用完需要delete销毁(释放内存,防止之后改变了指针指向,但该段内存未释放从而造成内存泄漏,之后也要a = NULL,防止野指针),此时会执行析构函数;
  • ③返回:得到的变量a是指针,需要寻址访问,具有指针特性,一处初始化,多处使用
  • 【注意】若遇此种形式:A *a = new B;//B为A的派生类,且A、B都有定义test函数
    则调用a-> test()时,可能是调用自A也可能是调用自B ,就要看是否是虚函数,见下《虚函数创建的实质》讲解

2.不用new 创建类对象:A a;

  • ①语句创建过程:
    A a;//此时直接在中分配了内存空间用以存放A型变量a,也即对象a
  • ②内存释放: 使用完后系统自动释放,析构函数会自动执行(且因为是在上,所以中途调用其他函数跳出不算,也不是整个程序走完后释放,而是所在局部区域,随着{}结束之后销毁;而函数中return一个对象的本质,是对它进行了拷贝);
  • ③返回: 得到的变量a是对象
  • 【补充】且此时只能发生静态编联,无法实现虚函数多态

二、虚函数创建的实质

1、虚函数的实现

  • ①大多数编译器通过vtbl(virtual table)和vptr(virtual table pointer)来实现对虚函数的调用
  • ②当一个类声明了虚函数,或者重定义了父类中的虚函数,或者直接继承了虚函数,这个类都会产生自己的vtbl和vptr,同时该类的每个对象都会包含一个vptr去指向该类的vpbl
  • ③vtbl表中指针顺序 对应 其对应的虚函数被声明的顺序
    • vptr:每个对象都会有自己的一个vptr指针(【a】同一个类的所有对象的指针值相同
      该指针存放在该对象所占内存区域的首地址处,【b】指向把它实例化的类的vtbl
    • vtbl:【c】每个派生类对应每个基类都会有自己的一个vtbl指针数组【d】但对应同一个基类,若无虚函数覆盖或增改时,则所有派生类的vtbl指针数组是一样的,都是基类中定义的虚函数,且实质是:所有vtbl指针数组中每个数组元素的指针值,也即地址,是同一个虚函数(都只是基类中的那个虚函数)
      【a】【b】【c】【d】见实例讲解

2、实例

#include<iostream>
using namespace std;

class A {//第一个基类
public:
    virtual void Func1(){};
    virtual void Func2(){};
    virtual void Func3(){};
    virtual void Func4(){};
    void fuck(){};
};

class A2{//第二个基类
public:
    virtual void Func(){};
};

class B : public A {//基类A的派生类
public:
    virtual void Func1(){};
    virtual void Func5(){};
    void fuck(){};
};

class C :public B,public A2{//基类A2和派生类B的派生类
public:
    virtual void Func6(){};
};


int main()
{
    A *a = new A;
    A *b1 = new B;
    B *b2 = new B;
    B *b3 = new B;
    C *c = new C;
    A a2;

    system("pause");//(1)
    delete a; a = NULL;
    delete b1; b1 = NULL;
    delete b2; b2 = NULL;
    delete b3; b3 = NULL;
    delete c; c = NULL;
    return 0;
}

1.
同一个类的两个对象
【a】同一个类的所有对象的vptr指针值相同:
可见对象b2和b3的vptr指针的值,都是0x00edcc30
2.

【b】指向把它实例化的类的vtbl
可见对象b2、b3的vptr指针与对象a的vptr指针指向不同,b2、b3指向类B的vpbl,a指向类A的vpbl
3.

【c】每个派生类对应每个基类都会有自己的一个vtbl指针数组
类C多继承自类B(B又继承自类A)和类A2(是一个基类),最终有两个vptr指针指向两个vpbl,分别是0x00edca74地址处的类A的表和0x00edcb1c地址处的类A2的表
4.

【d】对应同一个基类,若无虚函数覆盖或增改时,则所有派生类的vtbl指针数组是一样的,其实质是:所有vtbl指针数组中每个数组元素的指针值,也即地址,是同一个虚函数(都只是基类中的那个虚函数)
可见b2未对函数Fun2()、Fun3()、Fun4()重定义,于是vpbl中元素对应的地址与a中完全相同,但因对Fun1()进行了重定义,于是覆盖了原来的Fun1()

【注】见网上其他地方说,派生类中新定义的虚函数会放置在第一个父类的vtbl中的最后,如图:
这里写图片描述
所以我在类B中新添加类Func5()函数,在类C中添加了Func6()函数,但我观察了b2、b3和c的vpbl,均未发现:
这里写图片描述
此处不解


三、动态编联和静态编联

1、动态编联

  • 必须在指针指向的对象创建出来后才能决定究竟用那个函数(也正是在代码运行前无法确定运行的函数,所以才被称作“虚函数”)

2、静态编联

  • 在编译时就决定了函数是哪个类的函数的方式

3、为何说虚函数利用动态编联可以实现多态

1.对于一个类成员函数的调用:

A *b1 = new B;
b1->fuck();//注意这里的fuck()函数并非虚函数,所以是静态编联
  • ①表面上看来是在一个对象内部去调用函数fuck(),但代码背后的实质是:所有函数都存放在一个代码区,也即是说:
    【b1->fuck()】被转换成了【A::fuck(b1)】:把对象b1传进去,才告诉了编译器具体是哪个对象调用的这个函数。
    但是,对于fuck()函数来说,类A有,类B也有啊,究竟是选哪个类的函数执行?
  • ②这时候就要想起:fuck()因为非虚函数,所以不支持动态编联,即在编译时就会确定调用函数对象
  • ③而因为是在编译阶段,也就是内存中A啊B啊的对象还没有存在(前面说 A a;会在栈中分配内存空间,但是这里是A *a,只是声明一个A类型的指针变量),所以只能根据a这个指针的类型来决定使用哪个函数
  • ④所以,此时a所调用的fuck()函数,是A::fuck()

【结论】此时无法达到多态,B中的fuck()简直白定义了!!

2.于是虚函数登场:

A *b1 = new B;
b1->Func1();//注意这里的Func1()函数为虚函数,所以是动态编联
  • ①然而现在希望调用的是B中的Func1()函数
  • ②因为Func1()是虚函数,所以施行动态编联
  • ③当所指的函数是虚函数时,编译器将不会再做类似以下的转换:
    【b1->fuck()】被转换成了【A::fuck(b1)】,而是变成:
    【b1->Func1()】转换成【b1->vptr->vpbl->Func1()】(即此时虚函数的机制:vptr和vpbl登场)
  • ④因为此时必须要在b1指向的对象里面找,所以必须等到b1指向的对象被创建出来才能确定调用的是哪个函数,所以必须是运行时
  • ⑤所以结合之前对vptr和vpbl的介绍,可想而知,最终指向的是B类中重新定义了的Func1()函数
  • ⑥至此,实现了多态

【2017.03.04补充】
注意:虚函数的动态编联仅在基类指针或引用绑定派生类对象时发生

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值