单例,抽象类和接口的作用是什么?


13

我正在学习C ++中的OOP,即使我知道这3个概念的定义,我也无法真正意识到何时或如何使用它。

让我们以此类为例:

class Person{
    private:
             string name;
             int age;
    public:
             Person(string p1, int p2){this->name=p1; this->age=p2;}
             ~Person(){}

             void set_name (string parameter){this->name=parameter;}                 
             void set_age (int parameter){this->age=parameter;}

             string get_name (){return this->name;}
             int get_age (){return this->age;}

             };

1. 单身人士

类的限制如何只有一个对象起作用?

CAN你设计一个类,将有只有 2个实例?也许3?

何时 /建议何时使用单例?这是好习惯吗?

2. 抽象类

据我所知,如果只有一个纯虚函数,则该类将变为抽象。因此,添加

virtual void print ()=0;

会做到的,对吧?

为什么需要一个不需要其对象的类?

3.接口

如果接口是其中所有方法都是纯虚函数的抽象类,则

两者之间的主要区别是什么

提前致谢!


2
辛格尔顿(Singleton)有争议,请在此网站上进行搜索以获得各种意见。
温斯顿·埃韦特

2
值得注意的是,虽然抽象类是语言的一部分,但单例或接口都不是。它们是人们实施的模式。特别是Singleton,需要一些巧妙的技巧才能工作。(尽管当然您只能按照惯例创建一个单例。)
Gort Robot'Robot''5

1
请一次一个。
JeffO

Answers:


17

1.单身人士

您限制实例的数量是因为构造函数将是私有的,这意味着只有静态方法才能创建该类的实例(实际上还有其他肮脏的技巧可以实现该目的,但不要被迷惑)。

创建只包含2个或3个实例的类是完全可行的。每当需要在整个系统中仅拥有该类的一个实例时,就应该使用单例。这通常发生在具有“经理”行为的类上。

如果您想了解更多关于单身,你可以在开始维基百科,特别是用于C ++的这个帖子

这种模式肯定有好有坏,但是这种讨论属于其他地方。

2.抽象类

是的,这是对的。只有单个虚拟方法会将类标记为抽象。

当您具有较大的类层次结构时,您将使用这类类,而顶级类不应真正实例化。

假设您要定义一个哺乳动物类,然后将其继承给Dog and Cat。如果您考虑一下,那么拥有纯哺乳动物的实例就没有多大意义,因为您首先需要知道它的真正类型。

可能有一个叫做MakeSound()的方法仅在继承的类中才有意义,但没有所有哺乳动物都能发出的声音(这只是一个示例,在这里不尝试说明哺乳动物的声音)。

因此,这意味着哺乳动物应该成为抽象类,因为它将对所有哺乳动物实施一些常见的行为,但实际上并没有被实例化。那是抽象类背后的基本概念,但是您还应该学习更多的基本概念。

3.接口

与您在Java或C#中的含义相同,C ++中没有纯接口。创建一个的唯一方法是拥有一个纯抽象类,该抽象类模仿您希望从接口获得的大多数行为。

基本上,您要寻找的行为是定义一个契约,其他对象可以与之交互而不关心基础实现。当您将一个类完全抽象时,这意味着所有实现都属于其他位置,因此该类的目的仅在于它定义的协定。在OO中,这是一个非常强大的概念,您绝对应该对其进行更多研究。

您可以阅读有关MSDN中C#的接口规范的更多信息:

http://msdn.microsoft.com/en-us/library/ms173156.aspx

C ++将通过具有一个纯抽象类来提供相同的行为。


2
一个纯抽象基类为您提供接口所做的一切。接口存在于Java(和C#)中,因为语言设计人员希望防止多重继承(由于它造成的麻烦),但是却认识到多重继承的一种非常普遍的用法,这没有问题。
Gort机器人2012年

@StevenBurnap:但是不是在C ++中,这是问题所在。
DeadMG

3
他在问C ++和接口。“接口”不是C ++的语言功能,但是人们肯定会使用抽象基类在C ++中创建与Java接口完全一样的接口。他们这样做是在Java尚未出现之前。
Gort机器人2012年


1
Singletons也是如此。在C ++中,两者都是设计模式,而不是语言功能。这并不意味着人们不会谈论C ++中的接口及其用途。“接口”的概念来自于像Corba和COM这样的组件系统,它们最初都是为在纯C中使用而开发的。在C ++中,接口通常使用抽象基类实现,其中所有方法都是虚拟的。其功能与Java接口的功能相同。因此,Java接口的概念故意是C ++抽象类的子集。
Gort机器人2012年

8

大多数人已经解释了什么是单例/抽象类。希望我会提供一些不同的观点并给出一些实际的例子。

单例-无论出于何种原因,当您希望所有调用代码都使用单个变量实例时,可以使用以下选项:

  • 全局变量-显然没有封装,大多数代码都耦合到了全局变量上。
  • 具有所有静态函数的类-比简单的全局函数好一点,但是此设计决策仍将您带到一条代码依赖全局数据的路径,以后可能很难更改。如果您只有静态函数,那么您也无法利用OO之类的多态性
  • Singleton-即使该类只有一个实例,该类的实际实现也不必知道它是全局的。因此,今天您可以拥有一个单例类,明天您可以简单地将其构造函数公开,并让客户端实例化多个副本。引用单例的大多数客户端代码无需更改,单例本身的实现也无需更改。唯一的变化是客户端代码首先如何获取单例引用。

在所有有害和不良的选择中,如果您确实需要全局数据,则单例方法比前两种方法都要好得多。如果明天您改变主意并决定使用控制反转而不是使用全局数据,它还使您可以保持选择的状态。

那么您将在哪里使用单例呢?以下是一些示例:

  • 日志记录-如果您希望整个过程只有一个日志,则可以创建一个日志对象并将其传递到任何地方。但是,如果您有100,000k行的旧版应用程序代码怎么办?修改所有这些?或者,您可以简单地介绍以下内容,并在您喜欢的任何地方开始使用它:

    CLog::GetInstance().write( "my log message goes here" );
  • 服务器连接缓存-这是我必须在我们的应用程序中引入的东西。我们的代码库(其中有很多)可随时用于连接服务器。在大多数情况下,这是可以的,除非网络中存在任何类型的延迟。我们需要一个解决方案,而重新设计一个已经使用了10年的应用程序并没有真正实现。我写了一个单例的CServerConnectionManager。然后,我搜索了代码,并将CoCreateInstanceWithAuth调用替换为调用我的类的相同签名调用。现在,在第一次尝试连接被缓存之后,其余时间“连接”尝试都是瞬时的。有人说单身是邪恶的。我说他们救了我的屁股。

  • 对于调试,我们经常发现全局运行对象表非常有用。我们有一些我们想跟踪的课程。它们都源自相同的基类。在实例化期间,它们将对象表称为单例并注册自己。当它们被销毁时,它们将取消注册。我可以走到任何机器,附加到进程并创建运行对象的列表。进入产品已有超过五年的时间,我从没有觉得我们曾经需要2个“全局”对象表。

  • 我们有一些相对复杂的字符串解析器实用程序类,它们依赖于正则表达式。正则表达式类需要先进行初始化,然后才能执行匹配。初始化有些昂贵,因为那是基于解析字符串生成FSM的时候。但是在那之后,正则表达式类可以被100个线程安全地访问,因为一旦构建FSM便再也不会改变。这些解析器类在内部使用单例来确保此初始化仅发生一次。这显着提高了性能,并且从未因“邪恶的单例”而引起任何问题。

综上所述,您确实需要记住何时何地使用单例。每10次就有9次有更好的解决方案,无论如何,您应该使用它。但是,有时候单例绝对是正确的设计选择。

下一主题...接口和抽象类。首先,正如其他人提到的那样,接口是一个抽象类,但是它通过强制它绝对没有实现超出了它。在某些语言中,interface关键字是该语言的一部分。在C ++中,我们仅使用抽象类。Microsoft VC ++采取了内部定义此步骤:

typedef struct interface;

...因此您仍然可以使用interface关键字(甚至会突出显示为“ real”关键字),但是就实际的编译器而言,它只是一个结构。

那么您将在哪里使用它呢?让我们回到运行对象表的示例。假设基类有...

虚拟虚空print()= 0;

有你的抽象课。使用运行时对象表的类都将从同一个基类派生。基类包含用于注册/注销的通用代码。但是它永远不会被实例化。现在,我可以拥有派生类(例如,请求,侦听器,客户端连接对象...),每个类都将实现print(),以便在附加到进程并询问它在运行什么时,每个对象将报告其自己的状态。

抽象类/接口的示例数不胜数,与单例相比,您肯定会更频繁地使用(或应该使用)它们。简而言之,它们允许您编写适用于基本类型的代码,并且与实际实现无关。这样,您以后就可以修改实现,而不必更改太多代码。

这是另一个例子。假设我有一个实现记录器的类CLog。此类写入本地磁盘上的文件。我开始在旧的100,000行代码中使用此类。到处都是。除非有人说,生活是美好的,嘿,让我们写数据库而不是文件。现在,我创建了一个新类,我们将其称为CDbLog并写入数据库。您能想象一下穿越100,000行并将所有内容从CLog更改为CDbLog的麻烦吗?或者,我可以有:

interface ILogger {
    virtual void write( const char* format, ... ) = 0;
};

class CLog : public ILogger { ... };

class CDbLog : public ILogger { ... };

class CLogFactory {
    ILogger* GetLog();
};

如果所有代码都使用ILogger接口,那么我要做的就是更改CLogFactory :: GetLog()的内部实现。其余代码将自动运行,而无需我费力。

有关接口和良好的OO设计的更多信息,我强烈推荐Bob叔叔的C#中敏捷原理,模式和实践。本书包含使用抽象的示例,并提供了所有内容的通俗易懂的语言说明。


4

何时/建议何时使用单例?这是好习惯吗?

决不。更糟糕的是,它们绝对是要摆脱的母狗,因此一旦犯了这个错误,您可能会困扰很多年。

在C ++中,抽象类和接口之间的区别绝对没有。通常,您可以使用接口来指定派生类的某些行为,而不必完全指定所有行为。这使您的代码更灵活,因为您可以换出任何符合更严格规范的类。需要运行时抽象时使用运行时接口。


接口是抽象类的子集。接口是没有定义方法的抽象类。(完全没有任何代码的抽象类。)
弄乱了机器人

1
@StevenBurnap:也许用其他语言。
DeadMG

4
“接口”只是C ++中的一个约定。当我看到它使用时,它是一个仅包含纯虚方法而没有属性的抽象类。显然,您可以编写任何旧类,并在名称前面打一个“ I”。
Gort机器人2012年

这是我期望人们回答此帖子的方式。一次提出一个问题。无论如何,非常感谢你们分享您的知识。这个社区是值得投资的时间。
appoll

3

当您不希望特定对象有多个副本时,单例功能非常有用,该对象只能有一个此类的实例-它用于维护全局状态,必须以某种方式处理非可重入代码的对象等。

具有固定数量的2个或更多实例的单例是一个multiton,请考虑数据库连接池等。

接口指定了定义良好的API,可帮助建模对象之间的交互。在某些情况下,您可能会有一组确实具有某些通用功能的类-如果是这样,则可以在接口中添加方法定义以将其变成抽象类,而不必在实现中进行复制。

您甚至可以在其中实现所有方法的抽象类中进行标记,但是将其标记为抽象,以指示在没有子类的情况下不应按原样使用它。

注意:接口和抽象类在具有多重继承等的C ++世界中并没有太大的区别,但是在Java等人中却有不同的含义。


说得好!+1
jmort253

3

如果您停止考虑它,那么它全都涉及多态性。您希望能够一次编写一段代码,然后再根据所传递的内容进行思考。

假设我们有一个类似以下Python代码的函数:

function foo(objs):
    for obj in objs:
        obj.printToScreen()

class HappyWidget:
    def printToScreen(self):
        print "I am a happy widget"

class SadWidget:
    def printToScreen(self):
        print "I am a sad widget"

关于此功能的好处是,只要这些对象实现“ printToScreen”方法,它将能够处理任何对象列表。您可以向其传递一个快乐的小部件列表,一个悲伤的小部件列表,甚至是将它们混合在一起的列表,并且foo函数仍然可以正确执行其操作。

我们将这种类型的限制称为需要将一组方法(在本例中为printToScreen)实现为接口,并且将实现所有方法的对象称为实现接口。

如果我们正在谈论像Python这样的动态鸭式语言,那么现在基本上已经结束了。但是,C ++的静态类型系统要求我们在函数中为objecs提供一个类,并且该类只能与该初始类的子类一起使用。

void foo( Printable *objs[], int n){ //Please correctme if I messed up on the type signature
    for(int i=0; i<n; i++){
        objs[i]->printToScreen();
    }
}

在我们的例子中,Printable类存在的唯一原因是为printToScreen方法提供一个存在的地方。由于在实现printToScreen方法的类之间没有共享的实现,因此将Printable变成抽象类是有意义的,该抽象类仅用作在公共层次结构中对相似类进行分组的一种方式。

在C ++中,抽象类和接口概念有些模糊。如果要更好地定义它们,则要考虑抽象类,而接口通常意味着对象公开的一组可见方法更通用,更跨语言。(尽管某些语言(例如Java)使用接口术语来更直接地引用某些东西,例如抽象基类)

基本上,具体类指定对象的实现方式,而抽象类指定它们与其余代码的接口。为了使您的函数具有更多的多态性,您应该尝试在适当的时候接收一个指向抽象超类的指针。


至于Singletons,它们实际上是没有用的,因为它们通常可以被一组静态方法或简单的旧函数代替。但是,有时您会受到某种限制,即使您确实不想使用一个对象,也可能会强制您使用某个对象,因此单例模式是合适的。


顺便说一句,有些人可能已经评论说“接口”一词在Java语言中具有特定的含义。我认为现在最好还是使用更笼统的定义。


1

介面

很难理解解决从未遇到过的问题的工具的目的。开始编程后一段时间我都不了解接口。我们会了解他们的所作所为,但我不知道您为什么要使用其中一个。

问题出在这里-您知道要做什么,但是您有多种方式来做,否则以后可能会更改。如果您可以担任无能为力的经理一职,那就很不错了-吠叫一些订单并获得所需的结果而无需关心它的完成方式。

假设您有一个很小的网站,并且将所有用户信息保存在一个csv文件中。这不是最复杂的解决方案,但它足以存储您妈妈的用户详细信息。后来,您的网站起飞了,您有10,000个用户。也许是时候使用适当的数据库了。

如果您一开始很聪明,您会发现这种情况即将发生,而不会直接调用将其保存到csv。取而代之的是,无论实现方式如何,您都需要考虑它需要做什么。假设store()retrieve()。你让一个Persister接口,与抽象方法store(),并retrieve()创造了CsvPersister实际实现这些方法的子类。

以后,您可以创建一个DbPersister实现与csv类完全不同的方式来实现数据的实际存储和检索。

很棒的是,您现在要做的就是改变

Persister* prst = new CsvPersister();

Persister* prst = new DbPersister();

然后就完成了。您的来电prst.store(),并prst.retrieve()都将仍然工作,他们只是不同的处理“幕后”。

现在,您仍然必须创建cvs和db实现,因此您还没有体验过成为老板的奢华。当您使用别人创建的接口时,真正的好处显而易见。如果其他人足够创建一个CsvPersister()并且DbPersister()已经足够了,那么您只需要选择一个并调用必要的方法即可。如果您决定以后再使用另一个,或在另一个项目中使用,则已经知道它是如何工作的。

我对C ++真的很生疏,所以我只使用一些通用的编程示例。容器是接口如何使您的生活更轻松的一个很好的例子。

你可以有ArrayLinkedListBinaryTree,等所有子类的Container具有类似方法insert()find()delete()

现在,当在链表的中间添加一些内容时,您甚至不必知道链表是​​什么。您只需致电myLinkedList->insert(4),它就会神奇地遍历列表并将其粘贴在其中。即使您知道链表是​​如何工作的(您确实应该这样做),也不必查找其特定功能,因为您可能已经很早就知道了它们的作用Container

抽象类

抽象类非常类似于接口(从技术上讲,接口抽象类,但是这里我指的是具有某些方法充实的基类。

假设您正在创建游戏,并且需要检测敌人何时在玩家的打击范围内。您可以创建一个Enemy具有方法的基类inRange()。尽管敌人有很多不同之处,但用于检查其射程的方法是一致的。因此,您的Enemy班级将拥有充实的方法来检查射程,但对于其他事物则使用纯虚拟方法,这些方法在敌人类型之间没有相似之处。

这样做的好处是,如果您弄乱了范围检测代码或想要对其进行调整,则只需在一个地方进行更改即可。

当然,接口和抽象基类还有很多其他原因,但这是您可能会使用它们的一些原因。

单身人士

我偶尔使用它们,但从未被它们烧死。这并不是说,根据别人的经验,他们不会在某个时候毁了我的生活。

以下是一些经验丰富且警惕的人们对全球状态的精彩讨论: 为什么全球状态如此邪恶?


1

在动物界,有各种各样的哺乳动物。在这里,哺乳动物是基类,各种各样的动物都源于它。

您见过哺乳动物走过吗?是的,我确定很多次-但是它们都是哺乳动物的类型,不是吗?

您从未见过真正只是哺乳动物的东西。它们都是哺乳动物。

要求哺乳动物类定义各种特征和组,但它不作为物理实体存在。

因此,它是一个抽象基类。

哺乳动物如何运动?他们会走路,游泳,飞行等吗?

在哺乳动物的层次上没有办法知道,但是所有的哺乳动物都必须以某种方式运动(让我们说这是使例子更容易的生物学定律)。

因此MoveAround()是一个虚函数,因为从此类派生的每个哺乳动物都需要能够以不同的方式实现它。

但是,因为每个哺乳动物都必须定义MoveAround,因为所有哺乳动物都必须移动,并且不可能在哺乳动物一级进行移动。它必须由所有子类实现,但在基类中没有任何意义。

因此,MoveAround是纯虚拟函数。

如果您有一个完整的类允许活动,但不能在顶层定义应如何进行活动,则所有功能都是纯虚拟的,这是一个接口。
例如,如果我们有一个游戏,您将在其中编码一个机器人并将其提交给我,以便在战场上作战,那么我需要知道要调用的函数名称和原型。只要“接口”清晰可见,我都不关心您如何实现它。因此,我可以为您提供一个接口类,您可以从该接口类中编写杀手机器人。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.