回 帖 发 新 帖 刷新版面

主题:[原创]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个回复)

沙发

值得加精的好贴!

建议给出阅读资料等相关信息。

板凳

在《面向对象编程导论》 (美)Timothy A.Budd著 有相似的例子。它给出的例子是出现两个类Orange和Apple,抽象类为Fruit。我改成Message,也修改了另外一些地方。不过原理是一样的,建议看看。那本书是将很多面向对象语言进行比较的,有Java,C++,C#,Smalltalk等等。

3 楼

C++学得不太好。顶一下。

4 楼

cool!!学习中。。
不过有两个问题,毕竟 vector 里面存储的是指针,如果 msg1 或 msg2 过早的析构了,那么指针就垂悬了;
还有一个则是 v.push_back(newMsg(new MouseMessage("Mouse Dray"))); 这样产生的 Message 不能 delete
自己想加入引用计数来解决,不过没什么头绪,明天在来想想。

5 楼

很多谢4楼,4楼是个有心人。这两个确实是个问题。我做了修改。
其中将适配器修改为
template <typename T>
class MessageAdapter : public Message
{
public:
    MessageAdapter(const T& msg)        { ptMsg = new T(msg);   }
    MessageAdapter(T* pt)            { ptMsg = pt;        }
    void virtual write(ostream& out)    { ::write(out, *ptMsg); }
    void virtual set(const string& str)    { ::set(*ptMsg, str);    }

    ~MessageAdapter()    { delete ptMsg; }
private:
    T* ptMsg;
};
辅助函数修改为
template <typename T>
Message* newMsg(const T& msg)
{
    return new MessageAdapter<T>(msg);
}

template <typename T>
Message* newMsg(T* ptMsg)
{
    return new MessageAdapter<T>(ptMsg);
}
////////////////////////////////////
这样的话,要是你想引用指针,就可以写成
KeyMessage keyMsg("Key Up");
Message* newMsg(&keyMsg);
要是你写成
Message* ptMsg = newMsg(keyMsg);
就可以将整个KeyMessage复制了过去,keyMsg的析构跟指针没有联系了,就没有指针空悬了。用那种调用可以根据你自己的程序而定。
自然这个时候可以调用
delete ptMsg, 将对象注销掉。这样可以不失灵活和效率。
这时候可以这样使用 Message* ptMsg = newMsg(KeyMessage("Key Up"));
比原来的Message* ptMsg = newMsg(new KeyMessage("Key UP"));还要方便和易用。

6 楼

还有一点要注意的是,new和delete必须出于同一个项目内,不要期望这个项目new的对象再另外一个项目delete,因为new和delete的实现在不同的开发环境中是不一样的。

因此跨模块的对象引用必须使用引用计数,让自己delete自己。

7 楼

我也想不到这个例子可以引申出这样多的东西。
我之前没有听说过引用计数,不太清楚。不过想来应该是设置一个计数器之类吧。不过应该怎么样实现啊!我会再去找找书来看看的。要是有朋友方便的话,可不可以说一说啊?最好是举个例子。自己注销自己,应该怎么做啊?

8 楼

delete this;

自己注销自己。

引用计数就是封装地址赋值。

STL中有封装好的模版。

9 楼

这是Think.in.C++上的一个例子,在page554上。

//: C12:ReferenceCounting.cpp
// Reference count, copy-on-write
#include <string>
#include <iostream>
using namespace std;

class Dog {
  string nm;
  int refcount;
  Dog(const string& name)
    : nm(name), refcount(1) {
    cout << "Creating Dog: " << *this << endl;
  }
  // Prevent assignment:
  Dog& operator=(const Dog& rv);
public:
  // Dogs can only be created on the heap:
  static Dog* make(const string& name) {
    return new Dog(name);
  }
 Dog(const Dog& d)
    : nm(d.nm + " copy"), refcount(1) {
    cout << "Dog copy-constructor: "
         << *this << endl;
  }
  ~Dog() {
    cout << "Deleting Dog: " << *this << endl;
  }
  void attach() {
    ++refcount;
    cout << "Attached Dog: " << *this << endl;
  }
  void detach() {
    cout << "Detaching Dog: " << *this << endl;
    // Destroy object if no one is using it:
    if(--refcount == 0) delete this;
  }
  // Conditionally copy this Dog.
  // Call before modifying the Dog, assign
  // resulting pointer to your Dog*.
  Dog* unalias() {
    cout << "Unaliasing Dog: " << *this << endl;
    // Don't duplicate if not aliased:
    if(refcount == 1) return this;
    --refcount;
    // Use copy-constructor to duplicate:
    return new Dog(*this);
  }
  void rename(const string& newName) {
    nm = newName;
    cout << "Dog renamed to: " << *this << endl;
  }
  friend ostream&
  operator<<(ostream& os, const Dog& d) {
    return os << "[" << d.nm << "], rc = "
      << d.refcount;
  }
};

class DogHouse {
  Dog* p;
  string houseName;
public:
  DogHouse(Dog* dog, const string& house)
  : p(dog), houseName(house) {
   cout << "Created DogHouse: "<< *this << endl;
 }
 DogHouse(const DogHouse& dh)
   : p(dh.p),
     houseName("copy-constructed " +
       dh.houseName) {
   p->attach();
   cout << "DogHouse copy-constructor: "
        << *this << endl;
 }
 DogHouse& operator=(const DogHouse& dh) {
   // Check for self-assignment:
   if(&dh != this) {
     houseName = dh.houseName + " assigned";
     // Clean up what you're using first:
     p->detach();
     p = dh.p; // Like copy-constructor
     p->attach();
   }
   cout << "DogHouse operator= : "
        << *this << endl;
   return *this;
 }
 // Decrement refcount, conditionally destroy
 ~DogHouse() {
   cout << "DogHouse destructor: "
        << *this << endl;
   p->detach();
 }
 void renameHouse(const string& newName) {
   houseName = newName;
 }
 void unalias() { p = p->unalias(); }
 // Copy-on-write. Anytime you modify the
 // contents of the pointer you must
 // first unalias it:
 void renameDog(const string& newName) {
   unalias();
   p->rename(newName);
 }
 // ... or when you allow someone else access:
 Dog* getDog() {
   unalias();
   return p;
  }
  friend ostream&
  operator<<(ostream& os, const DogHouse& dh) {
    return os << "[" << dh.houseName
      << "] contains " << *dh.p;
  }
};

int main() {
  DogHouse
    fidos(Dog::make("Fido"), "FidoHouse"),
    spots(Dog::make("Spot"), "SpotHouse");
  cout << "Entering copy-construction" << endl;
  DogHouse bobs(fidos);
  cout << "After copy-constructing bobs" << endl;
  cout << "fidos:" << fidos << endl;
  cout << "spots:" << spots << endl;
  cout << "bobs:" << bobs << endl;
  cout << "Entering spots = fidos" << endl;
  spots = fidos;
  cout << "After spots = fidos" << endl;
  cout << "spots:" << spots << endl;
  cout << "Entering self-assignment" << endl;
  bobs = bobs;
  cout << "After self-assignment" << endl;
  cout << "bobs:" << bobs << endl;
  // Comment out the following lines:
  cout << "Entering rename(\"Bob\")" << endl;
  bobs.getDog()->rename("Bob");
  cout << "After rename(\"Bob\")" << endl;
} ///:~

10 楼

学东西了。
我的理解: Message 与 MessageAdapter 之间的多态继承实现了接口的动态多态,从而可以去使用MouseMessage 与 KeyMessage 中我们感兴趣的接口。而模板则实现了内置数据成员的静态多态。上面几楼的讨论重点在于对动态内存细节处理。理解应该对吧。

我来回复

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