Подтвердить что ты не робот

Как работают заголовки и исходные файлы на C?

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

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?

4b9b3361

Ответ 1

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

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

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

В каждом блоке компиляции все используемые функции должны быть объявлены, чтобы компилятор знал, что функция существует и каковы ее аргументы. В вашем примере объявление функции 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, она имеет внутреннюю связь - она ​​видна только внутри единицы компиляции, в которой она определена (компоновщик не знает, что он существует). Это может быть полезно для функций, которые делают что-то внутренне в исходном файле и что вы хотите скрыть от остальной части программы.

Ответ 2

Язык 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. В случаях, когда все не совпадает, возникает ошибка компоновщика.

Ответ 3

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

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

Рассмотрим программу "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

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

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

Ответ 5

Сам компилятор не имеет специфического "знания" отношений между исходными файлами и файлами заголовков. Эти типы отношений обычно определяются файлами проекта (например, makefile, solution и т.д.).

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