Wpływ słowa kluczowego extern na funkcje C.

171

W C nie zauważyłem żadnego efektu externsłowa kluczowego użytego przed deklaracją funkcji. Na początku myślałem, że definiowanie extern int f();w jednym pliku wymusza zaimplementowanie go poza zakresem pliku. Jednak dowiedziałem się, że oba:

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

i

extern int f() {return 0;}

kompiluje się dobrze, bez ostrzeżeń z gcc. Użyłem gcc -Wall -ansi; nie zaakceptowałby nawet //komentarzy.

Czy są jakieś efekty do użycia extern przed definicją funkcji ? A może jest to tylko opcjonalne słowo kluczowe bez skutków ubocznych dla funkcji.

W tym drugim przypadku nie rozumiem, dlaczego projektanci standardu zdecydowali się zaśmiecać gramatykę zbędnymi słowami kluczowymi.

EDIT: Do wyjaśnienia, wiem, że korzystanie z Internetu do externzmiennych, ale pytam tylko externw funkcjach .

Elazar Leibovich
źródło
Według niektórych badań, które przeprowadziłem, próbując użyć tego do jakichś szalonych celów związanych z szablonami, extern nie jest obsługiwany w formie, w jakiej jest przeznaczony przez większość kompilatorów, więc tak naprawdę nie robi, cóż, niczego.
Ed James
4
Nie zawsze jest to zbyteczne, zobacz moją odpowiedź. Za każdym razem, gdy chcesz udostępnić coś między modułami, czego NIE chcesz w publicznym nagłówku, jest to bardzo przydatne. Jednak „wystawianie na zewnątrz” każdej funkcji w publicznym nagłówku (przy użyciu nowoczesnych kompilatorów) ma bardzo niewielką lub żadną korzyść, ponieważ mogą to zrozumieć samodzielnie.
Tim Post
@Ed .. jeśli volatile int foo jest globalnym w foo.c, a bar.c tego potrzebuje, to bar.c musi zadeklarować go jako extern. Ma swoje zalety. Poza tym może być konieczne udostępnienie jakiejś funkcji, której NIE chcesz ujawniać w publicznym nagłówku.
Tim Post
Zobacz też: stackoverflow.com/questions/496448/…
Steve Melnikoff
2
@Barry Jeśli w ogóle, drugie pytanie jest duplikatem tego. 2009 vs 2012
Elazar Leibovich

Odpowiedzi:

138

Mamy dwa pliki, foo.c i bar.c.

Tutaj jest 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;
}

Teraz tutaj jest bar.c

#include <stdio.h>

extern volatile unsigned int stop_now;

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

Jak widać, nie mamy współdzielonego nagłówka między foo.c i bar.c, jednak bar.c wymaga czegoś zadeklarowanego w foo.c, gdy jest dowiązany, a foo.c potrzebuje funkcji z bar.c, gdy jest dowiązana.

Używając 'extern', mówisz kompilatorowi, że cokolwiek po nim następuje, zostanie znalezione (niestatyczne) w czasie łączenia; nie rezerwuj dla niego niczego w bieżącym przebiegu, ponieważ zostanie napotkany później. Funkcje i zmienne są pod tym względem traktowane jednakowo.

Jest to bardzo przydatne, jeśli chcesz udostępnić część globalną między modułami i nie chcesz umieszczać / inicjalizować jej w nagłówku.

Z technicznego punktu widzenia każda funkcja w publicznym nagłówku biblioteki jest „extern”, jednak oznaczanie ich jako takich nie przynosi żadnych korzyści, w zależności od kompilatora. Większość kompilatorów może to zrozumieć samodzielnie. Jak widzisz, te funkcje są w rzeczywistości zdefiniowane gdzie indziej.

W powyższym przykładzie main () wypisze hello world tylko raz, ale nadal będzie wprowadzać bar_function (). Zauważ również, że bar_function () nie zwróci w tym przykładzie (ponieważ jest to tylko prosty przykład). Wyobraź sobie, że stop_now jest modyfikowany, gdy sygnał jest obsługiwany (stąd, niestabilny), jeśli nie wydaje się to wystarczająco praktyczne.

Zewnętrzne są bardzo przydatne do takich rzeczy, jak programy obsługi sygnałów, muteks, którego nie chcesz umieszczać w nagłówku lub strukturze itp. Większość kompilatorów optymalizuje, aby upewnić się, że nie rezerwują pamięci dla obiektów zewnętrznych, ponieważ wiedzą o tym Zarezerwuję go w module, w którym zdefiniowany jest obiekt. Jednak ponownie nie ma sensu określanie go przy użyciu nowoczesnych kompilatorów podczas tworzenia prototypów funkcji publicznych.

Mam nadzieję, że to pomoże :)

Tim Post
źródło
56
Twój kod skompiluje się dobrze bez extern przed funkcją bar_function.
Elazar Leibovich
2
@Tim: Zatem nie miałeś wątpliwego przywileju pracy z kodem, z którym pracuję. To może się zdarzyć. Czasami nagłówek zawiera również definicję funkcji statycznej. To jest brzydkie i niepotrzebne w 99,99% przypadków (mógłbym się oddalić o rząd, dwa lub wielkość, zawyżając, jak często jest to konieczne). Zwykle występuje, gdy ludzie nie rozumieją, że nagłówek jest potrzebny tylko wtedy, gdy inne pliki źródłowe wykorzystają informacje; nagłówek jest (ab) używany do przechowywania informacji o deklaracji dla jednego pliku źródłowego i żaden inny plik nie powinien go zawierać. Czasami występuje z bardziej skrzywionych powodów.
Jonathan Leffler
2
@Jonathan Leffler - Rzeczywiście wątpliwe! Odziedziczyłem wcześniej dość szkicowy kod, ale mogę szczerze powiedzieć, że nigdy nie widziałem, aby ktoś umieszczał statyczną deklarację w nagłówku. Wygląda na to, że masz dość fajną i interesującą pracę :)
Tim Post
1
Wadą „prototypu funkcji nie ma w nagłówku” jest to, że nie uzyskuje się automatycznego niezależnego sprawdzania spójności między definicją funkcji w bar.ci deklaracją w foo.c. Jeśli funkcja jest zadeklarowana w programie foo.h i oba pliki zawierają foo.h, nagłówek wymusza spójność między dwoma plikami źródłowymi. Bez tego, jeśli definicja bar_functionw bar.czmianach, ale deklaracja w foo.cnie zostanie zmieniona, wtedy coś pójdzie nie tak w czasie wykonywania; kompilator nie może wykryć problemu. Kompilator dostrzega problem z prawidłowo użytym nagłówkiem.
Jonathan Leffler
1
extern w deklaracjach funkcji jest zbędny, jak „int” w „unsigned int”. Dobrą praktyką jest używanie 'extern', gdy prototyp NIE jest deklaracją do przodu ... Ale tak naprawdę powinien znajdować się w nagłówku bez 'extern', chyba że zadanie jest przypadkowe. stackoverflow.com/questions/10137037/…
82

O ile pamiętam standard, wszystkie deklaracje funkcji są domyślnie traktowane jako „extern”, więc nie ma potrzeby jawnego określania tego.

To nie sprawia, że ​​to słowo kluczowe jest bezużyteczne, ponieważ może być również używane ze zmiennymi (i to w tym przypadku - jest to jedyne rozwiązanie problemów z powiązaniami). Ale z funkcjami - tak, to jest opcjonalne.


źródło
21
Następnie jako standardowy projektanta będę zabronić korzystania z funkcji extern, jak to tylko dodaje hałas do gramatyki.
Elazar Leibovich
3
Zgodność wsteczna może być uciążliwa.
MathuSum Mut
1
@ElazarLeibovich Właściwie w tym konkretnym przypadku niedopuszczenie tego jest tym, co spowodowałoby zakłócenie gramatyki.
Wyścigi lekkości na orbicie,
1
Nie rozumiem, jak ograniczenie słowa kluczowego powoduje dodanie szumu, ale myślę, że to kwestia gustu.
Elazar Leibovich
Przydatne jest jednak zezwolenie na użycie "extern" dla funkcji, ponieważ wskazuje on innym programistom, że funkcja jest zdefiniowana w innym pliku, a nie w bieżącym pliku, a także nie jest zadeklarowana w żadnym z dołączonych nagłówków.
DimP
23

Musisz rozróżnić dwie odrębne koncepcje: definicję funkcji i deklarację symbolu. „extern” to modyfikator linkowania, wskazówka dla kompilatora o tym, gdzie zdefiniowany jest symbol, do którego odwołuje się później (wskazówka brzmi „nie tutaj”).

Jeśli napiszę

extern int i;

w zakresie pliku (poza blokiem funkcyjnym) w pliku C, wtedy mówisz „zmienna może być zdefiniowana gdzie indziej”.

extern int f() {return 0;}

jest zarówno deklaracją funkcji f, jak i definicją funkcji f. Definicja w tym przypadku jest ważniejsza od zewnętrznej.

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

to najpierw deklaracja, po której następuje definicja.

Użycie opcji externjest niewłaściwe, jeśli chcesz zadeklarować i jednocześnie zdefiniować zmienną o zasięgu pliku. Na przykład,

extern int i = 4;

poda błąd lub ostrzeżenie, w zależności od kompilatora.

Użycie opcji externjest przydatne, jeśli jawnie chcesz uniknąć definicji zmiennej.

Pozwól mi wyjaśnić:

Powiedzmy, że plik ac zawiera:

#include "a.h"

int i = 2;

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

Plik ah zawiera:

extern int i;
int f(void);

a plik bc zawiera:

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

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

Extern w nagłówku jest przydatny, ponieważ informuje kompilator podczas fazy łączenia, że ​​„to jest deklaracja, a nie definicja”. Jeśli usunę wiersz w ac, który definiuje i, przydzieli dla niego miejsce i przypisze mu wartość, program nie powinien się skompilować z niezdefiniowanym odniesieniem. To informuje dewelopera, że ​​odniósł się do zmiennej, ale jeszcze jej nie zdefiniował. Jeśli z drugiej strony pominę słowo kluczowe „extern” i usunę int i = 2wiersz, program nadal się kompiluje - i zostanie zdefiniowany z domyślną wartością 0.

Zmienne zakresu pliku są niejawnie definiowane z wartością domyślną 0 lub NULL, jeśli nie przypiszesz do nich jawnie wartości - w przeciwieństwie do zmiennych o zakresie blokowym, które deklarujesz na początku funkcji. Słowo kluczowe extern pozwala uniknąć tej niejawnej definicji, a tym samym pomaga uniknąć błędów.

W przypadku funkcji w deklaracjach funkcji słowo kluczowe jest rzeczywiście zbędne. Deklaracje funkcji nie mają niejawnej definicji.

Dave Neary
źródło
Czy chodziło Ci o usunięcie int i = 2wiersza w -3-tym akapicie? I czy słuszne jest stwierdzenie, widząc int i;, że kompilator przydzieli pamięć dla tej zmiennej, ale widząc extern int i;, kompilator NIE przydzieli pamięci, ale szuka zmiennej gdzie indziej?
Frozen Flame
Właściwie, jeśli pominiesz słowo kluczowe „extern”, program nie będzie się kompilował z powodu przedefiniowania i w ac i bc (z powodu ah).
Nixt
15

Słowo externkluczowe przybiera różne formy w zależności od środowiska. Jeśli deklaracja jest dostępna, externsłowo kluczowe przyjmuje powiązanie, jak określono wcześniej w jednostce tłumaczeniowej. W przypadku braku takiej deklaracji externokreśla powiązania zewnętrzne.

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

Oto odpowiednie akapity z projektu C99 (n1256):

6.2.2 Powiązania identyfikatorów

[…]

4 W przypadku identyfikatora zadeklarowanego ze specyfikatorem klasy pamięci extern w zakresie, w którym wcześniejsza deklaracja tego identyfikatora jest widoczna, 23) jeśli poprzednia deklaracja określa powiązanie wewnętrzne lub zewnętrzne, powiązanie identyfikatora w późniejszej deklaracji jest takie samo jako powiązanie określone we wcześniejszej deklaracji. Jeśli żadna wcześniejsza deklaracja nie jest widoczna lub jeśli poprzednia deklaracja nie określa żadnego powiązania, wówczas identyfikator ma powiązanie zewnętrzne.

5 Jeśli deklaracja identyfikatora funkcji nie ma specyfikatora klasy pamięci, jej powiązanie jest określane dokładnie tak, jakby było zadeklarowane za pomocą specyfikatora klasy pamięci extern. Jeśli deklaracja identyfikatora obiektu ma zasięg plikowy i nie ma specyfikatora klasy pamięci, jej powiązanie jest zewnętrzne.

dirkgently
źródło
Czy to standard, czy po prostu mówisz mi o zachowaniu typowego kompilatora? W przypadku standardu będę zadowolony z linku do standardu. Ale dzięki!
Elazar Leibovich
To jest standardowe zachowanie. Wersja robocza C99 jest dostępna tutaj: < open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf >. Rzeczywisty standard nie jest jednak darmowy (wersja robocza jest wystarczająca do większości zastosowań).
dirkgently
1
Właśnie przetestowałem to w gcc i oba „extern int h (); static int h () {return 0;}” i „int h (); static int h () {return 0;}” są akceptowane z tym samym ostrzeżenie. Czy to tylko C99, a nie ANSI? Czy możesz skierować mnie do dokładnej sekcji w szkicu, ponieważ wydaje się, że nie jest to prawdą w przypadku gcc.
Elazar Leibovich
Sprawdź ponownie. Próbowałem tego samego z gcc 4.0.1 i pojawia się błąd dokładnie tam, gdzie powinien. Wypróbuj także kompilator online comeau lub codepad.org, jeśli nie masz dostępu do innych kompilatorów. Przeczytaj normę.
dirkgently
2
@dirkgently, moje prawdziwe pytanie brzmi, czy użycie exetrn z deklaracją funkcji ma jakiś wpływ, a jeśli nie ma, dlaczego można dodać extern do deklaracji funkcji. Odpowiedź brzmi: nie, nie ma żadnego efektu, a kiedyś był efekt w przypadku niezbyt standardowych kompilatorów.
Elazar Leibovich
11

Funkcje wbudowane mają specjalne zasady dotyczące tego, co externoznaczają. (Zauważ, że funkcje wbudowane są rozszerzeniem C99 lub GNU; nie były w oryginalnym C.

W przypadku funkcji innych niż wbudowane externnie jest potrzebne, ponieważ jest domyślnie włączone.

Zauważ, że zasady dla C ++ są różne. Na przykład extern "C"jest potrzebny w deklaracji C ++ funkcji C, które zamierzasz wywołać z C ++, i istnieją różne reguły dotyczące inline.

user9876
źródło
To jedyna odpowiedź, która jest poprawna i faktycznie odpowiada na pytanie.
robinjam
4

IOW, extern jest zbędny i nic nie robi.

Dlatego 10 lat później:

Zobacz commit ad6dad0 , commit b199d71 , commit 5545442 (29 kwietnia 2019) autorstwa Denton Liu ( Denton-L) .
(Scalenie przez Junio ​​C Hamano - gitster- w zatwierdzeniu 4aeeef3 , 13 maja 2019)

*.[ch]: usuń externz deklaracji funkcji używającspatch

Pojawiła się potrzeba usunięcia externz deklaracji funkcji.

Usuń niektóre wystąpienia „ extern” dla deklaracji funkcji przechwytywanych przez Coccinelle.
Zauważ, że Coccinelle ma pewne trudności z przetwarzaniem funkcji z __attribute__lub varargs, więc niektóre externdeklaracje zostaną pozostawione, aby zająć się nimi w przyszłym patchu.

To był użyty plaster Coccinelle:

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

i został uruchomiony z:

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

Nie zawsze jest to jednak proste:

Zobacz commit 7027f50 (04 września 2019) autorstwa Denton Liu ( Denton-L) .
(Scalone przez Dentona Liu - Denton-L- w zobowiązaniu 7027f50 , 05 września 2019)

compat/*.[ch]: usuń externz deklaracji funkcji za pomocą spatch

W 5545442 ( *.[ch]: remove externfrom function deklarations using spatch, 2019-04-29, Git v2.22.0-rc0), usunęliśmy zewnętrzne z deklaracji funkcji używając, spatchale celowo wykluczyliśmy pliki poniżej, compat/ponieważ niektóre są bezpośrednio kopiowane z nadrzędnego źródła i powinniśmy unikać ubijanie ich, aby ręczne łączenie przyszłych aktualizacji było prostsze.

W ostatnim zatwierdzeniu określiliśmy pliki, które zostały pobrane ze źródła, abyśmy mogli je wykluczyć i uruchomić spatchw pozostałej części.

To był użyty plaster Coccinelle:

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

i został uruchomiony z:

$ 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 ma pewne problemy z radzeniem sobie z __attribute__Varargami, więc sprawdziliśmy, czy nie pozostały żadne pozostałe zmiany:

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

Zwróć uwagę, że w Git 2.24 (Q4 2019) wszelkie fałszywe informacje externsą odrzucane.

Zobacz commit 65904b8 (30 września 2019) autorstwa Emily Shaffer ( nasamuffin) .
Pomógł: Jeff King ( peff) .
Zobacz commit 8464f94 (21 września 2019) autorstwa Dentona Liu ( Denton-L) .
Pomoc: Jeff King ( peff) .
(Scalone przez Junio ​​C Hamano - gitster- w zobowiązaniu 59b19bc , 07 października 2019 r.)

promisor-remote.h: drop externz deklaracji funkcji

Podczas tworzenia tego pliku za każdym razem, gdy wprowadzano nową deklarację funkcji, zawierała ona rozszerzenie extern.
Jednak począwszy od 5545442 ( *.[ch]: usuń externz deklaracji funkcji przy użyciu spatch, 2019-04-29, Git v2.22.0-rc0), aktywnie staraliśmy się zapobiec używaniu zewnętrznych elementów w deklaracjach funkcji, ponieważ są one niepotrzebne.

Usuń te fałszywe pliki extern.

VonC
źródło
3

Na externinformuje słów kluczowych kompilatora, że funkcja lub zmienna ma zewnętrzny podnośnik - innymi słowy, że jest on widoczny z plików innym niż to, w którym jest zdefiniowana. W tym sensie ma znaczenie odwrotne do staticsłowa kluczowego. Jest to trochę dziwne, aby umieścić externdefinicję w momencie definiowania, ponieważ żadne inne pliki nie miałyby widoczności definicji (lub spowodowałoby to wiele definicji). Zwykle umieszczasz externdeklarację w pewnym miejscu z widocznością zewnętrzną (na przykład plik nagłówkowy) i umieszczasz definicję w innym miejscu.

1800 INFORMACJE
źródło
2

zadeklarowanie funkcji extern oznacza, że ​​jej definicja zostanie rozwiązana w momencie linkowania, a nie podczas kompilacji.

W przeciwieństwie do zwykłych funkcji, które nie są zadeklarowane jako zewnętrzne, można je zdefiniować w dowolnym pliku źródłowym (ale nie w wielu plikach źródłowych, w przeciwnym razie pojawi się błąd konsolidatora, który mówi, że podałeś wiele definicji funkcji), w tym jeden w który jest zadeklarowany jako extern, więc w przypadku ur konsolidator rozwiązuje definicję funkcji w tym samym pliku.

Nie sądzę, by było to zbyt użyteczne, jednak robienie tego rodzaju eksperymentów daje lepszy wgląd w to, jak działa kompilator i linker języka.

Rampal Chaudhary
źródło
2
IOW, extern jest zbędny i nic nie robi. Byłoby znacznie jaśniejsze, gdybyś to ujął w ten sposób.
Elazar Leibovich
@ElazarLeibovich Właśnie napotkałem podobny przypadek w naszej bazie kodu i doszedłem do tego samego wniosku. Wszystkie te odpowiedzi można podsumować w jednej linijce. Nie ma to praktycznego efektu, ale może być przyjemne dla czytelności. Miło cię widzieć online, a nie tylko na spotkaniach :)
Aviv
1

Przyczyną braku efektu jest to, że w momencie łączenia konsolidator próbuje rozwiązać definicję extern (w twoim przypadku extern int f()). Nie ma znaczenia, czy znajdzie go w tym samym pliku, czy w innym pliku, o ile zostanie znaleziony.

Mam nadzieję, że to odpowiada na twoje pytanie.

Umer
źródło
1
Po co więc externw ogóle pozwalać na dodawanie do dowolnej funkcji?
Elazar Leibovich
2
Prosimy nie umieszczać w swoich postach niepowiązanego spamu. Dzięki!
Mac,