Вплив ключового слова extern на функції C


171

В C я не помітив жодного ефекту від externключового слова, яке було використано до оголошення функції. Спочатку я подумав, що, визначаючи extern int f();в одному файлі, змушує вас реалізувати його поза межами області файлу. Однак я з’ясував, що обидва:

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

і

extern int f() {return 0;}

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

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

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

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


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

4
Не завжди це зайве, дивіться мою відповідь. Кожен раз, коли вам потрібно поділитися чимось між модулями, яких ви НЕ хочете, у загальнодоступному заголовку, це дуже корисно. Однак "винищення" кожної окремої функції в загальнодоступному заголовку (із сучасними компіляторами) має мало користі, оскільки вони можуть зрозуміти це самостійно.
Тім Пост

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


2
@Barry Якщо взагалі, інше питання - це дублікат цього. 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 () надрукував привіт світ лише один раз, але продовжував би вводити bar_function (). Також зауважте, bar_function () не збирається повертатися в цьому прикладі (оскільки це просто простий приклад). Уявіть, що stop_now змінюється, коли сигнал обслуговується (отже, непостійний), якщо це не здається достатньо практичним.

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

Сподіваюся, що це допомагає :)


56
Ваш код складеться просто без зовнішнього вигляду перед функцією bar_function.
Елазар Лейбович

2
@Tim: Тоді ви не мали сумнівної привілею працювати з кодом, з яким працюю. Це може статися. Іноді заголовок містить також визначення статичної функції. Це некрасиво і непотрібно 99,99% часу (я міг би бути відхиленим на порядок або два чи величину, завищуючи, як часто це потрібно). Зазвичай це трапляється, коли люди неправильно розуміють, що заголовок потрібен лише тоді, коли інші вихідні файли будуть використовувати інформацію; заголовок (ab) використовується для зберігання інформації декларації для одного вихідного файлу, і жоден інший файл не передбачає його включення. Іноді це трапляється з більш підкреслених причин.
Джонатан Леффлер

2
@Jonathan Leffler - Дуже сумнівно! Раніше я успадкував якийсь досить схематичний код, але, чесно можу сказати, я ніколи не бачив, щоб хтось ставив статичну декларацію в заголовок. Це здається, що у вас досить весела та цікава робота :)
Tim Post

1
Недоліком "прототипу функції не в заголовку" є те, що ви не отримуєте автоматичної незалежної перевірки відповідності між визначенням функції в bar.cі декларацією в foo.c. Якщо функція оголошена в foo.h і обидва файли включають foo.h, то заголовок виконує узгодженість між двома вихідними файлами. Без нього, якщо визначення bar_functionв bar.cзмінах , але заяву в foo.cне змінилося, то все піде не так під час виконання; компілятор не може визначити проблему. При правильному використанні заголовка компілятор вирішує проблему.
Джонатан Леффлер

1
extern для оголошень функцій зайвий, як 'int' у 'unsigned int'. Добре застосовувати "extern", коли прототип НЕ є прямою декларацією ... Але він дійсно повинен жити в заголовку без "extern", якщо завдання не є крайнім регістром. stackoverflow.com/questions/10137037 / ...

82

Наскільки я пам’ятаю стандарт, всі декларації функції вважаються за замовчуванням «зовнішніми», тому немає необхідності чітко вказувати це.

Це не робить це ключовим словом непотрібним, оскільки воно також може використовуватися зі змінними (і в цьому випадку - це єдине рішення для вирішення проблем зв’язку). Але з функціями - так, це необов’язково.


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

3
Зворотна сумісність може бути болем.
MathuSum Mut

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

1
Наскільки обмеження ключового слова додає шум, поза мною, але я думаю, це питання смаку.
Елазар Лейбович

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

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, виділяє для нього простір і присвоює йому значення, програма не може компілювати з невизначеною посиланням. Це говорить розробнику, що він посилався на змінну, але ще не визначив її. Якщо з іншого боку я опущу ключове слово "екстерн" і видаляю int i = 2рядок, програма все-таки компілюється - я визначуся зі значенням за замовчуванням 0.

Змінні області файлу неявно визначаються зі значенням за замовчуванням 0 або NULL, якщо ви явно не призначите їм значення - на відміну від змінних блоків області, які ви оголошуєте у верхній частині функції. Ключове слово "екстерн" уникає цього неявного визначення і, таким чином, допомагає уникнути помилок.

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


Ви мали на увазі видалити int i = 2рядок у -3-му абзаці? І чи правильно констатувати, бачачи int i;, що компілятор виділить пам'ять для цієї змінної, але бачачи extern int i;, компілятор НЕ виділяє пам'ять, а шукатиме змінну в іншому місці?
Заморожене полум'я

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

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 */

Ось відповідні параграфи проекту С99 (n1256):

6.2.2 Зв'язки ідентифікаторів

[...]

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

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


Це стандарт, чи ти просто кажеш мені типову поведінку компілятора? У випадку стандарту я буду радий за посилання на стандарт. Але дякую!
Елазар Лейбович

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

1
Я щойно перевірив це в gcc, і обидва "extern int h (); статичний int h () {return 0;}" і "int h (); статичний int h () {return 0;}" приймаються з однаковим увага. Це тільки C99, а не ANSI? Чи можете ви віднести мене до точного розділу в проекті, оскільки це не здається правдою для gcc.
Елазар Лейбович

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

2
@dirkgently, Моє справжнє запитання: чи є якийсь ефект від використання exetrn з декларацією функції, і якщо немає жодної, чому можна додати extern до декларації функції. І відповідь - ні, ефекту немає, і колись був ефект із не надто стандартними компіляторами.
Елазар Лейбович

11

Вбудовані функції мають спеціальні правила щодо того, що externозначає. (Зверніть увагу, що вбудовані функції - це розширення C99 або GNU; вони не були в оригіналі C.

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

Зауважте, що правила для C ++ різні. Наприклад, extern "C"потрібне оголошення C ++ C функцій, до яких ви збираєтесь зателефонувати з C ++, і щодо цього існують різні правила inline.


Це єдина відповідь тут, яка і правильна, і фактично відповідає на питання.
robinjam

4

IOW, зовнішність зайва, і нічого не робить.

Ось чому через 10 років:

Див. Комісію ad6dad0 , фіксувати b199d71 , зробити 5545442 (29 квітня 2019 р.) Від Дентона Лю ( Denton-L) .
(Об'єднав Хуніо С Хамано - gitster- у комітеті 4aeeef3 , 13 травня 2019 р.)

*.[ch]: видалити externз декларацій функції за допомогоюspatch

Був поштовх до видалення externз декларацій функцій.

Видаліть кілька екземплярів " extern" для декларацій функції, які потрапили в Coccinelle.
Зауважте, що у Coccinelle є деякі труднощі з обробкою функцій за допомогою __attribute__або varargs, тому деякіextern декларації залишаються для розгляду в майбутньому патчі.

Це пластир Кокцинеля, який використовується:

  @@
    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-L- у комітеті 7027f50 , 05 вересня 2019 р.)

compat/*.[ch]: видалити externз декларацій функції за допомогою шпага

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

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

Це пластир Кокцинеля, який використовується:

@@
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) .
(Об’єднав Хуніо С Хамано - gitster- у комітеті 59b19bc , 07 жовтня 2019 р.)

promisor-remote.h: падіння externз оголошення функції

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

Видаліть ці помилкові externs.


3

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


2

декларація функції extern означає, що її визначення буде вирішено під час посилання, а не під час компіляції.

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

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


2
IOW, зовнішність зайва, і нічого не робить. Було б набагато зрозуміліше, якби ви поставили це так.
Елазар Лейбович

@ElazarLeibovich Я просто зіткнувся з подібним випадком у нашій кодовій базі і мав той самий висновок. Усі відповіді тут можна підсумувати у вашому одному вкладиші. Це не має практичного ефекту, але може бути приємним для читабельності. Приємно бачити вас в Інтернеті, а не тільки в зустрічах :)
Авів,

1

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

Сподіваюся, що це відповість на ваше запитання.


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

2
Утримайтеся від розміщення спорідненої спаму у своїх публікаціях. Дякую!
Мак
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.