Как использовать extern? Печать
Добавил(а) microsin   

Вы наверняка знаете, что ключевое слово extern применяют для того, чтобы совместно использовать одну и ту же переменную в разных модулях кода на языке C/C++. С помощью extern переменные становятся глобальными. Но что в реальности происходит, когда используется extern? Что такое декларация? Какая область действия у переменной? Как правильно использовать extern?

Использование extern уместно только в тех случаях, когда построенная Вами программа состоит из нескольких исходных файлов, соединяемых вместе на этапе линковки, где некоторые переменные определены, например, в исходном файле file1.c, и к ним нужно обращаться в других исходных файлах, таких как file2.c.

Важно понимать разницу между терминами "определение переменной" (defining a variable) и "декларирование переменной" (иногда говорят "объявление переменной", declaring a variable). Причем можно определять и декларировать не только переменные, но и константы. Вот смысл этих понятий:

Переменная (или константа) определена в том месте программы, где компилятор выделяет под неё память. Переменная (или константа) декларируется, когда компилятор информируется о том, что эта переменная где-то уже определена.

В декларации обязательно указывается тип переменной, чтобы компилятор знал, какой код нужно генерировать при обращении к переменной. В месте декларации не выделяется место в памяти для размещения переменной. Вы можете декларировать переменную в модуле кода несколько раз (хотя достаточно только одной декларации); но определить переменную в области её видимости можно только один раз.

[Как лучше всего декларировать и определять переменные]

Хотя есть и другие способы декларации и определения глобальных переменных, но самый надежный и удобный способ для этого - создать файл file3.h, в котором будет содержаться внешняя декларация переменной (с ключевым словом extern). Получится так называемый заголовочный файл, хедер (header). Этот хедер подключается один раз в том файле исходного кода, где переменная определена (хотя это не обязательно), и также хедер подключается во всех файлах исходного кода, где есть обращение к этой переменной. Для каждой программы один исходный файл (и только один исходный файл) декларирует переменную. Подобным образом только один хедер должен декларировать переменную. Ниже показан пример декларирования, определения и использования глобальной переменной global_variable.

file3.h

// file3.h (пример заголовочного файла с декларацией переменной)
extern int global_variable;  /* Декларация переменной */

file1.c

//file1.c (пример модуля исходного кода с определением переменной)
#include "file3.h"  /* В этом месте доступна декларация переменной */
#include "prog1.h"  /* Декларация функции */
/* Здесь переменная определяется */
int global_variable = 37;    /* Определение будет сверено с декларацией */
 
int increment(void) { return global_variable++; }

file2.c

//file2.c (в этом месте используется глобальная переменная)
#include "file3.h"
#include "prog1.h"
#include < stdio.h >
 
void use_it(void)
{
    printf("Global variable: %d\n", global_variable++);
}

Подобным образом можно декларировать и определять функции. Пример (функции increment и use_it):

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

Модуль prog1 использует prog1.c, file1.c, file2.c, file3.h и prog1.h. 

Примечание: по каким-то странным обстоятельствам, известным только Брайану Кернигану и Деннису Ритчи, при декларации функций использовать ключевое слово extern не обязательно. Это не относится к декларации переменных. Т. е., примеру вот эти две декларации функции совершенно равнозначны.

Так правильно:

//Хедер prog1.h.
extern void use_it(void);

И так тоже правильно:

//Хедер prog1.h.
void use_it(void);

[Общие правила]

Ниже приведены основные правила программирования, связанные с декларацией и определением переменных, констант и функций. Правила придуманы не просто так - они упрощают понимание и поддержку как своего, так и чужого кода. Итак, эти правила могут нарушать только эксперты, и только с серьезным основанием:

• Заголовочный файл (header, файл с расширением *.h) должен содержать в себе только extern-декларации переменных. Здесь не должно быть никаких статических переменных (static).

• Для любой имеющейся переменной только один заголовочный файл должен декларировать её (правило SPOT — Single Point of Truth, т. е. "правда только в одном месте").

• Никогда не вставляйте в модуль исходного кода (файл с расширением *.c или *.cpp) extern-декларации переменных — исходные файлы всегда подключают (единственный) хедер, в котором декларируются переменные.

• Для любой имеющейся переменной есть только один исходный файл, где она определена, и также желательно в месте определения наличия и инициализации переменной. Хотя нет необходимости явно инициализировать переменную нулевым значением (это обычно делает компилятор автоматически), инициализация нулем не составляет никаких проблем, и дает некоторую выгоду в том, что всегда соблюдается правило - глобальная переменная инициализируется в одном и только одном месте программы.

• К исходному файлу, где определена переменная, следует также подключить (директивой #include) и файл заголовка с декларацией переменной - чтобы гарантировать, что и определение, и декларация полностью соответствуют друг другу.

• В функции никогда не следует декларировать переменную с использованием extern.

• Избегайте использования глобальных переменных, где это только возможно - применяйте вместо этого функции.

Если Вы не являетесь опытным C-программистом, то можете (и возможно должны) дальше не читать.

[Не очень хороший способ определять глобальные переменные]

С некоторыми компиляторами языка C (в действительности, со многими) Вам может сойти с рук то, что можно назвать также "общим" определением переменной. Слово "общий" здесь относится к технике, используемой в языке Fortran для совместного использования переменной между исходными файлами, используя (возможно именованный) ОБЩИЙ блок кода. Подобное произойдет, когда каждый из нескольких файлов предоставят предварительное определение переменной. Пока не больше, чем в одном файле есть инициализированное определение, различные файлы используют одно общее определение переменной:

file10.c

#include "prog2.h"
 
int i;   /* Не делайте этого для портируемого кода */
 
void inc(void) { i++; }

file11.c

#include "prog2.h"
 
int i;   /* Не делайте этого для портируемого кода */
 
void dec(void) { i--; }

file12.c

#include "prog2.h"
#include < stdio.h >
 
int i = 9;   /* Не делайте этого для портируемого кода */
 
void put(void) { printf("i = %d\n", i); }

Эта техника не придерживается букве стандарта C и правилу 'одного определения', однако стандарт C упоминает это как общую вариацию этого правила одного определения. Поскольку эта техника поддерживается не всегда, то лучше её избегать, особенно если Ваш код должен в будущем портироваться на другие платформы и системы. С использованием этой техники Вы можете также столкнуться с неожиданным каламбуром типов. Если в одном из файлов переменная i декларирована как double вместо int, то линковщики, не соблюдающие жесткую типизацию C, возможно не определят несоответствие типов. Если на Вашем компьютере у типов int и double разрядность 64 бита, то даже не получите предупреждения; но на компьютере, где int 32-битный, и double 64-битный, скорее всего линковщик выдаст предупреждение о разных размерах типов, и линковщик будет использовать самый большой размер, точно так же как программа на Фортране будет использовать самый большой размер переменной из любого общего блока.

Это упомянуто в C-стандарте Annex J как общее расширение:

There may be more than one external definition for the identifier of an object, with or without the explicit use of the keyword extern; if the definitions disagree, or more than one is initialized, the behavior is undefined (6.9.2).

Перевод: может существовать больше одного внешнего определения для идентификатора объекта, вместе с явным использованием ключевого слова extern или без его использования; если эти определения противоречат друг другу, или если имеется больше одной инициализации в определениях, то поведение кода не определено (6.9.2).

В следующих 2 файлах приведен полный код для 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();
}

Модуль программы prog2 использует prog2.c, file10.c, file11.c, file12.c, prog2.h.

Предупреждение: использование нескольких конкурентных определений для глобальной переменной приводит к неопределенному поведению, которое является способом для стандарта выразить, что "что-то, но непонятно что, должно произойти". Т. е. может произойти так, что в одном случае программа будет вести себя так, как ожидалось; как сказано в J.5.11, приблизительно "Вы могли бы быть удачливы чаще, чем того заслуживаете". Но программа, которая полагается не несколько определений внешней переменной — с ключевым словом 'extern' или без него — не только запутывает программиста, но и еще не гарантирует, что будет работать везде. Короче говоря: код будет содержать ошибку, которая может не показать себя.

[Нарушение правил]

faulty_header.h

//Пример ошибочного заголовка.
int some_var;    /* Не делайте этого в заголовочном файле!!! */

Замечание 1: если в хедере определена переменная без ключевого слова extern, то каждый файл, который подключит этот заголовок, создаст предварительное определение переменной.

broken_header.h

//Еще один пример плохого заголовка.
int some_var = 13;   /* Только в одном исходном файле программы можно делать
                        инициализацию переменной. */

Замечание 2: если заголовок определяет и инициализирует переменную, то только один исходный файл в программе может использовать этот заголовок. Смысл заголовочного файла теряется!

seldom_correct.h

//Возможно правильный заголовок, но зачем так делать?
static int hidden_global = 3;   /* В каждом исходном файле будет определена отдельная
                                   скрытая переменная hidden_global. */

Замечание 3: если в заголовке определена статическая переменная (с инициализацией значением или без), то каждый исходный файл, который подключит этот заголовок, получит свою собственную частную версию 'глобальной' переменной. В кавычках потому, что это уже не будет глобальная переменная для всех модулей программы, она будет глобальной только для одного модуля.

Если, например, переменная в действительности является сложным массивом, то это приведет к чрезмерному дублированию кода. Это в редких случаях может быть способом достигнуть какого-то эффекта, но такое использование определения весьма необычно.

[Общие выводы]

Всегда используйте стандартную технику заголовков (как это описано в разделе "Общие правила"). Это надежно работает везде. Обратите внимание, в частности, что заголовок, где декларируется внешняя переменная, следует подключить во все файлы, которые используют эту переменную, даже в тот файл, где переменная определена. Это гарантирует точное соответствие декларации и определения переменной.

Подобные проблемы возникают и с декларированием и определением функций — для этого работают аналогичные правила. К константам относится то же самое.

При соблюдении описанных правил получается так, что в код определения функций подставляются и их декларации. Я использую в заголовках как для переменных, так и для функций при декларации ключевое слово extern. Однако многие программисты не используют extern перед функциями при их декларации; компилятор это не беспокоит, так что не указание extern перед декларации функции не составит проблему, пока Вы последовательны в кодировании.

[Избегайте дублирования кода]

С принципом "декларация в заголовках, определение в исходном коде" возникает одна проблема (и это обоснованно), что есть два файла, которые должны быть синхронизированы друг с другом - хедер и модуль исходного кода. Это обычно контролируется вручную, и может быть применен макрос, который возлагает на хедер двойную функцию — обычно он задает декларацию переменных, но когда определенный макрос устанавливается до того, как был подключен заголовок, он вместо этого делает определение переменных.

Другая проблема может возникнуть с тем, что переменные должны быть определены в каждой из нескольких "основных программ, main". Обычное побочное беспокойство; Вы можете просто представить исходный файл C для определения переменных и прилинковать его объектный файл к каждой из программ.

Обычная схема может выглядеть следующим образом, с использованием глобальной переменной, которая иллюстрировалась в файле file3.h:

file3a.h

#ifdef DEFINE_VARIABLES
 #define EXTERN /* пустота */
#else
 #define EXTERN extern
#endif /* DEFINE_VARIABLES */
 
EXTERN int global_variable;

file1a.c

#define DEFINE_VARIABLES
#include "file3a.h"  /* Переменная определена, но не инициализирована */
#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++);
}

Ниже показаны полные два файла для 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;
}

Программа prog3 использует prog3.c, file1a.c, file2a.c, file3a.h, prog3.h.

[Инициализация переменных]

Проблема с этой показанной схемой состоит в том, что не сделана инициализация глобальной переменной. Со стандартами C99 или C11 и переменными списками аргументов (variable argument lists) для макроса, Вы также должны определить макрос для поддержки инициализации. Со стандартом C89 и без поддержки переменных списков аргумента нет простого способа обработать произвольно длинные инициализаторы.

file3b.h

#ifdef DEFINE_VARIABLES
 #define EXTERN                  /* пустота */
 #define INITIALIZER(...)        = __VA_ARGS__
#else
 #define EXTERN                  extern
 #define INITIALIZER(...)        /* пустота */
#endif /* DEFINE_VARIABLES */
 
EXTERN int global_variable INITIALIZER(37);
EXTERN struct { int a; int b; } oddball_struct INITIALIZER({ 41, 43 });

file1b.c

#define DEFINE_VARIABLES
#include "file3b.h"  /* Переменные теперь определены и инициализированы */
#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;
}

Понятно, что этот код может выглядеть не так, как Вы бы его написали, но он иллюстрирует общий принцип. Первый аргумент второго вызова INITIALIZER будет { 41 и оставшийся аргумент (единственный в этом примере) будет 43 }. Без C99 или подобной поддержки переменных списков аргумента для макросов, будет очень проблематично создать инициализаторы, в которых должны содержаться скобки.

Следующие два файла предоставляют полный исходный код для 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;
}

Программа prog4 использует prog4.c, file1b.c, file2b.c, prog4.h, file3b.h.

[Защитники заголовков (Header Guards)]

Любой заголовок должен быть защищен от повторного подключения, чтобы определения типов (типы enum, struct или union, или главным образом типы typedef) не вызывали проблем. Стандартная техника - обернуть тело заголовка "защитником" (header guard) наподобие следующего:

#ifndef FILE3B_H_INCLUDED
#define FILE3B_H_INCLUDED
 
... тут содержимое хедера ...
 
#endif /* FILE3B_H_INCLUDED */

Хедер может быть неявно подключен дважды или большее количество раз. Например в файле file4b.h подключает file3b.h для определения типа, который не показан, и file1b.c должен использовать оба заголовка file4b.h и file3b.h, тогда у Вас без защитника будут довольно трудные проблемы. Понятно, что Вы могли бы сделать ревизию списка заголовков для подключения только file4b.h. Однако Вы могли бы не знать о внутренних зависимостях - и в идеале код должен продолжать работать.

Далее это начинает становиться сложным, потому что Вы можете подключить file4b.h перед подключением file3b.h для генерации определений, но обычный header guard в файле file3b.h не позволит сделать повторное подключение заголовка.

Таким образом, Вам нужно подключить тело file3b.h самое большее один раз для деклараций, и самое большее один раз для определений, но возможно Вам понадобились они оба в одной единице трансляции (translation unit, TU — комбинация использования исходного файла и файлов заголовка, которые он использует).

[Множественное подключение с определениями переменных]

Однако это можно реализовать, если допустить некоторые вполне разумные ограничения. Давайте представим новый набор имен файлов:

external.h для макроопределений EXTERN, и т. д.
file1c.h для определения типов (особенно struct oddball, тип oddball_struct).
file2c.h для определения или декларации глобальных переменных.
file3c.c, который определяет глобальные переменные.
file4c.c, который просто использует глобальные переменные.
file5c.c, который показывает, что Вы можете декларировать и затем определять глобальные переменные.
file6c.c, который показывает, что Вы можете определять и затем (попытаться) декларировать глобальные переменные.

В этих примерах модули file5c.c и file6c.c напрямую подключают хедер file2c.h несколько раз, но это самый простой способ показать, что механизм работает. Это означает, что если хедер был неявно подключен дважды, то подключение будет безопасным.

Ограничения для этого следующие:

1. Хедер, декларирующий или определяющий глобальные переменные, не может сам определять какие-либо типы.
2. Сразу перед подключением хедера, который должен определить переменные, Вы определяете макрос DEFINE_VARIABLES.
3. Хедер, который определяет или декларирует переменные, должен иметь стилизованное содержание.

external.h

/*
** Этот хедер не должен содержать header guards (не должно быть
** таких как < assert.h >). Каждый раз при своем вызове этот
** хедер переопределяет макросы EXTERN, INITIALIZE, в зависимости
** от того, определен ли макрос DEFINE_VARIABLES сейчас или нет.
*/
#undef EXTERN
#undef INITIALIZE
 
#ifdef DEFINE_VARIABLES
 #define EXTERN              /* пустота */
#define INITIALIZE(...)     = __VA_ARGS__
#else
 #define EXTERN              extern
 #define INITIALIZE(...)     /* пустота */
#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

/* Стандартный пролог */
#if defined(DEFINE_VARIABLES) && !defined(FILE2C_H_DEFINITIONS)
 #undef FILE2C_H_INCLUDED
#endif
 
#ifndef FILE2C_H_INCLUDED
#define FILE2C_H_INCLUDED
 
#include "external.h"   /* Поддержка макросов EXTERN, INITIALIZE */
#include "file1c.h"     /* Определения типа для структуры oddball */
#if !defined(DEFINE_VARIABLES) || !defined(FILE2C_H_DEFINITIONS)
 
/* Декларации / определения глобальных переменных */
 
EXTERN int global_variable INITIALIZE(37);
EXTERN struct oddball oddball_struct INITIALIZE({ 41, 43 });
 
#endif /* !DEFINE_VARIABLES || !FILE2C_H_DEFINITIONS */
/* Стандартный эпилог */
#ifdef DEFINE_VARIABLES
 #define FILE2C_H_DEFINITIONS
#endif /* DEFINE_VARIABLES */
 
#endif /* FILE2C_H_INCLUDED */

file3c.c

#define DEFINE_VARIABLES
#include "file2c.h"  /* Теперь переменные определены и инициализированы */
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"     /* Декларация переменных */
#define DEFINE_VARIABLES
#include "file2c.h"  /* Теперь переменные определены и инициализированы */
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"     /* Теперь переменные определены и инициализированы */
#include "file2c.h"     /* Декларация переменных */
int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }

Следующий исходный файл показывает полный исходный код (основная программа, содержащая тело функции program) для prog5, prog6 и prog7:

prog5.c (то же самое и для prog6 и 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 использует prog5.c, file3c.c, file4c.c, file1c.h, file2c.h, external.h.
Программа prog6 использует prog5.c, file5c.c, file4c.c, file1c.h, file2c.h, external.h.
Программа prog7 использует prog5.c, file6c.c, file4c.c, file1c.h, file2c.h, external.h.

Эта схема позволяет избежать большинства проблем. Вы столкнетесь с проблемой, только если хедер, который определяет переменные (такой как file2c.h) подключается другим хедером (скажем, хедером file7c.h), который определяет переменные. Нет простого способа обойти эту проблему, отличающегося от "не делай так".

Вы можете частично обойти проблему, пересмотрев подключение file2c.h в file2d.h:

file2d.h

/* Стандартный пролог */
#if defined(DEFINE_VARIABLES) && !defined(FILE2D_H_DEFINITIONS)
 #undef FILE2D_H_INCLUDED
#endif
 
#ifndef FILE2D_H_INCLUDED
#define FILE2D_H_INCLUDED
 
#include "external.h"   /* Поддержка макросов EXTERN, INITIALIZE */
#include "file1c.h"     /* Определение типа для структуры oddball */
 
#if !defined(DEFINE_VARIABLES) || !defined(FILE2D_H_DEFINITIONS)
 
/* Декларации / определения глобальных переменных */
EXTERN int global_variable INITIALIZE(37);
EXTERN struct oddball oddball_struct INITIALIZE({ 41, 43 });
 
#endif /* !DEFINE_VARIABLES || !FILE2D_H_DEFINITIONS */
 
/* Стандартный эпилог */
#ifdef DEFINE_VARIABLES
 #define FILE2D_H_DEFINITIONS
 #undef DEFINE_VARIABLES
#endif /* DEFINE_VARIABLES */
 
#endif /* FILE2D_H_INCLUDED */

Проблема становится в 'должен ли хедер включать #undef DEFINE_VARIABLES?' Если Вы опустите это в заголовке и обернете какой-то вызов #define и #undef ...

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

... в исходном коде (так чтобы заголовки никогда не отменяли значение DEFINE_VARIABLES), то все должно быть чисто. Неприятность только в том, что нужно не забыть писать дополнительную строку. Альтернативой может быть:

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

externdef.h

/*
** Этот хедер не должен содержать header guards (не должно быть
** таких как < assert.h >). Каждый раз при его подключении
** макрос HEADER_DEFINING_VARIABLES должен быть определен
** с именем (в двойных кавычках - или возможно в угловых скобках)
** подключаемого заголовка, который определяет переменные, когда
** определен макрос DEFINE_VARIABLES. См. также external.h
** (который использует DEFINE_VARIABLES и определяет макросы
** EXTERN и INITIALIZE соответственно).
**
** #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 */

Это становится довольно замысловатым, однако должно быть защищенным (при использовании file2d.h без #undef DEFINE_VARIABLES).

file7c.c

/* Декларация переменных */
#include "file2d.h"
 
/* Определение переменных */
#define HEADER_DEFINING_VARIABLES "file2d.h"
#include "externdef.h"
 
/* Еще раз декларация переменных */
#include "file2d.h"
 
/* Еще раз определение переменных */
#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

/* Стандартный пролог */
#if defined(DEFINE_VARIABLES) && !defined(FILE8C_H_DEFINITIONS)
 #undef FILE8C_H_INCLUDED
#endif
 
#ifndef FILE8C_H_INCLUDED
#define FILE8C_H_INCLUDED
 
#include "external.h"   /* Поддержка макросов EXTERN, INITIALIZE */
#include "file2d.h"     /* struct oddball */
 
#if !defined(DEFINE_VARIABLES) || !defined(FILE8C_H_DEFINITIONS)
 
/* Декларации / определения глобальных переменных */
EXTERN struct oddball another INITIALIZE({ 14, 34 });
 
#endif /* !DEFINE_VARIABLES || !FILE8C_H_DEFINITIONS */
 
/* Стандартный пролог */
#ifdef DEFINE_VARIABLES
 #define FILE8C_H_DEFINITIONS
#endif /* DEFINE_VARIABLES */
 
#endif /* FILE8C_H_INCLUDED */

file8c.c

/* Определение переменных */
#define HEADER_DEFINING_VARIABLES "file2d.h"
#include "externdef.h"
 
/* Определение переменных */
#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; }

В следующих двух файлах дан полный исходный код для prog8 и 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;
}

Программа prog8 использует prog8.c, file7c.c, file9c.c.
Программа prog9 использует prog8.c, file8c.c, file9c.c.

Однако проблемы на практике обычно никогда не возникают, если Вы следуете стандартным принципам использования заголовков для декларации и определения переменных.

[Избегайте использования глобальных переменных]

Есть ли еще кто-нибудь, кто пропустил это правило?

Оригинальная схема оставляет для Вас только 2 места модификаций, чтобы сохранять синхронность между декларацией и определением переменной, что намного лучше, чем декларации переменных external, рассыпанные по всему базовому коду (это действительно важно, когда проект состоит из тысячи файлов). Однако код в этих файлах с именами fileNc.[ch] (плюс external.h и externdef.h) показывает, как можно организовать эту работу. И конечно, не было бы трудно создать скрипт для генерации заголовков, чтобы дать Вам стандартизированный шаблон для файла заголовка, определяющего и декларирующего переменные.

[Ссылки]

1. How do I use extern to share variables between source files in C? site:stackoverflow.com.