Компилятор GCC
GСС — это свободно доступный оптимизирующий компилятор для языков C, C++.
Программа gcc, запускаемая из командной строки, представяляет собой надстройку над группой компиляторов. В зависимости от расширений имен файлов, передаваемых в качестве параметров, и дополнительных опций, gcc запускает необходимые препроцессоры, компиляторы, линкеры.
Файлы с расширением .cc или .C рассматриваются, как файлы на языке C++, файлы с расширением .c как программы на языке C, а файлы c расширением .o считаются объектными.
Чтобы откомпилировать исходный код C++, находящийся в файле F.cc, и создать объектный файл F.o, необходимо выполнить команду:
Опция –c означает «только компиляция».
Чтобы скомпоновать один или несколько объектных файлов, полученных из исходного кода — F1.o, F2.o, . — в единый исполняемый файл F, необходимо ввести команду:
Опция -o задает имя исполняемого файла.
Можно совместить два этапа обработки — компиляцию и компоновку — в один общий этап с помощью команды:
<compile-and-link –options> — возможные дополнительные опции компиляции и компоновки. Опция –lg++ указывает на необходимость подключить стандартную библиотеку языка С++, <other-libraries> — возможные дополнительные библиотеки.
После компоновки будет создан исполняемый файл F, который можно запустить с помощью команды
<arguments> — список аргументов командной строки Вашей программы.
В процессе компоновки очень часто приходится использовать библиотеки. Библиотекой называют набор объектных файлов, сгруппированных в единый файл и проиндексированных. Когда команда компоновки обнаруживает некоторую библиотеку в списке объектных файлов для компоновки, она проверяет, содержат ли уже скомпонованные объектные файлы вызовы для функций, определенных в одном из файлов библиотек. Если такие функции найдены, соответствующие вызовы связываются с кодом объектного файла из библиотеки. Библиотеки могут быть подключены с помощью опции вида -lname . В этом случае в стандартных каталогах, таких как /lib , /usr/lib, /usr/local/lib будет проведен поиск библиотеки в файле с именем libname.a. Библиотеки должны быть перечислены после исходных или объектных файлов, содержащих вызовы к соответствующим функциям.
Опции компиляции
Среди множества опций компиляции и компоновки наиболее часто употребляются следующие:
Gcc как пользоваться
Для создания программ на Си необходим текстовый редактор, с помощью которого можно набрать исходный код. И также необходим компилятор, который принимает файл с исходным кодом на Си и компилирует его в исполняемый файл.
В качестве компилятора мы будем использовать наиболее популярный на сегодня компилятор gcc, который доступен для разных платформ. Более подобному информацию о GCC можно получить на официальном сайте проекта https://gcc.gnu.org/.
Набор компиляторов GCC распространяется в различных версиях. Одной из наиболее популярных версий является пакет средств для разработки от некоммерческого проекта Mingw-w64. Для установки соответствующего пакета компиляторов загрузим файл установщика по ссылке https://sourceforge.net/projects/mingw-w64/files/Toolchains%20targetting%20Win32/Personal%20Builds/mingw-builds/installer/mingw-w64-install.exe
При запуске установщика откроется следующее окно:

Нажмем на кнопку Next > и перейдем к следующему шагу:

Если версия ОС 64-битная, то в поле Architecture следует выбрать пункт x86_64 . Остальные настройки оставим по умолчанию и нажмем на кнопку Next > . На следующем шаге укажем путь, по которому будет устанавливаться пакет:

Можно оставить настройки по умолчанию. И после перехода к следующему шагу собственно начнется установка.
После завершения установки на жестком диске по пути, которое было выбрано для установки, появятся все необходимые файлы компиляторов. В моем случае они находятся по пути C:\Program Files (x86)\mingw-w64\i686-7.1.0-posix-dwarf-rt_v5-rev0\mingw32\bin :

В зависимости от версии пакета точный путь может отличаться, но в любом случае все файлы будут располагаться по тому пути, который был указан на шаге установки.
В частности, файл gcc.exe как раз и будет представлять компилятор для языка Си.
Далее для упрощения запуска компилятора мы можем добавить путь к нему в Переменные среды. Для этого перейдем к окну Система -> Дополнительные параметры системы -> Переменные среды :

И добавим путь к компилятору:

Итак, компилятор установлен, и теперь мы можем написать первую программу. Для этого потребуется любой текстовый редактор для набора исходного кода. Можно взять распространенный редактор Notepad++ или даже обычный встроенный Блокнот.
Итак, создадим на жестком диске папку для исходных файлов. А в этой папке создадим новый файл, который назовем hello.c .

В моем случае файл hello.c находится в папке C:\c.
Теперь определим в файле hello.c простейший код, который будет выводить строку на консоль:
Для вывода строки на консоль необходимо подключить нужный функционал. Для этого в начале файла идет строка
Директива include подключает заголовочный файл stdio.h, который содержит определение функции printf, которая нужна для вывода строки на консоль.
Далее идет определение функции int main(void) . Функция main должна присутствовать в любой программе на Си, с нее собственно и начинается выполнение приложения.
Ключевое слово int в определении функции int main(void) говорит о том, что функция возвращает целое число. А слово void в скобках указывает, что функция не принимает параметров.
Тело функции main заключено в фигурные скобки <>. В теле функции происходит вывод строки на консоль с помощью функции printf, в которую передается выводимая строка «Hello world!».
В конце осуществляем выход из функции с помощью оператора return . Так как функция должна возвращать целое число, то после return указывается число 0. Ноль используется в качестве индикатора успешного завершения программы.
После каждого действия в функции ставятся точка с запятой.

Теперь скомпилируем этот файл. Для этого откроем командную строку Windows и вначале с помощью команды cd перейдем к папке с исходным файлом:
Использование GCC
Простейший способ задействовать средства GCC — выполнить в командной строке команду: ( $ здесь и далее означает приглашение командной строки, а курсивом выделены строки, которые должны быть чем-то замещены, причем сама строка указывает на то, чем). Файл с указанным именем должен содержать исходный код на одном из языков, поддерживаемых GCC. Расширение файла может подсказать, какой именно язык используется. В частности, для C++ можно использовать такие расширения:
- c c-header c-cpp-output
- c++ c++-header c++-cpp-output
- objective-c objective-c-header objective-c-cpp-output
- objective-c++ objective-c++-header objective-c++-cpp-output
- assembler assembler-with-cpp
- ada
- f95 f95-cpp-input
- java
- treelang
Если имя ‘a.out’ кажется вам не очень выразительным, то вы наверняка захотите указать другое, делается это так:
Приведённые выше примеры должны натолкнуть на мысль о том, что поведением компилятора можно управлять посредством ключей (наподобие -l , -o ). На самом деле, искусство использования конкретного компилятора на конкретных платформах определяется умением использовать такой набор ключей компиляции, который позволит создавать корректные и максимально эффективные программы по возможности быстро. Мы познакомимся с несколькими группами ключей GCC, приближающими к этой цели.
Языковые стандарты
Для каждого языка программирования, который может компилироваться GCC и для которого существует стандарт, GCC пытается следовать одной или нескольким редакциям этого стандарта, возможно, с некоторыми исключениями и, возможно, с некоторыми расширениями. Явно указать, какого стандарта нужно придерживаться при компиляции вашей программы можно, используя ключ компиляции -std , присваивая ему значения, обозначающие тот или иной стандарт, например: скомпилирует нашу программу в режиме соответствия стандарту C++ 1998 года. Поскольку нас интересуют два языка, C и C++, посмотрим, какие из стандартов поддерживаются для них.
Язык C
GCC поддерживает три версии стандарта C, хотя последняя версия поддерживается лишь частично.
Первый стандарт C появился в 1989 как документ американской стандартизирующей организации ANSI и стал международным стандартом в следующем году, после ратицификации его международной организацией по стандартам ISO. Потребовать, чтобы GCC руководствовался именно им, можно, добавив один из трёх ключей компиляции: -ansi , -std=c89 или -std=iso9899:1990 .
В 1995 в язык была внесена первая «поправка» (amendment), которая добавила диграфы (комбинации символов, замещавших одиночные символы, которых могло не быть на некоторых клавиатурах), макрос __STDC_VERSION__ для того, чтобы в исходном коде можно было проверить версию стандарта, которую поддерживает компилятор, обрабатывающий этот код, и внёсшая изменения в стандартную библиотеку. Поддержка этой редакции включается ключом -std=iso9899:199409 .
Наконец, новая версия стандарта C была опубликована в 1999 и внесла довольно серьезные изменения в язык. Её GCC поддерживает пока не полностью, но эта поддержка включается ключом — std=iso9899:199409 .
По умолчанию GCC использует некоторые расширения языка C, которые довольно редко конфликтуют со стандартом. Использование ключей -std со значениями, перечисленными выше, заблокирует эти расширения. Потребовать явного использования расширений можно, указав ключи -std=gnu89 или -std=gnu99 для стандартов 1989-го и 1999-го годов, соответственно. По умолчанию компиляция программ C происходит в режиме -std=gnu89 , однажды он изменится на -std=gnu99 , когда поддержка стандарта 1999-го года станет полной. Кстати, некоторые расширения GCC для стандарта 1989-го года стали частью стандарта 1999.
Язык C++
GCC поддерживает стандарт ISO C++ (1998 год) и предоставляет экспериментальную поддержку грядущего нового стандарта ISO, который обычно обозначают как C++0x (имея ввиду предполагаемые сроки выхода: 200x). В стандарт 1998-го года вносилась поправка в 2003-м году. GCC реализует большую часть стандарта 1998 (ключевое слово export для раздельной трансляции шаблонов C++ является важным исключением) и почти целиком поправки 2003: -std=c++98 . Некоторые уже окончательно утверждённые средства C++0x можно использовать при использовании ключа -std=c++0x .
Как и в случае с языком C, по умолчанию GCC использует некоторые расширения языка C++, которые блокируются при указании одной из опций, описанных выше. Явное использование расширений можно затребовать, указав ключ -std=gnu++98 или -std=gnu++0x , первый используется по умолчанию при компиляции программ на C++.
Предупреждения компилятора
Предупреждения компилятора это текстовые сообщения, которые может выдавать компилятор при обработке ваших исходных кодов. Эти сообщения указывают на некоторые нелогичные или не строгие по отношению к существующим стандартам языков программирования места в программе. Они могут сигнализировать о потенциальных ошибках или, попросту говоря, о сомнительных с точки зрения компилятора конструкциях.
- операция = в условии оператора if , которую начинающие программисты на C или C++ часто путают с операцией == ;
- неиспользуемые переменные (вы что-то хотели присвоить им, но забыли?);
- отсутствие указанного каталога для поиска включаемых заголовочных файлов (вы опечатались в пути для этого каталога, а соответствующий заголовочный файл взят из какого-то другого места?)
Предупреждения компилятора это очень полезный механизм, позволивший программистам избежать многих незаметных на первый взгляд ошибок, которые могли проявиться только после запуска уже скомпилированной программы (возможно, много лет спустя после первого запуска). Его, безусловно, нужно использовать, и относиться к предупреждениям следует максимально внимательно. Многие авторитетные книги по C++, написанные в жанре сборников советов (Скотта Мейерса, Герба Саттера, Андрея Александреску, Стивена Дьюхерста), содержат совет компилировать программу на максимально строгом уровне предупреждений и добиваться того, чтобы компилятору не к чему было придраться.
- -Wall :
- -Waddress
подозрительное использование адресов в памяти; например, использование имени функции (которое вернёт её адрес) в условном выражении может означать, что программист хотел вызвать эту функцию и использовать её результат, но забыл поставить круглые скобки; - -Warray-bounds (только вместе с ключом -O2 , см. раздел ключи оптимизации)
обращения к элементам массива, которые заведомо выводят за его пределы; - -Wc++0x-compat
использование конструкций, значение которых изменится при переходе от стандарта ISO C++ 1998 к стандарту ISO C++ 200x; например, использование идентификаторов, которые станут ключевыми словами; - -Wchar-subscripts
использование выражения типа char для обращения к элементу массива (внутри квадратных скобок); источником проблем является то, что на некоторых машинах char имеет знаковый тип, а на других беззнаковый; - -Wimplicit-int
объявление не содержит типа: в C считается, что в этом случае использован int и это не приводит к синтаксической ошибке (зато часто приводит к более трудно уловимым); в C++ эта опция не нужна, так как «неявный int» запрещён и объявление без типа считается ошибкой; - -Wimplicit-function-declaration
только для C: функция используется не будучи объявленной (в C++ это вызовет ошибку компиляции); - -Wcomment
использование /* внутри уже начавшегося комментария /* (вложение комментариев не допускается и чаще всего приводит к ошибкам компиляции, но так случается не всегда); - -Wformat
проверка соответствия форматных строк в функциях printf, scanf и других переданным в них аргументам; - -Wmain
(только для C, в C++ возникнет ошибка компиляции) подозрительный тип функции main (в общем случае она должна быть функцией, возвращающей int, с внешней линковкой и имеющей 0, 2 или 3 параметра); - -Wmissing-braces
нехватка фигурных скобок в инициализации массива, например: — на вторую строку будет выдано предупреждение; - -Wnonnull (только C)
передача нулевого указателя на месте параметра, объявленного с ключевым словом nonnull; - -Wparentheses
возможно, не хватает скобок (любого типа); например, предупреждение появится для выражения ‘x -Wpointer-sign (только C)
передача указателей или присваивание с различной «знаковостью» (между знаковыми и беззнаковыми типами); - -Wreorder (только C++)
порядок членов в списке инициализации конструктора класса или структуры не соответствует порядку их объявления; - -Wreturn-type
объявление функции без возвращаемого типа, отсутствие return в не- void функции, наличие return у void функции; в C++ сообщение об отсутствующем return будет подано в любом случае, а отсутствие типа в объявлении считается ошибкой; - -Wsequence-point
указывает на выражения, порядок вычисления которых не специфицирован стандартом и потому зависит от реализации (попросту говоря, непредсказуем), например: a = a++; , a[n] = b[n++]; ; - -Wsign-compare (только для C++)
сравнение знаковых и беззнаковых выражений; - -Wstrict-aliasing (активно только если указан ключ оптимизации -fstrict-aliasing )
преобразование типов небезопасное относительно оптимизаций компилятора, которые он выполняет на основе предположения о том, что любой участок памяти используется только для значений одного типа (или очень похожих типов); - -Wstrict-overflow=1 (активно только если указан ключ оптимизации -fstrict-overflow )
предупреждает о возможных переполнениях типа в тех местах, где компилятор собирается выполнить оптимизацию, основанную на предположении об отсутствии переполнения; - -Wswitch
при использовании переменной типа enum в операторе switch одно или несколько значений этого enum не перечислено в case (наличие default подавляет это предупреждение); метки case проверяющие значения за пределами данного enum также вызовут предупреждение; - -Wtrigraphs
триграф используется вне комментария и может изменить смысл программы (триграф это последовательность символов, обозначающая некоторый специальный символ; задумывались для использования в тех странах, где клавиатуры не содержали необходимых для программирования символов, но сейчас абсолютно устарели); - -Wuninitialized (только при оптимизации с ключом -O1 или выше)
переменная не инициализирована или может оказаться таковой в результате использования setjmp ; - -Wunknown-pragmas
встречена директива компиляции #pragma с неизвестным GCC именем; - -Wunused-function
статическая функция объявлена, но не определена, или невстраиваемая статическая функция определена, но не используется; - -Wunused-label
метка объявлена но не использована; - -Wunused-value
значение выражения, не имеющего сторонних эффектов, нигде не используется; - -Wunused-variable
локальная переменная или статическая неконстантная переменная не используется нигде кроме своего объявления;
- -Wclobbered
переменная может быть изменена вызовами longjmp или vfork ; - -Wempty-body
пустое тело у if , else или do-while ; в C++ также будет выдано предупреждение для пустого тела циклов while и for , если перед ; , закрывающей цикл, не стоит пробельного символа (пустые <> не вызовут предупреждения); - -Wignored-qualifiers
использован квалификатор типа (например, const ) для типа возвращаемого значения функции; в C++ это предупреждение возникнет только для скалярных встроенных типов и void в качестве возвращаемых типов; - -Wmissing-field-initializers
пропущена инициализация одного из полей структуры; - -Wmissing-parameter-type (только C)
пропущен тип параметра функции в её объявлении; - -Wold-style-declaration (только C)
использован какой-либо стиль объявлений, признанный стандартом C устаревшим (например, static не на первом месте в объявлении); - -Woverride-init (только C)
инициализация поля без сторонних эффектов перекрывается при последующем использовании «назначенных инициализаций» (designated initializers); - -Wsign-compare
см. описание этого ключа в разделе -Wall ; - -Wtype-limits
сравнение (обычно на неравенство) всегда имеет значение ‘истина’ (или всегда ‘ложь’) из-за ограничений типов; например, сравнение беззнаковой переменной с нулём; - -Wuninitialized
см. описание этого ключа в разделе -Wall ; - -Wunused-parameter (только в присутствии -Wall или -Wunused )
неиспользуемый параметр функции. -Wextra также выдаёт предупреждения в следующих случаях: - указатель сравнивается с нулём с помощью , , > или >= . Следующие опции имеют смысл только для C++:
- члены enum используются в одном условном выражении вместе с не-членами этого enum ;
- нестатическое поле-ссылка или нестатическое константное поле в классе без конструктора;
- неоднозначность с виртуальными базовыми классами;
- обращение к элементу массива, объявленному как register ;
- взятие адреса переменной, объявленной как register ;
- базовый класс не инициализирован в копирующем конструкторе класса-наследника.
После знакомства с концепцией предупреждений компилятора и поддерживаемыми стандартами языков, стоит заметить, что стандарты сами содержат набор рекомендаций к производителям компиляторов для генерации предупреждений. Включить соответствующие предупреждения можно указав для команды компиляции ключ -pedantic (это можно делать независимо от того, указаны ли другие ключи).
Далее в примерах при использовании GCC мы не будем указывать опции, включающие предупреждения, чтобы не загромождать код. Мы даём честное слово, что на практике всегда используем максимальный уровень предупреждений и рекомендуем вам делать то же.
Отладочная информация
Сложно представить себе программу, которая выполняет что-то осмысленное и которая была написана с первого раза без единой ошибки. Хуже того, иногда может оказаться, что давно и успешно работающая программа также содержит изъяны. Установить источник проблем помогает отладочная информация, которую компилятор может добавить в каждый объектный файл. Она включает в себя указание на типы данных, использованные в программе, и соответствие объектного кода строкам текста исходного файла. Отладочная информация, сгенерированная GCC, предполагается для использования в первую очередь отладчиком GDB (GNU debugger, отладчик GNU), но не только им.
- stabs — используется отладчиком DBX на большинстве BSD-систем. На системах MIPS, Alpha и System V Release 4 эта опция даст результат, котрый не будет понят DBX или другим распространённым отладчиком SDB. На системе System V Release 4 потребует наличие GNU Assembler (ассемблера GNU).
- COFF — используется отладчиком SDB на большинстве System V, до версии System V Release 4.
- XCOFF — используется отладчиком DBX на системах IBM RS/6000.
- DWARF 2 — используется DBX на IRIX 6.
- VMS — используется программой DEBUG на системах VMS.
Каждый из перечисленных форматов может быть использован, только если он поддерживается текущей платформой. Указать формат можно с помощью ключа -gимя_формата_строчными_буквами . Для форматов stabs и COFF можно добавить к ключу символ ‘+’ справа без пробела, это включит дополнительную информацию, специфичную для GDB и может помешать работе других отладчиков с этим файлом.
Указав ключ -g , можно получить информацию в формате, родном для текущей архитектуры (но будут использоваться расширения, специфичные для GDB). Самая подробная с точки зрения GDB отладочная информация, доступная на данной платформе, будет сгенерирована при использовании ключа -ggdb (будет использован максимально выразительный доступный на данной платформе формат).
- Уровень 0 отключает генерацию отладочной информации.
- Уровень 1 обеспечивает минимум отладочной информации, позволяющей просматривать стеки вызовов подпрограмм, которые вы не планировали отлаживать. Включаются описания функций и внешних переменных, но отсутствует информация о локальных переменных и номерах строк исходного кода.
- Уровень 2 используется по умолчанию и добавляет к уровню 1 информацию о локальных переменных и номерах строк исходного кода.
- Уровень 3 включает дополнительную информацию, например, определения макросов.
В заключение этого пункта следует сказать ещё две вещи. Во-первых, как и многие современные компиляторы, GCC поддерживает разные уровни оптимизации генерируемого кода, они задаются ключом -O и сопутствующими. Некоторые современные компиляторы в то же время не позволяют использовать одновременно отладочную информацию и оптимизацию, другими словами, ключи семейств -g и -O конфликтуют. Последнее не относится к GCC: он позволяет совмещать указанные возможности. При этом отладка может выявить интересные эффекты оптимизации: например, строки кода исполняются в неожиданном порядке, некоторые из них просто пропускаются, некоторые объявленные переменные могут исчезнуть (не проявляться при отладке).
- C ключом -s .
- Без каких-либо ключей, касающихся отладки.
- С ключом -ggddb3 , который должен поместить довольно много отладочной информации в исполняемый файл.
Оптимизация
Если с предупреждениями компилятора всё ясно (действует принцип «чем больше, тем лучше»), количество отладочной информации также, в основном, контролируется программистом в соответствии с его целями (доведение программы до работоспособного состояния или изготовление окончательной версии программы для передачи заказчику), то с оптимизацией дело обстоит намного сложней. Оптимизация это процесс улучшения производительности программы, в том числе, увеличение скорости её работы и снижение объёма занимаемой памяти (эти две цели, между прочим, иногда противоречат друг другу). GCC поддерживает большой набор ключей оптимизации и часть из них объединены в группы (как и в случае с предупреждениями), что позволяет активировать их также по групповому принципу.
Возникает закономерный вопрос: если есть ключи, ускоряющие программу, почему бы не включить их все разом и получить самый быстрый код? Проблема состоит в том, что эффективность значительной части средств для увеличения производительности программы сильно зависит от характера оптимизируемого кода и архитектуры аппаратной платформы. Оптимизация, таким образом, превращается в искусство отражения конкретных особенностей реализации и имеющегося аппаратного обеспечения на существующие ключи компилятора. Кроме того, результат этой деятельности, вероятно, окажется плохо или просто непереносимым. Неудачный выбор ключей, с другой стороны, может обеспечить гарантированное и весьма существенное падение производительности.
Мы упомянем только несколько ключей, обозначающих группы опций оптимизации, которые были подобраны разработчиками GCC и позволяют улучшить производительность во многих случаях, оставив рассмотрение отдельных опций и их связь с особенностями аппаратного обеспечения для более специализированных публикаций.
- -O1 (также носит обозначение -O , признанное устаревшим)
Мягкая оптимизация, некоторое увеличение времени компиляции, возможно ощутимое увеличение памяти для больших функций; включены оптимизации, которые должны одновременно уменьшать общую занимаемую память и ускорять код. - -02
Включает практически все доступные оптимизации, не ухудшающие сильно один из двух показателей двоичного кода — память или скорость — за счёт другого; не включает развертку циклов и автоматическое встраивание функций. - -O3
Наиболее «агрессивная» оптимизация, включаются развертку циклов и автоматическое встраивание функций. - -O0
Отключение оптимизаций, основная цель: высокая скорость компиляции и предсказуемость результатов отладки; эта опция задействована по умолчанию. - -Os
Оптимизация размера программы. Включаются те опции из набора -O2 , которые обычно не увеличивают объём кода. Также применяются некоторые другие оптимизации, направленные на снижение его объёма.
Раздельная трансляция
- любое изменение в тексте файла с программой приводит к его полной перекомпиляции, что занимает ощутимое время;
- сложно организовать коллективную разработку (согласовать внесение изменений в один файл несколькими программистами);
- затрудняется навигация по созданному коду.
Начнём с простого случая, когда программа представляет из себя набор функций, и мы хотим вынести их часть в отдельный ‘.cpp’-файл. Возьмём уже использовавшийся пример с программой ‘first.cpp’ и добавим в неё вызов функции, определение которой помещается в файле ‘second.cpp‘: Обратите внимание на объявление int f(); в ‘first.cpp’: правила языка C++ требуют объявить каждую функцию перед первым использованием. Обычно вместо того, чтобы вручную писать объявления всех функций, определённых в другой единице трансляции (‘.cpp’-файле), используют заголовочные файлы, содержащие все необходимые объявления, текст которых целиком включается в нужный ‘.cpp’-файл директивой препроцессора C #include . Обычно для каждого ‘.cpp’-файла, который не содержит точки входа в программу (функции main ), а значит, содержимое которого будет использовано другими частями программы, создаётся заголовочный файл с таким же основным именем, но другим расширением. В нашем примере можно добавить заголовочный файл ‘second.hpp’ и получить эквивалентный приведённому выше, но в перспективе более удобный в использовании код: Директивы препроцессора C (строки, начинающиеся # ) в заголовочном файле — «стражи включения», — как обычно, добавляются для того, чтобы данный файл не был по случайности включён в один ‘.cpp’-файл несколько раз (символ SECOND_HPP должен быть своим у каждого заголовочного файла, потому разумно связывать его с именем этого файла).
Вернёмся от увлекательных особенностей C++ к использованию GCC. Оба указанных выше варианта компилируются и линкуются в один исполняемый файл одинаково, при помощи простого перечисления: Стоит отметить удобную возможность использования шаблонов имён (wildcards), чтобы избежать полного перечисления файлов: даст аналогичный приведённому выше результат.
Итак, мы сумели разнести код в несколько файлов, но пока еще не добились того, чтобы изменения в одном файле не приводили к перекомпиляции всех. Здесь снова придется вспомнить о том, что используемые до этого момента вызовы gcc ( g++ ) приводят не только к компиляции, но и к линковке полученных после компиляции объектных фалов в один исполняемый файл. Нам потребуется явно разбить создание исполняемого файла на две стадии, используя ключ gcc -с , который просит оставить получившиеся объектные файлы на диске и не линковать их: Как видно, на диске появились два объектных файла (расширение ‘.o’), которые, очевидно, соответствуют исходным ‘.cpp’. Слинковать и получить исполняемый файл можно обычным образом: (GCC определяет тип файлов и не пытается компилировать их как исходные тексты, а сразу линкует). Теперь, внеся изменение в файл ‘second.cpp’, можно перекомпилировать только его и заново слинковать все объектные файлы программы: Не поленитесь исправить в тексте ‘second.cpp’ 42 на ваше любимое число и убедиться, что эта схема работает.
Библиотеки
Раздельная трансляция файлов делает доступным систематическое повторное использование кода: набор объектных файлов, содержащих функции, которые могут быть использованы в разных программах, собирается в библиотеку (программного кода), представляющую собой единый файл, который содержит весь код объектных с некоторой дополнительной информацией (наподобие индекса в настоящих библиотеках). Последнее может ускорять процесс линковки при использовании библиотеки по сравнению с обычной сборкой программы из большого числа объектных файлов. Информация об исходных объектных файлах также сохраняется.
- статические: код из библиотеки добавляется в исполняемый файл на стадии линковки, и после её окончания файл библиотеки больше не нужен полученной программе;
- динамические: код из библиотеки не добавляется в исполняемый файл, а загружается в память во время запуска программы, таким образом, он должен быть доступен при каждом запуске;
Бесспорное преимущество динамических библиотек состоит в том, что если несколько программ используют одну библиотеку, то она загружается в память только один раз. Иными словами, сразу несколько программ могут (и будут) использовать один загруженный экземпляр библиотеки «одновременно». В то же время использование статической библиотеки заставит добавлять части её кода в каждый исполняемый файл по отдельности. Обновление динамической библиотеки потребует перезапуска, использующих её программ, статической — их перелинковки (что обычно занимает немало времени).
Статические библиотеки
- c — создать (create) библиотеку,
- r — заменить (replace) совпадающие объектные файлы внутри библиотеки указанными новыми (в случае, если библиотека существовала),
- s — создать индекс внутри библиотеки (может быть, у вас получится догадаться о происхождении имени ключа. ).
- префикс ‘lib’,
- расширение ‘.a’.
- l — для имени библиотеки,
- L — для указания пути к ней.
Динамические библиотеки
Создание и использование динамической библиотеки более сложны. При компиляции исходных файлов в объектные, которые станут основой для будущей библиотеки, нужно указать один дополнительный ключ: (мы не упоминаем файл ‘third.cpp’, чтобы не загромождать год: создадим библиотеку из одного файла). Ключ -fPIC позволяет создавать независимый от размещения в оперативной памяти код (position independent code), что позволяет использовать один библиотечный код из разных программ (находящихся, очевидно, в разных местах оперативной памяти) во время их выполнения. Будем для краткости называть результирующий файл этой команды так: объектный pic-файл. Можно вместо ключа -fPIC использовать -fpic аналогичного назначения, который может создать файл меньшего размера, но работает не на всех платформах. Если на вашей платформе он не поддерживается, GCC сообщит вам об этом.
-
«Реальное имя» (real name): имя файла на диске, содержащего код библиотеки. Должно иметь форму:
- n это номер версии библиотеки,
- m это номер промежуточной («минорной») версии данной версии библиотеки,
- k это номер «релиза» промежуточной версии библиотеки.
- $ ldconfig -n /путь/к/каталогу
- $ ln -s /путь/к/каталогу/libимя.so.n.m /путь/к/каталогу/libимя.so.n
Рассмотрим команду получения файла библиотеки (который должен носить «реальное имя», п. 1 списка выше) из объектного pic-файла: Ключ -shared имеет вполне понятное значение: мы сигнализируем GCC о том, что хотим получить динамическую библиотеку («разделяемый объект»). После ключа -o идёт имя выходного файла («реальное имя» файла нашей библиотеки), а затем список файлов, которые должны в эту библиотеку войти: в нашем случае это всего один объектный файл ‘second.cpp’.
Теперь попробуем разобраться с тем, что написано между ключами -shared и -o . Ключ -Wl говорит о том, что далее пойдут опции, которые предназначаются непосредственно линковщику. В соответствии с синтаксическими правилами, в списке этих опций не должно присутствовать пробелов: мы передаём GCC что-то, на что он должен «закрыть глаза» (опции предназначены не ему, а линковщику) и в этом случае мы договариваемся о том, что он откроет глаза только когда встретит первый пробел. В качестве символа-разделителя в списке этих опций вместо пробела выступает запятая. Теперь должно быть ясно, что между ключами -shared и -o стоит опция, передаваемая линковщику, которая указывает на «so-имя» (п. 2 списка выше) создаваемой библиотеки.
Такое усложнение возникает из основного принципа работы GCC (как и большинства Unix-программ): решаемая задача неявно разбивается на подзадачи, которые выполняют отдельные утилиты. Это создаёт некоторый уровень абстракции: для большинства операций мы просто вызываем команду g++ и не задумываемся, какие именно программы трудятся над обработкой наших файлов, а зачастую их (программ) бывает немало. В случае с -Wl мы натолкнулись на явление, которое Джоэль Спольски назвал «протекающими абстракциями» (его статью «Закон дырявых абстракций» на эту тему можно и сейчас найти в интернете). Более прозрачное решение текущей задачи (получение библиотечного файла из объектного(ых) pic-файла(ов)) состоит в прямом вызове линковщика, минуя g++ :Результат в данном случае будет аналогичным.
Обсудим теперь процесс загрузки библиотек в память. Он, как было сказано, происходит при запуске первой программы, которая использует данную динамическую библиотеку. Загрузка необходимых библиотек при запуске программы осуществляется специальной программой, являющейся частью операционной системы — динамическим линковщиком. Он ищет файлы библиотек в некоторых заранее определённых каталогах, список которых на многих Unix-подобных операционных системах хранится в файле /etc/ld.so.conf . Почти наверняка в этом списке имеются каталоги /lib и /usr/lib . Возможно, сюда входит и /usr/local/lib . Как это часто бывает, разные авторитетные организации и специалисты советуют использовать для динамических библиотек разные папки. Мы остановим свой выбор на /usr/lib . Создание необходимой инфраструктуры библиотечных файлов: одного реального, ссылки с so-именем и ссылки с именем для линковщика — в каком-то из системных каталогов для хранения библиотек часто называют установкой библиотеки.
Теперь, наконец, посмотрим на весь путь от создания динамической библиотеки до запуска программы, её использующей: Приятной неожиданностью можно считать то, что сборка основной программы (из файла ‘first.cpp’) осталась практически без изменений по сравнению со случаем статической библиотеки (за исключением того, что исчезла необходимость в указании пути для поиска файла библиотеки с ключом -L : это и понятно, все библиотечный файлы находятся теперь в системных каталогах). Выполняя эту операцию, мы использовали четвёртое имя библиотеки (п. 4 списка выше).
Обращает на себя внимание необходимость наличия администраторских прав (вызов sudo ) для установки библиотеки. Во время тестовых запусков программы можно обойтись без копирования в системный каталог /usr/lib/ , если использовать переменную окружения LD_LIBRARY_PATH , которая содержит пути для поиска библиотек. В этом случае перед запуском программы её нужно модифицировать, а также снова (как в случае со статической библиотекой) указать ключ -L :
- -Waddress