不可定义为虚函数的函数

类的静态函数和构造函数不可以定义为虚函数:

静态函数的目的是通过类名+函数名访问类的static变量,或者通过对象调用staic函数实现对static成员变量的读写,要求内存中只有一份数据。而虚函数在子类中重写,并且通过多态机制实现动态调用,在内存中需要保存不同的重写版本。

构造函数的作用是构造对象,而虚函数的调用是在对象已经构造完成,并且通过调用时动态绑定。动态绑定是因为每个类对象内部都有一个指针,指向虚函数表的首地址。而且虚函数,类的成员函数,static成员函数都不是存储在类对象中,而是在内存中只保留一份。

将析构函数定义为虚函数的作用

类的构造函数不能定义为虚函数,析构函数可以定义为虚函数,这样当我们delete一个指向子类对象的基类指针时可以达到调用子类析构函数的作用,从而动态释放内存。

如下我们先定义一个基类和子类

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

class VirtualTableA

{

public:

    virtual ~VirtualTableA()

    {

        cout << "Desturct Virtual Table A" << endl;

    }

    virtual void print()

    {

        cout << "print virtual table A" << endl;

    }

};

class VirtualTableB : public VirtualTableA

{

public:

    virtual ~VirtualTableB()

    {

        cout << "Desturct Virtual Table B" << endl;

    }

    virtual void print();

};

void VirtualTableB::print()

{

    cout << "this is virtual table B" << endl;

}

我们写一个函数做测试

1

2

3

4

5

6

7

8

9

10

11

void destructVirtualTable()

{

    VirtualTableA *pa = new VirtualTableB();

    useTable(pa);

    delete pa;

}

void useTable(VirtualTableA *pa)

{

    //实现动态调用

    pa->print();

}

程序输出

this is virtual table B
Desturct Virtual Table B
Desturct Virtual Table A

在上面的例子中我们先在destructVirtualTable函数中new了一个VirtualTableB类型对象,并用基类VirtualTableA的指针指向了这个对象。

然后将基类指针对象pa传递给useTable函数,这样会根据多态原理调用VirtualTableB的print函数,然后再执行delete pa操作。

此时如果pa的析构函数不写成虚函数,那么就只会调用VirtualTableA的析构函数,不会调用子类VirtualTableB的析构函数,导致内存泄露。

而我们将析构函数写成虚析构之后,可以看到先调用了子类VirtualTableB的析构函数,再调用了基类VirtualTableA的析构函数,达到了释放子类空间的目的。

有人会问?将析构函数不写为虚函数,直接delete子类对象VirtualTableB,调用子类的析构函数不可以吗?比如,如下的调用

1

2

VirtualTableB *pb = new VirtualTableB();

delete pa;

上述调用没有问题,无论析构函数是否为虚析构都可以成功释放子类空间。但是项目编程中常常会编写一些通用接口,比如上面的useTable函数,

它只接受VirtualTableA类型的指针,所以我们常常会用基类指针接受子类对象来通过多态的方式调用子类函数,为了方便delete基类指针也要释放子类空间,

就要将析构函数设置为虚函数。

虚函数表原理

为了介绍虚函数表原理,我们先实现一个基类和子类

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

class Baseclass

{

public:

    Baseclass() : a(1024) {}

    virtual void f() { cout << "Base::f" << endl; }

    virtual void g() { cout << "Base::g" << endl; }

    virtual void h() { cout << "Base::h" << endl; }

    int a;

};

// 0 1 2 3   4 5 6 7(虚函数表空间)    8 9 10 11 12 13 14 15(存储的是a)

class DeriveClass : public Baseclass

{

public:

    virtual void f() { cout << "Derive::f" << endl; }

    virtual void g2() { cout << "Derive::g2" << endl; }

    virtual void h3() { cout << "Derive::h3" << endl; }

};

一个类对象其内存分布的基本结构为虚函数表地址+非静态成员变量,类的成员函数不占用类对象的空间,他们分布在一片属于类的共有区域。

类的静态成员函数喝成员变量不占用类对象的空间,他们分配在静态区。

虚函数表的地址存储在类对象的起始位置。所以我们利用这个原理,通过寻址的方式访问虚函数表里的函数

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

void useVitualTable()

{

    Baseclass b;

    b.a = 1024;

    cout << "sizeof b is " << sizeof(b) << endl;

    int *p = (int *)(&b);

    cout << "pointer address of vitural table " << p << endl;

    cout << "address of b is " << &b << endl;

    cout << "address of a is " << p + 2 << endl;

    cout << "address of p+1 is " << p +1 << endl;

    cout << "value of a is " << *(p + 2) << endl;

    cout << "address of vitural table" << (int *)(*p) << endl;

    cout << "sizeof int is " << sizeof(int) << endl;

    cout << "sizeof p is " << sizeof(p) << " sizeof(int*) is " << sizeof(int *) << endl;

    Func pFun = (Func)(*(int *)(*p));

    pFun();

    pFun = (Func) * ((int *)(*p) + 2);

    pFun();

    pFun = (Func)(*((int *)(*p) + 4));

    pFun();

}

上面的程序输出

sizeof b is 16
pointer address of vitural table 0xb6fdd0
address of b is 0xb6fdd0
address of a is 0xb6fdd8
address of p+1 is 0xb6fdd4
value of a is 1024
address of vitural table0x46d890
sizeof int is 4
sizeof p is 8 sizeof(int*) is 8
Base::f
Base::g
Base::h

可以看到b的大小为16字节,因为我的机器是64位的,所以指针类型都占用8字节,int 占用4字节,但是要遵循补齐原则,结构体的大小要为最大成员大小的整数倍,所以要补齐4字节,那么8+4+4 = 16 字节,关于类对象对齐和补齐原则稍后再详述。

b的内存分布如下图

这个根据不同的机器所占的字节数不一样,在32位机器上int为4字节,虚函数表地址为4字节,4+4 = 8字节,这个再之后再说明对齐和补齐的原则。

&b表示取b的地址,因为虚函数表地址存储在b的起始地址,所以&b也是虚函数表的地址的地址,我们通过int* 强转是方便存储b的地址,因为64位机器指针都是8字节,32位机器指针是4字节。

p为虚函数表的地址的地址,p+1具体移动了4个字节,因为p+1移动多少个字节取决于p所指向的数据类型int,int为4字节,所以p+1在p的地址移动四个字节,p+2在p的地址移动8个字节。

p只想虚函数表的地址,换句话说p存储的是虚函数表的地址,虚函数表地址占用8字节,p+2就是从p向后移动8字节,这样刚好找到a的地址。

那么*(p+2)就是取a的数值。

int*(*p)就是取虚函数表的地址,转为int*是方便读写。

我们将b的内存分布以及虚函数表结构画出来

上图中可以看到虚函数表中存储的是虚函数的地址,所以通过不断位移虚函数表的指针就可以达到指向不同虚函数的目的。

1

2

Func pFun = (Func)(*(int *)(*p));

pFun();

*(int *)(*p)就是取出虚函数表首地址指向的虚函数,再通过Func转化为函数类型,然后调用pFun即可调用虚函数f。

所以想调用第二个虚函数g,将(int*)(*p) 加2 位移8个字节即可

1

2

pFun = (Func) * ((int *)(*p) + 2);

pFun();

同样的道理调用h就不赘述了。

继承关系中虚函数表结构

DeriveClass继承了BaseTest类,子类如果重写了虚函数,则子类的虚函数表中存储的虚函数为子类重写的,否则为基类的。

我们画一下DeriveClass的虚函数表结构

因为函数f被DeriveClass重写,所以DeriveClass的虚函数表存储的是自己重写的f。

而虚函数g和h没有被DeriveClass重写,所以DeriveClass虚函数表存储的是基类的g和h。

另外DeriveClass虚函数表里也存储了自己特有的虚函数g2和h3.

下面我们还是利用寻址的方式调用虚函数

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

void deriveTable()

{

    DeriveClass d;

    int *p = (int *)(&d);

    int *virtual_tableb = (int *)(*p);

    Func pFun = (Func)(*(virtual_tableb));

    pFun();

    pFun = (Func)(*(virtual_tableb + 2));

    pFun();

    pFun = (Func)(*(virtual_tableb + 4));

    pFun();

    pFun = (Func)(*(virtual_tableb + 6));

    pFun();

    pFun = (Func)(*(virtual_tableb + 8));

    pFun();

}

复制讲解

程序输出

Derive::f
Base::g
Base::h
Derive::g2
Derive::h3

可见DeriveClass虚函数表里存储的f是DeriveClass的f。

(int *)(*p)表述取出p所指向的内存空间的内容,p指向的正好是虚函数表的地址,所以*p就是虚函数表的地址。

因为我们不知道虚函数表的具体类型,所以转为int*类型,因为指针在64位机器上都是8字节,可以保证空间大小正确。接下来就是寻址和函数调用的过程,这里不再赘述。
 

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐