关于继承:如何声明C++中的接口?

How do you declare an interface in C++?

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


为了扩展bradtgmurray的答案,您可能需要通过添加虚拟析构函数对接口的纯虚拟方法列表进行一个异常。这允许您将指针所有权传递给另一方,而不公开具体的派生类。析构函数不必做任何事情,因为接口没有任何具体的成员。将函数定义为虚函数和内联函数似乎是矛盾的,但请相信我,事实并非如此。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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/J/Java接口。


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

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

1
2
3
4
class A
{
  virtual void foo() = 0;
};

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

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

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


就我所能测试的而言,添加虚拟析构函数是非常重要的。我使用的对象是用new创建的,用delete销毁的。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
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()从未被调用。


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

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

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

  • 和其他答案一样:

    • 用纯虚拟方法生成类。
    • 通过创建另一个重写这些虚拟方法的类来使用接口。

      1
      2
      3
      4
      5
      6
      class IDemo
      {
          public:
              virtual void OverrideMe() = 0;
              virtual ~IDemo() {}
      }

      1
      2
      3
      4
      5
      6
      7
      class IDemo
      {
          public:
              virtual void OverrideMe() = 0;
          protected:
              ~IDemo() {}
      }

      1
      2
      3
      4
      5
      6
      7
      8
      class Child : virtual public IDemo
      {
          public:
              virtual void OverrideMe()
              {
                  //do stuff
              }
      }


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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)对不具有/从具有虚拟函数的类型继承的类型执行更多优化)。

    最后,继承是复杂软件设计中万恶之源。在Sean Parent的价值语义和基于概念的多态性(强烈推荐,这里解释了更好的技术版本)中,研究了以下情况:

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

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

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

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

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

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

    首先,修改我的形状可能根本不可能。此外,多重继承导致了通往意大利面条代码的道路(假设第三个项目是使用TheirShape接口)。如果它们也将其draw函数称为my_draw,会发生什么?.

    更新:关于非继承的多态性有几个新的参考:

    • 肖恩父母的继承权是恶言恶语的基础。
    • Sean Parent的价值语义和基于概念的多态性对话。
    • PyryJahkola的无继承多态性对话和多文库文档。
    • 扎克·莱恩的实用类型擦除:用优雅的设计模式来解决OOP问题。
    • 塔尔科夫斯基的C++博客- Type Erasure部分I、II、III和IV。
    • 运行时多态通用编程conceptpc中的混合对象和概念++
    • boost.typeerasure文档
    • 土坯多边形
    • Boost.Any,Std::Any Proposal(Revision 3),Boost.Spirit::Hold_Any.


    以上都是好答案。你应该记住一件额外的事情——你也可以有一个纯粹的虚拟析构函数。唯一的区别是您仍然需要实现它。

    困惑的?

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
        --- header file ----
        class foo {
        public:
          foo() {;}
          virtual ~foo() = 0;

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

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

    您希望这样做的主要原因是,如果您像我一样希望提供接口方法,但是使覆盖它们成为可选的。

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

    在派生类中重新实现一个析构函数一点也不重要——我总是在派生类中重新实现一个析构函数,不管是虚拟的还是非虚拟的。


    如果你使用微软的C++编译器,那么你可以做到以下几点:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    struct __declspec(novtable) IFoo
    {
        virtual void Bar() = 0;
    };

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

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


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

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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.
    };


    除了上面写的内容:

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

    第二,在实现时,您可能希望实际上(而不是通常)继承,只是为了好的度量。


    在C++开发中,我还是新手。我从Visual Studio(vs)开始。

    然而,似乎没有人提到Vs(.net)中的__interface。我不太确定这是否是声明接口的好方法。但它似乎提供了额外的执行(在文件中提到)。这样您就不必显式地指定virtual TYPE Method() = 0;,因为它将自动转换。

    1
    2
    3
    4
    __interface IMyInterface {
       HRESULT CommitX();
       HRESULT get_X(BSTR* pbstrName);
    };

    However, I don't use it because I am concern about the cross platform compilation compatibility, since it only available under .NET.

    如果有人对此有兴趣,请分享。-)

    谢谢。


    下面是C++标准中EDCOX1第15条的定义

    N468

    3.4.2

    An abstract class is a class that can be used only as a base class of some other class; no objects of an abstract
    class can be created except as subobjects of a class derived from it. A class is abstract if it has at least
    one pure virtual function.


    虽然EDOCX1·2是定义接口的事实标准,但我们不要忘记C++中的构造函数,这是经典的C型模式:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    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与状态变化的数量,这可能比switch()es或ifs(预计周转时间约为3-4 ifs,但始终要先测量。
    • 如果您选择std::function<>而不是函数指针,那么您可能能够管理IBase中的所有对象数据。从这一点上,您可以获得IBase的值示意图(例如,std::vector将起作用)。请注意,根据编译器和STL代码的不同,这可能会慢一些;而且,与函数指针甚至虚拟函数相比,当前的std::function<>实现往往会有开销(这在将来可能会改变)。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    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()定义一个接口的,另外两个类实现了相同的函数,但是使用不同的算法来计算特定于形状的区域。