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

时间:2008-11-25 16:48:47

标签: c++ inheritance interface abstract-class pure-virtual

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

17 个答案:

答案 0 :(得分:661)

要通过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
        }
};

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

答案 1 :(得分:233)

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

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

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

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

答案 2 :(得分:144)

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

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

答案 3 :(得分:47)

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

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

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

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

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

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

答案 4 :(得分:42)

到目前为止,我可以测试,添加虚拟析构函数非常重要。我正在使用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()

答案 5 :(得分:32)

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

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

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

  3. 和其他答案一样:

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

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

答案 6 :(得分: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;
  // ...
};

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

这些类型的界面各有利弊:

最后,继承是复杂软件设计中所有邪恶的根源。在Sean Parent's Value Semantics and Concepts-based Polymorphism中(强烈推荐,在那里解释了这种技术的更好版本),研究了以下案例:

假设我有一个应用程序,我使用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会发生什么?)。

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

答案 7 :(得分:9)

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

困惑?


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

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

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

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

要使类成为一个接口类需要一个纯虚方法,但所有虚方法都有默认实现,所以留下来制作纯虚方法的唯一方法就是析构函数。

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

答案 8 :(得分: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

答案 9 :(得分: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.
};

答案 10 :(得分:4)

在那里写的东西的一点点补充:

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

其次,您可能希望在实施时虚拟(而非正常)继承,只是为了获得良好的衡量标准。

答案 11 :(得分:1)

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

然而,似乎没有人提到VS (。NET)中的__interface。我非常确定这是否是声明接口的好方法。但它似乎提供了额外的强制执行(在the documents中提到)。这样您就不必明确指定virtual TYPE Method() = 0;,因为它会自动转换。

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

但是,我没有使用它,因为我担心跨平台编译兼容性,因为它只能在.NET下使用。

如果有人对此有任何疑问,请分享。 : - )

感谢。

答案 12 :(得分:1)

在 C++20 中,您可以使用 concept 代替类。 它比继承更有效。

template <class T>
concept MyInterface = requires (T t) {
    { t.interfaceMethod() };
};

class Implementation {
public:
    void interfaceMethod();
};
static_assert(MyInterface<Implementation>);

然后就可以在函数中使用了:

void myFunction(MyInterface auto& arg);

限制是你不能在容器中使用它。

答案 13 :(得分: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或if更快(预计周转时间约为3) -4 if s,但始终先测量。
  • 如果您选择std::function<>超过函数指针,可能能够管理IBase中的所有对象数据。从这一点开始,您可以获得IBase的价值示意图(例如,std::vector<IBase>将起作用)。请注意,根据您的编译器和STL代码,此可能会更慢;另外,当与函数指针甚至虚函数相比时,std::function<>的当前实现往往会产生开销(这可能会在未来发生变化)。

答案 14 :(得分:0)

以下是c ++标准中abstract class的定义

n4687

<强> 13.4.2

  

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

答案 15 :(得分:0)

如果你只想要一个接口的静态绑定(没有虚拟的,没有接口类型本身的实例,接口只是一个指导):

#include <iostream>
#include <string>

// Static binding interface
// Notice: instantiation of this interface should be usefuless and forbidden.
class IBase {
 protected:
  IBase() = default;
  ~IBase() = default;

 public:
  // Methods that must be implemented by the derived class
  void behaviorA();
  void behaviorB();

  void behaviorC() {
    std::cout << "This is an interface default implementation of bC().\n";
  };
};

class CCom : public IBase {
  std::string name_;

 public:
  void behaviorA() { std::cout << "CCom bA called.\n"; };
};

class CDept : public IBase {
  int ele_;

 public:
  void behaviorB() { std::cout << "CDept bB called.\n"; };
  void behaviorC() {
    // Overwrite the interface default implementation
    std::cout << "CDept bC called.\n";
    IBase::behaviorC();
  };
};

int main(void) {
  // Forbid the instantiation of the interface type itself.
  // GCC error: ‘constexpr IBase::IBase()’ is protected within this context
  // IBase o;

  CCom acom;
  // If you want to use these interface methods, you need to implement them in
  // your derived class. This is controled by the interface definition.
  acom.behaviorA();
  // ld: undefined reference to `IBase::behaviorB()'
  // acom.behaviorB();
  acom.behaviorC();

  CDept adept;
  // adept.behaviorA();
  adept.behaviorB();
  adept.behaviorC();
  // adept.IBase::behaviorC();
}

答案 16 :(得分:-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()定义接口,另外两个类实现相同的函数,但使用不同的算法来计算特定于形状的区域。