Odpowiedzi:
Używanie extern
ma 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.c
należ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.
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.c
i 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++);
}
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 extern
przed deklaracjami funkcji w nagłówkach dla zachowania spójności - aby dopasować extern
przed zmiennymi deklaracjami w nagłówkach. Wiele osób woli nie używać extern
przed deklaracjami funkcji; kompilator nie dba o to - i ostatecznie, ja też nie, dopóki jesteś konsekwentny, przynajmniej w pliku źródłowym.
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
zastosowania prog1.c
, file1.c
, file2.c
, file3.h
i prog1.h
.Plik prog1.mk
jest tylko makefile prog1
. Będzie działał z większością wersji make
wyprodukowanych od przełomu tysiącleci. Nie jest związany konkretnie z 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}
Zasady powinny być łamane tylko przez ekspertów i tylko z ważnego powodu:
Plik nagłówkowy zawiera tylko extern
deklaracje zmiennych - nigdy
static
lub 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 extern
deklaracji 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.
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:
#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); }
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).
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
sizeof
lub_Alignof
operatora, 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 l
jako double
zamiast zamiast a long
, niebezpieczne łączniki typu C prawdopodobnie nie wykryją niezgodności. Jeśli korzystasz z komputera w wersji 64-bitowej long
i double
nawet nie dostaniesz ostrzeżenia; na komputerze z 32-bitowym long
i 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
:
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
zastosowania prog2.c
, file10.c
, file11.c
, file12.c
, prog2.h
.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.
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.
c int some_var; /* Do not do this in a header!!! */
Uwaga 1: jeśli nagłówek definiuje zmienną bez extern
sł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ć.
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.
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.
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.
Jeśli nie jesteś doświadczonym programistą C, prawdopodobnie powinieneś przestać czytać tutaj.
Późne poważne dodanie
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
:
#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++);
}
Następne dwa pliki uzupełniają źródło 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
zastosowania prog3.c
, file1a.c
, file2a.c
, file3a.h
, prog3.h
.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).
#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ść #if
i #else
bloki, naprawiając błąd zidentyfikowany przez
Denisa Kniażewia
#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;
}
Oczywiście, kod dla nieparzystej struktury nie jest tym, co normalnie piszesz, ale ilustruje to sens. Pierwszym argumentem do drugiego wywołania INITIALIZER
jest, { 41
a 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.h
Uwzględniono prawidłowy nagłówek (zamiast fileba.h
) według
Denisa Kniażewia
Następne dwa pliki uzupełniają źródło 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
zastosowania prog4.c
, file1b.c
, file2b.c
, prog4.h
, file3b.h
.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.h
zawiera file3b.h
definicję typu, która nie jest pokazana i file1b.c
wymaga użycia zarówno nagłówka, jak file4b.h
i 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.h
przed file3b.h
wygenerowaniem definicji, ale normalne zabezpieczenia nagłówka file3b.h
zapobiegną ponownemu włączeniu nagłówka.
Tak więc musisz podać treść file3b.h
co 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).
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.h
aby zdefiniować typy (w szczególności struct oddball
rodzaj 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.c
i file6c.c
bezpośrednio dołącz file2c.h
kilka 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:
Nagłówek definiujący lub deklarujący zmienne globalne może sam nie definiować żadnych typów.
Bezpośrednio przed dołączeniem nagłówka, który powinien definiować zmienne, należy zdefiniować makro DEFINE_VARIABLES.
Nagłówek definiujący lub deklarujący zmienne ma stylizowaną treść.
#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; }
Kolejny plik źródłowy uzupełnia źródło (zawiera program główny) o prog5
, prog6
i 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
zastosowania prog5.c
, file3c.c
, file4c.c
, file1c.h
, file2c.h
, external.h
.
prog6
zastosowania prog5.c
, file5c.c
, file4c.c
, file1c.h
, file2c.h
, external.h
.
prog7
zastosowania 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.h
do 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 */
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 #define
i #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"
#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_VARIABLES
w 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; }
Następne dwa pliki uzupełniają źródło prog8
i 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
zastosowania prog8.c
, file7c.c
, file9c.c
.
prog9
zastosowania prog8.c
, file8c.c
, file9c.c
.
Jednak w praktyce problemy raczej nie występują, zwłaszcza jeśli zastosujesz się do standardowej porady
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.h
i 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.c
i prog8.c
to 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).
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
.
extern
Zmienna 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 .c
pliki test1.c
i test2.c
. Jeśli zdefiniujesz zmienną globalną int test1_var;
w test1.c
i 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
extern int test1_var;
się int test1_var;
łącznik (GCC 5.4.0) nadal przechodzi. Czy extern
naprawdę jest w tym przypadku potrzebny?
extern
jest 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ć).
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.
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”.
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.
Dodanie extern
przekształca definicję zmiennej w deklarację zmiennej . Zobacz ten wątek, jaka jest różnica między deklaracją a definicją.
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.
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.
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>
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 extern
zmiennym.
Od teraz zadaniem łącznika jest utworzenie końcowego programu, ale extern
informacje 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 ++?
readelf
lub nm
może być pomocne, nie wyjaśniłeś podstaw korzystania z extern
ani nie ukończyłeś pierwszego programu z rzeczywistą definicją. Twój kod nawet nie używa notExtern
. Istnieje również problem z nomenklaturą: chociaż notExtern
jest 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;
!).
notExtern
był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.
global_def
ze zmienną zdefiniowaną tutaj i extern_ref
zmienną 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.
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, static
co informuje kompilator, że tej zmiennej lub funkcji nie można używać poza tym modułem.
Po pierwsze, extern
słowo kluczowe nie jest używane do definiowania zmiennej; raczej służy do deklarowania zmiennej. Mogę powiedzieć, że extern
to klasa pamięci, a nie typ danych.
extern
sł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ę.
extern
jest używany, aby jeden first.c
plik miał pełny dostęp do parametru globalnego w innym second.c
pliku.
extern
Mogą być zadeklarowane w first.c
pliku lub w którymś z plików nagłówkowych first.c
obejmuje.
extern
deklaracja 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.c
aby 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.
Z xc8 musisz uważać na zadeklarowanie zmiennej jako tego samego typu w każdym pliku, ponieważ możesz błędnie zadeklarować coś int
w jednym pliku i char
wypowiedzieć 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 gotcha
przypadku 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!
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