Почему некоторые функции объявили extern и заголовочный файл, не включенный в исходный код в Git исходный код?

Я хотел увидеть исходный код приложения реального мира, чтобы понять хорошие методы программирования и т.д. Поэтому я выбрал Git и загрузил источник для версии 1.8.4.

После случайного просмотра различных файлов что-то привлекло мое внимание в этих двух файлах: strbuf.h strbuf.c

Эти два файла, по-видимому, определяют API с этой документацией.

У меня есть два вопроса:

  • Почему объявления функций в строке 16,17,18,19 и глобальная переменная в строке 6 в 'strbuf.h' объявлены extern?

  • Почему "strbuf.h" не # включен в strbuf.c?

Я как начинающий программист всегда узнал, что вы пишете определения функций в файле .c, тогда как декларации функций, макросы, строки и т.д. записываются в .h файле, который затем включается в каждый .c файл, который хочет использовать эти функции и т.д.

Может кто-нибудь объяснить это?

Ответ 1

strbuf.c включает cache.h и cache.h включает strbuf.h, поэтому ваша предпосылка для вопроса 2 (что strbuf.c не включает strbuf.h) неверна: она включает его, а не напрямую.

extern применяется к функциям

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

static int foo(void); extern int foo(void);

второе объявление foo также объявляет его static, давая ему внутреннюю связь. Если вы пишете:

static int foo(void); int foo(void); /* wrong in 1990s era C */

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

extern int foo(void); static int foo(void); /* ERROR */

Эта третья форма по-прежнему ошибочна. В первом объявлении extern нет предыдущего видимого объявления, поэтому foo имеет внешнюю связь, а затем второе объявление static дает foo внутреннюю связь, создавая поведение undefined.

Короче говоря, extern не требуется для объявлений функций. Некоторые люди просто предпочитают это по причинам стиля.

(Примечание: я оставляю extern inline на C99, что является довольно странным, а реализации различаются. См. http://www.greenend.org.uk/rjk/2003/03/inline.html для более подробной информации.)

extern применяется к объявлениям переменных

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

Для переменных внутри функции (т.е. с "областью блока" ), например somevar в:

void f(void) {
    extern int somevar;
    ...
}

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

Как и в случае с объявлениями функций, привязка extern присваивается внутренней, если есть предыдущая видимая декларация внутренней привязки и внешняя в противном случае. Таким образом, x внутри f() имеет внутреннюю связь, несмотря на ключевое слово extern:

static int x;
void f(void) {
    extern int x; /* note: don't do this */
    ...
}

Единственная причина для написания такого кода - запутать других программистов, поэтому не делайте этого.: -)

В общем, причина аннотирования переменных "глобальная" (т.е. файловая область, статическая длительность, внешняя привязка) с ключевым словом extern заключается в том, чтобы не дать этому конкретному объявлению стать определением. Компиляторы C, использующие так называемую "def/ref" модель, получают неудовлетворение при времени ссылки, когда одно и то же имя определяется более одного раза. Таким образом, если file1.c говорит, что int globalvar; и file2.c также говорит int globalvar;, оба являются определениями, и код не может компилироваться (хотя большинство Unix-подобных систем по умолчанию используют так называемую "общую модель", что делает эта работа в любом случае). Если вы объявляете такую ​​переменную в файле заголовка, который, вероятно, будет включен из многих разных файлов .c, используйте extern, чтобы сделать это объявление "просто декларацией".

Один и только один из этих файлов .c может снова объявить эту переменную, оставив ключевое слово extern и/или включив инициализатор. Или некоторые люди предпочитают стиль, в котором заголовочный файл использует что-то вроде этого:

/* foo.h */
#ifndef EXTERN
# define EXTERN extern
#endif
EXTERN int globalvar;

В этом случае один (и только один) из этих файлов .c может содержать последовательность:

#define EXTERN
#include "foo.h"

Здесь, поскольку extern определен, #ifndef отключает последующий #define, а строка EXTERN int globalvar; расширяется до просто int globalvar;, так что это становится определением, а не объявлением. Лично мне не нравится этот стиль кодирования, хотя он действительно удовлетворяет принципу "не повторяй себя". В основном я считаю заглавную extern вводящей в заблуждение, и этот шаблон бесполезен при инициализации. Те, кто предпочитает это, обычно завершают добавление второго макроса, чтобы скрыть инициализаторы:

#ifndef EXTERN
# define EXTERN extern
# define INIT_VAL(x) /*nothing*/
#else
# define INIT_VAL(x) = x
#endif

EXTERN int globalvar INIT_VAL(42);

но даже это разваливается, когда для элемента, который требуется инициализировать, требуется составной инициализатор (например, a struct, который должен быть инициализирован до { 42, 23, 17, "hike!" }).

(Примечание: я намеренно замалчивал все "предварительное определение" здесь. Определение без инициализатора только "предварительно определено" до конца единицы перевода. Это позволяет использовать некоторые виды передовых ссылок, которые иначе слишком сложно выразить. Это обычно не очень важно.)

, включая заголовок, объявляющий функцию f в коде, который определяет функцию f

Это всегда хорошая идея по одной простой причине: компилятор сравнивает объявление f() в заголовке с определением f() в коде. Если они не совпадают (по какой-либо причине - обычно ошибка в первоначальном кодировании или отказ обновить один из двух во время обслуживания, но иногда просто из-за синдрома Cat Walked On Keyboard или аналогичного), компилятор может поймать ошибку во время компиляции.


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