Как работают заголовочные и исходные файлы в C? - PullRequest
48 голосов
/ 06 мая 2011

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

tl; dr: Как исходные файлы и файлы заголовков связаны с C? Разбирают ли проекты объявления / определения неявно во время сборки?

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

С учетом этих файлов:

header.h

int returnSeven(void);

source.c

int returnSeven(void){
    return 7;
}

main.c

#include <stdio.h>
#include <stdlib.h>
#include "header.h"
int main(void){
    printf("%d", returnSeven());
    return 0;
}

Будет ли этот беспорядок компилироваться? В настоящее время я делаю свою работу в NetBeans 7.0 с gcc от Cygwin, которая автоматизирует большую часть задачи сборки. Когда проект будет скомпилирован, будут ли соответствующие файлы проекта отсортированы в неявном включении source.c на основе объявлений в header.h?

Ответы [ 5 ]

70 голосов
/ 06 мая 2011

Преобразование файлов исходного кода C в исполняемую программу обычно выполняется в два этапа: компиляция и компоновка .

Сначала компилятор преобразует исходный код в объектные файлы (*.o). Затем компоновщик берет эти объектные файлы вместе со статически связанными библиотеками и создает исполняемую программу.

На первом этапе компилятор берет модуль компиляции , который обычно является предварительно обработанным исходным файлом (то есть исходным файлом с содержимым всех заголовков, которые он #include с) и преобразует это в объектный файл.

В каждом модуле компиляции все используемые функции должны быть объявлены , чтобы компилятор знал, что функция существует и каковы ее аргументы. В вашем примере объявление функции returnSeven находится в заголовочном файле header.h. Когда вы компилируете main.c, вы включаете заголовок в объявление, чтобы компилятор знал, что returnSeven существует, когда он компилирует main.c.

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

Ваша функция returnSeven определена в source.c (а функция main определена в main.c).

Итак, подведем итог: у вас есть два модуля компиляции: source.c и main.c (с заголовочными файлами, которые он включает). Вы компилируете их в два объектных файла: source.o и main.o. Первый будет содержать определение returnSeven, второй - определение main. Затем компоновщик склеит их в исполняемую программу.

О связи:

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

28 голосов
/ 06 мая 2011

Язык C не имеет понятия исходных файлов и заголовочных файлов (как и компилятор). Это просто соглашение; помните, что заголовочный файл всегда #include d в исходный файл; препроцессор буквально просто копирует и вставляет содержимое перед началом правильной компиляции.

Ваш пример должен скомпилироваться (несмотря на глупые синтаксические ошибки). Используя GCC, например, вы могли бы сначала сделать:

gcc -c -o source.o source.c
gcc -c -o main.o main.c

Это компилирует каждый исходный файл отдельно, создавая независимые объектные файлы. На данном этапе returnSeven() не было разрешено внутри main.c; компилятор просто пометил объектный файл так, чтобы он указывал, что он должен быть разрешен в будущем. Так что на данном этапе, это не проблема, что main.c не может видеть определение из returnSeven(). (Примечание: это отличается от того факта, что main.c должен иметь возможность видеть объявление из returnSeven() для компиляции; он должен знать, что это действительно функция, и каков ее прототип Вот почему вы должны #include "source.h" в main.c.)

Затем вы делаете:

gcc -o my_prog source.o main.o

Это связывает два объектных файла вместе в исполняемый двоичный файл и выполняет разрешение символов. В нашем примере это возможно, потому что main.o требует returnSeven(), а это выставляется source.o. В случаях, когда все не совпадает, может возникнуть ошибка компоновщика.

13 голосов
/ 06 мая 2011

В составлении нет ничего волшебного. Ни автоматический!

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

Рассмотрим программу «hello world» (с более простой функцией puts):

#include <stdio.h>
int main(void) {
    puts("Hello, World!");
    return 0;
}

без заголовка компилятор не знает, как обращаться с puts() (это не ключевое слово C). Заголовок позволяет компилятору знать, как управлять аргументами и возвращаемым значением.

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

Теперь рассмотрим, что вы хотели свою собственную версию puts()

int main(void) {
    myputs("Hello, World!");
    return 0;
}

Компиляция только этого кода дает ошибку, потому что у компилятора нет информации о функции. Вы можете предоставить эту информацию

int myputs(const char *line);
int main(void) {
    myputs("Hello, World!");
    return 0;
}

и код теперь компилируется --- но не связывается, т.е. не создает исполняемый файл, потому что нет кода для myputs(). Таким образом, вы пишете код для myputs() в файле с именем "myputs.c"

#include <stdio.h>
int myputs(const char *line) {
    while (*line) putchar(*line++);
    return 0;
}

и вы должны не забыть скомпилировать и ваш первый исходный файл и "myputs.c" вместе.

Через некоторое время ваш файл "myputs.c" расширился до набора функций, и вам необходимо включить информацию обо всех функциях (их прототипах) в исходные файлы, которые хотят их использовать.
Удобнее записать все прототипы в один файл и #include этот файл. С включением вы не рискуете ошибиться при наборе прототипа.

Вам все равно придется скомпилировать и связать все файлы кода вместе.


Когда они растут еще больше, вы помещаете весь уже скомпилированный код в библиотеку ... и это другая история:)

4 голосов
/ 06 мая 2011

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

Большинство компиляторов фактически не видят два файла по отдельности, они объединяются препроцессором.

2 голосов
/ 06 мая 2011

Сам компилятор не имеет определенных «знаний» о взаимоотношениях между исходными файлами и заголовочными файлами. Эти типы отношений обычно определяются файлами проекта (например, make-файл, решение и т. Д.).

Данный пример выглядит так, как если бы он правильно компилировался. Вам нужно будет скомпилировать оба исходных файла, а затем компоновщику понадобятся оба объектных файла для создания исполняемого файла.

...