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

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

568072 次浏览

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

纯虚方法是定义为虚并分配给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接口。

为了通过布拉德格默里扩展答案,你可能想通过添加一个虚拟析构函数来对接口的纯虚方法列表做一个例外。这允许你将指针所有权传递给另一方,而不暴露具体的派生类。析构函数不需要做任何事情,因为接口没有任何具体成员。将函数定义为虚拟和内联可能看起来很矛盾,但相信我——事实并非如此。

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

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

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

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

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

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

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

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

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

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

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

您还可以考虑使用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.};

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

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

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

困惑?

--- 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指针的所有引用,因此您永远无法直接实例化它。请参阅此处的留档-新的

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

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

class IBase {public:virtual ~IBase() {}; // destructor, use it to call destructor of the inherit classesvirtual 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()从未被调用。

在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会发生什么?)。

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

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

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

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

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

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

如果有人对此有任何兴趣,请分享:-)

谢了

虽然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与状态更改的数量,这个可能switch()es或if快(预计大约3-4个if,但总是先测量。
  • 如果您选择std::function<>而不是函数指针,则可能能够管理IBase中的所有对象数据。从这一点开始,您可以拥有IBase的值原理图(例如,std::vector<IBase>将起作用)。请注意,这个可能会更慢,具体取决于您的编译器和STL代码;此外,与函数指针甚至虚函数相比,std::function<>的当前实现往往有开销(这可能会在未来改变)。

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

n4687

13.4.2

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

在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);

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

如果你只想要接口的静态绑定(没有虚拟,没有接口类型本身的实例,接口仅充当指南):

#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 classvoid 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 implementationstd::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();}