C++虚函数表

来源:互联网 发布:java怎么截取字符串 编辑:程序博客网 时间:2024/06/10 10:16

考虑最简单的有虚函数的继承关系:

class F{
public:
virtual void func() {
printf("F func\n");
}
};


class S : public F{
public:
virtual void func() {
printf("S func\n");
}
};

此时,我们可以定义一个父类的指针,实际指向一个子类的对象。调用func函数的结果是子类的函数。虚函数在这里是动态绑定的。


F *f = new S();

f->func(); 输出S func


我们知道子类即使不定义虚函数也会继承该虚函数表。而且一个类,除了继承的虚函数表之外,自行定义的虚函数公用一个虚函数表。即使继承的虚函数名称相同,但是来自于不同的父类,即继承了多个父类相同的虚函数,依旧是继承每个父类的虚函数表。

class F{
public:
virtual void func() {
printf("F func\n");
}
};
class M{
public:
virtual void func() {
printf("M func\n");
}
};


class S : public F ,public M{
public:
virtual void func() {
printf("S func\n");
}

       virtual void foo() {}

};
在这种情况下,S一共有三个虚函数表,继承自两个父类的,每个父类都有一个,以及自己定义的。 继承父类的,两个虚函数表中的func的指针都是指向了类S自己定义的函数。

需要注意的是,编译的时候,非虚函数会链接绑定,类本身会构造虚函数表,子类和父类的虚函数表项可能指向不同的地方。 

假设考虑定义的不是指针的情况。

S s;

F f = s;

f.func(); 输出F func

F f = s;这里应该是编译器默认为我们生成的赋值构造函数。

f对象 s对象的虚函数表是形成了,表项中的指针也已经确定了,指向各自的函数。 赋值构造函数,我们没有定义,默认的这里测试的结果是什么都没做。无论如何,这里赋值构造函数与虚函数表无关。

如果是指针的情况,如果是父类的指针指向子类的实例,父类的非虚函数以及和类本身绑定好了,依旧会调用父类的函数。但是虚函数要通过虚函数表进行访问,那么父类的指针就会访问到子类的虚函数表,从而调用子类的函数。

反过来,如果让子类的指针指向父类的对象,首先会编译错误,如果强制类型转换(都是指针嘛),你可能会看到段错误。因为子类的内存是大于父类的内存的,父类的一些数据在子类中顺序可能不是你想的那样了。。

虚函数表指针总是在类的成员变量之前。


一个单继承的例子:

class F{
public:
virtual void func() {
printf("F virtual \n");
}
virtual void T() {
printf("T virtual \n");
}
char a;
F():a(1) {}
};


class S : public F{
public:
virtual void func() {
printf("S virtual \n");
}
// virtual void T() {}
// virtual void m() {}
char b;
S():b(2) {}
};


class G : public S {
public:
virtual void func() {
printf("G virtual \n");
}
virtual void func2() {
printf("G2 virtual \n");
}
G():c(3) {}
double c;
};


class L {
public:
inline virtual void func() {
printf("L virtual \n");
}
};


int main()
{
A a;
B b;
printf("sizeof S = %d\n",sizeof(S));
G f;
typedef void (*Func)();
/**虚函数表的指针在类的最开始
* 该指针指向一个表,一个数组
* 首先获得是虚函数表指针的地址

这里用int的原因是4个字节,和指针占的字节一样

* */
int *vtableAddr = (int *)&f;
/* *vtableAddr 实际代表了虚函数表的地址
*/
int *pVtable = (int *)(*vtableAddr);


for( int i = 0; (Func)pVtable[i] != NULL ; ++i) {
Func p1 = (Func)pVtable[i];
p1();
}


vtableAddr = (int *)((char *)vtableAddr + 4);
printf("a = %d\n",*((char *)vtableAddr));
vtableAddr = (int *)((char *)vtableAddr + 1);
printf("b = %d\n",*((char *)vtableAddr));
vtableAddr = (int *)((char *)vtableAddr + 3);
printf("c = %f\n",*((double *)vtableAddr));

return 0;

}

我们可以看到一个类的内存布局:首先是虚函数表指针,然后是父类的对象,然后是子类的对象。这里输出子类的对象的偏移计算时,需要考虑字节对齐。由于使用的gcc编译器,默认对齐系数是4.所以偏移计算如上。



原创粉丝点击