Як використовувати extern для обміну змінними між вихідними файлами?


987

Я знаю, що глобальні змінні в C іноді мають externключове слово. Що таке externзмінна? Що таке декларація? Яка сфера застосування?

Це пов'язано з обміном змінними між вихідними файлами, але як це точно працює? Де я використовую extern?

Відповіді:


1751

Використання 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є лише файлом makefile prog1. Він буде працювати з більшістю версій, що makeвипускаються приблизно з рубежу тисячоліття. Він не прив'язаний спеціально до GNU Make.

prog1.mk

# Minimal makefile for prog1

PROGRAM = prog1
FILES.c = prog1.c file1.c file2.c
FILES.h = prog1.h file3.h
FILES.o = ${FILES.c:.c=.o}

CC      = gcc
SFLAGS  = -std=c11
GFLAGS  = -g
OFLAGS  = -O3
WFLAG1  = -Wall
WFLAG2  = -Wextra
WFLAG3  = -Werror
WFLAG4  = -Wstrict-prototypes
WFLAG5  = -Wmissing-prototypes
WFLAGS  = ${WFLAG1} ${WFLAG2} ${WFLAG3} ${WFLAG4} ${WFLAG5}
UFLAGS  = # Set on command line only

CFLAGS  = ${SFLAGS} ${GFLAGS} ${OFLAGS} ${WFLAGS} ${UFLAGS}
LDFLAGS =
LDLIBS  =

all:    ${PROGRAM}

${PROGRAM}: ${FILES.o}
    ${CC} -o $@ ${CFLAGS} ${FILES.o} ${LDFLAGS} ${LDLIBS}

prog1.o: ${FILES.h}
file1.o: ${FILES.h}
file2.o: ${FILES.h}

# If it exists, prog1.dSYM is a directory on macOS DEBRIS = a.out core *~ *.dSYM RM_FR = rm -fr 

clean:
    ${RM_FR} ${FILES.o} ${PROGRAM} ${DEBRIS}

Керівні принципи

Правила, які потрібно порушувати лише експертам, і лише з поважних причин:

  • Файл заголовка містить лише externдекларації змінних - ніколи staticабо некваліфіковані визначення змінних.

  • Для будь-якої заданої змінної оголошує її лише один заголовок (SPOT - Єдина точка істини).

  • Вихідний файл ніколи не містить externдекларацій змінних - вихідні файли завжди містять (єдиний) заголовок, який їх оголошує.

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

  • Вихідний файл, що визначає змінну, також включає заголовок, щоб забезпечити відповідність визначення та декларації.

  • Функція ніколи не повинна оголошувати змінну за допомогою extern.

  • Уникайте глобальних змінних, коли це можливо - замість цього використовуйте функції.

Вихідний код і текст цієї відповіді доступні в моєму сховищі SOQ (Питання переповнення стека) в GitHub в підкаталозі src / so-0143-3204 .

Якщо ви не досвідчений програміст на С, ви можете (і, можливо, повинні) перестати читати тут.

Не дуже вдалий спосіб визначення глобальних змінних

З деякими (дійсно багатьма) компіляторами C ви можете піти з того, що називається "загальним" визначенням змінної. Тут "загальне" відноситься до техніки, що використовується у Fortran для обміну змінними між вихідними файлами, використовуючи (можливо, названий) блок COMMON. Тут відбувається те, що кожен із ряду файлів забезпечує попереднє визначення змінної. Поки не більше ніж один файл забезпечує ініціалізоване визначення, то різні файли в кінцевому підсумку мають спільне єдине визначення змінної:

file10.c

#include "prog2.h"

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

void inc(void) { l++; }

file11.c

#include "prog2.h"

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

void dec(void) { l--; }

file12.c

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

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

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

Ця методика не відповідає букві стандарту С та "одному правилу визначення" - це офіційно невизначена поведінка:

J.2 Невизначена поведінка

Використовується ідентифікатор із зовнішнім зв’язком, але в програмі не існує точно одного зовнішнього визначення для ідентифікатора, або ідентифікатор не використовується, і існує кілька зовнішніх визначень для ідентифікатора (6.9).

§6.9 Зовнішні визначення ¶5

Зовнішнє визначення є зовнішнім свідченням того, що є також визначення функції (інший , ніж визначення інлайн) або об'єкта. Якщо ідентифікатор, оголошений із зовнішнім зв’язком, використовується в виразі (крім частини операнду оператора sizeofчи _Alignofоператора, результатом якого є ціла константа), десь у всій програмі має бути саме одне зовнішнє визначення для ідентифікатора; інакше не повинно бути більше одного. 161)

161) Таким чином, якщо ідентифікатор, оголошений із зовнішнім зв’язком, не використовується в виразі, для нього не повинно бути зовнішнього визначення.

Однак стандарт C також перераховує його в інформаційному додатку J як одне із поширених розширень .

J.5.11 Кілька зовнішніх визначень

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

Оскільки ця методика не завжди підтримується, краще уникати її використання, особливо якщо ваш код повинен бути переносним . Використовуючи цю техніку, ви також можете закінчитися ненавмисним наказом типу.

Якщо один з файлів вище оголошено lяк doubleзамість того , щоб, як long, типу небезпечних лінкери C, ймовірно , не помітять невідповідність. Якщо ви працюєте на машині з 64-бітною версієюlong і doubleви не хотіли навіть отримати попередження; На машині з 32-розрядною longі 64-розрядною версією doubleви, мабуть, отримаєте попередження про різні розміри - лінкер буде використовувати найбільший розмір саме так, як програма Fortran приймала б найбільший розмір усіх загальних блоків.

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


Наступні два файли заповнюють джерело для prog2:

прог2.х

extern void dec(void);
extern void put(void);
extern void inc(void);

prog2.c

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

int main(void)
{
    inc();
    put();
    dec();
    put();
    dec();
    put();
}
  • prog2використання prog2.c, file10.c, file11.c, file12.c, prog2.h.

Увага

Як зазначається в коментарях тут, і як зазначено у моїй відповіді на аналогічне запитання , використання декількох визначень для глобальної змінної призводить до невизначеної поведінки (J.2; § 6.9), що є стандартним способом сказати "все може статися". Одна з речей, яка може статися, це те, що програма поводиться так, як ви очікували; а J.5.11 приблизно говорить: "Можливо, вам пощастить частіше, ніж ви заслуговуєте". Але програма, яка спирається на кілька визначень зовнішньої змінної - з явним ключовим словом "extern" або без нього - не є суворо відповідною програмою і не гарантовано працювати всюди. Еквівалентно: він містить помилку, яка може або не може проявити себе.

Порушення правил

Звичайно, існує безліч способів порушення цих керівних принципів. Інколи може бути вагомий привід для порушення правил, але такі випадки надзвичайно незвичні.

faulty_header.h

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

Примітка 1: якщо заголовок визначає змінну без externключового слова, то кожен файл, що включає заголовок, створює попереднє визначення змінної. Як зазначалося раніше, це часто працюватиме, але стандарт C не гарантує, що він буде працювати.

зламаний_головник.h

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

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

seldom_correct.h

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

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

Якщо змінна насправді є складним масивом, наприклад, це може призвести до крайнього дублювання коду. Це дуже часто може бути розумним способом досягти певного ефекту, але це дуже незвично.


Підсумок

Використовуйте техніку заголовка, яку я показав першим. Працює надійно і скрізь. Зауважте, зокрема, що заголовок, що декларує global_variable, міститься у кожному файлі, який його використовує - включаючи той, який визначає його. Це гарантує, що все самопослідовно.

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

Кінець оригінальної відповіді

Якщо ви не досвідчений програміст на С, вам, мабуть, варто припинити читання тут.


Пізня основна добавка

Уникнення дублювання коду

Одне занепокоєння, яке інколи (і на законних підставах) викликає механізм "декларацій у заголовках, визначення джерел", описаний тут, - це два файли, які слід синхронізувати - заголовок та джерело. Зазвичай це супроводжується зауваженням, що макрос можна використовувати так, щоб заголовок виконував подвійний обов'язок - зазвичай декларування змінних, але коли певний макрос встановлюється перед включенням заголовка, він замість цього визначає змінні.

Інше занепокоєння може викликати те, що змінні потрібно визначити в кожній з ряду "основних програм". Зазвичай це помилкова проблема; ви можете просто ввести вихідний файл C, щоб визначити змінні та зв’язати об'єктний файл, створений з кожною з програм.

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

file3a.h

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

EXTERN int global_variable;

file1a.c

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

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

file2a.c

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

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

Наступні два файли заповнюють джерело для prog3:

prog3.h

extern void use_it(void);
extern int increment(void);

prog3.c

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

int main(void)
{
    use_it();
    global_variable += 19;
    use_it();
    printf("Increment: %d\n", increment());
    return 0;
}
  • prog3використання prog3.c, file1a.c, file2a.c, file3a.h, prog3.h.

Змінна ініціалізація

Проблема цієї схеми, як показано, полягає в тому, що вона не передбачає ініціалізації глобальної змінної. З C99 або C11 та зі списками змінних аргументів для макросів, ви можете визначити макрос для підтримки ініціалізації. (З C89 та відсутністю підтримки списків змінних аргументів у макросах, немає простого способу обробки довільно довгих ініціалізаторів.)

file3b.h

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

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

Зворотний вміст #ifта #elseблоки, виправляючи помилку, виявлену Денисом Княжевим

file1b.c

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

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

file2b.c

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

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

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

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


Наступні два файли заповнюють джерело для prog4:

prog4.h

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

prog4.c

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

int main(void)
{
    use_them();
    global_variable += 19;
    use_them();
    printf("Increment: %d\n", increment());
    printf("Oddball:   %d\n", oddball_value());
    return 0;
}
  • prog4використання prog4.c, file1b.c, file2b.c, prog4.h, file3b.h.

Охоронці заголовків

Будь-який заголовок повинен бути захищений від повторного включення, щоб визначення типів (enum, структура, типи об'єднання або типи typedefs) не створювало проблем. Стандартна техніка полягає в загортанні корпусу заголовка в захисний заголовок, наприклад:

#ifndef FILE3B_H_INCLUDED
#define FILE3B_H_INCLUDED

...contents of header...

#endif /* FILE3B_H_INCLUDED */

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

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

Отже, вам потрібно включити тіло file3b.hмаксимум одночасно для декларацій, і не більше одного разу для визначень, але вам може знадобитися як в одному блоці перекладу (TU - комбінація вихідного файлу, так і заголовки, які він використовує).

Багаторазове включення зі змінними визначеннями

Однак це можна зробити за умови не надто необгрунтованого обмеження. Введемо новий набір файлів:

  • external.h для визначення макросів EXTERN тощо.

  • file1c.hвизначити типи (зокрема, struct oddballтип oddball_struct).

  • file2c.h визначити або оголосити глобальні змінні.

  • file3c.c що визначає глобальні змінні.

  • file4c.c яка просто використовує глобальні змінні.

  • file5c.c що показує, що ви можете оголосити, а потім визначити глобальні змінні.

  • file6c.c що показує, що ви можете визначити, а потім (спробувати) оголосити глобальні змінні.

У цих прикладах file5c.cі file6c.cбезпосередньо включайте заголовок file2c.hкілька разів, але це найпростіший спосіб показати, що механізм працює. Це означає, що якби заголовок був побічно включений двічі, це також було б безпечним.

Обмеженнями для цього є:

  1. Заголовок, що визначає або оголошує глобальні змінні, не може сам визначати будь-які типи.

  2. Безпосередньо перед тим, як включити заголовок, який повинен визначати змінні, ви визначаєте макрос DEFINE_VARIABLES.

  3. Заголовок, що визначає або оголошує змінні, має стилізований вміст.

зовнішня.h


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

file1c.h

#ifndef FILE1C_H_INCLUDED
#define FILE1C_H_INCLUDED

struct oddball
{
    int a;
    int b;
};

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

#endif /* FILE1C_H_INCLUDED */

file2c.h


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

#ifndef FILE2C_H_INCLUDED
#define FILE2C_H_INCLUDED

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

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

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

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

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

#endif /* FILE2C_H_INCLUDED */

file3c.c

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

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

file4c.c

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

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

file5c.c


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

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

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

file6c.c


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

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

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

Наступний вихідний файл завершує джерело (забезпечує основну програму) для prog5, prog6і prog7:

prog5.c

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

int main(void)
{
    use_them();
    global_variable += 19;
    use_them();
    printf("Increment: %d\n", increment());
    printf("Oddball:   %d\n", oddball_value());
    return 0;
}
  • prog5використання prog5.c, file3c.c, file4c.c, file1c.h, file2c.h, external.h.

  • prog6використання prog5.c, file5c.c, file4c.c, file1c.h, file2c.h, external.h.

  • prog7використання prog5.c, file6c.c, file4c.c, file1c.h, file2c.h, external.h.


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

Ви можете частково обійти цю проблему шляхом перегляду file2c.hв file2d.h:

file2d.h

/* Standard prologue */
#if defined(DEFINE_VARIABLES) && !defined(FILE2D_H_DEFINITIONS)
#undef FILE2D_H_INCLUDED
#endif

#ifndef FILE2D_H_INCLUDED
#define FILE2D_H_INCLUDED

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

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

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

#endif /* !DEFINE_VARIABLES || !FILE2D_H_DEFINITIONS */

/* Standard epilogue */
#ifdef DEFINE_VARIABLES
#define FILE2D_H_DEFINITIONS
#undef DEFINE_VARIABLES
#endif /* DEFINE_VARIABLES */

#endif /* FILE2D_H_INCLUDED */

Питання стає "чи має містити заголовок #undef DEFINE_VARIABLES?" Якщо ви пропустите це з заголовка та оберніть будь-яке визначальне виклик за допомогою #defineта #undef:

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

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

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

externdef.h


#if defined(HEADER_DEFINING_VARIABLES)
#define DEFINE_VARIABLES
#include HEADER_DEFINING_VARIABLES
#undef DEFINE_VARIABLES
#undef HEADER_DEFINING_VARIABLES
#endif /* HEADER_DEFINING_VARIABLES */

Це стає перекрученим, але, здається, захищеним (використовуючи file2d.h, без " #undef DEFINE_VARIABLESв" file2d.h).

file7c.c

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

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

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

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

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

file8c.h

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

#ifndef FILE8C_H_INCLUDED
#define FILE8C_H_INCLUDED

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

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

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

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

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

#endif /* FILE8C_H_INCLUDED */

file8c.c

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

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

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

Наступні два файли заповнюють джерело для prog8та prog9:

prog8.c

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

int main(void)
{
    use_them();
    global_variable += 19;
    use_them();
    printf("Increment: %d\n", increment());
    printf("Oddball:   %d\n", oddball_value());
    return 0;
}

file9c.c

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

void use_them(void)
{
    printf("Global variable: %d\n", global_variable++);
    oddball_struct.a += global_variable;
    oddball_struct.b -= global_variable / 2;
}
  • prog8використання prog8.c, file7c.c, file9c.c.

  • prog9використання prog8.c, file8c.c, file9c.c.


Однак проблеми на практиці малоймовірні, особливо якщо ви скористаєтесь стандартною порадою

Уникайте глобальних змінних


Чи вистачає цього експозиції?

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

Примітка. Це іграшкові програми з ледве достатньою кількістю коду, щоб зробити їх надзвичайно цікавими. У прикладах є повторення, які можна було б видалити, але це не для спрощення педагогічного пояснення. (Наприклад: різниця між prog5.cі prog8.cє ім'ям одного із включених заголовків. Можна було б реорганізувати код так, щоб main()функція не повторювалася, але вона приховувала б більше, ніж виявила.)


3
@litb: див. Додаток J.5.11 до загального визначення - це загальне розширення.
Джонатан Леффлер

3
@litb: і я погоджуюся, цього слід уникати - ось чому це знаходиться у розділі "Не дуже вдалий спосіб визначення глобальних змінних".
Джонатан Леффлер

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

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

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

125

externЗмінна є декларація (завдяки SbI для корекції) змінної , яка визначена в іншому ЕП. Це означає, що сховище для змінної виділено в іншому файлі.

Скажіть, у вас є два .cфайли test1.cі test2.c. Якщо визначити глобальну змінну int test1_var;в test1.cі ви хотіли б отримати доступ до цієї змінної в test2.cви повинні використовувати extern int test1_var;в test2.c.

Повний зразок:

$ cat test1.c 
int test1_var = 5;
$ cat test2.c
#include <stdio.h>

extern int test1_var;

int main(void) {
    printf("test1_var = %d\n", test1_var);
    return 0;
}
$ gcc test1.c test2.c -o test
$ ./test
test1_var = 5

21
"Псевдоозначень" немає. Це декларація.
sbi

3
У наведеному вище прикладі, якщо я зміню extern int test1_var;на int test1_var;, лінкер (gcc 5.4.0) все-таки передається. Отже, чи externсправді потрібна в цьому випадку?
радіоголова

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

Зовнішню декларацію, безумовно, не потрібно визначати в іншій перекладацькій одиниці (а зазвичай ні). Насправді декларація та визначення можуть бути одним і тим же.
Згадайте Моніку

40

Extern - це ключове слово, яке ви використовуєте, щоб оголосити, що сама змінна знаходиться в іншому блоці перекладу.

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

Якщо ви не оголосите це як екстерн, ви отримаєте 2 змінні, названі однаковими, але зовсім не пов'язаними, і помилка декількох визначень змінної.


5
Іншими словами, блок перекладу, де використовується екстерн, знає про цю змінну, її тип тощо, і тому дозволяє вихідному коду в базовій логіці використовувати її, але вона не виділяє змінну, інший блок перекладу зробить це. Якщо обидва перекладацькі одиниці повинні оголошувати змінну нормально, було б ефективно два фізичні розташування для змінної з пов'язаними "неправильними" посиланнями у складеному коді та із отриманою неоднозначністю для лінкера.
mjv

26

Мені подобається думати про зовнішню змінну як про обіцянку, яку ви даєте компілятору.

Зустрічаючи екстерн, компілятор може дізнатися лише його тип, а не там, де він "живе", тому він не може вирішити посилання.

Ви говорите це: "Повірте мені. У час посилання ця посилання буде вирішеною".


Більш загально, декларація - це обіцянка, що ім'я буде вирішуватися точно на одне визначення під час посилання. Екстерн оголошує змінну без визначення.
Лі Лі Райан

18

extern повідомляє компілятору довіряти вам, що пам'ять для цієї змінної оголошено в іншому місці, тому він не намагається виділити / перевірити пам'ять.

Тому ви можете скласти файл, який має посилання на екстерн, але ви не можете зв’язати, якщо ця пам'ять десь не оголошена.

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


Пам'ять не декларується. Дивіться відповіді на це запитання: stackoverflow.com/questions/1410563 для отримання більш детальної інформації.
sbi

15

Додавання externперетворень перетворює визначення змінної у оголошення змінної . Дивіться цю тему щодо різниці між декларацією та визначенням.


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

@ user14284: Обидва вони є декларацією лише в тому сенсі, що і кожне визначення є декларацією. Але я пов’язав пояснення цього. ("Дивіться цю тему щодо різниці між декларацією та визначенням.") Чому ви просто не перейдете за посиланням і не прочитаєте?
sbi

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

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

Декларація не виділяє пам'ять (змінна повинна бути визначена для розподілу пам'яті), але визначення буде. Це просто ще один простий погляд на ключове слово "зовнішній", оскільки інші відповіді справді чудові.


11

Правильна інтерпретація зовнішнього вигляду полягає в тому, що ви щось скажете компілятору. Ви повідомляєте компілятору, що, не дивлячись на те, що він зараз не присутній, декларована змінна якось знайдеться лінкером (як правило, в іншому об'єкті (файлі)). Тоді лінкер буде щасливим хлопцем, що знайде все і складе його разом, незалежно від того, чи мали ви якісь зовнішні декларації чи ні.


8

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


8

Реалізація GCC ELF Linux

Інші відповіді охоплюють погляди на використання мови, тому тепер давайте розглянемо, як це реалізовано в цій реалізації.

main.c

#include <stdio.h>

int not_extern_int = 1;
extern int extern_int;

void main() {
    printf("%d\n", not_extern_int);
    printf("%d\n", extern_int);
}

Складіть і декомпілюйте:

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

Вихід містить:

Num:    Value          Size Type    Bind   Vis      Ndx Name
 9: 0000000000000000     4 OBJECT  GLOBAL DEFAULT    3 not_extern_int
12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND extern_int

Розділ "Таблиця символів" у розділі "Таблиця символів" System V ABI Update :

SHN_UNDEF Цей індекс таблиці розділу означає, що символ не визначений. Коли редактор посилань поєднує цей об’єктний файл з іншим, який визначає вказаний символ, посилання цього файлу на символ будуть пов'язані з фактичним визначенням.

що в основному є поведінкою, яку стандарт C надає externзмінним.

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

Тестували на GCC 4.8.

C ++ 17 вбудованих змінних

У C ++ 17 ви можете використовувати вбудові змінні замість зовнішніх, оскільки вони прості у використанні (їх можна визначити лише один раз у заголовку) та більш потужні (підтримують constexpr). Див.: Що означає "const статична" в C і C ++?


3
Це не моє голосування, тому я не знаю. Однак я висловлю свою думку. Хоча, дивлячись на вихід readelfабо nmможе бути корисним, ви не пояснили основи використання та externне завершили першу програму фактичним визначенням. Ваш код навіть не використовується notExtern. Існує і проблема з номенклатурою: хоча notExternтут визначено, а не декларується за допомогою extern, це зовнішня змінна, до якої можна отримати доступ до інших вихідних файлів, якби ці перекладацькі одиниці містили відповідну декларацію (для чого знадобиться extern int notExtern;!).
Джонатан Леффлер

1
@JonathanLeffler дякую за відгук! Стандартні рекомендації щодо поведінки та використання вже були зроблені в інших відповідях, тому я вирішив трохи показати реалізацію, як це дійсно допомогло мені зрозуміти, що відбувається. Некорисне використання notExternбуло потворне, виправлено це. Про номенклатуру, повідомте мені, чи маєте ви краще ім’я. Звичайно, це не було б гарним ім'ям для фактичної програми, але я думаю, що вона добре відповідає дидактичній ролі.
Ciro Santilli 冠状 病毒 审查 六四 事件 法轮功

Що стосується імен, як щодо global_defзмінної, визначеної тут, і extern_refдля змінної, визначеної в якомусь іншому модулі? Чи мали б вони належно чітку симетрію? Ви все ще опинитесь int extern_ref = 57;у файлі, де він визначений, або щось подібне, тому ім'я не зовсім ідеальне, але в контексті одного вихідного файлу це вибір розумний. Маючи extern int global_def;в заголовку не так проблема, як мені здається. Цілком залежить від вас, звичайно.
Джонатан Леффлер

7

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

Це також означає, що ви можете використовувати змінну, оголошену за допомогою ключового слова extern у будь-якому файлі, хоча вона оголошена / визначена в іншому файлі.


5

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

Якщо ви не хочете, щоб програма отримувала доступ до змінних або функцій, ви використовуєте, staticщо повідомляє компілятору, що ця змінна або функція не може бути використана поза цим модулем.


5

extern просто означає, що змінна визначена в іншому місці (наприклад, в іншому файлі).


4

По-перше, externключове слово не використовується для визначення змінної; скоріше він використовується для оголошення змінної. Я можу сказати extern, що це клас зберігання, а не тип даних.

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


3

externвикористовується, щоб один first.cфайл мав повний доступ до глобального параметра в іншому second.cфайлі.

externМоже бути оголошений в first.cфайлі або в будь-якому з файлів заголовків first.cвключає в себе.


3
Зауважте, що externдекларація має бути в заголовку, а не в заголовку, first.cтак що якщо тип зміниться, декларація також зміниться. Також заголовок, який оголошує змінну, повинен бути включений, second.cщоб переконатися, що визначення відповідає узгодженню. Декларація в заголовку - це клей, який тримає все це разом; він дозволяє окремо збирати файли, але забезпечує їх послідовне уявлення про тип глобальної змінної.
Джонатан Леффлер

2

З xc8 ви повинні бути обережними щодо оголошення змінної як одного типу у кожному файлі, як ви могли, помилково, оголосити щось intв одному файлі і charсказати в іншому. Це може призвести до корупції змінних.

Цю проблему було елегантно вирішено на форумі з мікрочіпами близько 15 років тому / * Див. "Http: www.htsoft.com" / / "forum / all / showflat.php / Cat / 0 / Number / 18766 / an / 0 / page / 0 # 18766 "

Але це посилання, здається, більше не працює ...

Тому я швидко спробую пояснити це; створити файл під назвою global.h.

У ньому заявляють наступне

#ifdef MAIN_C
#define GLOBAL
 /* #warning COMPILING MAIN.C */
#else
#define GLOBAL extern
#endif
GLOBAL unsigned char testing_mode; // example var used in several C files

Тепер у файлі main.c

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

Це означає, що в main.c змінна буде оголошена як unsigned char.

Тепер в інших файлах, просто включаючи global.h, він буде оголошений як зовнішній для цього файлу .

extern unsigned char testing_mode;

Але це буде правильно оголошено як unsigned char.

Старий пост форуму, ймовірно, пояснив це дещо чіткіше. Але це справжній потенціал gotchaпри використанні компілятора, який дозволяє оголосити змінну в одному файлі, а потім оголосити її зовнішньою як інший тип в іншому. Проблеми, пов’язані з цим, полягають у тому, що якщо ви скажете, що оголошено testing_mode як int в іншому файлі, він би подумав, що це 16-бітний var і перезаписав якусь іншу частину оперативної пам’яті, потенційно пошкодивши іншу змінну. Складно налагодити!


0

Я використовую дуже коротке рішення, щоб дозволити файлу заголовка містити зовнішню посилання або фактичну реалізацію об'єкта. Файл, який насправді містить об'єкт, просто робить #define GLOBAL_FOO_IMPLEMENTATION. Тоді, коли я додаю новий об’єкт до цього файлу, він відображається у цьому файлі і без того, щоб мені потрібно було копіювати та вставляти визначення.

Я використовую цю схему в декількох файлах. Тож для того, щоб зберегти речі максимально автономними, я просто повторно використовую єдиний макрос GLOBAL у кожному заголовку. Мій заголовок виглядає так:

//file foo_globals.h
#pragma once  
#include "foo.h"  //contains definition of foo

#ifdef GLOBAL  
#undef GLOBAL  
#endif  

#ifdef GLOBAL_FOO_IMPLEMENTATION  
#define GLOBAL  
#else  
#define GLOBAL extern  
#endif  

GLOBAL Foo foo1;  
GLOBAL Foo foo2;


//file main.cpp
#define GLOBAL_FOO_IMPLEMENTATION
#include "foo_globals.h"

//file uses_extern_foo.cpp
#include "foo_globals.h
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.