Risposte:
L'uso extern
è rilevante solo quando il programma che stai creando è costituito da più file di origine collegati tra loro, in cui è file1.c
necessario 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 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.c
e file2.c
:
extern int global_variable; /* Declaration of the variable */
#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++; }
#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). extern
Per coerenza, uso la parola chiave davanti alle dichiarazioni di funzione nelle intestazioni - per abbinare le extern
dichiarazioni di fronte alle variabili nelle intestazioni. Molte persone preferiscono non usare extern
davanti alle dichiarazioni di funzione; al compilatore non importa - e alla fine, nemmeno io fintanto che sei coerente, almeno all'interno di un file sorgente.
extern void use_it(void);
extern int increment(void);
#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;
}
prog1
usi prog1.c
, file1.c
, file2.c
, file3.h
e prog1.h
.Il file prog1.mk
è solo un makefile prog1
. Funzionerà con la maggior parte delle versioni di make
prodotti fin dalla fine del millennio. Non è legato in modo specifico a GNU Make.
# 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}
Le regole devono essere violate solo dagli esperti e solo con una buona ragione:
Un file di intestazione contiene solo extern
dichiarazioni di variabili - static
definizioni 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 extern
dichiarazioni 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.
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:
#include "prog2.h"
long l; /* Do not do this in portable code */
void inc(void) { l++; }
#include "prog2.h"
long l; /* Do not do this in portable code */
void dec(void) { l--; }
#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:
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).
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
sizeof
o il_Alignof
cui 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 l
come double
invece che come a long
, i linker non sicuri di C probabilmente non individuerebbero la mancata corrispondenza. Se sei su un computer con 64 bit long
e double
non riceveresti nemmeno un avviso; su una macchina con 32 long
e 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
:
extern void dec(void);
extern void put(void);
extern void inc(void);
#include "prog2.h"
#include <stdio.h>
int main(void)
{
inc();
put();
dec();
put();
dec();
put();
}
prog2
usi prog2.c
, file10.c
, file11.c
, file12.c
, prog2.h
.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.
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.
c int some_var; /* Do not do this in a header!!! */
Nota 1: se l'intestazione definisce la variabile senza la extern
parola 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à.
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.
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.
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.
Se non sei un programmatore esperto C, probabilmente dovresti smettere di leggere qui.
Aggiunta tardiva
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
:
#ifdef DEFINE_VARIABLES
#define EXTERN /* nothing */
#else
#define EXTERN extern
#endif /* DEFINE_VARIABLES */
EXTERN int global_variable;
#define DEFINE_VARIABLES
#include "file3a.h" /* Variable defined - but not initialized */
#include "prog3.h"
int increment(void) { return global_variable++; }
#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
:
extern void use_it(void);
extern int increment(void);
#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;
}
prog3
usi prog3.c
, file1a.c
, file2a.c
, file3a.h
, prog3.h
.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.)
#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 #if
e #else
blocchi, correzione del bug identificato da
Denis Kniazhev
#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; }
#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
è { 41
e 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.h
inclusa (anziché fileba.h
) per
Denis Kniazhev
I prossimi due file completano l'origine per prog4
:
extern int increment(void);
extern int oddball_value(void);
extern void use_them(void);
#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;
}
prog4
usi prog4.c
, file1b.c
, file2b.c
, prog4.h
, file3b.h
.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.h
include file3b.h
una definizione di tipo che non viene mostrato, e file1b.c
ha la necessità di utilizzare sia di intestazione file4b.h
e 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.h
prima di includere file3b.h
per generare le definizioni, ma le normali protezioni dell'intestazione su file3b.h
impedirebbero la reinclusione dell'intestazione.
Pertanto, è necessario includere il corpo file3b.h
al 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).
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.h
per 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.c
e file6c.c
includi direttamente l'intestazione file2c.h
più 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:
L'intestazione che definisce o dichiara le variabili globali non può di per sé definire alcun tipo.
Immediatamente prima di includere un'intestazione che dovrebbe definire le variabili, si definisce la macro DEFINE_VARIABLES.
L'intestazione che definisce o dichiara le variabili ha contenuti stilizzati.
#ifdef DEFINE_VARIABLES
#define EXTERN /* nothing */
#define INITIALIZE(...) = __VA_ARGS__
#else
#define EXTERN extern
#define INITIALIZE(...) /* nothing */
#endif /* DEFINE_VARIABLES */
#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 */
/* 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 */
#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; }
#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;
}
#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; }
#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
, prog6
e prog7
:
#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;
}
prog5
usi prog5.c
, file3c.c
, file4c.c
, file1c.h
, file2c.h
, external.h
.
prog6
usi prog5.c
, file5c.c
, file4c.c
, file1c.h
, file2c.h
, external.h
.
prog7
usi 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.h
in 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 #define
e #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"
#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_VARIABLES
in file2d.h
).
/* 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; }
/* 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 */
/* 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 prog8
e prog9
:
#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;
}
#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;
}
prog8
usi prog8.c
, file7c.c
, file9c.c
.
prog9
usi prog8.c
, file8c.c
, file9c.c
.
Tuttavia, nella pratica, è improbabile che si verifichino problemi, soprattutto se si seguono i consigli standard
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.h
e 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.c
e 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.)
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
.
Una extern
variabile è 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 .c
lime test1.c
e test2.c
. Se si definisce una variabile globale int test1_var;
in test1.c
e 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
extern int test1_var;
a int test1_var;
, il linker (gcc 5.4.0) passa comunque. Quindi, è extern
davvero necessario in questo caso?
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).
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.
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."
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.
L'aggiunta di una extern
trasforma una definizione di variabile in una dichiarazione di variabile . Vedi questa discussione su quale sia la differenza tra una dichiarazione e una definizione.
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.
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.
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>
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 extern
variabili.
Da ora in poi, è compito del linker creare il programma finale, ma le extern
informazioni 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 ++?
readelf
o nm
può 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 notExtern
sia 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;
!).
notExtern
era 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.
global_def
della variabile qui definita e extern_ref
della 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.
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ò static
che indica al compilatore che questa variabile o funzione non può essere utilizzata al di fuori di questo modulo.
Prima di tutto, la extern
parola 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.
extern
viene 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.
extern
viene utilizzato in modo che un first.c
file possa avere pieno accesso a un parametro globale in un altro second.c
file.
La extern
può essere dichiarata nel first.c
file o in uno qualsiasi dei file di intestazione first.c
include.
extern
dichiarazione 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.c
per 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.
Con xc8 devi stare attento a dichiarare una variabile dello stesso tipo in ogni file che potresti, erroneamente, dichiarare qualcosa int
in un file e char
dire 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 gotcha
quando 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!
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