Как происходит компиляция
Содержание
Зачем нужен .h файл?
Напомним, что на прошлой практике был создан проект, состоящий из трёх файлов:
- 1.cpp —
main
(вызываетfoo()
) - 2.cpp —
foo
— определение - 2.h —
foo
— объявление
Заметим, что этот же код можно было написать, без использования 2.h.
void foo(); // вместо #include "2.h"
int main() {
foo(3);
}
Возникает законный вопрос: зачем был создан файл 2.h?
Для этой небольшой программы он действительно избыточен, однако в больших проектах его использование крайне желательно. Действительно, ведь во-первых использование этой директивы позволяет значительно сократить код (всего одна строчка, тогда как объявлений может быть достаточно много), а во-вторых, перенося все объявления в один файл, мы значительно сократим стоимость внесения модификаций в программу. Так, например, при желании добавить еще один параметр функции, нам нужно будет изменить всего два файла (заголовочный, и соответствующий файл с определением), тогда как в противном случае пришлось бы вносить изменения в каждый файл, данную функцию использующий.
Защита от повторного включения заголовочных файлов
Иногда может возникнуть ситуация, когда один заголовочный файл включён не один, а несколько раз. Посмотрим, к каким проблемам это может привести. Если в файле содержались только объявления (как в файле 2.h из примера), то ничего страшного не произойдёт (стол останется столом, сколько об этом не объявляй), но не всегда всё так удачно. Действительно, ведь заголовочный файл кроме объявлений файлов может содержать и определения (например констант или классов). Тогда его повторное включение приведет к ошибке компоновки. Чтобы этого избежать, все заголовочные файлы следует защищать от повторного включения.
Делается это так:
#ifndef _2_H_
#define _2_H_
void foo(int k);
#endif //_2_H_
Здесь директива #ifndef
указывает препроцессору, что участок кода до #endif
следует компилировать только в случае, если объявления _2_H_
не было. Директива #define
же указывает, что _2_H_
следует объявить.
Допустим, что есть файл 3.cpp
.
#include "2.h"
#include "2.h"
Что произойдет при препроцессинге этого файла?
Вместо каждого #include "2.h"
будет подставлено содержимое соответствующего файла. На момент первой подстановки _2_H_
еще не определено, по этому произойдут подстановка объявления функции и объявление _2_H_
. На момент же, когда препроцессор перейдет ко второму включению, _2_H_
уже определено, и потому подстановка выполнена не будет.
Так как для совершение столь распространенного действия приходится писать целых три директивы, а к тому же следить за уникальностью объявляемых констант — была придумана директива #pragma once, которая, будучи помещенной в начало заголовочного файла , позволяет добиться того же результата. Однако пользоваться ей надо осторожно, так как в стандарт она не вошла, и потому поддерживается не всеми компиляторами (gcc и компилятор компании Microsoft — поддерживают).
Подробнее о препроцессоре
Препроцессор — весьма мощное средство, и в языке C он использовался весьма широко.
Возьмем для примера функцию, вычисляющую максимум:
int max(int a, int b) {
return a > b ? a : b;
}
В языке C нет перегрузки функций!!!
Поэтому, при необходимости написать функцию максимума, принимающая аргументы как целого, так и действительного типов приходится использовать препроцессор:
#define max(a, b) \
(a > b? a : b)
// '\' — означает перенос строки. В отличие от команд c++
// все макрокоманды должны обязательно
// быть написаны на одной строке.
int a = 1;
max (a, next());
// next() — функция, возвращающая при каждом новом запуске
// значение на 1 больше, чем при предыдущем. Допустим
// она определена ранее в этом файле.
Подводные камни:
- Происходит текстовая подстановка в макрос. (Результат работы программы будет 2, а не 1)
- В случае если не все параметры заключены в скобки, возможны неожиданности с приоритетами операций.
Сборка
Вспомним, как происходит процесс сборки:
Из этой картинки хорошо видно, зачем нужен Makefile. Процесс сборки весьма громоздок, причем большая часть процесса проделывается для каждого файла независимо от других. Значит, если проделывать каждый шаг лишь для тех файлов, для которых это необходимо (зависимости которых обновились) сборка значительно ускорится. (а для больших проектов, например OpenOffice, она может занимать несколько часов)
Многие компиляторы умеют сами обнаруживать зависимости и создавать Makefile
.
Библиотеки
Какие бывают библиотеки?
Статические (*.a
, *.lib
) — код функций вставляется в исполняемый файл
Динамические (*.so
, *.dll
) — в исполняемый файл вставляется имя функции и ее адрес в библиотеке. Для работы необходим файл библиотеки
Отличие библиотек от программ — нет точки входа int main()
.
Пример подключения библиотек:
g++
это синоним для gcc -lstd++
, что указывает, что нужно линковать со стандартной библиотекой с++
. По умолчанию приложение собирается с динамическими библиотеками, в случае, если требуется собрать со статическими необходимо явно указать ключ -static
. Кстати, как уже говорилось, gcc
— это огромный конвейер, содержащий несколько различных компиляторов, и вызов g++
ничего не говорит, о языке написанной нами программы. Информацию о том, какой компилятор следует использовать gcc
получает из расширения файла.
Упражнение 1. Попробовать собрать статическую/динамическую библиотеку и использовать ее в программе.
Подробнее об объектных файлах
В объектных файлах не содержится информации о языке, на котором была написана программа (их формат одинаков для различных языков, различен для разных платформ). По этому можно использовать библиотеки, написанные на языке отличном от языка программы. В каждом объектном файле написана написана информация, об объявленных функциях, переменных — она необходима линковщику. Посмотреть эту информацию, можно, например с помощью программы objdump.
Упражнение 2. Посмотреть содержимое объектного файла.
Можно заметить, что в объектных файлах, скомпилированных из исходников, написанных на C++ названия функций изменены, по сравнению с теми, что были им даны в программе.
Почему компилятор переименовывает функции?
В языке C++ возможна перегрузка функций, т.е. есть возможность создать несколько функций с одинаковыми названиями, но разными типами принимаемых и возвращаемых значений.
Пример:
int max(int a, int b);
double max(double a, double b);
Так как в объектном файле информации о языке уже не сохраняется (в том числе о типах передаваемых/возвращаемых значений), то для того, чтобы эти функции различать (иначе бы при линковке возникла ошибка повторного определения) компилятор дописывает к названиям функций информацию о типах (этот процесс называется маскированием). Восстановить объявление функции по ее расширенному названию можно, например, с помощью программы c++filt.
Как уже говорилось, названия функций можно восстановить из объектного файла, а часто эта информация совершенно излишня. В случаях, когда размер исполняемой программы или библиотеки критичен, его можно уменьшить, заменив названия функций на более короткие. Одна из программ, разработанных специально для этого случая: strip.