回 帖 发 新 帖 刷新版面

主题:[原创]C++的多态威力,结合分离的类

这个帖子所用的例子参考了一个面向对象的书,希望大家可以看看这方面的书。C++是种强大的语言,尽量从多方面角度去思考,不要老是写成C的风格。
(注: 下面的程序还有两个地方有缺陷。是4楼的朋友指正的。修改方法,见4楼和5楼)
//////////////////////////////////////////////////////////
编程的时候,有个原则,将类和函数的实现细节隐藏起来,给用户一个统一的接口,这样用起来就会很方便。基本上,很多设计模式,很多技巧都是为了达到这个目的。如果你可以隐藏细节,给出一个统一的接口,你这个设计就是好的。

现在考虑两个类MouseMessage, KeyMessage, 他们的定义如下:
class MouseMessage
{
public:
    MouseMessage()                { message = "";  }
    MouseMessage(string str)        { message = str; }
    void setMessage(const string& str)    { message = str; }
    void writeMessage(ostream& out) const;
private:
    string message;
};

void MouseMessage::writeMessage(ostream& out) const
{
    out<<message<<endl;
}

//////////////////////////////////////////////////////////////
class KeyMessage
{
public:
    KeyMessage()            { message = "";  }
    KeyMessage(string str)        { message = str; }
    void set(const string& str)    { message = str; }
    friend ostream& operator<<(ostream& out, const KeyMessage& keyMsg);
private:
    string message;
};

ostream& operator<<(ostream& out, const KeyMessage& keyMsg)
{
    out<<keyMsg.message<<endl;
    return out;
}
///////////////////////////////////////////////////////
可以看出,这两个类都完成了设置和打印消息的功能,不过他们给出的接口,或者说函数的调用方法不一样。如果这两个类是你自己写的,你当然可以修改接口,使他们函数调用方法一致。问题就在于,他们是由不同的人写的,你得到的只是编译好的二进制文件,你不可以修改他们的源代码。不过他们有你想要的功能,所以你想利用他们的功能,节省自己重新编写代码的时间。现在你就有个目的,利用已有的类,并且使自己利用它们的方式一致。你可以这样做,写几个简单的函数:
inline void set(KeyMessage& keyMsg, const string& str)
{
    keyMsg.set(str);
}

inline void set(MouseMessage& mouseMsg, const string& str)
{
    mouseMsg.setMessage(str);
}

inline void write(ostream& out, const KeyMessage& keyMsg)
{
    out<<keyMsg;
}

inline void write(ostream& out, const MouseMessage& mouseMsg)
{
    mouseMsg.writeMessage(out);
}
/////////////////////////////////////////////////////////////
这个时候,因为函数是重载的,你可以统一的调用方法set,和write. 你可以写出这样的代码。
MouseMessage msg1("Mouse Move");
KeyMessage   msg2("Key Pressed");

set(msg1, "Mouse Drag");
set(msg2, "Key Up");

write(cout, msg1);
write(cout, msg2);
///////////////////////////////////////////////////////
不过这样调用起来,没有什么面向对象的感觉,你有点不满意。因为MouseMessage和KeyMessage都是消息,你也希望可以将他们都放在一个向量或者数组里面,可以历遍向量或者数组,统一的调用。很可惜,这两个类是不一样的,你不可以把他们放在一起。它们也不是在同一个类中派生出来的,所以你也不可以将它们的指针放在一起。那为了实现你的美好愿望,应该怎么做呢?
这时候,你可以使用下面的方法,写一个抽象类,名字就叫做Message
class Message
{
public:
    void virtual write(ostream& out) = 0;
    void virtual set(const string& str) = 0;
};
跟着,使用一个适配器
template <typename T>
class MessageAdapter : public Message
{
public:
    MessageAdapter(T* pt)    { ptMsg = pt; }
    void virtual write(ostream& out)    { ::write(out, *ptMsg); }
    void virtual set(const string& str)    { ::set(*ptMsg, str);    }
private:
    T* ptMsg;
};
再写一个函数
template <typename T>
Message* newMsg(T& msg)
{
    return new MessageAdapter<T>(&msg);
}
///////////////////////////////////////////////////////
现在你可以写出这样的代码了
MouseMessage msg1("Mouse Move");
KeyMessage   msg2("Key Pressed");

vecotr<Message*> v;
v.push_back(newMsg(msg1));
v.push_back(newMsg(msg2));

for (int i=0; i<v.size(); i++)
{
    v[i]->write(cout);
}
为什么你可以这样做呢,这是模板、虚函数和函数重载的结合。模板根据你的调用类型,推断出你用的是MouseMessage还是KeyMessage,跟着new出一个MessageAdapter,MessageAdapter派生自Message,所以可以用Message指针统一调用。根据虚函数的作用,Message指针调用MessageAdapter的write和set方法。因为MessageAdapter也是模板类,它也可以推断出你用的是MouseMessage还是KeyMessage,跟着调用了重载的全局函数write和set。这时候,你写出的代码可以很优美和统一了。
////////////////////////////////////////////////////
不过这时候,有点不足,你不可以写出这样的代码:
v.push_back(newMsg(MouseMessage("Mouse Drag")));
v[0]->write(cout);
因为newMsg的返回是个指针,MouseMessage("Mouse Drag")作为参数只不过是个临时变量,之后就注销了,既然没有对象,v[0]->write(cout);就没有意义了。所以你想将Message放进向量,一定要先定义一个对象。这样太不合理了,你没有理由想用向量存储100个指针,就要定义100个对象,这个是不可以忍受的。所以可以在定义一个函数:
template <typename T>
Message* newMsg(T* ptMsg)
{
    return new MessageAdapter<T>(ptMsg);
}
/////////////////////////////
注意,这个函数的参数是个指针。要是你传递的是指针就会调用这个函数,要是你传递的是数值,就会调用先前的那个函数。
从此之后,你可以写出这样的代码:
MouseMessage msg1("Mouse Move");
KeyMessage   msg2("Key Pressed");

vector<Message*> v;
v.push_back(newMsg(msg1));
v.push_back(newMsg(msg2));

v.push_back(newMsg(new MouseMessage("Mouse Dray")));
v.push_back(newMsg(new KeyMessage("Key Up")));

for (int i=0; i<v.size(); i++)
{
    v[i]->write(cout);
}
/////////////////////////////////////////
到这样,Every thing is perfect. 无论你传递的是指针,还是传值,都可以了,MouseMessage和KeyMessage也可以用相同的方式调用。
上面的例子用到了很多东西,可能会觉得有点复杂。而我们用的都是些短小的函数,并且都定义为inline形式。模板参数是在运行之前就已经确定的了,所以效率不会减少太多,几乎和你直接使用原先的两个类一样。例子用到了,虚函数,重载,模板,这都是多态的一种表现。这就是C++中多态的威力,帮助你写出优美的代码。

回复列表 (共19个回复)

11 楼

我也找到资料了,在More Effective c++里面也有。不过很长,learning.

12 楼

有的时候很多对象需要引用计数。为每个对象都添加相关的引用计数的代码感觉很麻烦。我想到两种在编码的时候可以简单一点的办法:

1、使用宏来拼装代码

// 必须放到class的public段
#define REF_COUNTER_IMPL(ref_func_name, deref_func_name) \
    private: int m_refCount; \
    public: int ref_func_name() { return ++m_refCount; } \
            int deref_func_name() \
    { if(--m_refCount==0){delete this;return 0;}else return m_refCount; }

#define REF_INIT() m_refCount(1)

2、使用继承
class RefCounter
{
public:
    RefCounter() : REF_INIT()
    {}

    REF_COUNTER_IMPL(reference, dereference)
};

class MyClass : public RefCounter, public IMyInterface, public ParentClass
{
 //...
};

如果一个对象又从其他的类或者接口继承的话,就要使用多重继承。似乎记得有篇文章列数了多重继承的N多坏处,不过我实在没有发现什么不好的地方。

13 楼

多谢sarrow的代码。现在正在看More Effective c++中关于引用计数的那个章节。有了点了解,总的来说引用计数是为了内存共享和提高对象复制和注销的效率,并安全回收内存。说起来简单,不过要实现得完美还要注意很多细节。

怎么才算完美呢,我的看法是使用户看不出你用了什么技术,你自己使用引用计数是你自己的事,不应该让调用你代码的人知道。引用计数的实现应该是在类的内部实现的,接口处要看不出来。
所以我觉得sarrow贴出来的那段代码,有两个方法是attach和detach将指针附上和撤掉,太漏痕迹了,总觉得不太好。应该是对象复制和注销的时候程序代码自己自动调用,不应该手动去调用。当然,那只是个小例子。是为了简单起见。

另外12楼SonicLing使用的宏或继承得到那两个方法只不过是增加引用计数值和减少引用计数值,本身没有多大用处。和前面所说的,太漏痕迹了。

至于多重继承。我自己是不喜欢继承的,尽量少用。我觉得继承来的函数,很多都是子类不会用到的。所以你继承的层次越多,得到的无用方法越多,在里面要找到你需要的方法越麻烦,还会一不小心就调用了原则上不应该调用的函数。这里有个原则,是使对外开放的接口要尽可能的少。多重继承,主要麻烦的是两个父类都有相同名称的函数,不过他们实现的功能不一样。这样你就要重新改写他们。
比如有两个类Graphics和Card. 他们都有个函数叫做draw.
对于Graphics类, draw函数是用来画出图象。draw的意思是画。
对于Card类,draw函数的作用是从一叠卡片中抽一张出来。draw的意思是抽取。
对于父类,draw的含义很确定。
不过要是有个子类GraphicsGard(图形卡片)继承了上面两个类,得到两个draw方法,就要重新改写。
另外多重继承还继承了很多没有用的方法。并且增加了类与类之间的联系。所以我不太喜欢继承。可以用组合的时候就不要用继承。

以上面是我自己的看法。望sarrow和SonicLing指教。

14 楼

还有点,引用计数是很有用。不过我觉得我最开始的那个程序例子没有必要用引用计数。因为结合那两个类的主要目的是为来可以存在同一个地方,统一的调用。本身就没有多少复制。改成5楼的那个样子就已经足够灵活了,复制对象和引用指针都可以自由选择了。new 和 delete是要成对出现,没有错。只是这个时候newMsg的就已经代替了new,本身和new的用法就很相似。只要在函数的中说明一下,还是很容易用的。为已经有的类加上引用计数是比较麻烦的,要是程序本身不是有大量相同的对象,似乎不值得。

15 楼

引用计数只在跨模块的时候用到。在模块内部使用引用计数感到很笨拙。

而我在12楼所说的与主题没太大关系,就当是胡扯吧。

16 楼

不会啊!也不是胡扯的,至少提供了种方法。使用宏来拼装代码本身也是挺妙的,只不过不好调试,要是错误了,很难定位到错误的地方。在模块内部也可以使用引用计数的,也很妙。正在看,似乎有点收获。要是看懂了,就再发发贴讨论这方面的问题。

17 楼

多态和模板体现的是C++编程思想,
Reference counting 看来更是一种实用技巧了。

18 楼

受益颇深,只是不知道具体是如何在汇编的层面上实现的.

19 楼

还是尽量使用标准库的比较好

这些代码
vecotr<Message*> v;
v.push_back(newMsg(msg1));
v.push_back(newMsg(msg2));

for (int i=0; i<v.size(); i++)
{
    v[i]->write(cout);
}
最好写成
struct print_message
{
  void operator () (Message * item) const
  {
     item ? item->write(cout) : NULL;
  }
};
for_each(v.begin(), v.end(), print_message());
如果嫌声明结构体太麻烦了(其实也不麻烦)
最起码也用 iterator去遍历
因为每调用一次v.size()就会计算一边 size大小

vector<Message *>::const_iterator it_end =
    v.end();
while (it_end != it)
{
    ......
}

则安全和易读 很多
或许觉得是习惯问题

不过,很多bug就是在这种习惯中,产生或者无意识的预防的

我来回复

您尚未登录,请登录后再回复。点此登录或注册