当C++多继承遇上类型转换[转]

Posted on 2016-05-23 17:31  Jack Cheung  阅读(488)  评论(0)    收藏  举报

 

 

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.   
  5.   
  6. class I1  
  7. {  
  8. public:  
  9.     virtual void vf1()  
  10.     {  
  11.         cout << "I'm I1:vf1()" << endl;  
  12.     }  
  13. };  
  14.   
  15.   
  16. class I2  
  17. {  
  18. public:  
  19.     virtual void vf2()  
  20.     {  
  21.         cout << "I'm I2:vf2()" << endl;  
  22.     }  
  23. };  
  24.   
  25.   
  26. class C : public I1, public I2  
  27. {  
  28. private:  
  29.     hash_map<string, string> m_cache;  
  30. };  
  31.   
  32.   
  33. I1* CreateC()  
  34. {  
  35.     return new C();  
  36. }  
  37.   
  38.   
  39. int main(int argc, char** argv)  
  40. {  
  41.     I1* pI1 = CreateC();  
  42.     pI1->vf1();  
  43.   
  44.   
  45.     I2* pI2 = (I2*)pI1;  
  46.     pI2->vf2();  
  47.   
  48.   
  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;
	}
};


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.   
  6.     I2* pI2 = (I2*)pI1;  
  7.     pI2->vf2();  
  8.   
  9.     cout << "pI1指向的地址为:"<<std::hex << pI1 << endl;  
  10.     cout << "pI2指向的地址为:"<<std::hex << pI2 << endl;  
  11.     delete pI1;  
  12.     return 0;  
  13. }  
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.   
  5.   
  6. class I1  
  7. {  
  8. public:  
  9.     virtual void vf1()  
  10.     {  
  11.         cout << "I'm I1:vf1()" << endl;  
  12.     }  
  13. };  
  14.   
  15.   
  16. class I2  
  17. {  
  18. public:  
  19.     virtual void vf2()  
  20.     {  
  21.         cout << "I'm I2:vf2()" << endl;  
  22.     }  
  23.     virtual void vf3()  
  24.     {  
  25.         cout << "I'm I2:vf3()" << endl;  
  26.     }  
  27. };  
  28.   
  29.   
  30. class C : public I1, public I2  
  31. {  
  32. private:  
  33.     hash_map<string, string> m_cache;  
  34. };  
  35.   
  36.   
  37. I1* CreateC()  
  38. {  
  39.     return new C();  
  40. }  
  41.   
  42.   
  43. int main(int argc, char** argv)  
  44. {  
  45.     I1* pI1 = CreateC();  
  46.     pI1->vf1();  
  47.   
  48.   
  49.     I2* pI2 = (I2*)pI1;  
  50.     pI2->vf2();  
  51.     pI2->vf3();  
  52.   
  53.   
  54.     cout << "pI1指向的地址为:"<<std::hex << pI1 << endl;  
  55.     cout << "pI2指向的地址为:"<<std::hex << pI2 << endl;  
  56.     delete pI1;  
  57.     return 0;  
  58. }  
#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.   
  6.     I2* pI2 = dynamic_cast<I2*>(pI1);  
  7.     pI2->vf2();  
  8.   
  9.     delete pI1;  
  10.     return 0;  
  11. }  
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.   
  5. class I1  
  6. {  
  7. public:  
  8.     virtual void vf1()  
  9.     {  
  10.         cout << "I'm I1:vf1()" << endl;  
  11.     }  
  12. };  
  13.   
  14. class I2  
  15. {  
  16. public:  
  17.     virtual void vf2()  
  18.     {  
  19.         cout << "I'm I2:vf2()" << endl;  
  20.     }  
  21.     virtual void vf3()  
  22.     {  
  23.         cout << "I'm I2:vf3()" << endl;  
  24.     }  
  25. };  
  26.   
  27. class C : public I1, public I2  
  28. {  
  29. private:  
  30.     hash_map<string, string> m_cache;  
  31. };  
  32.   
  33. I1* CreateC()  
  34. {  
  35.     return new C();  
  36. }  
  37.   
  38. I2* QueryInterface(I1* obj)  
  39. {  
  40.     C* pC = static_cast<C*>(obj);  
  41.     return static_cast<I2*>(pC);  
  42. }  
  43.   
  44. I1* QueryInterface(I2* obj)  
  45. {  
  46.     C* pC = static_cast<C*>(obj);  
  47.     return static_cast<I1*>(pC);  
  48. }  
  49.   
  50. int main(int argc, char** argv)  
  51. {  
  52.     I1* pI1 = CreateC();  
  53.     pI1->vf1();  
  54.   
  55.     I2* pI2 = QueryInterface(pI1);  
  56.     pI2->vf2();  
  57.   
  58.     delete pI1;  
  59.     return 0;  
  60. }  
#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.   
  7. namespace ConsoleApplication1  
  8. {  
  9.     class Program  
  10.     {  
  11.         static void Main(string[] args)  
  12.         {  
  13.             I1 pI1 = new C();  
  14.             pI1.vf1();  
  15.             I2 pI2 = (I2)pI1;  
  16.             pI2.vf2();  
  17.         }  
  18.     }  
  19.   
  20.     interface I1  
  21.     {  
  22.         void vf1();  
  23.     }  
  24.   
  25.     interface I2  
  26.     {  
  27.         void vf2();  
  28.     }  
  29.   
  30.     class C : I1, I2  
  31.     {  
  32.         public void vf1()  
  33.         {  
  34.             Console.WriteLine("I'm vf1()");  
  35.         }  
  36.         public void vf2()  
  37.         {  
  38.             Console.WriteLine("I'm vf2()");  
  39.         }  
  40.     }  
  41. }  
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