Warum müssen die statischen Datenmember in C ++ außerhalb der Klasse separat definiert werden (im Gegensatz zu Java)?

41
class A {
  static int foo () {} // ok
  static int x; // <--- needed to be defined separately in .cpp file
};

Ich sehe keine Notwendigkeit, A::xeine .cpp-Datei (oder dieselbe Datei für Vorlagen) separat zu definieren. Warum kann nicht gleichzeitig A::xdeklariert und definiert werden?

Wurde es aus historischen Gründen verboten?

Meine Hauptfrage ist, ob sich dies auf die Funktionalität auswirkt, wenn staticDatenelemente gleichzeitig deklariert / definiert wurden (wie bei Java ).

iammilind
quelle
Als bewährte Methode ist es im Allgemeinen besser, Ihre statische Variable in eine statische Methode (möglicherweise als lokale statische) zu verpacken, um Probleme mit der Initialisierungsreihenfolge zu vermeiden.
Tamás Szelei
2
Diese Regel ist in C ++ 11 tatsächlich etwas gelockert. Statische const-Member müssen normalerweise nicht mehr definiert werden. Siehe: en.wikipedia.org/wiki/…
mirk
4
@afishwhoswimsaround: Es ist keine gute Idee, allgemeine Regeln für alle Situationen anzugeben (Best Practices sollten im Kontext angewendet werden). Hier versuchen Sie ein nicht vorhandenes Problem zu lösen. Das Problem mit der Initialisierungsreihenfolge betrifft nur Objekte, die über Konstruktoren verfügen und auf andere Objekte mit statischer Speicherdauer zugreifen. Da 'x' int ist, gilt das erste nicht, da 'x' privat ist, gilt das zweite nicht. Drittens hat dies nichts mit der Frage zu tun.
Martin York
1
Gehört zu Stack Overflow?
Leichtigkeit Rennen mit Monica
2
C ++ 17 ermöglicht inline Initialisierung von statischen Datenelementen (auch für nicht-Integer - Typen): inline static int x[] = {1, 2, 3};. Siehe de.cppreference.com/w/cpp/language/static#Static_data_members
Vladimir Reshetnikov

Antworten:

15

Ich denke, die Einschränkung, die Sie in Betracht gezogen haben, hängt nicht mit der Semantik zusammen (warum sollte sich etwas ändern, wenn die Initialisierung in derselben Datei definiert wurde?), Sondern mit dem C ++ - Kompilierungsmodell, das aus Gründen der Abwärtskompatibilität nicht einfach geändert werden kann, weil dies der Fall wäre entweder zu komplex werden (gleichzeitig ein neues und ein vorhandenes Kompilierungsmodell unterstützen) oder es nicht zulassen, vorhandenen Code zu kompilieren (indem ein neues Kompilierungsmodell eingeführt und das vorhandene gelöscht wird).

Das C ++ - Kompilierungsmodell stammt aus dem von C, in dem Sie Deklarationen in eine Quelldatei importieren, indem Sie (Header-) Dateien einschließen. Auf diese Weise sieht der Compiler rekursiv genau eine große Quelldatei, die alle enthaltenen Dateien und alle darin enthaltenen Dateien enthält. Dies hat IMO einen großen Vorteil, nämlich dass es den Compiler einfacher zu implementieren macht. Natürlich können Sie alles in die enthaltenen Dateien schreiben, dh sowohl Deklarationen als auch Definitionen. Es ist nur eine gute Praxis, Deklarationen in Headerdateien und Definitionen in C- oder CPP-Dateien abzulegen.

Andererseits ist es möglich, ein Kompilierungsmodell zu haben, in dem der Compiler sehr gut weiß, ob er die Deklaration eines globalen Symbols importiert, das in einem anderen Modul definiert ist , oder ob er die Definition eines globalen Symbols kompiliert, das von bereitgestellt wird das aktuelle Modul . Nur im letzteren Fall muss der Compiler dieses Symbol (zB eine Variable) in die aktuelle Objektdatei einfügen.

In GNU Pascal können Sie beispielsweise eine Unit ain eine Datei a.paswie die folgende schreiben :

unit a;

interface

var MyStaticVariable: Integer;

implementation

begin
  MyStaticVariable := 0
end.

Dabei wird die globale Variable in derselben Quelldatei deklariert und initialisiert.

Dann können Sie verschiedene Einheiten haben, die a importieren und die globale Variable verwenden MyStaticVariable, z. B. eine Einheit b ( b.pas):

unit b;

interface

uses a;

procedure PrintB;

implementation

procedure PrintB;
begin
  Inc(MyStaticVariable);
  WriteLn(MyStaticVariable)
end;
end.

und eine Einheit c ( c.pas):

unit c;

interface

uses a;

procedure PrintC;

implementation

procedure PrintC;
begin
  Inc(MyStaticVariable);
  WriteLn(MyStaticVariable)
end;
end.

Schließlich können Sie die Einheiten b und c in einem Hauptprogramm verwenden m.pas:

program M;

uses b, c;

begin
  PrintB;
  PrintC;
  PrintB
end.

Sie können diese Dateien separat kompilieren:

$ gpc -c a.pas
$ gpc -c b.pas
$ gpc -c c.pas
$ gpc -c m.pas

und dann erstellen Sie eine ausführbare Datei mit:

$ gpc -o m m.o a.o b.o c.o

und führe es aus:

$ ./m
1
2
3

Der Trick dabei ist , dass , wenn der Compiler sieht eine Nutzung in einem Programmmodul - Richtlinie (zB in b.pas verwendet), ist es nicht die entsprechende .pas - Datei enthält, sieht aber für eine .gpi Datei, dh für ein vorkompilierte Schnittstellendatei (siehe Dokumentation ). Diese .gpiDateien werden vom Compiler zusammen mit den .oDateien generiert, wenn jedes Modul kompiliert wird. Das globale Symbol MyStaticVariablewird also nur einmal in der Objektdatei definiert a.o.

Java funktioniert auf ähnliche Weise: Wenn der Compiler dann eine Klasse A in Klasse B importiert, sucht er in der Klassendatei nach A und benötigt die Datei nicht A.java. So können alle Definitionen und Initialisierungen für Klasse A in einer Quelldatei abgelegt werden.

Zurück zu C ++: Der Grund, warum Sie in C ++ statische Datenelemente in einer separaten Datei definieren müssen, hängt mehr mit dem C ++ - Kompilierungsmodell zusammen als mit Einschränkungen, die durch den Linker oder andere vom Compiler verwendete Tools auferlegt werden. In C ++ bedeutet das Importieren einiger Symbole, dass ihre Deklaration als Teil der aktuellen Kompilierungseinheit erstellt wird. Dies ist unter anderem aufgrund der Art und Weise, wie Vorlagen kompiliert werden, sehr wichtig. Dies impliziert jedoch, dass Sie keine globalen Symbole (Funktionen, Variablen, Methoden, statische Datenelemente) in einer eingeschlossenen Datei definieren können / sollten, da diese Symbole andernfalls in den kompilierten Objektdateien mehrfach definiert sein könnten.

Giorgio
quelle
42

Da statische Member von ALLEN Instanzen einer Klasse gemeinsam genutzt werden, müssen sie an einer einzigen Stelle definiert werden. In Wirklichkeit handelt es sich um globale Variablen mit einigen Zugriffsbeschränkungen.

Wenn Sie versuchen, sie in der Kopfzeile zu definieren, werden sie in jedem Modul definiert, das diese Kopfzeile enthält, und beim Verknüpfen werden Fehler angezeigt, da alle doppelten Definitionen gefunden werden.

Ja, dies ist zumindest teilweise ein historisches Problem, das von der Front ausgeht. Es könnte ein Compiler geschrieben werden, der eine Art versteckte "static_members_of_everything.cpp" erzeugt und auf diese verlinkt. Es würde jedoch die Abwärtskompatibilität aufheben, und dies hätte keinen wirklichen Vorteil.

mjfgates
quelle
2
Meine Frage ist nicht der Grund für das aktuelle Verhalten, sondern die Rechtfertigung für eine solche Sprachgrammatik. Mit anderen Worten: Angenommen, staticVariablen werden an derselben Stelle deklariert / definiert (wie Java). Was kann dann schief gehen?
iammilind
8
@iammilind Ich glaube, Sie verstehen nicht, dass die Grammatik wegen der Erklärung dieser Antwort notwendig ist. Jetzt, warum? Aufgrund des Kompilierungsmodells von C (und C ++): c- und cpp-Dateien sind die eigentlichen Codedateien, die wie separate Programme separat kompiliert und dann miteinander verknüpft werden, um eine vollständige ausführbare Datei zu erhalten. Die Header sind kein eigentlicher Code für den Compiler, sondern nur Text zum Kopieren und Einfügen in c- und cpp-Dateien. Wenn etwas mehrmals definiert wurde, kann es nicht kompiliert werden. Dies ist auch nicht der Fall, wenn Sie mehrere lokale Variablen mit demselben Namen haben.
Klaim am
1
@Klaim, was ist mit staticMitgliedern in template? Sie sind in allen Header-Dateien zulässig, da sie sichtbar sein müssen. Ich bestreite diese Antwort nicht, aber sie passt auch nicht zu meiner Frage.
iammilind
@iammilind-Vorlagen sind kein echter Code, sondern Code, der Code generiert. Jede Instanz einer Vorlage verfügt über eine und nur eine statische Instanz jeder statischen Deklaration, die vom Compiler bereitgestellt wird. Sie müssen die Instanz noch definieren, aber wenn Sie eine Vorlage einer Instanz definieren, handelt es sich, wie oben erwähnt, nicht um echten Code. Vorlagen sind buchstäblich Code-Vorlagen, mit denen der Compiler Code generiert.
Klaim,
2
@iammilind: Vorlagen werden in der Regel in jeder Objektdatei instanziiert, einschließlich ihrer statischen Variablen. Unter Linux mit ELF-Objektdateien markiert der Compiler die Instanziierungen als schwache Symbole , was bedeutet, dass der Linker mehrere Kopien derselben Instanziierung kombiniert. Dieselbe Technologie könnte verwendet werden, um statische Variablen in Header-Dateien zu definieren. Der Grund, warum dies nicht erfolgt, ist wahrscheinlich eine Kombination aus historischen Gründen und Überlegungen zur Kompilierungsleistung. Das gesamte Kompilierungsmodell wird hoffentlich repariert, sobald der nächste C ++ - Standard Module enthält .
han
6

Der wahrscheinliche Grund dafür ist, dass dadurch die C ++ - Sprache in Umgebungen implementierbar bleibt, in denen das Zusammenführen mehrerer Definitionen aus mehreren Objektdateien durch die Objektdatei und das Verknüpfungsmodell nicht unterstützt wird.

Eine Klassendeklaration (aus guten Gründen als Deklaration bezeichnet) wird in mehrere Übersetzungseinheiten aufgeteilt. Wenn die Deklaration Definitionen für statische Variablen enthalten würde, würden Sie am Ende mehrere Definitionen in mehreren Übersetzungseinheiten haben (und denken Sie daran, dass diese Namen eine externe Verknüpfung haben.)

Diese Situation ist möglich, erfordert jedoch, dass der Linker mehrere Definitionen verarbeitet, ohne sich zu beschweren.

(Beachten Sie, dass dies im Widerspruch zur Ein-Definition-Regel steht, es sei denn, dies kann anhand der Art eines Symbols oder der Art des Abschnitts erfolgen, in dem es platziert ist.)

Kaz
quelle
6

Es gibt einen großen Unterschied zwischen C ++ und Java.

Java arbeitet auf seiner eigenen virtuellen Maschine, die alles in ihrer eigenen Laufzeitumgebung erstellt. Wenn eine Definition mehrmals vorkommt, wird sie einfach auf dasselbe Objekt angewendet, das die Laufzeitumgebung letztendlich kennt.

In C ++ gibt es keinen "ultimativen Wissensinhaber": C ++, C, Fortran Pascal usw. sind alle "Übersetzer" von einem Quellcode (CPP-Datei) in ein Zwischenformat (die OBJ-Datei oder ".o" -Datei, abhängig von das Betriebssystem), in dem Anweisungen in Maschinenbefehle übersetzt werden und Namen zu indirekten Adressen werden, die durch eine Symboltabelle vermittelt werden.

Ein Programm wird nicht vom Compiler erstellt, sondern von einem anderen Programm (dem "Linker"), der alle OBJ-s zusammenfügt (unabhängig von der Sprache, aus der sie stammen), indem er alle Adressen, die sich in Richtung der Symbole befinden, in Richtung ihrer wirksame Definition.

Anhand der Funktionsweise des Linkers muss eine Definition (was den physischen Raum für eine Variable schafft) eindeutig sein.

Beachten Sie, dass C ++ selbst keine Verknüpfung herstellt und der Linker nicht von C ++ - Spezifikationen ausgegeben wird: Der Linker ist aufgrund des Aufbaus der Betriebssystemmodule vorhanden (normalerweise in C und ASM). C ++ muss es so verwenden, wie es ist.

Nun: Eine Header-Datei kann in mehrere CPP-Dateien "eingefügt" werden. Jede CPP-Datei wird unabhängig von jeder anderen übersetzt. Ein Compiler, der verschiedene CPP-Dateien übersetzt, die alle in derselben Definition empfangen, platziert den " Erstellungscode " für das definierte Objekt in allen resultierenden OBJs.

Der Compiler weiß nicht (und wird es auch nie wissen), ob all diese OBJs jemals zusammen verwendet werden, um ein einziges Programm zu bilden, oder separat, um verschiedene unabhängige Programme zu bilden.

Der Linker weiß nicht, wie und warum Definitionen existieren und woher sie kommen (er weiß sogar nichts über C ++: Jede "statische Sprache" kann Definitionen und Referenzen erzeugen, die verknüpft werden sollen). Es weiß nur, dass es Verweise auf ein bestimmtes "Symbol" gibt, das an einer bestimmten resultierenden Adresse "definiert" ist.

Wenn es für ein bestimmtes Symbol mehrere Definitionen gibt (verwechseln Sie Definitionen nicht mit Referenzen), hat der Linker keine Kenntnisse (da er sprachunabhängig ist) darüber, was er damit tun soll.

Es ist so, als würde man mehrere Städte zu einer Großstadt zusammenlegen: Wenn zwei " Time Square " und eine Reihe von Leuten von außerhalb nach " Time Square " fragen , kann man sich nicht für eine rein technische Basis entscheiden (ohne Kenntnis der Politik , die diese Namen vergeben hat und für deren Verwaltung verantwortlich ist), an welchen genauen Ort sie gesendet werden sollen.

Emilio Garavaglia
quelle
3
Der Unterschied zwischen Java und C ++ in Bezug auf globale Symbole hängt nicht damit zusammen, dass Java über eine virtuelle Maschine verfügt, sondern mit dem C ++ - Kompilierungsmodell. In dieser Hinsicht würde ich Pascal und C ++ nicht in dieselbe Kategorie einordnen. Eher würde ich C und C ++ als "Sprachen, in denen die importierten Deklarationen enthalten und zusammen mit der Hauptquelldatei kompiliert sind" zusammenfassen, im Gegensatz zu Java und Pascal (und vielleicht OCaml, Scala, Ada usw.) als "Sprachen, in denen die importierte Deklarationen werden vom Compiler in vorkompilierten Dateien nachgeschlagen, die Informationen zu exportierten Symbolen enthalten ".
Giorgio
1
@Giorgio: Der Verweis auf Java ist vielleicht nicht erwünscht, aber ich denke, dass Emilios Antwort größtenteils richtig ist, wenn man zum Kern des Problems kommt, nämlich der Objektdatei / Linker-Phase nach der separaten Kompilierung.
Ixache
5

Dies ist erforderlich, da der Compiler sonst nicht weiß, wo die Variable abgelegt werden soll. Jede cpp-Datei wird einzeln kompiliert und kennt die andere nicht. Der Linker löst Variablen, Funktionen usw. auf. Ich persönlich sehe keinen Unterschied zwischen vtable und statischen Elementen (wir müssen nicht auswählen, in welcher Datei die vtable definiert ist).

Ich gehe meistens davon aus, dass es für Compiler-Autoren einfacher ist, es auf diese Weise zu implementieren. Statische Variablen außerhalb von class / struct existieren und möglicherweise aus Gründen der Konsistenz oder weil es für Compiler-Autoren einfacher wäre, sie zu implementieren, haben sie diese Einschränkung in den Standards definiert.


quelle
2

Ich glaube, ich habe den Grund gefunden. Durch das Definieren einer staticVariablen in einem separaten Bereich kann sie auf einen beliebigen Wert initialisiert werden. Wenn nicht initialisiert, wird standardmäßig 0 verwendet.

Vor C ++ 11 war die In-Class-Initialisierung in C ++ nicht zulässig. Man kann also nicht schreiben wie:

struct X
{
  static int i = 4;
};

Um die Variable nun zu initialisieren, muss sie außerhalb der Klasse wie folgt geschrieben werden:

struct X
{
  static int i;
};
int X::i = 4;

Wie auch in anderen Antworten besprochen, int X::iist es nun ein globaler und das Deklarieren eines globalen Werts in vielen Dateien verursacht einen Mehrfachsymbolverbindungsfehler.

Daher muss eine Klassenvariable staticin einer separaten Übersetzungseinheit deklariert werden. Es kann jedoch immer noch argumentiert werden, dass der folgende Weg den Compiler anweisen sollte, nicht mehrere Symbole zu erstellen

static int X::i = 4;
^^^^^^
iammilind
quelle
0

A :: x ist nur eine globale Variable, jedoch mit Namespace für A und Zugriffsbeschränkungen.

Jemand muss es immer noch deklarieren, wie jede andere globale Variable, und das kann sogar in einem Projekt geschehen, das statisch mit dem Projekt verknüpft ist, das den Rest des A-Codes enthält.

Ich würde das alles schlechtes Design nennen, aber es gibt ein paar Funktionen, die Sie auf diese Weise ausnutzen können:

  1. Aufrufreihenfolge des Konstruktors ... Nicht wichtig für ein int, aber für ein komplexeres Element, das möglicherweise auf andere statische oder globale Variablen zugreift, kann dies kritisch sein.

  2. der statische Initialisierer - Sie können einem Client überlassen, auf was A :: x initialisiert werden soll.

  3. In c ++ und c ist der physische Speicherort von Variablen von Bedeutung, da Sie über Zeiger uneingeschränkt auf den Speicher zugreifen können. Es gibt sehr ungezogene Dinge, die Sie ausnutzen können, je nachdem, wo sich eine Variable in einem Verknüpfungsobjekt befindet.

Ich bezweifle, dass dies das "Warum" dieser Situation ist. Es ist wahrscheinlich nur eine Weiterentwicklung von C zu C ++ und ein Abwärtskompatibilitätsproblem, das Sie davon abhält, die Sprache jetzt zu ändern.

James Podesta
quelle
2
dies scheint nicht zu bieten alles wesentliche über gemacht Punkte und erläuterte vor 6 Antworten
gnat