你如何在C++中声明一个接口?

Aar*_*her 781 c++ inheritance abstract-class interface pure-virtual

如何设置代表接口的类?这只是一个抽象的基类吗?

Mar*_*som 671

要扩展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
        }
};
Run Code Online (Sandbox Code Playgroud)

您不必为虚拟析构函数包含一个主体 - 事实证明,某些编译器在优化空析构函数时遇到问题,您最好使用默认析构函数.

  • 虚拟desctuctor ++!这是非常重要的.您可能还希望包含operator =的纯虚拟声明和复制构造函数定义,以防止编译器自动为您生成这些声明. (103认同)
  • 顶级答案没有直接回答问题(虽然显然代码是完美的),C++的典型回答是多么典型,而是优化了简单的答案. (48认同)
  • 虚拟析构函数的替代方法是受保护的析构函数.这会禁用多态破坏,这在某些情况下可能更合适.在http://www.gotw.ca/publications/mill18.htm中查找"准则#4". (31认同)
  • 不要忘记,在C++ 11中,您可以指定`override`关键字以允许编译时参数和返回值类型检查.例如,在Child的虚拟void OverrideMe()覆盖;`的声明中 (17认同)
  • 另一个选择是使用正文定义纯虚拟(`= 0`)析构函数.这里的优点是,从理论上讲,编译器可以看到vtable现在没有有效成员,并完全丢弃它.使用带有主体的虚拟析构函数,可以通过`this`指针(当构造对象仍然是`Parent`类型时)(例如在构造中间)调用(虚拟)所述析构函数,因此编译器必须提供有效的vtable .因此,如果您在构造期间没有通过`this`显式调用虚拟析构函数,则可以节省代码大小. (9认同)
  • 如果您知道不打算通过基类删除该类,则不需要虚拟析构函数.然而,无论如何使析构函数变为虚拟永远都不是真的有害(除了一个vtable查找,哦不!). (4认同)
  • 四十个月前,但无论如何 - @Mark Ransom,你写道:"这允许你将指针所有权传递给另一方而不暴露基类." 你的意思是"子类"吗?因为在你的评论中你写道:"如果你知道你不会通过基类删除类,那么你就不需要虚拟析构函数." - 对不起,如果我的评论没有意义 - 我是一个C++菜鸟,并尝试从这些问题和答案中学习. (3认同)
  • @Griddo不,你理解得很对.Mark Ransom建议不要定义虚拟析构函数,而GCC要求你这样做.除了使用#ifdef和编译器检查混乱你的代码之外,我真的不认为有一个通用的解决方案,但在这种情况下,我宁愿去定义它,也许在某些编译器上会失去一些优化优势. (3认同)
  • @Lumi,现在我想想你是绝对正确的。接口是基类,因为具体类从它派生并实现方法。我应该解决这个问题。给自己一颗金星,因为它是第一个注意到的,我相信这个答案已经被浏览了一千次。 (2认同)
  • @MarkRansom,感谢虚拟金星。:)同时,我发现你的意思一定是我猜测的意思,因为我到达了页面底部[卡洛斯的回答](http://stackoverflow.com/a/9571456/269126#9571456)通过代码示例可以清楚地了解这一点。感谢您时隔这么久的反馈!——最好,迈克尔 (2认同)
  • @ZacharyKraus:MarkRansom说的话.如果以这种方式声明接口,则编译器自动生成这些接口的尝试极不可能是正确的.因此,自己明确地声明它们是虚拟的更安全,并确保您不会意外地陷入使用它们的陷阱. (2认同)
  • @Sparker0i `virtual` 不是必需的,因为基类声明已经使它成为虚拟的,但保持一致是一种很好的做法。`void` 仍然是必要的。 (2认同)

bra*_*ray 236

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

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

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

class Child : public IDemo
{
    public:
        virtual void OverrideMe()
        {
            //do stuff
        }
};
Run Code Online (Sandbox Code Playgroud)

  • 你应该在IDemo中不做任何析构函数,以便定义行为:IDemo*p = new Child;/*无论*/删除p; (29认同)
  • @Kevin除了在C++ 11中使用`override` (26认同)
  • 为什么Child类中的OverrideMe方法是虚拟的?这有必要吗? (11认同)
  • 通常在覆盖虚拟方法时保持关键字"虚拟"通常是个好主意.虽然不是必需的,但它可以使代码更清晰 - 否则,您没有迹象表明该方法可以多态使用,甚至存在于基类中. (11认同)
  • @Cemre - 没有必要,但它也没有伤害. (9认同)
  • 没有必要将`Child::OverrideMe()`声明为`virtual`,@Cemre,因为`IDemo::OverrideMe()`使`Child::OverrideMe()`隐式为`virtual`。表明它是一个虚函数(例如“virtual”,或者更好的是 C++11 或更高版本中的“override”)将为程序员提供提醒,特别是“override”还可以帮助编译器检查它确实覆盖了某些东西。 (6认同)
  • 不要忘记类声明结束时的分号. (4认同)

Joe*_*orn 145

除了C#/ Java中的抽象基类之外,你有一个特殊的接口类型类的全部原因是因为C#/ Java不支持多重继承.

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

  • 能够创建接口,以避免我们输入这么多(虚拟,= 0,虚拟析构函数)仍然是很好的.对我来说,多重继承对我来说似乎是一个非常糟糕的主意,我从未在实践中看到过它,但是界面一直是需要的.糟糕的是,C++ comity不会因为我想要它们而引入接口. (17认同)
  • Ha11owed:它有接口.它们被称为具有纯虚方法的类,没有方法实现. (9认同)
  • @doc:java.lang.Thread包含您可能不希望在对象中拥有的方法和常量.如果从Thread扩展,编译器应该做什么,而另一个类使用public方法checkAccess()?你真的更喜欢在C++中使用强名称的基本指针吗?这看起来像糟糕的设计,你通常需要组合,你认为你需要多重继承. (6认同)
  • @ Ha11owed它是很久以前所以我不记得细节,但它有我想要在我的类中的方法和内容,更重要的是我希望我的派生类对象是一个`Thread`实例.多重继承可能是糟糕的设计和组合.这完全取决于案例. (4认同)
  • @戴夫:真的吗?Objective-C有编译时评估和模板吗? (2认同)

Dim*_*ima 50

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

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

class A
{
  virtual void foo() = 0;
};
Run Code Online (Sandbox Code Playgroud)

无法实例化抽象基类,即您不能声明类A的对象.您只能从A派生类,但任何不提供实现的派生类foo()也将是抽象的.为了不再抽象,派生类必须为它继承的所有纯虚函数提供实现.

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

而且,正如Mark Ransom指出的那样,抽象基类应该像任何基类一样提供虚拟析构函数.

  • 我会说,除了"缺乏多重继承"之外,还要取代多重继承.Java从一开始就是这样设计的,因为多重继承会产生比它解决的问题更多的问题.好答案 (13认同)
  • 奥斯卡,这取决于你是否是一个学习Java的C++程序员,反之亦然.:)恕我直言,如果明智地使用,几乎像C++中的任何东西,多重继承解决问题."接口"抽象基类是非常明智地使用多重继承的示例. (11认同)
  • @OscarRyz错了.MI只会在误用时产生问题.大多数涉嫌MI的问题也会提出替代设计(没有MI).**当人们对MI设计有问题时,这就是MI的错误; 如果他们有SI的设计问题,这是他们自己的错.**"死亡之钻"(重复继承)是一个很好的例子.MI抨击不是纯粹的虚伪,而是接近. (8认同)
  • 从语义上讲,接口与抽象类不同,因此Java的接口不仅仅是一种技术解决方案.定义接口或抽象类之间的选择是由语义驱动的,而不是技术考虑因素.让我们设想一些接口"HasEngine":这是一个方面,一个特性,它可以应用于/实现非常不同的类型(无论是类还是抽象类),因此我们将为它定义一个接口,而不是一个抽象类. (4认同)
  • @MarekStanley,你可能是对的,但我希望你能选择一个更好的例子.我喜欢在继承接口和继承实现方面考虑它.在C++中,您可以同时继承接口和实现(公共继承),也可以只继承实现(私有继承).在Java中,您可以选择仅继承接口,而无需实现. (2认同)

小智 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;
}
Run Code Online (Sandbox Code Playgroud)

如果没有运行前面的代码virtual ~IBase() {};,您将看到Tester::~Tester()永远不会调用析构函数.

  • 这个页面上的最佳答案,因为它提供了一个实用的,可编译的示例.干杯! (3认同)

Rex*_*xar 33

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

  1. 在接口中声明虚拟析构函数或创建受保护的非虚拟析构函数,以避免在有人尝试删除类型对象时出现未定义的行为IDemo.

  2. 使用虚拟继承来避免多重继承问题.(当我们使用接口时,通常会有多重继承.)

和其他答案一样:

  • 提及虚拟继承的+1. (5认同)
  • @Avishay_"_因为在接口中没有任何数据成员,所以不需要虚拟继承."错误. (5认同)
  • 虚拟继承对于方法也很重要.没有它,你会遇到OverrideMe()的歧义,即使其中一个'实例'是纯虚拟的(我自己也试过). (3认同)
  • 由于接口中没有任何数据成员,因此不需要虚拟继承. (2认同)

gnz*_*lbg 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;
  // ...
};
Run Code Online (Sandbox Code Playgroud)

在这种情况下,接口具有引用语义,即您必须确保该对象比接口更长(也可以使接口具有值语义).

这些类型的接口有它们的优点和缺点:

  • 它们需要比基于继承的多态性更多的内存.
  • 它们通常比基于继承的多态更快.
  • 在您了解最终类型的情况下,它们会快得多!(像gcc和clang这样的编译器在没有/继承具有虚函数的类型的类型中执行更多优化).

最后,继承是复杂软件设计中所有邪恶的根源.在Sean Parent的Value Semantics和基于概念的多态性中(强烈推荐,此技术的更好版本在那里解释),研究了以下案例:

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

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

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

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

现在说你想使用我在你的应用程序中开发的一些形状.从概念上讲,我们的形状具有相同的界面,但为了使我的形状在您的应用程序中工作,您需要扩展我的形状如下:

struct Circle : MyShape, YourShape { 
  void my_draw() { /*stays the same*/ };
  void your_draw() { my_draw(); }
};
Run Code Online (Sandbox Code Playgroud)

首先,根本不可能修改我的形状.此外,多重继承引领了意大利面条代码的道路(想象一下第三个项目就是使用TheirShape界面......如果它们也调用了它们的绘制函数会发生什么my_draw?).

更新:有一些关于基于非继承的多态性的新引用:

  • TBH继承比C++ 11的东西要清楚得多,C++ 11假装是一个接口,但却是绑定一些不一致设计的粘合剂.Shapes示例与现实分离,而"Circle"类是一个糟糕的设计.在这种情况下你应该使用`Adapter`模式.对不起,如果它听起来有点刺耳,但在判断继承之前,尝试使用像`Qt`这样的现实生活库.继承让生活更轻松. (5认同)
  • 它听起来并不严厉.形状示例如何脱离现实?你能给出一个使用`Adapter`模式修复Circle的例子(也许是想法)吗?我很想看到它的优点. (2认同)
  • 那时它并没有脱离现实.当公司A收购B公司并希望将公司B的代码库集成到A中时,您有两个完全独立的代码库.想象一下,每个都有不同类型的Shape层次结构.你不能轻易地将它们与继承结合起来,并且添加公司C并且你有一个很大的混乱.我想你应该看一下这个话题:https://www.youtube.com/watch?v = 0I0FD3N5cgM我的答案比较老,但你会看到相似之处.您不必一直重新实现所有内容,您可以在界面中提供实现,并选择成员函数(如果可用). (2认同)

Rod*_*and 9

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

困惑?


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

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

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

Run Code Online (Sandbox Code Playgroud)

您想要这样做的主要原因是,如果您想提供接口方法,就像我一样,但是将它们覆盖为可选项.

要使类成为接口类需要纯虚方法,但是所有虚方法都有默认实现,因此唯一的生成纯虚方法的方法是析构函数.

在派生类中重新实现析构函数根本没什么大不了的 - 我总是在派生类中重新实现析构函数,无论是否为虚函数.

  • 更新了我的回答以回答您的问题.纯虚拟析构函数是实现(实现唯一的方法?)接口类的有效方法,其中所有方法都具有默认实现. (6认同)
  • 为什么,为什么,有人想让dtor在这种情况下是纯虚拟的吗?会有什么收获?你只需强制一些他们可能不需要包含的派生类 - 一个dtor. (4认同)

Luc*_*tte 7

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

struct Contract1 : boost::noncopyable
{
    virtual ~Contract1() = default;
    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:
    void do_f(Parameters p) override; // From contract 1.
    void do_g(Parameters p) override; // From contract 2.
};
Run Code Online (Sandbox Code Playgroud)

  • 对于其他读者,Jim Hyslop 和 Herb Sutter 撰写的这篇 [Dobbs 博士文章](http://drdobbs.com/cpp/184403760)“对话:几乎属于您”详细阐述了为什么人们可能想要使用 NVI。 (3认同)
  • 还有[本文](http://www.gotw.ca/publications/mill18.htm) Herb Sutter 的“虚拟性”。 (3认同)

Mar*_*ram 7

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

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

class Child : public IFoo
{
public:
    virtual void Bar() override { /* Do Something */ }
}
Run Code Online (Sandbox Code Playgroud)

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

  • 我不太明白为什么你使用`novtable`而不是标准`virtual void Bar()= 0;` (4认同)
  • 它是另外的(我刚刚注意到缺少的`= 0;`我已经添加了它).如果您不理解,请阅读文档. (2认同)

Uri*_*Uri 5

对上面写的内容做一点补充:

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

其次,您可能希望在实施时虚拟继承(而不是通常),只是为了好的措施。

  • 如果存在通过指向接口的指针销毁对象的情况,则应确保析构函数是虚拟的... (2认同)