Как использовать extern для обмена переменными между исходными файлами?


988

Я знаю, что глобальные переменные в C иногда имеют externключевое слово. Что такое externпеременная? Как выглядит декларация? Какова его сфера применения?

Это связано с совместным использованием переменных в исходных файлах, но как это работает точно? Где я могу использовать extern?

Ответы:


1752

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

Важно понимать разницу между определением переменной и объявлением переменной :

  • Переменная объявляется, когда компилятору сообщают, что переменная существует (и это ее тип); он не выделяет хранилище для переменной в этой точке.

  • Переменная определяется, когда компилятор выделяет хранилище для переменной.

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

Лучший способ объявить и определить глобальные переменные

Чистый и надежный способ объявления и определения глобальных переменных - использовать заголовочный файл, содержащий extern объявление переменной.

Заголовок включается одним исходным файлом, который определяет переменную, и всеми исходными файлами, которые ссылаются на переменную. Для каждой программы один исходный файл (и только один исходный файл) определяет переменную. Точно так же один заголовочный файл (и только один заголовочный файл) должен объявить переменную. Заголовочный файл имеет решающее значение; он позволяет осуществлять перекрестную проверку между независимыми TU (единицами перевода - думать исходные файлы) и обеспечивает согласованность.

Хотя есть и другие способы сделать это, этот метод прост и надежен. Это подтверждается file3.h, file1.cи file2.c:

file3.h

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

file1.c

#include "file3.h"  /* Declaration made available here */
#include "prog1.h"  /* Function declarations */

/* Variable defined here */
int global_variable = 37;    /* Definition checked against declaration */

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

file2.c

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

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

Это лучший способ объявить и определить глобальные переменные.


Следующие два файла дополняют источник prog1:

Показанные полные программы используют функции, поэтому объявления функций закрались. И C99, и C11 требуют, чтобы функции были объявлены или определены до их использования (тогда как C90 не сделал этого по веским причинам). Я использую ключевое слово externперед объявлениями функций в заголовках для согласованности - чтобы соответствовать externперед объявлениями переменных в заголовках. Многие люди предпочитают не использовать externперед объявлениями функций; компилятору все равно - и, в конечном счете, я тоже, если вы последовательны, по крайней мере, в исходном файле.

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.

Файл prog1.mkявляется make-файлом prog1только для . Он будет работать с большинством версий, makeвыпущенных примерно на рубеже тысячелетий. Он не привязан специально к 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}

Методические рекомендации

Правила должны нарушаться только экспертами и только по уважительной причине:

  • Заголовочный файл содержит только externобъявления переменных - никогда staticили без определения переменных.

  • Для любой данной переменной только один заголовочный файл объявляет это (SPOT - единая точка истины).

  • Исходный файл никогда не содержит externобъявлений переменных - исходные файлы всегда содержат (единственный) заголовок, который их объявляет.

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

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

  • Функция никогда не должна объявлять переменную используя extern.

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

Исходный код и текст этого ответа доступны в моем репозитории SOQ (вопросы о переполнении стека) на GitHub в подкаталоге src / so-0143-3204 .

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

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

С некоторыми (на самом деле, многими) C-компиляторами вы можете избавиться от так называемого «общего» определения переменной. «Общий» здесь относится к методике, используемой в Fortran для разделения переменных между исходными файлами с использованием (возможно, названного) блока COMMON. Здесь происходит то, что каждый из нескольких файлов дает предварительное определение переменной. Если не более одного файла содержит инициализированное определение, различные файлы в конечном итоге совместно используют одно общее определение переменной:

file10.c

#include "prog2.h"

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

void inc(void) { l++; }

file11.c

#include "prog2.h"

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

void dec(void) { l--; }

file12.c

#include "prog2.h"
#include <stdio.h>

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

void put(void) { printf("l = %ld\n", l); }

Этот метод не соответствует букве стандарта C и «правила одного определения» - это официально неопределенное поведение:

J.2 Неопределенное поведение

Идентификатор с внешней связью используется, но в программе не существует точно одного внешнего определения для идентификатора, или идентификатор не используется, и существует несколько внешних определений для идентификатора (6.9).

§6.9 Внешние определения №5

Внешнее определение является внешним свидетельством того, что является также определение функции (другой , чем определение инлайн) или объекта. Если идентификатор, объявленный с внешней связью, используется в выражении (кроме как как часть операнда оператора sizeofили _Alignof, результатом которого является целочисленная константа), где-то во всей программе должно быть ровно одно внешнее определение для идентификатора; в противном случае их должно быть не больше одного. 161)

161) Таким образом, если идентификатор, объявленный с внешней связью, не используется в выражении, для него не требуется никакого внешнего определения.

Тем не менее, стандарт C также перечисляет его в информативном Приложении J как одно из Общих расширений .

J.5.11 Несколько внешних определений

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

Поскольку этот метод не всегда поддерживается, лучше избегать его использования, особенно если ваш код должен быть переносимым . Используя эту технику, вы также можете получить непреднамеренное наказание.

Если один из файлов выше объявлен lкак doubleвместо, а как long, небезопасные компоновщики C, вероятно, не обнаружит несоответствие. Если вы на машине с 64-битной longи doubleвы не хотели даже получить предупреждение; на компьютерах с 32-разрядной longи 64-разрядной версиями doubleвы, вероятно, получите предупреждение о разных размерах - компоновщик будет использовать самый большой размер, точно так же, как программа на Фортране будет принимать самый большой размер из всех распространенных блоков.

Обратите внимание, что GCC 10.1.0, выпущенный 2020-05-07, изменяет используемые параметры компиляции по умолчанию -fno-common, что означает, что по умолчанию приведенный выше код больше не связывается, если вы не переопределите значение по умолчанию -fcommon(или не используете атрибуты и т. Д. - смотрите ссылку).


Следующие два файла дополняют источник 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.2; §6.9), что является стандартом для выражения «все может произойти». Одна из вещей, которая может произойти, заключается в том, что программа ведет себя так, как вы ожидаете; и в J.5.11 примерно сказано: «Возможно, вам повезет чаще, чем вы заслуживаете». Но программа, которая опирается на несколько определений внешней переменной - с явным ключевым словом extern или без него - не является строго соответствующей программой и не гарантирует, что она будет работать везде. Эквивалентно: это содержит ошибку, которая может показать или не показать себя.

Нарушение руководящих принципов

Есть, конечно, много способов, которыми эти принципы могут быть нарушены. Иногда могут быть веские причины нарушать руководящие принципы, но такие случаи крайне необычны.

faulty_header.h

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

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

broken_header.h

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

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

seldom_correct.h

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

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

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


Резюме

Используйте технику заголовка, которую я показал первым. Работает надежно и везде. Обратите внимание, в частности, что заголовок, объявляющий, global_variableвключен в каждый файл, который его использует, включая тот, который его определяет. Это гарантирует, что все является самосогласованным.

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

Конец оригинального ответа

Если вы не опытный программист на C, вам, вероятно, следует перестать читать здесь.


Позднее крупное дополнение

Как избежать дублирования кода

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

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

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

file3a.h

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

EXTERN int global_variable;

file1a.c

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

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

file2a.c

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

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

Следующие два файла дополняют источник 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 и переменными списками аргументов для макросов вы можете определить макрос для поддержки инициализации. (С C89 и отсутствием поддержки списков переменных аргументов в макросах не существует простого способа обработки произвольно длинных инициализаторов.)

file3b.h

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

EXTERN int global_variable INITIALIZER(37);
EXTERN struct { int a; int b; } oddball_struct INITIALIZER({ 41, 43 });

Обратное содержимое блоков #ifи #elseблоков, исправление ошибки, выявленной Денисом Княжевым

file1b.c

#define DEFINE_VARIABLES
#include "file3b.h"  /* Variables now defined and initialized */
#include "prog4.h"

int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }

file2b.c

#include "file3b.h"
#include "prog4.h"
#include <stdio.h>

void use_them(void)
{
    printf("Global variable: %d\n", global_variable++);
    oddball_struct.a += global_variable;
    oddball_struct.b -= global_variable / 2;
}

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

Правильный заголовок file3b.hвключен (вместо fileba.h) по Денису Княжеву


Следующие два файла дополняют источник 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.

Страж заголовка

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

#ifndef FILE3B_H_INCLUDED
#define FILE3B_H_INCLUDED

...contents of header...

#endif /* FILE3B_H_INCLUDED */

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

Кроме того, он начинает усложняться, потому что вы можете включить его file4b.hперед file3b.hсозданием определений, но обычные средства защиты заголовка file3b.hпредотвратят повторное включение заголовка.

Таким образом, вам нужно включить тело file3b.hне более одного раза для объявлений и не более одного раза для определений, но вам может понадобиться как в одной единице перевода (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


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

file1c.h

#ifndef FILE1C_H_INCLUDED
#define FILE1C_H_INCLUDED

struct oddball
{
    int a;
    int b;
};

extern void use_them(void);
extern int increment(void);
extern int oddball_value(void);

#endif /* FILE1C_H_INCLUDED */

file2c.h


/* Standard prologue */
#if defined(DEFINE_VARIABLES) && !defined(FILE2C_H_DEFINITIONS)
#undef FILE2C_H_INCLUDED
#endif

#ifndef FILE2C_H_INCLUDED
#define FILE2C_H_INCLUDED

#include "external.h"   /* Support macros EXTERN, INITIALIZE */
#include "file1c.h"     /* Type definition for struct oddball */

#if !defined(DEFINE_VARIABLES) || !defined(FILE2C_H_DEFINITIONS)

/* Global variable declarations / definitions */
EXTERN int global_variable INITIALIZE(37);
EXTERN struct oddball oddball_struct INITIALIZE({ 41, 43 });

#endif /* !DEFINE_VARIABLES || !FILE2C_H_DEFINITIONS */

/* Standard epilogue */
#ifdef DEFINE_VARIABLES
#define FILE2C_H_DEFINITIONS
#endif /* DEFINE_VARIABLES */

#endif /* FILE2C_H_INCLUDED */

file3c.c

#define DEFINE_VARIABLES
#include "file2c.h"  /* Variables now defined and initialized */

int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }

file4c.c

#include "file2c.h"
#include <stdio.h>

void use_them(void)
{
    printf("Global variable: %d\n", global_variable++);
    oddball_struct.a += global_variable;
    oddball_struct.b -= global_variable / 2;
}

file5c.c


#include "file2c.h"     /* Declare variables */

#define DEFINE_VARIABLES
#include "file2c.h"  /* Variables now defined and initialized */

int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }

file6c.c


#define DEFINE_VARIABLES
#include "file2c.h"     /* Variables now defined and initialized */

#include "file2c.h"     /* Declare variables */

int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }

Следующий исходный файл завершает источник (обеспечивает основную программу) для prog5, prog6и 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;
}
  • 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

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

Вопрос становится «должен ли заголовок включать #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


#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в file2d.h).

file7c.c

/* Declare variables */
#include "file2d.h"

/* Define variables */
#define HEADER_DEFINING_VARIABLES "file2d.h"
#include "externdef.h"

/* Declare variables - again */
#include "file2d.h"

/* Define variables - again */
#define HEADER_DEFINING_VARIABLES "file2d.h"
#include "externdef.h"

int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }

file8c.h

/* Standard prologue */
#if defined(DEFINE_VARIABLES) && !defined(FILE8C_H_DEFINITIONS)
#undef FILE8C_H_INCLUDED
#endif

#ifndef FILE8C_H_INCLUDED
#define FILE8C_H_INCLUDED

#include "external.h"   /* Support macros EXTERN, INITIALIZE */
#include "file2d.h"     /* struct oddball */

#if !defined(DEFINE_VARIABLES) || !defined(FILE8C_H_DEFINITIONS)

/* Global variable declarations / definitions */
EXTERN struct oddball another INITIALIZE({ 14, 34 });

#endif /* !DEFINE_VARIABLES || !FILE8C_H_DEFINITIONS */

/* Standard epilogue */
#ifdef DEFINE_VARIABLES
#define FILE8C_H_DEFINITIONS
#endif /* DEFINE_VARIABLES */

#endif /* FILE8C_H_INCLUDED */

file8c.c

/* Define variables */
#define HEADER_DEFINING_VARIABLES "file2d.h"
#include "externdef.h"

/* Define variables */
#define HEADER_DEFINING_VARIABLES "file8c.h"
#include "externdef.h"

int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }

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


Тем не менее, на практике эти проблемы относительно маловероятны, особенно если принять стандартные рекомендации

Избегайте глобальных переменных


Эта экспозиция ничего не пропустит?

Признание : схема «избежания дублирования кода», изложенная здесь, была разработана, потому что проблема затрагивает некоторый код, над которым я работаю (но не владею), и вызывает беспокойство по поводу схемы, изложенной в первой части ответа. Тем не менее, оригинальная схема оставляет вам всего два места для изменения, чтобы синхронизировать определения и объявления переменных, что является большим шагом вперед по сравнению с разбросом объявлений внешних переменных по всей базе кода (что действительно имеет значение, когда в общей сложности тысячи файлов) , Однако код в файлах с именами fileNc.[ch](плюс external.hи externdef.h) показывает, что его можно заставить работать. Ясно, что было бы несложно создать скрипт генератора заголовков, который предоставил бы вам стандартизированный шаблон для переменной, определяющей и объявляющей файл заголовка.

NB. Это игрушечные программы с едва достаточным кодом, чтобы сделать их немного интересными. В примерах есть повторение, которое можно удалить, но не для упрощения педагогического объяснения. (Например: разница между prog5.cи prog8.cявляется именем одного из включенных заголовков. Было бы возможно реорганизовать код так, чтобы main()функция не повторялась, но она скрыла бы больше, чем показала.)


3
@litb: см. Приложение J.5.11 для общего определения - это общее расширение.
Джонатан Леффлер

3
@litb: и я согласен, что этого следует избегать - поэтому он находится в разделе «Не очень хороший способ определения глобальных переменных».
Джонатан Леффлер

3
Действительно, это обычное расширение, но для программы это ненадежное поведение. Мне просто было не ясно, говорили ли вы, что это разрешено по собственным правилам Си. Теперь я вижу, что вы говорите, что это обычное расширение, и избегайте его, если ваш код должен быть переносимым. Так что я могу выразить вам без сомнения. Действительно отличный ответ ИМХО :)
Йоханнес Шауб -

19
Если вы остановитесь на вершине, все будет просто. Как вы читаете дальше, он имеет дело с большим количеством нюансов, сложностей и деталей. Я только что добавил две «точки раннего прекращения» для менее опытных программистов на Си - или программистов на Си, которые уже знают предмет. Нет необходимости читать все это, если вы уже знаете ответ (но дайте мне знать, если вы обнаружите техническую ошибку).
Джонатан Леффлер

4
@supercat: мне приходит в голову, что вы можете использовать литералы массива C99, чтобы получить значение перечисления для размера массива, примером которого является ( foo.h): #define FOO_INITIALIZER { 1, 2, 3, 4, 5 }определить инициализатор для массива, enum { FOO_SIZE = sizeof((int [])FOO_INITIALIZER) / sizeof(((int [])FOO_INITIALIZER)[0]) };получить размер массива и extern int foo[];объявить массив , Понятно, что определение должно быть справедливым int foo[FOO_SIZE] = FOO_INITIALIZER;, хотя размер не обязательно должен быть включен в определение. Это дает вам целочисленную константу FOO_SIZE.
Джонатан Леффлер

125

externПеременная является декларация (благодаря SbI для коррекции) переменной , которая определена в другом ЕП. Это означает, что хранилище для переменной размещено в другом файле.

Скажем, у вас есть два .cфайла test1.cи test2.c. Если вы определяете глобальную переменную int test1_var;в, test1.cи вы хотите получить доступ к этой переменной, test2.cвы должны использовать extern int test1_var;в test2.c.

Полный образец:

$ 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
Там нет "псевдо-определений". Это декларация.
SBI

3
В приведенном выше примере, если изменить , extern int test1_var;чтобы int test1_var;линкер (GCC 5.4.0) по- прежнему проходит. Так externдействительно ли нужно в этом случае?
радиоголовка

2
@radiohead: В моем ответе вы найдете информацию о том, что удаление extern- это распространенное расширение, которое часто работает - и в частности работает с GCC (но GCC далеко не единственный компилятор, который его поддерживает; он распространен в системах Unix). Вы можете посмотреть на «J.5.11» или в разделе «Не так хороший способ» в моем ответе (я знаю - это есть давно) и текст рядом , что объясняет его (или пытается сделать это).
Джонатан Леффлер

Декларация extern определенно не должна быть определена в другом модуле перевода (и обычно это не так). На самом деле декларация и определение могут быть одним и тем же.
Помните, Моника

40

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

Таким образом, вы можете решить использовать переменную в модуле перевода, а затем получить к ней доступ из другой, затем во второй вы объявите ее как extern, и символ будет разрешен компоновщиком.

Если вы не объявите его как extern, вы получите 2 переменные с одинаковыми именами, но никак не связанные, и ошибка нескольких определений переменной.


5
Другими словами, модуль перевода, в котором используется extern, знает об этой переменной, ее типе и т. Д. И, следовательно, позволяет исходному коду в базовой логике использовать его, но он не выделяет переменную, это сделает другой модуль перевода. Если бы обе единицы перевода должны были объявить переменную как обычно, было бы эффективно два физических местоположения для переменной со связанными «неправильными» ссылками в скомпилированном коде и с результирующей неоднозначностью для компоновщика.
MJV

26

Мне нравится думать о переменной extern как об обещании, которое вы даете компилятору.

При обнаружении extern компилятор может узнать только его тип, а не место, где он «живет», поэтому он не может разрешить ссылку.

Вы говорите: «Поверь мне. Во время ссылки эта ссылка будет разрешена».


В более общем смысле, объявление - это обещание, что имя будет разрешено до одного определения во время ссылки. Extern объявляет переменную без определения.
Ли Райан

18

extern говорит компилятору доверять вам, что память для этой переменной объявлена ​​в другом месте, поэтому он не пытается выделить / проверить память.

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

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


Память не объявлена. См. Ответы на этот вопрос: stackoverflow.com/questions/1410563 для получения дополнительной информации.
SBI

15

Добавление externпеременной превращает определение переменной в объявление переменной . Посмотрите эту ветку о разнице между объявлением и определением.


Какая разница между int fooи extern int foo(область действия файла)? Оба являются декларацией, не так ли?

@ user14284: Они оба декларации только в том смысле, что каждое определение тоже декларация. Но я связался с объяснением этого. («Посмотрите эту ветку о разнице между объявлением и определением».) Почему бы вам просто не перейти по ссылке и не прочитать?
2012 г.

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

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

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


11

Правильная интерпретация extern заключается в том, что вы говорите что-то компилятору. Вы сообщаете компилятору, что, несмотря на то, что он не присутствует прямо сейчас, объявленная переменная каким-то образом будет найдена компоновщиком (обычно в другом объекте (файле)). Тогда линкер станет счастливчиком, который все найдет и соберет, независимо от того, были ли у вас какие-то внешние заявления или нет.


8

В C переменная внутри файла, скажем example.c, имеет локальную область видимости. Компилятор ожидает, что переменная будет иметь свое определение в том же файле example.c, и когда он не найдет его, он выдаст ошибку. С другой стороны, функция имеет глобальную область видимости по умолчанию. Таким образом, вам не нужно явно упоминать компилятору «смотрите, чувак ... вы можете найти определение этой функции здесь». Для функции, включающей файл, содержащий ее объявление, достаточно (файл, который вы фактически называете заголовочным файлом). Например, рассмотрим следующие 2 файла:
example.c

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

example1.c

int a = 5;

Теперь, когда вы скомпилируете два файла вместе, используйте следующие команды:

шаг 1) cc -o ex example.c example1.c шаг 2) ./ ex

Вы получите следующий вывод: значение a <5>


8

GCC ELF Linux реализация

Другие ответы охватили сторону использования языка, так что теперь давайте посмотрим, как это реализовано в этой реализации.

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

Компилировать и декомпилировать:

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

Выход содержит:

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

В главе « Обновление спецификации ELF » System V ABI объясняется:

SHN_UNDEF Этот индекс таблицы разделов означает, что символ не определен. Когда редактор ссылок объединяет этот объектный файл с другим, который определяет указанный символ, ссылки этого файла на символ будут связаны с фактическим определением.

что в основном такое поведение, которое стандарт C дает extern переменным.

Теперь линкер должен создавать финальную программу, но extern информация уже извлечена из исходного кода в объектный файл.

Протестировано на GCC 4.8.

C ++ 17 встроенных переменных

В C ++ 17 вы можете захотеть использовать встроенные переменные вместо внешних, поскольку они просты в использовании (могут быть определены только один раз в заголовке) и более мощны (поддерживают constexpr). Смотрите: что означает «const static» в C и C ++?


3
Это не мой отрицательный голос, поэтому я не знаю. Тем не менее, я предложу мнение. Хотя просмотр результатов readelfили nmможет быть полезен, вы не объяснили основы того, как использовать extern, и не завершили первую программу с фактическим определением. Ваш код даже не использует notExtern. Также есть проблема с номенклатурой: хотя notExternона определена здесь, а не объявлена ​​с помощью extern, это внешняя переменная, к которой могут обращаться другие исходные файлы, если эти единицы перевода содержат подходящее объявление (которое потребуется extern int notExtern;!).
Джонатан Леффлер

1
@JonathanLeffler спасибо за отзыв! Стандартные рекомендации по поведению и использованию уже были даны в других ответах, поэтому я решил немного показать реализацию, так как это действительно помогло мне понять, что происходит. Не использовать notExternбыло некрасиво, исправлено. О номенклатуре, дайте мне знать, если у вас есть лучшее имя. Конечно, это не будет хорошим названием для реальной программы, но я думаю, что она хорошо подходит для дидактической роли.
Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功

Что касается имен, как насчет global_defпеременной, определенной здесь, и extern_refпеременной, определенной в каком-то другом модуле? Будут ли они иметь достаточно четкую симметрию? В int extern_ref = 57;файле, в котором оно определено, вы все равно или что-то подобное, так что имя не совсем идеальное, но в контексте одного исходного файла это разумный выбор. Имея extern int global_def;в заголовке не столько проблема, как мне кажется. Конечно, целиком и полностью за вами.
Джонатан Леффлер

7

Ключевое слово extern используется с переменной для ее идентификации в качестве глобальной переменной.

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


5

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

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


5

extern просто означает, что переменная определена в другом месте (например, в другом файле).


4

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

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


3

externиспользуется, чтобы один first.cфайл мог иметь полный доступ к глобальному параметру в другом second.cфайле.

externМожет быть объявлен в first.cфайле или в любом из файлов заголовков first.cвключает в себя.


3
Обратите внимание, что externобъявление должно быть в заголовке, а не в first.c, так что если тип изменится, объявление тоже изменится. Кроме того, заголовок, который объявляет переменную, должен быть включен, second.cчтобы гарантировать, что определение согласуется с объявлением. Объявление в заголовке - это клей, который скрепляет все это; он позволяет компилировать файлы отдельно, но обеспечивает согласованное представление о типе глобальной переменной.
Джонатан Леффлер

2

С xc8 вы должны быть осторожны, объявляя переменную одного и того же типа в каждом файле, так как вы можете ошибочно объявить что-то intв одном файле иchar сказать в другом. Это может привести к повреждению переменных.

Эта проблема была элегантно решена на форуме по микрочипам около 15 лет назад / * См. «Http: www.htsoft.com» // «forum / all / showflat.php / Cat / 0 / Number / 18766 / an / 0 / page / 0 # 18766"

Но эта ссылка, похоже, больше не работает ...

Поэтому я быстро попытаюсь объяснить это; сделать файл с именем global.h.

В нем заявляют следующее

#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

Теперь в файле main.c

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

Это означает, что в main.c переменная будет объявлена ​​как unsigned char.

Теперь в других файлах, включая global.h, он будет объявлен как extern для этого файла .

extern unsigned char testing_mode;

Но это будет правильно объявлено как unsigned char.

Старый пост на форуме, вероятно, объяснил это немного более четко. Но это реальный потенциал gotchaпри использовании компилятора, который позволяет вам объявить переменную в одном файле, а затем объявить ее extern как другой тип в другом. Проблемы, связанные с этим, заключаются в том, что если вы скажете, что в другом файле объявлен testing_mode как int, то это будет думать, что это 16-битное var, и перезапишет какую-то другую часть ram, что может повредить другую переменную. Сложно отлаживать!


0

Очень короткое решение, которое я использую, чтобы файл заголовка содержал внешнюю ссылку или фактическую реализацию объекта. Файл, который на самом деле содержит объект, просто делает#define GLOBAL_FOO_IMPLEMENTATION . Затем, когда я добавляю новый объект в этот файл, он обнаруживается в этом файле, и мне не нужно копировать и вставлять определение.

Я использую этот шаблон для нескольких файлов. Поэтому для того, чтобы все было как можно более автономно, я просто повторно использую один макрос GLOBAL в каждом заголовке. Мой заголовок выглядит так:

//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
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.