Perché i membri dei dati statici devono essere definiti al di fuori della classe separatamente in C ++ (a differenza di Java)?

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

Non vedo la necessità di avere A::x definito separatamente in un file .cpp (o lo stesso file per i modelli). Perché non può essere A::x dichiarato e definito allo stesso tempo?

È stato proibito per motivi storici?

La mia domanda principale è, interesserà qualsiasi funzionalità se i membri di dati static sono stati dichiarati / definiti allo stesso tempo (come Java )?

    
posta iammilind 20.04.2012 - 07:12
fonte

7 risposte

15

Penso che la limitazione che hai considerato non sia legata alla semantica (perché qualcosa dovrebbe cambiare se l'inizializzazione fosse definita nello stesso file?), ma piuttosto al modello di compilazione C ++ che, per ragioni di compatibilità con le versioni precedenti, non può essere facilmente modificato perché altrimenti diventerebbe troppo complesso (supportando un nuovo modello di compilazione e quello esistente allo stesso tempo) o non permetterebbe di compilare il codice esistente (introducendo un nuovo modello di compilazione e rilasciando quello esistente).

Il modello di compilazione C ++ deriva da quello di C, in cui si importano dichiarazioni in un file sorgente includendo file (di intestazione). In questo modo, il compilatore vede esattamente un grande file sorgente, contenente tutti i file inclusi e tutti i file inclusi da quei file, in modo ricorsivo. Questo ha IMO un grande vantaggio, cioè che rende il compilatore più facile da implementare. Naturalmente, puoi scrivere qualsiasi cosa nei file inclusi, cioè entrambe le dichiarazioni e le definizioni. È solo una buona pratica inserire le dichiarazioni nei file di intestazione e nelle definizioni nei file .c o .cpp.

D'altra parte, è possibile avere un modello di compilazione in cui il compilatore conosce molto bene se è importare la dichiarazione di un simbolo globale che è definito in un altro modulo , oppure se sta compilando la definizione di un simbolo globale fornito dal modulo corrente . Solo in quest'ultimo caso il compilatore deve mettere questo simbolo (ad esempio una variabile) nella corrente file oggetto.

Ad esempio, in GNU Pascal puoi scrivere un'unità a in un file a.pas come questo:

unit a;

interface

var MyStaticVariable: Integer;

implementation

begin
  MyStaticVariable := 0
end.

dove la variabile globale è dichiarata e inizializzata nello stesso file sorgente.

Quindi puoi avere unità diverse che importano e usare la variabile globale MyStaticVariable , ad es. un'unità b ( b.pas ):

unit b;

interface

uses a;

procedure PrintB;

implementation

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

e un'unità c ( c.pas ):

unit c;

interface

uses a;

procedure PrintC;

implementation

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

Finalmente puoi usare le unità be c in un programma principale m.pas :

program M;

uses b, c;

begin
  PrintB;
  PrintC;
  PrintB
end.

Puoi compilare questi file separatamente:

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

e quindi produrre un eseguibile con:

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

ed eseguilo:

$ ./m
1
2
3

Il trucco qui è che quando il compilatore vede una direttiva uses in un modulo di programma (ad esempio usa a in b.pas), non include il corrispondente file .pas, ma cerca un file .gpi, cioè per un file di interfaccia precompilato (vedi la documentazione ). Questi file .gpi vengono generati dal compilatore insieme ai file .o quando ogni modulo è compilato. Quindi il simbolo globale MyStaticVariable viene definito solo una volta nel file oggetto a.o .

Java funziona in modo simile: quando il compilatore importa una classe A in classe B, guarda il file di classe per A e non ha bisogno del file A.java . Quindi tutte le definizioni e le inizializzazioni per la classe A possono essere inserite in un file sorgente.

Tornando al C ++, la ragione per cui in C ++ devi definire membri di dati statici in un file separato è più legata al modello di compilazione C ++ che alle limitazioni imposte dal linker o da altri strumenti usati dal compilatore. In C ++, importare alcuni simboli significa costruire la loro dichiarazione come parte di l'attuale unità di compilazione. Questo è molto importante, tra le altre cose, a causa del modo in cui i modelli sono compilati. Ma questo implica che non puoi / non devi definire alcun simbolo globale (funzioni, variabili, metodi, membri di dati statici) in un file incluso, altrimenti questi simboli potrebbero essere definizione multipla nei file oggetto compilati.

    
risposta data 21.04.2012 - 10:58
fonte
41

Poiché i membri statici sono condivisi tra TUTTE le istanze di una classe, devono essere definiti in un'unica posizione. In realtà, sono variabili globali con alcune restrizioni di accesso.

Se provi a definirli nell'intestazione, verranno definiti in ogni modulo che include quell'intestazione e riceverai errori durante il collegamento poiché trova tutte le definizioni duplicate.

Sì, questo è almeno in parte un problema storico che risale a Cfront; si potrebbe scrivere un compilatore che creerebbe una sorta di "static_members_of_everything.cpp" nascosto e collegherà a questo. Tuttavia, romperebbe la retrocompatibilità e non ci sarebbe alcun reale vantaggio nel farlo.

    
risposta data 20.04.2012 - 07:24
fonte
6

La ragione probabile per questo è che questo mantiene il linguaggio C ++ implementabile in ambienti in cui il modello di oggetto e il modello di collegamento non supporta la fusione di più definizioni da più file oggetto.

Una dichiarazione di classe (chiamata una dichiarazione per buoni motivi) viene estratta in più unità di traduzione. Se la dichiarazione contenesse definizioni di variabili statiche, allora si otterrebbero più definizioni in più unità di traduzione (e ricorda, questi nomi hanno un collegamento esterno.)

Questa situazione è possibile, ma richiede al linker di gestire più definizioni senza lamentarsi.

(E si noti che questo è in conflitto con la regola One Definition, a meno che non possa essere eseguita in base al tipo di simbolo o al tipo di sezione in cui è inserita.)

    
risposta data 20.04.2012 - 08:00
fonte
6

C'è una grande differenza tra C ++ e Java.

Java opera sulla propria macchina virtuale che crea tutto nel proprio ambiente runtime. Se una definizione sembra essere vista più di una volta, agirà semplicemente sullo stesso oggetto che l'ambiente runtime conosce in ultima analisi.

In C ++ non esiste un "ultimo proprietario della conoscenza": C ++, C, Fortran Pascal ecc. sono tutti "traduttori" da un codice sorgente (file CPP) in un formato intermedio (il file OBJ o il file ".o") , a seconda del sistema operativo) in cui le istruzioni vengono tradotte in istruzioni macchina e i nomi diventano indirizzi indiretti mediati da una tabella dei simboli.

Un programma non viene creato dal compilatore, ma da un altro programma (il "linker"), che unisce tutti gli OBJ insieme (indipendentemente dalla lingua da cui provengono) reindirizzando tutti gli indirizzi verso i simboli , verso la loro effettiva definizione.

Dal modo in cui funziona il linker, una definizione (ciò che crea lo spazio fisico per una variabile) deve essere unica.

Si noti che C ++ non si collega da solo e che il linker non viene emesso dalle specifiche C ++: il linker esiste a causa del modo in cui i moduli del sistema operativo sono creati (di solito in C e ASM). C ++ deve usarlo così com'è.

Ora: un file di intestazione è qualcosa da "incollare" in diversi file CPP. Ogni file CPP viene tradotto indipendentemente da ogni altro. Un compilatore che traduce diversi file CPP, tutti che ricevono una stessa definizione posizionerà il " codice di creazione " per l'oggetto definito in tutti gli OBJ risultanti.

Il compilatore non sa (e non lo saprà mai) se tutti questi OBJ saranno mai usati insieme per formare un singolo programma o separatamente per formare diversi programmi indipendenti.

Il linker non sa come e perché esistono le definizioni e da dove provengono (non sa nemmeno di C ++: ogni "linguaggio statico" può produrre definizioni e riferimenti da collegare). Sa solo che ci sono riferimenti a un determinato "simbolo" che è "definito" in un determinato indirizzo risultante.

Se esistono più definizioni (non confondere le definizioni con i riferimenti) per un dato simbolo, il linker non ha alcuna conoscenza (essendo indipendente dal linguaggio) su cosa fare con loro.

È come unire un certo numero di città per formare una grande città: se ti trovi con due " Time square " e un numero di persone che vengono da fuori chiedendo di andare a " Time square ", non puoi decidere su una base tecnica pura (senza alcuna conoscenza sulla politica che ha assegnato quei nomi e sarà responsabile della loro gestione) in quale luogo esatto inviare loro.

    
risposta data 21.04.2012 - 09:10
fonte
5

È necessario perché altrimenti il compilatore non sa dove mettere la variabile. Ogni file cpp è compilato individualmente e non conosce l'altro. Il linker risolve variabili, funzioni, ecc. Personalmente non vedo quale sia la differenza tra i membri vtable e static (non dobbiamo scegliere in quale file sono definiti i vtable).

Per lo più presumo che sia più facile per gli autori di compilatori implementarlo in quel modo. Le variabili statiche al di fuori della classe / struct esistono e forse per ragioni di coerenza o perché sarebbe "più facile da implementare" per gli scrittori di compilatori che hanno definito tale restrizione negli standard.

    
risposta data 21.04.2012 - 05:57
fonte
2

Penso di aver trovato il motivo. La definizione della variabile static nello spazio separato consente di inizializzarlo su qualsiasi valore. Se non è inizializzato, per impostazione predefinita verrà impostato su 0.

Prima di C ++ 11 l'inizializzazione in classe non era consentita in C ++. Quindi uno non può scrivere come:

struct X
{
  static int i = 4;
};

Così ora per inizializzare la variabile si deve scriverlo fuori dalla classe come:

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

Come già discusso anche in altre risposte, int X::i è ora un globale e la dichiarazione globale in molti file causa più errori di collegamento simbolico.

Quindi si deve dichiarare una variabile di classe static all'interno di un'unità di traduzione separata. Tuttavia, si può ancora sostenere che il modo seguente dovrebbe istruire il compilatore a non creare più simboli

static int X::i = 4;
^^^^^^
    
risposta data 18.11.2012 - 09:00
fonte
0

A :: x è solo una variabile globale ma namespace 'da A a, e con restrizioni di accesso.

Qualcuno deve ancora dichiararlo, come qualsiasi altra variabile globale, e questo può anche essere fatto in un progetto che è staticamente collegato al progetto che contiene il resto del codice A.

Chiamerei tutti questi cattivi design, ma ci sono alcune funzionalità che puoi sfruttare in questo modo:

  1. l'ordine delle chiamate del costruttore ... Non importante per un int, ma per un membro più complesso che potrebbe accedere ad altre variabili statiche o globali, può essere fondamentale.

  2. l'inizializzatore statico: puoi consentire a un client di decidere a che cosa deve essere inizializzato A :: x.

  3. in c ++ e c, perché hai pieno accesso alla memoria attraverso i puntatori, la posizione fisica delle variabili è significativa. Ci sono cose molto cattive che puoi sfruttare in base a dove si trova una variabile in un oggetto link.

Dubito che questi siano "perché" questa situazione sia sorta. Probabilmente è solo un'evoluzione di C che si trasforma in C ++ e un problema di compatibilità con le versioni precedenti che ti impedisce di cambiare la lingua ora.

    
risposta data 23.02.2017 - 07:21
fonte

Leggi altre domande sui tag