虚构造函数里调用虚函数调用为什么不能在编译时确定

2012年5月 专题开发/技术/项目大版内专家分月排行榜第二2010年3月 C/C++大版内专家分月排行榜第二
2012年4月 Linux/Unix社区大版内专家分月排行榜第三2011年7月 Linux/Unix社区大版内专家分月排行榜第三2010年2月 C/C++大版内专家分月排行榜第三
本帖子已过去太久远了,不再提供回复功能。先看一段在构造函数中直接调用虚函数的代码:
1 #include &iostream&
3 class Base
Base() { Foo(); }
///& 打印 1
virtual void Foo()
std::cout && 1 && std::
14 class Derive : public Base
16 public:
Derive() : Base(), m_pData(new int(2)) {}
~Derive() { delete m_pD }
virtual void Foo()
std::cout && *m_pData && std::
24 private:
28 int main()
Base* p = new Derive();
  这里的结果将打印:1。
  这表明第6行执行的的是Base::Foo()而不是Derive::Foo(),也就是说:虚函数在构造函数中&不起作用&。为什么?
  当实例化一个派生类对象时,首先进行基类部分的构造,然后再进行派生类部分的构造。即创建Derive对象时,会先调用Base的构造函数,再调用Derive的构造函数。
  当在构造基类部分时,派生类还没被完全创建,从某种意义上讲此时它只是个基类对象。即当Base::Base()执行时Derive对象还没被完全创建,此时它被当成一个Base对象,而不是Derive对象,因此Foo绑定的是Base的Foo。
  C++之所以这样设计是为了减少错误和Bug的出现。假设在构造函数中虚函数仍然&生效&,即Base::Base()中的Foo();所调用的是Derive::Foo()。当Base::Base()被调用时派生类中的数据m_pData还未被正确初始化,这时执行Derive::Foo()将导致程序对一个未初始化的地址解引用,得到的结果是不可预料的,甚至是程序崩溃(访问非法内存)。
  总结来说:基类部分在派生类部分之前被构造,当基类构造函数执行时派生类中的数据成员还没被初始化。如果基类构造函数中的虚函数调用被解析成调用派生类的虚函数,而派生类的虚函数中又访问到未初始化的派生类数据,将导致程序出现一些未定义行为和bug。
  对于这一点,一般编译器会给予一定的支持。如果将基类中的Foo声明成纯虚函数时(看下面代码),编译器可能会:在编译时给出警告、链接时给出符号未解析错误(unresolved external symbol)。如果能生成可执行文件,运行时一定出错。因为Base::Base()中的Foo总是调用Base::Foo,而此时Base::Foo只声明没定义。大部分编译器在链接时就能识别出来。
1 #include &iostream&
3 class Base
Base() { Foo(); }
///& 可能的结果:编译警告、链接出错、运行时错误
virtual void Foo() = 0;
11 class Derive : public Base
13 public:
Derive() : Base(), m_pData(new int(2)) {}
~Derive() { delete m_pD }
virtual void Foo()
std::cout && *m_pData && std::
21 private:
25 int main()
Base* p = new Derive();
  如果编译器都能够在编译或链接时识别出这种错误调用,那么我们犯错的机会将大大减少。只是有一些比较不直观的情况(看下面代码),编译器是无法判断出来的。这种情况下它可以生成可执行文件,但是当程序运行时会出错。
1 #include &iostream&
3 class Base
Base() { Subtle(); }
///& 运行时错误(pure virtual function call)
virtual void Foo() = 0;
void Subtle() { Foo(); }
12 class Derive : public Base
14 public:
Derive() : Base(), m_pData(new int(2)) {}
~Derive() { delete m_pD }
virtual void Foo()
std::cout && *m_pData && std::
22 private:
26 int main()
Base* p = new Derive();
  从编译器开发人员的角度上看,如何实现上述的&特性&呢?
  我的猜测是在虚函数表地址的绑定上做文章:在&当前类&(正在被构造的类)的构造函数被调用时,将&当前类&的虚函数表地址绑定到对象上。当基类部分被构造时,&当前类&是基类,这里是Base,即当Base::Base()的函数体被调用时,Base的虚函数表地址会被绑定到对象上。而当Derive::Derive()的函数体被调用时,Derive的虚函数表地址被绑定到对象上,因此最终对象上绑定的是Derive的虚函数表。
  这样编译器在处理的时候就会变得很自然。因为每个类在被构造时不用去关心是否有其他类从自己派生,而不需要关心自己是否从其他类派生,而只要按照一个统一的流程,在自身的构造函数执行之前把自身的虚函数表地址绑定到当前对象上(一般是保存在对象内存空间中的前4个字节)。因为对象的构造是从最基类部分(比如A&-B&-C,A是最基类,C是最派生类)开始构造,一层一层往外构造中间类(B),最后构造的是最派生类(C),所以最终对象上绑定的就自然而然就是最派生类的虚函数表。
  也就是说对象的虚函数表在对象被构造的过程中是在不断变化的,构造基类部分(Base)时被绑定一次,构造派生类部分(Derive)时,又重新绑定一次。基类构造函数中的虚函数调用,按正常的虚函数调用规则去调用函数,自然而然地就调用到了基类版本的虚函数,因为此时对象绑定的是基类的虚函数表。
  下面要给出在WIN7下的Visual Studio2010写的一段程序,用以验证&对象的虚函数表在对象被构造的过程中是在不断变化的&这个观点。
  这个程序在类的构造函数里做了三件事:1.打印出this指针的地址;2.打印虚函数表的地址;3.直接通过虚函数表来调用虚函数。
  打印this指针,是为了表明创建Derive对象是,不管是执行Base::Base()还是执行Derive::Derive(),它们构造的是同一个对象,因此两次打印出来的this指针必定相等。
  打印虚函数表的地址,是为了表明在创建Derive对象的过程中,虚函数表的地址是有变化的,因此两次打印出来的虚函数表地址必定不相等。
  直接通过函数表来调用虚函数,只是为了表明前面所打印的确实是正确的虚函数表地址,因此Base::Base()的第19行将打印Base,而Derive::Derive()的第43行将打印Derive。
  注意:这段代码是编译器相关的,因为虚函数表的地址在对象中存储的位置不一定是前4个字节,这是由编译器的实现细节来决定的,因此这段代码在不同的编译器未必能正常工作,这里所使用的是Visual Studio2010。
1 #include &iostream&
3 class Base
Base() { PrintBase(); }
void PrintBase()
std::cout && "Address of Base: " && this && std::
// 虚表的地址存在对象内存空间里的头4个字节
int* vt = (int*)*((int*)this);
std::cout && "Address of Base Vtable: " && vt && std::
// 通过vt来调用Foo函数,以证明vt指向的确实是虚函数表
std::cout && "Call Foo by vt -& ";
void (*pFoo)(Base* const) = (void (*)(Base* const))vt[0];
(*pFoo)(this);
std::cout && std::
virtual void
Foo() { std::cout && "Base" && std:: }
27 class Derive : public Base
29 public:
Derive() : Base() { PrintDerive(); }
void PrintDerive()
std::cout && "Address of Derive: " && this && std::
// 虚表的地址存在对象内存空间里的头4个字节
int* vt = (int*)*((int*)this);
std::cout && "Address of Derive Vtable: " && vt && std::
// 通过vt来调用Foo函数,以证明vt指向的确实是虚函数表
std::cout && "Call Foo by vt -& ";
void (*pFoo)(Base* const) = (void (*)(Base* const))vt[0];
(*pFoo)(this);
std::cout && std::
virtual void Foo() { std::cout && "Derive" && std:: }
51 int main()
Base* p = new Derive();
输出的结果跟预料的一样:
1 Address of Base: 002E7F98
2 Address of Base Vtable:
3 Call Foo by vt -& Base
5 Address of Derive: 002E7F98
6 Address of Derive Vtable:
7 Call Foo by vt -& Derive
  在析构函数中调用虚函数,和在构造函数中调用虚函数一样。
  析构函数的调用跟构造函数的调用顺序是相反的,它从最派生类的析构函数开始的。也就是说当基类的析构函数执行时,派生类的析构函数已经执行过,派生类中的成员数据被认为已经无效。假设基类中虚函数调用能调用得到派生类的虚函数,那么派生类的虚函数将访问一些已经&无效&的数据,所带来的问题和访问一些未初始化的数据一样。而同样,我们可以认为在析构的过程中,虚函数表也是在不断变化的。
  将上面的代码增加析构函数的调用,并稍微修改一下,就能验证这一点:
1 #include &iostream&
3 class Base
Base() { PrintBase(); }
virtual ~Base() { PrintBase(); }
void PrintBase()
std::cout && "Address of Base: " && this && std::
// 虚表的地址存在对象内存空间里的头4个字节
int* vt = (int*)*((int*)this);
std::cout && "Address of Base Vtable: " && vt && std::
// 通过vt来调用Foo函数,以证明vt指向的确实是虚函数表
std::cout && "Call Foo by vt -& ";
void (*pFoo)(Base* const) = (void (*)(Base* const))vt[1];
///& 注意这里索引变成 1 了,因为析构函数定义在Foo之前
(*pFoo)(this);
std::cout && std::
virtual void
Foo() { std::cout && "Base" && std:: }
28 class Derive : public Base
30 public:
Derive() : Base() { PrintDerive(); }
virtual ~Derive() { PrintDerive(); }
void PrintDerive()
std::cout && "Address of Derive: " && this && std::
// 虚表的地址存在对象内存空间里的头4个字节
int* vt = (int*)*((int*)this);
std::cout && "Address of Derive Vtable: " && vt && std::
// 通过vt来调用Foo函数,以证明vt指向的确实是虚函数表
std::cout && "Call Foo by vt -& ";
void (*pFoo)(Base* const) = (void (*)(Base* const))vt[1];
///& 注意这里索引变成 1 了,因为析构函数定义在Foo之前
(*pFoo)(this);
std::cout && std::
virtual void Foo() { std::cout && "Derive" && std:: }
53 int main()
Base* p = new Derive();
下面是打印结果,可以看到构造和析构是顺序相反的两个过程:
1 Address of Base: 001E7F98
2 Address of Base Vtable:
3 Call Foo by vt -& Base
5 Address of Derive: 001E7F98
6 Address of Derive Vtable:
7 Call Foo by vt -& Derive
9 Address of Derive: 001E7F98
10 Address of Derive Vtable:
11 Call Foo by vt -& Derive
13 Address of Base: 001E7F98
14 Address of Base Vtable:
15 Call Foo by vt -& Base
  最终结论:
    1.&不要在构造函数和析构函数中调用虚函数,因为这种情况下的虚函数调用不会调用到外层派生类的虚函数(参考:、)。
    2. 对象的虚函数表地址在对象的构造和析构过程中会随着部分类的构造和析构而发生变化,这一点应该是编译器实现相关的。
注:以上的讨论是基于简单的单继承,对于多重继承或虚继承会有一些细节上的差别。
阅读(...) 评论()c/c++(61)
面试(21)
1.(mov ecx,dword ptr [ebp-0Ch])将this指针压入ecx
2.(mov edx,dword ptr [ecx])this指针指向该对象的首地址,而该处的前四个字节存放着该对象的虚函数表的首地址,将虚表指针放到edx中。
3.(&dword
ptr [edx+4])由于edx中存放着虚表指针,则edx+4表示调用该虚表中的第二个函数
4.执行到上述操作后,执行该条指令(jmp B::say ()),从而真正调用我们的虚函数!
&& 如果我们的程序是通过指向对象的指针或者是引用来调用该对象的虚函数,则在调用虚函数的过程需要查表(虚函数表)来调用真正的函数。
&& 调用的不是虚函数则不需要查表,在编译时即可确定调用的是那个函数。
&& 如果是通过对象来调用则对任何类型的函数都不需要查表。
&& 虚指针是在对象的构造函数中初始化的。
关于虚表指针的测试程序:
//virtual1.cpp
#include &iostream&
virtual void show(){cout&&&show&&&a_val&&}
virtual void say(){cout&&&say&&&}
inline void setVal(int val){a_val =}
int main()
a.setVal(123);
//受保护的虚函数是不能被直接调用的
//而通过虚表指针的地址拷贝则可实现函数的调运
//a.show();
//该指针存放虚表地址
int *des =
//该指针存放虚表中的第一个函数
int *ptr =
memcpy(des,&a,4);
memcpy(ptr,reinterpret_cast&int *&(*des),4);
void (*pshow)() = reinterpret_cast&void (*)()&(*ptr);
//依据__thiscall的调用约定将this指针传入ecx
//从而使虚函数能够正确地取出参数
int addre = reinterpret_cast&int&(&a);
mov ecx,addre
//获得私有的成员v_val
memcpy(des, reinterpret_cast&int *&(&a)+1, 4);
cout&&*des&&
//virtual2.cpp
#include &iostream&
#include &string&
virtual void show(std::string str)
int addr = reinterpret_cast&int&(this);
mov ecx,addr16
cout&&a_val&&
cout&&str&&
virtual void say(){cout&&&say&&&}
inline void setVal(int val){a_val =}};
class B: public A
void say(){cout&&&B in say&&&}
virtual void hello(){cout&&&hello&&&}
int main()
a.setVal(123);
//受保护的虚函数是不能被直接调用的
//而通过虚表指针的地址拷贝则可实现函数的调运
//a.show();
//该指针存放虚表地址
int *des =
//该指针存放虚表中的第一个函数
int *ptr =
memcpy(des,&a,4);
memcpy(ptr,reinterpret_cast&int *&(*des),4);
void (*pshow)(std::string) = reinterpret_cast&void (*)(std::string)&(*ptr);
int addre = reinterpret_cast&int&(&a);
mov ecx,addre57
string str(&hello world&);
pshow(str);
sub esp,10h65
//获得私有的成员v_val
memcpy(des, reinterpret_cast&int *&(&a)+1, 4);
cout&&*des&&
//cout&&*des&&
//cout&&*ptr&&
//virtual3.cpp
#include &iostream&
#include &string&
virtual void show()
cout&&&show&&&
virtual void say(){cout&&&say&&&}
inline void seta_Val(char val){a_val =}
inline char geta_Val()const{return a_}
class B: public A19 {
void say(){cout&&&B in say&&&}
virtual void hello(){cout&&&hello&&&}
inline void setb_Val(int val){b_val =}
inline int getb_Val()const{return b_}
int main()
b.setb_Val(123);
b.seta_Val('A');
int *vfptr =
memcpy(vfptr, &b, 4);
memcpy(pf, reinterpret_cast&int *&(*vfptr)+2, 4);
void (*pfun)() = reinterpret_cast&void (*)()&(*pf);
char *pa_val =
int *pb_val =
memcpy(pa_val, reinterpret_cast&int *&(&b)+1, sizeof(char));
memcpy(pb_val, reinterpret_cast&int *&(&b)+2, sizeof(int));
cout&&*pa_val&&
cout&&*pb_val&&
cout&&&&&&&&&&&&&&&&&&&&
*pa_val = 'B';
*pb_val = 999;
memcpy(reinterpret_cast&int *&(&b)+1, pa_val, sizeof(char));
memcpy(reinterpret_cast&int *&(&b)+2, pb_val, 4);
cout&&b.geta_Val()&&
cout&&b.getb_Val()&&
由以上测试程序可以得出以下结论:
1.c++对象(基类对象)的内存布局是:对象的内存地址(&a)所指向的内存中的前四个字节中存放的是该对象的虚函数表的首地址(前提是该对象有虚函数),接下来的内存中依次存放该对象的数据成员(非静态的数据成员)。
注意:对象的虚函数表中存放的实际上并不是虚函数的入口地址,而是一个跳转指令(jmp)的地址,该跳转指令,转向虚函数的入口,为了叙述方便,我这里作出约定:我们就认为虚函数表中就存放的是虚函数的入口地址。
虚函数的存放顺序与函数的顺序是相同的。
2.派生类的对象的内存布局是:前四个字节依然存放虚表指针,虚表中首先存放父类的虚函数地址,注意,由于派生类中也可能有①自己的虚函数,同时派生类也可以②重写父类的虚函数,虚函数表的分布如何:
对于情况一而言,将派生类新增加的虚函数地址依次添加到虚表(虚表中已经有父类的虚函数地址)的后面。
对于情况二而言,如果派生类重写了父类的虚,则将重写后的虚函数地址替换掉父类原来的虚函数地址,如果没有重写,则按照父类的虚表顺序存放虚函数地址
接下来的内存中依次存放该对象的父类的数据成员(非静态的数据成员),然后再存放派生类自己的数据成员。(还有内存对齐的问题)
#include &iostream&
#include &string&
int a_val2;
virtual void show(){cout&&&show&&&}
virtual void say(){cout&&&say&&&}
inline void seta_Val(char val){a_val =}
inline void seta_Val2(int val2){a_val2 = val2;}
inline char geta_Val()const{return a_}
inline int geta_Val2()const{return a_val2;}
class B: public A21 {
char b_val2;
void say(){cout&&&B in say&&&}
virtual void hello(){cout&&&hello&&&}
inline void setb_Val(int val){b_val =}
inline void setb_Val2(char val2){b_val2 = val2;}
inline int getb_Val()const{return b_}
inline char getb_Val2()const{return b_val2;}
int main()
b.seta_Val('A');
b.seta_Val2(1);
b.setb_Val(2);
b.setb_Val2('B');
int *vfptr =
memcpy(vfptr, &b, 4);
memcpy(pf, reinterpret_cast&int *&(*vfptr)+2, 4);
void (*pfun)() = reinterpret_cast&void (*)()&(*pf);
char *pa_val =
int *pb_val =
memcpy(pa_val, reinterpret_cast&int *&(&b)+1, sizeof(char));
memcpy(pb_val, reinterpret_cast&int *&(&b)+2, sizeof(int));
cout&&*pa_val&&59
cout&&*pb_val&&
memcpy(pb_val, reinterpret_cast&int *&(&b)+3, sizeof(int));
//存在内存对齐的问题
memcpy(pa_val, reinterpret_cast&int *&(&b)+4, sizeof(char));
cout&&*pb_val&&66
cout&&*pa_val&&
cout&&&&&&&&&&&&&&&&&&&&
*pa_val = 'B';
*pb_val = 999;
memcpy(reinterpret_cast&int *&(&b)+1, pa_val, sizeof(char));
memcpy(reinterpret_cast&int *&(&b)+2, pb_val, 4);
cout&&b.geta_Val()&&
cout&&b.getb_Val()&&
&&相关文章推荐
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:192017次
积分:1928
积分:1928
排名:千里之外
原创:12篇
转载:154篇
评论:15条
(1)(15)(35)(6)(1)(1)(2)(2)(5)(5)(10)(11)(6)(15)(1)(5)(5)(5)(10)(8)(2)(3)(12)
(window.slotbydup = window.slotbydup || []).push({
id: '4740881',
container: s,
size: '200,200',
display: 'inlay-fix'2005年1月 C/C++大版内专家分月排行榜第二
2007年6月 总版技术专家分月排行榜第三
2007年6月 VC/MFC大版内专家分月排行榜第一
2007年6月 总版技术专家分月排行榜第三
2007年6月 VC/MFC大版内专家分月排行榜第一
2005年11月 Java大版内专家分月排行榜第二
2007年6月 Java大版内专家分月排行榜第三2005年12月 Java大版内专家分月排行榜第三
本帖子已过去太久远了,不再提供回复功能。

我要回帖

更多关于 调用纯虚函数 的文章

 

随机推荐