如何设置代表接口的类?这只是一个抽象基类吗?
17 回答
要扩展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()
永远不会被调用。
我的回答与其他人基本相同,但我认为还有两件重要的事情要做:
在您的接口中声明一个虚拟析构函数或创建一个受保护的非虚拟析构函数以避免在有人试图删除类型对象时出现未定义的行为
IDemo
。使用虚拟继承来避免多重继承的问题。(当我们使用接口时,更常见的是多重继承。)
和其他答案一样:
- 使用纯虚方法创建一个类。
通过创建另一个覆盖这些虚拟方法的类来使用该接口。
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 对不具有/继承自具有虚函数的类型的类型执行更多优化)。
最后,继承是复杂软件设计中万恶之源。在Sean Parent 的 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
?)。
更新:有一些关于基于非继承的多态性的新参考:
- Sean Parent's Inheritance 是恶语的基类。
- Sean Parent 的价值语义和基于概念的多态性演讲。
- Pyry Jahkola 的无继承多态性演讲和poly 库文档。
- Zach Laine 的实用类型擦除:用优雅的设计模式演讲解决 OOP 问题。
- Andrzej 的 C++ 博客 - 类型擦除部分i、ii、iii和iv。
- 运行时多态泛型编程——在 ConceptC++ 中混合对象和概念
- Boost.TypeErasure 文档
- Adobe Poly 文档
- Boost.Any,std::any 提案(修订版 3),Boost.Spirit::hold_any。
上面所有的好答案。您应该记住的另一件事 - 您还可以拥有一个纯虚拟析构函数。唯一的区别是您仍然需要实现它。
使困惑?
--- header file ----
class foo {
public:
foo() {;}
virtual ~foo() = 0;
virtual bool overrideMe() {return false;}
};
---- source ----
foo::~foo()
{
}
你想要这样做的主要原因是如果你想提供接口方法,就像我一样,但要覆盖它们是可选的。
要使类成为接口类,需要纯虚方法,但所有虚方法都有默认实现,因此唯一可以制作纯虚的方法是析构函数。
在派生类中重新实现析构函数没什么大不了的——我总是在派生类中重新实现析构函数,无论是否虚拟。
您还可以考虑使用 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.
};
如果您使用的是 Microsoft 的 C++ 编译器,则可以执行以下操作:
struct __declspec(novtable) IFoo
{
virtual void Bar() = 0;
};
class Child : public IFoo
{
public:
virtual void Bar() override { /* Do Something */ }
}
我喜欢这种方法,因为它会产生更小的接口代码,并且生成的代码大小可以显着更小。novtable 的使用删除了对该类中 vtable 指针的所有引用,因此您永远不能直接实例化它。请参阅此处的文档 - novtable。
对上面写的内容做一点补充:
首先,确保你的析构函数也是纯虚拟的
其次,您可能希望在实施时虚拟(而不是通常)继承,只是为了采取良好的措施。
在 C++20 中,您可以使用 aconcept
代替类。它比继承更有效。
template <class T>
concept MyInterface = requires (T t) {
{ t.interfaceMethod() };
};
class Implementation {
public:
void interfaceMethod();
};
static_assert(MyInterface<Implementation>);
然后你可以在一个函数中使用它:
void myFunction(MyInterface auto& arg);
限制是您不能在容器中使用它。
我还是 C++ 开发的新手。我从 Visual Studio (VS) 开始。
然而,似乎没有人提到__interface
VS (.NET)。我不太确定这是否是声明接口的好方法。但它似乎提供了额外的强制执行(在文件中提到)。这样您就不必明确指定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++ 没有用于更改多态类型的语法,这是变色龙类的解决方法)。
尖端:
- 您可以从 this 作为基类继承(允许虚拟和非虚拟)并填写
click
后代的构造函数。 - 您可能将函数指针作为
protected
成员并具有public
引用和/或 getter。 - 如上所述,这允许您在运行时切换实现。因此,它也是一种管理状态的方法。根据
if
代码中 s 与状态更改的数量,这可能比switch()
es 或if
s 更快(预计周转时间约为 3-4if
秒,但始终先测量。 - 如果您选择
std::function<>
函数指针,您可能能够在IBase
. 从这一点开始,您可以获得价值示意图IBase
(例如,std::vector<IBase>
将起作用)。请注意,这可能会更慢,具体取决于您的编译器和 STL 代码;std::function<>
此外,与函数指针甚至虚函数相比,当前的实现往往会产生开销(这可能会在未来发生变化)。
这是abstract class
c ++标准中的定义
n4687
13.4.2
抽象类是只能用作其他类的基类的类;抽象类的任何对象都不能被创建,除非是从它派生的类的子对象。如果一个类至少有一个纯虚函数,那么它就是抽象的。
如果您只想要接口的静态绑定(没有虚拟,没有接口类型本身的实例,接口仅作为指导):
#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();
}
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() 定义一个接口,而另外两个类实现了相同的功能,但使用不同的算法来计算特定于形状的区域。