GCC

Средствами, традиционно используемыми для создания программ для открытых операционных систем, являются инструменты разработчика GNU. Сделаем маленькую историческую справку. Проект GNU был основан в 1984 году Ричардом Столлманом. Его необходимость была вызвана тем, что в то время сотрудничество между программистами было затруднено, так как владельцы коммерческого программного обеспечения чинили многочисленные препятствия такому сотрудничеству. Целью проекта GNU было создание комплекта программного обеспечения под единой лицензией, которая не допускала бы возможности присваивания кем-то эксклюзивных прав на это ПО. Частью этого комплекта и является набор инструментов для разработчика, которым мы будем пользоваться и который должен входить во все дистрибутивы Linux.

Одним из этих инструментов является компилятор GCC. Первоначально эта аббревиатура расшифровывалась, как GNU C Compiler. Сейчас она означает – GNU Compiler Collection.

Создадим первую программу с помощью GCC. По сложившейся традиции первая программа будет просто выводить в консоли приветствие «Hello world!» – «Здравствуй Мир!».

Файлы с исходными кодами программ, которые мы будем создавать, это обычные текстовые файлы, и создавать их можно с помощью любого текстового редактора (например GEdit KWrite, Kate, а также более традиционные для пользователей Linux – vi и emacs). Помимо текстовых редакторов, существуют специализированные среды разработки со своими встроенными редакторами. Одним из таких средств является KDevelop. Интересно, что в нём есть встроенный редактор и встроенная консоль, расположенная прямо под редактором. Так что можно прямо в одной программе, не переключаясь между окнами, и редактировать код и давать консольные команды.

Создайте отдельный каталог hello. Это будет каталог нашего первого проекта. В нём создайте текстовый файл hello.c со следующим текстом:

#include <stdio.h>

int main(void)
{
    printf("Hello world!\n");
    return(0);
}

Затем в консоли зайдите в каталог проекта. Наберите команду gcc hello.c

Теперь посмотрите внимательно, что произошло. В каталоге появился новый файл a.out. Это и есть исполняемый файл. Запустим его. Наберите в консоли: ./a.out

Программа должна запуститься, то есть должен появиться текст:

Hello world!

Компилятор gcc по умолчанию присваивает всем созданным исполняемым файлам имя a.out. Если хотите назвать его по-другому, нужно к команде на компиляцию добавить флаг -o и имя, которым вы хотите его назвать. Давайте наберём такую команду: gcc hello.c -o hello

Мы видим, что в каталоге появился исполняемый файл с названием hello. Запустим его.

./hello

Как видите, получился точно такой же исполняемый файл, только с удобным для нас названием.

Флаг -o является лишь одним из многочисленных флагов компилятора gcc. Некоторые другие флаги мы рассмотрим позднее. Чтобы просмотреть все возможные флаги, можно воспользоваться справочной системой man. Наберите в командной строке: man gcc

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

Вы, конечно, обратили внимание, что, когда мы запускаем программу из нашего каталога разработки, мы перед названием файла набираем точку и слэш. Зачем же мы это делаем?

Дело в том, что, если мы наберём только название исполняемого файла, операционная система будет искать его в каталогах /usr/bin и /usr/local/bin, и, естественно, не найдёт. Каталоги /usr/bin и /usr/local/bin – системные каталоги размещения исполняемых программ. Первый из них предназначен для размещения стабильных версий программ, как правило, входящих в дистрибутив Linux. Второй – для программ, устанавливаемых самим пользователем (за стабильность которых никто не ручается). Такая система нужна, чтобы отделить их друг от друга. По умолчанию при сборке программы устанавливаются в каталог /usr/local/bin. Крайне нежелательно помещать что-либо лишнее в /usr/bin или удалять что-то оттуда вручную, потому что это может привести к краху системы. Там должны размещаться программы, за стабильность которых отвечают разработчики дистрибутива.

Чтобы запустить программу, находящуюся в другом месте, надо прописать полный путь к ней, например так: /home/dima/projects/hello/hello

Или другой вариант: прописать путь относительно текущего каталога, в котором вы в данной момент находитесь в консоли. При этом одна точка означает текущий каталог, две точки – родительский. Например, команда ./hello запускает программу hello, находящуюся в текущем каталоге, команда ../hello – программу hello, находящуюся в родительском каталоге, команда ./projects/hello/hello – программу во вложенных каталогах, находящихся внутри текущего.

Есть возможность добавлять в список системных путей к программам дополнительные каталоги. Для этого надо добавить новый путь в системную переменную PATH. Но давайте пока не будем отвлекаться от главной темы. Переменные окружения – это отдельный разговор.

Теперь рассмотрим, что же делает программа gcc. Её работа включает три этапа: обработка препроцессором, компиляция и компоновка (или линковка).

Препроцессор включает в основной файл содержимое всех заголовочных файлов, указанных в директивах #include. В заголовочных файлах обычно находятся объявления функций, используемых в программе, но не определённых в тексте программы. Их определения находятся где-то в другом месте: или в других файлах с исходным кодом или в бинарных библиотеках.

Вторая стадия – компиляция. Она заключается в превращении текста программы на языке C/C++ в набор машинных команд. Результат сохраняется в объектном файле. Разумеется, на машинах с разной архитектурой процессора двоичные файлы получаются в разных форматах, и на одной машине невозможно запустить бинарник, собранный на другой машине (разве только, если у них одинаковая архитектура процессора и одинаковые операционные системы). Вот почему программы для UNIX-подобных систем распространяются в виде исходных кодов: они должны быть доступны всем пользователям, независимо от того, у кого какой процессор и какая операционная система.

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

Теперь посмотрим на практике, как всё это выглядит. Напишем другую программу. Это будет примитивнейший калькулятор, способный складывать, вычитать, умножать и делить. При запуске он будет запрашивать по очереди два числа, над которыми следует произвести действие, а затем потребует ввести знак арифметического действия. Это могут быть четыре знака: «+», «–», «*», «/». После этого программа выводит результат и останавливается (возвращает нас в операционную систему, а точнее – в командный интерпретатор, из которого мы программу и вызывали).

Создадим для проекта новую папку kalkul, в ней создадим файл kalkul.c.

#include <stdio.h>

int main(void)
{
    float num1;
    float num2;
    char op;
    printf("Первое число: ");
    scanf("%f",&num1);
    printf("Второе число: ");
    scanf("%f",&num2);
    printf("Оператор ( + - * / ): ");
    while ((op = getchar()) != EOF)
    {
        if (op == '+')
        {
            printf("%6.2f\n",num1 + num2);
            break;
        }
        else if(op == '-')
        {
            printf("%6.2f\n",num1 - num2);
            break;
        }
        else if(op == '*')
        {
            printf("%6.2f\n",num1 * num2);
            break;
        }
        else if(op == '/')
        {
            if(num2 == 0)
            {
                printf("Ошибка: деление на ноль!\n");
                break;
            }
            else
            {
                printf("%6.2f\n",num1 / num2);
                break;
            }
        }
    }
    return 0;
}

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

В нашем случае мы включали один заголовочный файл – stdio.h – коллекцию стандартных функций ввода-вывода. Эти функции и выводили на консоль нужный текст, а также считывали с консоли вводимые нами слова.

Введите следующую команду:

gcc -E kalkul.c -o kalkul.cpp

Полученному файлу мы дали имя kalkul.cpp. Откройте его. Обратите внимание на то, что он весьма длинный. Это потому что в него вошёл весь код заголовочного файла stdio.h. Кроме того, препроцессор сюда добавил некоторые теги, указывающие компилятору способ связи с объявленными функциями. Основной текст нашей программы виден только в самом низу.

Можете заодно посмотреть, какие ещё функции объявлены в заголовочном файле stdio.h. Если вам захочется получить информацию о какой-нибудь функции, можно поинтересоваться о ней во встроенном руководстве man. Например, если вам вдруг захочется узнать, что же делает таинственная функция fopen, можно набрать:

man fopen

Много информации также есть в справочной системе info.

info fopen

Можно поинтересоваться и всем заголовочным файлом сразу.

man stdio.h
info stdio.h

Посмотрим теперь следующий этап. Создадим объектный файл. Объектный файл представляет собой «дословный» перевод нашего программного кода на машинный язык, пока без связи вызываемых функций с их определениями. Для формирования объектного файла служит опция -c.

gcc -c kalkul.c

Название получаемого файла можно не указывать, так как компилятор просто берёт название исходного и меняет расширение .c на .o (указать можно, если нам захочется назвать его по-другому).

Если мы создаём объектный файл из исходника, уже обработанного препроцессором (например, такого, какой мы получили выше), то мы должны обязательно указать явно, что компилируемый файл является файлом исходного кода, обработанный препроцессором, и имеющий теги препроцессора. В противном случае он будет обрабатываться, как обычный файл C++, без учёта тегов препроцессора, а значит связь с объявленными функциями не будет устанавливаться. Для явного указания на язык и формат обрабатываемого файла служит опция -x. Файл C++, обработанный препроцессором обозначается cpp-output.

gcc -x cpp-output -c kalkul.cpp

Наконец, последний этап – компоновка. Получаем из объектного файла исполняемый.

gcc kalkul.o -o kalkul

Можно его запускать.

./kalkul

Вы спросите: «Зачем вся эта возня с промежуточными этапами? Не лучше ли просто один раз скомандовать gcc kalkul.c -o kalkul?»

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