Я знаю, что глобальные переменные в C иногда имеют ключевое слово extern
. Что такое переменная extern
? Что такое декларация? Какова его область действия?
Это связано с обменом переменными в исходных файлах, но как это работает? Где я могу использовать extern
?
Использование extern
имеет extern
только в том случае, когда создаваемая вами программа состоит из нескольких исходных файлов, связанных друг с другом, где некоторые из переменных, определенных, например, в исходном файле file1.c
должны быть указаны в других исходных файлах, таких как file2.c
.
Важно понимать разницу между определением переменной и объявлением переменной:
Вы можете объявить переменную несколько раз (хотя один раз достаточно); вы можете определить его только один раз в заданной области. Определение переменной также является объявлением, но не все объявления переменных являются определениями.
Чистый, надежный способ объявить и определить глобальные переменные - использовать заголовочный файл, чтобы содержать объявление extern
переменной.
Заголовок включается одним исходным файлом, который определяет переменную и всеми исходными файлами, ссылающимися на переменную. Для каждой программы один параметр (и только один исходный файл) определяет переменную. Аналогично, один заголовочный файл (и только один заголовочный файл) должен объявить переменную. Файл заголовка имеет решающее значение; он позволяет осуществлять перекрестную проверку между независимыми TU (единицами перевода - исходными файлами) и обеспечивает согласованность.
Хотя есть и другие способы сделать это, этот метод прост и надежен. Это демонстрируется file3.h
, file1.c
и file2.c
:
extern int global_variable; /* Declaration of the variable */
#include "file3.h" /* Declaration made available here */
#include "prog1.h" /* Function declarations */
/* Variable defined here */
int global_variable = 37; /* Definition checked against declaration */
int increment(void) { return global_variable++; }
#include "file3.h"
#include "prog1.h"
#include <stdio.h>
void use_it(void)
{
printf("Global variable: %d\n", global_variable++);
}
Это лучший способ объявить и определить глобальные переменные.
Следующие два файла завершают источник для prog1
:
В приведенных программах показаны функции, поэтому в них вписались декларации функций. Оба C99 и C11 требуют, чтобы функции объявлялись или определялись до их использования (в то время как C90 по уважительным причинам). Я использую ключевое слово extern
перед декларациями функций в заголовках для согласованности - для соответствия extern
перед объявлениями переменных в заголовках. Многие люди предпочитают не использовать extern
перед декларациями функций; компилятору все равно - и в конечном итоге, я не до тех пор, пока вы согласны, по крайней мере, в исходном файле.
extern void use_it(void);
extern int increment(void);
#include "file3.h"
#include "prog1.h"
#include <stdio.h>
int main(void)
{
use_it();
global_variable += 19;
use_it();
printf("Increment: %d\n", increment());
return 0;
}
prog1
использует prog1.c
, file1.c
, file2.c
, file3.h
и prog1.h
. Файл prog1.mk
является make- prog1
только для prog1
. Она будет работать с большинством версий make
производства, так как о рубеже тысячелетий. Он не привязан специально к GNU Make.
# Minimal makefile for prog1
PROGRAM = prog1
FILES.c = prog1.c file1.c file2.c
FILES.h = prog1.h file3.h
FILES.o = ${FILES.c:.c=.o}
CC = gcc
SFLAGS = -std=c11
GFLAGS = -g
OFLAGS = -O3
WFLAG1 = -Wall
WFLAG2 = -Wextra
WFLAG3 = -Werror
WFLAG4 = -Wstrict-prototypes
WFLAG5 = -Wmissing-prototypes
WFLAGS = ${WFLAG1} ${WFLAG2} ${WFLAG3} ${WFLAG4} ${WFLAG5}
UFLAGS = # Set on command line only
CFLAGS = ${SFLAGS} ${GFLAGS} ${OFLAGS} ${WFLAGS} ${UFLAGS}
LDFLAGS =
LDLIBS =
all: ${PROGRAM}
${PROGRAM}: ${FILES.o}
${CC} -o $@ ${CFLAGS} ${FILES.o} ${LDFLAGS} ${LDLIBS}
prog1.o: ${FILES.h}
file1.o: ${FILES.h}
file2.o: ${FILES.h}
# If it exists, prog1.dSYM is a directory on macOS
DEBRIS = a.out core *~ *.dSYM
RM_FR = rm -fr
clean:
${RM_FR} ${FILES.o} ${PROGRAM} ${DEBRIS}
Правила должны быть нарушены только экспертами и только с полным основанием:
extern
переменных - никогда не static
или неквалифицированные определения переменных.extern
деклараций переменных - исходные файлы всегда включают заголовок (единственный), который их объявляет.extern
.Исходный код и текст этого ответа доступны в моем репозитории SOQ (Qaru Questions) в GitHub в подкаталоге src/so-0143-3204.
Если вы не опытный программист на C, вы можете (и, возможно, должны) перестать читать здесь.
С некоторыми (действительно, многими) компиляторами C вы можете избавиться от так называемого "общего" определения переменной. "Common" здесь относится к методу, используемому в Fortran для обмена переменными между исходными файлами, используя (возможно, называемый) COMMON-блок. Что здесь происходит, так это то, что каждый из нескольких файлов содержит предварительное определение переменной. Если не более одного файла предоставляет инициализированное определение, то различные файлы в конечном итоге используют общее одно определение переменной:
#include "prog2.h"
int i; /* Do not do this in portable code */
void inc(void) { i++; }
#include "prog2.h"
int i; /* Do not do this in portable code */
void dec(void) { i--; }
#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 и "одному правилу определения" - это официально неопределенное поведение:
Идентификатор с внешней связью используется, но в программе не существует только одного внешнего определения для идентификатора, или идентификатор не используется, и существует множество внешних определений для идентификатора (6.9).
Внешнее определение - это внешнее объявление, которое также является определением функции (отличной от встроенного определения) или объекта. Если идентификатор, объявленный с внешней связью, используется в выражении (кроме как в части операнда оператора
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
:
extern void dec(void);
extern void put(void);
extern void inc(void);
#include "prog2.h"
#include <stdio.h>
int main(void)
{
inc();
put();
dec();
put();
dec();
put();
}
prog2
использует prog2.c
, file10.c
, file11.c
, file12.c
, prog2.h
.Как отмечено в комментариях здесь, и, как указано в моем ответе на аналогичный вопрос, использование нескольких определений для глобальной переменной приводит к неопределенному поведению (J.2, §6.9), что является стандартным способом сказать "что-нибудь может случиться". Одна из вещей, которая может случиться, заключается в том, что программа ведет себя так, как вы ожидаете; и J.5.11 говорит примерно так: "вам может повезти чаще, чем вы этого заслуживаете". Но программа, которая опирается на несколько определений внешней переменной - с или без явного ключевого слова "extern" - не является строго соответствующей программой и не гарантируется работать повсюду. Эквивалентно: в нем содержится ошибка, которая может показаться или не отображаться.
Разумеется, существует множество способов устранения этих рекомендаций. Иногда может быть веская причина нарушать рекомендации, но такие случаи чрезвычайно необычны.
int some_var; /* Do not do this in a header!!! */
Примечание 1: если заголовок определяет переменную без ключевого слова extern
, то каждый файл, содержащий заголовок, создает предварительное определение переменной. Как отмечалось ранее, это часто будет работать, но стандарт C не гарантирует, что он будет работать.
int some_var = 13; /* Only one source file in a program can use this */
Примечание 2: если заголовок определяет и инициализирует переменную, тогда только один исходный файл в данной программе может использовать заголовок. Поскольку заголовки предназначены в основном для обмена информацией, немного глупо создавать тот, который можно использовать только один раз.
static int hidden_global = 3; /* Each source file gets its own copy */
Примечание 3: если заголовок определяет статическую переменную (с инициализацией или без нее), то каждый исходный файл заканчивается своей частной версией "глобальной" переменной.
Например, если переменная представляет собой сложный массив, это может привести к крайнему дублированию кода. Иногда это может быть разумным способом добиться определенного эффекта, но это очень необычно.
Сначала используйте технику заголовка, которую я показал. Он работает надежно и везде. Обратите внимание, в частности, что заголовок, объявляющий global_variable
, включен в каждый используемый файл - в том числе тот, который его определяет. Это гарантирует, что все самосогласовано.
Аналогичные проблемы возникают с объявлением и определением функций - применяются аналогичные правила. Но вопрос касался только переменных, поэтому я сохранил ответ только на переменные.
Если вы не опытный программист на C, вы, вероятно, должны перестать читать здесь.
Позднее значительное дополнение
Одна из проблем, которая иногда (и законно) возникает в связи с описанным здесь механизмом "объявления в заголовках, определениях в источнике", заключается в том, что два синхронизируемых файла - заголовок и источник. Обычно это сопровождается наблюдением, что макрос можно использовать так, чтобы заголовок обслуживал двойную нагрузку - обычно объявляя переменные, но когда определенный макрос установлен до включения заголовка, он вместо этого определяет переменные.
Еще одна проблема может заключаться в том, что переменные должны быть определены в каждом из нескольких "основных программ". Обычно это ложная забота; вы можете просто ввести исходный файл C, чтобы определить переменные и связать файл объекта, созданный с каждой из программ.
Типичная схема работает так, используя исходную глобальную переменную, показанную в file3.h
:
#ifdef DEFINE_VARIABLES
#define EXTERN /* nothing */
#else
#define EXTERN extern
#endif /* DEFINE_VARIABLES */
EXTERN int global_variable;
#define DEFINE_VARIABLES
#include "file3a.h" /* Variable defined - but not initialized */
#include "prog3.h"
int increment(void) { return global_variable++; }
#include "file3a.h"
#include "prog3.h"
#include <stdio.h>
void use_it(void)
{
printf("Global variable: %d\n", global_variable++);
}
Следующие два файла завершают источник для prog3
:
extern void use_it(void);
extern int increment(void);
#include "file3a.h"
#include "prog3.h"
#include <stdio.h>
int main(void)
{
use_it();
global_variable += 19;
use_it();
printf("Increment: %d\n", increment());
return 0;
}
prog3
использует prog3.c
, file1a.c
, file2a.c
, file3a.h
, prog3.h
.Проблема с этой схемой, как показано, заключается в том, что она не предусматривает инициализацию глобальной переменной. С C99 или C11 и списками переменных аргументов для макросов вы также можете определить макрос для поддержки инициализации. (С C89 и без поддержки списков переменных аргументов в макросах нет простого способа обработки условно длинных инициализаторов.)
#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
, исправление ошибок, выявленных Денисом Княжевым
#define DEFINE_VARIABLES
#include "file3b.h" /* Variables now defined and initialized */
#include "prog4.h"
int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }
#include "file3b.h"
#include "prog4.h"
#include <stdio.h>
void use_them(void)
{
printf("Global variable: %d\n", global_variable++);
oddball_struct.a += global_variable;
oddball_struct.b -= global_variable / 2;
}
Очевидно, что код для структуры oddball - это не то, что вы обычно пишете, но это иллюстрирует суть. Первым аргументом второго вызова INITIALIZER
является { 41
а оставшийся аргумент (единственный в этом примере) равен 43 }
. Без C99 или аналогичной поддержки для списков переменных аргументов для макросов, инициализаторы, которые должны содержать запятые, очень проблематичны.
Исправлен заголовок file3b.h
(вместо fileba.h
) за Дениса Княжева
Следующие два файла завершают источник для prog4
:
extern int increment(void);
extern int oddball_value(void);
extern void use_them(void);
#include "file3b.h"
#include "prog4.h"
#include <stdio.h>
int main(void)
{
use_them();
global_variable += 19;
use_them();
printf("Increment: %d\n", increment());
printf("Oddball: %d\n", oddball_value());
return 0;
}
prog4
использует 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
несколько раз, но это самый простой способ показать, что механизм работает. Это означает, что если заголовок косвенно включался дважды, это также было бы безопасно.
Ограничения для этого:
/*
** 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 */
#ifndef FILE1C_H_INCLUDED
#define FILE1C_H_INCLUDED
struct oddball
{
int a;
int b;
};
extern void use_them(void);
extern int increment(void);
extern int oddball_value(void);
#endif /* FILE1C_H_INCLUDED */
/* Standard prologue */
#if defined(DEFINE_VARIABLES) && !defined(FILE2C_H_DEFINITIONS)
#undef FILE2C_H_INCLUDED
#endif
#ifndef FILE2C_H_INCLUDED
#define FILE2C_H_INCLUDED
#include "external.h" /* Support macros EXTERN, INITIALIZE */
#include "file1c.h" /* Type definition for struct oddball */
#if !defined(DEFINE_VARIABLES) || !defined(FILE2C_H_DEFINITIONS)
/* Global variable declarations / definitions */
EXTERN int global_variable INITIALIZE(37);
EXTERN struct oddball oddball_struct INITIALIZE({ 41, 43 });
#endif /* !DEFINE_VARIABLES || !FILE2C_H_DEFINITIONS */
/* Standard epilogue */
#ifdef DEFINE_VARIABLES
#define FILE2C_H_DEFINITIONS
#endif /* DEFINE_VARIABLES */
#endif /* FILE2C_H_INCLUDED */
#define DEFINE_VARIABLES
#include "file2c.h" /* Variables now defined and initialized */
int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }
#include "file2c.h"
#include <stdio.h>
void use_them(void)
{
printf("Global variable: %d\n", global_variable++);
oddball_struct.a += global_variable;
oddball_struct.b -= global_variable / 2;
}
#include "file2c.h" /* Declare variables */
#define DEFINE_VARIABLES
#include "file2c.h" /* Variables now defined and initialized */
int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }
#define DEFINE_VARIABLES
#include "file2c.h" /* Variables now defined and initialized */
#include "file2c.h" /* Declare variables */
int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }
Следующий исходный файл завершает исходный код (предоставляет основную программу) для prog5
, 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
file2c.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"
/*
** 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
).
/* Declare variables */
#include "file2d.h"
/* Define variables */
#define HEADER_DEFINING_VARIABLES "file2d.h"
#include "externdef.h"
/* Declare variables - again */
#include "file2d.h"
/* Define variables - again */
#define HEADER_DEFINING_VARIABLES "file2d.h"
#include "externdef.h"
int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }
/* Standard prologue */
#if defined(DEFINE_VARIABLES) && !defined(FILE8C_H_DEFINITIONS)
#undef FILE8C_H_INCLUDED
#endif
#ifndef FILE8C_H_INCLUDED
#define FILE8C_H_INCLUDED
#include "external.h" /* Support macros EXTERN, INITIALIZE */
#include "file2d.h" /* struct oddball */
#if !defined(DEFINE_VARIABLES) || !defined(FILE8C_H_DEFINITIONS)
/* Global variable declarations / definitions */
EXTERN struct oddball another INITIALIZE({ 14, 34 });
#endif /* !DEFINE_VARIABLES || !FILE8C_H_DEFINITIONS */
/* Standard epilogue */
#ifdef DEFINE_VARIABLES
#define FILE8C_H_DEFINITIONS
#endif /* DEFINE_VARIABLES */
#endif /* FILE8C_H_INCLUDED */
/* Define variables */
#define HEADER_DEFINING_VARIABLES "file2d.h"
#include "externdef.h"
/* Define variables */
#define HEADER_DEFINING_VARIABLES "file8c.h"
#include "externdef.h"
int increment(void) { return global_variable++; }
int oddball_value(void) { return oddball_struct.a + oddball_struct.b; }
Следующие два файла завершают источник для prog8
и prog9
:
#include "file2d.h"
#include <stdio.h>
int main(void)
{
use_them();
global_variable += 19;
use_them();
printf("Increment: %d\n", increment());
printf("Oddball: %d\n", oddball_value());
return 0;
}
#include "file2d.h"
#include <stdio.h>
void use_them(void)
{
printf("Global variable: %d\n", global_variable++);
oddball_struct.a += global_variable;
oddball_struct.b -= global_variable / 2;
}
prog8
использует 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()
не повторялась, но она скрывала бы больше чем это показало.)
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
extern int test1_var;
к int test1_var;
, компоновщик (gcc 5.4.0) все еще проходит. Итак, действительно ли в этом случае нужен extern
?
Extern - это ключевое слово, которое вы используете, чтобы объявить, что сама переменная находится в другой единицы перевода.
Таким образом, вы можете решить использовать переменную в блоке трансляции, а затем получить доступ к ней из другой, а затем во втором вы объявите ее как extern, и символ будет разрешен компоновщиком.
Если вы не объявите его как extern, вы получите две переменные с именем одинаковые, но не связанные вообще, и ошибку нескольких определений переменной.
Мне нравится думать о переменной extern в качестве обещания, которое вы делаете в компиляторе.
При встрече с extern компилятор может узнать только его тип, а не там, где он "живет", поэтому он не может разрешить ссылку.
Вы говорите: "Поверьте мне. В момент ссылки эта ссылка будет разрешимой".
extern сообщает компилятору доверять вам, что память для этой переменной объявлена в другом месте, поэтому она не пытается выделить/проверить память.
Поэтому вы можете скомпилировать файл, который ссылается на extern, но вы не можете связать, если эта память не была объявлена где-то.
Полезно для глобальных переменных и библиотек, но опасно, потому что компоновщик не печатает проверку.
Добавление extern
превращает определение переменной в объявление переменной. См. этот поток относительно разницы между объявлением и определением.
int foo
и extern int foo
(область действия файла)? Оба являются декларацией, не так ли?
Правильная интерпретация extern заключается в том, что вы сообщаете что-то компилятору. Вы сообщаете компилятору, что, несмотря на то, что он не присутствует прямо сейчас, объявленная переменная каким-то образом будет найдена компоновщиком (как правило, в другом объекте (файле)). Компилятор тогда будет счастливым парнем, чтобы найти все и собрать его вместе, были ли у вас какие-то объявления extern или нет.
В 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 >
ключевое слово extern используется с переменной для ее идентификации как глобальной переменной.
Он также означает, что вы можете использовать переменную, объявленную с использованием extern ключевое слово в любом файле, хотя оно объявлено/определено в другом файле.
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++?
readelf
выходных данных readelf
или nm
может быть полезен, вы не объяснили основы использования extern
и не завершили первую программу с фактическим определением. Ваш код даже не использует notExtern
. Также есть проблема с номенклатурой: хотя notExtern
здесь определен, а не объявлен с extern
, это внешняя переменная, к которой могут обращаться другие исходные файлы, если эти единицы перевода содержат подходящее объявление (для которого нужно extern int notExtern;
).
notExtern
было некрасиво, это исправили. О номенклатуре, дайте мне знать, если у вас есть лучшее имя. Конечно, это не будет хорошим названием для реальной программы, но я думаю, что она хорошо подходит для дидактической роли.
extern
просто означает, что переменная определена в другом месте (например, в другом файле).
extern
позволяет одному модулю вашей программы получить доступ к глобальной переменной или функции, объявленной в другом модуле вашей программы.
Обычно у вас есть внешние переменные, объявленные в файлах заголовков.
Если вы не хотите, чтобы программа обращалась к вашим переменным или функциям, вы используете static
, который сообщает компилятору, что эту переменную или функцию нельзя использовать вне этого модуля.
Во-первых, ключевое слово extern
не используется для определения переменной; скорее он используется для объявления переменной. Я могу сказать, что extern
- это класс хранения, а не тип данных.
extern
используется, чтобы другие C файлы или внешние компоненты знали, что эта переменная уже определена где-то. Пример: если вы создаете библиотеку, не нужно обязательно определять глобальную переменную где-то в самой библиотеке. Библиотека будет скомпилирована напрямую, но при связывании файла она проверяет определение.
extern
используется, поэтому один файл first.c
может иметь полный доступ к глобальному параметру в другом файле second.c
.
extern
может быть объявлен в файле first.c
или в любом из файлов заголовков first.c
.
extern
должно быть в заголовке, а не в first.c
, так что если тип изменится, объявление тоже изменится. Кроме того, заголовок, который объявляет переменную, должен быть включен в second.c
чтобы гарантировать, что определение соответствует объявлению. Объявление в заголовке - это клей, который скрепляет все это; он позволяет компилировать файлы отдельно, но обеспечивает согласованное представление о типе глобальной переменной.
declare | define | initialize |
----------------------------------
extern int a; yes no no
-------------
int a = 2019; yes yes yes
-------------
int a; yes yes no
-------------
Объявление не будет выделять память (переменная должна быть определена для выделения памяти), но определение будет. Это просто еще один простой взгляд на ключевое слово extern, поскольку другие ответы действительно хороши.
С 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, что может повредить другую переменную. Сложно отлаживать!