【发布时间】:2010-09-24 00:02:14
【问题描述】:
如何设置代表接口的类?这只是一个抽象基类吗?
【问题讨论】:
标签: c++ inheritance interface abstract-class pure-virtual
如何设置代表接口的类?这只是一个抽象基类吗?
【问题讨论】:
标签: c++ inheritance interface abstract-class pure-virtual
要扩展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) 析构函数。这里的好处是,理论上编译器可以看到 vtable 现在没有有效成员,并完全丢弃它。使用带有主体的虚拟析构函数,可以(虚拟)调用所述析构函数,例如在通过this 指针构造的中间(当构造的对象仍然是Parent 类型时),因此编译器必须提供一个有效的vtable。因此,如果您在构造过程中没有通过this 显式调用虚拟析构函数:) 您可以节省代码大小。
override 关键字以允许编译时参数和返回值类型检查。比如声明Childvirtual void OverrideMe() override;
用纯虚方法创建一个类。通过创建另一个覆盖这些虚拟方法的类来使用该接口。
纯虚方法是定义为虚并赋值为0的类方法。
class IDemo
{
public:
virtual ~IDemo() {}
virtual void OverrideMe() = 0;
};
class Child : public IDemo
{
public:
virtual void OverrideMe()
{
// do stuff
}
};
【讨论】:
override 除外
除了 C#/Java 中的抽象基类之外,您还有一个特殊的接口类型类别的全部原因是因为 C#/Java 不支持多重继承。
C++ 支持多重继承,因此不需要特殊类型。没有非抽象(纯虚拟)方法的抽象基类在功能上等同于 C#/Java 接口。
【讨论】:
C++ 中没有“接口”的概念。 AFAIK,接口最初是在 Java 中引入的,以解决缺乏多重继承的问题。事实证明,这个概念非常有用,在 C++ 中使用抽象基类也可以达到同样的效果。
抽象基类是一个类,其中至少一个成员函数(Java 术语中的方法)是使用以下语法声明的纯虚函数:
class A
{
virtual void foo() = 0;
};
抽象基类不能被实例化,即。 e.您不能声明类 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;
// ...
};
在这种情况下,接口具有引用语义,即您必须确保对象的寿命比接口长(也可以使用值语义制作接口)。
这些类型的接口各有优缺点:
最后,继承是复杂软件设计中万恶之源。在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 会发生什么?)。
更新:有几个关于基于非继承的多态性的新参考:
【讨论】:
Circle 类是一个糟糕的设计。在这种情况下,您应该使用 Adapter 模式。对不起,如果这听起来有点刺耳,但在判断继承之前尝试使用一些现实生活中的库,如Qt。继承让生活更轻松。
Adapter 模式修复Circle 的示例(可能在ideone 上)?我很想看看它的优势。
以上所有好的答案。 您应该记住的另一件事 - 您还可以拥有一个纯虚拟析构函数。唯一的区别是您仍然需要实现它。
困惑?
--- 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。
【讨论】:
novtable 而非标准 virtual void Bar() = 0;
= 0;)。如果您不理解,请阅读文档。
= 0; 的情况下阅读了它,并认为这只是一种非标准的完全相同的方式。
您还可以考虑使用 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.
};
【讨论】:
对上面写的内容做一点补充:
首先,确保你的析构函数也是纯虚拟的
其次,您可能希望在实施时虚拟(而不是通常)继承,只是为了采取良好的措施。
【讨论】:
在 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);
限制是你不能在容器中使用它。
【讨论】:
我还是 C++ 开发的新手。我从 Visual Studio (VS) 开始。
然而,似乎没有人在 VS (.NET) 中提到 __interface。我不是很确定这是否是声明接口的好方法。但它似乎提供了额外的强制执行(在the documents 中提到)。这样您就不必显式指定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。ifs 与代码中状态更改的数量,这可能比switch()es 或ifs 快(预计周转时间约为 3-4 ifs ,但总是先测量。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() 定义一个接口,而另外两个类实现了相同的功能,但使用不同的算法来计算特定于形状的面积。
【讨论】: