Page's Personal Website

一道c++面试题的思考与学习

2016-12-17
c++

[TOC]

先上面试题,问打印结果是什么?

#include <stdio.h>

class A{
public:
        void fun() {printf("A");}
};

class B : public A{
public:
        virtual void fun(){printf("B");}
};

class C : public B{
public:
        void fun() {printf("C");}
};

class D : public C{
public:
        virtual void fun() {printf("D");}
        virtual void funD() {printf("D_Test");}
};

class E : public D {
public:
        virtual void fun(){printf("E");}
        virtual void funE(){printf("E_Test");}
};

class E1 : public D{
public:
        virtual void fun(){printf("E1");}
        virtual void funE1(){printf("E1_Test");}
};

int main(){
        A* p1 = (A*)new B; p1->fun();printf(", ");
        B* p2 = (B*)new C; p2->fun(); printf(", ");
        B* p3 = (B*)new D; p3->fun(); printf(", ");
        B* p4 = (E*)new D; p4->fun(); printf(", ");
        D* p5 = (D*)new E; p5->funD(); printf(", ");
        E1* p6 = (E1*)new E; p6->funE1(); printf(", ");
        p6->funD(); printf(", ");
        printf("%d, %d\n", sizeof(B) - sizeof(A), sizeof(D) - sizeof(A));
}

再看到这里的时候,再做一遍吧,答案放在最后

问题:

  1. 类中的内存布局是什么样的?
  2. 类的非静态成员函数是放在什么地方的?
  3. 虚函数是放在什么地方的?虚表是在哪里,以及如何存储的?

类内存布局

  • static数据成员放在对象之外,和其他static成员变量一样,都是在全局的静态区;
  • static和nonstatic函数也都放在对象之外,具体在哪呢?自己的理解是也在全局某个位置,编译的时候就已经分配了一个地址了,因为函数本身是不需要跟类对象走的,因为类函数调用的时候会默认传一个this指针,所以区别就在这里了;
  • virtual函数通过虚指针指向的虚函数表中。

简单的结构如下图,参考[2]描述地超级详细,也利于理解,可以仔细阅读一下。

大致结构图

C++对象模型

C++对象模型

类成员函数地址

  • 静态成员函数地址是全局的,跟对象无关;
  • 动态成员函数也是跟类绑定而不是跟对象绑定的,我们会发现&p1->fun这样的代码编译不通过,而A::fun打印出了动态成员函数的地址;
  • 类虚函数地址指的是该函数在类虚表中的偏移量,而不是真正的地址,例如:&B::fun的结果为1.

小结

  • 当一个类本身定义了虚函数,或其父类有虚函数时,为了支持多态机制,编译器将为该类添加一个虚函数指针(vptr)。 虚函数指针一般都放在对象内存布局的第一个位置上,这是为了保证在多层继承或多重继承的情况下能以最高效率取到虚函数表。
  • 虚函数指针指向虚表vTables;函数只要有virtual,我们就需要把它添加进vTable。
  • 每个类(而不是类实例)都有自己的虚表,因此vTable就变成了vTables。
  • 虚表存放的位置一般存放在模块的常量段中,从始至终都只有一份。详情可在此参考

另外,为了解题,还要讲讲编译链接以及函数调用的简单原理

编译链接和函数调用

编译的时候,静态以及非静态成员函数也是已经编译好了的。虚函数,编译器能够确定的是偏移量,待运行的时候会去找具体对象的vptr,指向该对象所在类对应的虚表,找到真正应该调用的函数[3]。 文章[4]也说到了,虚函数和普通函数的在汇编一层的表现形式,虚函数要通过虚表去查找真正该调用的函数,而普通函数直接就找到了的。

解题

现在,运用上面的结论来回答问题

  • p1:A类是没有虚指针的,在编译阶段就已经是去找A类的fun函数了;
  • p2:C对象继承B,就已经有虚指针了,查找对象所对应的C类的虚表;
  • p3:查找D类所对应的虚表;
  • p4:查找D类所对应的虚表。打印p3和p4的虚表地址,发现就是一样的;
  • p5:E类的虚表中有从D继承过来的虚函数,在虚表中第二个位置,第一个位置是fun,第三个位置是funE;
  • p6:E对象强制转换成E1,funE1是在类E对应的虚表的第三个位置(1:fun;2:funD;3:funE1)。E1::funE1其实就是偏移量,在类E的虚表中第三个虚函数就是funE了。

看一下测试代码和结果如下: 在上面面试题后面加入以下测试代码

    printf("===========vritual address=========\n");
    printf("vtable p1: %x\n", *((int*)p1));
    printf("vtable p2: %x\n", *((int*)p2));
    printf("vtable p3: %x\n", *((int*)p3));
    printf("vtable p4: %x\n", *((int*)p4));
    printf("vtable p5: %x\n", *((int*)p5));
    printf("vtable p6: %x\n", *((int*)p6));
    printf("p1 virtual fun: %x\n", *(int*)*((int*)p1));
    printf("p2 virtual fun: %x\n", *(int*)*((int*)p2));
    printf("p3 virtual fun: %x\n", *(int*)*((int*)p3));
    printf("p4 virtual fun: %x\n", *(int*)*((int*)p4));
    printf("p5 virtual fun 1: %x, offset: %x\n", *(int*)*((int*)p5), &E::fun);
    printf("p5 virtual fun 2: %x, offset: %x\n", *((int*)*((int*)p5)+1), &E::funD);
    printf("p5 virtual fun 3: %x, offset: %x\n", *((int*)*((int*)p5)+2), &E::funE);
    printf("p6 virtual fun 1: %x, offset: %x\n", *(int*)*((int*)p6), &E1::fun);
    printf("p6 virtual fun 2: %x, offset: %x\n", *((int*)*((int*)p6)+1), &E1::funD);
    printf("p6 virtual fun 3: %x, offset: %x\n", *((int*)*((int*)p6)+2), &E1::funE1);

结果:

===========vritual address=========
vtable p1: 8049290
vtable p2: 8049258
vtable p3: 80492a0
vtable p4: 80492a0
vtable p5: 80492c0
vtable p6: 80492c0
p1 virtual fun: 8048e1e
p2 virtual fun: 8048e0a
p3 virtual fun: 8048e32
p4 virtual fun: 8048e32
p5 virtual fun 1: 8048e46, offset: 1
p5 virtual fun 2: 8048dce, offset: 5
p5 virtual fun 3: 8048de2, offset: 9
p6 virtual fun 1: 8048e46, offset: 1
p6 virtual fun 2: 8048dce, offset: 5
p6 virtual fun 3: 8048de2, offset: 9

小结

  • 取虚表(vtable)地址:*((int*)p1;
  • 取虚表中函数的地址:*(int*)*((int*)p1);
  • p1虽然在编译的时候fun是类A的,但是对象毕竟还是B对象,所以vptr的地址指向的依旧是B类的虚表地址,可以再添加一条打印b对象的虚表的测试代码,发现也是0x8049170;
  • p3和p4,p5和p6的虚表地址是一样的,这也说明了他们是指向同一个类的实例(对象)的;
  • p3和p4,p5和p6的第一个,第二个,第三个的虚函数地址都一样,也同样说明了是同一个类中的虚表;
  • 也可以看到funD的偏移量是5(和1之间的差是4,测试机器是32位的),funE和funE1都是偏移9,所以能够通过pE1调用E对象的第三个虚函数。

参考

[1]图说C++对象模型:对象内存布局详解

[2]C++虚表,你搞懂了吗?

[3]C/C++杂记:虚函数的实现的基本原理

[4]关于C++虚函数与普通函数的编译与调用机制

答案:A, C, D, D, D_test, E_test, D_test


Comments

Content