1 由来

客户用陈旧的VC++6.0进行项目开发,有一块功能需要我来实现。让一个早就习惯了VS2013的人去使用C++支持不太好的VC6去做开发实在是非常不爽,于是另辟蹊径,打算使用VC++2013开发编译出DLL,供VC6下调用即可。使用C++开发DLL的基本原则是减少暴露和接口简单化,最常用的方式就是使用纯虚类导出接口。另一种就是使用C++实现,但是导出时只导出C函数。处于使用的便利性考虑,采用了第一种方式。

2 原型与问题

基本的设计思路可以用如下代码描述。
  1. #include <iostream>
  2. #include <hash_map>
  3. using namespace std;
  4. class I1
  5. {
  6. public:
  7. virtual void vf1()
  8. {
  9. cout << "I'm I1:vf1()" << endl;
  10. }
  11. };
  12. class I2
  13. {
  14. public:
  15. virtual void vf2()
  16. {
  17. cout << "I'm I2:vf2()" << endl;
  18. }
  19. };
  20. class C : public I1, public I2
  21. {
  22. private:
  23. hash_map<string, string> m_cache;
  24. };
  25. I1* CreateC()
  26. {
  27. return new C();
  28. }
  29. int main(int argc, char** argv)
  30. {
  31. I1* pI1 = CreateC();
  32. pI1->vf1();
  33. I2* pI2 = (I2*)pI1;
  34. pI2->vf2();
  35. delete pI1;
  36. return 0;
  37. }
#include <iostream>
#include <hash_map>
using namespace std; class I1
{
public:
virtual void vf1()
{
cout << "I'm I1:vf1()" << endl;
}
}; class I2
{
public:
virtual void vf2()
{
cout << "I'm I2:vf2()" << endl;
}
}; class C : public I1, public I2
{
private:
hash_map<string, string> m_cache;
}; I1* CreateC()
{
return new C();
} int main(int argc, char** argv)
{
I1* pI1 = CreateC();
pI1->vf1(); I2* pI2 = (I2*)pI1;
pI2->vf2(); delete pI1;
return 0;
}
采用基于接口的设计方法,对外只暴露接口类I1和I2,对于实际的实现类C则对外隐藏。客户在使用的时候,只需要调用CreateC()就可以产生C类型的对象,而不必知道C的实现细节。然后通过不同的接口调用不同方面的功能。看起来一切还可以,但实际运行却是有问题的,上述代码执行结果如下:
 

第二行的输出对应pI2->vf2(),显然结果是错误的,调用者的本意是调用I2::vf2(),实际却调用了I1::vf1()。随后我发现这个问题其实在论坛上也有人提出过,也有不少人给出了答案,但是感觉解释的不够明确和详细,所以决定亲自一探究竟。

3 分析

对于多继承下的内存布局问题,请参考本人的博文:http://blog.csdn.net/smstong/article/details/6604388。其实这里的问题也是与内存不就息息相关,也算是对前面这篇博文的一点补充。前面博文指出了使用同一对象调用不同的函数时,在被调用函数内部的this指针是不同的,以及为什么不同然而没有说明这里的this是何时被确定的,是编译时?还是运行时?
还是先来看看前面代码的内存布局。
 
之所以会出现pI1和pI2指向了同一个地址,是因为C++编译器没有足够的知识来把IA*类型转换为IB*类型,只能按照传统的C指针强制转换处理,也就是指针位置不变。为了验证上面的结论,简单的把pIA和pIB打印出来即可。把main()函数修改为如下:
 
  1. int main(int argc, char** argv)
  2. {
  3. I1* pI1 = CreateC();
  4. pI1->vf1();
  5. I2* pI2 = (I2*)pI1;
  6. pI2->vf2();
  7. cout << "pI1指向的地址为:"<<std::hex << pI1 << endl;
  8. cout << "pI2指向的地址为:"<<std::hex << pI2 << endl;
  9. delete pI1;
  10. return 0;
  11. }
int main(int argc, char** argv)
{
I1* pI1 = CreateC();
pI1->vf1(); I2* pI2 = (I2*)pI1;
pI2->vf2(); cout << "pI1指向的地址为:"<<std::hex << pI1 << endl;
cout << "pI2指向的地址为:"<<std::hex << pI2 << endl;
delete pI1;
return 0;
}

执行结果为:

 
 
可见pI1和pI2确实指向了同一个地址,而这个地址就是I1类的虚表。由于虚函数是按照顺序定位的,编译器编译pI2->vf2()的时候,不管实际的pI2指向哪里,都把它当做指向了I2的虚表,根据I2类定义,推出I2::vf2()这个函数位于其虚表的第0个位置,所以就直接把pI2指向的地址作为vf2来调用。而实际上,这个位置恰恰是I1虚表的第0个位置,也就是I1::vf1的位置,所以实际执行时调用的是I1::vf1()。其实这种情况是有些特殊的,也就是这个位置正好也是一个函数地址,而且函数原型也一样,要是有任何不同的地方,就会造成调用失败,反而更容易及时的提醒开发者。如下代码所示:
  1. #include <iostream>
  2. #include <hash_map>
  3. using namespace std;
  4. class I1
  5. {
  6. public:
  7. virtual void vf1()
  8. {
  9. cout << "I'm I1:vf1()" << endl;
  10. }
  11. };
  12. class I2
  13. {
  14. public:
  15. virtual void vf2()
  16. {
  17. cout << "I'm I2:vf2()" << endl;
  18. }
  19. virtual void vf3()
  20. {
  21. cout << "I'm I2:vf3()" << endl;
  22. }
  23. };
  24. class C : public I1, public I2
  25. {
  26. private:
  27. hash_map<string, string> m_cache;
  28. };
  29. I1* CreateC()
  30. {
  31. return new C();
  32. }
  33. int main(int argc, char** argv)
  34. {
  35. I1* pI1 = CreateC();
  36. pI1->vf1();
  37. I2* pI2 = (I2*)pI1;
  38. pI2->vf2();
  39. pI2->vf3();
  40. cout << "pI1指向的地址为:"<<std::hex << pI1 << endl;
  41. cout << "pI2指向的地址为:"<<std::hex << pI2 << endl;
  42. delete pI1;
  43. return 0;
  44. }
#include <iostream>
#include <hash_map>
using namespace std; class I1
{
public:
virtual void vf1()
{
cout << "I'm I1:vf1()" << endl;
}
}; class I2
{
public:
virtual void vf2()
{
cout << "I'm I2:vf2()" << endl;
}
virtual void vf3()
{
cout << "I'm I2:vf3()" << endl;
}
}; class C : public I1, public I2
{
private:
hash_map<string, string> m_cache;
}; I1* CreateC()
{
return new C();
} int main(int argc, char** argv)
{
I1* pI1 = CreateC();
pI1->vf1(); I2* pI2 = (I2*)pI1;
pI2->vf2();
pI2->vf3(); cout << "pI1指向的地址为:"<<std::hex << pI1 << endl;
cout << "pI2指向的地址为:"<<std::hex << pI2 << endl;
delete pI1;
return 0;
}
此时的内存布局为:
 
 
在执行pI2->vf2()时,执行的是I1::vf1(),但是不会报错。当执行pI2->vf3();时,由于调用的地址并不是一个函数指针,所以会报错。

4 解决思路

上述问题的发生,根本原因就是接口指针指向了错误的地方,而导致这种错误的原因就是使用了强制类型转换。C++允许类型转换并能正确处理的是具有继承关系的类型的对象的类型转换,这也是多态的基础。C++编译器能够根据头文件中类的声明在类型转换时自动调整对象指针的位置,从而能够正确的实现多态。
然而如果C++编译器不能根据类声明推算出类型转换时的指针调整方式时,如果使用了强制类型转换,那么编译器只是简单的默默无作为,也就是根本就不调整指针位置,也不会警告开发者。这就导致了问题的发生。
 
解决思路有三个:
(1)不使用强制类型转换,使用static_cast进行编译期类型转换,此时如果C++编译期不能推算出指针调整算法,就会报错,提醒开发者。
这种方法可以提示开发者出现错误,但不能解决问题。
(2)不使用强制类型转换,使用dynamic_cast进行运行期动态类型转换,这需要开启编译器的RTTI,如下所示。
  1. int main(int argc, char** argv)
  2. {
  3. I1* pI1 = CreateC();
  4. pI1->vf1();
  5. I2* pI2 = dynamic_cast<I2*>(pI1);
  6. pI2->vf2();
  7. delete pI1;
  8. return 0;
  9. }
int main(int argc, char** argv)
{
I1* pI1 = CreateC();
pI1->vf1(); I2* pI2 = dynamic_cast<I2*>(pI1);
pI2->vf2(); delete pI1;
return 0;
}

此时,编译和运行都如预期一样,完全正确。缺陷就是开启RTTI会影响程序性能,而且好像VC++6中无法正常工作。

(3)某种程序上学习COM,提供接口查询功能。
  1. #include <iostream>
  2. #include <hash_map>
  3. using namespace std;
  4. class I1
  5. {
  6. public:
  7. virtual void vf1()
  8. {
  9. cout << "I'm I1:vf1()" << endl;
  10. }
  11. };
  12. class I2
  13. {
  14. public:
  15. virtual void vf2()
  16. {
  17. cout << "I'm I2:vf2()" << endl;
  18. }
  19. virtual void vf3()
  20. {
  21. cout << "I'm I2:vf3()" << endl;
  22. }
  23. };
  24. class C : public I1, public I2
  25. {
  26. private:
  27. hash_map<string, string> m_cache;
  28. };
  29. I1* CreateC()
  30. {
  31. return new C();
  32. }
  33. I2* QueryInterface(I1* obj)
  34. {
  35. C* pC = static_cast<C*>(obj);
  36. return static_cast<I2*>(pC);
  37. }
  38. I1* QueryInterface(I2* obj)
  39. {
  40. C* pC = static_cast<C*>(obj);
  41. return static_cast<I1*>(pC);
  42. }
  43. int main(int argc, char** argv)
  44. {
  45. I1* pI1 = CreateC();
  46. pI1->vf1();
  47. I2* pI2 = QueryInterface(pI1);
  48. pI2->vf2();
  49. delete pI1;
  50. return 0;
  51. }
#include <iostream>
#include <hash_map>
using namespace std; class I1
{
public:
virtual void vf1()
{
cout << "I'm I1:vf1()" << endl;
}
}; class I2
{
public:
virtual void vf2()
{
cout << "I'm I2:vf2()" << endl;
}
virtual void vf3()
{
cout << "I'm I2:vf3()" << endl;
}
}; class C : public I1, public I2
{
private:
hash_map<string, string> m_cache;
}; I1* CreateC()
{
return new C();
} I2* QueryInterface(I1* obj)
{
C* pC = static_cast<C*>(obj);
return static_cast<I2*>(pC);
} I1* QueryInterface(I2* obj)
{
C* pC = static_cast<C*>(obj);
return static_cast<I1*>(pC);
} int main(int argc, char** argv)
{
I1* pI1 = CreateC();
pI1->vf1(); I2* pI2 = QueryInterface(pI1);
pI2->vf2(); delete pI1;
return 0;
}

这种方式,既可以得到正确的运行结果,也不需要用户调用dynamic_cast,所以效果最好。但实现和调用都较为麻烦,使得库的使用不方便。

 

5 一点感想

 
(1)C++到处充满细节,使得开发者必须考虑很多细节,而且编译器有时候对开发者隐藏了很多东西,有时候又做的不好,使得这个语言做开发不太顺手,也许这就是C#,Java盛行的原因,C#中完全不存在上面说的问题,因为C#一定是运行时类型识别的。
 
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using System.Text;
  5. using System.Threading.Tasks;
  6. namespace ConsoleApplication1
  7. {
  8. class Program
  9. {
  10. static void Main(string[] args)
  11. {
  12. I1 pI1 = new C();
  13. pI1.vf1();
  14. I2 pI2 = (I2)pI1;
  15. pI2.vf2();
  16. }
  17. }
  18. interface I1
  19. {
  20. void vf1();
  21. }
  22. interface I2
  23. {
  24. void vf2();
  25. }
  26. class C : I1, I2
  27. {
  28. public void vf1()
  29. {
  30. Console.WriteLine("I'm vf1()");
  31. }
  32. public void vf2()
  33. {
  34. Console.WriteLine("I'm vf2()");
  35. }
  36. }
  37. }
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks; namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
I1 pI1 = new C();
pI1.vf1();
I2 pI2 = (I2)pI1;
pI2.vf2();
}
} interface I1
{
void vf1();
} interface I2
{
void vf2();
} class C : I1, I2
{
public void vf1()
{
Console.WriteLine("I'm vf1()");
}
public void vf2()
{
Console.WriteLine("I'm vf2()");
}
}
}

(2)开发库的时候,对外接口以类的形式是否合适?是否还是以纯粹的C函数为接口更简洁?C++的前途....

 
原文地址:http://blog.csdn.net/smstong/article/details/24455371

当C++多继承遇上类型转换[转]的更多相关文章

  1. 谁还没遇上过NoClassDefFoundError咋地——浅谈字节码生成与热部署

    谁还没遇上过NoClassDefFoundError咋地--浅谈字节码生成与热部署 前言 在Java程序员的世界里,NoClassDefFoundError是一类相当令人厌恶的错误,因为这类错误通常非 ...

  2. MVC遇上bootstrap后的ajax表单模型验证

    MVC遇上bootstrap后的ajax表单验证 使用bootstrap后他由他自带的样式has-error,想要使用它就会比较麻烦,往常使用jqueyr.validate的话只有使用他自己的样式了, ...

  3. 敏捷遇上UML-需求分析及软件设计最佳实践(郑州站 2014-6-7)

      邀请函: 尊敬的阁下:我们将在郑州为您奉献高端知识大餐,当敏捷遇上UML,会发生怎样的化学作用呢?首席专家张老师将会为您分享需求分析及软件设计方面的最佳实践,帮助您掌握敏捷.UML及两者相结合的实 ...

  4. 敏捷遇上UML—软创基地马年大会(广州站 2014-4-19)

        我们将在广州为您奉献高端知识大餐,当敏捷遇上UML,会发生怎样的化学作用呢?首席专家张老师将会为您分享需求分析及软件设计方面的最佳实践,帮助您掌握敏捷.UML及两者相结合的实战技巧. 时间:2 ...

  5. 敏捷遇上UML——软创基地马年大会(深圳站 2014-3-15)

    邀请函: 尊敬的阁下: 我们将在深圳为您奉献高端知识大餐,当敏捷遇上UML,会发生怎样的化学作用呢?首席专家张老师将会为您分享需求分析及软件设计方面的最佳实践,帮助您掌握敏捷.UML及两者相结合的实战 ...

  6. 初识genymotion安装遇上的VirtualBox问题

    想必做过Android开发的都讨厌那慢如蜗牛的 eclipse原生Android模拟器吧! 光是启动这个模拟器都得花上两三分钟,慢慢的用起来手机来调试,但那毕竟不是长久之计,也确实不方便,后来知道了g ...

  7. SQL SERVER 2008 R2 SP1更新时,遇上共享功能更新失败解决方案

    SQL SERVER 2008 R2 SP1更新时,遇上共享功能更新失败的问题,可作如下尝试: 更新失败后,在windows的[事件查看器→应用程序]中找到来源为MsiInstaller,事件ID为1 ...

  8. 当创业遇上O2O,新一批死亡名单,看完震惊了!

    当创业遇上O2O,故事就开始了,总投入1.6亿.半年开7家便利店.会员猛增至10万……2015半年过去后,很多故事在后面变成了一场创业“事故”,是模式错误还是烧钱过度?这些项目的失败能给国内创业者带来 ...

  9. LoadRunner - 当DiscuzNT遇上了Loadrunner(下) (转发)

    当DiscuzNT遇上了Loadrunner(下) 在之前的两篇文章中,基本上介绍了如何录制脚本和生成并发用户,同时还对测试报告中的几个图表做了简单的说明.今天这篇文章做为这个系列的最后一篇,将会介绍 ...

随机推荐

  1. PHP之OOP要点摘要

    类和对象:    类是生成对象的模板,对象是活动组件;    面向对象编程实际操作都是通过类的实例(而不是类本身)完成的:    访问控制(public.protected.private):(1) ...

  2. Jquery mobile 新手问题总汇

    1页面缩放显示问题 问题描述: 页面似乎被缩小了,屏幕太宽了. 解决办法: 在head标签内加入: <meta name="viewport" content="w ...

  3. 数组中的toString,toLocalString,valueOf方法有什么区别

    1. 2.简单来说,tostring就是用字符串来代替对象.tolocalstring就是根据不同的语言环境吧对象转成字符串,实际上totolocalstring是有缺省参数的,如tolocalstr ...

  4. ios新特性(泛型)

    协变 子类转父类   逆变父类给子类赋值

  5. 总结js中数据类型的bool值及其比较

    首先需要知道的是,js中有6个值为false,分别是: 0, '', null, undefined, NaN 和 false, 其他(包括{}, [], Infinity)为true. 可以使用Bo ...

  6. [转载]再来重新认识JavaEE完整体系架构

    移步: http://www.jizhuomi.com/software/644.html

  7. 【编辑器】【Sublime Text】使用笔记

    1.安装 官网下载即可 2.插件 sublime-text - Sublime Text 怎么高亮 Markdown 的文件语法 设置Sublime为VIM模式 如何在sublime 里面设置 ver ...

  8. 关于Xcode6创建的工程在Xcode5打开

    Xcode6创建的工程在Xcode5打开- 4.0只显示3.5大小的问题 只需要在工程里添加Default-568h@2x.png,即可以解决

  9. callback 转换到 promise

    最近项目迭代,从express到koa,面对callback,想偷懒,就想到了Proxy对象 new Proxy(docker,{ get : function (obj,name) { return ...

  10. AnyCAD.NET C#开发CAD软件实践(一)

    免费的AnyCAD.NET发布了!俺喜欢的C#有了大展前途的机会了. 打算用这个框架搭建一套实用的CAD系统,目标是能买出去10套以上. 先看看AnyCAD.NET的自我介绍. http://www. ...