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

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

答案

为了扩展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
        }
};

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

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

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

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

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

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

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

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

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

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

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

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

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

据我测试,添加虚拟析构函数非常重要。我正在使用用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()从未被调用。

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

  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
            }
    }

在 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接口…… 如果他们还调用其绘制函数my_draw会发生什么?)。

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

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

困惑?

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

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

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

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

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

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

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

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

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

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

除了这里写的内容:

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

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