Jak używać extern do udostępniania zmiennych między plikami źródłowymi?


987

Wiem, że zmienne globalne w C czasami mają externsłowo kluczowe. Co to jest externzmienna? Jaka jest deklaracja? Jaki jest jego zakres?

Jest to związane z udostępnianiem zmiennych między plikami źródłowymi, ale jak to dokładnie działa? Gdzie mam używać extern?

Odpowiedzi:


1751

Używanie externma znaczenie tylko wtedy, gdy budowany program składa się z wielu połączonych plików źródłowych, przy czym do niektórych zmiennych zdefiniowanych, na przykład w pliku źródłowym, file1.cnależy odwoływać się w innych plikach źródłowych, takich jak file2.c.

Ważne jest, aby zrozumieć różnicę między definiowaniem zmiennej a deklarowaniem zmiennej :

  • Zmienna jest deklarowana, gdy kompilator zostanie poinformowany o istnieniu zmiennej (i to jest jej typ); w tym momencie nie przydziela pamięci dla zmiennej.

  • Zmienna jest definiowana, gdy kompilator przydziela pamięć dla zmiennej.

Możesz zadeklarować zmienną wiele razy (choć raz wystarczy); możesz go zdefiniować tylko raz w ramach danego zakresu. Definicja zmiennej jest również deklaracją, ale nie wszystkie deklaracje zmiennych są definicjami.

Najlepszy sposób deklarowania i definiowania zmiennych globalnych

Czystym, niezawodnym sposobem deklarowania i definiowania zmiennych globalnych jest użycie pliku nagłówka, który zawiera extern deklarację zmiennej.

Nagłówek jest zawarty w jednym pliku źródłowym, który definiuje zmienną oraz we wszystkich plikach źródłowych, które odwołują się do zmiennej. Dla każdego programu jeden plik źródłowy (i tylko jeden plik źródłowy) definiuje zmienną. Podobnie, jeden plik nagłówkowy (i tylko jeden plik nagłówkowy) powinien deklarować zmienną. Plik nagłówkowy ma kluczowe znaczenie; umożliwia kontrolę krzyżową niezależnych JT (jednostki tłumaczeniowe - pomyśl pliki źródłowe) i zapewnia spójność.

Chociaż istnieją inne sposoby, aby to zrobić, ta metoda jest prosta i niezawodna. To świadczy file3.h, file1.ci file2.c:

plik3.h

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

plik1.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++; }

plik2.c

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

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

To najlepszy sposób na deklarowanie i definiowanie zmiennych globalnych.


Następne dwa pliki uzupełniają źródło prog1:

Przedstawione pełne programy używają funkcji, więc wkradły się deklaracje funkcji. Zarówno C99, jak i C11 wymagają deklaracji lub zdefiniowania funkcji przed ich użyciem (podczas gdy C90 nie, z dobrych powodów). Używam słowa kluczowego externprzed deklaracjami funkcji w nagłówkach dla zachowania spójności - aby dopasować externprzed zmiennymi deklaracjami w nagłówkach. Wiele osób woli nie używać externprzed deklaracjami funkcji; kompilator nie dba o to - i ostatecznie, ja też nie, dopóki jesteś konsekwentny, przynajmniej w pliku źródłowym.

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

Plik prog1.mkjest tylko makefile prog1. Będzie działał z większością wersji makewyprodukowanych od przełomu tysiącleci. Nie jest związany konkretnie z 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}

Wytyczne

Zasady powinny być łamane tylko przez ekspertów i tylko z ważnego powodu:

  • Plik nagłówkowy zawiera tylko externdeklaracje zmiennych - nigdy staticlub niekwalifikowane definicje zmiennych.

  • Dla dowolnej zmiennej deklaruje ją tylko jeden plik nagłówkowy (SPOT - Single Point of Truth).

  • Plik źródłowy nigdy nie zawiera externdeklaracji zmiennych - pliki źródłowe zawsze zawierają (jedyny) nagłówek, który je deklaruje.

  • Dla każdej zmiennej dokładnie jeden plik źródłowy definiuje zmienną, najlepiej też ją inicjując. (Chociaż nie ma potrzeby jawnej inicjalizacji do zera, nie szkodzi i może zrobić coś dobrego, ponieważ w programie może istnieć tylko jedna inicjowana definicja konkretnej zmiennej globalnej).

  • Plik źródłowy, który definiuje zmienną, zawiera również nagłówek, aby zapewnić spójność definicji i deklaracji.

  • Funkcja nigdy nie powinna deklarować zmiennej za pomocą extern.

  • W miarę możliwości unikaj zmiennych globalnych - zamiast tego używaj funkcji.

Kod źródłowy i tekst tej odpowiedzi są dostępne w moim repozytorium SOQ (Stack Overflow Questions) w GitHub w podkatalogu src / so-0143-3204 .

Jeśli nie jesteś doświadczonym programistą C, możesz (i być może powinieneś) przestać czytać tutaj.

Nie tak dobry sposób definiowania zmiennych globalnych

Dzięki niektórym (a właściwie wielu) kompilatorom języka C możesz uciec od tak zwanej „wspólnej” definicji zmiennej. „Wspólne” odnosi się tutaj do techniki stosowanej w Fortranie do dzielenia się zmiennymi między plikami źródłowymi, przy użyciu (ewentualnie nazwanego) bloku COMMON. Tutaj dzieje się tak, że każdy z wielu plików zawiera wstępną definicję zmiennej. Tak długo, jak nie więcej niż jeden plik zawiera zainicjowaną definicję, różne pliki mają wspólną wspólną definicję zmiennej:

plik 10.c

#include "prog2.h"

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

void inc(void) { l++; }

plik 11.c

#include "prog2.h"

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

void dec(void) { l--; }

plik 12.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); }

Ta technika nie jest zgodna z literą standardu C i „regułą jednej definicji” - jest to oficjalnie niezdefiniowane zachowanie:

J.2 Niezdefiniowane zachowanie

Używany jest identyfikator z zewnętrznym powiązaniem, ale w programie nie istnieje dokładnie jedna zewnętrzna definicja identyfikatora lub identyfikator nie jest używany i istnieje wiele zewnętrznych definicji identyfikatora (6.9).

§6.9 Definicje zewnętrzne ¶5

Zewnętrzny definicja jest zgłoszenie zewnętrzny, który jest również określenie zależności (inne niż określenie bezpośrednia) lub obiektu. Jeżeli w wyrażeniu używany jest identyfikator zadeklarowany za pomocą powiązania zewnętrznego (inny niż jako część argumentu operatora sizeoflub _Alignofoperatora, którego wynikiem jest stała całkowita), gdzieś w całym programie musi istnieć dokładnie jedna zewnętrzna definicja identyfikatora; w przeciwnym razie nie będzie więcej niż jeden. 161)

161) Zatem, jeżeli identyfikator zadeklarowany za pomocą powiązania zewnętrznego nie jest używany w wyrażeniu, nie musi być dla niego żadnej definicji zewnętrznej.

Jednak norma C wymienia ją również w informacyjnym załączniku J jako jedno ze wspólnych rozszerzeń .

J.5.11 Wiele definicji zewnętrznych

Może istnieć więcej niż jedna zewnętrzna definicja identyfikatora obiektu, z jawnym użyciem słowa kluczowego extern lub bez niego; jeśli definicje się nie zgadzają lub zainicjowano więcej niż jedną, zachowanie jest niezdefiniowane (6.9.2).

Ponieważ ta technika nie zawsze jest obsługiwana, najlepiej jej unikać, zwłaszcza jeśli kod musi być przenośny . Korzystając z tej techniki, możesz również skończyć z nieumyślnym pisaniem na klawiaturze.

Jeśli jeden z powyższych plików zadeklarowany ljako doublezamiast zamiast a long, niebezpieczne łączniki typu C prawdopodobnie nie wykryją niezgodności. Jeśli korzystasz z komputera w wersji 64-bitowej longi doublenawet nie dostaniesz ostrzeżenia; na komputerze z 32-bitowym longi 64-bitowymdouble prawdopodobnie otrzymasz ostrzeżenie o różnych rozmiarach - linker użyłby największego rozmiaru, dokładnie tak jak program Fortran wziąłby największy rozmiar spośród wszystkich popularnych bloków.

Należy pamiętać, że GCC 10.1.0, który został wydany w dniu 2020-05-07, zmienia domyślne opcje kompilacji do użycia -fno-common, co oznacza, że ​​domyślnie powyższy kod nie łączy się, chyba że zastąpisz domyślną za pomocą -fcommon(lub użyjesz atrybutów itp. patrz link).


Następne dwa pliki uzupełniają źródło 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();
}
  • prog2zastosowania prog2.c, file10.c, file11.c, file12.c, prog2.h.

Ostrzeżenie

Jak zauważono w komentarzach tutaj i jak stwierdzono w mojej odpowiedzi na podobne pytanie , stosowanie wielu definicji zmiennej globalnej prowadzi do nieokreślonego zachowania (J.2; §6.9), co jest standardowym sposobem powiedzenia „wszystko może się zdarzyć”. Jedną z rzeczy, które mogą się zdarzyć, jest to, że program zachowuje się zgodnie z oczekiwaniami; a J.5.11 mówi w przybliżeniu: „możesz mieć więcej szczęścia, niż zasługujesz”. Ale program, który opiera się na wielu definicjach zmiennej zewnętrznej - z wyraźnym słowem kluczowym „extern” lub bez niej, nie jest programem ściśle zgodnym i nie ma gwarancji, że będzie działać wszędzie. Równoważnie: zawiera błąd, który może się pokazać lub nie.

Naruszenie wytycznych

Istnieje oczywiście wiele sposobów na złamanie tych wytycznych. Czasami może istnieć dobry powód, aby złamać wytyczne, ale takie sytuacje są niezwykle niezwykłe.

faulty_header.h

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

Uwaga 1: jeśli nagłówek definiuje zmienną bez externsłowa kluczowego, wówczas każdy plik zawierający nagłówek tworzy wstępną definicję zmiennej. Jak wspomniano wcześniej, często będzie to działać, ale standard C nie gwarantuje, że będzie działać.

broken_header.h

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

Uwaga 2: jeśli nagłówek definiuje i inicjuje zmienną, wówczas tylko jeden plik źródłowy w danym programie może używać nagłówka. Ponieważ nagłówki służą przede wszystkim do dzielenia się informacjami, tworzenie grono, które można wykorzystać tylko raz, jest trochę głupie.

rzadko_poprawnie.h

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

Uwaga 3: jeśli nagłówek definiuje zmienną statyczną (z inicjacją lub bez), wówczas każdy plik źródłowy kończy swoją własną prywatną wersją zmiennej „globalnej”.

Jeśli zmienna jest w rzeczywistości tablicą złożoną, może to na przykład prowadzić do ekstremalnego powielania kodu. Bardzo rzadko może to być rozsądny sposób na osiągnięcie pewnego efektu, ale jest to bardzo niezwykłe.


Podsumowanie

Użyj techniki nagłówka, którą pokazałem jako pierwszy. Działa niezawodnie i wszędzie. Zwróć uwagę w szczególności, że nagłówek deklarującyglobal_variable jest zawarty w każdym pliku, który go używa - także w tym, który go definiuje. To gwarantuje, że wszystko jest spójne.

Podobne obawy pojawiają się przy deklarowaniu i definiowaniu funkcji - obowiązują analogiczne reguły. Ale pytanie dotyczyło konkretnie zmiennych, więc zachowałem odpowiedź tylko na zmienne.

Koniec oryginalnej odpowiedzi

Jeśli nie jesteś doświadczonym programistą C, prawdopodobnie powinieneś przestać czytać tutaj.


Późne poważne dodanie

Unikanie powielania kodu

Jedną z obaw, która czasem (i uzasadniona) jest podnoszona w związku z opisanym tutaj mechanizmem „deklaracji w nagłówkach, definicji w źródle” jest to, że istnieją dwa pliki do synchronizacji - nagłówek i źródło. Zwykle następuje to z obserwacją, że można użyć makra, aby nagłówek pełnił podwójną funkcję - zwykle deklarując zmienne, ale gdy określone makro jest ustawione przed dołączeniem nagłówka, to zamiast tego definiuje zmienne.

Innym problemem może być to, że zmienne należy zdefiniować w każdym z „głównych programów”. Jest to zwykle fałszywa obawa; możesz po prostu wprowadzić plik źródłowy C, aby zdefiniować zmienne i połączyć plik obiektowy utworzony z każdym programem.

Typowy schemat działa w ten sposób, wykorzystując oryginalną zmienną globalną zilustrowaną na file3.h:

plik3a.h

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

EXTERN int global_variable;

plik1a.c

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

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

plik2a.c

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

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

Następne dwa pliki uzupełniają źródło 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;
}
  • prog3zastosowania prog3.c, file1a.c, file2a.c, file3a.h, prog3.h.

Zmienna inicjalizacja

Problem z tym schematem, jak pokazano, polega na tym, że nie zapewnia on inicjalizacji zmiennej globalnej. Za pomocą C99 lub C11 i list argumentów zmiennych dla makr można zdefiniować makro, które również będzie obsługiwać inicjalizację. (Ponieważ C89 i brak obsługi list zmiennych w makrach, nie ma łatwego sposobu na obsługę dowolnie długich inicjatorów).

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

Odwróć zawartość #ifi #elsebloki, naprawiając błąd zidentyfikowany przez Denisa Kniażewia

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

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

Oczywiście, kod dla nieparzystej struktury nie jest tym, co normalnie piszesz, ale ilustruje to sens. Pierwszym argumentem do drugiego wywołania INITIALIZERjest, { 41a pozostałym argumentem (liczba pojedyncza w tym przykładzie) jest 43 }. Bez C99 lub podobnego wsparcia dla zmiennych list argumentów dla makr, inicjalizatory, które muszą zawierać przecinki, są bardzo problematyczne.

file3b.hUwzględniono prawidłowy nagłówek (zamiast fileba.h) według Denisa Kniażewia


Następne dwa pliki uzupełniają źródło 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;
}
  • prog4zastosowania prog4.c, file1b.c, file2b.c, prog4.h, file3b.h.

Strażnicy hedera

Każdy nagłówek powinien być chroniony przed ponownym włączeniem, aby definicje typów (typy wyliczeniowe, strukturalne lub uniowe lub ogólnie typy typedefs) nie powodowały problemów. Standardową techniką jest zawijanie treści nagłówka w osłonę nagłówka, taką jak:

#ifndef FILE3B_H_INCLUDED
#define FILE3B_H_INCLUDED

...contents of header...

#endif /* FILE3B_H_INCLUDED */

Nagłówek może być dołączony dwa razy pośrednio. Na przykład, jeśli file4b.hzawiera file3b.hdefinicję typu, która nie jest pokazana i file1b.cwymaga użycia zarówno nagłówka, jak file4b.hi file3b.h, to masz kilka trudniejszych problemów do rozwiązania. Oczywiście, możesz zmienić listę nagłówków, aby uwzględnić tylko file4b.h. Jednak możesz nie zdawać sobie sprawy z wewnętrznych zależności - a kod powinien, najlepiej, kontynuować pracę.

Co więcej, zaczyna się robić trudne, ponieważ możesz uwzględnić file4b.hprzed file3b.hwygenerowaniem definicji, ale normalne zabezpieczenia nagłówka file3b.hzapobiegną ponownemu włączeniu nagłówka.

Tak więc musisz podać treść file3b.hco najwyżej raz dla deklaracji i co najwyżej raz dla definicji, ale możesz potrzebować zarówno w jednej jednostce tłumaczącej (TU - kombinacja pliku źródłowego i nagłówków, których używa).

Wielokrotne włączenie z definicjami zmiennych

Można to jednak zrobić z zastrzeżeniem nieuzasadnionego ograniczenia. Wprowadźmy nowy zestaw nazw plików:

  • external.h dla definicji makr EXTERN itp.

  • file1c.haby zdefiniować typy (w szczególności struct oddballrodzaj oddball_struct).

  • file2c.h aby zdefiniować lub zadeklarować zmienne globalne.

  • file3c.c który definiuje zmienne globalne.

  • file4c.c który po prostu używa zmiennych globalnych.

  • file5c.c co pokazuje, że możesz zadeklarować, a następnie zdefiniować zmienne globalne.

  • file6c.c co pokazuje, że możesz zdefiniować, a następnie (spróbować) zadeklarować zmienne globalne.

W tych przykładach file5c.ci file6c.cbezpośrednio dołącz file2c.hkilka razy nagłówek , ale jest to najprostszy sposób wykazania, że ​​mechanizm działa. Oznacza to, że jeśli nagłówek byłby pośrednio zawarty dwukrotnie, byłby również bezpieczny.

Ograniczenia tego działania są następujące:

  1. Nagłówek definiujący lub deklarujący zmienne globalne może sam nie definiować żadnych typów.

  2. Bezpośrednio przed dołączeniem nagłówka, który powinien definiować zmienne, należy zdefiniować makro DEFINE_VARIABLES.

  3. Nagłówek definiujący lub deklarujący zmienne ma stylizowaną treść.

zewnętrzny. h


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

plik1c.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 */

plik2c.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 */

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

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

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

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

Kolejny plik źródłowy uzupełnia źródło (zawiera program główny) o prog5, prog6i 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;
}
  • prog5zastosowania prog5.c, file3c.c, file4c.c, file1c.h, file2c.h, external.h.

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

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


Ten schemat pozwala uniknąć większości problemów. Problem występuje tylko wtedy, gdy nagłówek definiujący zmienne (np. file2c.h) Jest zawarty w innym nagłówku (powiedzmy file7c.h), który definiuje zmienne. Nie ma łatwego sposobu na obejście tego poza „nie rób tego”.

Można częściowo obejść ten problem poprzez zmianę file2c.hdo file2d.h:

plik2d.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 */

Problem brzmi „czy nagłówek powinien zawierać #undef DEFINE_VARIABLES?” Jeśli pominąć, że z nagłówka i owinąć każdą określającą inwokację z #definei #undef:

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

w kodzie źródłowym (więc nagłówki nigdy nie zmieniają wartości DEFINE_VARIABLES), powinieneś być czysty. To tylko uciążliwość, aby pamiętać o napisaniu dodatkowego wiersza. Alternatywą może być:

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

Jest to trochę skomplikowane, ale wydaje się być bezpieczne (przy użyciu file2d.h, bez #undef DEFINE_VARIABLESw file2d.h).

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

plik8c.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 */

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

Następne dwa pliki uzupełniają źródło prog8i 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;
}

plik9c.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;
}
  • prog8zastosowania prog8.c, file7c.c, file9c.c.

  • prog9zastosowania prog8.c, file8c.c, file9c.c.


Jednak w praktyce problemy raczej nie występują, zwłaszcza jeśli zastosujesz się do standardowej porady

Unikaj zmiennych globalnych


Czy w tej wystawie coś brakuje?

Spowiedź : Przedstawiony tutaj schemat „unikania powielonego kodu” został opracowany, ponieważ problem dotyczy niektórych kodów, nad którymi pracuję (ale nie posiadam), i jest niepokojący w stosunku do schematu przedstawionego w pierwszej części odpowiedzi. Jednak oryginalny schemat pozostawia tylko dwa miejsca do zmodyfikowania, aby zachować synchronizację definicji i deklaracji zmiennych, co jest dużym krokiem naprzód w porównaniu z zewnętrznymi deklaracjami zmiennych rozproszonymi w całej bazie kodu (co naprawdę ma znaczenie, gdy w sumie są tysiące plików) . Jednak kod w plikach o nazwach fileNc.[ch](plus external.hi externdef.h) pokazuje, że można go uruchomić . Oczywiście nie byłoby trudno stworzyć skrypt generatora nagłówków, który dałby ustandaryzowany szablon dla zmiennej definiującej i deklarującej plik nagłówkowy.

Uwaga: Są to programy zabawkowe z ledwo wystarczającym kodem, aby uczynić je marginalnie interesującymi. Przykłady są powtarzalne, ale można je uprościć, ale nie mają na celu uproszczenia wyjaśnienia pedagogicznego. (Na przykład: różnica między prog5.ci prog8.cto nazwa jednego z dołączonych nagłówków. Można by zreorganizować kod, aby main()funkcja nie została powtórzona, ale ukryłaby więcej, niż się ujawniła).


3
@litb: wspólna definicja znajduje się w załączniku J.5.11 - jest to wspólne rozszerzenie.
Jonathan Leffler

3
@litb: i zgadzam się, że należy tego unikać - dlatego znajduje się w sekcji „Niezbyt dobry sposób definiowania zmiennych globalnych”.
Jonathan Leffler,

3
Rzeczywiście jest to powszechne rozszerzenie, ale jest to niezdefiniowane zachowanie programu, na którym można polegać. Po prostu nie byłem pewien, czy mówisz, że jest to dozwolone przez własne zasady C. Teraz widzę, że mówisz, że jest to zwykłe rozszerzenie i unikaj go, jeśli potrzebujesz przenośnego kodu. Więc mogę cię głosować bez wątpliwości. Naprawdę świetna odpowiedź IMHO :)
Johannes Schaub - litb

19
Jeśli zatrzymasz się na górze, proste rzeczy będą proste. W dalszej części tekstu omawiamy więcej niuansów, komplikacji i szczegółów. Właśnie dodałem dwa „punkty wczesnego zatrzymania” dla mniej doświadczonych programistów C - lub programistów C, którzy już znają ten temat. Nie musisz czytać wszystkiego, jeśli znasz już odpowiedź (ale daj mi znać, jeśli znajdziesz usterkę techniczną).
Jonathan Leffler,

4
@ superupat: Przyszło mi do głowy, że można użyć literałów tablic C99, aby uzyskać wartość wyliczenia dla rozmiaru tablicy, na przykład ( foo.h): w #define FOO_INITIALIZER { 1, 2, 3, 4, 5 }celu zdefiniowania inicjalizatora tablicy, enum { FOO_SIZE = sizeof((int [])FOO_INITIALIZER) / sizeof(((int [])FOO_INITIALIZER)[0]) };uzyskania rozmiaru tablicy i extern int foo[];zadeklarowania tablicy . Oczywiście definicja powinna być po prostu int foo[FOO_SIZE] = FOO_INITIALIZER;, chociaż rozmiar tak naprawdę nie musi być zawarty w definicji. Ten dostaje stałą Integer FOO_SIZE.
Jonathan Leffler,

125

externZmienna jest deklaracją (dzięki SBI do korekty) zmiennej, która jest zdefiniowana w innej jednostce tłumaczeniowej. Oznacza to, że pamięć dla zmiennej jest przydzielona w innym pliku.

Powiedzmy, że masz dwa .cpliki test1.ci test2.c. Jeśli zdefiniujesz zmienną globalną int test1_var;w test1.ci chcesz uzyskać dostęp do tej zmiennej test2.c, musisz użyć extern int test1_var;w test2.c.

Pełna próbka:

$ 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
Nie ma „pseudo-definicji”. To deklaracja.
sbi

3
W powyższym przykładzie, jeśli zmienić extern int test1_var;się int test1_var;łącznik (GCC 5.4.0) nadal przechodzi. Czy externnaprawdę jest w tym przypadku potrzebny?
radiohead

2
@radiohead: W mojej odpowiedzi znajdziesz informację, że upuszczanie externjest powszechnym rozszerzeniem, które często działa - a konkretnie działa z GCC (ale GCC nie jest jedynym kompilatorem, który go obsługuje; jest powszechny w systemach Unix). W mojej odpowiedzi możesz poszukać „J.5.11” lub sekcji „Niezbyt dobry sposób” (wiem - jest długi) i tekstu w pobliżu, który to wyjaśnia (lub próbuje to zrobić).
Jonathan Leffler

Deklaracja zewnętrzna z pewnością nie musi być zdefiniowana w innej jednostce tłumaczeniowej (i zwykle nie jest). W rzeczywistości deklaracja i definicja mogą być jednym i tym samym.
Pamiętaj Monica

40

Extern to słowo kluczowe używane do deklarowania, że ​​sama zmienna znajduje się w innej jednostce tłumaczeniowej.

Możesz więc zdecydować o użyciu zmiennej w jednostce tłumaczeniowej, a następnie uzyskać do niej dostęp z innej, a następnie w drugiej deklarujesz ją jako zewnętrzną, a symbol zostanie rozwiązany przez linker.

Jeśli nie zadeklarujesz go jako zewnętrznego, otrzymasz 2 zmienne o takich samych, ale w ogóle nie spokrewnionych, oraz błąd wielu definicji zmiennej.


5
Innymi słowy, jednostka tłumacząca, w której używany jest zewnętrzny, wie o tej zmiennej, jej typie itp., A zatem pozwala na użycie kodu źródłowego w podstawowej logice, ale nie alokuje zmiennej, zrobi to inna jednostka tłumacząca. Gdyby obie jednostki tłumaczeniowe normalnie zadeklarowały zmienną, byłyby efektywnie dwie fizyczne lokalizacje dla zmiennej, z powiązanymi „złymi” referencjami w skompilowanym kodzie i wynikającą z niejasnością dla linkera.
mjv

26

Lubię myśleć o zmiennej zewnętrznej jako obietnicy złożonej kompilatorowi.

Po napotkaniu elementu zewnętrznego kompilator może jedynie dowiedzieć się, jaki jest jego typ, a nie gdzie „mieszka”, więc nie może rozpoznać odwołania.

Mówisz mu: „Zaufaj mi. W czasie linku to odniesienie będzie możliwe do rozwiązania”.


Mówiąc bardziej ogólnie, deklaracja jest obietnicą, że nazwa będzie zrozumiała dla dokładnie jednej definicji w czasie łączenia. Zewnętrzny deklaruje zmienną bez definiowania.
Lie Ryan,

18

extern mówi kompilatorowi, aby zaufał ci, że pamięć dla tej zmiennej jest zadeklarowana gdzie indziej, więc nie próbuje alokować / sprawdzać pamięci.

Dlatego możesz skompilować plik, który zawiera odwołanie do zewnętrznego, ale nie możesz połączyć, jeśli ta pamięć nie jest gdzieś zadeklarowana.

Przydatne w przypadku zmiennych globalnych i bibliotek, ale niebezpieczne, ponieważ linker nie sprawdza typu.


Pamięć nie została zadeklarowana. Zobacz odpowiedzi na to pytanie: stackoverflow.com/questions/1410563, aby uzyskać więcej informacji.
sbi

15

Dodanie externprzekształca definicję zmiennej w deklarację zmiennej . Zobacz ten wątek, jaka jest różnica między deklaracją a definicją.


Jaka jest różnica między int fooi extern int foo(zakres pliku)? Oba są deklaracjami, prawda?

@ user14284: Oba są deklaracjami tylko w tym sensie, że każda definicja jest również deklaracją. Ale powiązałem to z wyjaśnieniem tego. („Zobacz ten wątek, jaka jest różnica między deklaracją a definicją.”) Dlaczego po prostu nie skorzystasz z linku i nie przeczytasz?
sbi

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

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

Deklaracja nie przydziela pamięci (zmienna musi być zdefiniowana dla przydziału pamięci), ale definicja tak. To tylko kolejny prosty widok słowa kluczowego extern, ponieważ inne odpowiedzi są naprawdę świetne.


11

Prawidłowa interpretacja extern polega na tym, że mówisz coś kompilatorowi. Mówisz kompilatorowi, że pomimo nieobecności w tej chwili zadeklarowana zmienna jakoś zostanie znaleziona przez linker (zazwyczaj w innym obiekcie (pliku)). Linker będzie wtedy szczęśliwym facetem, który znajdzie wszystko i złoży to wszystko, niezależnie od tego, czy miałeś jakieś zewnętrzne deklaracje, czy nie.


8

W C zmienna wewnątrz pliku, powiedz przykład.c, ma zasięg lokalny. Kompilator oczekuje, że zmienna będzie miała swoją definicję w tym samym pliku file.c, a gdy nie znajdzie tego samego, wygeneruje błąd. Z drugiej strony funkcja ma domyślnie zasięg globalny. Dlatego nie musisz wyraźnie wspominać kompilatorowi: „look dude ... możesz znaleźć tutaj definicję tej funkcji”. Wystarczy funkcja zawierająca plik, który zawiera deklarację (plik, który faktycznie nazywamy plikiem nagłówkowym). Weźmy na przykład następujące 2 pliki:
przyklad.c

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

przyklad1.c

int a = 5;

Teraz, kiedy skompilujesz oba pliki razem, używając następujących poleceń:

krok 1) cc -o ex przykład. c przykład1.c krok 2) ./ ex

Otrzymujesz następujące dane wyjściowe: Wartość a wynosi <5>


8

Implementacja GCC ELF Linux

Inne odpowiedzi obejmowały stronę dotyczącą użycia języka, więc przyjrzyjmy się teraz, jak jest implementowana w tej implementacji.

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

Kompiluj i dekompiluj:

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

Dane wyjściowe zawierają:

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

The System V ABI Aktualizacja elf Spec rozdział "Symbol Table" wyjaśnia:

SHN_UNDEF Ten indeks tablicy sekcji oznacza, że ​​symbol jest niezdefiniowany. Kiedy edytor łączy łączy ten plik obiektowy z innym, który definiuje wskazany symbol, odniesienia tego pliku do symbolu zostaną połączone z rzeczywistą definicją.

co jest w zasadzie zachowaniem, jakie standard C nadaje externzmiennym.

Od teraz zadaniem łącznika jest utworzenie końcowego programu, ale externinformacje zostały już wyodrębnione z kodu źródłowego do pliku obiektowego.

Testowane na GCC 4.8.

C ++ 17 zmiennych wbudowanych

W C ++ 17 możesz chcieć używać zmiennych wbudowanych zamiast zewnętrznych, ponieważ są one proste w użyciu (można je zdefiniować tylko raz w nagłówku) i bardziej wydajne (wsparcie constexpr). Zobacz: Co oznacza „const static” w C i C ++?


3
To nie jest mój głos negatywny, więc nie wiem. Jednak przedstawię opinię. Chociaż patrząc na wynik readelflub nmmoże być pomocne, nie wyjaśniłeś podstaw korzystania z externani nie ukończyłeś pierwszego programu z rzeczywistą definicją. Twój kod nawet nie używa notExtern. Istnieje również problem z nomenklaturą: chociaż notExternjest tu zdefiniowany, a nie zadeklarowany extern, jest to zmienna zewnętrzna, do której mogłyby uzyskać dostęp inne pliki źródłowe, gdyby jednostki tłumaczeniowe zawierały odpowiednią deklarację (która by tego potrzebowała extern int notExtern;!).
Jonathan Leffler,

1
@JonathanLeffler dzięki za opinie! Standardowe zalecenia dotyczące zachowania i użytkowania zostały już wykonane w innych odpowiedziach, więc postanowiłem trochę pokazać implementację, ponieważ to naprawdę pomogło mi zrozumieć, co się dzieje. Nieużywanie notExternbyło brzydkie, naprawiłem to. O nomenklaturze, daj mi znać, jeśli masz lepsze imię. Oczywiście nie byłoby to dobre imię dla rzeczywistego programu, ale myślę, że dobrze pasuje tutaj do roli dydaktycznej.
Ciro Santilli 冠状 病毒 审查 六四 事件 法轮功

Co do nazw, co global_defze zmienną zdefiniowaną tutaj i extern_refzmienną zdefiniowaną w innym module? Czy mieliby odpowiednio wyraźną symetrię? Nadal int extern_ref = 57;pojawia się coś takiego w pliku, w którym jest zdefiniowany, więc nazwa nie jest idealna, ale w kontekście pojedynczego pliku źródłowego jest to rozsądny wybór. extern int global_def;Wydaje mi się, że posiadanie nagłówka nie stanowi większego problemu. Oczywiście całkowicie do ciebie.
Jonathan Leffler,

7

Słowo kluczowe extern jest używane ze zmienną do identyfikacji jej jako zmiennej globalnej.

Oznacza to również, że można użyć zmiennej zadeklarowanej przy użyciu słowa kluczowego extern w dowolnym pliku, chociaż jest ona zadeklarowana / zdefiniowana w innym pliku.


5

extern pozwala jednemu modułowi twojego programu uzyskać dostęp do zmiennej globalnej lub funkcji zadeklarowanej w innym module twojego programu. Zwykle masz zadeklarowane zmienne w plikach nagłówków.

Jeśli nie chcesz, aby program miał dostęp do twoich zmiennych lub funkcji, użyj tego, staticco informuje kompilator, że tej zmiennej lub funkcji nie można używać poza tym modułem.


5

extern oznacza po prostu, że zmienna jest zdefiniowana gdzie indziej (np. w innym pliku).


4

Po pierwsze, externsłowo kluczowe nie jest używane do definiowania zmiennej; raczej służy do deklarowania zmiennej. Mogę powiedzieć, że externto klasa pamięci, a nie typ danych.

externsłuży do informowania innych plików C lub komponentów zewnętrznych, że ta zmienna jest już gdzieś zdefiniowana. Przykład: jeśli budujesz bibliotekę, nie musisz obowiązkowo definiować zmiennej globalnej gdzieś w samej bibliotece. Biblioteka zostanie skompilowana bezpośrednio, ale podczas łączenia pliku sprawdza definicję.


3

externjest używany, aby jeden first.cplik miał pełny dostęp do parametru globalnego w innym second.cpliku.

externMogą być zadeklarowane w first.cpliku lub w którymś z plików nagłówkowych first.cobejmuje.


3
Zauważ, że externdeklaracja powinna znajdować się w nagłówku, a nie w first.c, więc jeśli typ się zmieni, deklaracja również się zmieni. Należy również uwzględnić nagłówek deklarujący zmienną, second.caby zapewnić zgodność definicji z deklaracją. Deklaracja w nagłówku jest klejem, który trzyma to wszystko razem; umożliwia osobną kompilację plików, ale zapewnia spójny widok typu zmiennej globalnej.
Jonathan Leffler

2

Z xc8 musisz uważać na zadeklarowanie zmiennej jako tego samego typu w każdym pliku, ponieważ możesz błędnie zadeklarować coś intw jednym pliku i charwypowiedzieć się w innym. Może to prowadzić do uszkodzenia zmiennych.

Ten problem został elegancko rozwiązany na forum mikroprocesorów około 15 lat temu / * Patrz „http: www.htsoft.com” / / ”forum / all / showflat.php / Cat / 0 / Number / 18766 / an / 0 / page / 0 # 18766 "

Ale ten link wydaje się już nie działać ...

Więc ja; spróbuję to szybko wyjaśnić; utwórz plik o nazwie global.h.

W nim zadeklaruj, co następuje

#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

Teraz w pliku main.c

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

Oznacza to, że w main.c zmienna zostanie zadeklarowana jako unsigned char.

Teraz w innych plikach, w tym po prostu global.h, zostanie zadeklarowany jako zewnętrzny dla tego pliku .

extern unsigned char testing_mode;

Ale zostanie poprawnie zadeklarowany jako unsigned char.

Stary post na forum prawdopodobnie wyjaśnił to nieco jaśniej. Ale jest to prawdziwy potencjał w gotchaprzypadku korzystania z kompilatora, który pozwala zadeklarować zmienną w jednym pliku, a następnie zadeklarować ją jako inny typ w innym pliku. Problemy z tym związane polegają na tym, że jeśli powiesz, że deklarujesz test_mode jako int w innym pliku, pomyślałbyś, że to 16-bitowy var i nadpisuje inną część pamięci RAM, potencjalnie powodując uszkodzenie innej zmiennej. Trudne do debugowania!


0

Bardzo krótkie rozwiązanie, którego używam, aby plik nagłówkowy zawierał zewnętrzne odniesienie lub rzeczywistą implementację obiektu. Plik, który faktycznie zawiera obiekt, właśnie to robi #define GLOBAL_FOO_IMPLEMENTATION. Następnie, gdy dodam nowy obiekt do tego pliku, pojawia się on również w tym pliku bez konieczności kopiowania i wklejania definicji.

Używam tego wzorca w wielu plikach. Aby więc zachować jak najbardziej samowystarczalność, po prostu ponownie używam jednego GLOBALNEGO makra w każdym nagłówku. Mój nagłówek wygląda następująco:

//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
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.