您如何在C ++中声明接口?


Answers:


685

为了扩展bradtgmurray的答案,您可能需要通过添加虚拟析构函数来对接口的纯虚拟方法列表进行例外处理。这使您可以将指针所有权传递给另一方,而无需暴露具体的派生类。析构函数不需要执行任何操作,因为接口没有任何具体成员。将函数定义为虚拟函数和内联函数似乎很矛盾,但是请相信我,事实并非如此。

class IDemo
{
    public:
        virtual ~IDemo() {}
        virtual void OverrideMe() = 0;
};

class Parent
{
    public:
        virtual ~Parent();
};

class Child : public Parent, public IDemo
{
    public:
        virtual void OverrideMe()
        {
            //do stuff
        }
};

您不必包括虚拟析构函数的主体-事实证明,某些编译器在优化空析构函数时遇到了麻烦,最好使用默认值。


105
虚拟解码器++!这个非常重要。您可能还希望包括operator =的纯虚拟声明,并复制构造函数定义,以防止编译器为您自动生成那些声明。
xan

33
虚拟析构函数的替代方法是受保护的析构函数。这将禁用多态破坏,这在某些情况下可能更合适。在gotw.ca/publications/mill18.htm中查找“准则4” 。
Fred Larson

9
另一种选择是=0使用主体定义纯虚拟()析构函数。这样做的好处是,从理论上讲,编译器可以看到vtable现在没有有效的成员,并将其完全丢弃。对于带有主体的虚拟析构函数,可以在构造过程中(例如,通过this指针)(当所构造的对象仍为Parent类型时)调用(虚拟)该析构函数,因此编译器必须提供有效的vtable。因此,如果this在构造过程中不显式调用虚拟析构函数:),可以节省代码大小。
帕维尔米纳夫

50
在C ++答案中,最常见的答案通常不能直接回答问题(尽管显然代码是完美的),但是它可以优化简单的答案。
蒂姆(Tim)

18
不要忘记,在C ++ 11中,您可以指定override关键字以允许编译时参数和返回值类型检查。例如,在宣告Childvirtual void OverrideMe() override;
Sean

243

使用纯虚方法创建类。通过创建另一个覆盖这些虚拟方法的类来使用该接口。

纯虚拟方法是定义为虚拟并分配给0的类方法。

class IDemo
{
    public:
        virtual ~IDemo() {}
        virtual void OverrideMe() = 0;
};

class Child : public IDemo
{
    public:
        virtual void OverrideMe()
        {
            //do stuff
        }
};

29
您应该在IDemo中具有不执行任何操作的析构函数,以便将其定义为要执行的行为:IDemo * p = new Child; / *无论* /删除p;
埃文·特兰

11
为什么Child类中的OverrideMe方法是虚拟的?那有必要吗?
Cemre 2012年

9
@Cemre-不,这不是必需的,但也不会造成伤害。
PowerApp101

11
通常,每当覆盖虚拟方法时都保留关键字“ virtual”是一个好主意。尽管不是必需的,但是它可以使代码更清晰-否则,您没有指示该方法可以多态使用,甚至不存在于基类中。
凯文(Kevin)

27
@Kevin除了override在C ++ 11中使用
键入者

146

在C#/ Java中除了抽象基类之外,还具有特殊的接口类型类别的全部原因是,因为C#/ Java不支持多重继承。

C ++支持多重继承,因此不需要特殊类型。没有非抽象(纯虚拟)方法的抽象基类在功能上等效于C#/ Java接口。


17
能够创建接口,以免我们键入太多代码(virtual,= 0,virtual destructor),仍然很不错。同样,多重继承对我来说似乎不是一个好主意,我从未见过它在实践中使用过,但一直需要接口。糟糕的是,C ++社区不会仅仅因为我想要接口而引入接口。
Ha11ow12年

9
Ha11owed:它具有接口。它们被称为具有纯虚拟方法且没有方法实现的类。
Miles Rout

6
@doc:java.lang.Thread具有您可能不希望在对象中包含的方法和常量。如果您从Thread扩展到另一个带有公共方法checkAccess()的类,则编译器应该怎么做?您是否真的愿意像在C ++中那样使用强命名的基本指针?这似乎是一个糟糕的设计,通常您需要在需要多重继承的地方进行组合。
Ha11ow14年

4
@ Ha11owed是很久以前的,所以我不记得详细信息,但是它具有我想在我的类中拥有的方法和竞争者,更重要的是,我希望我的派生类对象是一个Thread实例。多重继承可能是不良的设计,也可能是不良的组合。这全视情况而定。
doc 2014年

2
@戴夫:真的吗?Objective-C有编译时评估和模板吗?
Deduplicator

51

C ++中本身没有“接口”的概念。AFAIK,首先在Java中引入接口来解决缺少多重继承的问题。事实证明,此概念非常有用,并且可以通过使用抽象基类在C ++中实现相同的效果。

抽象基类是其中至少一个成员函数(在Java语言中为方法)是使用以下语法声明的纯虚函数的类:

class A
{
  virtual void foo() = 0;
};

抽象的基类不能被实例化,即您不能声明类A的对象。您只能从A派生类,但是任何不提供实现的派生类foo()也将是抽象的。为了避免变得抽象,派生类必须为其继承的所有纯虚函数提供实现。

请注意,抽象基类可以不仅仅是接口,因为它可以包含非纯虚拟的数据成员和成员函数。等效的接口将是抽象基类,而没有任何只有纯虚函数的数据。

而且,正如Mark Ransom所指出的,就这一点而言,抽象基类应该像任何基类一样提供虚拟析构函数。


13
我要说的不仅仅是“缺乏多重继承”,而是要取代多重继承。Java从一开始就是这样设计的,因为多重继承会产生比其解决的问题更多的问题。好答案
OscarRyz

11
奥斯卡,这取决于您是否是学习过Java的C ++程序员,反之亦然。:)恕我直言,如果像C ++中的几乎所有内容一样谨慎使用,多重继承可以解决问题。“接口”抽象基类是非常明智地使用多重继承的一个示例。
Dima

8
@OscarRyz错误。MI仅在被滥用时才会产生问题。MI的大多数被指控的问题还将提出其他设计方案(无MI)。当人们对MI的设计有疑问时,这就是MI的错。如果他们有SI的设计问题,那是他们自己的错。“死亡钻石”(重复继承)就是一个很好的例子。MI扑打不是纯粹的伪善,而是紧密的。
curiousguy 2012年

4
从语义上讲,接口与抽象类不同,因此Java的接口不仅仅是一种技术解决方法。在定义接口还是抽象类之间的选择是由语义而不是技术考虑决定的。让我们想象一下某个接口“ HasEngine”:这是一个方面,一个功能,可以将其应用于/通过非常不同的类型(无论是类还是抽象类)来实现,因此我们将为此定义一个接口,而不是抽象类。
Marek Stanley

2
@MarekStanley,您也许是对的,但我希望您选择了一个更好的例子。我喜欢从继承接口与继承实现的角度来考虑。在C ++中,您可以一起继承接口和实现(公共继承),也可以仅继承实现(私有继承)。在Java中,您可以选择仅继承接口,而无需实现。
迪马

43

据我测试,添加虚拟析构函数非常重要。我正在使用用创建new并销毁的对象delete

如果没有在接口中添加虚拟析构函数,则不会调用继承类的析构函数。

class IBase {
public:
    virtual ~IBase() {}; // destructor, use it to call destructor of the inherit classes
    virtual void Describe() = 0; // pure virtual method
};

class Tester : public IBase {
public:
    Tester(std::string name);
    virtual ~Tester();
    virtual void Describe();
private:
    std::string privatename;
};

Tester::Tester(std::string name) {
    std::cout << "Tester constructor" << std::endl;
    this->privatename = name;
}

Tester::~Tester() {
    std::cout << "Tester destructor" << std::endl;
}

void Tester::Describe() {
    std::cout << "I'm Tester [" << this->privatename << "]" << std::endl;
}


void descriptor(IBase * obj) {
    obj->Describe();
}

int main(int argc, char** argv) {

    std::cout << std::endl << "Tester Testing..." << std::endl;
    Tester * obj1 = new Tester("Declared with Tester");
    descriptor(obj1);
    delete obj1;

    std::cout << std::endl << "IBase Testing..." << std::endl;
    IBase * obj2 = new Tester("Declared with IBase");
    descriptor(obj2);
    delete obj2;

    // this is a bad usage of the object since it is created with "new" but there are no "delete"
    std::cout << std::endl << "Tester not defined..." << std::endl;
    descriptor(new Tester("Not defined"));


    return 0;
}

如果在不带的情况下运行前面的代码virtual ~IBase() {};,您将看到析构函数Tester::~Tester()永远不会被调用。


3
本页上的最佳答案,因为它提供了一个实际的,可编译的示例来阐明了这一点。干杯!
Lumi 2012年

1
Testet ::〜Tester()仅在obj被“用Tester声明”时运行。
Alessandro L.

实际上,将调用字符串privatename的析构函数,并在内存中分配所有内容。就运行时而言,当一个类的所有具体成员都被销毁时,该类实例也会被销毁。我对具有两个Point结构的Line类进行了类似的实验,发现在删除调用或从包含函数返回时,两个结构都被破坏了(Ha!)。valgrind确认0泄漏。
克里斯·里德

33

我的答案与其他答案基本相同,但我认为还有两件事要做:

  1. 在界面中声明一个虚拟析构函数,或制作一个受保护的非虚拟析构函数,以避免在有人尝试删除类型为的对象时发生未定义的行为IDemo

  2. 使用虚拟继承可以避免多重继承带来的问题。(当使用接口时,通常会有多重继承。)

和其他答案一样:

  • 使用纯虚方法创建类。
  • 通过创建另一个覆盖这些虚拟方法的类来使用该接口。

    class IDemo
    {
        public:
            virtual void OverrideMe() = 0;
            virtual ~IDemo() {}
    }

    要么

    class IDemo
    {
        public:
            virtual void OverrideMe() = 0;
        protected:
            ~IDemo() {}
    }

    class Child : virtual public IDemo
    {
        public:
            virtual void OverrideMe()
            {
                //do stuff
            }
    }

2
由于接口中没有任何数据成员,因此不需要虚拟继承。
Robocide

3
虚拟继承对方法也很重要。没有它,即使OverrideMe()的“实例”之一是纯虚拟的(您自己尝试过),您也将陷入歧义。
Knarf Navillus'8年

5
@Avishay_“ 无需虚拟继承,因为您在接口中没有任何数据成员。 ”错误。
curiousguy

请注意,虚拟继承可能不适用于某些gcc版本,例如WinAVR 2010随附的版本4.3.3:gcc.gnu.org/bugzilla/show_bug.cgi?
id=35067

-1具有非虚拟受保护的析构函数,对不起
Wolf

10

在C ++ 11中,您可以轻松地完全避免继承:

struct Interface {
  explicit Interface(SomeType& other)
  : foo([=](){ return other.my_foo(); }), 
    bar([=](){ return other.my_bar(); }), /*...*/ {}
  explicit Interface(SomeOtherType& other)
  : foo([=](){ return other.some_foo(); }), 
    bar([=](){ return other.some_bar(); }), /*...*/ {}
  // you can add more types here...

  // or use a generic constructor:
  template<class T>
  explicit Interface(T& other)
  : foo([=](){ return other.foo(); }), 
    bar([=](){ return other.bar(); }), /*...*/ {}

  const std::function<void(std::string)> foo;
  const std::function<void(std::string)> bar;
  // ...
};

在这种情况下,接口具有引用语义,即您必须确保对象的寿命超过接口(也可以使接口具有值语义)。

这些类型的接口各有利弊:

  • 它们比基于继承的多态性需要更多的内存
  • 通常,它们比基于继承的多态性要快
  • 在您知道最终类型的情况下,它们要快得多!(某些编译器(例如gcc和clang)对不具有/继承自具有虚函数的类型的类型执行更多优化)。

最后,继承是复杂软件设计中万恶之源。在肖恩·普伦特的《价值语义学和基于概念的多态性》(强烈建议在此处解释该技术的更好版本)中,研究了以下情况:

假设我有一个应用程序,在该应用程序中我可以使用该MyShape接口多态处理形状:

struct MyShape { virtual void my_draw() = 0; };
struct Circle : MyShape { void my_draw() { /* ... */ } };
// more shapes: e.g. triangle

在您的应用程序中,使用YourShape接口对不同的形状执行相同的操作:

struct YourShape { virtual void your_draw() = 0; };
struct Square : YourShape { void your_draw() { /* ... */ } };
/// some more shapes here...

现在说您要使用我在您的应用程序中开发的某些形状。从概念上讲,我们的形状具有相同的界面,但是要使我的形状在您的应用程序中正常工作,您需要按以下方式扩展形状:

struct Circle : MyShape, YourShape { 
  void my_draw() { /*stays the same*/ };
  void your_draw() { my_draw(); }
};

首先,完全不可能修改形状。此外,多重继承引领了通向意大利面条代码的道路(想象中第三个项目正在使用TheirShape接口...如果他们还调用其draw函数会my_draw怎样?)。

更新:关于基于非继承的多态性,有一些新的参考文献:


5
TBH的继承比C ++ 11的继承要清楚得多,后者伪装成一个接口,但它是绑定某些不一致的设计的粘合剂。Shapes示例脱离了现实,并且Circle类设计不佳。Adapter在这种情况下,您应该使用模式。抱歉,如果听起来有些刺耳,请尝试使用一些现实生活的库,例如Qt在对继承进行判断之前。继承使生活更加轻松。
doc 2014年

2
听起来一点也不刺耳。形状示例如何脱离现实?您能否举一个使用该Adapter模式固定Circle的示例(也许在ideone上)?我有兴趣看到它的优势。
gnzlbg 2014年

好吧,我会尝试放入这个小盒子里。首先,通常在开始编写自己的应用程序之前,先选择“ MyShape”之类的库,以保护您的工作。否则,您怎么知道Square还不在那里?预知?这就是为什么它脱离现实。实际上,如果您选择依赖“ MyShape”库,则可以从一开始就采用其接口。在shapes示例中,有很多废话(其中之一是您有两个Circle结构),但是适配器看起来像这样-> ideone.com/UogjWk
doc

2
那时它并不脱离现实。当公司A购买公司B并想将公司B的代码库集成到公司A时,您就有两个完全独立的代码库。想象每个都有不同类型的Shape层次结构。您不能轻松地将它们与继承结合在一起,并添加公司C,您将陷入一片混乱。我认为您应该观看此演讲:youtube.com/watch?v=0I0FD3N5cgM我的回答较旧,但您会发现相似之处。您不必一直重新实现所有功能,可以在界面中提供实现,并选择一个成员函数(如果有)。
gnzlbg 2014年

1
我看了一部分视频,这是完全错误的。除了调试目的,我从不使用dynamic_cast。动态转换意味着您的设计有问题,并且此视频中的设计是设计有误的:)。Guy甚至提到了Qt,但是即使在这里他也错了-QLayout不会从QWidget继承,也不会从QWidget继承!
doc 2014年

9

以上所有好的答案。您应该记住的另一件事-您也可以拥有一个纯虚拟析构函数。唯一的区别是您仍然需要实现它。

困惑?


    --- header file ----
    class foo {
    public:
      foo() {;}
      virtual ~foo() = 0;

      virtual bool overrideMe() {return false;}
    };

    ---- source ----
    foo::~foo()
    {
    }

您想要这样做的主要原因是,如果您想像我一样提供接口方法,但是将其覆盖设置为可选的。

要使该类成为接口类,需要一个纯虚拟方法,但是您所有的虚拟方法都具有默认实现,因此使纯虚拟成为唯一的方法是析构函数。

在派生类中重新实现析构函数一点都不重要-我总是在派生类中重新实现一个析构函数,无论是否虚拟。


4
为什么,哦,为什么有人要在这种情况下将dtor设为纯虚拟?那会有什么好处?您只需将某些东西强加到派生类上,它们可能不需要包含-dtor。
约翰·杰雷尔

6
更新了我的答案以回答您的问题。纯虚拟析构函数是一种实现接口类(所有方法均具有默认实现)的有效方法(唯一的方法?)。
罗迪兰

7

如果您使用的是Microsoft的C ++编译器,则可以执行以下操作:

struct __declspec(novtable) IFoo
{
    virtual void Bar() = 0;
};

class Child : public IFoo
{
public:
    virtual void Bar() override { /* Do Something */ }
}

我喜欢这种方法,因为它可以使接口代码更小,并且生成的代码大小可以大大缩小。使用novtable会删除该类中对vtable指针的所有引用,因此您永远无法直接实例化它。请参阅此处的文档-novtable


4
我不太明白为什么你使用novtable过的标准virtual void Bar() = 0;
柔印

2
这是补充(我刚刚注意到= 0;我添加的缺失)。如果您不理解,请阅读文档。
Mark Ingram

我读了没有的文章,= 0;并认为这只是一种完全相同的非标准方式。
Flexo

4

除了这里写的内容外,还有一点补充:

首先,请确保您的析构函数也是纯虚拟的

其次,您可能想在实施时虚拟地(而不是正常地)继承,只是为了获得良好的效果。


我喜欢虚拟继承,因为从概念上讲它意味着继承的类只有一个实例。诚然,这里的类没有任何空间要求,因此可能是多余的。我已经有一段时间没有在C ++中完成MI了,但是非虚拟继承不会使转换变得复杂吗?
Uri

为什么,哦,为什么有人要在这种情况下将dtor设为纯虚拟?那会有什么好处?您只需将某些东西强加到派生类上,它们可能不需要包含-dtor。
约翰·杰雷尔

2
如果存在通过指向接口的指针破坏对象的情况,则应确保析构函数是虚拟的……
Uri

纯虚拟析构函数没有什么错。没必要,但是没有错。在派生类中实现析构函数几乎不会给该类的实现者带来沉重负担。请参阅以下我的答案,了解您为什么要这样做。
Rodyland

+1用于虚拟继承,因为对于接口而言,类更有可能从两个或更多路径派生接口。我在接口tho中选择受保护的析构函数。
doc 2014年

4

您还可以考虑使用NVI(非虚拟接口模式)实现的合同类。例如:

struct Contract1 : boost::noncopyable
{
    virtual ~Contract1();
    void f(Parameters p) {
        assert(checkFPreconditions(p)&&"Contract1::f, pre-condition failure");
        // + class invariants.
        do_f(p);
        // Check post-conditions + class invariants.
    }
private:
    virtual void do_f(Parameters p) = 0;
};
...
class Concrete : public Contract1, public Contract2
{
private:
    virtual void do_f(Parameters p); // From contract 1.
    virtual void do_g(Parameters p); // From contract 2.
};

对于其他读者来说,吉姆·海斯洛普(Jim Hyslop)和赫伯·萨特(Herb Sutter)撰写的多布斯博士的这篇文章 “对话:实际上是您的”,进一步说明了为什么要使用NVI。
user2067021

还有Herb Sutter的这篇文章 “虚拟性”。
user2067021

1

我还是C ++开发的新手。我从Visual Studio(VS)开始。

但是,似乎没有人提到__interfaceVS (.NET)。我不是很肯定,如果这是一个很好的方法来声明一个接口。但这似乎提供了额外的强制执行(在文档中提到)。这样您不必显式指定virtual TYPE Method() = 0;,因为它将被自动转换。

__interface IMyInterface {
   HRESULT CommitX();
   HRESULT get_X(BSTR* pbstrName);
};

但是,我不使用它,因为我担心跨平台编译的兼容性,因为它仅在.NET下可用。

如果有人对它感兴趣,请分享。:-)

谢谢。


0

虽然确实virtual是定义接口的事实上的标准,但不要忘了经典的类似于C的模式,该模式随C ++的构造函数一起提供:

struct IButton
{
    void (*click)(); // might be std::function(void()) if you prefer

    IButton( void (*click_)() )
    : click(click_)
    {
    }
};

// call as:
// (button.*click)();

这样的好处是您可以重新绑定事件运行时,而不必再次构造您的类(因为C ++没有更改多态类型的语法,这是变色龙类的解决方法)。

提示:

  • 您可以从中继承基类(允许虚拟和非虚拟),然后填写click后代的构造函数。
  • 您可能将函数指针作为protected成员并具有public引用和/或getter。
  • 如上所述,这允许您在运行时切换实现。因此,这也是一种管理状态的方法。根据if代码中s与状态更改的次数,这可能switch()es或ifs 快(预计周转时间约为3-4 ifs,但始终要先进行测量。
  • 如果你选择std::function<>了函数指针,你也许能中管理所有对象数据IBase。从这一点上,您可以为之准备有价值的原理图IBase(例如,std::vector<IBase>将起作用)。注意,这可能会慢一些,具体取决于您的编译器和STL代码。同样,std::function<>与函数指针甚至虚拟函数相比,的当前实现往往会产生开销(将来可能会改变)。

0

这是 abstract class c ++标准中

n4687

13.4.2

抽象类是只能用作其他类的基类的类。除了作为从其派生的类的子对象之外,不能创建抽象类的对象。如果一个类至少具有一个纯虚函数,则它是抽象的。


-2
class Shape 
{
public:
   // pure virtual function providing interface framework.
   virtual int getArea() = 0;
   void setWidth(int w)
   {
      width = w;
   }
   void setHeight(int h)
   {
      height = h;
   }
protected:
    int width;
    int height;
};

class Rectangle: public Shape
{
public:
    int getArea()
    { 
        return (width * height); 
    }
};
class Triangle: public Shape
{
public:
    int getArea()
    { 
        return (width * height)/2; 
    }
};

int main(void)
{
     Rectangle Rect;
     Triangle  Tri;

     Rect.setWidth(5);
     Rect.setHeight(7);

     cout << "Rectangle area: " << Rect.getArea() << endl;

     Tri.setWidth(5);
     Tri.setHeight(7);

     cout << "Triangle area: " << Tri.getArea() << endl; 

     return 0;
}

结果:矩形区域:35三角形区域:17

我们已经看到抽象类如何根据getArea()定义接口,另外两个类实现了相同的功能,但是使用了不同的算法来计算特定于形状的面积。


5
这不是接口!那只是一个抽象基类,其中一个方法需要重写!接口通常是仅包含方法定义的对象-其他类在实现接口时必须履行的“合同”。
guitarflow 2014年
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.