Comment utiliser extern pour partager des variables entre des fichiers source?

988

Je sais que les variables globales en C ont parfois le externmot - clé. Qu'est-ce qu'une externvariable? À quoi ressemble la déclaration? Quelle est sa portée?

Cela est lié au partage de variables entre les fichiers source, mais comment cela fonctionne-t-il précisément? Où dois-je l'utiliser extern?

Lundin
la source

Réponses:

1752

L'utilisation externn'est pertinente que lorsque le programme que vous créez est composé de plusieurs fichiers source liés entre eux, où certaines des variables définies, par exemple, dans le fichier source file1.cdoivent être référencées dans d'autres fichiers source, tels que file2.c.

Il est important de comprendre la différence entre définir une variable et déclarer une variable :

  • Une variable est déclarée lorsque le compilateur est informé qu'une variable existe (et c'est son type); il n'alloue pas le stockage pour la variable à ce stade.

  • Une variable est définie lorsque le compilateur alloue le stockage pour la variable.

Vous pouvez déclarer une variable plusieurs fois (bien qu'une seule fois soit suffisante); vous ne pouvez le définir qu'une seule fois dans une portée donnée. Une définition de variable est également une déclaration, mais toutes les déclarations de variable ne sont pas des définitions.

Meilleure façon de déclarer et de définir des variables globales

Le moyen propre et fiable de déclarer et de définir des variables globales consiste à utiliser un fichier d'en-tête pour contenir une extern déclaration de la variable.

L'en-tête est inclus par le fichier source unique qui définit la variable et par tous les fichiers source qui référencent la variable. Pour chaque programme, un fichier source (et un seul fichier source) définit la variable. De même, un fichier d'en-tête (et un seul fichier d'en-tête) doit déclarer la variable. Le fichier d'en-tête est crucial; il permet le recoupement entre les unités de traitement indépendantes (unités de traduction - pensez aux fichiers source) et garantit la cohérence.

Bien qu'il existe d'autres façons de le faire, cette méthode est simple et fiable. Il est démontré par file3.h, file1.cet 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++);
}

C'est la meilleure façon de déclarer et de définir des variables globales.


Les deux fichiers suivants complètent la source de prog1:

Les programmes complets présentés utilisent des fonctions, donc les déclarations de fonctions se sont glissées. C99 et C11 exigent que les fonctions soient déclarées ou définies avant d'être utilisées (contrairement à C90, pour de bonnes raisons). J'utilise le mot-clé externdevant les déclarations de fonction dans les en-têtes pour la cohérence - pour faire correspondre le externdevant les déclarations de variables dans les en-têtes. Beaucoup de gens préfèrent ne pas utiliser externdevant les déclarations de fonction; le compilateur s'en fiche - et finalement, moi non plus tant que vous êtes cohérent, au moins dans un fichier source.

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;
}
  • prog1utilisations prog1.c, file1.c, file2.c, file3.het prog1.h.

Le fichier prog1.mkest un makefile pour prog1seulement. Il fonctionnera avec la plupart des versions de makeproduites depuis environ le tournant du millénaire. Il n'est pas lié spécifiquement à 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}

Des lignes directrices

Les règles doivent être enfreintes uniquement par des experts, et uniquement pour une bonne raison:

  • Un fichier d'en-tête contient uniquement des externdéclarations de variables - jamais staticou des définitions de variables non qualifiées.

  • Pour une variable donnée, un seul fichier d'en-tête la déclare (SPOT - Single Point of Truth).

  • Un fichier source ne contient jamais de externdéclarations de variables - les fichiers source incluent toujours l'en-tête (unique) qui les déclare.

  • Pour toute variable donnée, exactement un fichier source définit la variable, de préférence en l'initialisant également. (Bien qu'il ne soit pas nécessaire d'initialiser explicitement à zéro, cela ne nuit pas et peut faire du bien, car il ne peut y avoir qu'une seule définition initialisée d'une variable globale particulière dans un programme).

  • Le fichier source qui définit la variable comprend également l'en-tête pour garantir la cohérence de la définition et de la déclaration.

  • Une fonction ne devrait jamais avoir besoin de déclarer une variable à l'aide de extern.

  • Évitez autant que possible les variables globales - utilisez plutôt des fonctions.

Le code source et le texte de cette réponse sont disponibles dans mon référentiel SOQ (Stack Overflow Questions) sur GitHub dans le sous-répertoire src / so-0143-3204 .

Si vous n'êtes pas un programmeur C expérimenté, vous pouvez (et devriez peut-être) arrêter de lire ici.

Pas si bon moyen de définir des variables globales

Avec certains (en fait, de nombreux) compilateurs C, vous pouvez aussi vous passer de ce que l'on appelle une définition «commune» d'une variable. «Commun», ici, fait référence à une technique utilisée dans Fortran pour partager des variables entre des fichiers source, en utilisant un bloc COMMON (éventuellement nommé). Ce qui se passe ici, c'est que chacun d'un certain nombre de fichiers fournit une définition provisoire de la variable. Tant qu'un seul fichier fournit une définition initialisée, les différents fichiers finissent par partager une définition unique commune de la variable:

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); }

Cette technique n'est pas conforme à la lettre de la norme C et à la «règle d'une définition» - c'est un comportement officiellement indéfini:

J.2 Comportement indéfini

Un identifiant avec liaison externe est utilisé, mais dans le programme, il n'existe pas exactement une définition externe pour l'identifiant, ou l'identifiant n'est pas utilisé et il existe plusieurs définitions externes pour l'identifiant (6.9).

§6.9 Définitions externes ¶5

Une définition externe est une déclaration externe qui est également une définition d'une fonction (autre qu'une définition en ligne) ou d'un objet. Si un identifiant déclaré avec liaison externe est utilisé dans une expression (autre que dans le cadre de l'opérande d'un opérateur sizeofor _Alignofdont le résultat est une constante entière), quelque part dans le programme entier, il doit y avoir exactement une définition externe pour l'identifiant; sinon, il n'y en aura pas plus d'un. 161)

161) Ainsi, si un identifiant déclaré avec un lien externe n'est pas utilisé dans une expression, il n'y a pas besoin de définition externe pour celui-ci.

Cependant, la norme C la répertorie également dans l'annexe informative J comme l'une des extensions communes .

J.5.11 Plusieurs définitions externes

Il peut y avoir plusieurs définitions externes pour l'identifiant d'un objet, avec ou sans l'utilisation explicite du mot-clé extern; si les définitions sont en désaccord, ou si plusieurs sont initialisées, le comportement n'est pas défini (6.9.2).

Cette technique n'étant pas toujours prise en charge, il vaut mieux éviter de l'utiliser, surtout si votre code doit être portable . En utilisant cette technique, vous pouvez également vous retrouver avec une punition de type non intentionnelle.

Si l'un des fichiers ci-dessus était déclaré en ltant que doubleau lieu de en tant que long, les éditeurs de liens non sécurisés de type C ne détecteraient probablement pas le décalage. Si vous êtes sur une machine avec 64 bits longet double, vous ne recevrez même pas d'avertissement; sur une machine avec 32 bits longet 64 bits double, vous obtiendriez probablement un avertissement concernant les différentes tailles - l'éditeur de liens utiliserait la plus grande taille, exactement comme un programme Fortran prendrait la plus grande taille de tous les blocs communs.

Notez que GCC 10.1.0, qui a été publié le 2020-05-07, modifie les options de compilation par défaut à utiliser -fno-common, ce qui signifie que par défaut, le code ci-dessus n'est plus lié sauf si vous remplacez la valeur par défaut par -fcommon(ou utilisez des attributs, etc - voir le lien).


Les deux fichiers suivants complètent la source de 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();
}
  • prog2utilisations prog2.c, file10.c, file11.c, file12.c, prog2.h.

Attention

Comme indiqué dans les commentaires ici, et comme indiqué dans ma réponse à une question similaire , l'utilisation de plusieurs définitions pour une variable globale conduit à un comportement indéfini (J.2; §6.9), qui est la manière standard de dire "tout peut arriver". L'une des choses qui peuvent se produire est que le programme se comporte comme vous l'attendez; et J.5.11 dit, approximativement, "vous pourriez avoir de la chance plus souvent que vous ne le méritez". Mais un programme qui repose sur plusieurs définitions d'une variable externe - avec ou sans le mot clé explicite «extern» - n'est pas un programme strictement conforme et n'est pas garanti pour fonctionner partout. De manière équivalente: il contient un bug qui peut ou non apparaître.

Violation des directives

Il existe, bien sûr, de nombreuses manières de rompre ces directives. Parfois, il peut y avoir une bonne raison de ne pas respecter les directives, mais de telles occasions sont extrêmement inhabituelles.

faulty_header.h

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

Remarque 1: si l'en-tête définit la variable sans le externmot - clé, chaque fichier qui inclut l'en-tête crée une définition provisoire de la variable. Comme indiqué précédemment, cela fonctionnera souvent, mais la norme C ne garantit pas que cela fonctionnera.

broken_header.h

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

Remarque 2: si l'en-tête définit et initialise la variable, alors un seul fichier source dans un programme donné peut utiliser l'en-tête. Étant donné que les en-têtes sont principalement destinés au partage d'informations, il est un peu idiot d'en créer un qui ne peut être utilisé qu'une seule fois.

seldom_correct.h

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

Remarque 3: si l'en-tête définit une variable statique (avec ou sans initialisation), chaque fichier source se retrouve avec sa propre version privée de la variable 'globale'.

Si la variable est en fait un tableau complexe, par exemple, cela peut entraîner une duplication extrême du code. Cela peut, très occasionnellement, être un moyen sensé d'obtenir un certain effet, mais c'est très inhabituel.


Sommaire

Utilisez la technique d'en-tête que j'ai montrée en premier. Cela fonctionne de manière fiable et partout. Notez, en particulier, que l'en-tête déclarant le global_variableest inclus dans chaque fichier qui l'utilise - y compris celui qui le définit. Cela garantit que tout est cohérent.

Des préoccupations similaires se posent avec la déclaration et la définition de fonctions - des règles analogues s'appliquent. Mais la question portait spécifiquement sur les variables, j'ai donc gardé la réponse aux variables uniquement.

Fin de la réponse originale

Si vous n'êtes pas un programmeur C expérimenté, vous devriez probablement arrêter de lire ici.


Addition majeure tardive

Éviter la duplication de code

Une préoccupation qui est parfois (et légitimement) soulevée au sujet du mécanisme «déclarations dans les en-têtes, définitions dans la source» décrit ici est qu'il y a deux fichiers à maintenir synchronisés - l'en-tête et la source. Ceci est généralement suivi d'une observation selon laquelle une macro peut être utilisée pour que l'en-tête remplisse une double fonction - déclarant normalement les variables, mais lorsqu'une macro spécifique est définie avant que l'en-tête ne soit inclus, elle définit les variables à la place.

Une autre préoccupation peut être que les variables doivent être définies dans chacun d'un certain nombre de «programmes principaux». Il s'agit normalement d'une fausse préoccupation; vous pouvez simplement introduire un fichier source C pour définir les variables et lier le fichier objet produit avec chacun des programmes.

Un schéma typique fonctionne comme ceci, en utilisant la variable globale d'origine illustrée dans 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++);
}

Les deux fichiers suivants complètent la source de 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;
}
  • prog3utilisations prog3.c, file1a.c, file2a.c, file3a.h, prog3.h.

Initialisation variable

Le problème avec ce schéma, comme indiqué, est qu'il ne prévoit pas l'initialisation de la variable globale. Avec C99 ou C11 et les listes d'arguments variables pour les macros, vous pouvez également définir une macro pour prendre en charge l'initialisation. (Avec C89 et sans prise en charge des listes d'arguments variables dans les macros, il n'y a pas de moyen facile de gérer des initialiseurs arbitrairement longs.)

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 });

Inverser le contenu #ifet les #elseblocs, correction d'un bug identifié par 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;
}

De toute évidence, le code de la structure bizarre n'est pas ce que vous écririez normalement, mais il illustre le point. Le premier argument de la deuxième invocation de INITIALIZERis { 41et l'argument restant (singulier dans cet exemple) est 43 }. Sans C99 ou un support similaire pour les listes d'arguments variables pour les macros, les initialiseurs qui doivent contenir des virgules sont très problématiques.

En-tête correct file3b.hinclus (au lieu de fileba.h) par Denis Kniazhev


Les deux fichiers suivants complètent la source de 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;
}
  • prog4utilisations prog4.c, file1b.c, file2b.c, prog4.h, file3b.h.

Protecteurs d'en-tête

Tout en-tête doit être protégé contre la réinclusion, afin que les définitions de type (types énum, ​​structure ou union, ou typedefs en général) ne causent pas de problèmes. La technique standard consiste à envelopper le corps de l'en-tête dans une protection d'en-tête telle que:

#ifndef FILE3B_H_INCLUDED
#define FILE3B_H_INCLUDED

...contents of header...

#endif /* FILE3B_H_INCLUDED */

L'en-tête peut être inclus indirectement deux fois. Par exemple, si file4b.hinclut file3b.hpour une définition de type qui n'est pas affichée et file1b.cdoit utiliser à la fois l'en-tête file4b.het file3b.h, vous avez des problèmes plus délicats à résoudre. De toute évidence, vous pouvez réviser la liste des en-têtes pour n'en inclure que file4b.h. Cependant, vous pourriez ne pas être au courant des dépendances internes - et le code devrait, idéalement, continuer à fonctionner.

De plus, cela commence à devenir difficile car vous pouvez inclure file4b.havant d'inclure file3b.hpour générer les définitions, mais les gardes d'en-tête normaux file3b.hempêcheraient la ré-inclusion de l'en-tête.

Ainsi, vous devez inclure le corps d' file3b.hau plus une fois pour les déclarations et au plus une fois pour les définitions, mais vous pourriez avoir besoin des deux dans une seule unité de traduction (TU - une combinaison d'un fichier source et des en-têtes qu'il utilise).

Inclusion multiple avec définitions de variables

Cependant, cela peut être fait sous réserve d'une contrainte pas trop déraisonnable. Introduisons un nouvel ensemble de noms de fichiers:

  • external.h pour les définitions de macro EXTERNES, etc.

  • file1c.hpour définir des types (notamment struct oddball, le type de oddball_struct).

  • file2c.h pour définir ou déclarer les variables globales.

  • file3c.c qui définit les variables globales.

  • file4c.c qui utilise simplement les variables globales.

  • file5c.c ce qui montre que vous pouvez déclarer puis définir les variables globales.

  • file6c.c ce qui montre que vous pouvez définir puis (tenter de) déclarer les variables globales.

Dans ces exemples, file5c.cet file6c.cincluez directement l'en-tête file2c.hplusieurs fois, mais c'est le moyen le plus simple de montrer que le mécanisme fonctionne. Cela signifie que si l'en-tête était indirectement inclus deux fois, il serait également sûr.

Les restrictions pour que cela fonctionne sont:

  1. L'en-tête définissant ou déclarant les variables globales ne peut lui-même définir aucun type.

  2. Immédiatement avant d'inclure un en-tête qui doit définir des variables, vous définissez la macro DEFINE_VARIABLES.

  3. L'en-tête définissant ou déclarant les variables a un contenu stylisé.

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; }

Le fichier source suivant complète la source (fournit un programme principal) pour prog5, prog6et 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;
}
  • prog5utilisations prog5.c, file3c.c, file4c.c, file1c.h, file2c.h, external.h.

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

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


Ce schéma évite la plupart des problèmes. Vous ne rencontrez un problème que si un en-tête qui définit des variables (comme file2c.h) est inclus par un autre en-tête (par exemple file7c.h) qui définit des variables. Il n'y a pas de moyen facile de contourner cela autre que "ne le faites pas".

Vous pouvez travailler en partie autour du problème en révisant file2c.hen 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 */

Le problème devient "l'en-tête doit-il inclure #undef DEFINE_VARIABLES?" Si vous omettez cela dans l'en-tête et encapsulez toute invocation de définition avec #defineet #undef:

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

dans le code source (afin que les en-têtes ne modifient jamais la valeur de DEFINE_VARIABLES), alors vous devriez être propre. C'est juste une nuisance d'avoir à se rappeler d'écrire la ligne supplémentaire. Une alternative pourrait être:

#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 */

Cela devient un peu alambiqué, mais semble être sécurisé (en utilisant le file2d.h, avec aucun #undef DEFINE_VARIABLESdans le 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; }

Les deux fichiers suivants complètent la source de prog8et 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;
}
  • prog8utilisations prog8.c, file7c.c, file9c.c.

  • prog9utilisations prog8.c, file8c.c, file9c.c.


Cependant, il est peu probable que les problèmes surviennent dans la pratique, surtout si vous suivez les conseils

Évitez les variables globales


Cette exposition manque-t-elle quelque chose?

Confession : Le schéma `` éviter le code dupliqué '' décrit ici a été développé parce que le problème affecte du code sur lequel je travaille (mais qui ne m'appartient pas), et est une inquiétante préoccupation avec le schéma décrit dans la première partie de la réponse. Cependant, le schéma d'origine ne vous laisse que deux endroits à modifier pour garder les définitions et les déclarations de variables synchronisées, ce qui est un grand pas en avant par rapport aux déclarations de variables externes dispersées dans la base de code (ce qui compte vraiment quand il y a des milliers de fichiers au total) . Cependant, le code dans les fichiers avec les noms fileNc.[ch](plus external.het externdef.h) montre qu'il peut fonctionner. De toute évidence, il ne serait pas difficile de créer un script de générateur d'en-tête pour vous donner le modèle standardisé pour un fichier d'en-tête définissant et déclarant une variable.

NB Ce sont des programmes de jouets avec à peine assez de code pour les rendre marginalement intéressants. Il y a de la répétition dans les exemples qui pourraient être supprimés, mais ce n'est pas pour simplifier l'explication pédagogique. (Par exemple: la différence entre prog5.cet prog8.cest le nom de l'un des en-têtes inclus. Il serait possible de réorganiser le code afin que la main()fonction ne soit pas répétée, mais elle cacherait plus qu'elle ne révélait.)

Jonathan Leffler
la source
3
@litb: voir l'annexe J.5.11 pour la définition commune - il s'agit d'une extension commune.
Jonathan Leffler
3
@litb: et je suis d'accord que cela devrait être évité - c'est pourquoi c'est dans la section «Pas si bon moyen de définir des variables globales».
Jonathan Leffler
3
En effet, c'est une extension courante, mais c'est un comportement indéfini pour qu'un programme s'appuie dessus. Je ne savais tout simplement pas si vous disiez que cela était autorisé par les propres règles de C. Maintenant, je vois que vous dites que ce n'est qu'une extension courante et que vous devez l'éviter si vous avez besoin que votre code soit portable. Je peux donc vous voter sans doutes. Vraiment une excellente réponse à
mon humble avis
19
Si vous vous arrêtez au sommet, cela simplifie les choses simples. Comme vous le lisez plus loin, il traite de plus de nuances, de complications et de détails. Je viens d'ajouter deux «premiers points d'arrêt» pour les programmeurs C moins expérimentés - ou les programmeurs C qui connaissent déjà le sujet. Il n'est pas nécessaire de tout lire si vous connaissez déjà la réponse (mais faites-moi savoir si vous trouvez un défaut technique).
Jonathan Leffler
4
@supercat: Il me semble que vous pouvez utiliser des littéraux de tableau C99 pour obtenir une valeur d'énumération pour la taille du tableau, illustrée par ( foo.h): #define FOO_INITIALIZER { 1, 2, 3, 4, 5 }pour définir l'initialiseur du tableau, enum { FOO_SIZE = sizeof((int [])FOO_INITIALIZER) / sizeof(((int [])FOO_INITIALIZER)[0]) };pour obtenir la taille du tableau et extern int foo[];pour déclarer le tableau . De toute évidence, la définition doit être juste int foo[FOO_SIZE] = FOO_INITIALIZER;, bien que la taille ne doive pas vraiment être incluse dans la définition. Cela vous obtient une constante entière, FOO_SIZE.
Jonathan Leffler
125

Une externvariable est une déclaration (merci à sbi pour la correction) d'une variable qui est définie dans une autre unité de traduction. Cela signifie que le stockage de la variable est alloué dans un autre fichier.

Disons que vous avez deux .cfichiers test1.cet test2.c. Si vous définissez une variable globale int test1_var;dans test1.cet que vous souhaitez accéder à cette variable, test2.cvous devez l'utiliser extern int test1_var;dans test2.c.

Échantillon complet:

$ 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
Johannes Weiss
la source
21
Il n'y a pas de "pseudo-définitions". C'est une déclaration.
sbi
3
Dans l'exemple ci-dessus, si je change le extern int test1_var;en int test1_var;, l'éditeur de liens (gcc 5.4.0) passe toujours. Alors, est-ce externvraiment nécessaire dans ce cas?
radiohead
2
@radiohead: Dans ma réponse , vous trouverez les informations selon lesquelles la suppression du externest une extension courante qui fonctionne souvent - et fonctionne spécifiquement avec GCC (mais GCC est loin d'être le seul compilateur qui le prend en charge; il est répandu sur les systèmes Unix). Vous pouvez chercher « J.5.11 » ou la section « Pas si bon chemin » dans ma réponse (je sais - il est long) et le texte près qui explique (ou tente de le faire).
Jonathan Leffler
Une déclaration externe n'a certainement pas à être définie dans une autre unité de traduction (et ce n'est généralement pas le cas). En fait, la déclaration et la définition peuvent être identiques.
Rappelez
40

Extern est le mot-clé que vous utilisez pour déclarer que la variable elle-même réside dans une autre unité de traduction.

Vous pouvez donc décider d'utiliser une variable dans une unité de traduction, puis y accéder à partir d'une autre, puis dans la seconde, vous la déclarez externe et le symbole sera résolu par l'éditeur de liens.

Si vous ne le déclarez pas comme externe, vous obtiendrez 2 variables nommées de la même façon mais pas liées du tout, et une erreur de plusieurs définitions de la variable.

Arkaitz Jimenez
la source
5
En d'autres termes, l'unité de traduction où extern est utilisé connaît cette variable, son type, etc. et permet donc au code source dans la logique sous-jacente de l'utiliser, mais il n'alloue pas la variable, une autre unité de traduction le fera. Si les deux unités de traduction devaient déclarer la variable normalement, il y aurait effectivement deux emplacements physiques pour la variable, avec les références "incorrectes" associées dans le code compilé, et avec l'ambiguïté qui en résulte pour l'éditeur de liens.
mjv
26

J'aime penser à une variable externe comme une promesse que vous faites au compilateur.

Lorsqu'il rencontre un externe, le compilateur ne peut découvrir que son type, pas où il "vit", il ne peut donc pas résoudre la référence.

Vous lui dites: "Faites-moi confiance. Au moment du lien, cette référence sera résoluble."

Buggieboy
la source
Plus généralement, une déclaration est une promesse que le nom pourra être résolu en une seule définition au moment de la liaison. Un extern déclare une variable sans la définir.
Lie Ryan
18

extern indique au compilateur de vous faire confiance que la mémoire de cette variable est déclarée ailleurs, il n'essaie donc pas d'allouer / vérifier la mémoire.

Par conséquent, vous pouvez compiler un fichier qui fait référence à un externe, mais vous ne pouvez pas lier si cette mémoire n'est pas déclarée quelque part.

Utile pour les variables globales et les bibliothèques, mais dangereux car l'éditeur de liens ne tape pas check.

Ben B
la source
La mémoire n'est pas déclarée. Voir les réponses à cette question: stackoverflow.com/questions/1410563 pour plus de détails.
sbi
15

L'ajout d'un externtransforme une définition de variable en une déclaration de variable . Voir ce fil pour savoir quelle est la différence entre une déclaration et une définition.

sbi
la source
Quelle différence entre int fooet extern int foo(portée du fichier)? Les deux sont une déclaration, n'est-ce pas?
@ user14284: Ils sont tous deux une déclaration uniquement dans le sens où chaque définition est également une déclaration. Mais j'ai lié à une explication de cela. ("Voir ce fil pour savoir quelle est la différence entre une déclaration et une définition.") Pourquoi ne suivez-vous pas simplement le lien et lisez-vous?
sbi
14
                 declare | define   | initialize |
                ----------------------------------

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

La déclaration n'allouera pas de mémoire (la variable doit être définie pour l'allocation de mémoire) mais la définition le sera. Ceci est juste une autre vue simple sur le mot-clé extern car les autres réponses sont vraiment excellentes.

Lucian Nut
la source
11

L'interprétation correcte de extern est que vous dites quelque chose au compilateur. Vous dites au compilateur que, bien qu'elle ne soit pas présente actuellement, la variable déclarée sera en quelque sorte trouvée par l'éditeur de liens (généralement dans un autre objet (fichier)). L'éditeur de liens sera alors le chanceux de tout trouver et de le rassembler, que vous ayez eu des déclarations externes ou non.

Alex Lockwood
la source
8

En C, une variable à l'intérieur d'un fichier, par exemple example.c, a une portée locale. Le compilateur s'attend à ce que la variable ait sa définition dans le même fichier example.c et quand elle ne le trouve pas, elle lèverait une erreur. Une fonction en revanche a une portée globale par défaut. Ainsi, vous n'avez pas à mentionner explicitement au compilateur "look mec ... vous pourriez trouver la définition de cette fonction ici". Pour une fonction comprenant le fichier qui contient sa déclaration suffit (le fichier que vous appelez en fait un fichier d'en-tête). Par exemple, considérez les 2 fichiers suivants:
example.c

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

exemple1.c

int a = 5;

Maintenant, lorsque vous compilez les deux fichiers ensemble, à l'aide des commandes suivantes:

étape 1) cc -o ex exemple.c exemple1.c étape 2) ./ ex

Vous obtenez la sortie suivante: La valeur de a est <5>

Phoenix225
la source
8

Implémentation de GCC ELF Linux

D'autres réponses ont couvert le côté de l'utilisation de la langue, alors regardons maintenant comment il est implémenté dans cette implémentation.

principal 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);
}

Compiler et décompiler:

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

La sortie contient:

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

Le chapitre "Table des symboles " de la spécification ELF de mise à jour ABI System V explique:

SHN_UNDEF Cet index de table de section signifie que le symbole n'est pas défini. Lorsque l'éditeur de liens combine ce fichier objet avec un autre qui définit le symbole indiqué, les références de ce fichier au symbole seront liées à la définition réelle.

qui est fondamentalement le comportement que la norme C donne aux externvariables.

Désormais, c'est le lieur de faire le programme final, mais les externinformations ont déjà été extraites du code source dans le fichier objet.

Testé sur GCC 4.8.

Variables en ligne C ++ 17

En C ++ 17, vous souhaiterez peut-être utiliser des variables en ligne au lieu de variables externes, car elles sont simples à utiliser (peuvent être définies une seule fois sur l'en-tête) et plus puissantes (prennent en charge constexpr). Voir: Que signifie «const statique» en C et C ++?

Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
la source
3
Ce n'est pas mon vote négatif, donc je ne sais pas. Cependant, je vais vous faire une opinion. Bien que regarder les résultats de readelfou nmpuisse être utile, vous n'avez pas expliqué les principes fondamentaux de l'utilisation extern, ni terminé le premier programme avec la définition réelle. Votre code n'utilise même pas notExtern. Il y a aussi un problème de nomenclature: bien que notExterndéfini ici plutôt que déclaré avec extern, il s'agit d'une variable externe à laquelle d'autres fichiers source pourraient accéder si ces unités de traduction contenaient une déclaration appropriée (ce qui serait nécessaire extern int notExtern;!).
Jonathan Leffler
1
@JonathanLeffler merci pour la rétroaction! Le comportement standard et les recommandations d'utilisation ont déjà été faits dans d'autres réponses, j'ai donc décidé de montrer un peu l'implémentation car cela m'a vraiment aidé à comprendre ce qui se passe. Ne pas utiliser notExternétait moche, l'a corrigé. À propos de la nomenclature, faites-moi savoir si vous avez un meilleur nom. Bien sûr, ce ne serait pas un bon nom pour un programme réel, mais je pense que cela correspond bien au rôle didactique ici.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
En ce qui concerne les noms, qu'en est-il global_defde la variable définie ici et extern_refde la variable définie dans un autre module? Auraient-ils une symétrie suffisamment claire? Vous vous retrouvez toujours avec int extern_ref = 57;ou quelque chose comme ça dans le fichier où il est défini, donc le nom n'est pas tout à fait idéal, mais dans le contexte du fichier source unique, c'est un choix raisonnable. Avoir extern int global_def;dans un en-tête n'est pas autant un problème, il me semble. Tout dépend de vous, bien sûr.
Jonathan Leffler
7

Le mot clé extern est utilisé avec la variable pour son identification en tant que variable globale.

Cela signifie également que vous pouvez utiliser la variable déclarée à l'aide du mot-clé extern dans n'importe quel fichier bien qu'elle soit déclarée / définie dans un autre fichier.

Anup
la source
5

extern permet à un module de votre programme d'accéder à une variable ou fonction globale déclarée dans un autre module de votre programme. Vous avez généralement des variables externes déclarées dans les fichiers d'en-tête.

Si vous ne voulez pas qu'un programme accède à vos variables ou fonctions, vous utilisez staticqui indique au compilateur que cette variable ou fonction ne peut pas être utilisée en dehors de ce module.

loganaayahee
la source
5

extern signifie simplement qu'une variable est définie ailleurs (par exemple, dans un autre fichier).

Geremia
la source
4

Tout d'abord, le externmot-clé n'est pas utilisé pour définir une variable; il est plutôt utilisé pour déclarer une variable. Je peux dire que externc'est une classe de stockage, pas un type de données.

externest utilisé pour informer les autres fichiers C ou composants externes que cette variable est déjà définie quelque part. Exemple: si vous construisez une bibliothèque, pas besoin de définir obligatoirement une variable globale quelque part dans la bibliothèque elle-même. La bibliothèque sera compilée directement, mais lors de la liaison du fichier, elle vérifie la définition.

user1270846
la source
3

externest utilisé pour qu'un first.cfichier puisse avoir un accès complet à un paramètre global dans un autre second.cfichier.

Le externpeut être déclaré dans le first.cfichier ou dans l'un des fichiers d'en-tête first.cinclus.

Shoham
la source
3
Notez que la externdéclaration doit être dans un en-tête, pas dans first.c, de sorte que si le type change, la déclaration changera aussi. En outre, l'en-tête qui déclare la variable doit être inclus par second.cpour garantir la cohérence de la définition avec la déclaration. La déclaration dans l'en-tête est la colle qui tient tout cela ensemble; il permet de compiler les fichiers séparément mais garantit qu'ils ont une vue cohérente du type de la variable globale.
Jonathan Leffler
2

Avec xc8, vous devez faire attention à déclarer une variable du même type dans chaque fichier car vous pourriez, par erreur, déclarer quelque chose d'un intdans un fichier et un charmot à dire dans un autre. Cela pourrait entraîner une corruption des variables.

Ce problème a été élégamment résolu dans un forum sur les puces électroniques il y a une quinzaine d'années / * Voir "http: www.htsoft.com" / / "forum / all / showflat.php / Cat / 0 / Number / 18766 / an / 0 / page / 0 # 18766 "

Mais ce lien ne semble plus fonctionner ...

Je vais donc essayer de l'expliquer rapidement; créer un fichier appelé global.h.

Dans ce document, déclarez ce qui suit

#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

Maintenant dans le fichier main.c

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

Cela signifie que dans main.c, la variable sera déclarée comme un unsigned char.

Désormais, dans d'autres fichiers, il suffit de déclarer global.h comme fichier externe pour ce fichier .

extern unsigned char testing_mode;

Mais il sera correctement déclaré en tant que unsigned char.

L'ancien message du forum expliquait probablement cela un peu plus clairement. Mais c'est un réel potentiel gotchalorsque vous utilisez un compilateur qui vous permet de déclarer une variable dans un fichier, puis de la déclarer externe comme un type différent dans un autre. Les problèmes qui y sont associés sont que si vous dites déclaré testing_mode en tant qu'int dans un autre fichier, il penserait qu'il s'agissait d'une var 16 bits et écraserait une autre partie de ram, corrompant potentiellement une autre variable. Difficile à déboguer!

user50619
la source
0

Une solution très courte que j'utilise pour permettre à un fichier d'en-tête de contenir la référence externe ou l'implémentation réelle d'un objet. Le fichier qui contient réellement l'objet fait juste #define GLOBAL_FOO_IMPLEMENTATION. Ensuite, lorsque j'ajoute un nouvel objet à ce fichier, il apparaît également dans ce fichier sans que je doive copier et coller la définition.

J'utilise ce modèle sur plusieurs fichiers. Donc, afin de garder les choses aussi autonomes que possible, je réutilise simplement la macro GLOBAL unique dans chaque en-tête. Mon en-tête ressemble à ceci:

//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
muusbolla
la source