昆仑  
公告
  • 昵称:ttylikl
    园龄:3年1个月
    粉丝:0
    关注:0
日历
<2012年2月>
2930311234
567891011
12131415161718
19202122232425
26272829123
45678910
统计
  • 随笔 - 31
  • 文章 - 0
  • 评论 - 108
  • 引用 - 0

导航

搜索

 

常用链接

我的标签

随笔分类

随笔档案

我的连接

最新评论

阅读排行榜

评论排行榜

推荐排行榜

 

2010年5月31日

2009年11月12日

大家可以先思考,想想如何写出这样的代码,然后再看看下面的代码是否与你的思路差不多。

注意:  展开后有很长一段字符数组定义(char sz[]=....)

 

Code

 

 

BTW,代码是我的同事LiuB完成的,必须得提及一下,不然会被K的。。。:)

posted @ 2009-11-12 19:51 ttylikl 阅读(1683) 评论(15) 编辑

2009年8月5日

遇到这个BUG已经过了好几天了,很小的一个问题。本不准备再去提及的,后来想想,还是应该写出来以警示自己。

问题发生于QQ群里的网友在编写一个字符串反转的函数,在测试的时候莫名其妙的遇到了段错误的问题,程序无法正常执行,但是光看函数代码貌似没什么问题,围绕着那个函数翻来覆去的看了N遍也没搞明白。最后却在函数外发现了问题。且以下面的代码做一个示例,演示一下这个BUG吧:

 1:  #include <iostream>
 2:  
 3:  void string_replace(char *p,char src,char ch)
 4:  {
 5:      char *head=p;
 6:      while(*head!=0)
 7:      {
 8:          if(*head==src)
 9:              *head=ch;
10:          ++head;
11:      }
12:  }
13:   
14:  int main()
15:  {
16:      char *str="This is a test!";
17:      string_replace(str,' ','_');
18:      return 0;
19:  }

 

如果你粗心大意的话,也可能会像我当时一样绕了半天没搞明白。错误原因其实很简单————————————不能修改常量!

虽然这个函数是对一个char *的指针所指的内存进行操作,从函数本身逻辑上来说,可能是不存在任何问题的。然而在函数之外却发生了问题,char *str虽然是一个普通的char *指针,但是它却指向了一个常量字符串"This is a test!",当string_replace试图修改str的时候,实际上是试图去修改常量区的信息,这是被禁止的,无论是windows还是linux,你的程序都会立即被终止掉!

好了,错误原因明白了,让我们验证试试看吧,将16行代码进行下面的修改:

1:  //char *str="This is a test!";
2:  char str[100];
3:  strcpy(str,"This is a test!");

这次,我们将str放到栈里,然后从常量区将字符串内容复制到str里,再对其调用string_replace。这次,程序再也不会崩溃了,不用说,函数也能如预期一般的完成功能啦!

小小总结一下:

很多时候,错误往往来自自己最容易忽视的地方,细心才是避免和检查BUG最重要的态度。

posted @ 2009-08-05 00:36 ttylikl 阅读(3135) 评论(20) 编辑

2009年8月4日

 

逗号操作符?C++里有逗号操作符么?

不少C++ newbie都会问到这问题,大家对+-*/自然是认识的了,对其他的%!&|之类的也不觉得陌生,但是逗号操作符?可能有一半的人会说不清楚它到底是干啥的。

其实,我们是经常会用到逗号操作符的,但是并不是所有代码里出现的逗号都是逗号操作符。

让我们先从一个类的示例代码开始:

 1:   
 2:  class mynum
 3:  {
 4:  public:
 5:      mynum(double ndb,...)
 6:      {
 7:          //init with arguments
 8:      }
 9:  };
10:  class someclass
11:  {
12:      someclass():
13:          num(3,4)
14:              ,a(0)
15:              ,b(0)
16:              ,c(15)
17:      {
18:          int i,j; 
19:          i=1,2; //int x=1,2;
20:          for(;b<10;++b,--c)
21:          {
22:              ++a;
23:          }
24:      }
25:      mynum num;
26:      int a;
27:      int b,c;
28:  };

 

在这段代码里,逗号在5,13-16,18-20,27行都出现了。

现在先让我们来看看逗号操作符是啥意思:

一个包含逗号的表达式首先计算逗号左边的表达式,然后计算逗号右边的表达式;整个表达式的结果是逗号右边表达式的值。

然后我们看看上面的代码:

第5行是在mynum类的构造函数里出现的逗号,只是用来区分开第一个double类型的参数和后面的可变参数。这是一个特例,如果在构造mynum对象的时候,本来是要传入3.4作为第一个参数的,结果不小心把小数点变成了逗号的话,3.4就变成了两个参数3和4——比如在第13行的情况。类似这样的情况编译器不会报错,程序也能运行,但是mynum类里的成员变量可能就因为构造函数传入的变量值变化而导致程序运行结果完全不一致了。这两个逗号都不是逗号操作符了。

第14,15,16行的逗号,也不是逗号操作符。这三个逗号只是用来间隔构造函数的成员变量初始化列表。

第18行的int i,j;是大家都很常用的变量声明的语句,逗号在这里,也只是起了一个间隔变量声明的作用,也不是逗号运算符。

第19行的逗号,如果不认识逗号操作符的话,一定会认为这肯定是写错了!其实这正是逗号操作符了。让我们看看,经过i=1,2;这个语句后,i 的值应该是多少呢?按照之前逗号操作符的定义,我们好像可以很轻松的得出i=2。然而,这个答案是错的!你在得出错误答案的时候,忽略了一个重要的因素——运算符优先级!由于=号的优先级更高,所以i=1,2;这个语句相当于(i=1),2; ,如果要得到期望的2,我们应该这样来写i=(1,2);。下面的代码可以验证这个说法的(注意第3行是无意义代码,但是可不是错误代码哦!)

 

1:  int i,j;
2:  i=1,2;
3:  3,4;
4:  j=(1,2);
5:  printf("%d\n%d\n",i,j);

 

继续看第一份代码里的第20行,这是一个for循环代码。这里的逗号,也是逗号操作符哦。其实for循环也是逗号操作符经常出现的地方哦。for循环里只能写一个表达式,而逗号表达式这个时候就可以让你完成两个甚至多个表达式的计算。比如“for(;b<10;++b,--c)”,++b--c就会在每次循环中都被执行到。

 

第27行就无须多说了。也就是一普通的变量声明语句,这里的逗号也不是逗号操作符的。

 

总的来说,逗号操作符并不是一个非常常用的操作符,它只在某些特定的上下文环境里会有很好的效果,同时,在编码中还要非常小心错误的输入的逗号引发错误的问题。

比如逗号和括号就会引发一些潜在问题,如果在编码的时候一疏忽就会产生一些难以追查的隐藏bug,只有遵守一些既定的编码规范,养成良好严谨的编码习惯,才有助于避免一些诸如此类的问题出现。看看下面的代码:

 1:  int myadd(int i,int j,int k)
 2:  {
 3:      return i+j+k;
 4:  }
 5:  int myadd(int i,int j)
 6:  {
 7:      return i+j;
 8:  }
 9:   
10:  int main()
11:  {
12:   
13:      for(int x=0;x<9;++x)
14:      {
15:          printf("%d",myadd((2,3),4));
16:      }
17:  }

 

由于在第15行,错误的添加了括号(在实际编码中这样的情况太常见了),结果导致整个的输出全错了!因为(2,3)这个逗号表达式的返回值是3,所以15行相当于是myadd(3,4)了!

so,一定要记住啦,小心,小心,再小心!^_^

posted @ 2009-08-04 23:02 ttylikl 阅读(1604) 评论(1) 编辑

2009年8月1日

链接:

关注问题:

  • 虚函数的作用
  • 虚函数的实现原理
  • 虚函数表在对象布局里的位置
  • 虚函数的类的sizeof
  • 纯虚函数的作用
  • 多级继承时的虚函数表内容
  • 虚函数如何执行父类代码
  • 多继承时的虚函数表定位,以及对象布局
  • 虚析构函数的作用
  • 虚函数在QT中的应用
  • 虚函数与inline修饰符,static修饰符

虚析构函数

大家都知道,在C++里需要自己严格管理好资源的分配和回收。通常情况下,在一个对象被析构的时候,是要由其释放其申请到的各种资源的。最常见的,当然就是内存资源啦。

当只有一个类的时候,我们可以不用考虑太多,只要在析构函数里检查并释放所有申请到的资源即可。但是在这个类继承了一个抽象接口基类时,就有点点不一样了。让我们看看类的析构过程:

在大多数的类的使用时,通常都是直接删除该类的实例对象,然后该类的析构函数就会被调用,从而使得这个类在析构函数里执行的资源释放代码被执行到。

如果这个类继承了其他类,那么编译器还会在这个类的析构函数里自动添加对父类的析构函数的调用,从而将父类里申请的资源也进行释放。如果偶多个父类,也会依次调用各个析构函数。

倘若继承的是一个抽象接口类,并且在程序运行期,可能通过一个基类指针将此对象释放掉,那么致命而又隐藏的内存泄露BUG就出现啦。。。因为试图删除的是基对象,删除时调用的是基类的析构函数,而基类的析构函数当然是不会去调用子类的析构函数的罗!

让我们看看下面的代码,使用vs2008编译并运行的时候,将会在程序运行结束时报告内存泄漏情况(如果要在linux下编译测试,需要去掉第一行的include,以及return前的_CrtDumpMemoryLeaks()函数,然后使用linux下检查内存泄露的工具进行测试)。

//Source filename: Win32Con.cpp
#include
class parent
{
public:
	parent()	{	}
	/*virtual */ ~parent()	{	}
};

class child:public parent
{
public:
	child()
	{
		p=new char[1000];
	}
	~child()
	{
		delete[] p;
	}
	char *p;
};

void free_child(parent *pp)
{
	delete pp;
}

int main()
{
	child *obj=new child();
	free_child(obj);
	_CrtDumpMemoryLeaks();
	return 0;
}

在这段代码里我们创建的是一个child类型的对象,然后使用free_child(parent*)函数来试图释放这个对象,这个时候,只会调用到parent::~parent()这个析构函数,而不会调用到child::~child()!

如何解决这个问题呢?

很简单的,只要在parent::~parent()前增加 virtual关键字,将其变成一个虚函数。这样,无论是以这个对象的父类指针进行删除的时候,就会从虚函数表里定位到子类child的析构函数,这样就能够从子类开始一级一级的向上调用析构函数,从而正确的将这个对象在各个继承层次上申请的所有资源都释放掉。

正因为这个原因,在很多C++编程原则的文章或者书里都会提到这样的原则:

如果一个类要被设计为可被继承的基类,那么其析构函数应该被声明为虚函数。

虚函数在QT中的应用

在QT里虚函数的应用非常的广泛,事实上,在大多数的C++类库里都不可避免的要使用到虚函数。这里简单的列举QT里使用虚函数的情况:

QT的事件机制

是使用了虚函数的,你因此才可以自定义事件处理函数。比如最核心的QObject类的定义里(在qobject.h里),我们可以看到如下的虚函数定义:

virtual bool event(QEvent *);

virtual bool eventFilter(QObject *, QEvent *);

然后,在QWidget类继承QObject类后重新实现了上面的两个虚函数,完成很多窗口控件类的缺省事件处理。

当你要编写自定义的QT控件的时候,对event虚函数的重新实现就更是重要啦。

QT的信号和槽

QT的槽函数可以被声明为虚函数,所以虽然QT在实现信号和槽机制的时候可能出于效率或者运行代价的原因未采用虚函数机制,但是我们依然可以在必要的时候使用虚函数来完成一些特定功能。比如为一些自定义控件类抽象出来一个抽象接口基类,在做信号和槽的连接的时候是对基类指针进行操作,而在基类里的槽定义为虚函数,那么虚函数在此依然可以实现信号与槽的多态。

然而虚函数在调用的时候,一定要经历查表的步骤,是存在一定的运行开销的,对于一些非常频繁的槽调用还是应该考虑到使用虚函数产生的代价的。

其他

在虚函数上,static和inline这两个关键词与virtual显得很不友好。

从语义上即可看出,static和virtual完全就是冲突的,所以如果你试图为一个虚函数增加一个static限定词,那么你的C++编译器就会很负责任的报告一个严重错误给你。

而inline的含义和虚函数其实也是非常冲突的,但是inline在语法上只是给编译器一个建议,而不是强制的语义限定,所以C++编译器应该会忽略掉inline关键词,继续正常的编译。

~END~

posted @ 2009-08-01 20:08 ttylikl 阅读(207) 评论(0) 编辑
 

链接:

关注问题:

  • 虚函数的作用
  • 虚函数的实现原理
  • 虚函数表在对象布局里的位置
  • 虚函数的类的sizeof
  • 纯虚函数的作用
  • 多级继承时的虚函数表内容
  • 虚函数如何执行父类代码
  • 多继承时的虚函数表定位,以及对象布局
  • 虚析构函数的作用
  • 虚函数在QT的信号与槽中的应用
  • 虚函数与inline修饰符,static修饰符

前面我们尝试了一个简单的例子,接下来尝试一个多级继承的例子,以及一个多继承的例子。主要涉及到以下问题:多级继承时虚函数表的内容是如何填写的,如何在多级继承的情况下调用某一级父类里的虚函数,以及在多继承(多个父类)的情况下的对象布局。

多级继承

在这里,多级继承指的是有3层或者多层继承关系的情形。让我们看看下面的代码:

多层继承代码示例
  1. //Source filename: Win32Con.cpp
  2.  #include <iostream>
  3.  using namespace std;
  4. class parent1
  5.  {
  6. public:
  7.     virtual int fun1(){cout<<"parent1::fun1()"<<endl;return 0;};
  8.     virtual int fun2()=0;
  9. };
  10. class child1:public parent1
  11.  {
  12. public:
  13.     virtual int fun1()
  14.     {
  15.         cout<<"child1::fun1()"<<endl;
  16.         parent1::fun1();
  17.         return 0;
  18.     }
  19.     virtual int fun2()
  20.     {
  21.         cout<<"child1::fun2()"<<endl;
  22.         return 0;
  23.     }
  24. };
  25. class grandson:public child1
  26.  {
  27. public:
  28.     virtual int fun2()
  29.     {
  30.         cout<<"grandson::fun2()"<<endl;
  31.         //parent1::fun2();
  32.         parent1::fun1();
  33.         child1::fun2();
  34.         return 0;
  35.     }
  36. };
  37. void test_func1(parent1 *pp)
  38. {
  39.     pp->fun1();
  40.     pp->fun2();
  41. }
  42. int main(int argc, char* argv[])
  43. {
  44.     grandson sunzi;
  45.     test_func1(&sunzi);
  46.     return 0;
  47. }

这段代码展示了三个class,分别是parent1,child1,grandson。

  • 类parent1定义了两个虚函数,其中fun2是一个纯虚函数,这个类是一个不可实例化的抽象基类。
  • 类child1继承了parent1,并且对两个虚函数fun1和fun2都编写了实现的代码,这个类可以被实例化。
  • 类grandson继承了child1,但是只对虚函数fun2编写了实现的代码。

此外,我们还改写了test_func1函数,它的参数为parent1类型的指针,我们可以将parent1的子孙类作为这个函数的参数传入。在这个函数里,我们将依次调用parent1类的两个虚函数。

可以先通过阅读代码预测一下程序的输出内容。

程序的输出内容将是:


child1::fun1()
parent1::fun1()
grandson::fun2()
parent1::fun1()
child1::fun2()

先看第一行输出child1::fun1(),为什么会输出它呢?我们定义的具体对象sunzi是grandson类型的,test_func1的参数类型是parent1类型。在调用这个虚函数的时候,是完成了一次怎样的调用过程呢?

让我们再次使用cl命令输出这几个类的对象布局:

class parent1   size(4):
        +---
 0      | {vfptr}
        +---

parent1::$vftable@:
        | &parent1_meta
        |  0
 0      | &parent1::fun1
 1      | &parent1::fun2

parent1::fun1 this adjustor: 0
parent1::fun2 this adjustor: 0

class child1    size(4):
        +---
        | +--- (base class parent1)
 0      | | {vfptr}
        | +---
        +---

child1::$vftable@:
        | &child1_meta
        |  0
 0      | &child1::fun1
 1      | &child1::fun2

child1::fun1 this adjustor: 0
child1::fun2 this adjustor: 0

class grandson  size(4): //grandson的对象布局
        +---
        | +--- (base class child1)
        | | +--- (base class parent1)
 0      | | | {vfptr}
        | | +---
        | +---
        +---

grandson::$vftable@:  //grandson虚函数表的内容
        | &grandson_meta
        |  0
 0      | &child1::fun1
 1      | &grandson::fun2

grandson::fun2 this adjustor: 0

因为我们实例化的是一个grandson对象,让我们看看它的对象布局。正如前面的例子一样,里面只有一个vfptr指针,但是不一样的却是这个指针所指的虚函数表的内容:

第一个虚函数,填写的是child1类的fun1的地址;第二个虚函数填写的才是grandson类的fun2的地址。

很显然我们可以得出这样一个结论:在一个子对象的虚函数表里,每一个虚函数的实际运行的函数地址,将填写为在继承体系里最后实现该虚函数的函数地址。

所以当我们在test_func1里调用了传入的parent1指针的fun1函数的时候,我们实际执行的是填写在虚函数表里的child1::fun1(),而调用fun2函数的时候,是从虚函数表里得到了grandson::fun2函数的地址并调用之。在“程序输出结果”表里的第一行和第三行结果证实了上述结论。

再看一下程序代码部分的child1::fun1()的实现代码,在第18行,我们有parent1::fun1();这样的语句,这行代码输出了运行结果里的第二行,而在grandson::fun2()的实现代码第35行的parent1::fun1();以及第36行的child1::fun2();则输出了运行结果里的第四行和第五行的内容。这三行代码展示了如何调用父类以及更高的祖先类里的虚函数。——事实上,这与调用父类的普通函数没有任何区别。

在程序代码的第34行,有一行被注释了的内容//parent1::fun2();,之所以会注释掉,是因为这样的代码是无法通过编译的,因为在parent1类里,fun2是一个“纯虚函数”也就是说这个函数没有代码实体,在编译的时候,链接器将无法找到fun2的目标代码从而报错。

其实有了对虚函数的正确的认识,上面的多级继承是很自然就能明白的。然而在多继承的情况下,情况就有所不同了。。。

多继承下虚函数的使用

假如一个类,它由多个父类继承而来,而在不同的父类的继承体系里,都存在虚函数的时候,这个类的对象布局又会是怎样的?它又是怎样定位虚函数的呢?

让我们看看下面的代码:

程序输出结果
  1. //Source filename: Win32Con.cpp
  2.  #include <iostream>
  3.  using namespace std;
  4. class parent1
  5.  {
  6. public:
  7.     virtual int fun1(){cout<<"parent1::fun1()"<<endl;return 0;};
  8. };
  9. class parent2
  10.  {
  11. public:
  12.     virtual int fun2(){cout<<"parent2::fun2()"<<endl;return 0;};
  13. };
  14. class child1:public parent1,public parent2
  15.  {
  16. public:
  17.     virtual int fun1()
  18.     {
  19.         cout<<"child1::fun1()"<<endl;
  20.         return 0;
  21.     }
  22.     virtual int fun2()
  23.     {
  24.         cout<<"child1::fun2()"<<endl;
  25.         return 0;
  26.     }
  27. };
  28. void test_func1(parent1 *pp)
  29. {
  30.     pp->fun1();
  31. }
  32. void test_func2(parent2 *pp)
  33. {
  34.     pp->fun2();
  35. }
  36. int main(int argc, char* argv[])
  37. {
  38.     child1 chobj;
  39.     test_func1(&chobj);
  40.     test_func2(&chobj);
  41.     return 0;
  42. }

这一次,我们有两个父类,parent1和parent2,在parent1里定义了虚函数fun1,而在parent2里定义了虚函数fun2,然后我们有一个子类child1,在里面重新实现了fun1和 fun2两个虚函数。然后我们编写了test_func1函数来调用parent1类型对象的fun1函数,编写了test_func2函数调用parent2对象的fun2函数。在main函数里我们实例化了一个child1类型的对象chobj,然后分别传给test_func1和test_func2去执行。

这段代码的运行结果非常简单就能看出来:

child1::fun1()

child1::fun2()

但是,让我们看看对象布局吧:

class child1    size(8):
        +---
        | +--- (base class parent1)
 0      | | {vfptr}
        | +---
        | +--- (base class parent2)
 4      | | {vfptr}
        | +---
        +---

child1::$vftable@parent1@:
        | &child1_meta
        |  0
 0      | &child1::fun1

child1::$vftable@parent2@:
        | -4
 0      | &child1::fun2

child1::fun1 this adjustor: 0
child1::fun2 this adjustor: 4

注意到没?在child1的对象布局里,出现了两个vfptr指针!

这两个虚函数表指针分别继承于parent1和parent2类,分别指向了不同的两个虚函数表。

问题来了,当我们使用test_func1调用parent1类的fun1函数的时候,调用个过程还比较好理解,可以从传入的地址参数取得继承自parent1的vfptr,从而执行正确的fun1函数代码,但是当我们调用test_func2函数的时候,为什么程序可以自动取得来自parent2的vfptr呢,从而得出正确的fun2函数的地址呢?

其实,这个工作是编译器自动根据实例的类型完成的,在编译阶段就已经确定了在调用test_func2的时候,传入的this指针需要增加一定的偏移(在这里则是第一个vfptr所占用的大小,也就是4字节)。

我们可以看看main函数里这部分代码的反汇编代码:

反汇编代码
  1.     child1 chobj;
  2. 00F5162E 8D 4D F4         lea         ecx,[chobj] 
  3. 00F51631 E8 F5 FB FF FF   call        child1::child1 (0F5122Bh) 
  4.     test_func1(&chobj);
  5. 00F51636 8D 45 F4         lea         eax,[chobj] 
  6. 00F51639 50               push        eax  
  7. 00F5163A E8 6F FB FF FF   call        test_func1 (0F511AEh) 
  8. 00F5163F 83 C4 04         add         esp,4 
  9.     test_func2(&chobj);
  10. 00F51642 8D 45 F4         lea         eax,[chobj] 
  11. 00F51645 85 C0            test        eax,eax 
  12. 00F51647 74 0E            je          main+47h (0F51657h) 
  13. 00F51649 8D 4D F4         lea         ecx,[chobj] 
  14. 00F5164C 83 C1 04         add         ecx,4 
  15. 00F5164F 89 8D 2C FF FF FF mov         dword ptr [ebp-0D4h],ecx 
  16. 00F51655 EB 0A            jmp         main+51h (0F51661h) 
  17. 00F51657 C7 85 2C FF FF FF 00 00 00 00 mov         dword ptr [ebp-0D4h],0 
  18. 00F51661 8B 95 2C FF FF FF mov         edx,dword ptr [ebp-0D4h] 
  19. 00F51667 52               push        edx  
  20. 00F51668 E8 F6 FA FF FF   call        test_func2 (0F51163h) 
  21. 00F5166D 83 C4 04         add         esp,4 
  22.     return 0;

从第4行至第5行,执行的是test_func1函数,this指针指向 chobj (第2行lea ecx,[chobj]),但是调用test_func2函数的时候,this指针被增加了4(第14行)!于是,在test_func2执行的时候,就可以从&chobj+4的地方获得vfptr指针,从而根据parent2的对象布局得到了fun2的地址并执行了。

为了证实这点,我们可以将代码做如下的修改:

1:  int main(int argc, char* argv[])


2:  {


3:      child1 chobj;


4:      test_func1(&chobj);


5:      test_func2((parent2 *)(void *)&chobj);


6:      return 0;


7:  }


8:  

请注意红色部分的变化,在讲chobj传入给test_func2之前,先用(void *)强制转换为无类型指针,再转换为parent2 指针,这样的转换,显然是可行的,因为chobj本身就是parent2的子类,然而,程序的执行效果却是:

child1::fun1()

child1::fun1()

执行test_func2函数,调用的是parent2::fun2,但是居然执行的是child1::fun1()函数!!!

这中间发生了些什么呢?我们再看看反汇编的代码:

反汇编的代码
  1.     child1 chobj;
  2. 013D162E 8D 4D F4         lea         ecx,[chobj] 
  3. 013D1631 E8 F5 FB FF FF   call        child1::child1 (13D122Bh) 
  4.     test_func1(&chobj);
  5. 013D1636 8D 45 F4         lea         eax,[chobj] 
  6. 013D1639 50               push        eax  
  7. 013D163A E8 6F FB FF FF   call        test_func1 (13D11AEh) 
  8. 013D163F 83 C4 04         add         esp,4 
  9.     test_func2((parent2*)(void *)&chobj);
  10. 013D1642 8D 45 F4         lea         eax,[chobj] 
  11. 013D1645 50               push        eax  
  12. 013D1646 E8 18 FB FF FF   call        test_func2 (13D1163h) 
  13. 013D164B 83 C4 04         add         esp,4 
  14.     return 0;

从调用test_func2的反汇编代码可以看到,这一次ecx寄存器的值没有做改变!所以在执行test_func2的时候,将取得parent1对象布局里的vfptr,而这个vfptr所指的虚函数表里的第一项就是fun1,并且被填写为child1::fun1的地址了。所以才出现了child::fun1的输出内容!显然这里有一个隐藏的致命问题,加入parent1和parent2的第一个虚函数的参数列表不一致,这样的调用显然就会导致堆栈被破坏掉,程序99%会立即崩溃。之前的程序没有崩溃并且成功输出内容,不过是因为parent1::fun1()和parent2::fun2()的参数列表一致的关系而已。
所以,千万不要在使用一个多继承对象的时候,将其类型信息丢弃,编译器还需要依靠正确的类型信息,在使用虚函数的时候来得到正确的汇编代码!

多继承与虚函数重复

既然说到了多继承,那么还有一个问题可能会需要解决,那就是如果两个父类里都有相同的虚函数定义,在子对象的布局里会是怎么样个情况?是否依然可以将这个虚函数指向到正确的实现代码上呢?

修改前面一个源代码,在parent2的接口里增加下面的虚函数定义:

virtual int fun1(){cout<<"parent2::fun1()"<<endl;return 0;};

上面的fun1的定义与parent1类里的完全重复相同(类型,参数列表),增加上面的代码后立即开始编译,程序正常编译通过。运行之,得到下面的结果:

child1::fun1()

child1::fun2()

这个程序居然正确的完成了执行,编译器在其中做了些怎样的工作,是怎么样避免掉队fun1函数的冲突问题呢?

让我们来看看这个时候的child1的对象布局:

class child1    size(8):
        +---
        | +--- (base class parent1)
 0      | | {vfptr}
        | +---
        | +--- (base class parent2)
 4      | | {vfptr}
        | +---
        +---

child1::$vftable@parent1@:
        | &child1_meta
        |  0
 0      | &child1::fun1

child1::$vftable@parent2@:
        | -4
 0      | &child1::fun2
 1      | &thunk: this-=4; goto child1::fun1

child1::fun1 this adjustor: 0
child1::fun2 this adjustor: 4

恩~~~还是两个vfptr在child1的对象布局里(不一样就怪啦,呵呵),但是第二个vfptr所指的虚函数表的内容有所变化哦!

注意看红色字体部分,虚函数表里并没有直接填写child::fun1的代码,而是多了一个 &thunk: this-=4;然后才goto child1::fun1!注意到一个关键名词thunk了吧?没错,vc在这里使用了名为thunk的技术,避免了虚函数fun1在两个基类里重复出现导致的冲突问题!(除了thunk,还有其他方法可以解决此类问题的)。

现在,我们知道为什么相同的虚函数不会在子类里出现冲突的情况了。

但是,倘若我们在基类里就是由两个冲突的普通函数,而不是虚函数,是个怎样的情况呢?

多继承产生的冲突与虚继承,虚基类

我们在parent1和parent2里添加一个相同的函数void fun3(),然后再进行编译,通过了!查看类对象布局,跟上面的完全一致。但是在main函数里调用chobj.fun3()的时候,编译器却不再能正确编译了,并且会提示“error C2385: 对“fun3”的访问不明确”的错误信息,没错,编译器不知道你要访问哪个fun3了。

如何解决这样的多继承带来的问题呢,其实有一个简单的做法。就是在方法前限定引用的具体是哪个类的函数,比如:chobj.parent1::fun3();  ,这样的写法就写明了是要调用chobj的父类parent1里的fun3()函数!

我们再看看另外一种情况,从parent1和parent2里抹去刚才添加的fun3函数,将之放到一个共同的基类里:

class commonbase


 {


public:


    void fun3(){cout<<"commonbase::fun3()"<<endl;}


};

而parent1和parent2都修改为从此类继承。可以看到,在这个情况下,依然需要使用chobj.parent1::fun3();  的方式才可以正确调用到fun3,难道,在这种情况下,就不能自然的使用chobj.fun3()这样的方式了吗?

虚继承可以解决这个问题——我们在parent1和parent2继承common类的地方添加上一个关键词virtual,如下:

class parent1:virtual public commonbase


 {


public:


    virtual int fun1(){cout<<"parent1::fun1()"<<endl;return 0;};


};

给parent2也同样的处理,然后再次编译,这次chobj.fun3()可以编译通过了!!!

编译器这次又在私下里做了哪些工作了呢????

class child1    size(16):
        +---
        | +--- (base class parent1)
 0      | | {vfptr}
 4      | | {vbptr}
        | +---
        | +--- (base class parent2)
 8      | | {vfptr}
12      | | {vbptr}
        | +---
        +---
        +--- (virtual base commonbase)
        +---

child1::$vftable@parent1@:
        | &child1_meta
        |  0
 0      | &child1::fun1

child1::$vftable@parent2@:
        | -8
 0      | &child1::fun2
 1      | &thunk: this-=8; goto child1::fun1

child1::$vbtable@parent1@:
 0      | -4
 1      | 12 (child1d(parent1+4)commonbase)

child1::$vbtable@parent2@:
 0      | -4
 1      | 4 (child1d(parent2+4)commonbase)

child1::fun1 this adjustor: 0
child1::fun2 this adjustor: 8

vbi:       class  offset o.vbptr  o.vbte fVtorDisp
      commonbase      16       4       4 0

这次变化可大了去了!!!

首先,可以看到两个类parent1和parent2的对象布局里,都多了一个vbptr的指针。而在child1的对象布局里,还有一个virtual base commonbase的虚拟基类。再看看两个vbptr的内容:

12 (child1d(parent1+4)commonbase) 这个很好理解,从parent1的vbptr开始,偏移12个字节,指向的是virtual base commonbase!

再看看4 (child1d(parent2+4)commonbase) ,从parent2的vbptr开始,便宜4个字节,也指向了virtual base commonbase!

这下明白了。虚基类在child1里只有一个共同的对象布局了,所以就可以直接用chobj.fun3()啦,当然,在commonbase里的其他成员变量此时也可以同样的方式访问了!

虽然解决方案有了,但是在一个系统的设计里,如果有一个基类出现多继承冲突的情况,大部分情况下都说明这样的设计是有问题的,应该尽量避免这样的设计,并且尽量用纯虚函数,来提取一些抽象的接口类,把共同的方法接口都抽取出来,通常就能避免多继承的问题。

posted @ 2009-08-01 20:07 ttylikl 阅读(256) 评论(0) 编辑
 

链接:

关注问题:

  • 虚函数的作用
  • 虚函数的实现原理
  • 虚函数表在对象布局里的位置
  • 虚函数的类的sizeof
  • 纯虚函数的作用
  • 多级继承时的虚函数表内容
  • 虚函数如何执行父类代码
  • 多继承时的虚函数表定位,以及对象布局
  • 虚析构函数的作用
  • 虚函数在QT的信号与槽中的应用
  • 虚函数与inline修饰符,static修饰符

啰嗦两句

虚函数在C++里的作用是在是非常非常的大,很多讲述C++的文章都会讲到它,要用好C++,就一定要学好虚函数。网络上可以google到很多很多关于它的文章,这一次的学习,我不准备去只是简单的阅读了解那些文章,而是希望通过编写一些测试代码,来对虚函数的一些实现机制,以及C++对象布局做一下探索。

虚函数的简单示例 !

虚函数常常出现在一些抽象接口类定义里,当然,还有一个更常见的“特例”,那就是虚析构函数,后面会提到这个。

下面是一段关于虚函数的简单代码,演示了使用基类接口操作对象时的效果:

//Source filename: Win32Con.cpp
#include <iostream>
using namespace std;
class parent1
{
public:
    virtual int fun1()=0;
};

class child1:public parent1
{
public:
    virtual int fun1()
    {
        cout<<"child1::fun1()"<<endl;
        return 0;
    }
};

class child2:public parent1
{
public:
    virtual int fun1()
    {
        cout<<"child2::fun1()"<<endl;
        return 0;
    }
};

void test_func1(parent1 *pp)
{
    pp->fun1();
}

int main(int argc, char* argv[])
{
    child1 co1;
    child2 co2;
    test_func1(&co1);
    test_func1(&co2);
    return 0;
}

在上面的代码里,类parent1是一个只具有纯虚函数的接口类,这个类不能被实例化,它唯一的用途就是抽象一些特定的接口函数,当然,在这里这个接口函数就是纯虚函数 parent1::fun1()。

而类child1和child2则是两个从parent1继承的类,我们要使用它定义具体的类实例,所以它实现了由parent1继承得来的fun1接口,并且各自的实现是不同的。

函数 test_func1 的参数是一个parent1类型的指针,它所要完成的功能就是调用这个parent1对象的fun1()函数。

让我们编译运行一下上面的代码,可以看到下面的输出

child1::fun1()

child2::fun1()

很显然,在两次调用test_func1函数的时候,虽然传入的参数都是一个parent1的指针,但是却都分别执行了child1和child2各自的fun1函数!这就是C++里类的多态。然而,这一切是怎么发生的呢?test_func1函数怎么会知道应该调用哪个函数的呢?我不准备像其他人一样画若干图来说明,我准备用具体某个编译器产生的对象布局以及相应的汇编代码来说明这个过程(这个编译器是vs2008里的vc9)。

我们先打开一个VS2008命令提示窗口,改变目录到上面的代码Win32Con.cpp所在目录,输入下面的命令:

cl  win32con.cpp  /d1reportSingleClassLayoutchild

上面的命令可以编译win32con.cpp源码,同时生成里面类名包含child 的类的对象布局(layout)

注意:d1reportSingleClassLayout和后面的child是相连的!

输入上面的命令后看到的对象布局如下,红色字为我添加的注释

class child1    size(4): 子类child1的对象布局,只包含一个vfptr,大小为4字节
        +---
        | +--- (base class parent1) 这是被嵌套的父类parent1的对象布局
 0      | | {vfptr}
        | +---
        +---
这是child1的vfptr所指的虚函数表的布局,只包含一个函数的地址,就是child1的fun1函数
child1::$vftable@:
        | &child1_meta
        |  0
 0      | &child1::fun1

child1::fun1 this adjustor: 0

class child2    size(4): 子类child2的对象布局,只包含一个vfptr,大小为4字节
        +---
        | +--- (base class parent1) 这是被嵌套的父类parent1的对象布局
 0      | | {vfptr}
        | +---
        +---
这是child2的vfptr所指的虚函数表的布局,只包含一个函数的地址,就是child2的fun1函数
child2::$vftable@:
        | &child2_meta
        |  0
 0      | &child2::fun1

child2::fun1 this adjustor: 0

从上面的对象布局可以知道:

  • 每个子对象都有一个隐藏的成员变量vfptr(你当然不能用这个名字访问到它),它的值是指向该子对象的虚函数表,而虚函数表里填写的函数地址是该子对象的fun1函数地址。
  • 对一个包含有虚函数的类做sizeof操作的时候,除了能直接看到的成员变量,还得增加4字节(在32位机器上),就是vfptr这个指针的大小。

所以当test_func1进行pp->fun1()调用的时候,会首先取出pp所指的内存地址并按照parent1的内存布局,获取到vfptr指针(由于pp在两次调用中分别指向co1和co2所以这里取得的实际上是co1的vfptr和co2的vfptr),然后从vfptr所指的虚函数表第一项(现在也只有 1 项)取出作为将要调用的函数,由于co1和co2在各自的虚函数表里填写了各自的fun1的地址,于是pp->fun1()最终就调用到了co1和co2各自的fun1,输出自然也就不同了。

让我们看看test_func1的反汇编代码:

void test_func1(parent1 *pp)
{
001C1530  push        ebp
001C1531  mov         ebp,esp
001C1533  sub         esp,0C0h
001C1539  push        ebx
001C153A  push        esi
001C153B  push        edi
001C153C  lea         edi,[ebp-0C0h]
001C1542  mov         ecx,30h
001C1547  mov         eax,0CCCCCCCCh
001C154C  rep stos    dword ptr es:[edi]
    pp->fun1();
001C154E  mov         eax,dword ptr [pp] //取得pp的值放到eax,即对象的地址
//取得对象的vfptr地址放到edx(因为vfptr在对象布局里拍在第一)
001C1551  mov         edx,dword ptr [eax]
001C1553  mov         esi,esp
001C1555  mov         ecx,dword ptr [pp]
001C1558  mov         eax,dword ptr [edx] //取出vfptr的第一个虚函数的地址到eax
001C155A  call        eax //调用虚函数,即fun1()

至此,应该比较清楚虚函数机制的基本实现了。然而,也许你还会有这些问题:

  • 虚函数表是每个子对象都有的么?
  • 虚函数是存在一个表里的,表的数据结构是怎样的,如何定位表里哪个才是我们要调用的虚函数?

略作变化

让我们对前面的代码做以下修改:

  • 定义一个普通类
  • 修改parent类,在fun1前增加虚函数fun2
  • 在child1里和child2里编写fun2的具体实现,一个在fun1之前编写,另外一个在之后编写

修改后的编码大致如下:

class parent1
{
public:
    virtual int fun2()=0;
    virtual int fun1()=0;
};

class child
{
    int a;
};

class child1:public parent1
{
public:

    virtual int fun1()
    {
        cout<<"child1::fun1()"<<endl;
        return 0;
    }
    virtual int fun2()
    {
        cout<<"child1::fun2()"<<endl;
        return 0;
    }
};

然后我们再使用cl命令以及/d1reportSingleClassLayout选项输出相关的类对象布局情况:

class child     size(4):  //在普通类child里,看不到vfptr的身影!
        +---
 0      | a
        +---

class child1    size(4):    //child1的对象布局,和之前没有变化!
        +---
        | +--- (base class parent1)
 0      | | {vfptr}
        | +---
        +---
//child1的虚函数表多了fun2,并且两个虚函数在表里的顺序相同于在parent类里声明的顺序
child1::$vftable@:	     
        | &child1_meta
        |  0
 0      | &child1::fun2
 1      | &child1::fun1

child1::fun1 this adjustor: 0
child1::fun2 this adjustor: 0

结论很明显:

  • 虚函数表指针vfptr只在类里有虚拟函数的时候才会存在
  • 当有多个虚函数的时候,虚函数在虚函数表里的顺序由父类里虚函数的定义顺序决定

并且我们还可以观察到:

  • 这个vfptr指针会放在类的起始处(这是必须的,vfptr在父类和子类的对象布局上必须一致!)
  • 虚函数表是以一个NULL指针标识结束

让我们对这次简单的示例代码测试来做个小小总结:

  • 有虚函数的类,一定会有一个虚函数表指针vfptr
  • 这个vfptr指针会放在类的起始处
  • 虚函数表里会按基类声明虚函数的顺序在vfptr里存放函数地址
  • 虚函数表里存放的是函数地址是具体子类的实现函数的地址
  • 调用虚函数的时候,是从vfptr所指的函数表里获取到函数地址,然后才调用具体的代码
posted @ 2009-08-01 19:54 ttylikl 阅读(1917) 评论(3) 编辑

2009年7月19日

摘要: Visual Studio 环境准备本文使用的WindowsMobile开发环境是Visual Studio 2008版本,要求在安装VS2008的时候,一定要选择安装Visual C++ ,并且要选择上智能设备可编程技术选项。如下图: 如果你已经安装过VS2008,那么只要从Windows的“添加/删除程序”找到VisualStudio2008进入维护模式即可重新添加或者...阅读全文
posted @ 2009-07-19 22:01 ttylikl 阅读(4230) 评论(15) 编辑

2009年7月18日

摘要: Qt.Visual.Studio.Integration 是一套很棒的的VS插件工具,个人感觉比QT的 qt-vs-addin-1.x.x 好用。 尤其是与QT 4.5.x Commercial for VS2008配合的时候,可以直接在Visual Studio 2008里就直接创建QT工程,直接使用内嵌的QT设计器,直接使用Visual Studio进行项目的编译和调试,完全的与Visual ...阅读全文
posted @ 2009-07-18 00:06 ttylikl 阅读(3328) 评论(8) 编辑

2009年7月13日

摘要: 介绍connectSlotsByName 是一个QMetaObject类里的static函数,其定义如下: static void connectSlotsByName(QObject *o); 其作用是如其名称一样,用来将QObject *o里的子孙QObject的某些信号按照其objectName连接到o的槽上。起因为啥会对这个函数产生一探究竟的想法呢?——既然是根据o...阅读全文
posted @ 2009-07-13 18:20 ttylikl 阅读(2298) 评论(0) 编辑
 
Copyright © ttylikl Powered by: 博客园 模板提供:沪江博客