Одним из инструментов создания программ является компилятор GCC. Первоначально эта аббревиатура расшифровывалась,
как GNU C Compiler. Сейчас она означает – GNU Compiler Collection.
Рассмотрим пример создания программы «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.
Точку и слэш перед названием исполняемого файла означает путь к файлу, т.е. файл находится в текущем каталоге.
Чтобы запустить программу, находящуюся в другом месте, надо прописать полный путь к ней, например так:
/home/user/projects/hello/hello
Или другой вариант: прописать путь относительно текущего каталога, в котором вы в данной момент находитесь в консоли.
При этом одна точка означает текущий каталог, две точки – родительский. Например, команда ./hello запускает программу hello,
находящуюся в текущем каталоге, команда ../hello – программу hello, находящуюся в родительском каталоге.
Если наберать только название исполняемого файла, операционная система будет
искать его в каталогах /usr/bin и /usr/local/bin, и, естественно, не найдёт.
Каталоги /usr/bin и /usr/local/bin – системные каталоги размещения исполняемых программ.
Первый из них предназначен для размещения стабильных версий программ, как правило, входящих в дистрибутив Linux.
Второй – для программ, устанавливаемых самим пользователем (за стабильность которых никто не ручается).
Такая разбиение нужно,чтобы отделить их друг от друга. По умолчанию при сборке программы устанавливаются в каталог /usr/local/bin.
Крайне нежелательно помещать что-либо лишнее в /usr/bin или удалять что-то оттуда вручную, потому что это может привести к краху системы.
Там должны размещаться программы, за стабильность которых отвечают разработчики дистрибутива.
Также есть возможность добавлять в список системных путей пути к своим программам.
Для этого надо добавить новый путь в системную переменную окружения PATH.
Рассмотрим, что же делает программа gcc
Работа gcc включает три этапа: обработка препроцессором, компиляция и компоновка (или линковка).
Препроцессор включает в основной файл содержимое всех заголовочных файлов, указанных в директивах #include.
В заголовочных файлах обычно находятся объявления функций, используемых в программе, но не определённых в тексте программы.
Их определения находятся где-то в другом месте: или в других файлах с исходным кодом или в бинарных библиотеках.
Для того, чтобы посмотреть, что на этом этапе делается, воспользуемся опцией -E. Эта опция останавливает выполнение программы на этапе
обработки препроцессором. В результате получается файл исходного кода с включённым в него содержимым заголовочных файлов.
В примере hello.c подключается один заголовочный файл – stdio.h – коллекция стандартных функций ввода-вывода.
Введите следующую команду:
gcc -E hello.c -o hello.cpp
Полученному файлу мы дали имя hello.cpp. Открыв его, вы увидите, что он весьма длинный. Это потому что в него вошёл весь
код заголовочного файла stdio.h. Кроме того, препроцессор сюда добавил некоторые теги, указывающие компилятору способ связи
с объявленными функциями. Основной текст нашей программы виден только в самом низу.
Можете заодно посмотреть, какие ещё функции объявлены в заголовочном файле stdio.h. Если вам захочется получить информацию о какой-нибудь
функции, можно поинтересоваться о ней во встроенном руководстве man.
Например, если вам вдруг захочется узнать, что же делает функция fopen, можно набрать:
man fopen
или
info fopen
Вторая стадия – компиляция. Она заключается в превращении текста программы на языке C/C++ в набор машинных команд.
Результат сохраняется в объектном файле. Разумеется, на машинах с разной архитектурой процессора двоичные файлы получаются в разных
форматах, и на одной машине невозможно запустить бинарник, собранный на другой машине (разве только, если у них одинаковая архитектура
процессора и одинаковые операционные системы). Вот почему программы для UNIX-подобных систем распространяются в виде исходных кодов:
они должны быть доступны всем пользователям, независимо от того, у кого какой процессор и какая операционная система.
Объектный файл представляет собой «дословный» перевод нашего программного кода на машинный язык,
пока без связи вызываемых функций с их определениями. Для формирования объектного файла служит опция -c.
gcc -c hello.c
Название получаемого объектного файла можно не указывать, так как компилятор просто берёт название исходного и меняет расширение .c на .o
(указать можно, если нам захочется назвать его по-другому).
Если мы создаём объектный файл из исходника, уже обработанного препроцессором (например, такого, какой мы получили выше),
то мы должны обязательно указать явно, что компилируемый файл является файлом исходного кода, обработанный препроцессором,
и имеющий теги препроцессора. В противном случае он будет обрабатываться, как обычный файл C, без учёта тегов препроцессора,
а значит связь с объявленными функциями не будет устанавливаться.
Для явного указания на язык и формат обрабатываемого файла служит опция -x. Файл C, обработанный препроцессором обозначается cpp-output.
gcc -x cpp-output -c hello.cpp
Последняя стадия – компоновка. Она заключается в связывании всех объектных файлов проекта в один, связывании вызовов функций
с их определениями, и присоединением библиотечных файлов, содержащих функции, которые вызываются, но не определены в проекте.
В результате формируется исполняемый файл – наша конечная цель. Если какая-то функция в программе используется,
но компоновщик не найдёт место, где эта функция определена, он выдаст сообщение об ошибке, и откажется создавать исполняемый файл.
Для получения из объектного файла исполняемого используется опция -o:
gcc hello.o -o helo
Полученный исполняемый файл можно запускать:
./hello
Вы спросите: «Зачем вся эта возня с промежуточными этапами?
Не лучше ли просто один раз скомандовать
gcc kalkul.c -o kalkul?
Дело в том, что настоящие программы очень редко состоят из одного файла.
Как правило исходных файлов несколько, и они объединены в проект. И в некоторых исключительных случаях программу приходится
компоновать из нескольких частей, написанных на разных языка. В этом случае приходится запускать компиляторы разных языков,
чтобы каждый получил объектный файл из своего исходника, а затем уже эти полученные объектные файлы компоновать в исполняемую программу.
Пример проекта из нескольких файлов
Рассмотрим программу, состоящую из двух исходных файлов и одного заголовочного.
Для этого возьмём в качестве примера примитивнй калькулятор, способный складывать, вычитать, умножать и делить.
При запуске он будет запрашивать два числа, над которыми следует произвести действие, и
знак арифметического действия. Это могут быть действия: «+», «–», «*», «/», pow, sqrt, sin, cos, tan.
После этого программа выводит результат и останавливается (возвращает нас в операционную систему, а точнее – в командный интерпретатор,
из которого мы программу и вызывали).
При этом после введения первого числа надо сразу вводить действие. Если действие оперирует только с одним числом
(как в случае синуса, косинуса, тангенса, квадратного корня), результат сразу будет выведен.
Если понадобится второе число, оно будет специально запрашиваться.
Создадим каталог проекта kalkul2. В нём создадим три файла: calculate.h, calculate.c, main.c.
Файл calculate.h:
///////////////////////////////////////
// calculate.h
#ifndef CALCULATE_H_
#define CALCULATE_H_
float Calculate(float Numeral, char Operation[4]);
#endif /*CALCULATE_H_*/
Файл calculate.c:
////////////////////////////////////
// calculate.c
#include <stdio.h>
#include <math.h>
#include <string.h>
#include "calculate.h"
float Calculate(float Numeral, char Operation[4])
{
float SecondNumeral;
if(strncmp(Operation, "+", 1) == 0)
{
printf("Второе слагаемое: ");
scanf("%f",&SecondNumeral);
return(Numeral + SecondNumeral);
}
else if(strncmp(Operation, "-", 1) == 0)
{
printf("Вычитаемое: ");
scanf("%f",&SecondNumeral);
return(Numeral - SecondNumeral);
}
else if(strncmp(Operation, "*", 1) == 0)
{
printf("Множитель: ");
scanf("%f",&SecondNumeral);
return(Numeral * SecondNumeral);
}
else if(strncmp(Operation, "/", 1) == 0)
{
printf("Делитель: ");
scanf("%f",&SecondNumeral);
if(SecondNumeral == 0)
{
printf("Ошибка: деление на ноль! ");
return(HUGE_VAL);
}
else
return(Numeral / SecondNumeral);
}
else if(strncmp(Operation, "pow", 3) == 0)
{
printf("Степень: ");
scanf("%f",&SecondNumeral);
return(pow(Numeral, SecondNumeral));
}
else if(strncmp(Operation, "sqrt", 4) == 0)
return(sqrt(Numeral));
else if(strncmp(Operation, "sin", 3) == 0)
return(sin(Numeral));
else if(strncmp(Operation, "cos", 3) == 0)
return(cos(Numeral));
else if(strncmp(Operation, "tan", 3) == 0)
return(tan(Numeral));
else
{
printf("Неправильно введено действие ");
return(HUGE_VAL);
}
}
Файл main.c:
////////////////////////////////////////
// main.c
#include <stdio.h>
#include "calculate.h"
int main(void)
{
float Numeral;
char Operation[4];
float Result;
printf("Число: ");
scanf("%f",&Numeral);
printf("Арифметическое действие (+,-,*,/,pow,sqrt,sin,cos,tan): ");
scanf("%s",&Operation);
Result = Calculate(Numeral, Operation);
printf("%6.2f\n",Result);
return 0;
}
У нас есть два файла исходного кода (c-файлы) и один заголовочный (h-файл). Заголовочный включается в оба c-файла.
Скомпилируем calculate.c.
gcc -c calculate.c
Получили calculate.o. Затем main.c.
gcc -c main.c
И вот он main.o перед нами! Теперь, как вам уже, наверное, подсказывает интуиция, надо из этих двух объектных файлов сделать исполняемый.
gcc calculate.o main.o -o kalkul
Упс... и не получилось... Вместо столь желаемого запускаемого файла, в консоли появилась какая-то ругань:
calculate.o(.text+0x1b5): In function `Calculate':
calculate.c: undefined reference to `pow'
calculate.o(.text+0x21e):calculate.c: undefined reference to `sqrt'
calculate.o(.text+0x274):calculate.c: undefined reference to `sin'
calculate.o(.text+0x2c4):calculate.c: undefined reference to `cos'
calculate.o(.text+0x311):calculate.c: undefined reference to `tan'
collect2: ld returned 1 exit status
Давайте разберёмся, за что нас так отругали. Undefined reference означает ссылку на функцию, которая не определена.
В данном случае gcc не нашёл определения функций pow, sqrt, sin, cos, tan. Где же их найти?
Как уже говорилось раньше, определения функций могут находиться в библиотеках.
Это скомпилированные двоичные файлы, содержащие коллекции однотипных операций, которые часто вызываются из многих программ,
а потому нет смысла многократно писать их код в программах. Стандартное расположение файлов библиотек – каталоги /usr/lib и
/usr/local/lib (при желании можно добавить путь). Если библиотечный файл имеет расширение .a, то это статическая библиотека,
то есть при компоновке весь её двоичный код включается в исполняемый файл. Если расширение .so, то это динамическая библиотека.
Это значит в исполняемый файл программы помещается только ссылка на библиотечный файл, а уже из него и запускается функция.
Когда мы писали программу hello, мы использовали функцию printf для вывода текстовой строки. Однако, как вы помните, мы нигде не
писали определения этой функции. Откуда же она тогда вызывается?
Просто при компоновке любой программы компилятор gcc по умолчанию включает в запускаемый файл библиотеку libc.
Это стандартная библиотека языка C. Она содержит рутинные функции, необходимые абсолютно во всех программах,
написанных на C, в том числе и функцию printf. Поскольку библиотека libc нужна во всех программах, она включается по умолчанию,
без необходимости давать отдельное указание на её включение.
Остальные библиотеки надо требовать включать явно. Ведь нельзя же во все программы помещать абсолютно все библиотеки.
Тогда исполняемый файл раздуется до немыслимо крупных размеров. Одним программам нужны одни функции, другим – другие.
Зачем же засорять их ненужным кодом! Пусть остаётся только то, что реально необходимо.
Нам в данном случае нужна библиотека libm. Именно она содержит все основные математические функции.
Она требует включения в текст программы заголовочного файла math.h.
Помимо этого дистрибутивы Linux содержат и другие библиотеки, например:
libGL Вывод трёхмерной графики в стандарте OpenGL. Требуется заголовочный файл <GL/gl.h>.
libcrypt Криптографические функции. Требуется заголовочный файл <crypt.h>.
libcurses Псевдографика в символьном режиме. Требуется заголовочный файл <curses.h>.
libform Создание экранных форм в текстовом режиме. Требуется заголовочный файл <form.h>.
libgthread Поддержка многопоточного режима. Требуется заголовочный файл <glib.h>.
libgtk Графическая библиотека в режиме X Window. Требуется заголовочный файл <gtk/gtk.h>.
libhistory Работы с журналами. Требуется заголовочный файл <readline/readline.h>.
libjpeg Работа с изображениям в формате JPEG. Требуется заголовочный файл <jpeglib.h>.
libncurses Работа с псевдографикой в символьном режиме. Требуется заголовочный файл <ncurses.h>.
libpng Работа с графикой в формате PNG. Требуется заголовочный файл <png.h>.
libpthread Многопоточная библиотека POSIX. Стандартная многопоточная библиотека для Linux.
Требуется заголовочный файл <pthread.h>.
libreadline Работа с командной строкой. Требуется заголовочный файл <readline/readline.h>.
libtiff Работа с графикой в формате TIFF. Требуется заголовочный файл <tiffio.h>.
libvga Низкоуровневая работа с VGA и SVGA. Требуется заголовочный файл <vga.h>.
А также многие-многие другие.
Обратите внимание, что названия всех этих библиотек начинаются с буквосочетания lib-.
Для их явного включения в исполняемый файл, нужно добавить к команде gcc опцию -l, к которой
слитно прибавить название библиотеки без lib-.
Например, чтобы включить библиотеку libvga надо указать опцию -lvga.
Нам нужны математические функции pow, sqrt, sin, cos, tan. Они, как уже было сказано, находятся
в математической библиотеке libm.
Следовательно, чтобы подключить эту библиотеку, мы должны указать опцию -lm.
gcc calculate.o main.o -o kalkul -lm
Ура! Исполняемый файл создан! Запустим его:
./kalkul