Mit der unten angegebenen Strukturdefinition ...
struct A {
virtual void hello() = 0;
};
Ansatz 1:
struct B : public A {
virtual void hello() { ... }
};
Ansatz 2:
struct B : public A {
void hello() { ... }
};
Gibt es einen Unterschied zwischen diesen beiden Möglichkeiten, um die Hallo-Funktion zu überschreiben?
Sie sind genau das gleiche. Es gibt keinen Unterschied zwischen ihnen, außer dass der erste Ansatz mehr Eingaben erfordert und potenziell klarer ist.
Die 'Virtualität' einer Funktion wird implizit weitergegeben. Mindestens ein Compiler, den ich verwende, generiert jedoch eine Warnung, wenn das Schlüsselwort virtual
nicht explizit verwendet wird. Sie können es also verwenden, wenn Sie nur den Compiler ruhig halten möchten .
Unter rein stilistischen Gesichtspunkten, einschließlich des Schlüsselworts virtual
, wird dem Benutzer die Tatsache klar angezeigt, dass die Funktion virtuell ist. Dies ist wichtig für alle, die B weiter unterteilen, ohne die Definition von A überprüfen zu müssen. Für tiefe Klassenhierarchien wird dies besonders wichtig.
Das Schlüsselwort virtual
ist in der abgeleiteten Klasse nicht erforderlich. Hier ist die unterstützende Dokumentation aus dem C++ Draft Standard (N3337) (Schwerpunkt Mine):
10.3 Virtuelle Funktionen
2 Wenn eine virtuelle Mitgliedsfunktion
vf
in einer KlasseBase
und in einer KlasseDerived
deklariert ist, die direkt oder indirekt vonBase
abgeleitet ist, wird eine Mitgliedsfunktionvf
mit demselben Namen, Parameter-Typ-Liste (8.3.5), Lebenslauf-Qualifikation und Referenz-Qualifikation (oder Fehlen derselben) wieBase::vf
wird deklariert, dannDerived::vf
ist auch virtuell (ob es so deklariert ist oder nicht) und überschreibtBase::vf
.
Nein, das Schlüsselwort virtual
für das Überschreiben der virtuellen Funktionen abgeleiteter Klassen ist nicht erforderlich. Es ist jedoch erwähnenswert, dass eine virtuelle Funktion nicht überschrieben werden kann.
Der Fehler beim Überschreiben tritt auf, wenn Sie beabsichtigen, eine virtuelle Funktion in einer abgeleiteten Klasse zu überschreiben, jedoch einen Fehler in der Signatur machen, sodass ein neues und deklariert wird andere virtuelle Funktion. Diese Funktion kann eine Überladung der Basisklassenfunktion sein oder sich im Namen unterscheiden. Unabhängig davon, ob Sie das Schlüsselwort virtual
in der Funktionsdeklaration der abgeleiteten Klasse verwenden oder nicht, kann der Compiler nicht erkennen, dass Sie beabsichtigen, eine Funktion einer Basisklasse zu überschreiben.
Diese Gefahr wird jedoch dankenswerterweise durch die Sprachfunktion C++ 11 explizites Überschreiben behoben, mit der der Quellcode eindeutig angeben kann, dass eine Member-Funktion eine Basisklassenfunktion überschreiben soll:
struct Base {
virtual void some_func(float);
};
struct Derived : Base {
virtual void some_func(int) override; // ill-formed - doesn't override a base class method
};
Der Compiler gibt einen Fehler beim Kompilieren aus, und der Programmierfehler ist sofort offensichtlich (möglicherweise hätte die Funktion in Derived ein float
als Argument verwenden sollen).
Siehe WP: C++ 11 .
Das Hinzufügen des Schlüsselworts "virtual" ist empfehlenswert, da es die Lesbarkeit verbessert, jedoch nicht erforderlich ist. Funktionen, die in der Basisklasse als virtuell deklariert wurden und in den abgeleiteten Klassen dieselbe Signatur aufweisen, werden standardmäßig als "virtuell" betrachtet.
Es gibt keinen Unterschied für den Compiler, wenn Sie virtual
in die abgeleitete Klasse schreiben oder weglassen.
Sie müssen sich jedoch die Basisklasse ansehen, um diese Informationen zu erhalten. Daher würde ich empfehlen, das Schlüsselwort virtual
auch in der abgeleiteten Klasse hinzuzufügen, wenn Sie dem Menschen zeigen möchten, dass diese Funktion virtuell ist.
Es gibt einen erheblichen Unterschied, wenn Sie Vorlagen haben und anfangen, Basisklassen als Vorlagenparameter zu verwenden:
struct None {};
template<typename... Interfaces>
struct B : public Interfaces
{
void hello() { ... }
};
struct A {
virtual void hello() = 0;
};
template<typename... Interfaces>
void t_hello(const B<Interfaces...>& b) // different code generated for each set of interfaces (a vtable-based clever compiler might reduce this to 2); both t_hello and b.hello() might be inlined properly
{
b.hello(); // indirect, non-virtual call
}
void hello(const A& a)
{
a.hello(); // Indirect virtual call, inlining is impossible in general
}
int main()
{
B<None> b; // Ok, no vtable generated, empty base class optimization works, sizeof(b) == 1 usually
B<None>* pb = &b;
B<None>& rb = b;
b.hello(); // direct call
pb->hello(); // pb-relative non-virtual call (1 redirection)
rb->hello(); // non-virtual call (1 redirection unless optimized out)
t_hello(b); // works as expected, one redirection
// hello(b); // compile-time error
B<A> ba; // Ok, vtable generated, sizeof(b) >= sizeof(void*)
B<None>* pba = &ba;
B<None>& rba = ba;
ba.hello(); // still can be a direct call, exact type of ba is deducible
pba->hello(); // pba-relative virtual call (usually 3 redirections)
rba->hello(); // rba-relative virtual call (usually 3 redirections unless optimized out to 2)
//t_hello(b); // compile-time error (unless you add support for const A& in t_hello as well)
hello(ba);
}
Das Schöne daran ist, dass Sie jetzt Interface- und Nicht-Interface-Funktionen definieren können später um Klassen zu definieren. Dies ist nützlich für die Zusammenarbeit von Schnittstellen zwischen Bibliotheken (verlassen Sie sich nicht auf diesen Standardentwurfsprozess für eine einzelne Bibliothek). Es kostet Sie nichts, dies für alle Ihre Klassen zuzulassen - Sie können sogar typedef
B für etwas festlegen, wenn Sie möchten.
Beachten Sie, dass Sie in diesem Fall möglicherweise auch Kopier-/Verschiebungskonstruktoren als Vorlagen deklarieren möchten: Wenn Sie Konstruktionen aus verschiedenen Schnittstellen erstellen, können Sie zwischen verschiedenen B<>
- Typen 'umwandeln'.
Es ist fraglich, ob Sie in t_hello()
Unterstützung für const A&
Hinzufügen sollten. Der übliche Grund für diese Umschreibung ist die Abkehr von einer vererbungsbasierten zu einer vorlagenbasierten Spezialisierung, hauptsächlich aus Gründen der Leistung. Wenn Sie die alte Schnittstelle weiterhin unterstützen, können Sie eine alte Verwendung kaum erkennen (oder von ihr abhalten).
Das Schlüsselwort virtual
sollte zu Funktionen einer Basisklasse hinzugefügt werden, um sie überschreibbar zu machen. In Ihrem Beispiel ist struct A
ist die Basisklasse. virtual
bedeutet nichts für die Verwendung dieser Funktionen in einer abgeleiteten Klasse. Wenn Sie jedoch möchten, dass Ihre abgeleitete Klasse auch selbst eine Basisklasse ist und diese Funktion überschrieben werden soll, müssen Sie virtual
dort ablegen.
struct B : public A {
virtual void hello() { ... }
};
struct C : public B {
void hello() { ... }
};
Hier erbt C
von B
, daher ist B
nicht die Basisklasse (es ist auch eine abgeleitete Klasse), und C
ist die abgeleitete Klasse. Das Vererbungsdiagramm sieht folgendermaßen aus:
A
^
|
B
^
|
C
Stellen Sie also virtual
vor Funktionen in potenzielle Basisklassen, die möglicherweise untergeordnete Klassen haben. Mit virtual
können Ihre Kinder Ihre Funktionen überschreiben. Es ist nichts Falsches daran, das virtual
vor Funktionen in den abgeleiteten Klassen zu setzen, aber es ist nicht erforderlich. Es wird jedoch empfohlen, da jemand, der von Ihrer abgeleiteten Klasse erben möchte, sich nicht darüber freut, dass das Überschreiben der Methode nicht wie erwartet funktioniert.
Stellen Sie also virtual
in allen an der Vererbung beteiligten Klassen vor Funktionen, es sei denn, Sie wissen, dass die Klasse keine untergeordneten Klassen hat, die die Funktionen der Basisklasse überschreiben müssten. Es ist eine gute Praxis.
Ich werde auf jeden Fall das Schlüsselwort Virtual für die untergeordnete Klasse einfügen, weil