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

829

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

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

Теги:
global-variables
extern

16 ответов

1569

Использование extern имеет 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 только для 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 - Single Point of Truth).
  • Исходный файл никогда не содержит extern деклараций переменных - исходные файлы всегда включают заголовок (единственный), который их объявляет.
  • Для любой заданной переменной ровно один исходный файл определяет переменную, предпочтительно инициализируя ее. (Хотя нет необходимости явно инициализировать нуль, это не наносит вреда и может принести пользу, потому что в программе может быть только одно инициализированное определение конкретной глобальной переменной).
  • Исходный файл, который определяет переменную, также включает заголовок, чтобы гарантировать, что определение и декларация согласованы.
  • Функция никогда не должна объявлять переменную с использованием extern.
  • Избегайте глобальных переменных, когда это возможно, вместо этого используйте функции.

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

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

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

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

file10.c

#include "prog2.h"

int i;   /* Do not do this in portable code */

void inc(void) { i++; }

file11.c

#include "prog2.h"

int i;   /* Do not do this in portable code */

void dec(void) { i--; }

file12.c

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

int i = 9;   /* Do not do this in portable code */

void put(void) { printf("i = %d\n", i); }

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

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

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

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

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

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

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

J.5.11 Множество внешних определений

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

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


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

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

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

broken_header.h

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

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

seldom_correct.h

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

Очевидно, что код для структуры oddball - это не то, что вы обычно пишете, но это иллюстрирует суть. Первым аргументом второго вызова 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.

Заголовки

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

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

Таким образом, вам нужно включить тело 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

/*
** This header must not contain header guards (like <assert.h> must not).
** Each time it is invoked, it redefines the macros EXTERN, INITIALIZE
** based on whether macro DEFINE_VARIABLES is currently defined.
*/
#undef EXTERN
#undef INITIALIZE

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

/*
** This header must not contain header guards (like <assert.h> must not).
** Each time it is included, the macro HEADER_DEFINING_VARIABLES should
** be defined with the name (in quotes - or possibly angle brackets) of
** the header to be included that defines variables when the macro
** DEFINE_VARIABLES is defined.  See also: external.h (which uses
** DEFINE_VARIABLES and defines macros EXTERN and INITIALIZE
** appropriately).
**
** #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 в 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.

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

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


Означает ли это изложение что-нибудь?

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

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

  • 0
    Вы уверены, что предварительные определения, распределенные по нескольким переводным единицам, благословлены C? В проекте C99 TC3 говорится: «Если модуль перевода содержит одно или несколько предварительных определений для идентификатора, а модуль перевода не содержит внешнего определения для этого идентификатора, то поведение будет таким, как если бы модуль перевода содержал объявление области файла этого идентификатора. с составным типом на конец единицы перевода с инициализатором, равным 0. "
  • 0
    Кажется, это означает, что каждая такая единица перевода содержит внешнее определение для нее и нарушает: «где-то во всей программе должно быть ровно одно внешнее определение для идентификатора; в противном случае [если идентификатор не используется], не должно быть никакого больше одного.". Как я понял, ОБЩИЕ блоки - это нестандартные расширения.
Показать ещё 25 комментариев
107

An extern variable - это объявление (благодаря 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
  • 18
    Там нет "псевдо-определений". Это декларация.
  • 1
    В приведенном выше примере, если я изменяю extern int test1_var; к int test1_var; , компоновщик (gcc 5.4.0) все еще проходит. Итак, действительно ли в этом случае нужен extern ?
Показать ещё 1 комментарий
36

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

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

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

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

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

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

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

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

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

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

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

  • 0
    Память не объявлена. Посмотрите ответы на этот вопрос: stackoverflow.com/questions/1410563 для более подробной информации.
15

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

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

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

8

В C переменная внутри файла say 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 >

7

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

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

6

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 выходных данных readelf или nm может быть полезен, вы не объяснили основы использования extern и не завершили первую программу с фактическим определением. Ваш код даже не использует notExtern . Также есть проблема с номенклатурой: хотя notExtern здесь определен, а не объявлен с extern , это внешняя переменная, к которой могут обращаться другие исходные файлы, если эти единицы перевода содержат подходящее объявление (для которого нужно extern int notExtern; ).
  • 0
    @JonathanLeffler спасибо за отзыв! Стандартные рекомендации по поведению и использованию уже были даны в других ответах, поэтому я решил немного показать реализацию, поскольку это действительно помогло мне понять, что происходит. Не использовать notExtern было некрасиво, это исправили. О номенклатуре, дайте мне знать, если у вас есть лучшее имя. Конечно, это не будет хорошим названием для реальной программы, но я думаю, что она хорошо подходит для дидактической роли.
Показать ещё 1 комментарий
5

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

5

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

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

4

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

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

3

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

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

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

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

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

1

С 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, что может повредить другую переменную. Сложно отлаживать!

Ещё вопросы

Сообщество Overcoder
Наверх
Меню