多态是面向对象编程(OOP)的正式术语。 多态性通过虚函数在C ++中实现。
本文使用了一个简单的示例层次结构,您可能已经多次以一种或另一种形式看到它。 对这个示例的分析产生了一些不明显的问题,但这些问题将严重限制您在实际程序中使用该示例的层次结构的能力。 然后,本文提出了提出的解决方案,并得出了一些结论。
此面向对象编程(OOP)需要一个类的层次结构,其中常规类(C ++基类)位于层次结构的顶部。 特定类(从C ++派生的类)从通用类继承。
在运行时,将创建特定的对象(C ++派生的对象)并将其传递给需要常规对象的方法(以基本对象指针或引用作为参数的C ++函数)。 然后,当这些函数调用被认为是通用对象的方法时,实际上是针对特定对象的方法进行了调用,因为这才是真正的对象。
从这一点开始,本文将使用C ++术语基类和派生类。
的
类的公共方法称为类的接口 。 这是因为对类私有成员的任何访问都必须通过调用一种公共方法来完成。当您具有多态层次时,
基类的公共方法称为层次结构的接口 。 这是因为要使用基类指针或引用来调用派生类方法,则必须调用基类方法。 C ++虚拟函数机制允许派生类重写基类方法,以便将基类方法调用重定向到派生类方法。虚函数在C ++中实现了面向对象的编程(OOP)。 也就是说,如果您的层次结构没有虚拟基类方法,则您的C ++程序不是面向对象的。 这并不意味着您的程序设计错误或在某种程度上无效。 这仅意味着您不能用派生对象代替基础对象。
多态性的优点在于,仅需为基础对象编写应用程序代码。 为此,应用程序功能必须使用基类指针或引用作为参数。 您会看到,用派生对象代替基础对象时,它仍然是派生对象。 当应用程序调用基类方法时,该调用必须重定向到派生类方法。 这是使用虚拟功能表(VTBL)完成的。 派生类的VTBL具有要使用派生对象调用的函数的正确地址。 VBTL的地址由编译器嵌入在每个派生对象中。 调用虚拟方法时,编译器已生成代码以访问VTBL,以获取要调用的方法的地址。
顾名思义,VTBL具有该类所有虚拟函数的地址,无论是由类继承还是定义。 每个班级只有一个VTBL。
当您从应用程序中隐藏派生类的名称时,可以自由地在以后(也许是几年后)添加新的派生类,并将这些新对象传递到该旧应用程序代码中。 结果是新对象调用了它们的新方法,因为在应用程序中已经存在执行此操作的VTBL代码。 应用程序代码的寿命已延长。
必备知识本文假定您熟悉:
1)C ++类和继承
2)C ++类和公共/私有/受保护的访问说明符
3)虚拟功能机制,包括虚拟功能表(VTBL)
4)C ++函数重载和覆盖的规则
这个例子在此示例中是一个熟悉的应用程序:
1)形状层次
2)应用程序函数,它以Shape引用(或指针)作为
参数并调用虚拟方法。 虚方法做某事或
其他并以字符串流返回结果。 在示例中,虚拟
方法返回自己的名称。
3)main()调用应用程序函数。
这是层次结构:
class Shape
{
public:
virtual stringstream& AMethod(stringstream& str);
//assume other methods…
};
class Circle : public Shape
{
public:
virtual stringstream& AMethod(stringstream& str);
int Area();
};
class Cylinder : public Shape
{
public:
virtual stringstream& AMethod(stringstream& str);
int Volume();
};
class Point : public Shape
{
public:
};
stringstream& Shape::AMethod(stringstream& str)
{
str << "Shape::AMethod()" << endl;
return str;
}
stringstream& Circle::AMethod(stringstream& str)
{
str << "Circle::AMethod()" << endl;
return str;
}
stringstream& Cylinder::AMethod(stringstream& str)
{
str << "Cylinder::AMethod()" << endl;
return str;
}
Application函数通过引用(或通过指针)获取基础对象并调用AMethod。
因为AMethod是Shape类中的虚函数,并且在派生类中被覆盖,所以将Circle对象传递给Application函数将导致对Circle对象的AMethod()的调用。
在示例中,Application函数仅将字符串流作为字符串插入到ostream中。
ostream& Application(Shape& obj, ostream& os)
{
stringstream ss;
obj.AMethod(ss);
os << ss.str() << endl;
return os;
}
然后在main()中,创建Circle,Cylinder和Point对象,并将其用作Shape对象。
您可以看到一次定向到屏幕,一次定向到光盘文件的输出。
int main()
{
ofstream outfile("Data.txt");
Circle c;
Application(c, cout);
Application(c, outfile);
Cylinder cyl;
Application(cyl, cout);
Application(cyl, outfile);
Point p;
Application(p, cout);
Application(p, outfile);
return 0;
}
到目前为止,您可以自己编译和执行此代码,并验证是否显示正确的显示。
问题1:并非所有类都可以支持Shape接口
当您运行示例代码时,Point对象的AMethod报告为Shape :: AMethod()。 那是因为基本函数没有被Point覆盖。 您会看到,它没有被Point覆盖的原因是AMethod不适合Point。 其他Shape方法(本示例中假定为存在的方法)可能适用于Point,但不适用于AMethod。
另外,如果Shape :: AMethod是纯虚拟函数,则无论是否想要,Point类都将被强制实现AMethod方法。
Shape公用虚拟函数是Shape层次结构的接口,并由派生类继承。 这使得所有派生类都具有与Shape类相同的接口。 这是一个错误的假设。 考虑如果Shape层次结构是将Car,Boat和Airplane作为派生类的Vehicle层次结构,则所有Car,Boats和Airplanes必须具有相同的特征,例如Boat上的机翼或飞机上的可伸缩天窗。
使用公共虚拟功能的前提是
同构层次 。 也就是说,所有类都具有完全相同的公共方法。 这种情况很少发生在教科书或教室之外。这里真正发生的是Shape :: AMethod被用于两件事。 一方面,它是Shape层次结构接口的一部分。 另一方面,派生类可以使用自定义行为进行覆盖。 因此,要么派生类没有覆盖并且Point得到不好的结果,要么派生类覆盖了Shape :: AMethod的代码,但派生类丢失了。 第二种现象导致派生类以这种方式进行编码:
stringstream& Circle::AMethod(stringstream& str)
{
Shape::AMethod(str); //Recover base class implementation.
str << "Circle::AMethod()" << endl;
return str;
}
在此,Circle类方法调用了覆盖的Shape :: AMethod以恢复Shape类的实现。
如上所示,Shape类逻辑被假定为派生类逻辑的前提条件,因此在派生类逻辑之前调用基类函数。
但是,Shape类逻辑可能是Circle类逻辑的后置条件。
在这种情况下,将在派生类逻辑之后调用Shape类函数。
现在存在歧义,这是一个真正的弱点。
如果在Shape中使用纯虚函数,则可以避免此问题,但不能解决Point不希望实现AMethod的问题。 请记住,仅当您要强制派生类实现方法时,才使用纯虚函数。
解决问题1:将接口与实现分开您的基类公共函数是层次结构接口函数,不应被派生类覆盖。 您的基类虚函数是派生类要覆盖的虚函数。 通过将接口函数设为公共,将虚拟函数设为私有,可以将它们分开。
当您将Shape :: AMethod设为私有时,该应用程序现在没有方法可以调用。 这就要求您为Shape类编写一个公共和非虚拟的接口方法。 这个新的Shape方法(Shape :: Show)可以调用私有的Shape :: AMethod,也可以由应用程序调用。
现在,接口是公共和非虚拟功能,而实现是私有虚拟功能。 该接口是公共的。 该实现是私有的。 他们是分开的。
在C ++中,当您覆盖方法时,访问说明符(公共/专用/受保护)为
忽略了 。 对于Circle对象,VTBL包含Circle :: AMethod的地址,因为该方法将覆盖Shape :: AMethod。 当您使用Circle对象调用AMethod时,它将导致为AMethod的地址访问VTBL。 VTBL中用于AMethod的地址是Circle :: AMethod的地址,因此被称为。当您用Shape类的指针或引用标识Circle对象并调用私有Shape方法的调用时,将导致对Circle类的层次结构的调用。 这需要重复:基类指针或引用可用于调用派生对象的私有方法。 有时这被称为好莱坞原则:
不要打电话给我们,我们会打电话给您 。接下来,一点软件工程魔术将帮助Point类。 由于Shape :: AMethod现在是私有的,因此您可以编写一个默认实现,当诸如Point之类的类选择不覆盖时将执行该默认实现。
虚拟方法的默认实现或空值实现就是所谓的a
钩 。 派生类可以覆盖钩子,但不是必需的。 Shape :: AMethod现在是一个钩子。这是修改后的层次结构:
class Shape
{
private:
//Implementation
//Hook method
virtual stringstream& AMethod(stringstream& str);
public:
//Interface
stringstream& Show(stringstream& str);
//assume other methods…
};
class Circle : public Shape
{
private:
virtual stringstream& AMethod(stringstream& str);
public:
int Area();
};
class Cylinder : public Shape
{
private:
virtual stringstream& AMethod(stringstream& str);
public:
int Volume();
};
class Point : public Shape
{
public:
};
stringstream& Shape::Show(stringstream& str)
{
this->AMethod(str);
return str;
}
//A default implementation. This is a hook.
stringstream& Shape::AMethod(stringstream& str)
{
str << "Unsupported" << endl;
return str;
}
stringstream& Circle::AMethod(stringstream& str)
{
str << "Circle::AMethod()" << endl;
return str;
}
stringstream& Cylinder::AMethod(stringstream& str)
{
str << "Cylinder::AMethod()" << endl;
return str;
}
ostream& Application(Shape& obj, ostream& os)
{
stringstream ss;
obj.Show(ss);
os << ss.str() << endl;
return os;
}
int main()
{
ofstream outfile("Data.txt");
Circle c;
Application(c, cout);
Application(c, outfile);
Cylinder cyl;
Application(cyl, cout);
Application(cyl, outfile);
Point p;
Application(p, cout);
Application(p, outfile);
return 0;
}
问题2:必须在派生类中实现基类更改
在原始示例中,每个派生类均覆盖Shape :: AMethod。 这迫使Shape :: AMethod所需的任何更改都必须在派生类中实现。
假设需求发生变化,Shape :: AMethod必须将字符串流插入窗口框内的输出流中。 也就是说,字符串流的内容必须用星号包围。
stringstream& Circle::AMethod(stringstream& str)
{
str << "*********************"<< endl
<< "* Circle::AMethod() *" <<endl
<< "*********************" << endl;
return str;
}
为此,将需要更改每个派生类中Shape :: AMethod的替代,以合并此新功能,并以完全相同的方式进行更改,可能由不同公司雇用的不同开发人员进行。 发生这种情况的机会为零。
Shape基类最好提供窗口框,Shape :: Show必须编写以解决之前的问题,这是执行此操作的地方。 派生类,例如Circle可以
通过覆盖Shape :: Show来调用Shape :: AMethod的自定义钩子 。Shape :: Show方法现在包含所有派生类要遵循的逻辑步骤,并允许派生类定制输出。 Shape :: Show已成为一种算法。
在窗口框中插入AMethod结果的唯一更改是Shape :: Show方法:
ostream& Shape::Show(ostream& os)
{
stringstream str;
this->AMethod(str); //The Hollywood Principle: Shape calls derived method
//Find the number of characters in the stringstream
int len;
str.seekg(0, ios_base::end);
len = str.tellg();
str.seekg(0, ios_base::beg);
len -= str.tellg();
if (!len)
{
return os;
}
//Top of window box
os << "**";
for (int i = 0; i < len; ++i)
{
os << "*";
}
os <<"**" << endl;
//Middle of window box
os << "* " << str.str() << " *" << endl;
//Bottom of window box
os << "**";
for (int i = 0; i < len; ++i)
{
os << "*";
}
os << "**" << endl;
//End
return os;
}
设计模式:模板方法
非虚拟公共方法的概念并不新鲜。 1994年,埃里克·伽玛(Eric Gamma)和“四人帮”撰写了第一本关于设计模式的权威书籍。 它是使用面向对象编程规则和C ++编码示例解决的23种常见设计问题的集合。
一种设计模式称为模板方法。 这是设计模式本身的名称,与C ++模板无关。
在这种模式下,基类具有公共接口方法,并且使用私有虚拟钩子进行可选的自定义,并使用私有纯虚拟函数进行强制性的自定义。
本文中Shape :: Show所说明的基类方法包含除字符串流内容之外的所有逻辑。 这些内容是从Shape :: AMethod获得的。 要么使用默认值,要么该函数被派生类覆盖以用于特定内容。
这允许基类控制应用程序要遵循的步骤或算法。 在这种情况下,它将准备窗口框。 派生类可以
钩住以定制窗口框的内容,但不能删除它。公共接口与私有实现功能完全分开。
研究这种设计模式非常值得。
问题3:派生类可能具有不在基类中的方法普通继承具有派生类,这些类将方法添加到从基类继承的方法上。 这样可以节省对派生类中的这些基本方法的重新编码。 当应用程序不是面向对象的(如创建派生对象并将其用作派生对象)时,此方法效果很好。 也就是说,程序是基于对象而不是面向对象的。
当程序是面向对象的(使用多态性)时,所有方法都必须在基类中声明为public,因为派生对象仅由基类指针或引用标识。 这意味着要执行的任何派生方法也必须在基类中是公共的。
但是,实际上,一类对于某些方法可能希望是多态的,但可能具有不属于基类的其他方法。 除非在基类中声明了这些方法,否则根本无法调用这些方法。 同样,派生类被迫实现无法使用的方法,或者程序将无法编译。
在示例中,Circle类具有Circle :: Area()方法,而Cylinder类具有Cylinder :: Volume()方法。 因为这些方法不在基类中,所以它们对应用程序功能不可用:
void Application(Shape& obj)
{
obj.Show();
obj.Area(); //ERROR: 'Area' : is not a member of 'Shape'
obj.Volume(); //ERROR: 'Volume' : is not a member of 'Shape'
}
实际上,如果obj是一个Point,则Area()和Volume()都不可行。
仅当obj为Circle且Volume()要求obj为圆柱体时,才可以使用Area()。
不幸的是,Application()函数是用Shape参数编写的,因此无法访问派生类中不在基类中的方法。
这不是使用RTTI的信号。 如果这样做,则必须对obj的实型进行运行时检查。 如果是Circle,则必须将obj的地址类型转换为Circle *,以便可以调用Area()方法。
这很丑陋,原因很多:
a)每次新派生的应用程序功能都需要更改
除了Shape库中的方法外,还使用其他方法设计了该类
类,
b)将新方法添加到现有派生类中
c)RTTI会为每个包含以下内容的对象维护一个数据库
对象的实际类型,这会减慢执行速度
d)必须进行类型转换,并且涉及人工干预
e)Shape层次结构暴露出来,这违反了封装
f)这仅在obj具有公共虚拟方法时才有效。
加上Application函数Shape&参数不提供任何有关派生类的线索。 隐藏派生类就是面向对象编程的全部目的。 公开它们违反了首先进行面向对象编程的原因。
当仅通过基类引用或指针知道对象时,Visitor设计模式用于访问不在基类中的派生类方法。
被设计为多态基类的任何基类都应为Visitor设计。
在本文的最后,您可以在原始层次结构的最终版本中看到ShapeVisitor类。
您可以在C / C ++ HowTos中阅读有关visiter设计模式及其实现方法的文章,而无需在此重复详细信息。
问题4:派生的类析构函数可能不会被调用该问题在示例中并不明显。 在这里,设计师根本不使用析构函数,因为没有什么需要清理的。
但是,如果以后编写需要清除的派生类,则不会调用派生对象的析构函数,因为它将作为基础对象删除。 当您这样做时,仅基类析构函数被调用。
在原始示例中,Shape没有析构函数。 这导致编译器使用C ++默认值,即仅在Shape数据成员上调用析构函数。 在该示例中,没有Shape数据成员,因此实际上什么也没做。 如果派生类分配了在派生对象超出范围时需要释放的资源,则将不会调用派生类析构函数,并且会泄漏。
通过在Shape基类中声明一个虚拟析构函数,可以轻松解决此问题。 这提示编译器首先在派生类上调用析构函数,然后在Shape基类上调用析构函数。
每当为多态性设计层次结构时,派生类始终可能会分配需要通过调用派生类析构函数进行清理的资源。 因此,所有多态基类都必须具有虚拟析构函数。
重述:任何没有虚拟析构函数的多态基类都是设计错误。
class Shape
{
public:
ostream& Show(ostream& os);
virtual ~Shape();
private:
virtual void draw();
};
Shape::~Shape()
{
}
结论
1)虚拟功能应该是私有的。
课业和教科书公开使用它们的事实确实是20世纪的产物。 起初,这被认为是正确的做法。 但是,到1994年,公共虚拟功能已经成为一个问题,这一点变得越来越明显。 开发了模板方法设计模式来避免它们。
通常,具有公共虚函数的任何多态基类都是设计错误。
2)应该设置多态基类以容纳访客。
即使认为永远不会使用访客,也应该这样做。 几乎可以肯定的是,如果似乎不再需要访问者,那么就将需要它。 通过使当前设计可扩展来尝试着眼于未来。
3)所有多态基类都需要一个虚拟析构函数。
也就是说,任何没有虚拟析构函数的多态基类都是设计错误。
最终修改示例这是原始示例,其中包含解决上述原始设计中的问题所需的修改。
class ShapeVisitor;
class Shape
{
public:
ostream& Show(ostream& os);
void Visitor(ShapeVisitor*);
virtual ~Shape();
private:
virtual stringstream& AMethod(stringstream& str);
};
class Circle : public Shape
{
public:
int Area();
private:
virtual stringstream& AMethod(stringstream& str);
};
class Cylinder : public Shape
{
public:
int Volume();
private:
virtual stringstream& AMethod(stringstream& str);
};
class Point : public Shape
{
};
stringstream& Shape::AMethod(stringstream& str)
{
//This is a hook method.
//It provides a default value if draw()
//not supported by the derived class
str << "Unsupported";
return str;
}
Shape::~Shape()
{
}
stringstream& Circle::AMethod(stringstream& str)
{
str << "Circle::AMethod()";
return str;
}
stringstream& Cylinder::AMethod(stringstream& str)
{
str << "Cylinder::AMethod()";
return str;
}
ostream& Application(Shape& obj, ostream& os)
{
obj.Show(os);
return os;
}
ostream& Shape::Show(ostream& os)
{
stringstream str;
this->AMethod(str);
//Find the number of characters in the stringstream
int len;
str.seekg(0, ios_base::end);
len = str.tellg();
str.seekg(0, ios_base::beg);
len -= str.tellg();
if (!len)
{
return os;
}
//Top of window box
os << "**";
for (int i = 0; i < len; ++i)
{
os << "*";
}
os <<"**" << endl;
//Middle of window box
os << "* " << str.str() << " *" << endl;
//Bottom of window box
os << "**";
for (int i = 0; i < len; ++i)
{
os << "*";
}
os << "**" << endl;
//End
return os;
}
int main()
{
ofstream outfile("Data.txt");
Circle c;
Application(c, cout);
Application(c, outfile);
Cylinder cyl;
Application(cyl, cout);
Application(cyl, outfile);
Point p;
Application(p, cout);
Application(p, outfile);
return 0;
}
[b]进一步的信息[b]
请参阅Erich Gamma等人的《设计模式》一书,Addison-Wesley,1994年。
本文仅显示模板方法设计模式的概念基础,而不显示使用此模式的动机和影响。
版权所有2008 Buchmiller Technical Associates美国北华盛顿本德
From: https://bytes.com/topic/c/insights/793836-designing-polymorphism-c