Wie richte ich eine Klasse ein, die eine Schnittstelle darstellt? Ist das nur eine abstrakte Basisklasse?
Wie richte ich eine Klasse ein, die eine Schnittstelle darstellt? Ist das nur eine abstrakte Basisklasse?
Antworten:
Um die Antwort von bradtgmurray zu erweitern , möchten Sie möglicherweise eine Ausnahme von der Liste der reinen virtuellen Methoden Ihrer Schnittstelle machen, indem Sie einen virtuellen Destruktor hinzufügen. Auf diese Weise können Sie den Zeigerbesitz an eine andere Partei übergeben, ohne die konkrete abgeleitete Klasse verfügbar zu machen. Der Destruktor muss nichts tun, da die Schnittstelle keine konkreten Elemente enthält. Es mag widersprüchlich erscheinen, eine Funktion sowohl als virtuell als auch als Inline zu definieren, aber vertrauen Sie mir - das ist es nicht.
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
}
};
Sie müssen keinen Body für den virtuellen Destruktor einfügen. Es stellt sich heraus, dass einige Compiler Probleme haben, einen leeren Destruktor zu optimieren, und Sie sollten die Standardeinstellung verwenden.
=0
) Destruktor mit einem Körper zu definieren . Der Vorteil hierbei ist, dass der Compiler theoretisch sehen kann, dass vtable jetzt keine gültigen Mitglieder hat, und es insgesamt verwerfen kann. Bei einem virtuellen Destruktor mit einem Körper kann der Destruktor (virtuell) aufgerufen werden, z. B. mitten in der Konstruktion über einen this
Zeiger (wenn das konstruierte Objekt noch vom Parent
Typ ist), und daher muss der Compiler eine gültige vtable bereitstellen. Wenn Sie also this
während der Erstellung nicht explizit virtuelle Destruktoren über aufrufen :), können Sie Codegröße sparen.
override
Schlüsselwort angeben können, um Argumente zur Kompilierungszeit und zur Überprüfung des Rückgabewerttyps zu ermöglichen. Zum Beispiel in der Erklärung von Kindvirtual void OverrideMe() override;
Erstellen Sie eine Klasse mit rein virtuellen Methoden. Verwenden Sie die Schnittstelle, indem Sie eine andere Klasse erstellen, die diese virtuellen Methoden überschreibt.
Eine rein virtuelle Methode ist eine Klassenmethode, die als virtuell definiert und 0 zugewiesen ist.
class IDemo
{
public:
virtual ~IDemo() {}
virtual void OverrideMe() = 0;
};
class Child : public IDemo
{
public:
virtual void OverrideMe()
{
//do stuff
}
};
override
in C ++ 11
Der ganze Grund, warum Sie zusätzlich zu abstrakten Basisklassen in C # / Java eine spezielle Schnittstellentypkategorie haben, ist, dass C # / Java keine Mehrfachvererbung unterstützt.
C ++ unterstützt Mehrfachvererbung, daher wird kein spezieller Typ benötigt. Eine abstrakte Basisklasse ohne nicht abstrakte (rein virtuelle) Methoden entspricht funktional einer C # / Java-Schnittstelle.
Thread
Instanz ist. Mehrfachvererbung kann sowohl schlechtes Design als auch schlechte Zusammensetzung sein. Es hängt alles vom Fall ab.
In C ++ gibt es kein Konzept für "Schnittstelle" an sich. AFAIK, Schnittstellen wurden zuerst in Java eingeführt, um das Fehlen einer Mehrfachvererbung zu umgehen. Dieses Konzept hat sich als sehr nützlich erwiesen, und der gleiche Effekt kann in C ++ durch Verwendung einer abstrakten Basisklasse erzielt werden.
Eine abstrakte Basisklasse ist eine Klasse, in der mindestens eine Mitgliedsfunktion (Methode in Java-Jargon) eine reine virtuelle Funktion ist, die mit der folgenden Syntax deklariert wird:
class A
{
virtual void foo() = 0;
};
Eine abstrakte Basisklasse kann nicht instanziiert werden, dh Sie können kein Objekt der Klasse A deklarieren. Sie können nur Klassen von A ableiten, aber jede abgeleitete Klasse, die keine Implementierung von bereitstellt, foo()
ist auch abstrakt. Um nicht mehr abstrakt zu sein, muss eine abgeleitete Klasse Implementierungen für alle reinen virtuellen Funktionen bereitstellen, die sie erbt.
Beachten Sie, dass eine abstrakte Basisklasse mehr als eine Schnittstelle sein kann, da sie Datenelemente und Elementfunktionen enthalten kann, die nicht rein virtuell sind. Ein Äquivalent einer Schnittstelle wäre eine abstrakte Basisklasse ohne Daten mit nur rein virtuellen Funktionen.
Und wie Mark Ransom betonte, sollte eine abstrakte Basisklasse einen virtuellen Destruktor bereitstellen, genau wie jede Basisklasse.
Soweit ich testen konnte, ist es sehr wichtig, den virtuellen Destruktor hinzuzufügen. Ich verwende Objekte, die mit erstellt new
und mit zerstört wurden delete
.
Wenn Sie den virtuellen Destruktor nicht zur Schnittstelle hinzufügen, wird der Destruktor der geerbten Klasse nicht aufgerufen.
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;
}
Wenn Sie den vorherigen Code ohne ausführen virtual ~IBase() {};
, werden Sie feststellen, dass der Destruktor Tester::~Tester()
niemals aufgerufen wird.
Meine Antwort ist im Grunde die gleiche wie die der anderen, aber ich denke, es gibt zwei weitere wichtige Dinge zu tun:
Deklarieren Sie einen virtuellen Destruktor in Ihrer Benutzeroberfläche oder erstellen Sie einen geschützten nicht virtuellen Destruktor, um undefiniertes Verhalten zu vermeiden, wenn jemand versucht, ein Objekt vom Typ zu löschen IDemo
.
Verwenden Sie die virtuelle Vererbung, um Probleme mit der Mehrfachvererbung zu vermeiden. (Es gibt häufiger Mehrfachvererbung, wenn wir Schnittstellen verwenden.)
Und wie andere Antworten:
Verwenden Sie die Schnittstelle, indem Sie eine andere Klasse erstellen, die diese virtuellen Methoden überschreibt.
class IDemo
{
public:
virtual void OverrideMe() = 0;
virtual ~IDemo() {}
}
Oder
class IDemo
{
public:
virtual void OverrideMe() = 0;
protected:
~IDemo() {}
}
Und
class Child : virtual public IDemo
{
public:
virtual void OverrideMe()
{
//do stuff
}
}
In C ++ 11 können Sie die Vererbung ganz einfach vermeiden:
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;
// ...
};
In diesem Fall verfügt eine Schnittstelle über eine Referenzsemantik, dh Sie müssen sicherstellen, dass das Objekt die Schnittstelle überlebt (es ist auch möglich, Schnittstellen mit Wertesemantik zu erstellen).
Diese Art von Schnittstellen hat ihre Vor- und Nachteile:
Schließlich ist die Vererbung die Wurzel allen Übels im komplexen Software-Design. In Sean Parent's Value Semantics and Concepts-based Polymorphism (sehr empfohlene, bessere Versionen dieser Technik werden dort erklärt) wird der folgende Fall untersucht:
Angenommen, ich habe eine Anwendung, in der ich über die MyShape
Benutzeroberfläche polymorph mit meinen Formen umgehe :
struct MyShape { virtual void my_draw() = 0; };
struct Circle : MyShape { void my_draw() { /* ... */ } };
// more shapes: e.g. triangle
In Ihrer Anwendung tun Sie dasselbe mit verschiedenen Formen über die YourShape
Benutzeroberfläche:
struct YourShape { virtual void your_draw() = 0; };
struct Square : YourShape { void your_draw() { /* ... */ } };
/// some more shapes here...
Angenommen, Sie möchten einige der Formen verwenden, die ich in Ihrer Anwendung entwickelt habe. Konzeptionell haben unsere Formen dieselbe Oberfläche, aber damit meine Formen in Ihrer Anwendung funktionieren, müssen Sie meine Formen wie folgt erweitern:
struct Circle : MyShape, YourShape {
void my_draw() { /*stays the same*/ };
void your_draw() { my_draw(); }
};
Erstens ist es möglicherweise überhaupt nicht möglich, meine Formen zu ändern. Darüber hinaus führt die Mehrfachvererbung zum Spaghetti-Code (stellen Sie sich vor, ein drittes Projekt verwendet die TheirShape
Schnittstelle ... was passiert, wenn sie auch ihre Zeichenfunktion aufrufen my_draw
?).
Update: Es gibt einige neue Referenzen zum nicht vererbungsbasierten Polymorphismus:
Circle
Klasse ist ein schlechtes Design. Adapter
In solchen Fällen sollten Sie Muster verwenden. Es tut mir leid, wenn es ein bisschen hart klingt, aber versuchen Sie, eine Bibliothek aus dem wirklichen Leben zu verwenden, Qt
bevor Sie über die Vererbung urteilen. Vererbung macht das Leben viel einfacher.
Adapter
Musters reparieren können ? Ich bin interessiert, seine Vorteile zu sehen.
Square
es nicht schon da ist? Vorwissen? Deshalb ist es von der Realität losgelöst. Wenn Sie sich in Wirklichkeit auf die "MyShape" -Bibliothek verlassen, können Sie diese von Anfang an in die Benutzeroberfläche übernehmen. Im Beispiel für Formen gibt es viele Unsinns (einer davon ist, dass Sie zwei Circle
Strukturen haben), aber der Adapter würde ungefähr so aussehen -> ideone.com/UogjWk
Alle guten Antworten oben. Eine zusätzliche Sache, die Sie beachten sollten - Sie können auch einen reinen virtuellen Destruktor haben. Der einzige Unterschied besteht darin, dass Sie es noch implementieren müssen.
Verwirrt?
--- header file ----
class foo {
public:
foo() {;}
virtual ~foo() = 0;
virtual bool overrideMe() {return false;}
};
---- source ----
foo::~foo()
{
}
Der Hauptgrund, warum Sie dies tun möchten, ist, wenn Sie wie ich Schnittstellenmethoden bereitstellen möchten, diese jedoch optional überschreiben möchten.
Um die Klasse zu einer Schnittstellenklasse zu machen, ist eine rein virtuelle Methode erforderlich. Alle Ihre virtuellen Methoden verfügen jedoch über Standardimplementierungen. Die einzige Methode, die zur reinen virtuellen Methode übrig bleibt, ist der Destruktor.
Die Neuimplementierung eines Destruktors in der abgeleiteten Klasse ist überhaupt keine große Sache - ich implementiere immer einen virtuellen oder nicht virtuellen Destruktor in meinen abgeleiteten Klassen neu.
Wenn Sie den C ++ - Compiler von Microsoft verwenden, können Sie Folgendes tun:
struct __declspec(novtable) IFoo
{
virtual void Bar() = 0;
};
class Child : public IFoo
{
public:
virtual void Bar() override { /* Do Something */ }
}
Ich mag diesen Ansatz, weil er zu einem viel kleineren Schnittstellencode führt und die generierte Codegröße erheblich kleiner sein kann. Durch die Verwendung von novtable werden alle Verweise auf den vtable-Zeiger in dieser Klasse entfernt, sodass Sie ihn niemals direkt instanziieren können. Siehe die Dokumentation hier - novtable .
novtable
über Standard verwendet habenvirtual void Bar() = 0;
= 0;
das ich hinzugefügt habe). Lesen Sie die Dokumentation, wenn Sie sie nicht verstehen.
= 0;
und nahm an, dass es nur eine nicht standardmäßige Methode war, genau dasselbe zu tun.
Eine kleine Ergänzung zu dem, was dort geschrieben steht:
Stellen Sie zunächst sicher, dass Ihr Destruktor auch rein virtuell ist
Zweitens möchten Sie möglicherweise virtuell (und nicht normal) erben, wenn Sie implementieren, nur für gute Maßnahmen.
Sie können auch Vertragsklassen berücksichtigen, die mit dem NVI (Non Virtual Interface Pattern) implementiert wurden. Zum Beispiel:
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.
};
Ich bin noch neu in der C ++ - Entwicklung. Ich habe mit Visual Studio (VS) begonnen.
Dennoch scheint niemand das __interface
in VS (.NET) zu erwähnen . Ich bin mir nicht sicher, ob dies eine gute Möglichkeit ist, eine Schnittstelle zu deklarieren. Es scheint jedoch eine zusätzliche Durchsetzung zu bieten (in den Dokumenten erwähnt ). So dass Sie das nicht explizit angeben müssen virtual TYPE Method() = 0;
, da es automatisch konvertiert wird.
__interface IMyInterface {
HRESULT CommitX();
HRESULT get_X(BSTR* pbstrName);
};
Ich verwende es jedoch nicht, da ich Bedenken hinsichtlich der plattformübergreifenden Kompilierungskompatibilität habe, da es nur unter .NET verfügbar ist.
Wenn jemand etwas Interessantes daran hat, teilen Sie es bitte mit. :-)
Vielen Dank.
Es ist zwar wahr, dass dies virtual
der De-facto-Standard zum Definieren einer Schnittstelle ist, aber vergessen wir nicht das klassische C-ähnliche Muster, das mit einem Konstruktor in C ++ geliefert wird:
struct IButton
{
void (*click)(); // might be std::function(void()) if you prefer
IButton( void (*click_)() )
: click(click_)
{
}
};
// call as:
// (button.*click)();
Dies hat den Vorteil, dass Sie die Laufzeit von Ereignissen neu binden können, ohne Ihre Klasse erneut erstellen zu müssen (da C ++ keine Syntax zum Ändern polymorpher Typen hat, ist dies eine Problemumgehung für Chamäleonklassen).
Tipps:
click
den Konstruktor Ihres Nachkommens ausfüllen .protected
Mitglied und eine public
Referenz und / oder einen Getter.if
Änderungen von s gegenüber dem Status in Ihrem Code ist dies möglicherweise schneller als switch()
es oder if
s (der Turnaround wird um 3-4 if
s erwartet , aber messen Sie immer zuerst.std::function<>
Funktionszeiger auswählen , können Sie möglicherweise alle Ihre Objektdaten darin verwalten IBase
. Ab diesem Punkt können Sie Wertschemata für haben IBase
(z. B. std::vector<IBase>
wird funktionieren). Beachten Sie, dass dies je nach Compiler und STL-Code möglicherweise langsamer ist. Außerdem haben aktuelle Implementierungen von im std::function<>
Vergleich zu Funktionszeigern oder sogar virtuellen Funktionen tendenziell einen Overhead (dies könnte sich in Zukunft ändern).Hier ist die Definition von abstract class
in c ++ Standard
n4687
13.4.2
Eine abstrakte Klasse ist eine Klasse, die nur als Basisklasse einer anderen Klasse verwendet werden kann. Es können keine Objekte einer abstrakten Klasse erstellt werden, außer als Unterobjekte einer davon abgeleiteten Klasse. Eine Klasse ist abstrakt, wenn sie mindestens eine reine virtuelle Funktion hat.
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;
}
Ergebnis: Rechteckfläche: 35 Dreiecksfläche: 17
Wir haben gesehen, wie eine abstrakte Klasse eine Schnittstelle in Bezug auf getArea () definierte und zwei andere Klassen dieselbe Funktion implementierten, jedoch mit unterschiedlichen Algorithmen, um die formspezifische Fläche zu berechnen.