Come posso usare extern per condividere variabili tra i file sorgente?


988

So che le variabili globali in C a volte hanno la externparola chiave. Cos'è una externvariabile? Com'è la dichiarazione? Qual è il suo scopo?

Ciò è correlato alla condivisione di variabili tra i file di origine, ma come funziona esattamente? Dove uso extern?

Risposte:


1752

L'uso externè rilevante solo quando il programma che stai creando è costituito da più file di origine collegati tra loro, in cui è file1.cnecessario fare riferimento ad alcune delle variabili definite, ad esempio, nel file di origine in altri file di origine, ad esempio file2.c.

È importante comprendere la differenza tra la definizione di una variabile e la dichiarazione di una variabile :

  • Una variabile viene dichiarata quando il compilatore viene informato dell'esistenza di una variabile (e questo è il suo tipo); non alloca la memoria per la variabile in quel punto.

  • Una variabile viene definita quando il compilatore alloca la memoria per la variabile.

Puoi dichiarare una variabile più volte (anche se una volta è sufficiente); puoi definirlo una sola volta all'interno di un determinato ambito. Una definizione di variabile è anche una dichiarazione, ma non tutte le dichiarazioni di variabili sono definizioni.

Il modo migliore per dichiarare e definire variabili globali

Il modo pulito e affidabile per dichiarare e definire le variabili globali è utilizzare un file di intestazione per contenere una extern dichiarazione della variabile.

L'intestazione è inclusa da un file di origine che definisce la variabile e da tutti i file di origine che fanno riferimento alla variabile. Per ciascun programma, un file sorgente (e solo un file sorgente) definisce la variabile. Allo stesso modo, un file di intestazione (e solo un file di intestazione) dovrebbe dichiarare la variabile. Il file di intestazione è cruciale; consente il controllo incrociato tra TU indipendenti (unità di traduzione - pensa ai file sorgente) e garantisce coerenza.

Sebbene ci siano altri modi per farlo, questo metodo è semplice e affidabile. Lo dimostra file3.h, file1.ce file2.c:

file3.h

extern int global_variable;  /* Declaration of the variable */

file1.c

#include "file3.h"  /* Declaration made available here */
#include "prog1.h"  /* Function declarations */

/* Variable defined here */
int global_variable = 37;    /* Definition checked against declaration */

int increment(void) { return global_variable++; }

file2.c

#include "file3.h"
#include "prog1.h"
#include <stdio.h>

void use_it(void)
{
    printf("Global variable: %d\n", global_variable++);
}

Questo è il modo migliore per dichiarare e definire variabili globali.


I prossimi due file completano l'origine per prog1:

I programmi completi mostrati usano le funzioni, quindi si sono insinuate le dichiarazioni delle funzioni. Sia C99 che C11 richiedono che le funzioni siano dichiarate o definite prima di essere usate (mentre C90 non lo ha fatto, per buoni motivi). externPer coerenza, uso la parola chiave davanti alle dichiarazioni di funzione nelle intestazioni - per abbinare le externdichiarazioni di fronte alle variabili nelle intestazioni. Molte persone preferiscono non usare externdavanti alle dichiarazioni di funzione; al compilatore non importa - e alla fine, nemmeno io fintanto che sei coerente, almeno all'interno di un file sorgente.

prog1.h

extern void use_it(void);
extern int increment(void);

prog1.c

#include "file3.h"
#include "prog1.h"
#include <stdio.h>

int main(void)
{
    use_it();
    global_variable += 19;
    use_it();
    printf("Increment: %d\n", increment());
    return 0;
}
  • prog1usi prog1.c, file1.c, file2.c, file3.he prog1.h.

Il file prog1.mkè solo un makefile prog1. Funzionerà con la maggior parte delle versioni di makeprodotti fin dalla fine del millennio. Non è legato in modo specifico a GNU Make.

prog1.mk

# Minimal makefile for prog1

PROGRAM = prog1
FILES.c = prog1.c file1.c file2.c
FILES.h = prog1.h file3.h
FILES.o = ${FILES.c:.c=.o}

CC      = gcc
SFLAGS  = -std=c11
GFLAGS  = -g
OFLAGS  = -O3
WFLAG1  = -Wall
WFLAG2  = -Wextra
WFLAG3  = -Werror
WFLAG4  = -Wstrict-prototypes
WFLAG5  = -Wmissing-prototypes
WFLAGS  = ${WFLAG1} ${WFLAG2} ${WFLAG3} ${WFLAG4} ${WFLAG5}
UFLAGS  = # Set on command line only

CFLAGS  = ${SFLAGS} ${GFLAGS} ${OFLAGS} ${WFLAGS} ${UFLAGS}
LDFLAGS =
LDLIBS  =

all:    ${PROGRAM}

${PROGRAM}: ${FILES.o}
    ${CC} -o $@ ${CFLAGS} ${FILES.o} ${LDFLAGS} ${LDLIBS}

prog1.o: ${FILES.h}
file1.o: ${FILES.h}
file2.o: ${FILES.h}

# If it exists, prog1.dSYM is a directory on macOS DEBRIS = a.out core *~ *.dSYM RM_FR = rm -fr 

clean:
    ${RM_FR} ${FILES.o} ${PROGRAM} ${DEBRIS}


Linee guida

Le regole devono essere violate solo dagli esperti e solo con una buona ragione:

  • Un file di intestazione contiene solo externdichiarazioni di variabili - staticdefinizioni di variabili mai o non qualificate.

  • Per ogni data variabile, solo un file di intestazione lo dichiara (SPOT - Single Point of Truth).

  • Un file di origine non contiene mai externdichiarazioni di variabili: i file di origine includono sempre l'intestazione (unica) che le dichiara.

  • Per ogni data variabile, esattamente un file sorgente definisce la variabile, preferibilmente inizializzandola anch'essa. (Sebbene non sia necessario inizializzare esplicitamente a zero, non fa alcun danno e può fare del bene, perché in un programma può esserci solo una definizione inizializzata di una particolare variabile globale).

  • Il file di origine che definisce la variabile include anche l'intestazione per garantire che la definizione e la dichiarazione siano coerenti.

  • Una funzione non dovrebbe mai aver bisogno di dichiarare una variabile usando extern.

  • Evita le variabili globali quando possibile - usa invece le funzioni.

Il codice sorgente e il testo di questa risposta sono disponibili nel mio repository SOQ (Stack Overflow Questions) su GitHub nella sottodirectory src / so-0143-3204 .

Se non sei un programmatore esperto, potresti (e forse dovresti) smettere di leggere qui.

Non è un buon modo per definire le variabili globali

Con alcuni (in effetti, molti) compilatori C, puoi cavartela anche con quella che viene definita una definizione 'comune' di una variabile. 'Comune', qui, si riferisce a una tecnica usata in Fortran per condividere variabili tra file sorgente, usando un blocco COMMON (possibilmente chiamato). Quello che succede qui è che ognuno di un numero di file fornisce una definizione provvisoria della variabile. Finché non più di un file fornisce una definizione inizializzata, i vari file finiscono per condividere una singola definizione comune della variabile:

file10.c

#include "prog2.h"

long l; /* Do not do this in portable code */ 

void inc(void) { l++; }

file11.c

#include "prog2.h"

long l; /* Do not do this in portable code */ 

void dec(void) { l--; }

file12.c

#include "prog2.h"
#include <stdio.h>

long l = 9; /* Do not do this in portable code */ 

void put(void) { printf("l = %ld\n", l); }

Questa tecnica non è conforme alla lettera dello standard C e alla "regola di una definizione" - è un comportamento ufficialmente indefinito:

J.2 Comportamento indefinito

Viene utilizzato un identificatore con collegamento esterno, ma nel programma non esiste esattamente una definizione esterna per l'identificatore, oppure l'identificatore non viene utilizzato e esistono più definizioni esterne per l'identificatore (6.9).

§6.9 Definizioni esterne ¶5

Una definizione esterna è una dichiarazione esterna che è anche una definizione di una funzione (diversa da una definizione incorporata) o di un oggetto. Se un identificatore dichiarato con collegamento esterno viene utilizzato in un'espressione (tranne che come parte dell'operando di un operatore sizeofo il _Alignofcui risultato è una costante intera), da qualche parte nell'intero programma deve esserci esattamente una definizione esterna per l'identificatore; in caso contrario, non ci deve essere più di uno. 161)

161) Pertanto, se un identificatore dichiarato con collegamento esterno non viene utilizzato in un'espressione, non è necessario che sia presente una definizione esterna.

Tuttavia, la norma C lo elenca anche nell'allegato informativo J come una delle estensioni comuni .

J.5.11 Definizioni esterne multiple

Potrebbe esserci più di una definizione esterna per l'identificatore di un oggetto, con o senza l'uso esplicito della parola chiave extern; se le definizioni non sono d'accordo, o se ne inizializza più di una, il comportamento non è definito (6.9.2).

Poiché questa tecnica non è sempre supportata, è meglio evitare di usarla, soprattutto se il codice deve essere portatile . Usando questa tecnica, puoi anche finire con una puntura di tipo non intenzionale.

Se uno dei file sopra dichiarati lcome doubleinvece che come a long, i linker non sicuri di C probabilmente non individuerebbero la mancata corrispondenza. Se sei su un computer con 64 bit longe doublenon riceveresti nemmeno un avviso; su una macchina con 32 longe 64 bit double, probabilmente riceveresti un avviso sulle diverse dimensioni: il linker userebbe la dimensione più grande, esattamente come un programma Fortran prenderebbe la dimensione più grande di qualsiasi blocco comune.

Si noti che GCC 10.1.0, che è stato rilasciato il 2020-05-07, modifica le opzioni di compilazione predefinite da utilizzare -fno-common, il che significa che, per impostazione predefinita, il codice sopra non si collega più a meno che non si ignori l'impostazione predefinita con -fcommon(o si utilizzino gli attributi, ecc. vedi il link).


I prossimi due file completano l'origine per prog2:

prog2.h

extern void dec(void);
extern void put(void);
extern void inc(void);

prog2.c

#include "prog2.h"
#include <stdio.h>

int main(void)
{
    inc();
    put();
    dec();
    put();
    dec();
    put();
}
  • prog2usi prog2.c, file10.c, file11.c, file12.c, prog2.h.

avvertimento

Come notato nei commenti qui, e come affermato nella mia risposta a una domanda simile , l'uso di più definizioni per una variabile globale porta a un comportamento indefinito (J.2; §6.9), che è il modo standard di dire "tutto potrebbe accadere". Una delle cose che può succedere è che il programma si comporti come previsto; e J.5.11 dice approssimativamente "potresti essere fortunato più spesso di quanto meriti". Ma un programma che si basa su più definizioni di una variabile esterna - con o senza la parola chiave esplicita "esterna" - non è un programma strettamente conforme e non garantisce che funzioni ovunque. Equivalentemente: contiene un bug che può o meno mostrarsi.

Violare le linee guida

Esistono, ovviamente, molti modi in cui queste linee guida possono essere infrante. Occasionalmente, potrebbe esserci una buona ragione per infrangere le linee guida, ma tali occasioni sono estremamente insolite.

faulty_header.h

c int some_var; /* Do not do this in a header!!! */

Nota 1: se l'intestazione definisce la variabile senza la externparola chiave, ogni file che include l'intestazione crea una definizione provvisoria della variabile. Come notato in precedenza, questo spesso funzionerà, ma lo standard C non garantisce che funzionerà.

broken_header.h

c int some_var = 13; /* Only one source file in a program can use this */

Nota 2: se l'intestazione definisce e inizializza la variabile, solo un file di origine in un determinato programma può utilizzare l'intestazione. Poiché le intestazioni sono principalmente per la condivisione di informazioni, è un po 'sciocco crearne uno che può essere utilizzato solo una volta.

seldom_correct.h

c static int hidden_global = 3; /* Each source file gets its own copy */

Nota 3: se l'intestazione definisce una variabile statica (con o senza inizializzazione), ogni file sorgente finisce con la propria versione privata della variabile 'globale'.

Se la variabile è in realtà un array complesso, ad esempio, ciò può portare a un'estrema duplicazione del codice. Può, molto occasionalmente, essere un modo sensato per ottenere qualche effetto, ma è molto insolito.


Sommario

Usa la tecnica dell'intestazione che ho mostrato per prima. Funziona in modo affidabile e ovunque. Si noti, in particolare, che l'intestazione che dichiara il global_variableè inclusa in ogni file che lo utilizza, incluso quello che lo definisce. Questo assicura che tutto sia coerente.

Preoccupazioni simili sorgono con la dichiarazione e la definizione di funzioni - si applicano regole analoghe. Ma la domanda riguardava specificamente le variabili, quindi ho mantenuto la risposta solo alle variabili.

Fine della risposta originale

Se non sei un programmatore esperto C, probabilmente dovresti smettere di leggere qui.


Aggiunta tardiva

Evitare la duplicazione del codice

Una preoccupazione che a volte (e legittimamente) viene sollevata in merito al meccanismo "dichiarazioni nelle intestazioni, definizioni nella fonte" qui descritte è che ci sono due file da mantenere sincronizzati: l'intestazione e la fonte. Questo di solito è seguito da un'osservazione che una macro può essere utilizzata in modo che l'intestazione serva a doppio dovere - normalmente dichiarando le variabili, ma quando viene impostata una macro specifica prima che sia inclusa l'intestazione, definisce invece le variabili.

Un'altra preoccupazione può essere che le variabili debbano essere definite in ciascuno dei numerosi "programmi principali". Questa è normalmente una preoccupazione falsa; puoi semplicemente introdurre un file sorgente C per definire le variabili e collegare il file oggetto prodotto con ciascuno dei programmi.

Uno schema tipico funziona in questo modo, usando la variabile globale originale illustrata in file3.h:

file3a.h

#ifdef DEFINE_VARIABLES
#define EXTERN /* nothing */
#else
#define EXTERN extern
#endif /* DEFINE_VARIABLES */

EXTERN int global_variable;

file1a.c

#define DEFINE_VARIABLES
#include "file3a.h"  /* Variable defined - but not initialized */
#include "prog3.h"

int increment(void) { return global_variable++; }

file2a.c

#include "file3a.h"
#include "prog3.h"
#include <stdio.h>

void use_it(void)
{
    printf("Global variable: %d\n", global_variable++);
}

I prossimi due file completano l'origine per prog3:

prog3.h

extern void use_it(void);
extern int increment(void);

prog3.c

#include "file3a.h"
#include "prog3.h"
#include <stdio.h>

int main(void)
{
    use_it();
    global_variable += 19;
    use_it();
    printf("Increment: %d\n", increment());
    return 0;
}
  • prog3usi prog3.c, file1a.c, file2a.c, file3a.h, prog3.h.

Inizializzazione variabile

Il problema con questo schema come mostrato è che non prevede l'inizializzazione della variabile globale. Con C99 o C11 e gli elenchi di argomenti variabili per le macro, è possibile definire una macro per supportare anche l'inizializzazione. (Con C89 e nessun supporto per gli elenchi di argomenti variabili nelle macro, non esiste un modo semplice per gestire inizializzatori arbitrariamente lunghi.)

file3b.h

#ifdef DEFINE_VARIABLES
#define EXTERN                  /* nothing */
#define INITIALIZER(...)        = __VA_ARGS__
#else
#define EXTERN                  extern
#define INITIALIZER(...)        /* nothing */
#endif /* DEFINE_VARIABLES */

EXTERN int global_variable INITIALIZER(37);
EXTERN struct { int a; int b; } oddball_struct INITIALIZER({ 41, 43 });

Contenuto inverso #ife #elseblocchi, correzione del bug identificato da Denis Kniazhev

file1b.c

#define DEFINE_VARIABLES
#include "file3b.h"  /* Variables now defined and initialized */
#include "prog4.h"

int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }

file2b.c

#include "file3b.h"
#include "prog4.h"
#include <stdio.h>

void use_them(void)
{
    printf("Global variable: %d\n", global_variable++);
    oddball_struct.a += global_variable;
    oddball_struct.b -= global_variable / 2;
}

Chiaramente, il codice per la struttura di oddball non è quello che scriveresti normalmente, ma illustra il punto. Il primo argomento alla seconda invocazione di INITIALIZERè { 41e quello rimanente (singolare in questo esempio) è 43 }. Senza C99 o supporto simile per gli elenchi di argomenti variabili per le macro, gli inizializzatori che devono contenere virgole sono molto problematici.

Intestazione corretta file3b.hinclusa (anziché fileba.h) per Denis Kniazhev


I prossimi due file completano l'origine per prog4:

prog4.h

extern int increment(void);
extern int oddball_value(void);
extern void use_them(void);

prog4.c

#include "file3b.h"
#include "prog4.h"
#include <stdio.h>

int main(void)
{
    use_them();
    global_variable += 19;
    use_them();
    printf("Increment: %d\n", increment());
    printf("Oddball:   %d\n", oddball_value());
    return 0;
}
  • prog4usi prog4.c, file1b.c, file2b.c, prog4.h, file3b.h.

Guardie di intestazione

Qualsiasi intestazione deve essere protetta dalla reinclusione, in modo che le definizioni dei tipi (enum, struct o tipi di unione o typedef in genere) non causino problemi. La tecnica standard consiste nell'avvolgere il corpo dell'intestazione in una protezione dell'intestazione come:

#ifndef FILE3B_H_INCLUDED
#define FILE3B_H_INCLUDED

...contents of header...

#endif /* FILE3B_H_INCLUDED */

L'intestazione potrebbe essere inclusa due volte indirettamente. Ad esempio, se file4b.hinclude file3b.huna definizione di tipo che non viene mostrato, e file1b.cha la necessità di utilizzare sia di intestazione file4b.he file3b.h, quindi si dispone di alcuni problemi più difficili da risolvere. Chiaramente, potresti rivedere l'elenco delle intestazioni per includere solo file4b.h. Tuttavia, potresti non essere a conoscenza delle dipendenze interne e, idealmente, il codice dovrebbe continuare a funzionare.

Inoltre, inizia a diventare complicato perché potresti includere file4b.hprima di includere file3b.hper generare le definizioni, ma le normali protezioni dell'intestazione su file3b.himpedirebbero la reinclusione dell'intestazione.

Pertanto, è necessario includere il corpo file3b.hal massimo una volta per le dichiarazioni e al massimo una volta per le definizioni, ma potrebbe essere necessario sia in una singola unità di traduzione (TU - una combinazione di un file sorgente che delle intestazioni che utilizza).

Inclusione multipla con definizioni variabili

Tuttavia, può essere fatto soggetto a un vincolo non troppo irragionevole. Introduciamo un nuovo set di nomi di file:

  • external.h per le definizioni macro EXTERN, ecc.

  • file1c.hper definire i tipi (in particolare struct oddball, il tipo di oddball_struct).

  • file2c.h per definire o dichiarare le variabili globali.

  • file3c.c che definisce le variabili globali.

  • file4c.c che utilizza semplicemente le variabili globali.

  • file5c.c che mostra che è possibile dichiarare e quindi definire le variabili globali.

  • file6c.c che mostra che è possibile definire e quindi (tentare di) dichiarare le variabili globali.

In questi esempi, file5c.ce file6c.cincludi direttamente l'intestazione file2c.hpiù volte, ma questo è il modo più semplice per mostrare che il meccanismo funziona. Significa che se l'intestazione fosse inclusa indirettamente due volte, sarebbe anche sicura.

Le restrizioni per far funzionare tutto questo sono:

  1. L'intestazione che definisce o dichiara le variabili globali non può di per sé definire alcun tipo.

  2. Immediatamente prima di includere un'intestazione che dovrebbe definire le variabili, si definisce la macro DEFINE_VARIABLES.

  3. L'intestazione che definisce o dichiara le variabili ha contenuti stilizzati.

external.h


#ifdef DEFINE_VARIABLES
#define EXTERN              /* nothing */
#define INITIALIZE(...)     = __VA_ARGS__
#else
#define EXTERN              extern
#define INITIALIZE(...)     /* nothing */
#endif /* DEFINE_VARIABLES */

file1c.h

#ifndef FILE1C_H_INCLUDED
#define FILE1C_H_INCLUDED

struct oddball
{
    int a;
    int b;
};

extern void use_them(void);
extern int increment(void);
extern int oddball_value(void);

#endif /* FILE1C_H_INCLUDED */

file2c.h


/* Standard prologue */
#if defined(DEFINE_VARIABLES) && !defined(FILE2C_H_DEFINITIONS)
#undef FILE2C_H_INCLUDED
#endif

#ifndef FILE2C_H_INCLUDED
#define FILE2C_H_INCLUDED

#include "external.h"   /* Support macros EXTERN, INITIALIZE */
#include "file1c.h"     /* Type definition for struct oddball */

#if !defined(DEFINE_VARIABLES) || !defined(FILE2C_H_DEFINITIONS)

/* Global variable declarations / definitions */
EXTERN int global_variable INITIALIZE(37);
EXTERN struct oddball oddball_struct INITIALIZE({ 41, 43 });

#endif /* !DEFINE_VARIABLES || !FILE2C_H_DEFINITIONS */

/* Standard epilogue */
#ifdef DEFINE_VARIABLES
#define FILE2C_H_DEFINITIONS
#endif /* DEFINE_VARIABLES */

#endif /* FILE2C_H_INCLUDED */

file3c.c

#define DEFINE_VARIABLES
#include "file2c.h"  /* Variables now defined and initialized */

int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }

file4c.c

#include "file2c.h"
#include <stdio.h>

void use_them(void)
{
    printf("Global variable: %d\n", global_variable++);
    oddball_struct.a += global_variable;
    oddball_struct.b -= global_variable / 2;
}

file5c.c


#include "file2c.h"     /* Declare variables */

#define DEFINE_VARIABLES
#include "file2c.h"  /* Variables now defined and initialized */

int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }

file6c.c


#define DEFINE_VARIABLES
#include "file2c.h"     /* Variables now defined and initialized */

#include "file2c.h"     /* Declare variables */

int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }

Il file sorgente successivo completa la fonte (fornisce un programma principale) per prog5, prog6e prog7:

prog5.c

#include "file2c.h"
#include <stdio.h>

int main(void)
{
    use_them();
    global_variable += 19;
    use_them();
    printf("Increment: %d\n", increment());
    printf("Oddball:   %d\n", oddball_value());
    return 0;
}
  • prog5usi prog5.c, file3c.c, file4c.c, file1c.h, file2c.h, external.h.

  • prog6usi prog5.c, file5c.c, file4c.c, file1c.h, file2c.h, external.h.

  • prog7usi prog5.c, file6c.c, file4c.c, file1c.h, file2c.h, external.h.


Questo schema evita la maggior parte dei problemi. Si verifica un problema solo se un'intestazione che definisce le variabili (come file2c.h) è inclusa in un'altra intestazione (diciamo file7c.h) che definisce le variabili. Non c'è un modo semplice per aggirare questo se non "non farlo".

È possibile risolvere parzialmente il problema modificando file2c.hin file2d.h:

file2d.h

/* Standard prologue */
#if defined(DEFINE_VARIABLES) && !defined(FILE2D_H_DEFINITIONS)
#undef FILE2D_H_INCLUDED
#endif

#ifndef FILE2D_H_INCLUDED
#define FILE2D_H_INCLUDED

#include "external.h"   /* Support macros EXTERN, INITIALIZE */
#include "file1c.h"     /* Type definition for struct oddball */

#if !defined(DEFINE_VARIABLES) || !defined(FILE2D_H_DEFINITIONS)

/* Global variable declarations / definitions */
EXTERN int global_variable INITIALIZE(37);
EXTERN struct oddball oddball_struct INITIALIZE({ 41, 43 });

#endif /* !DEFINE_VARIABLES || !FILE2D_H_DEFINITIONS */

/* Standard epilogue */
#ifdef DEFINE_VARIABLES
#define FILE2D_H_DEFINITIONS
#undef DEFINE_VARIABLES
#endif /* DEFINE_VARIABLES */

#endif /* FILE2D_H_INCLUDED */

Il problema diventa "dovrebbe includere l'intestazione #undef DEFINE_VARIABLES?" Se lo ometti dall'intestazione e avvolgi qualsiasi invocazione di definizione con #definee #undef:

#define DEFINE_VARIABLES
#include "file2c.h"
#undef DEFINE_VARIABLES

nel codice sorgente (quindi le intestazioni non modificano mai il valore di DEFINE_VARIABLES), quindi dovresti essere pulito. È solo una seccatura dover ricordare di scrivere la riga in più. Un'alternativa potrebbe essere:

#define HEADER_DEFINING_VARIABLES "file2c.h"
#include "externdef.h"

externdef.h


#if defined(HEADER_DEFINING_VARIABLES)
#define DEFINE_VARIABLES
#include HEADER_DEFINING_VARIABLES
#undef DEFINE_VARIABLES
#undef HEADER_DEFINING_VARIABLES
#endif /* HEADER_DEFINING_VARIABLES */

Questo sta diventando un po 'contorto, ma sembra essere sicuro (usando il file2d.h, con nessun #undef DEFINE_VARIABLESin file2d.h).

file7c.c

/* Declare variables */
#include "file2d.h"

/* Define variables */
#define HEADER_DEFINING_VARIABLES "file2d.h"
#include "externdef.h"

/* Declare variables - again */
#include "file2d.h"

/* Define variables - again */
#define HEADER_DEFINING_VARIABLES "file2d.h"
#include "externdef.h"

int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }

file8c.h

/* Standard prologue */
#if defined(DEFINE_VARIABLES) && !defined(FILE8C_H_DEFINITIONS)
#undef FILE8C_H_INCLUDED
#endif

#ifndef FILE8C_H_INCLUDED
#define FILE8C_H_INCLUDED

#include "external.h"   /* Support macros EXTERN, INITIALIZE */
#include "file2d.h"     /* struct oddball */

#if !defined(DEFINE_VARIABLES) || !defined(FILE8C_H_DEFINITIONS)

/* Global variable declarations / definitions */
EXTERN struct oddball another INITIALIZE({ 14, 34 });

#endif /* !DEFINE_VARIABLES || !FILE8C_H_DEFINITIONS */

/* Standard epilogue */
#ifdef DEFINE_VARIABLES
#define FILE8C_H_DEFINITIONS
#endif /* DEFINE_VARIABLES */

#endif /* FILE8C_H_INCLUDED */

file8c.c

/* Define variables */
#define HEADER_DEFINING_VARIABLES "file2d.h"
#include "externdef.h"

/* Define variables */
#define HEADER_DEFINING_VARIABLES "file8c.h"
#include "externdef.h"

int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }

I prossimi due file completano l'origine per prog8e prog9:

prog8.c

#include "file2d.h"
#include <stdio.h>

int main(void)
{
    use_them();
    global_variable += 19;
    use_them();
    printf("Increment: %d\n", increment());
    printf("Oddball:   %d\n", oddball_value());
    return 0;
}

file9c.c

#include "file2d.h"
#include <stdio.h>

void use_them(void)
{
    printf("Global variable: %d\n", global_variable++);
    oddball_struct.a += global_variable;
    oddball_struct.b -= global_variable / 2;
}
  • prog8usi prog8.c, file7c.c, file9c.c.

  • prog9usi prog8.c, file8c.c, file9c.c.


Tuttavia, nella pratica, è improbabile che si verifichino problemi, soprattutto se si seguono i consigli standard

Evita le variabili globali


Questa esposizione manca qualcosa?

Confessione : lo schema di "evitare il codice duplicato" delineato qui è stato sviluppato perché il problema riguarda alcuni codici su cui lavoro (ma non possiedo) ed è una preoccupazione preoccupante con lo schema delineato nella prima parte della risposta. Tuttavia, lo schema originale ti lascia solo due posti per modificare per mantenere sincronizzate le definizioni e le dichiarazioni delle variabili, il che è un grande passo avanti rispetto alla presenza di dichiarazioni di variabili esterne sparse in tutta la base di codice (che conta davvero quando ci sono migliaia di file in totale) . Tuttavia, il codice nei file con i nomi fileNc.[ch](più external.he externdef.h) mostra che può essere fatto funzionare. Chiaramente, non sarebbe difficile creare uno script del generatore di intestazione per darti il ​​modello standardizzato per una variabile che definisce e dichiara il file di intestazione.

NB Questi sono programmi giocattolo con un codice appena sufficiente per renderli marginalmente interessanti. C'è una ripetizione negli esempi che potrebbe essere rimossa, ma non è per semplificare la spiegazione pedagogica. (Ad esempio: la differenza tra prog5.ce prog8.cè il nome di una delle intestazioni incluse. Sarebbe possibile riorganizzare il codice in modo che la main()funzione non fosse ripetuta, ma nasconderebbe più di quanto rivelato.)


3
@litb: vedere l'allegato J.5.11 per la definizione comune - è un'estensione comune.
Jonathan Leffler,

3
@litb: e sono d'accordo che dovrebbe essere evitato - ecco perché è nella sezione "Non è un buon modo per definire le variabili globali".
Jonathan Leffler,

3
In effetti è un'estensione comune, ma è un comportamento indefinito per un programma di fare affidamento su di esso. Non ero chiaro se stavi dicendo che questo è consentito dalle stesse regole di C. Ora vedo che stai dicendo che è solo un'estensione comune e per evitarlo se hai bisogno che il tuo codice sia portatile. Quindi posso votarti senza dubbi. Davvero un'ottima risposta IMHO :)
Johannes Schaub - litb

19
Se ti fermi in cima, mantiene le cose semplici semplici. Mentre leggi più in basso, tratta di più sfumature, complicazioni e dettagli. Ho appena aggiunto due "punti di arresto precoce" per programmatori C meno esperti - o programmatori C che già conoscono l'argomento. Non è necessario leggere tutto se conosci già la risposta (ma fammi sapere se trovi un guasto tecnico).
Jonathan Leffler il

4
@supercat: Mi viene in mente che puoi usare i letterali di array C99 per ottenere un valore di enumerazione per le dimensioni dell'array, esemplificato da ( foo.h): #define FOO_INITIALIZER { 1, 2, 3, 4, 5 }per definire l'inizializzatore per l'array, enum { FOO_SIZE = sizeof((int [])FOO_INITIALIZER) / sizeof(((int [])FOO_INITIALIZER)[0]) };ottenere le dimensioni dell'array e extern int foo[];dichiarare l'array . Chiaramente, la definizione dovrebbe essere giusta int foo[FOO_SIZE] = FOO_INITIALIZER;, anche se la dimensione non deve essere inclusa nella definizione. In questo modo si ottiene una costante intera, FOO_SIZE.
Jonathan Leffler,

125

Una externvariabile è una dichiarazione (grazie a sbi per la correzione) di una variabile che è definita in un'altra unità di traduzione. Ciò significa che l'archiviazione per la variabile è allocata in un altro file.

Diciamo che avete due .clime test1.ce test2.c. Se si definisce una variabile globale int test1_var;in test1.ce si desidera accedere a questa variabile in test2.cè necessario utilizzare extern int test1_var;in test2.c.

Campione completo:

$ cat test1.c 
int test1_var = 5;
$ cat test2.c
#include <stdio.h>

extern int test1_var;

int main(void) {
    printf("test1_var = %d\n", test1_var);
    return 0;
}
$ gcc test1.c test2.c -o test
$ ./test
test1_var = 5

21
Non ci sono "pseudo-definizioni". È una dichiarazione
sabato

3
Nell'esempio sopra, se cambio extern int test1_var;a int test1_var;, il linker (gcc 5.4.0) passa comunque. Quindi, è externdavvero necessario in questo caso?
radiohead

2
@radiohead: Nella mia risposta , troverai le informazioni secondo cui il rilascio di externè un'estensione comune che spesso funziona - e in particolare funziona con GCC (ma GCC è lungi dall'essere l'unico compilatore che la supporta; è prevalente sui sistemi Unix). Puoi cercare "J.5.11" o la sezione "Non è un buon modo" nella mia risposta (lo so - è lungo) e il testo accanto lo spiega (o cerca di farlo).
Jonathan Leffler,

Una dichiarazione esterna certamente non deve essere definita in un'altra unità di traduzione (e comunemente non lo è). In effetti, la dichiarazione e la definizione possono essere la stessa cosa.
Ricorda Monica, il

40

Extern è la parola chiave che usi per dichiarare che la variabile stessa risiede in un'altra unità di traduzione.

Quindi puoi decidere di usare una variabile in un'unità di traduzione e quindi accedervi da un'altra, quindi nella seconda la dichiari come esterna e il simbolo verrà risolto dal linker.

Se non lo dichiari come esterno, otterrai 2 variabili denominate uguali ma non correlate affatto e un errore di più definizioni della variabile.


5
In altre parole, l'unità di traduzione in cui viene utilizzato extern conosce questa variabile, il suo tipo, ecc. E quindi consente al codice sorgente nella logica sottostante di usarla, ma non alloca la variabile, un'altra unità di traduzione lo farà. Se entrambe le unità di traduzione dovessero dichiarare la variabile normalmente, ci sarebbero effettivamente due posizioni fisiche per la variabile, con i riferimenti "errati" associati all'interno del codice compilato e con l'ambiguità risultante per il linker.
mjv,

26

Mi piace pensare a una variabile esterna come una promessa che fai al compilatore.

Quando incontra un esterno, il compilatore può solo scoprire il suo tipo, non dove "vive", quindi non può risolvere il riferimento.

Stai dicendo: "Fidati di me. Al momento del collegamento questo riferimento sarà risolvibile."


Più in generale, una dichiarazione promette che il nome sarà risolvibile in una definizione esattamente al momento del collegamento. Un extern dichiara una variabile senza definire.
Lie Ryan,

18

extern dice al compilatore di fidarsi di te che la memoria per questa variabile è dichiarata altrove, quindi non cerca di allocare / controllare la memoria.

Pertanto, è possibile compilare un file che fa riferimento a un esterno, ma non è possibile collegarsi se tale memoria non viene dichiarata da qualche parte.

Utile per variabili e librerie globali, ma pericoloso perché il linker non digita check.


La memoria non è dichiarata. Vedi le risposte a questa domanda: stackoverflow.com/questions/1410563 per maggiori dettagli.
sabato

15

L'aggiunta di una externtrasforma una definizione di variabile in una dichiarazione di variabile . Vedi questa discussione su quale sia la differenza tra una dichiarazione e una definizione.


Quale differenza tra int fooe extern int foo(ambito del file)? Entrambe sono dichiarazioni, no?

@ user14284: sono entrambe dichiarazioni solo nel senso che ogni definizione è anche una dichiarazione. Ma ho collegato a una spiegazione di questo. ("Vedi questa discussione su qual è la differenza tra una dichiarazione e una definizione.") Perché non segui semplicemente il link e leggi?
sabato

14
                 declare | define   | initialize |
                ----------------------------------

extern int a;    yes          no           no
-------------
int a = 2019;    yes          yes          yes
-------------
int a;           yes          yes          no
-------------

La dichiarazione non alloca la memoria (la variabile deve essere definita per l'allocazione della memoria) ma la definizione lo farà. Questa è solo un'altra semplice visione della parola chiave extern poiché le altre risposte sono davvero fantastiche.


11

L'interpretazione corretta di extern è che dici qualcosa al compilatore. Dite al compilatore che, nonostante non sia presente in questo momento, la variabile dichiarata sarà in qualche modo trovata dal linker (in genere in un altro oggetto (file)). Il linker sarà quindi il ragazzo fortunato a trovare tutto e metterlo insieme, sia che tu abbia delle dichiarazioni esterne o meno.


8

In C una variabile all'interno di un file dice esempio.c è dato ambito locale. Il compilatore si aspetta che la variabile abbia la sua definizione all'interno dello stesso file esempio.c e quando non trova lo stesso genererebbe un errore. D'altra parte, la funzione ha un ambito globale predefinito. Quindi non devi menzionare esplicitamente il compilatore "guarda amico ... potresti trovare qui la definizione di questa funzione". Per una funzione che include il file che contiene la sua dichiarazione è sufficiente (il file che in realtà si chiama un file di intestazione). Ad esempio, considerare i seguenti 2 file:
esempio.c

#include<stdio.h>
extern int a;
main(){
       printf("The value of a is <%d>\n",a);
}

example1.c

int a = 5;

Ora quando compili i due file insieme, usando i seguenti comandi:

passaggio 1) cc -o ex esempio.c esempio1.c passaggio 2) ./ ex

Si ottiene il seguente output: Il valore di a è <5>


8

Implementazione GCC ELF Linux

Altre risposte hanno riguardato il lato dell'utilizzo della lingua, quindi ora diamo un'occhiata a come viene implementato in questa implementazione.

main.c

#include <stdio.h>

int not_extern_int = 1;
extern int extern_int;

void main() {
    printf("%d\n", not_extern_int);
    printf("%d\n", extern_int);
}

Compilare e decompilare:

gcc -c main.c
readelf -s main.o

L'output contiene:

Num:    Value          Size Type    Bind   Vis      Ndx Name
 9: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 not_extern_int
12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND extern_int

Il capitolo "Tabella dei simboli" delle specifiche ELF sull'aggiornamento ABI di System V spiega:

SHN_UNDEF Questo indice della tabella di sezione indica che il simbolo non è definito. Quando l'editor di collegamenti combina questo file oggetto con un altro che definisce il simbolo indicato, i riferimenti di questo file al simbolo verranno collegati alla definizione effettiva.

che è fondamentalmente il comportamento che lo standard C attribuisce alle externvariabili.

Da ora in poi, è compito del linker creare il programma finale, ma le externinformazioni sono già state estratte dal codice sorgente nel file oggetto.

Testato su GCC 4.8.

Variabili inline C ++ 17

In C ++ 17, potresti voler usare le variabili inline invece di quelle esterne, poiché sono semplici da usare (possono essere definite una sola volta nell'intestazione) e più potenti (support constexpr). Vedi: Cosa significa "const static" in C e C ++?


3
Non è il mio voto negativo, quindi non lo so. Tuttavia, ti darò un'opinione. Anche se guardare l'output di readelfo nmpuò essere utile, non hai spiegato i fondamenti di come utilizzarlo extern, né completato il primo programma con la definizione effettiva. Il tuo codice non usa nemmeno notExtern. C'è anche un problema di nomenclatura: sebbene notExternsia definito qui piuttosto che dichiarato con extern, è una variabile esterna a cui potrebbe accedere un altro file sorgente se quelle unità di traduzione contenessero una dichiarazione adatta (che avrebbe bisogno extern int notExtern;!).
Jonathan Leffler,

1
@JonathanLeffler grazie per il feedback! Il comportamento standard e le raccomandazioni sull'utilizzo sono già state fatte in altre risposte, quindi ho deciso di mostrare un po 'l'implementazione in quanto ciò mi ha davvero aiutato a capire cosa sta succedendo. Non usare notExternera brutto, risolto. A proposito di nomenclatura, fammi sapere se hai un nome migliore. Ovviamente non sarebbe un buon nome per un programma reale, ma penso che si adatti bene al ruolo didattico qui.
Ciro Santilli 2 冠状 病 六四 事件 法轮功

Quanto ai nomi, che dire global_defdella variabile qui definita e extern_refdella variabile definita in qualche altro modulo? Avrebbero una simmetria opportunamente chiara? Finisci ancora con int extern_ref = 57;qualcosa del genere nel file in cui è definito, quindi il nome non è del tutto ideale, ma nel contesto del singolo file sorgente, è una scelta ragionevole. Avere extern int global_def;in un'intestazione non è un grosso problema, mi sembra. Interamente da te, ovviamente.
Jonathan Leffler,

7

La parola chiave extern viene utilizzata con la variabile per la sua identificazione come variabile globale.

Rappresenta inoltre che è possibile utilizzare la variabile dichiarata utilizzando la parola chiave extern in qualsiasi file sebbene sia dichiarata / definita in un altro file.


5

extern consente a un modulo del programma di accedere a una variabile o funzione globale dichiarata in un altro modulo del programma. Di solito hai variabili esterne dichiarate nei file di intestazione.

Se non si desidera che un programma acceda alle proprie variabili o funzioni, si utilizza ciò staticche indica al compilatore che questa variabile o funzione non può essere utilizzata al di fuori di questo modulo.


5

extern significa semplicemente che una variabile è definita altrove (ad esempio, in un altro file).


4

Prima di tutto, la externparola chiave non viene utilizzata per definire una variabile; piuttosto è usato per dichiarare una variabile. Posso dire che externè una classe di archiviazione, non un tipo di dati.

externviene utilizzato per comunicare ad altri file C o componenti esterni che questa variabile è già definita da qualche parte. Esempio: se si sta creando una libreria, non è necessario definire obbligatoriamente una variabile globale da qualche parte nella libreria stessa. La libreria verrà compilata direttamente, ma durante il collegamento del file, controlla la definizione.


3

externviene utilizzato in modo che un first.cfile possa avere pieno accesso a un parametro globale in un altro second.cfile.

La externpuò essere dichiarata nel first.cfile o in uno qualsiasi dei file di intestazione first.cinclude.


3
Nota che la externdichiarazione dovrebbe essere in un header, non in first.c, quindi se il tipo cambia, anche la dichiarazione cambierà. Inoltre, l'intestazione che dichiara la variabile deve essere inclusa second.cper garantire che la definizione sia coerente con la dichiarazione. La dichiarazione nell'intestazione è la colla che tiene tutto insieme; consente di compilare i file separatamente ma garantisce che abbiano una visione coerente del tipo di variabile globale.
Jonathan Leffler,

2

Con xc8 devi stare attento a dichiarare una variabile dello stesso tipo in ogni file che potresti, erroneamente, dichiarare qualcosa intin un file e chardire in un altro. Ciò potrebbe portare alla corruzione delle variabili.

Questo problema è stato elegantemente risolto in un forum di microchip circa 15 anni fa / * Vedi "http: www.htsoft.com" / / "forum / all / showflat.php / Cat / 0 / Numero / 18766 / an / 0 / page / 0 # 18766"

Ma questo link sembra non funzionare più ...

Quindi cercherò presto di spiegarlo; crea un file chiamato global.h.

In esso dichiarare quanto segue

#ifdef MAIN_C
#define GLOBAL
 /* #warning COMPILING MAIN.C */
#else
#define GLOBAL extern
#endif
GLOBAL unsigned char testing_mode; // example var used in several C files

Ora nel file main.c

#define MAIN_C 1
#include "global.h"
#undef MAIN_C

Ciò significa che in main.c la variabile verrà dichiarata come unsigned char.

Ora in altri file semplicemente includendo global.h sarà dichiarato come esterno per quel file .

extern unsigned char testing_mode;

Ma sarà dichiarato correttamente come unsigned char.

Il vecchio post sul forum probabilmente lo ha spiegato un po 'più chiaramente. Ma questo è un potenziale reale gotchaquando si utilizza un compilatore che consente di dichiarare una variabile in un file e quindi dichiararlo esternamente come un tipo diverso in un altro. I problemi associati a questo sono se si dice dichiarando testing_mode come int in un altro file, si potrebbe pensare che fosse una var a 16 bit e sovrascrivere qualche altra parte di ram, corrompendo potenzialmente un'altra variabile. Difficile eseguire il debug!


0

Una soluzione molto breve che uso per consentire a un file di intestazione di contenere il riferimento esterno o l'implementazione effettiva di un oggetto. Il file che contiene effettivamente l'oggetto lo fa #define GLOBAL_FOO_IMPLEMENTATION. Quindi quando aggiungo un nuovo oggetto a questo file, questo appare anche in quel file senza che io debba copiare e incollare la definizione.

Uso questo modello su più file. Quindi, al fine di mantenere le cose il più autosufficienti possibile, ho appena riutilizzato la singola macro GLOBALE in ogni intestazione. La mia intestazione è simile a questa:

//file foo_globals.h
#pragma once  
#include "foo.h"  //contains definition of foo

#ifdef GLOBAL  
#undef GLOBAL  
#endif  

#ifdef GLOBAL_FOO_IMPLEMENTATION  
#define GLOBAL  
#else  
#define GLOBAL extern  
#endif  

GLOBAL Foo foo1;  
GLOBAL Foo foo2;


//file main.cpp
#define GLOBAL_FOO_IMPLEMENTATION
#include "foo_globals.h"

//file uses_extern_foo.cpp
#include "foo_globals.h
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.