找回密码
 FreeOZ用户注册
查看: 1605|回复: 7
打印 上一主题 下一主题

[论坛技术] Java程序员用C++ 继承时需要注意的两个陷阱问题

[复制链接]
跳转到指定楼层
1#
发表于 11-5-2009 12:39:28 | 只看该作者 回帖奖励 |倒序浏览 |阅读模式

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有帐号?FreeOZ用户注册

x
0. 虚函数
1. 被隐蔽的成员函数
2. 在构造函数和析构函数中调用虚函数

0. 虚函数
Java中所在成员函数都是虚函数,这是一个语法原则,所以不是两个陷阱问题之一

1. 被隐蔽的成员函数
看看下面的C++程序:
  1. template<class T>
  2. class Base
  3. {
  4. public:
  5.   void setValue(T const & v) { cout << "SetValue by reference in Base" << endl; }
  6. };

  7. template<class T>
  8. class Derived : public Base<T>
  9. {
  10. public:
  11.   void setValue(T const * v) { cout << "SetValue by pointer in Derived" << endl; }
  12. };

  13. int main()
  14. {
  15.   Derived<int> x;
  16.   x.setValue(10);
  17.   return 0;
  18. }
复制代码
这一是段怎样看都正确但实际上通不过编译的程序。原因是Derived<T>::setValue(T const * v)
屏蔽掉了Base<T>::setValue(T const & v)。在Visual Studio 2008中出现的错误是:
  1. error C2664: 'Derived<T>::setValue' : cannot convert parameter 1 from 'int' to 'const int *'
复制代码
而在gcc 4中出现的错误是:
  1. error: invalid conversion from 'int' to 'const int*'
  2. error:   initializing argument 1 of 'void Derived<T>::setValue(const T*) [with T = int]'
复制代码
解决的办法就是在Derived<T>类中重新声明一次setValue(T const &),下面是修改后的Derived<T>类:
  1. template<class T>
  2. class Derived : public Base<T>
  3. {
  4. public:
  5.   void setValue(T const * v) { cout << "SetValue by pointer in Derived" << endl; }
  6.   void setValue(T const & v) { Base<T>::setValue(v); }

  7. };
复制代码
个人认为C++的这个语法定义和OOP中的overriding的概念有点矛盾。估计是C++编译器为了优化编译带来的问题吧,
具体还需要看看相关的书才知道。

2. 在构造函数和析构函数中调用虚函数。
对于已经熟悉于Design Pattern中的template method模式的同学来说,写出下面的代码是很正常的:
  1. template<class T>
  2. class Base
  3. {
  4. public:
  5.   Base() { constTempl(); }
  6.   ~Base() { destTempl(); }

  7.   //virtual void constTempl() = 0;
  8.   virtual void constTempl() { cout << "constTempl from Base" << endl; }
  9.   //virtual void destTempl() = 0;
  10.   virtual void destTempl() { cout << "destTempl from Derived" << endl; }

  11.   void setValue(T const & v) { cout << "SetValue by reference in Base" << endl; }
  12. };

  13. template<class T>
  14. class Derived : public Base<T>
  15. {
  16. public:
  17.   virtual void constTempl() { cout << "constTempl from Derived" << endl; }
  18.   virtual void destTempl() { cout << "destTempl from Dervied" << endl; }
  19.   void setValue(T const * v) { cout << "SetValue by pointer in Derived" << endl; }
  20.   void setValue(T const & v) { Base<T>::setValue(v); }

  21. };

  22. int main()
  23. {
  24.   Derived<int> x;
  25.   x.setValue(10);
  26.   return 0;
  27. }
复制代码
事实上。上面的程序中,我们通过系统自动建立的缺省构造函数调用了Base<T>中的构造函数,
而在Base<T>的构造函数中,我们利用了template method调用virtual void constTempl()。
在直觉中,我们会认为针对不同的子类,会调用不同的constTempl(),事实上情况并不是这样的。

上面的程序无论在Visual Studio 2008还是在gcc4中,得到的结果都是:
constTempl from Base
SetValue by reference in Base
destTempl from Derived

也就是说,constTempl调用的是Base<T>中的函数,而destTempl调用的是Derived中的函数。
这个实验结果看,析构函数的调用过程是符合我们的直觉的。我手上这本2003译成的
《C++ Strategies and Tactics》
(中文名:《C++编译惯用法》)
还说析构函数也会有类似的anti-intuitive的行为。不过我现在用两个编译器测试的结果都发现
析构能成功调用虚拟函数。

造成这样的问题的原因,书中解译为:对象的基类部分的构造要早于其数据成员。当Base部分被构造
时,Derived中的其他数据成员还没有被构造,此时调用Derived中的虚函数将变得毫无意义(并可能出错?)。

可以参考Java中类似的实现:
  1. public class X {
  2.         public X() {
  3.                 ct();
  4.         }
  5.         public void ct() {
  6.                 System.out.println("ct() of X");
  7.         }

  8.         public static class X2 extends X {
  9.                                         private int data;
  10.                                        
  11.                 public X2() {
  12.                         super();
  13.                         data = 10;
  14.                 }
  15.                 public void ct() {
  16.                         System.out.println("ct() of X2: " + data);
  17.                 }
  18.         }

  19.         public static void main(String [] args){
  20.                 X x = new X2();
  21.         }
  22. }
复制代码
这段代码可以看到Java可以成功地调用构造函数中的“虚”函数形成模板。这里data成员数据被初始化成 0 。
事实上Java成员的初始化是可以直接在声明中被初始化,或者系统会自动使其设置为 0,而C++只能在初始化
列表中对成员进行初始化,这是很大的语法不同。也就是说,从语法上Java可以避免数据成员没有被初始化
的问题,而C++则不能。

对于析构函数,我估计由于没有成员初始化问题,所以在某个版本(具体哪个版本我也不知道,找找看)中改变了。
然而,由于C++中复制的虚析构函数的问题,我觉得如果可能,最好还是避免在析构函数中调用虚函数来进行构析工作,
毕竟析构过程是一个有层次的过程,每一个析构函数只要管好自己当前类/对象的成员/占有资源的析构/释放则可。

参考书:
C++ Strategies and Tactics 中译本《C++编程惯同法——高级程序员常用方法和技巧》
Robert B. Murray,王昕(译)

评分

参与人数 1威望 +30 收起 理由
coredump + 30 谢谢分享!

查看全部评分

回复  

使用道具 举报

2#
发表于 11-5-2009 14:17:20 | 只看该作者

回复 #1 key 的帖子

对于继承中名字隐藏的设计理由是这样:
如果一个基类中的函数是非virtual的,则表示它不像让子类重写其行为,而如果子类出现了和此基类函数同名的函数,则说明子类对此函数的行为和基类有完全不同的理解,名字相同可以视为巧合,这时候如果不隐藏积累的同名函数,则程序的行为可能会不符合基类的预期。绕过这个限制可以通过上面这样子类重写一边wrapper实现,也可以通过在基类中把这个函数改为virtual来实现,其实子类如果需要写wrapper来让基类的隐藏函数可见时,也就说明这个函数需要重新声明为virtual了。

此外C++构造和析构函数中更大的隐患是exception的问题。
回复  

使用道具 举报

3#
 楼主| 发表于 11-5-2009 15:06:39 | 只看该作者
原帖由 coredump 于 11-5-2009 14:17 发表
对于继承中名字隐藏的设计理由是这样:
如果一个基类中的函数是非virtual的,则表示它不像让子类重写其行为,而如果子类出现了和此基类函数同名的函数,则说明子类对此函数的行为和基类有完全不同的理解,名字相同可以视为巧合,这时候如果不隐藏积累的同名函数,则程序的行为可能会不符合基类的预期。绕过这个限制可以通过上面这样子类重写一边wrapper实现,也可以通过在基类中把这个函数改为virtual来实现,其实子类如果需要写wrapper来让基类的隐藏函数可见时,也就说明这个函数需要重新声明为virtual了。


你说的是虚函数问题,我说的是方法屏蔽问题,不是同一个问题。
即使你把父类中同名单不同参数列表的函数声明为virtual,到了子类中,
如果出现同名单不同参数列表的方法,父类的方向一样会被屏蔽。
为了更清楚的说明问题,可以试试下面的实验程序:
  1. template<class T>
  2. class Base
  3. {
  4. public:
  5.   virtual void setValue(T const & v) { cout << "SetValue by reference in Base" << endl; }
  6. };

  7. template<class T>
  8. class Derived : public Base<T>
  9. {
  10. public:
  11.   virtual void setValue(T const * v, int len) { cout << "SetValue by pointer in Derived" << endl; } //这里边参数的个数都不同了,但都没有用
  12. };

  13. int main()
  14. {
  15.   Derived<int> x;
  16.   x.setValue(10);
  17.   return 0;
  18. }
复制代码
回复  

使用道具 举报

4#
发表于 11-5-2009 15:20:16 | 只看该作者

回复 #3 key 的帖子


  Base<int>* pX = new  Derived<int>();
  pX->setValue(10);

应该就行了。
回复  

使用道具 举报

5#
 楼主| 发表于 11-5-2009 15:47:34 | 只看该作者
原帖由 coredump 于 11-5-2009 15:20 发表

  Base* pX = new  Derived();
  pX->setValue(10);

应该就行了。


对的,但问题是,这样一来就和is-a的直觉思想相违背。所以我严重不理解C++在这个问题上的设计思路。
看来有必要看看The Annotated C++这本书,可能我会明白BS大爷深澳的思想。
回复  

使用道具 举报

6#
发表于 11-5-2009 15:50:48 | 只看该作者

回复 #5 key 的帖子

is-a概念在C++中必须体现在创建于堆上的对象,栈上的对象没有多态性,其实栈上的C++对象很多OOP的概念都体现不了。
回复  

使用道具 举报

7#
 楼主| 发表于 11-5-2009 16:08:45 | 只看该作者
原帖由 coredump 于 11-5-2009 15:50 发表
is-a概念在C++中必须体现在创建于堆上的对象,栈上的对象没有多态性,其实栈上的C++对象很多OOP的概念都体现不了。


我觉得情况并不如此。
我们可以采用引用的方式来让栈上的对象多态起来。下面的写法就不会有编译错误:

int main()
{
  Derived<int> x;
  Base<int> &bx = x;
  //x.setValue(10);
  bx.setValue(10);
  return 0;
}
回复  

使用道具 举报

8#
发表于 11-5-2009 16:16:44 | 只看该作者

回复 #7 key 的帖子

对,你说的对。

在exceptional c++中,对这一设计的解释是这样的:
For example, one might think that if none of the functions found in an inner scope were usable, then it could be okay to let the compiler start searching further enclosing scopes. That would, however, produce surprising results in some cases (consider the case in which there's a function that would be an exact match in an outer scope, but there's a function in an inner scope that's a close match, requiring only a few parameter conversions). Or, one might think that the compiler should just make a list of all functions with the required name in all scopes and then perform overload resolution across scopes. But, alas, that too has its pitfalls (consider that a member function ought to be preferred over a global function, rather than result in a possible ambiguity).
回复  

使用道具 举报

您需要登录后才可以回帖 登录 | FreeOZ用户注册

本版积分规则

小黑屋|手机版|Archiver|FreeOZ论坛

GMT+10, 17-4-2025 02:53 , Processed in 0.019210 second(s), 27 queries , Gzip On, Redis On.

Powered by Discuz! X3.2

© 2001-2013 Comsenz Inc.

快速回复 返回顶部 返回列表