Как скомпилировать си в линукс
Перейти к содержимому

Как скомпилировать си в линукс

  • автор:

Сборка проекта на С++ в GNU/Linux

Язык С++ является компилируемым, то есть трансляция кода с языка высокого уровня на инструкции машинного кода происходит не в момент выполнения, а заранее — в процессе изготовления так называемого исполняемого файла (в ОС Windows такие файлы имеют расширение .exe , а в ОС GNU/Linux чаще всего не имеют расширения).

hello.cpp

Пример простой программы на С++, которая печатает «Привет, Мир!»:

#include int main()  std::cout  <"Hello, World!"  <std::endl; return 0; > 

Для вывода здесь используется стандартная библиотека iostream , поток вывода std::cout .

Исполняемые операторы в программах на С++ не могут быть сами по себе — они должны быть обязательно заключены в функции.

Функция main() — это главная функция, выполнение программы начинается с её вызова и заканчивается выходом из неё. Возвращаемое значение main() в случае успешных вычислений должно быть равно 0, что значит «ошибка номер ноль», то есть «нет ошибки». В противном процесс, вызвавший программу, может посчитать её выполнившейся с ошибкой.

Чтобы выполнить программу, нужно её сохранить в текстовом файле hello.cpp и скомпилировать следующей командой:

$ g++ -o hello hello.cpp

Опция -o сообщает компилятору, что итоговый исполняемый файл должен называться hello . g++ — это компилятор языка C++, входящий в состав проекта GCC (GNU Compiler Collection). g++ не является единственным компиляторм языка C++. Помимо него в ходе курса мы будет использовать компилятор clang , поскольку он обладает рядом преимуществ, из которых нас больше всего интересует одно — этот компилятор выдаёт более понятные сообщения об ошибках по сравнению с g++ .

Упражнение №1

Скомпилируйте и выполните данную программу.

Ввод и вывод на языке С++

В Python и в С ввод и вывод синтаксически оформлены как вызов функции, а в С++ — это операция над объектом специального типа — потоком.

Потоки определяются в библиотеке iostream, где определены операции ввода и вывода для каждого встроенного типа.

Вывод

Все идентификаторы стандартной библиотеки определены в пространстве имен std , что означает необходимость обращения к ним через квалификатор std:: .

std::cout  "mipt"; std::cout  2018; std::cout  '.'; std::cout  true; std::cout  std::endl; 

Заметим, что в С++ мы не прописываем типы выводимых значений, компилятор неким (пока непонятным) способом разбирается в типе выводимого значения и выводит его соответствующим образом.

Вывод в один и тот же поток можно писать в одну строчку:

std::cout  "mipt"  2018  '.'  true  std::endl; 

Для вывода в поток ошибок определён поток std::cerr .

Ввод

Поток ввода с клавиатуры называется std::cin , а считывание из потока производится другой операцией — >> :

std::cin >> x; 

Тип считываемого значения определяется автоматически по типу переменной x .

Для всех типов, кроме char , считывание будет производиться с пропуском символов-разделителей и до следующего символа-разделителя. При этом пробел и табуляция так же, как и символ перевода каретки, являются корректными разделителями. Считывание в char происходит посимвольно независимо от типа символа.

Например для введенной строки «Иван Иванович Иванов»,

std::string name; std::cin >> name; 

считает в name только первое слово «Иван».

Считать всю строку целиком можно с помощью функции getline() :

std::string name; std::getline(std::cin, name); 

Считывать несколько значений можно и в одну строку:

std::cin >> x >> y >> z; 

Упражнение №2

Напишите программу, которая считает гипотенузу прямоугольного треугольника по двум катетам. Ввод и вывод стандартные.

Ввод Вывод
3 4 5

Сумма первых n натуральных чисел

Пример программы, которая подсчитывает сумму первых n натуральных чисел:

#include int main()  int n = 0; std::cin >> n; int sum = 0; for (int i = 1; i  n; i++)  sum += i; > std::cout  <sum  <std::endl; return 0; > 

Как известно, если сложную задачу разбить на несколько простых подзадач, то её решение сильно упрощается. Поэтому не стоит писать весь код в одной функции main() . Лучше разбивать код на отдельные функции, каждая из которых решает свою несложную подзадачу, но делает это хорошо. Например, в предыдущем примере можно вынести функциональность подсчёта суммы первых n натуральных чисел в отдельную функцию:

#include int GetNaturalsSum(const int n)  int sum = 0; for (int i = 1; i  n; i++)  sum += i; > return sum; > int main()  int n = 0; std::cin >> n; std::cout  <GetNaturalsSum(n)  <std::endl; return 0; > 

Эмперическое правило: каждая функция не должна превышать по размеру 1 экран вашего монитора.

Этапы сборки: препроцессинг, компиляция, компоновка

Компиляция исходных текстов на Си в исполняемый файл происходит в три этапа.

Препроцессинг

Эту операцию осуществляет текстовый препроцессор.

Исходный текст частично обрабатывается — производятся:

  1. Замена комментариев пустыми строками
  2. Текстовое включение файлов — #include
  3. Макроподстановки — #define
  4. Обработка директив условной компиляции — #if , #ifdef , #elif , #else , #endif

Компиляция

Процесс компиляции состоит из следующих этапов:

  1. Лексический анализ. Последовательность символов исходного файла преобразуется в последовательность лексем.
  2. Синтаксический анализ. Последовательность лексем преобразуется в дерево разбора.
  3. Семантический анализ. Дерево разбора обрабатывается с целью установления его семантики (смысла) — например, привязка идентификаторов к их декларациям, типам, проверка совместимости, определение типов выражений и т. д.
  4. Оптимизация. Выполняется удаление излишних конструкций и упрощение кода с сохранением его смысла.
  5. Генерация кода. Из промежуточного представления порождается объектный код.

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

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

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

Компоновка

Компоновка также называется связывание или линковка. На этом этапе отдельные объектные файлы проекта соединяются в единый исполняемый файл.

На этом этапе возможны так называемые ошибки связывания: если функция была объявлена, но не определена, ошибка обнаружится только на этом этапе.

Упражнение №3

Выполните в консоли для ранее созданного файла hello.cpp последовательно операции препроцессинга, компиляции и компоновки:

$ g++ -E -o hello1.cpp hello.cpp
  1. Компиляция:
$ g++ -c -o hello.o hello1.cpp
  1. Компоновка:
$ g++ -o hello hello.o

Принцип раздельной компиляции

Компиляция — алгоритмически сложный процесс, для больших программных проектов требующий существенного времени и вычислительных возможностей ЭВМ. Благодаря наличию в процессе сборки программы этапа компоновки (связывания) возникает возможность раздельной компиляции.

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

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

Пример модульной программы с раздельной компиляцией на С++

Рассмотрим пример: есть желание вынести часть кода в отдельный файл — пользовательскую библиотеку.

program.cpp

#include "mylib.hpp" const int MAX_DIVISORS_NUMBER = 10000; int main()  int number = read_number(); int Divisor[MAX_DIVISORS_NUMBER]; int Divisor_top = 0; factorize(number, Divisor, &Divisor_top); print_array(Divisor, Divisor_top); return 0; > 

Подключение пользовательской библиотеки в С++ на самом деле не так просто, как кажется.

Сама библиотека должна состоять из двух файлов: mylib.hpp и mylib.cpp :

mylib.hpp

#ifndef MY_LIBRARY_H_INCLUDED #define MY_LIBRARY_H_INCLUDED #include //считываем число int read_number(); //получаем простые делители числа // сохраняем их в массив, чей адрес нам передан void factorize(int number, int *Divisor, int *Divisor_top); //выводим число void print_number(int number); //распечатывает массив размера A_size в одной строке через TAB void print_array(int A[], size_t A_size); #endif // MY_LIBRARY_H_INCLUDED 

mylib.cpp

#include #include "mylib.hpp" //считываем число int read_number()  int number; std::cin >> number; return number; > //получаем простые делители числа // сохраняем их в массив, чей адрес нам передан void factorize(int x, int *Divisor, int *Divisor_top)  for (int d = 2; d  x; d++)  while (x%d == 0)  Divisor[(*Divisor_top)++] = d; x /= d; > > > //выводим число void print_number(int number)  std::cout  <number  <std::endl; > //распечатывает массив размера A_size в одной строке через TAB void print_array(int A[], size_t A_size)  for(int i = A_size-1; i >= 0; i--)  std::cout  <A[i]  <'\t'; > std::cout  <std::endl; > 

Препроцессор С++, встречая #include «mylib.hpp» , полностью копирует содержимое указанного файла (как текст) вместо вызова директивы. Благодаря этому на этапе компиляции не возникает ошибок типа Unknown identifier при использовании функций из библиотеки.

Файл mylib.cpp компилируется отдельно.

А на этапе компоновки полученный файл mylib.o должен быть включен в исполняемый файл program .

Cреда разработки обычно скрывает весь этот процесс от программиста, но для корректного анализа ошибок сборки важно представлять себе, как это делается.

Упражнение №4

Давайте сделаем это руками:

$ g++ -c mylib.cpp # 1 $ g++ -c program.cpp # 2 $ g++ -o program mylib.o program.o # 3 

Теперь, если изменения коснутся только mylib.cpp , то достаточно выполнить только команды 1 и 3. Если только program.cpp, то только команды 2 и 3. И только в случае, когда изменения коснутся интерфейса библиотеки, т.е. заголовочного файла mylib.hpp , придётся перекомпилировать оба объектных файла.

Утилита make и Makefile

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

Правила преобразования задаются в скрипте с именем Makefile , который должен находиться в корне рабочей директории проекта. Сам скрипт состоит из набора правил, которые в свою очередь описываются:

  1. целями (то, что данное правило делает);
  2. реквизитами (то, что необходимо для выполнения правила и получения целей);
  3. командами (выполняющими данные преобразования).

В общем виде синтаксис Makefile можно представить так:

# Отступ (indent) делают только при помощи символов табуляции, # каждой команде должен предшествовать отступ : .

То есть, правило make это ответы на три вопроса:

—> [Как делаем? (команды)] —>

Несложно заметить что процессы трансляции и компиляции очень красиво ложатся на эту схему:

Простейший Makefile

Для компиляции hello.cpp достаточно очень простого мэйкфайла:

hello: hello.cpp gcc -o hello hello.cpp

Данный Makefile состоит из одного правила, которое в свою очередь состоит из цели — hello , реквизита — hello.cpp , и команды — gcc -o hello hello.cpp .

Теперь, для компиляции достаточно дать команду make в рабочем каталоге. По умолчанию make станет выполнять самое первое правило, если цель выполнения не была явно указана при вызове:

Makefile для модульной программы

program: program.o mylib.o g++ -o program program.o mylib.o program.o: program.cpp mylib.hpp g++ -c program.cpp mylib.o: mylib.cpp mylib.hpp g++ -c hylib.cpp

Попробуйте собрать этот проект командой make или make hello . Теперь измените любой из файлов .cpp и соберите проект снова. Обратите внимание на то, что во время повторной компиляции будет транслироваться только измененный файл.

После запуска make попытается сразу получить цель program , но для ее создания необходимы файлы program.o и mylib.o , которых пока еще нет. Поэтому выполнение правила будет отложено и make станет искать правила, описывающие получение недостающих реквизитов. Как только все реквизиты будут получены, make`вернется к выполнению отложенной цели. Отсюда следует, что `make выполняет правила рекурсивно.

Фиктивные цели

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

$ make $ make install

Командой make производят компиляцию программы, командой make install — установку. Такой подход весьма удобен, поскольку все необходимое для сборки и развертывания приложения в целевой системе включено в один файл (забудем о скрипте configure ). Обратите внимание на то, что в первом случае мы не указываем цель, а во втором целью является вовсе не создание файла install , а процесс установки приложения в систему. Проделывать такие фокусы нам позволяют так называемые фиктивные (phony) цели. Вот краткий список стандартных целей:

all — является стандартной целью по умолчанию. При вызове make ее можно явно не указывать; clean — очистить каталог от всех файлов полученных в результате компиляции; install — произвести инсталляцию; uninstall — и деинсталляцию соответственно.

Для того чтобы make не искал файлы с такими именами, их следует определить в Makefile , при помощи директивы .PHONY . Далее показан пример Makefile с целями all , clean , install и uninstall :

.PHONY: all clean install uninstall all: program clean: rm -rf mylib *.o program.o: program.cpp mylib.hpp gcc -c -o program.o program.cpp mylib.o: mylib.cpp mylib.hpp gcc -c -o mylib.o mylib.cpp program: program.o mylib.o gcc -o mylib program.o mylib.o install: install ./program /usr/local/bin uninstall: rm -rf /usr/local/bin/program

Теперь мы можем собрать нашу программу, произвести ее инсталлцию/деинсталляцию, а так же очистить рабочий каталог, используя для этого стандартные make цели.

Обратите внимание на то, что в цели all не указаны команды; все что ей нужно — получить реквизит program . Зная о рекурсивной природе make, не сложно предположить, как будет работать этот скрипт. Также следует обратить особое внимание на то, что если файл program уже имеется (остался после предыдущей компиляции) и его реквизиты не были изменены, то команда make ничего не станет пересобирать. Это классические грабли make. Так, например, изменив заголовочный файл, случайно не включенный в список реквизитов (а надо включать!), можно получить долгие часы головной боли. Поэтому, чтобы гарантированно полностью пересобрать проект, нужно предварительно очистить рабочий каталог:

$ make clean $ make

P.S. Неплохая статья с описанием мейкфайлов.

Сайт построен с использованием Pelican. За основу оформления взята тема от Smashing Magazine. Исходные тексты программ, приведённые на этом сайте, распространяются под лицензией GPLv3, все остальные материалы сайта распространяются под лицензией CC-BY-SA.

LibreBay

Статьи про ОС Ubuntu. Языки программирования Си и C++.
Инструменты разработки и многое другое.

понедельник, 5 декабря 2016 г.

Как скомпилировать программу на C/C++ в Ubuntu

ubuntu terminal

Помню, когда я только начинал программировать, у меня возник вопрос: «Как скомпилировать программу на C в Ubuntu?» Для новичков это не легкая задача, как может показаться на первый взгляд.

Мой путь изучения C начался с бестселлера «Брайан Керниган, Деннис Ритчи, Язык программирования C, 2-е издание». Там рассказывается как скомпилировать программу в операционной системе Unix, но этот способ не работает в Linux. Авторы книги выкрутились, написав следующее:

В других системах это процедура будет отличаться. Обратитесь к справочнику или специалисту за подробностями.

Побуду специалистом 🙂 и расскажу в данной статье, как скомпилировать первые программы на C и заодно на C++ в терминале Ubuntu.

Текстовый редактор gedit

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

Запуск текстового редактора
Рис. 1. Запуск текстового редактора.

Первой программой по традиции является «Hello, World!», выводящее приветствие на экран:

#include int main(int argc, char **argv)

Печатаем или копируем текст программы в gedit и сохраняем в файл Hello.c , например, на рабочий стол. Не самое лучше место для сохранения, но это позволит рассмотреть случай, когда в имени директории содержится пробел.

Программа hello, World!
Рис. 2. Программа hello, World.

Компиляция программы на C

Теперь запускаем терминал, можно через Dash, а можно с помощью горячих клавиш + + . Здесь в начале установим инструменты сборки, куда входят необходимые компиляторы gcc для языка C и g++ для языка C++:

sudo apt install build-essential

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

Далее в терминале нам необходимо перейти в директорию, куда сохранили файл с текстом программы. Перемещение выполняется командой cd (англ. change directory — изменить каталог). Чтобы воспользоваться командой в начале пишется cd , затем через пробел путь , куда нужно перейти.

Для перехода на рабочий стол, команда будет следующей:

cd ~/Рабочий\ стол

Обратите внимание на символ обратной косой черты \ в имени директории Рабочий стол . Обратная косая экранирует пробел, и сообщает команде cd , что пробел и следующие за ним символы являются частью имени. Символ ~ в начале пути обозначает путь до домашней папки пользователя.

Для просмотра содержимого директории применяется команда ls (сокращение от англ. list).

Работа в терминале
Рис. 3. Работа в терминале.

Команда компиляции для программы на C выглядит следующим образом:

gcc -Wall -o hello hello.c
  • gcc — компилятор для языка программирования C;
  • -Wall — ключ вывода всех предупреждений компилятора;
  • -o hello — с помощью ключа -o указывается имя выходного файла;
  • hello.c — имя нашего исходного файла, который компилируем.

В завершение запустим hello , вводом имени программы с префиксом ./ :

./hello

Префикс ./ сообщает терминалу о необходимости выполнить программу с заданным именем в текущем каталоге. (Точка — это условное название текущего каталога.)

Работа в терминале, продолжение
Рис. 4. Работа в терминале, продолжение.

Компиляция программы на С++

Программы на C++ компилируются аналогично, как и программы на C. «Hello, World!» на C++ можно написать так:

#include int main(int argc, char **argv)

Сохраняем текст программы в файл под именем hello2.cpp . Таким образом, команда компилирования будет иметь вид:

g++ -Wall -o hello2 hello2.cpp

Для запуска результата вводим в терминале:

./hello2

Заключение

Данный способ позволяет скомпилировать программу лишь из одного файла с исходным кодом. Но этого вполне достаточно, чтобы начать изучение языков программирования C/C++ по книгам или по статьям в интернете.

  • Иванов Н. Н. — Программирование в Linux. Самоучитель. — 2-е издание;
  • Нейл Метьэ, Ричард Стоунс — Основы программирования в Linux: Пер. с англ. — 4-е издание;
  • Колисниченко Д. Н. — Разработка Linux-приложений.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *