Архив рубрики: С++ для начинающих

C++ для начинающих. Урок 3. Функции, заголовочные файлы и библиотеки

Текст программы урока

Рассмотрим следующую программу. Не следует искать в программе какой-то сокровенный смысл, так как это не более чем попытка соответствовать теме урока. Как и прежде, следует помнить, что номера строк не должны быть в файле с текстом программы и приведены только для упрощения комментариев к коду.

01. #include <iostream>
02. 
03. int addUpToLimit(int op1, int op2)
04. {
05.     int res = op1+op2;
06.     if (res>100) res = 100;
07.     return res;
08. }
09. 
10. int incrementator(int value)
11. {
12.     return addUpToLimit(value, 8);
13. }
14. 
15. int main(int argc, char *argv[])
16. {
17.    int n = 0;
18. 
19.    n = addUpToLimit(n, 20);
20.    int counter = 0;
21.    for (int m = 0, delta = m-n;
22.         delta!=0;
23.         m = incrementator(n), delta = m-n, n = m, ++counter);
24. 
25.    std::cout << "Cycle counter = " << counter << std::endl;
26. 
27.    return 0;
28. }

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

$ g++ main.cpp

В результате выполнения такой команды под операционной системой Linux получится файл a.out, который можно будет запустить следующим образом.

$ ./a.out

Пояснения по тексту программы урока

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

Сигнатуру функции (от англ. signature — подпись, нечто идентифицирующее) в языке C++, определяет не только имя функции, но и тип параметров вызова функции, а также, тип, который функция возвращает.

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

Ничего особенного в трех функциях представленного примера нет. Предполагается, что общие сведения по синтаксису и основным элементам языка C++ у вас уже есть или вы изучаете их по какому-нибудь учебнику. Внимания, возможно, заслуживает только строки 21-23 записи цикла for. Вообще, цикл for в языке C++ имеет интересные особенности. Особенно для тех, кто пришел в язык C++ из Pascal или Fortran. Дополнительную особенность, указанным строкам, придает использование операторов "запятая". Сама шапка цикла for, состоит из трех частей, которые разделяются через символ "точка с запятой". Подробности записи оператора for следует прочитать в учебнике. Для сравнения, приведем четыре часто используемых формы записи цикла for.

// Обычный цикл по параметру 
for (int i=0; i<n; ++i) { ... } 

// Цикл обхода списка по итератору
std::list<T> ls;
...
for (std::list<T>::iterator it=ls.begin(); it != ls.end(); ++it) { ... }

// Еще один, часто используемый, вариант записи обхода списка по итератору
std::list<T> ls;
...
std::list<T>::const_iterator it=ls.begin();
for ( ; it != ls.end(); ++it) { ... }

// "Бесконечный" цикл. Внутри цикла, по выполнении какого-то условия выполняется break.
for(;;) { ... break; ...}

Не следует сейчас углубляться в понимание синтаксиса элементов использованных во второй и третьей записи цикла — они требуют знания объектно-ориентированного программирования (ООП), обобщенного программирования (ОП) и шаблонов классов-контейнеров из стандартной библиотеки STL. Главное здесь — увидеть в шапке цикла раздел инициализации, раздел проверки выполнения следующей итерации и раздел выполняющийся по завершении каждой из итераций. Каждый из разделов может быть пустым, а может содержать в себе оператор "запятую", который группирует операторы выполняющиеся слева направо.

Разделение текста программы урока на три компиляционных листа

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

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

Файлы с исходными текстами функций можно назвать формально, например, file-1.cpp, file-2.cpp и file-3.cpp. Однако, чтобы проще было связывать имя файла с содержащимися в них функциях примем названия, соответствующие именам функций, которые будут содержаться в файлах. Итак, реализуем следующее.

Файл add-up-to-limit.cpp

int addUpToLimit(int op1, int op2)
{
    int res = op1+op2;
    if (res>100) res = 100;
    return res;
}

Файл incrementator.cpp

int incrementator(int value)
{
    return addUpToLimit(value, 8);
}

Файл main.cpp

#include <iostream>

int main(int argc, char *argv[])
{
   int n = 0;

   n = addUpToLimit(n, 20);
   int counter = 0;
   for (int m = 0, delta = m-n;
        delta!=0;
        m = incrementator(n), delta = m-n, n = m, ++counter);

   std::cout << "Cycle counter = " << counter << std::endl;

   return 0;
}

Попробуем откомпилировать эти файлы тремя компиляционными листами. Чтобы создать объектный файл из файла исходного кода, следует остановить компиляцию после одноименного этапа — этапа компиляции (см. урок 1 по этапам компиляции). Для этого, следует воспользоваться ключом -c. Таким образом, чтобы откомпилировать файл add-up-to-limit.cpp выполним следующую команду.

$ g++ -с add-up-to-limit.cpp

Выполнение команды должно пройти без проблем. Действительно, файл add-up-to-limit.cpp состоит из одной только функции addUpToLimit(). Внутри файла не содержится ни одной инструкции препроцессора, так как для компиляции функции определены все символы, и, следовательно, компилятор найдет все необходимое для того, чтобы перевести данный исходный текст на языке C++ в язык ассемблера, и, потом, синтезировать по нему последовательность процессорных инструкций, которые будут соответствовать исходному тексту. Таким образом, первый компиляционный лист у нас полностью соответствует тексту файлу add-up-to-limit.cpp.

Сможем ли мы так же поступить с файлом incrementator.cpp? При попытке выполнить аналогичную команду компиляции мы оказываемся перед решением следующей проблемы.

$ g++ -c incrementator.cpp 
incrementator.cpp: В функции «int incrementator(int)»:
incrementator.cpp:3:33: ошибка: нет декларации «addUpToLimit» в этой области видимости

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

Возникает задача. Как решить эту проблему? Если мы добавим всю реализацию функции addUpToLimit() в файл incrementator.cpp, то рушится наша задача разделения исходного текста приложения по файлам.

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

Итак, если мы правы, то нам необходимо добавить объявление (declaration) функции addUpToLimit() в файл incrementator.cpp. Следовательно файл incrementator.cpp изменится до следующего состояния.

Исправленный файл incrementator.cpp

int addUpToLimit(int op1, int op2);

int incrementator(int value)
{
    return addUpToLimit(value, 8);
}

Если поставить себе задачу упростить запись объявления функции addUpToLimit(), то его можно записать по следующему образцу, без имени параметров.

int addUpToLimit(int, int);

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

Теперь создание объектного файла по файлу incrementator.cpp должно пройти без проблем. Выполним для этого следующую команду.

$ g++ -c incrementator.cpp 

Таким образом, второй компиляционный лист нашего примера представляет собой несколько исправленный файл incrementator.cpp в который было добавлен объявлении функции addUpToLimit().

Подумаем теперь, как быть с компиляцией третьего файла — main.cpp. Можно даже не пытаться его компилировать, так как мы уже заранее можем увидеть, что компилятор не сможет оформить стеки для вызова функций addUpToLimit() и incrementator(), которые вызываются в функции main(). Действительно выполнение компиляции неправленного файла main.cpp завершится со следующими ошибками.

g++ -c main.cpp 
main.cpp: В функции «int main(int, char**)»:
main.cpp:7:26: ошибка: нет декларации «addUpToLimit» в этой области видимости
main.cpp:11:28: ошибка: нет декларации «incrementator» в этой области видимости

Итак, мы видим уже известные проблемы и можем предложить известное решение. Добавим объявление требуемых функций в файл main.cpp.

Исправленный файл main.cpp

#include <iostream>

int addUpToLimit(int, int);
int incrementator(int);

int main(int argc, char *argv[])
{
   int n = 0;

   n = addUpToLimit(n, 20);
   int counter = 0;
   for (int m = 0, delta = m-n;
        delta!=0;
        m = incrementator(n), delta = m-n, n = m, ++counter);

   std::cout << "Cycle counter = " << counter << std::endl;

   return 0;
}

После внесения указанных исправлений выполним создание последнего объектного файла следующей командой.

$ g++ -c main.cpp 

Третий компиляционный лист нашего исходного примера будет представлять собой исправленный файл main.cpp в который были добавлены объявления функций addUpToLimit() и incrementator(), и в который, командой препроцессора include было "влито" содержимое заголовочного файла iostream.

Теперь у нас есть все необходимое для линковки объектных файлов сделанных по трем компиляционным листам. Выполним для этого следующую команду.

$ g++ *.o 

Символ "звездочка" символизирует любой символ любое количество раз и сочетание *.o представляет собой специальную маску (типа wildcard), которая соответствует всем объектным файлам, которые будут найдены в текущей директории исполнения команды. В нашем случае, это соответствует файлам add-up-to-limit.o, incrementator.o и main.o.

Для тех, кто предпочитает более подробную запись, команду линковки можно написать следующим образом.

$ g++ add-up-to-limit.o incrementator.o main.o

В результате выполнения последней команды, в операционной системе Linux будет образован исполняемый файл a.out, инструкции которого соответствуют исходному тексту нашей исходной программе, которая была разнесена на три файла.

Использование заголовочных файлов

Представленная в предыдущем разделе система сборки файлов является достаточно общей. Она демонстрирует крайнюю примитивность компилятора C++ в вопросах получения сведений об элементах при обработке ситуаций использования этих элементов. Мы можем раскидать определения (definitions) функций по любому количеству файлов и на основе этих файлов сделать компиляционные листы добавляя в каждый из них объявления (declarations) необходимых символов. Однако в таком решении кроются следующие неудобства.

  1. Если множество символов из одного файла используются во многих других файлах, то в каждом их этих файлов придется повторять множество объявлений этих символов.
  2. Если делается изменения в одном из символов файла, то придется копировать эти изменение во все файлы, где используется этот символ.

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

Наверное нетрудно догадаться, что используя директиву препроцессора include можно решить эту проблему тем, что для каждого файла с определениями (definitions) символов написать файл с объявлениями (declarations) тех символов, которые мы захотим объявить для стороннего использования (возможно, что какие-то символы внутренней реализации следует скрыть). Такие файлы с объявлениями, в русском языке, называются "заголовочными". Возможно, такой термин возник как следствие того факта, что эти файлы хранят в себе "заголовки" определений, т.е. то, что в оригинальной английской терминологии называется объявлениями (declarations). По аналогичным причинам, в английском языке, такие файлы называются header files, а наиболее часто используемым суффиксом для имени таких файлов является суффикс .h (для операционных систем типа DOS/Windows, говорят о расширении имен файлов — h).

Для того, чтобы определить, сколько заголовочных файлов требуется для некоторого количества файлов с описаниями функций можно использовать разные подходы. Можно использовать один заголовочный файл на несколько файлов с описаниями или несколько заголовочных файлов на один файл описания. Наиболее часто используемым, и, возможно, формально оптимальным решением является создание одного заголовочного файла с объявлениями всех публикуемых символов из соответствующего файла с описаниями символов. Кроме этого, удобно, когда такие файлы отличаются только суффиксом (для DOS/Windows — расширением). Т.е. если файл с описаниями символов имеет суффикс .cpp, то соответствующий ему заголовочный файл с определениями публикуемых символов имеет суффикс .h.

Переводя наш пример в традиции языков C/C++, для группы файлов add-up-to-limit.cpp, incrementator.cpp и main.cpp, следует определить два заголовочных файла — add-up-to-limit.h и incrementator.h. Для файла main.cpp заголовочный файл не нужен, так как в файле main.cpp определен только символ функции main(), который не нужно публиковать где-нибудь еще.

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

Файл add-up-to-limit.h

int addUpToLimit(int, int);

Файл incrementator.h

int incrementator(int);

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

Файл add-up-to-limit.cpp

#include "add-up-to-limit.h"

int addUpToLimit(int op1, int op2)
{
    int res = op1+op2;
    if (res>100) res = 100;
    return res;
}

Файл incrementator.cpp

#include "add-up-to-limit.h"
#include "incrementator.h"

int incrementator(int value)
{
    return addUpToLimit(value, 8);
}

Файл main.cpp

#include <iostream>

#include "add-up-to-limit.h"
#include "incrementator.h"

int main(int argc, char *argv[])
{
   int n = 0;

   n = addUpToLimit(n, 20);
   int counter = 0;
   for (int m = 0, delta = m-n;
        delta!=0;
        m = incrementator(n), delta = m-n, n = m, ++counter);

   std::cout << "Cycle counter = " << counter << std::endl;

   return 0;
}

Справедливым будут вопросы о том, зачем в файл add-up-to-limit.cpp включать заголовочный файл add-up-to-limit.h, а в файл incrementator.cpp включать incrementator.h. Действительно, в данном конкретном случае, это совершенно не обязательно, но, в общем случае, это следует делать потому, что в каждом файле .cpp может быть определено множество символов и какие-то из них даже могут использоваться, однако, если их использование находится до объявления, то компилятор не сможет обработать использование этого символа по причине своего примитивного устройства, проблемы которого мы обсудили выше. Поэтому, чтобы не следить за порядком определения символов, просто используют включение заголовочного файла в начало файла с определениями этих символов.

Компиляторы более современных языков, часто, устроены более изящно, но следует помнить, что язык C++ следует совместимости с языком C, а язык C является одним из старейших языков, появление которого датируется началом 70-х годов XX-го столетия. Из компилирующих языков уровня выше ассеблера, старее, чем язык C, наверное, только язык Fortran, схема компиляции которого не многим отличается, а организация областей данных, при этом, еще более примитивна.

По правилам языка C++, объявление одного и того же символа может быть сделано любое количество раз при условии, если они не противоречат друг другу. Таким образом, можно внутри одного компиляционного листа многократно объявить одну и ту же функцию и это ни сколько не приведет к каким-нибудь проблемам компиляции.

Действительно, можно проверить, что к проблемам это не приведет. Для этого достаточно повторить несколько раз объявление какой-нибудь из функций в каком-нибудь из наших заголовочных файлов и повторить компиляцию. Однако, нетрудно понять, что все такие повторения влияют на скорость компиляции. Чем больше будет повторений, тем ниже скорость компиляции. Для языков C/C++ скорость компиляции является большой проблемой, например, по сравнению с языком Pascal, поэтому следует обязательно использовать все те трюки, которые позволяют сократить это время компиляции.

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

Исправленный файл add-up-to-limit.h

#ifndef ADD_UP_TO_LIMIT_H
#define ADD_UP_TO_LIMIT_H
int addUpToLimit(int, int);
#endif

Исправленный файл incrementator.h

#ifndef INCREMENTATOR_H
#define INCREMENTATOR_H
int incrementator(int);
#endif

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

Рассмотрим подробнее, как это работает.

Пусть в каком-то файле описан ряд включений, при исполнении которых происходит попытка многократного включения какого-то заголовочного файла, который защищен только что приведенной схемой из директив сопроцессора. Тогда, в процессе препроцессорной обработки, при обработке первого включения проверяется, определен ли некий препроцессорный символ, указанный в предикате (условии) директивы препроцессора #ifndef … #endif. Если при формировании данного компиляционного листа указанный символ еще не был определен, то в компиляционный лист включается все содержимое директивы #ifndef … #endif и, кроме прочего, исполняется директива #define, которая приводит к определению указанного символа для текущего листа компиляции. Соответственно, в следующий раз, когда в предикате директивы #ifndef … #endif проверяется этот символ, условие оказывается не выполненым (так как символ уже определен) и содержимое директивы #ifndef … #endif не включается в текущий компиляционный лист.

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

  1. Не следует путать объявления (declarations) с описаниями (definitions) и не следует в заголовочный файл включать описания. Наиболее часто, новички создают себе проблему определяя перемнную в заголовочном файле. Это приводит к тому, что при включении такого заголовочного файла в разные компиляционные листы, в разных объектных файлах появляются переменные с одинаковым именем, но связанные с разными адресами из области данных. В языке C такая проблема решается через правильное понимание проблемы и использование ключевого слова extern. В языке C++ использование extern можно существенно минимизировать за счет использования ООП.
  2. Не следует допускать циклов во включении заголовочных файлов — файл A включает файл B, а файл B включает файл A. Такая проблема является ошибкой проектирования и решается только более удачным перепроектированием — другим разложением определений символов по файлам.

Реализация программных библиотек

Представим себе случай разработки каких-нибудь инструментальных библиотек, которые предоставляют для дальнейшего использования большое количество символов (например, функций). Можно представить себе библиотеки для работы с графикой, текстом, видео, звуком, и многое другое. В репозиториях *nix можно найти бесчиссленное множество таких библиотечных пакетов.

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

Следует задуматься вот над чем. Если вы пишете большую библиотеку для C++, то как передать результаты этой работы потребителям или заказчикам?

Для того, чтобы выполнить компиляцию кода использующего символы сторонней библиотеки, требуются определения (declarations) этих символов. Тут вопрос удачно решается предоставлением набора заголовочных файлов, где будут определены эти символы. Таким образом, разработчики сторонней библиотеки должны предоставить набор заголовочных файлов. В системах *nix такие заголовочные файлы из сторонних библиотек обычно устанавливаются соответствующими инсталляторами в директории типа /usr/include и /usr/local/include/.

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

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

Пользователи DOS/Windows привыкли к несколько искаженной трактовке термина "файловый архив". Изначально, под файловым архивом понимается простое объединение произвольного количество файлов в один с добавлением к этой байтовой последовательности некоторой служебной информации, которая указывает на размещение каждого конкретного файла в едином архиве. Для создания таких архивов еще со времен начала эпохи Unix служит известная всем юниксоидам утилита tar. Однако практика использования файловых архивов такова, что, обычно, эти архивы подвергают какому-нибудь алгоритму компрессии. Это и стало причиной массового заблуждения. Общение с программами, которые не выставляют на показ суть операций, называют себя архиваторами и выдают результатом скомпрессированные архивы, закрепило в сознании простых пользователей неправильную мысль о том, что операция архивирование означает сжатие. Интересно, что название программы tar, которая до сих пор является основным инструментом создания архивов и компрессированных архивов в *nix образовано от англ. tape archive — архив на магнитной ленте. Многим сегодня кажется это устаревшим, но магнитная лента до сих пор считается самым надежным носителем информации после бумаги.

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

Существуют два решения: статические и динамические архивы. Другое их название — библиотеки. Следует лишь заметить, что термин "динамический архив", наверное, не используеся никогда хотя, по сути, означал бы тоже самое, что и термин "динамическая библиотека".

Статические библиотеки (статические архивы)

Рассмотрим пример текущего урока в контексте создания и использования статической библиотеки. После того, как исходный код примера был разбит на файлы, сборка программы производилась по трем компиляционным листам из которых были сделаны три объектных файла: add-up-to-limit.o, incrementator.o и main.o. Чтобы смоделировать на данном материале создание и использование библиотеки, можно посчитать, исходный код нашей сторонней библиотеки составляют следующие файлы.

    Список исходных файлов "сторонней" библиотеки

  1. add-up-to-limit.cpp
  2. add-up-to-limit.h
  3. incrementator.cpp
  4. incrementator.h

При таком предположении, получается, что файл main.cpp является потребителем этой сторонней библиотеки и ему для компиляции и последующей линковки нужны следующие артефакты.

  • Компиляция: Требуются заголовочные файлы сторонней библиотеки, в которых объявлены символы, используемые в функции main().
  • Линковка: Требуется специальный файловый архив из объектных файлов сторонней библиотеки. В нашем конкретном случае, нам нужна будет статическая библиотека сделанная на основе этих объектных файлов.

Для чистоты эксперимента, давайте уберем в отдельную директорию все файлы из представленного выше списка исходных файлов нашей условно-сторонней библиотеки. Пусть это будет поддиректория mytools в директории проекта. При этом, в самой директории проекта останется только исходный файл нашей программы — main.cpp. Все результаты компиляции должны быть убраны.

Перейдем в директорию mytools где лежат файлы нашей библиотеки. Чтобы сделать архив из объектных файлов сделаем сначала соответствующие объектные файлы. Создание объектных файлов для будущей статической библиотеки ничем не отличается от создания объектных файлов которые прямиком передаются в линковку для приложения. Выполним следующие команды.

$ g++ -c add-up-to-limit.cpp
$ g++ -c incrementator.cpp
$ ar cr libmytools.a add-up-to-limit.o incrementator.o
$ rm *.o

С помощью первых двух команд создаются объектные файлы add-up-to-limit.cpp и incrementator.o. Третья команда создает из этих двух объектных файлов статический архив в файле с именем libmytools.a. Последняя команда rm (от англ. remove — удалить) удаляет файлы по указанной маске. Под эту маску попадают оба сделанных ранее объектных файла.

Рассмотрим подробнее команду ar, которая используется для создания статических архивов. Опции cr, в нашем случае, означают создать (create) архив с именем файла libmytools.a и вставить (insert) в него объектные файлы с именами add-up-to-limit.o и incrementator.o.

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

$ g++ -c main.cpp
main.cpp:3:29: фатальная ошибка: add-up-to-limit.h: Нет такого файла или каталога
компиляция прервана.

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

$ g++ -Imytools -c main.cpp

Осталось выполнить линковку созданного объектного файла с файлом созданного статического архива. Для указания пути на архив используется ключ -L, а для указания имени используемого архива — ключ -l. Обратите внимание, что в значении ключа -l следует использовать не имя файла архива, а имя архива.

Имя файла архива и имя архива традиционно различаются. В *nix существует следующая традиция. Имена файлов статических и динамических библиотек (архивы) начинаются с префикса lib за которым идет имя архива и специальный суффикс. Для статических архивов используется суффикс .a, а для динамических — .so. В операционной системе Windows вместо суффиксов используются расширения. Для статических архивов используется расширение lib, а для динамических библиотек — dll (Dynamic Link Library). Таким образом, для архива с именем mytools имя файла архива будет выглядить как libmytools.a.

Таким образом, для нашего случая, команда линковки объектного файла main.o исходной программы с файлом статической библиотеки libmytools.a выглядит следующим образом. Обратите внимания на каламбур с именами директории размещения библиотеки и имени архива.

g++ main.o  -Lmytools -lmytools

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

Далее мы рассмотрим динамические библиотеки, которые являются более сложным случаем использования кода сторонних библиотек.