virtual函数的缺省参数,是C++语言的一种语法糖。即使有多年编程经验的老程序员,也不免在此跌跟头。发生此类现象最根本的问题是:没有理解虚函数采用动态绑定策略,而缺省参数却采用的是静态绑定策略。
对象的所谓静态类型,是它在程序中被声明时所采用的类型。函数的默认参数就采用静态绑定,就是指在程序运行的过程中不会发生变化。
class CStreamBase
{
public:
virtual SendStream(int iStreamLength = 1500);
};
class CTCPStream: public CStreamBase
{
public:
virtual SendStream(int iStreamLength = 1460);
};
class CUDPStream: public CStreamBase
{
public:
virtual SendStream(int iStreamLength = 1460);
};
CStreamBase *pStream = NULL;
CStreamBase *pTCPStream = new CTCPStream();
CStreamBase *pUDPStream = new CUDPStream ();
所谓动态类型是指“目前所指对象的类型”。动态类型可以在程序执行过程中变化。虚函数就采用这种机制,如果你调用一个虚函数,究竟调用哪分函数代码实现,这取决于发生函数调用的那个动态类型。
pTCPStream->SendStream(1000);
pUDPStream-> SendStream(1000);
那么,两种结合呢?我们看两者结合的结果。带有缺省参数的virtual函数,由于virtual函数是动态绑定,而缺省参数是静态绑定。这种实现真正的意义是:“调用一个定义于派生类内的virtual函数”的同时,而使用基类为其指定的缺省参数值。
pUDPStream -> SendStream ();
pUDPStream的动态类型是CUDPStream*,所以调用的是CUDPStream的virtual函数。CUDPStream:: SendStream函数的缺省参数应该是1500,但由于pUDPStream的静态类型是CStreamBase *,所以调用的缺省参数值来自CStreamBase class而非CUDPStreamclass!这种问题对于引用依然存在。
这种实现规则确实让我们有些惊讶,但是为什么C++会采用这种奇怪的实现策略呢?答案在于运行期效率。如果缺省参数是动态绑定的,编译器就必须有某种办法在运行期为virtual函数决定适当的参数缺省值。这比目前实行的“在编译期决定”的机制更慢并且复杂。为了程序的执行速度和编译器实现上的简易度,c++做了这样的取舍,其结果就是你如今享受的执行效率。
带缺省参数的virtual函数
- 带缺省参数的virtual函数实现时,虚函数采用动态绑定,缺省参数采用静态绑定。在基类虚函数中声明的确省参数,即使派生类中重新指定了缺省参数,在多态调用时,依然采用基类虚函数中的缺省参数。
- C++采用这种奇怪的实现机制,主要是从实现效率的角度考虑的。
关于virtual函数的缺省参数的运行原理,我们彻底明白了。现在的问题是如果避免坠入陷阱。
有人提过这样一个策略:试图要求所有的派生类类型的设计工程师,在继承类实现时精确的复制基类中的虚函数参数默认值 ,已解决此类陷阱问题。实践证明这是一个糟糕的主意。
(1)未必所有的工程师都遵从这个要求,有些人也许根本不把此约定当回事。
(2)这个建议,没有考虑到(基类类型变化)不可预知的因素。其将派生类置于“基类类型变化”的危卵上。一旦虚函数的缺省参数发生变化了。这就势必造成所以派生类都要做相应的变化。对于一个大型工程,这种变化是一个浩大的工程。
(3)没有考虑到缺省参数可能发生变化。这种变化一般与基类和派生类的作用域有关。看下面这个例子。你很难指责派生类的设计人员使用了错误的MAX_STREAM_LENGTH。
const int MAX_STREAM_LENGTH = 1500;
namespace STREAM
{
class CStreamBase
{
public:
virtual SendStream(int iStreamLength = MAX_STREAM_LENGTH);
};
}
namespace STREAM
{
const int MAX_STREAM_LENGTH = 1460;
class CTCPStream: public CStreamBase
{
public:
virtual SendStream(int iStreamLength = MAX_STREAM_LENGTH);
};
class CUDPStream: public CStreamBase
{
public:
virtual SendStream(int iStreamLength = MAX_STREAM_LENGTH);
}
}
其实,问题还远非这些。最简单的避免方案就是避免在虚函数中提供参数的缺省初始化。考虑替代方案。NVI替代方案就是一种较好的替代方案:令基类的一个public non-virtual函数调用一个private virtual函数,后者可被派生类重新定义。可让non-virtual函数指定缺省参数,而private virtual函数负责真正实现工作。
class CStreamBase
{
public:
void SendStream(int iStreamLength = MAX_STREAM_LENGTH)
{
SendStreamImpl(iStreamLength);
}
virtual SendStreamImpl(int iStreamLength);
};
class CTCPStream: public CStreamBase
{
public:
virtual SendStreamImpl(int iStreamLength);
};
class CUDPStream: public CStreamBase
{
public:
virtual SendStreamImpl(int iStreamLength);
};
采用NVI这种方式,用户可采用静态的方式从基类类型的接口取得参数默认值,而派生类也可自由的变更函数的行为,而不用担心什么缺省初始化物了。
请谨记
|