Spiiin's blog

Build

Тривиальные вещи, а знают не все С++-программисты.

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

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

Т.е. если в двух файлах с исходным кодом написать #include “header.h”, то компилятор воткнет этот файл в каждый из исходников, директива #pragma once и #ifdef с уникальным именем файла нужны не для этого..

  • Нету даже защиты от рекурсивного включения - 2 заголовочных файла, включающих друг друга, позволяют проверить, насколько используемый компилятор устойчив к выходкам дураков.

Например, MSVS 2008 выдает в таких ситуациях fatal error C1014: too many include files : depth = 1024.

Собственно, компилятор можно попросить остановиться на стадии подстановки инклюдов (как и на любой другой) и изучить результат. Помимо #include препроцессор также выполняет и другие директивы, например, заменяет __LINE__ на номер текущей строки, и раскрывает макросы.

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

Препроцессор проходит по тексту последовательно и имеет состояние, которое может изменяться в ходе обработки текста и проверяться простыми логическими предикатами. Состоянием может управлять текст, который стоит выше места включения и программист/его среда разработки, передающие это состояние при вызове команды компиляции.

То есть #define перед #include может полностью поменять то, что попадет в результате в файл.

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

Если компиляция прошла успешно, будет создан объектный файл (object file, хз, как адекватно перевести), содержащий код, а также таблицу имен для содержащихся в модуле функций.

Для разных языков и компиляторов приняты разные соглашения о названиях имен (Name mangling ), поэтому объектный файл, собранный одним компилятором, может не подойти для другого (скорее всего так и будет!).

О подводных камнях именования функций в связке языков C/C++/Objective C/Objective C++ напишу отдельным постом.

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

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

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

*Из того, что хидер попадет по разу в каждый исходник, который его включает, следует, что в хидеры можно выносить только объявления, а не определения символов. Что является определением, а что объявлением чаще всего понятно (пункт 3.1 стандарта С++, занимает всего одну страницу, как для С++ это очень мало).

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

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