主题:[原创]C++的多态威力,结合分离的类
toyasimple
[专家分:820] 发布于 2006-12-11 12:26:00
这个帖子所用的例子参考了一个面向对象的书,希望大家可以看看这方面的书。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个回复)
沙发
sarrow [专家分:35660] 发布于 2006-12-10 19:01:00
值得加精的好贴!
建议给出阅读资料等相关信息。
板凳
toyasimple [专家分:820] 发布于 2006-12-10 19:16:00
在《面向对象编程导论》 (美)Timothy A.Budd著 有相似的例子。它给出的例子是出现两个类Orange和Apple,抽象类为Fruit。我改成Message,也修改了另外一些地方。不过原理是一样的,建议看看。那本书是将很多面向对象语言进行比较的,有Java,C++,C#,Smalltalk等等。
3 楼
eastcowboy [专家分:25370] 发布于 2006-12-11 00:19:00
C++学得不太好。顶一下。
4 楼
zlbruce [专家分:1700] 发布于 2006-12-11 04:08:00
cool!!学习中。。
不过有两个问题,毕竟 vector 里面存储的是指针,如果 msg1 或 msg2 过早的析构了,那么指针就垂悬了;
还有一个则是 v.push_back(newMsg(new MouseMessage("Mouse Dray"))); 这样产生的 Message 不能 delete
自己想加入引用计数来解决,不过没什么头绪,明天在来想想。
5 楼
toyasimple [专家分:820] 发布于 2006-12-11 12:17:00
很多谢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 楼
SonicLing [专家分:6260] 发布于 2006-12-11 13:18:00
还有一点要注意的是,new和delete必须出于同一个项目内,不要期望这个项目new的对象再另外一个项目delete,因为new和delete的实现在不同的开发环境中是不一样的。
因此跨模块的对象引用必须使用引用计数,让自己delete自己。
7 楼
toyasimple [专家分:820] 发布于 2006-12-11 13:32:00
我也想不到这个例子可以引申出这样多的东西。
我之前没有听说过引用计数,不太清楚。不过想来应该是设置一个计数器之类吧。不过应该怎么样实现啊!我会再去找找书来看看的。要是有朋友方便的话,可不可以说一说啊?最好是举个例子。自己注销自己,应该怎么做啊?
8 楼
sarrow [专家分:35660] 发布于 2006-12-11 13:41:00
delete this;
自己注销自己。
引用计数就是封装地址赋值。
STL中有封装好的模版。
9 楼
sarrow [专家分:35660] 发布于 2006-12-11 14:02:00
这是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 楼
iamasucker [专家分:160] 发布于 2006-12-11 14:04:00
学东西了。
我的理解: Message 与 MessageAdapter 之间的多态继承实现了接口的动态多态,从而可以去使用MouseMessage 与 KeyMessage 中我们感兴趣的接口。而模板则实现了内置数据成员的静态多态。上面几楼的讨论重点在于对动态内存细节处理。理解应该对吧。
我来回复