Відповіді:
Використання 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
є лише файлом makefile 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
або некваліфіковані визначення змінних.
Для будь-якої заданої змінної оголошує її лише один заголовок (SPOT - Єдина точка істини).
Вихідний файл ніколи не містить extern
декларацій змінних - вихідні файли завжди містять (єдиний) заголовок, який їх оголошує.
Для будь-якої заданої змінної рівно один вихідний файл визначає змінну, переважно, ініціалізуючи її. (Хоча немає необхідності явно ініціалізуватись до нуля, це не приносить шкоди і може принести користь, оскільки в програмі може бути лише одне ініціалізоване визначення певної глобальної змінної).
Вихідний файл, що визначає змінну, також включає заголовок, щоб забезпечити відповідність визначення та декларації.
Функція ніколи не повинна оголошувати змінну за допомогою extern
.
Уникайте глобальних змінних, коли це можливо - замість цього використовуйте функції.
Вихідний код і текст цієї відповіді доступні в моєму сховищі SOQ (Питання переповнення стека) в GitHub в підкаталозі src / so-0143-3204 .
Якщо ви не досвідчений програміст на С, ви можете (і, можливо, повинні) перестати читати тут.
З деякими (дійсно багатьма) компіляторами C ви можете піти з того, що називається "загальним" визначенням змінної. Тут "загальне" відноситься до техніки, що використовується у Fortran для обміну змінними між вихідними файлами, використовуючи (можливо, названий) блок COMMON. Тут відбувається те, що кожен із ряду файлів забезпечує попереднє визначення змінної. Поки не більше ніж один файл забезпечує ініціалізоване визначення, то різні файли в кінцевому підсумку мають спільне єдине визначення змінної:
#include "prog2.h"
long l; /* Do not do this in portable code */
void inc(void) { l++; }
#include "prog2.h"
long l; /* Do not do this in portable code */
void dec(void) { l--; }
#include "prog2.h"
#include <stdio.h>
long l = 9; /* Do not do this in portable code */
void put(void) { printf("l = %ld\n", l); }
Ця методика не відповідає букві стандарту С та "одному правилу визначення" - це офіційно невизначена поведінка:
Використовується ідентифікатор із зовнішнім зв’язком, але в програмі не існує точно одного зовнішнього визначення для ідентифікатора, або ідентифікатор не використовується, і існує кілька зовнішніх визначень для ідентифікатора (6.9).
Зовнішнє визначення є зовнішнім свідченням того, що є також визначення функції (інший , ніж визначення інлайн) або об'єкта. Якщо ідентифікатор, оголошений із зовнішнім зв’язком, використовується в виразі (крім частини операнду оператора
sizeof
чи_Alignof
оператора, результатом якого є ціла константа), десь у всій програмі має бути саме одне зовнішнє визначення для ідентифікатора; інакше не повинно бути більше одного. 161)161) Таким чином, якщо ідентифікатор, оголошений із зовнішнім зв’язком, не використовується в виразі, для нього не повинно бути зовнішнього визначення.
Однак стандарт C також перераховує його в інформаційному додатку J як одне із поширених розширень .
J.5.11 Кілька зовнішніх визначень
Для ідентифікатора об’єкта може бути більше одного зовнішнього визначення, з явним використанням ключового слова extern; якщо визначення не погоджуються або ініціалізується більше, поведінка не визначена (6.9.2).
Оскільки ця методика не завжди підтримується, краще уникати її використання, особливо якщо ваш код повинен бути переносним . Використовуючи цю техніку, ви також можете закінчитися ненавмисним наказом типу.
Якщо один з файлів вище оголошено l
як double
замість того , щоб, як long
, типу небезпечних лінкери C, ймовірно , не помітять невідповідність. Якщо ви працюєте на машині з 64-бітною версієюlong
і double
ви не хотіли навіть отримати попередження; На машині з 32-розрядною long
і 64-розрядною версією double
ви, мабуть, отримаєте попередження про різні розміри - лінкер буде використовувати найбільший розмір саме так, як програма Fortran приймала б найбільший розмір усіх загальних блоків.
Зауважте, що GCC 10.1.0, який був випущений 2020-05-07, змінює параметри компіляції за замовчуванням для використання -fno-common
, що означає, що за замовчуванням код вище не посилається, якщо ви не заміните за замовчуванням-fcommon
(або використовуєте атрибути тощо) - дивіться посилання).
Наступні два файли заповнюють джерело для 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" або без нього - не є суворо відповідною програмою і не гарантовано працювати всюди. Еквівалентно: він містить помилку, яка може або не може проявити себе.
Звичайно, існує безліч способів порушення цих керівних принципів. Інколи може бути вагомий привід для порушення правил, але такі випадки надзвичайно незвичні.
c int some_var; /* Do not do this in a header!!! */
Примітка 1: якщо заголовок визначає змінну без extern
ключового слова, то кожен файл, що включає заголовок, створює попереднє визначення змінної. Як зазначалося раніше, це часто працюватиме, але стандарт C не гарантує, що він буде працювати.
c int some_var = 13; /* Only one source file in a program can use this */
Примітка 2: якщо заголовок визначає та ініціалізує змінну, то лише один вихідний файл у даній програмі може використовувати заголовок. Оскільки заголовки в основному призначені для обміну інформацією, створити таку, яка може бути використана лише один раз, трохи нерозумно.
c static int hidden_global = 3; /* Each source file gets its own copy */
Примітка 3: якщо заголовок визначає статичну змінну (з ініціалізацією або без неї), то кожен вихідний файл закінчується власною приватною версією 'глобальної' змінної.
Якщо змінна насправді є складним масивом, наприклад, це може призвести до крайнього дублювання коду. Це дуже часто може бути розумним способом досягти певного ефекту, але це дуже незвично.
Використовуйте техніку заголовка, яку я показав першим. Працює надійно і скрізь. Зауважте, зокрема, що заголовок, що декларує global_variable
, міститься у кожному файлі, який його використовує - включаючи той, який визначає його. Це гарантує, що все самопослідовно.
Аналогічні проблеми виникають і при оголошенні та визначенні функцій - застосовуються аналогічні правила. Але питання стосувалося конкретних змінних, тож я відповів лише на змінні.
Якщо ви не досвідчений програміст на С, вам, мабуть, варто припинити читання тут.
Пізня основна добавка
Одне занепокоєння, яке інколи (і на законних підставах) викликає механізм "декларацій у заголовках, визначення джерел", описаний тут, - це два файли, які слід синхронізувати - заголовок та джерело. Зазвичай це супроводжується зауваженням, що макрос можна використовувати так, щоб заголовок виконував подвійний обов'язок - зазвичай декларування змінних, але коли певний макрос встановлюється перед включенням заголовка, він замість цього визначає змінні.
Інше занепокоєння може викликати те, що змінні потрібно визначити в кожній з ряду "основних програм". Зазвичай це помилкова проблема; ви можете просто ввести вихідний файл 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;
}
Зрозуміло, що код для структури дивного балу - це не те, що ви зазвичай пишете, але він ілюструє суть. Перший аргумент другого виклику 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
.Будь-який заголовок повинен бути захищений від повторного включення, щоб визначення типів (enum, структура, типи об'єднання або типи 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
кілька разів, але це найпростіший спосіб показати, що механізм працює. Це означає, що якби заголовок був побічно включений двічі, це також було б безпечним.
Обмеженнями для цього є:
Заголовок, що визначає або оголошує глобальні змінні, не може сам визначати будь-які типи.
Безпосередньо перед тим, як включити заголовок, який повинен визначати змінні, ви визначаєте макрос DEFINE_VARIABLES.
Заголовок, що визначає або оголошує змінні, має стилізований вміст.
#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
:
/* 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"
#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
.
Однак проблеми на практиці малоймовірні, особливо якщо ви скористаєтесь стандартною порадою
Чи вистачає цього експозиції?
Сповідь : Схему "уникнення дублювання коду", описану тут, було розроблено, оскільки проблема стосується деякого коду, над яким я працюю (але не володію ним), і викликає неприємність щодо схеми, викладеної в першій частині відповіді. Однак оригінальна схема залишає вам лише два місця для зміни, щоб зберегти визначення змінних та декларації синхронізованими, що є великим кроком вперед над тим, що експланальні декларації змінних розкидані по кодовій базі (що дійсно має значення, коли загалом є тисячі файлів) . Однак код у файлах з іменами fileNc.[ch]
(плюс external.h
і externdef.h
) показує, що він може бути змушений працювати. Зрозуміло, що створити сценарій генератора заголовків було б важко, щоб надати вам стандартизований шаблон для змінної, що визначає і оголошує файл заголовка.
Примітка. Це іграшкові програми з ледве достатньою кількістю коду, щоб зробити їх надзвичайно цікавими. У прикладах є повторення, які можна було б видалити, але це не для спрощення педагогічного пояснення. (Наприклад: різниця між prog5.c
і prog8.c
є ім'ям одного із включених заголовків. Можна було б реорганізувати код так, щоб main()
функція не повторювалася, але вона приховувала б більше, ніж виявила.)
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
.
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
extern int test1_var;
на int test1_var;
, лінкер (gcc 5.4.0) все-таки передається. Отже, чи extern
справді потрібна в цьому випадку?
extern
файл є загальним розширенням, яке часто працює - і спеціально працює з GCC (але GCC далеко не єдиний компілятор, який його підтримує; він є поширеним в системах Unix). Ви можете подивитися на «J.5.11» або в розділі «Не так хороший спосіб» в моїй обороні (я знаю - це є давно) і текст поруч , що пояснює його (або намагається зробити це).
Extern - це ключове слово, яке ви використовуєте, щоб оголосити, що сама змінна знаходиться в іншому блоці перекладу.
Таким чином, ви можете вирішити використовувати змінну в блоці перекладу, а потім отримати доступ до неї з іншого, потім у другому ви оголосите її як зовнішню, і символ буде вирішений лінкером.
Якщо ви не оголосите це як екстерн, ви отримаєте 2 змінні, названі однаковими, але зовсім не пов'язаними, і помилка декількох визначень змінної.
Мені подобається думати про зовнішню змінну як про обіцянку, яку ви даєте компілятору.
Зустрічаючи екстерн, компілятор може дізнатися лише його тип, а не там, де він "живе", тому він не може вирішити посилання.
Ви говорите це: "Повірте мені. У час посилання ця посилання буде вирішеною".
extern повідомляє компілятору довіряти вам, що пам'ять для цієї змінної оголошено в іншому місці, тому він не намагається виділити / перевірити пам'ять.
Тому ви можете скласти файл, який має посилання на екстерн, але ви не можете зв’язати, якщо ця пам'ять десь не оголошена.
Корисний для глобальних змінних та бібліотек, але небезпечний тим, що лінкер не вводить перевірку.
Додавання extern
перетворень перетворює визначення змінної у оголошення змінної . Дивіться цю тему щодо різниці між декларацією та визначенням.
declare | define | initialize |
----------------------------------
extern int a; yes no no
-------------
int a = 2019; yes yes yes
-------------
int a; yes yes no
-------------
Декларація не виділяє пам'ять (змінна повинна бути визначена для розподілу пам'яті), але визначення буде. Це просто ще один простий погляд на ключове слово "зовнішній", оскільки інші відповіді справді чудові.
Правильна інтерпретація зовнішнього вигляду полягає в тому, що ви щось скажете компілятору. Ви повідомляєте компілятору, що, не дивлячись на те, що він зараз не присутній, декларована змінна якось знайдеться лінкером (як правило, в іншому об'єкті (файлі)). Тоді лінкер буде щасливим хлопцем, що знайде все і складе його разом, незалежно від того, чи мали ви якісь зовнішні декларації чи ні.
У C змінній всередині файла скажіть example.c надається локальної області. Компілятор очікує, що змінна матиме своє визначення всередині того самого файлу example.c, а коли він не знаходить те саме, він би кинув помилку. А функція з іншого боку має глобальну область застосування за замовчуванням. Таким чином, вам не потрібно прямо згадувати компілятору "дивіться чувак ... ви можете знайти тут визначення цієї функції". Для функції, що включає файл, що містить її декларацію, достатньо. (Файл, який ви насправді називаєте файлом заголовка). Наприклад, розгляньте такі два файли:
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>
Реалізація 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
Розділ "Таблиця символів" у розділі "Таблиця символів" System V ABI Update :
SHN_UNDEF Цей індекс таблиці розділу означає, що символ не визначений. Коли редактор посилань поєднує цей об’єктний файл з іншим, який визначає вказаний символ, посилання цього файлу на символ будуть пов'язані з фактичним визначенням.
що в основному є поведінкою, яку стандарт C надає extern
змінним.
Відтепер завдання лінкера складають остаточну програму, але extern
інформація вже витягнута з вихідного коду в файл об'єкта.
Тестували на GCC 4.8.
C ++ 17 вбудованих змінних
У C ++ 17 ви можете використовувати вбудові змінні замість зовнішніх, оскільки вони прості у використанні (їх можна визначити лише один раз у заголовку) та більш потужні (підтримують constexpr). Див.: Що означає "const статична" в C і C ++?
readelf
або nm
може бути корисним, ви не пояснили основи використання та extern
не завершили першу програму фактичним визначенням. Ваш код навіть не використовується notExtern
. Існує і проблема з номенклатурою: хоча notExtern
тут визначено, а не декларується за допомогою extern
, це зовнішня змінна, до якої можна отримати доступ до інших вихідних файлів, якби ці перекладацькі одиниці містили відповідну декларацію (для чого знадобиться extern int notExtern;
!).
notExtern
було потворне, виправлено це. Про номенклатуру, повідомте мені, чи маєте ви краще ім’я. Звичайно, це не було б гарним ім'ям для фактичної програми, але я думаю, що вона добре відповідає дидактичній ролі.
global_def
змінної, визначеної тут, і extern_ref
для змінної, визначеної в якомусь іншому модулі? Чи мали б вони належно чітку симетрію? Ви все ще опинитесь int extern_ref = 57;
у файлі, де він визначений, або щось подібне, тому ім'я не зовсім ідеальне, але в контексті одного вихідного файлу це вибір розумний. Маючи extern int global_def;
в заголовку не так проблема, як мені здається. Цілком залежить від вас, звичайно.
extern
дозволяє одному модулю вашої програми отримати доступ до глобальної змінної або функції, оголошеної в іншому модулі вашої програми. У вас зазвичай є змінні зовнішніх змін у файлах заголовка.
Якщо ви не хочете, щоб програма отримувала доступ до змінних або функцій, ви використовуєте, static
що повідомляє компілятору, що ця змінна або функція не може бути використана поза цим модулем.
По-перше, extern
ключове слово не використовується для визначення змінної; скоріше він використовується для оголошення змінної. Я можу сказати extern
, що це клас зберігання, а не тип даних.
extern
використовується для того, щоб інші файли C або зовнішні компоненти знали, що ця змінна вже десь визначена. Приклад: якщо ви будуєте бібліотеку, не потрібно обов'язково визначати глобальну змінну десь у самій бібліотеці. Бібліотека буде складена безпосередньо, але, посилаючись на файл, вона перевіряє визначення.
extern
використовується, щоб один first.c
файл мав повний доступ до глобального параметра в іншому second.c
файлі.
extern
Може бути оголошений в first.c
файлі або в будь-якому з файлів заголовків first.c
включає в себе.
extern
декларація має бути в заголовку, а не в заголовку, first.c
так що якщо тип зміниться, декларація також зміниться. Також заголовок, який оголошує змінну, повинен бути включений, second.c
щоб переконатися, що визначення відповідає узгодженню. Декларація в заголовку - це клей, який тримає все це разом; він дозволяє окремо збирати файли, але забезпечує їх послідовне уявлення про тип глобальної змінної.
З 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 unsigned char testing_mode;
Але це буде правильно оголошено як unsigned char
.
Старий пост форуму, ймовірно, пояснив це дещо чіткіше. Але це справжній потенціал gotcha
при використанні компілятора, який дозволяє оголосити змінну в одному файлі, а потім оголосити її зовнішньою як інший тип в іншому. Проблеми, пов’язані з цим, полягають у тому, що якщо ви скажете, що оголошено testing_mode як int в іншому файлі, він би подумав, що це 16-бітний var і перезаписав якусь іншу частину оперативної пам’яті, потенційно пошкодивши іншу змінну. Складно налагодити!
Я використовую дуже коротке рішення, щоб дозволити файлу заголовка містити зовнішню посилання або фактичну реалізацію об'єкта. Файл, який насправді містить об'єкт, просто робить #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