如何设置表示接口的类?这只是一个抽象的基类吗?
为了扩展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
}
}; |
您不必为虚拟析构函数包含主体-事实证明,有些编译器在优化空析构函数时遇到问题,最好不要使用默认值。
- 虚拟描述器++!这很重要。您还可能希望包含运算符=的纯虚拟声明,并复制构造函数定义,以防止编译器为您自动生成这些声明。
- 虚拟析构函数的替代方法是受保护的析构函数。这将禁用多态性破坏,在某些情况下可能更合适。在gotw.ca/publications/mill18.htm中查找"guideline 4"。
- 如果知道不打算通过基类删除类,则不需要虚拟析构函数。但是,无论如何,将析构函数设为虚拟的并不真正有害(除了一个vtable查找,哦,不!).
- 另一种选择是定义一个带有主体的纯虚拟(=0析构函数。这里的优点是,理论上,编译器可以看到vtable现在没有有效的成员,并将其全部丢弃。使用带有主体的虚拟析构函数,可以(实际上)调用所述析构函数,例如在构造过程中通过this指针调用(当构造的对象仍为Parent类型时),因此编译器必须提供有效的vtable。因此,如果在构造过程中没有通过this显式调用虚拟析构函数,可以节省代码大小。
- 四十个月前,但不管怎样,@mark ransom,你写道:"这允许你将指针所有权传递给另一方,而不暴露出基类。"你是说"子类"?因为在你的评论中你写道:"如果你知道你不会通过一个基类删除这个类,你就不需要虚拟析构函数。"-如果我的话没有意义的话,抱歉。我是C++ NoOB,试图从这些问题和答案中学习。
- @鲁米,现在我想起来了,你是绝对正确的。接口是一个基类,因为具体的类从它派生并实现方法。我应该解决这个问题。如果你是第一个注意到这一点的人,那就给自己一颗金星吧,我相信这个答案已经被看过一千遍了。
- @Markransom,感谢虚拟金星。:)与此同时,我发现你一定是指我猜想的你的意思,因为我到了页面的底部,卡洛斯的答案用代码示例说明了这一点。多谢你这么久的反馈!--最好的,迈克尔
- @Pavelminaev"用一个带有身体的虚拟析构函数,所说的析构函数可以(实际上)称为(例如在施工过程中)"严重吗?
- @好奇心,是的,当您从派生类的构造函数throw时,可以在构造过程中调用析构函数。
- 但实际上呢?!!!
- 考虑继承层次:Base<-Derived<-MostDerived。您在派生ctor中。调用一个接受Base*参数的全局函数,并将其传递给this。该函数可以通过指针显式调用析构函数,并且必须进行虚拟调度。
- @Pavelminaev,在完成一个对象的构建之前明确地销毁它不是ub吗?我可以看到MostDerived的建设者非常不高兴。至少在通过throw摧毁的情况下,建筑的其余部分被绕开。
- 当它到达EDOCX1的ctor(7)时,它肯定是ub,但我看不出任何东西会单独在ctor ub中生成,例如this->~Foo()。考虑销毁它的情况,然后永远不要返回到ctor(例如,只需在无限循环中坐在那里,获取和处理输入)。在这个循环中,据我所见,您的行为将是定义良好的。
- C++的典型答案是,顶层答案不能直接回答问题(尽管代码很完美),而是优化了简单的答案。
- 不要忘记,在C++ 11中,可以指定EDCOX1×14 }关键字,以允许编译时参数和返回值类型检查。例如,在《儿童宣言》中,virtual void OverrideMe() override;。
- 肖恩,这个答案早在C++ 11前几年,但是你做的很好,谢谢!
- 我同意@pavelminaev的观点,几乎没有理由让析构函数是非纯虚拟的。
- 还要注意,gcc不能处理纯虚拟析构函数,您需要定义它们:stackoverflow.com/a/3065223/447490
- @克里斯,关于Mark Ransoms关于编译器优化的注释和您的评论,我现在有点困惑。听上去,前者建议使用纯虚拟析构函数,而后者则禁止使用。我误解了什么吗?如果没有,是否有一个通用的解决方案,或者它依赖于编译器?
- @不,你理解得很对。mark ransom建议不要定义虚拟析构函数,而gcc要求您这样做。我并不认为有一个通用的解决方案可以解决这个问题,而不是用ifdef和编译器检查来扰乱代码,但是在这种情况下,我宁愿继续定义它,也许会失去一些编译器的优化优势。
- @xan您能解释一下为什么要在IDEMO类中使赋值操作符和复制构造函数是纯虚拟的吗?
- @Zacharykraus接口应该是实际实现接口的类的一个非常小的子集;特别是它不会有任何数据成员。这几乎可以保证编译器生成的复制和分配操作符会做错误的事情。如果你想自己实现它们,那么就把自己打倒,但通常不需要——你通过指针访问一个接口,只要你处理了所有权,指针的副本就可以了。
- @扎卡里克劳斯:马克兰森说的。如果您以这种方式声明一个接口,那么编译器在这些方面的自动生成尝试是不太可能正确的。因此,显式地声明它们是纯虚拟的您自己,并确保您不会意外地落入使用它们的陷阱中是比较安全的。
- @Markransom我现在明白了原因,但我仍然有两个我不明白的问题。第一个问题是编译器是否自动为任何类创建赋值和复制构造函数,即使您从未在代码中使用它们。其次,当对象是指针时,是否可以意外触发赋值或复制构造函数?
- @Zacharykraus除非你打电话给他们,否则他们的存在或不存在是无法检测到的,所以根据"好像"规则,这两种情况都有可能发生——我怀疑大多数编译器都没有。你可以用*a = *b生成一个调用,但我同意这一点并不常见。
- 而且,它实际上并不重要,因为辅助/复制接口没有任何作用。
- 在Child中,是否仍有必要将OverrideMe()写成virtual void?
- @sparker0i virtual不是必需的,因为基类声明已经使它成为虚拟的,但是保持一致是很好的做法。void仍然是必要的。
使用纯虚拟方法创建类。通过创建另一个重写这些虚拟方法的类来使用接口。
纯虚方法是一个类方法,它被定义为虚方法并分配给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
}
}; |
- 您应该在IDEMO中有一个"不做任何事情"析构函数,这样它就定义了要做的行为:IDEMO*p=new child;/*whatever*/delete p;
- 为什么子类中的overrideMe方法是虚拟的?有必要吗?
- @不,不需要,但也不疼。
- 通常,每当重写虚方法时,最好保留关键字"virtual"。尽管不是必需的,但它可以使代码更清晰——否则,您没有迹象表明该方法可以多态使用,甚至存在于基类中。
- 除凯文以外,在C++ 11中使用EDOCX1 0
- 不要忘记类声明末尾的分号。
- "分配给0"嗯,不…
除了C类/ Java中的抽象基类之外,还有一个特殊的接口类型类别的全部原因是因为C.//Java不支持多重继承。
C++支持多重继承,因此不需要特殊类型。一个抽象的基类,没有非抽象(纯虚拟)方法在功能上相当于一个C/J/Java接口。
- 能够创建接口,避免输入太多内容(virtual,=0,virtual析构函数),仍然是一件好事。同时,多重继承对我来说似乎是一个非常糟糕的主意,我从来没有见过它在实践中被使用过,但是界面一直都是需要的。坏的是C++接口不会因为我需要接口而引入接口。
- ha11欠:它有接口。它们被称为具有纯虚拟方法且没有方法实现的类。
- 由于缺乏多重继承,Java中存在一些怪癖。看看java.lang.Thread类和java.lang.Runnable接口,它的存在仅仅是因为不能将Thread扩展到另一个基。从文档上看,它似乎是为您提供的,但我曾经因为缺乏多重继承而被迫使用Runnable。
- @米尔斯鲁特:我知道,但是,为什么还要强迫我写这么多样板代码呢?而且很明显,您正在查看一个接口,而不必扫描所有方法并检查它们是否都是纯虚拟的。
- @doc:java.lang.thread有一些方法和常量,您可能不想在对象中使用它们。如果您从线程扩展,并且使用public方法checkaccess()扩展另一个类,编译器应该做什么?您真的喜欢使用像C++那样的强命名基指针吗?这看起来像是糟糕的设计,您通常需要在您认为需要多个继承的地方进行组合。
- @ha11因为它是很久以前的,所以我不记得细节,但它有我希望在我的类中拥有的方法和内容,更重要的是,我希望我的派生类对象是一个Thread实例。多重继承可能是糟糕的设计和组合。这完全取决于具体情况。
- @ha11owed+1表示"您通常需要组合,而您认为需要多重继承"
- C++中没有EDCOX1的6个关键字。
- "除了C.A/JAVA中的抽象基类之外,还有一个特殊的接口类型类别的全部原因是因为C/J/Java不支持多重继承。"——这是完全错误的断言。接口存在于Java中,以便不将构象的概念与契约和共享实现相混淆。这是一个常见的误解,在C++开发人员试图贬低什么是可以说是一个设计更符合纯OO语言。
- Objective-C还通过协议来支持这一点,并通过使协议方法实现成为可选的来进一步(起初让人困惑)。这为Objul-C提供了很多C++将很快通过概念获得的东西。
- 戴夫:真的吗?目标C有编译时评估和模板?
- @Deduplicator-我指的是概念的关键属性,即它们允许被调用方(在您的例子中是模板)在参数必须/应该支持的API上指定约束,这些约束在被调用方的声明或定义中表示,而不是在参数的类层次结构中表示。没有目标C没有模板。讨论中的模板是通过类似于Objective-C协议的"概念"改进的内容。但我相信你已经知道了。
- 然后给出一个你所说的多重继承的例子,因为到目前为止,Java似乎可以做到这一点,而不是C++。
- C++可以直接从多个类继承。Java只能直接从一个类继承。
C++中没有"接口"本身的概念。AFIK,首先在Java中引入接口,以解决缺乏多重继承的问题。这个概念被证明是非常有用的,并且在C++中通过使用抽象基类可以实现同样的效果。
抽象基类是一个类,其中至少一个成员函数(Java LINGO方法)是使用以下语法声明的纯虚函数:
1 2 3 4
| class A
{
virtual void foo() = 0;
}; |
抽象基类不能实例化,即不能声明类A的对象。只能从派生类,但任何不提供foo()实现的派生类也将是抽象的。为了停止抽象,派生类必须为它继承的所有纯虚拟函数提供实现。
请注意,抽象基类可以不仅仅是接口,因为它可以包含非纯虚拟的数据成员和成员函数。一个接口的等价物将是一个没有任何数据的抽象基类,只有纯虚拟函数。
而且,正如MarkRansom指出的那样,抽象基类应该为此提供一个虚拟析构函数,就像任何基类一样。
- 我想说的不仅仅是"缺乏多重继承",而是取代多重继承。Java从一开始就是这样设计的,因为多重继承比它解决的问题产生更多的问题。好答案
- 奥斯卡,这取决于你是否是一个学习Java的C++程序员,反之亦然。IMHO,如果明智地使用,就像在C++中几乎任何东西一样,多重继承解决问题。"interface"抽象基类是一个非常明智地使用多重继承的例子。
- @Oscarryz错误。我只会在误用时制造问题。大多数声称与MI有关的问题也会提出替代设计(没有MI)。当人们对MI的设计有问题时,这是MI的错误;如果他们对SI的设计有问题,那是他们自己的错误。""死亡钻石"(重复继承)就是一个典型的例子。我痛击不是纯粹的虚伪,而是接近。
- 从语义上讲,接口与抽象类不同,因此Java的接口不仅仅是一种技术解决方案。定义接口或抽象类之间的选择是由语义驱动的,而不是技术考虑。让我们设想一些接口"hasEngine":这是一个方面,一个特性,它可以应用于不同的类型(无论是类还是抽象类),因此我们将为此定义一个接口,而不是抽象类。
- @MarekStanley,你也许是对的,但我希望你能选一个更好的例子。我喜欢从继承接口和继承实现的角度来考虑。在C++中,您既可以继承接口,也可以继承实现(公共继承),或者只能继承实现(私有继承)。在爪哇中,您可以选择继承接口,而不需要实现。
- @好奇心的家伙,总结得很好。一般来说,正确地使用多重继承比正确地使用单一继承更困难,但仅仅是更困难并不意味着不可能。例如:class Button; class Image; class ClickableIcon : public Button, public Image {};。如果设计得当,图像将提供图形功能,按钮将提供交互功能,没有可能导致问题的重叠,而ClickableIcon本身处理按钮的功能。
- …我本来可以把这个例子说得更好的。Button提供交互代码,Image提供图形代码,ClickableIcon提供特定于自身的代码,以及将两个父类焊接在一起所需的任何代码。只要这两个类设计得当,并且没有重叠的成员名,那么类就应该是完全可行的,并且比一个或两个都是接口更容易实现。
就我所能测试的而言,添加虚拟析构函数是非常重要的。我使用的对象是用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()从未被调用。
- 通过提供一个实用的、可编译的示例,在本页上给出了最佳答案。干杯!
- testet::~tester()仅在obj"用tester声明"时运行。
- 实际上,将调用字符串privatename的析构函数,在内存中,这就是将要分配的全部内容。就运行时而言,当类的所有具体成员都被销毁时,类实例也是如此。我用一个具有两点结构的线类进行了类似的实验,发现两个结构都被破坏了(ha!)删除调用或从包含函数返回时。Valgrind确认0泄漏。
我的回答与其他人基本相同,但我认为还有两件重要的事情要做:
在您的接口中声明一个虚拟析构函数,或者创建一个受保护的非虚拟析构函数,以避免在有人试图删除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
}
} |
- +一提到虚拟继承。
- 不需要虚拟继承,因为接口中没有任何数据成员。
- 虚拟继承对方法也很重要。如果没有它,即使它的一个"实例"是纯虚拟的(只是我自己尝试过),您也会对overrideMe()产生歧义。
- @Avishay_uu"不需要虚拟继承,因为您在接口中没有任何数据成员。"错误。
- 请注意,虚拟继承可能无法在某些GCC版本上工作,如WinAVR 2010附带的4.3.3版本:gcc.gnu.org/bugzilla/show_bug.cgi?ID=35067
- -1有一个非虚拟保护的析构函数,对不起
在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.
- TBH继承比C++ 11的东西更清晰,它假装是一个接口,但它是粘合一些不一致设计的粘合剂。形状的例子脱离了现实,Circle类的设计很差。在这种情况下,您应该使用Adapter模式。很抱歉,如果这听起来有点刺耳,但在对遗产做出判断之前,试着使用一些现实生活中的图书馆,如Qt。继承使生活容易得多。
- 听起来一点也不刺耳。形状的例子是如何脱离现实的?你能举出一个(可能在ideone上)使用Adapter模式固定圆的例子吗?我想看看它的优点。
- 好的,我尽量装在这个小盒子里。首先,您通常在开始编写自己的应用程序之前选择类似"myshape"的库,以确保您的工作安全。否则,你怎么能知道埃多克斯一〔8〕还没到?预知?这就是它脱离现实的原因。实际上,如果你选择依赖"myshape"库,你可以从一开始就采用它的接口。在形状示例中,有许多无意义的内容(其中一个是您有两个Circle结构),但是适配器看起来像这样->ideone.com/uogjwk
- 正如我提到的qt。检查这个例子->qt project.org/doc/qt-5/qt widgets widgets scribble example.h&zwnj;&8203;tml,看看虚拟方法的继承有多强大。有了禁止继承和C++ 11"接口"的东西,我应该每次都实现QWIDGET所有的虚拟方法吗?我是否需要委派每个调用来检查我的自定义小部件位置?另一方面,由于调用我的实现所需的"接口",qt必须成为只包含头部的库。
- 那时候,它并没有脱离现实。当公司A购买公司B并希望将公司B的代码库集成到A中时,您有两个完全独立的代码库。想象一下每一个都有不同类型的形状层次。你不能轻易地将它们与继承结合起来,加上C公司,你就陷入了一片混乱。我想你应该看这个节目:youtube.com/watch?v=0i0fd3n5cgm我的答案比较老,但你会看到相似之处。您不必总是重新实现所有的东西,您可以在接口中提供一个实现,并选择一个成员函数(如果可用)。
- Sean Parent还谈到了基于概念的多态性,您可能会感兴趣。
- 提到Qt由三家公司(Trolltech、Nokia、Digia)所有,但他们都没有做你说的事情。它脱离了现实。给我一个现实世界中发生这种代码合并的软件的例子。即使如此,您也应该使用Adapter模式。在接口中提供类似于您答案中的一个实现意味着我必须提供所有虚拟方法,否则这些方法可能只是简单地继承的。
- 我看了部分视频,这是完全错误的。除了用于调试之外,我从不使用动态u cast。动态演员阵容意味着你的设计有问题,在这段视频中的设计是错误的。盖伊甚至提到qt,但即使在这里,他也是错的——qlayout既不是从qwidget继承的,也不是从其他方面继承的!
- 我已经更新了参考答案,以防您有兴趣了解更多信息。你提到的这三家公司都在使用同一个库。如果不是这样的话,继承就成了一个大问题。Sean Parent谈论基于概念的多态性时展示了公司的例子。提供的链接比我在这里提供的信息更多。
- 我看过关于基于概念的多态性的视频。虽然这项技术很有趣,可能会发现一些应用程序,但它不能取代基于继承的多态性(而且这种基于概念的多态性仍然只使用隐藏在私有模型结构中的继承)。多态性不仅适用于您的内部,还提供应用程序与库交互的方式。photoshop不是一个库,所以视频中甚至没有考虑它。在基于conecpt的多态性中,我看不到任何用imo更干净的继承无法实现的实用特性。
- 我同意在C++中很容易使用基于继承的多态性(这个特性具有完全的语言支持)。例如,在C语言中,很难做到这一点。在哈斯克尔,铁锈,鳞片……您有一些特性和类型类,它们类似于对非继承性多态性的语言支持。我在一个应用程序中使用过它,对此我很满意。主要是因为它提高了我的性能(我可以拥有每种类型的向量和接口向量,所以我只在真正需要的地方使用多态性)。我们将看到C++中的这种演变,人们最近开始谈论它…
- 正确的。问题是我不明白为什么遗产是"万恶之源"。这种说法很荒谬。
- 这种说法更像是"基于继承的多态性是万恶之源"。在更多地考虑了使用适配器模式提供的解决方案之后,我看到使用类型擦除接口(如Sean父级使用的接口)的好处是,您的接口位于一个单独的位置,您可以通过非成员非友元函数扩展其行为并提供默认值。然而,接口的实现要复杂得多。扩展机制…它的ADL与非ADL。如果您使用ADL,那么基本上就是从用户名称空间中"窃取"函数名。
- 这是我的错,因为你想把复杂的软件分成小的逻辑部分,继承可以帮助你完成它。将所有类型的对象(本例中的模型)退化为单个类对我来说是不可信的。如果我没有访问object_t内部构件的权限,或者在它之后我不能继承,那么我该如何扩展这种基于概念的接口呢?从我所见,我必须自己从头开始实现draw()函数,并且我没有绑定到concept_t接口。另外,使用虚拟方法,我可以有默认的实现,我可以覆盖或者不覆盖。
- 术语"接口"来自Java世界。JavaDoc必须说:"实现一个接口允许一个类对它承诺提供的行为变得更加正式。接口在类和外部世界之间形成一个契约,这个契约在编译时由编译器强制执行。如果你的类声称要实现一个接口,那么该接口定义的所有方法必须在它的源代码中出现,然后才能成功编译。"(DOCS.Oracle。COM/JavaS/Tooge/Java/Value/Stutial.HTM&ZWNJ;和8203;L)你所呈现的东西就像是颠倒的世界。
- 正是如此,我认为这是正确的做法:"先编写类,然后让它为接口建模"。它完全将类实现与对给定接口的建模分离开来,并允许您在编写该接口时无法想到的上下文中使用类。C++概念,Haskell类型,锈/鳞片特性,以及你用这种方式工作的适配器模式。Nvi习惯用法背后也有相同的推理,但这些其他解决方案更能使设计脱钩,因为类不再是接口(在"是"继承意义中)。
- 这是一条通往纯粹混乱的道路。适配器模式的存在是为了绑定不兼容的接口(如果需要),但接口已经存在。我不知道为什么用"先写你的课"的方法来上课。这要求回到纯C。如果你不知道它将如何与外部世界互动,你在什么前提下建立你的班级?假设您在类中使用了自己的gnzlbg::vector实现,并且我提供了与自定义doc::vector的接口,并且您必须将输入和输出转换为内部。如果需要转换代码,我们的代码重用和效率在哪里?
- 注意,首先,接口可以按值、唯一引用或共享引用包装类型。第二,向量的这个问题已经存在,因为分配器在std::vector类型中。解决方案是转换(涉及copy:expensive),或者提供迭代器接口,而不是使用boost.iterator.any_迭代器(使用这里描述的类型擦除,速度很慢)。你为什么说它要求回到纯C?接口是类型安全的,可以非常薄。我建立我的课程,知道我对我的问题(和我的需求)的了解会随着时间的推移而改变。
- 如果您不能修改我的代码,那么基于继承的解决方案是什么?我能想到的最好的方法是:拥有一个纯粹的虚拟基类向量,让你的向量继承它(总是支付vtable),使你的向量功能虚拟化(总是支付这个价格),并为我的向量使用适配器模式。另一种选择是,创建一个向量接口,它适应您的向量和我的向量。当你需要多态地访问你的向量和我的向量时,只在那些地方使用这个接口。这里,当我说接口时,我是指我的,或者基于适配器模式的接口。
- 让我们在聊天中继续讨论。
- 包装类型不会阻止您重新实现相同的功能以及在向量类型之间进行非常昂贵的转换。如果我们都在接口前提下使用doc::vector或gnzlbg::vector,那么向量问题就不存在了。你应该学会如何正确地使用继承,因为你现在所说的没有任何意义!
以上都是好答案。你应该记住一件额外的事情——你也可以有一个纯粹的虚拟析构函数。唯一的区别是您仍然需要实现它。
困惑的?
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()
{
} |
您希望这样做的主要原因是,如果您像我一样希望提供接口方法,但是使覆盖它们成为可选的。
要使类成为接口类,需要纯虚方法,但所有虚方法都有默认实现,因此唯一剩下的纯虚方法是析构函数。
在派生类中重新实现一个析构函数一点也不重要——我总是在派生类中重新实现一个析构函数,不管是虚拟的还是非虚拟的。
- 为什么,哦,为什么,在这种情况下,有人想让DTOR纯粹是虚拟的吗?那会有什么好处呢?您只需将一些内容强制到派生类上,它们可能不需要包含-dtor。
- 更新了我的答案以回答您的问题。纯虚拟析构函数是一种有效的实现方法(唯一的实现方法?)所有方法都有默认实现的接口类。
如果你使用微软的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。
- 我不太明白你为什么用novtable超过标准virtual void Bar() = 0;
- 它是另外一个(我刚刚注意到我添加的缺失的= 0;)。如果你不理解,请阅读文档。
- 我在没有= 0;的情况下阅读它,并认为它只是一种完全相同的非标准方式。
您还可以考虑使用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.
}; |
- 对于其他读者来说,这篇由JimHyslop和Herb Sutter撰写的Dobbs博士的文章"对话:实际上是你的",更详细地阐述了为什么要使用Nvi。
- 本文还介绍了Herb Sutter的"虚拟性"。
除了上面写的内容:
首先,确保析构函数也是纯虚拟的
第二,在实现时,您可能希望实际上(而不是通常)继承,只是为了好的度量。
- 为什么要实际上继承?
- 我喜欢虚拟继承,因为从概念上讲,它意味着继承类只有一个实例。诚然,这里的类没有任何空间要求,因此它可能是多余的。我在C++中没有做过一段时间,但是非虚拟继承会不会使上行复杂化呢?
- 为什么,哦,为什么,在这种情况下,有人想让DTOR纯粹是虚拟的吗?那会有什么好处呢?您只需将一些内容强制到派生类上,它们可能不需要包含-dtor。
- 如果存在通过指向接口的指针销毁对象的情况,则应确保析构函数是虚拟的…
- 纯虚拟析构函数没有任何问题。没必要,但没什么问题。在派生类中实现析构函数对该类的实现者来说并不是一个巨大的负担。你为什么要这样做,请看下面我的答案。
- +1表示虚拟继承,因为对于接口,类更可能从两个或多个路径派生接口。我在接口tho中选择了受保护的析构函数。
- @我相信纯虚拟析构函数背后的意图是防止生成vtable,除非从接口类继承的类本身继承自。如果这是意图,那么析构函数应该声明为纯虚拟的,并且定义为:class Base { public: ~Base() = 0; }; Base::~Base() {},以便编译器为派生类生成默认的构造函数。
在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()定义一个接口的,另外两个类实现了相同的函数,但是使用不同的算法来计算特定于形状的区域。
- 这不是所谓的接口!这只是一个抽象基类,其中有一个方法需要重写!接口通常是只包含方法定义的对象——其他类在实现接口时必须实现的"契约"。