Влияние ключевого слова extern на функции C


172

В Си я не заметил никакого эффекта от externключевого слова, использованного до объявления функции. Сначала я подумал , что при определении extern int f();в одном файле сил вы реализовать его за пределы объема файла. Однако я узнал, что оба:

extern int f();
int f() {return 0;}

и

extern int f() {return 0;}

компилируется просто отлично, без предупреждений от gcc. Я использовал gcc -Wall -ansi; он даже не принял бы //комментарии.

Существуют ли какие-либо эффекты для использования extern перед определениями функций ? Или это просто необязательное ключевое слово без побочных эффектов для функций.

В последнем случае я не понимаю, почему стандартные дизайнеры решили засорять грамматику лишними ключевыми словами.

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


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

4
Это не всегда лишнее, см. Мой ответ. Каждый раз, когда вам нужно поделиться чем-то между модулями, которые вы НЕ хотите в публичном заголовке, это очень полезно. Однако «извлечение» каждой отдельной функции в общедоступном заголовке (с помощью современных компиляторов) приносит мало пользы, поскольку они могут понять это самостоятельно.
Тим Пост

@Ed .. если volatile int foo является глобальным в foo.c, и bar.c нуждается в этом, bar.c должен объявить его как extern. У него есть свои преимущества. Кроме того, вам может понадобиться поделиться некоторыми функциями, которые вы НЕ хотите показывать в публичном заголовке.
Тим Пост


2
@ Барри Если вообще, другой вопрос является дубликатом этого. 2009 против 2012
Элазар Лейбович

Ответы:


138

У нас есть два файла, foo.c и bar.c.

Вот foo.c

#include <stdio.h>

volatile unsigned int stop_now = 0;
extern void bar_function(void);

int main(void)
{
  while (1) {
     bar_function();
     stop_now = 1;
  }
  return 0;
}

Теперь вот bar.c

#include <stdio.h>

extern volatile unsigned int stop_now;

void bar_function(void)
{
   while (! stop_now) {
      printf("Hello, world!\n");
      sleep(30);
   }
}

Как вы можете видеть, у нас нет общего заголовка между foo.c и bar.c, однако bar.c требуется что-то объявленное в foo.c, когда он связан, и foo.c нужна функция из bar.c, когда он связан.

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

Это очень полезно, если вам нужно разделить какой-то глобал между модулями и не хотите помещать / инициализировать его в заголовке.

Технически, каждая функция в общедоступном заголовке библиотеки является «внешней», однако маркировка их как таковых имеет очень мало или вообще никаких преимуществ, в зависимости от компилятора. Большинство компиляторов могут понять это самостоятельно. Как видите, эти функции на самом деле определены где-то еще.

В приведенном выше примере main () напечатает hello world только один раз, но продолжит ввод bar_function (). Также обратите внимание, что bar_function () не собирается возвращаться в этом примере (так как это простой пример). Просто представьте, что stop_now модифицируется, когда сигнал обслуживается (следовательно, изменчив), если это не кажется достаточно практичным.

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

Надеюсь, это поможет :)


56
Ваш код будет прекрасно компилироваться без extern до bar_function.
Элазар Лейбович

2
@Tim: Тогда у вас не было сомнительной привилегии работать с кодом, с которым я работаю. Это может случится. Иногда заголовок также содержит определение статической функции. Это уродливо и ненужно в 99,99% случаев (я могу быть выключен на порядок или два или на величину, преувеличивая, как часто это необходимо). Обычно это происходит, когда люди неправильно понимают, что заголовок нужен только тогда, когда другие исходные файлы будут использовать информацию; заголовок (ab) используется для хранения информации объявления для одного исходного файла, и никакой другой файл не должен включать его. Иногда это происходит по более искаженным причинам.
Джонатан Леффлер

2
@ Джонатан Леффлер - Сомнительно! Я унаследовал некоторый довольно схематичный код раньше, но я могу честно сказать, что никогда не видел, чтобы кто-то помещал статическую декларацию в заголовок. Похоже, у вас довольно веселая и интересная работа :)
Тим Пост

1
Недостаток «прототипа функции не в заголовке» заключается в том, что вы не получаете автоматическую независимую проверку согласованности между определением функции в bar.cи объявлением в foo.c. Если функция объявлена ​​в foo.h и оба файла включены foo.h, то заголовок обеспечивает согласованность между двумя исходными файлами. Без этого, если определение bar_functionin bar.cизменяется, но объявление in foo.cне изменяется, то во время выполнения все идет не так; компилятор не может определить проблему. С правильно используемым заголовком компилятор обнаруживает проблему.
Джонатан Леффлер

1
extern для объявлений функций является излишним, как 'int' в 'unsigned int'. Хорошей практикой является использование 'extern', когда прототип НЕ является предварительным объявлением ... Но он действительно должен находиться в заголовке без 'extern', если задача не является крайним случаем. stackoverflow.com/questions/10137037/…

82

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

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


21
Затем, как стандартный дизайнер, я не позволю использовать extern с функциями, так как это просто добавляет шум в грамматику.
Элазар Лейбович

3
Обратная совместимость может быть болью.
MathuSum Mut

1
@ElazarLeibovich На самом деле, в данном конкретном случае, запретить его - это то, что добавит шум в грамматику.
Гонки

1
То, как ограничение ключевого слова добавляет шум, мне не подходит, но я думаю, что это дело вкуса.
Элазар Лейбович

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

23

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

Если я напишу

extern int i;

в области видимости файла (вне функционального блока) в файле C вы говорите: «переменная может быть определена в другом месте».

extern int f() {return 0;}

является одновременно объявлением функции f и определением функции f. Определение в этом случае переопределяет внешний.

extern int f();
int f() {return 0;}

сначала декларация, затем определение.

Использование externнеправильно, если вы хотите объявить и одновременно определить переменную области файла. Например,

extern int i = 4;

выдаст ошибку или предупреждение, в зависимости от компилятора.

Использование externполезно, если вы явно хотите избежать определения переменной.

Позволь мне объяснить:

Допустим, файл ac содержит:

#include "a.h"

int i = 2;

int f() { i++; return i;}

Файл ах включает в себя:

extern int i;
int f(void);

и файл bc содержит:

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

int main(void){
    printf("%d\n", f());
    return 0;
}

Экстерьер в заголовке полезен, потому что он сообщает компилятору на этапе компоновки: «это объявление, а не определение». Если я удалю строку в ac, которая определяет i, выделит для нее место и присвоит ей значение, программа не сможет скомпилироваться с неопределенной ссылкой. Это говорит разработчику, что он ссылался на переменную, но еще не определил ее. Если, с другой стороны, я опускаю ключевое слово "extern" и удаляю int i = 2строку, программа все равно компилируется - для меня будет задано значение по умолчанию 0.

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

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


Вы хотели удалить int i = 2строку в -3 абзаце? И правильно ли утверждать, видя int i;, компилятор будет выделять память для этой переменной, но, видя extern int i;, компилятор НЕ будет выделять память, а будет искать переменную в другом месте?
Замороженное пламя

На самом деле, если вы опустите ключевое слово "extern", программа не будет компилироваться из-за переопределения i в ac и bc (из-за ах).
Nixt

15

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

static int g();
extern int g(); /* g has internal linkage */

extern int j(); /* j has tentative external linkage */

extern int h();
static int h(); /* error */

Вот соответствующие параграфы из проекта C99 (n1256):

6.2.2 Связи идентификаторов

[...]

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

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


Это стандарт, или вы просто рассказываете мне типичное поведение компилятора? В случае стандарта, я буду рад за ссылку на стандарт. Но спасибо!
Элазар Лейбович

Это стандартное поведение. Черновик C99 доступен здесь: < open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf >. Фактический стандарт не является бесплатным (черновик достаточно хорош для большинства целей).
Dirkgently

1
Я только что проверил это в gcc, и оба "extern int h (); static int h () {return 0;}" и "int h (); static int h () {return 0;}" принимаются с тем же предупреждение. Это только С99, а не ANSI? Можете ли вы отослать меня к точному разделу в черновике, поскольку это не похоже на gcc.
Элазар Лейбович

Проверьте еще раз. Я попробовал то же самое с gcc 4.0.1, и я получаю сообщение об ошибке именно там, где и должно быть. Попробуйте онлайн-компилятор comeau или codepad.org, если у вас нет доступа к другим компиляторам. Прочитайте стандарт.
Dirkgently

2
@dirkgently, Мой реальный вопрос в том, есть ли какой-либо эффект для использования exetrn с объявлением функции, и если нет, то почему можно добавить extern к объявлению функции. И ответ - нет, эффекта нет, и когда-то был эффект с нестандартными компиляторами.
Элазар Лейбович

11

Встроенные функции имеют специальные правила о том, что externозначает. (Обратите внимание, что встроенные функции являются расширением C99 или GNU; их не было в оригинальном C.

Для не встроенных функций externне требуется, так как включено по умолчанию.

Обратите внимание, что правила для C ++ разные. Например, extern "C"требуется объявление C ++ функций C, которые вы собираетесь вызывать из C ++, и существуют разные правила inline.


Это единственный ответ, который является правильным и фактически отвечает на вопрос.
Робинджам

4

IOW, extern избыточен и ничего не делает.

Вот почему 10 лет спустя:

Смотрите коммит ad6dad0 , коммит b199d71 , коммит 5545442 (29 апреля 2019 г.) от Denton Liu ( Denton-L) .
(Слиты Junio C Hamano - gitster- в фиксации 4aeeef3 , 13 мая 2019)

*.[ch]: удалить externиз объявлений функций используяspatch

Был толчок для удаления externиз объявлений функций.

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

Это был патч Coccinelle:

  @@
    type T;
    identifier f;
    @@
    - extern
    T f(...);

и он был запущен с:

  $ git ls-files \*.{c,h} |
    grep -v ^compat/ |
    xargs spatch --sp-file contrib/coccinelle/noextern.cocci --in-place

Это не всегда просто, хотя:

См. Коммит 7027f50 (04 сентября 2019 г.) Дентона Лю ( Denton-L) .
(Слиты Denton Лю - Denton-L- в фиксации 7027f50 , 05 Sep 2019)

compat/*.[ch]: удалить externиз объявлений функций, используя spatch

В 5545442 ( *.[ch]: удалить externиз объявлений функций, используя spatch, 2019-04-29, Git v2.22.0-rc0), мы удалили внешние объекты из объявлений функций, используя, spatchно мы намеренно исключили файлы, compat/так как некоторые из них напрямую копируются из восходящего потока, и мы должны избегать сбивая их так, что ручное объединение будущих обновлений будет проще.

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

Это был патч Coccinelle:

@@
type T;
identifier f;
@@
- extern
  T f(...);

и он был запущен с:

$ git ls-files compat/\*\*.{c,h} |
    xargs spatch --sp-file contrib/coccinelle/noextern.cocci --in-place
$ git checkout -- \
    compat/regex/ \
    compat/inet_ntop.c \
    compat/inet_pton.c \
    compat/nedmalloc/ \
    compat/obstack.{c,h} \
    compat/poll/

У Coccinelle возникли некоторые проблемы, связанные с __attribute__varargs, поэтому мы выполнили следующее, чтобы убедиться, что оставшихся изменений не осталось:

$ git ls-files compat/\*\*.{c,h} |
    xargs sed -i'' -e 's/^\(\s*\)extern \([^(]*([^*]\)/\1\2/'
$ git checkout -- \
    compat/regex/ \
    compat/inet_ntop.c \
    compat/inet_pton.c \
    compat/nedmalloc/ \
    compat/obstack.{c,h} \
    compat/poll/

Обратите внимание, что с Git 2.24 (Q4 2019), любой паразит externотбрасывается.

См. Коммит 65904b8 (30 сентября 2019 г.) Эмили Шаффер ( nasamuffin) .
Помогает: Джефф Кинг ( peff) .
См. Коммит 8464f94 (21 сентября 2019 г.) Дентона Лю ( Denton-L) .
Помогает: Джефф Кинг ( peff) .
(Слиты Junio C Hamano - gitster- в фиксации 59b19bc , 7 октября 2019)

promisor-remote.h: исключение externиз объявления функции

Во время создания этого файла каждый раз, когда вводилось новое объявление функции, оно включало extern.
Однако, начиная с 5545442 ( *.[ch]: удалить externиз объявлений функций с помощью spatch, 2019-04-29, Git v2.22.0-rc0), мы активно пытались предотвратить использование внешних элементов в объявлениях функций, поскольку они не нужны.

Удалить эти ложные externс.


3

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


2

объявление функции extern означает, что ее определение будет разрешено во время компоновки, а не во время компиляции.

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

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


2
IOW, extern избыточен и ничего не делает. Было бы намного понятнее, если бы вы так выразились.
Элазар Лейбович

@ElazarLeibovich Я только что столкнулся с подобным случаем в нашей кодовой базе и пришел к такому же выводу. Все эти ответы здесь могут быть суммированы в один лайнер. Это не имеет практического эффекта, но может быть полезно для удобства чтения. Приятно видеть вас в сети, а не только на встречах :)
Авив,

1

Причина этого не в том, что во время компоновки компоновщик пытается разрешить внешнее определение (в вашем случае extern int f()). Не имеет значения, находит ли он его в том же файле или в другом файле, если он найден.

Надеюсь, что это ответ на ваш вопрос.


1
Тогда зачем externвообще добавлять к какой-либо функции?
Элазар Лейбович

2
Пожалуйста, воздержитесь от размещения несвязанного спама в ваших сообщениях. Спасибо!
Mac
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.