无论是接口继承还是实现继承,都是public继承的范畴。public继承可分为两部分:接口继承和实现继承。同一种public继承方式,在不同代码环境中表现出不同的特性。这正是C++吸引人的地方,蕴含了深刻的面向对象设计理解和实现机制。

public继承可实现3种希望:希望派生类只继承父类的函数接口(即一个简单的函数声明);希望同时继承父类的函数接口和实现,同时还希望可重写实现;还有一种情况是,在某些情况下希望继承父类的函数接口和实现,但不希望对接口和实现进行任何修改。

为了说明这三种希望的区别,我们看Window自绘制的实现程序。IwndBase是一个抽象类,包含了一个Draw纯虚函数。客户不能声明一个IwndBase对象,而只能创建其派生类对象。IwndBase包含了三种函数声明:纯虚函数声明,非纯虚函数声明以及非虚函数声明。

IWndBase抽象类说明

  • 声明Draw纯虚函数,目的让派生类仅继承抽象类的函数接口,而不继承函数实现。
  • 声明ShowLog非纯虚函数,目的是派生类可继承抽象类的函数接口,也可继承抽象类的函数实现。
  • 声明SetColor非虚函数,目的是保证派生类继承抽象类的函数接口以及一份强制实现。
// Wnd 抽象基类
class IWndBase
{
public:

   virtual void Draw() = 0; 					// Wnd 绘制接口
   virtual void ShowLog(string &strLog);		// 显示Log日志接口
   void    SetColor(const COLOR &color);		// 设置颜色接口
   
private:
   COLOR   m_color;  					        // 颜色属性
};

class CButton : public IWndBase {....}; 	    // 按钮类
class CDialog : public IWndBase {....}; 	    // 对话框类

声明IwndBase::Draw为纯虚函数是最合理不过的了,因为所有的Wnd应该都可以Draw绘制,但是IwndBase不能提供默认实现,由于不同的Wnd其Draw绘制方式不一样的。IwndBase::Draw的真正作用是对所有派生类声明:“你们必须提供一个Draw函数,但是我不干涉你们怎么实现。”

程序开发人员可为纯虚函数提供一个函数定义,也就是说你可以为Draw提供一份函数实现代码,C++编译器不会因此抱怨。但有一点需要明确的是调用时需明确指定类域名称。

编程案例:

IWndBase *pWnd = new CButton;
pWnd->Draw();         		// 调用CButton::Draw函数
pWnd->CShape::Draw(); 		// IWndBase::Draw函数

声明非纯虚函数,目的是派生类可继承基类的函数接口,也可继承基类函数默认实现。基类为所有派生类提供默认实现,派生类必须提供一个ShowLog函数,如果你不愿意提供也没问题,CShape帮你提供一个默认版本。

为派生类提供默认函数实现听起来是一个不错的主意。如果你深入研究,你会发现其实不是那么回事,在有些时候这将成为陷阱。看笔者曾经实现过的报文构造设计继承体系。代码如下所示:

// 报文构造抽象类
class IMsgBuildBase
{
public:
	int BuildMsg(char *pszBuffer,  int iBufferLength) = 0;
    .....
protected:
    // 非加密报文默认构造实现
    int DefaultBuild(char *pszBuffer,  int iBufferLength);
};

// Radius报文构造实现类
class CRadiusMsg : public IMsgBuildBase
{
public:
    int BuildMsg(char *pszBuffer,  int iBufferLength)
    {
        return DefaultBuild(pszBuffer, iBufferLength);
    }
};

// SSL加密报文构造类
class CSSLMsg : public IMsgBuildBase
{
public:
    int BuildMsg(char *pszBuffer,  int iBufferLength)
    {
        // 加密的构造实现
    }
};

作为一个C++新手,要实现报文构造设计继承体系,必须对继承机制尤其是public的is-a机制有了深刻的掌握,但是这种实现还是存在一些瑕疵。

  1. 如果DefaultBuild是虚函数,在派生类重新定义了DefaultBuild会发生循环的问题,当然IMsgBuildBase::DefaultBuild是一个非虚函数,派生类不应该重定义此函数。
  2. DefaultBuild和BuildMsg函数命名过于雷同,是否存在命名空间污染的问题?

针对上述问题,介绍一种更好的实现方式:利用“纯虚函数在派生类中必须重新声明实现,但自己也可拥有实现”这一机制。来看下面的改进实现:

// 报文构造抽象类
class IMsgBuildBase
{
public:
	int BuildMsg(char *pszBuffer,  int iBufferLength) = 0;
    .....
};

//为纯虚函数提供定义
int IMsgBuildBase::BuildMsg(char *pszBuffer,  int iBufferLength)
{
	//缺省报文组装函数
}

// Radius报文构造实现类
class CRadiusMsg : public IMsgBuildBase
{
public:
	int BuildMsg(char *pszBuffer,  int iBufferLength)
    {
        return IMsgBuildBase::BuildMsg(pszBuffer, iBufferLength);
    }
};

// SSL加密报文构造类
class CSSLMsg : public IMsgBuildBase
{
public:
    int BuildMsg(char *pszBuffer,  int iBufferLength)
    {
        // 加密的构造实现
    }
};

最后,来看IWndBase的SetColor函数,SetColor是一个非虚函数。目的是为派生类提供一份强制性的函数接口实现。在任何的派生类中,都不应该重定义他。这是实用经验85所讨论的重点。

class IWndBase
{
public:
   void SetColor(const COLOR &color);
};

纯虚函数,非纯虚函数,非虚函数之间的差异,促使我们按照需要选择继承的方式。对于那些类设计经验不足的设计者来说,下面两类错误是需要注意的。

  1. 将所有的函数声明为virtual虚函数,虽然这样做不会产生什么严重问题。但是如无特殊情况,建议还是不要这么做。
  2. 将所有的函数声明为非虚函数,尽量不要这么做,因为这样会导致将C++多态和继承全部抛弃了。

建议

  • 在class设计时,将所有成员函数声明为virtual函数或将所有成员函数声明为非virtual函数,都是不可取的。
  • 对于接口类,所有函数声明为virtual函数,声明为纯virtual函数是一个不错的选择。

请谨记