Назад

Введение

Содержание


Компилятор языка С

Операционная система (ОС) Linux/Unix и язык C - "близненцы-братья". Вспомните, что язык программирования C был создан (Д.Ритчи, 1972 г) специально для написания ОС Unix, и с тех пор и "каноническая" ОС Unix, а также все ее клоны и подобные ей ОС пишутся на языке C. Поэтому во всех версиях Unix и Unix-подобных систем компилятор языка C в большинстве случаев входит в комплект поставки системы.

Одним из первых программных продуктов, созданных в рамках проекта GNU, также явился компилятор языка С с открытым кодом. Этот компилятор включается в поставку всех версий ОС Linux.

Таким образом, среда, в которой выполняется наш лабораторный практикум предоставляет в Ваше распоряжение 4 компилятора на выбор:

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

Компилятор языка C выполняет как собственно компиляцию - перевод исходного текста на машинный язык, результатом чего является объектный модуль, так и редактирование связей - сборку из нескольких объектных модулей (в том числе, и библиотечных) исполняемого модуля.

Файлы с исходными текстами C-программ должны иметь расширение .c, например: hello.c. Результатом компиляции является файл, содержащий объектный модуль, его имя совпадает с именем исходного модуля, а расширение - .o, например: hello.o. Для файла, содержащего исполняемый модуль стандартного расширения не существует. При компиляции программы, состоящей из единственного исходного модуля, объектный модуль автоматически удаляется после создания компилятором исполняемого модуля.

Общий формат команды вызова компилятора имеет следующий вид:

       gcc [опции] [выходной_файл] файл1 [файл2 :]

Наиболее часто употребляемые опции компилятора следующие:

-c Подавляет фазу редактирования связей, создает объектный модуль для каждого исходного модуля из перечисленных в параметрах вызова. Выходной_файл с этой опцией не задается. Опция может применяться вместе с опцией -I
  
-o Компиляция и редактирование связей. Cоздает объектный модуль для каждого исходного модуля из перечисленных в параметрах вызова и имеющих расширение .c. Файлы с расширением .c рассматриваются как исходные модули и компилируются; файлы, имеющие расширение .o, рассматриваются как объектные модули и подключаются при редактировании связей. Параметр выходной_файл задает имя файла исполняемого модуля. Опция может применяться вместе с опциями -L, -l, -I.
  
-L каталог Добавить каталог в список каталогов, которые содержат объектные библиотечные модули.
  
-l библиотека При редактировании связей подключить модули из библиотеки.
  
-I каталог Искать включаемые (#include) файлы, имена которых не начинаются с / сначала в каталоге, а лишь затем - в стандартных каталогах для включаемых файлов.
  
-E Выполнить обработку указанных исходных модулей только препроцессором, результат направляется в стандартный вывод. Выходной_файл с этой опцией не задается. Опция может применяться вместе с опцией -I.
  
-w Подавить выдачу предупреждающих сообщений.

Примеры использования компилятора:

gcc hello.c Компиляция исходного модуля hello.c с выдачей сообщений об ошибках на стандартный вывод. Файл объектного модуля не создается.
  
gcc -c hello.c Компиляция исходного модуля hello.c с выдачей сообщений об ошибках на стандартный вывод. При успешной компиляции объектный модуль записывается в файл hello.o.
  
gcc -o hello hello.o Редактирование связей для объектного модуля hello.o, исполняемый модуль записывается в файл hello.
  
gcc -o hello hello.o hello1.c Создание исполняемого модуля в файле hello из объектного модуля hello.o и модуля hello1.c (последний модуль является исходным, он предварительно компилируется.
  
gcc -o hello hello.o hello1.o -l hellolib Создание исполняемого модуля в файле hello из объектных модулей hello.o и hello1.o c с подключением объектных модулей из библиотеки hellolib.
  
gcc -o hello hello1.с -lm Создание исполняемого модуля в файле hello из исходного модуля hello.с с выдачей сообщений об ошибках на стандартный вывод с подключением бибилиотеки math.h. Файл объектного модуля не создается.


Подробное знакомство с компилятором GCC

Одним из инструментов создания программ является компилятор 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


Make-файлы

У вас, вероятно, появился вопрос: можно ли не компилировать файлы проекта kalkul2 по отдельности, а собрать сразу всю программу одной командой? Можно.

gcc calculate.c main.c -o kalkul -lm
Вы скажете, что это удобно? Удобно для нашей программы, потому что она состоит всего из двух c-файлов.

Однако профессиональная программа может состоять из нескольких десятков таких файлов. Каждый раз набирать названия их всех в одной строке было бы делом чрезмерно утомительным. Но есть возможность решить эту проблему. Названия всех исходных файлов и все команды для сборки программы можно поместить в отдельный текстовый файл. А потом считывать их оттуда одной короткой командой.

Давайте создадим такой текстовый файл и воспользуемся им. В каталоге проекта kalkul2 удалите все файлы, кроме исходных calculate.h, calculate.c, main.c. Затем создайте в этом же каталоге новый файл, назовите его makefile (без расширений). Поместите туда следующий текст.

kalkul: calculate.o main.o
	gcc calculate.o main.o -o kalkul -lm
calculate.o: calculate.c calculate.h
	gcc -c calculate.c
main.o: main.c calculate.h
	gcc -c main.c
clean:
	rm -f kalkul calculate.o main.o
install:
	cp kalkul /usr/local/bin/kalkul
uninstall: 
	rm -f /usr/local/bin/kalkul
Обратите внимание на строки, введённые с отступом от левого края. Этот отступ получен с помощью клавиши Tab. Только так его и надо делать! Если будете использовать клавишу «Пробел», команды не будут исполняться.

Затем дадим команду, состоящую всего из одного слова:

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

Makefile является списком правил. Каждое правило начинается с указателя, называемого «Цель». После него стоит двоеточие, а далее через пробел указываются зависимости. В нашем случае ясно, что конечный файл kalkul зависит от объектных файлов calculate.o и main.o. Поэтому они должны быть собраны прежде сборки kalkul. После зависимостей пишутся команды. Каждая команда должна находиться на отдельной строке, и отделяться от начала строки клавишей Tab. Структура правила Makefile может быть очень сложной. Там могут присутствовать переменные, конструкции ветвления, цикла. Этот вопрос требует отдельного подробного изучения.

Если мы посмотрим на три первых правила, то они нам хорошо понятны. Там те же самые команды, которыми мы уже пользовались. А что же означают правила clean, install и uninstall?

В правиле clean стоит команда rm, удаляющая исполняемый и объектные файлы. Флаг -f означает, что, если удаляемый файл отсутствует, программа должна это проигнорировать, не выдавая никаких сообщений. Итак, правило clean предназначено для «очистки» проекта, приведения его к такому состоянию, в каком он был до команды make.

Запустите

make
Появились объектные файлы и исполняемый. Теперь
make clean
Объектные и исполняемый файлы исчезли. Остались только c-файлы, h-файл и сам Makefile. То есть, проект «очистился» от результатов команды make.

Правило install помещает исполняемый файл в каталог /usr/local/bin – стандартный каталог размещения пользовательских программ. Это значит, что её можно будет вызывать из любого места простым набором её имени. Но помещать что-либо в этот каталог можно только, зайдя в систему под «суперпользователем». Для этого надо дать команду su и набрать пароль «суперпользователя». В противном случае система укажет, что вам отказано в доступе. Выход из «суперпользователя» осуществляется командой exit.

Итак,

make
su make install exit
Теперь вы можете запустить это программу просто, введя имя программы, без прописывания пути.
kalkul
Можете открыть каталог /usr/local/bin. Там должен появиться файл с названием kalkul.

Давайте теперь «уберём за собой», не будем засорять систему.

su
make uninstall
exit
Посмотрите каталог /usr/local/bin. Файл kalkul исчез. Итак, правило uninstall удаляет программу из системного каталога.


Компилятор G++

Переделаем нашу программу, но на этот раз уже напишем её на C++. Создайте новый каталог проекта kalkulcpp, а в нём три файла: problem.h, problem.cpp, main.cpp.

problem.h
//------------------------------
#ifndef PROBLEM_H_
#define PROBLEM_H_
#include <string>
using namespace std;
class CProblem
{
private:
float Numeral;
float SecondNumeral;
string Operation;
float Result;
string Error;
bool Calculate();

public:
void SetValues();
void Solve();
};
#endif /*PROBLEM_H_*/

problem.cpp
//-------------------------------------------
#include <iostream>
#include <cmath>
#include "problem.h"

using namespace std;

void CProblem::SetValues()
{
  cout << "Число: ";
  cin >> Numeral;
  cout <<"Арифметическое действие (+,-,*,/,pow,sqrt,sin,cos,tan): ";
  cin >> Operation;
}

bool CProblem::Calculate()
{
  if(Operation == "+")
   {
      cout << "Второе слагаемое: ";
      cin >> SecondNumeral;
      Result = Numeral + SecondNumeral;
      return true;
    }
  else if(Operation == "-")
      {
	cout << "Второе слагаемое: ";
	cin >> SecondNumeral;
	Result = Numeral - SecondNumeral;
	return true;
      }
      else if(Operation == "*")
	  {
	    cout << "Множитель: ";
	    cin >> SecondNumeral;
	    Result = Numeral * SecondNumeral;
	    return true;
	  }
	  else if(Operation == "/")
	      {
		cout << "Делитель: ";
		 cin >> SecondNumeral;
		if(SecondNumeral == 0)
		  {
		    Error = "Ошибка: деление на ноль.";
		    return false;
		  }
		else
		{
		  Result = Numeral/SecondNumeral;
		  return true;
		}
	      }
	      else if(Operation == "pow")
		  {
		    cout << "Степень: ";
		    cin >> SecondNumeral;
		    Result = pow(Numeral,SecondNumeral);
		    return true;
		  }
		  else if(Operation == "sqrt")
			{
			  Result = sqrt(Numeral);
			  return true;
			}
			else if(Operation == "sin")
			    {
			      Result = sin(Numeral);
			      return true;
			    }
			    else if(Operation == "cos")
				 {
				    Result = cos(Numeral);
				    return true;
				  }
				else if(Operation == "tan")
				     {
					Result = tan(Numeral);
					 return true;
				     }
				    else
				    {
					Error = "Ошибка ввода действия.";
					return false;
				    }
}

void CProblem::Solve()
{
    if(Calculate() == true)
      cout << Result << "\n";
    else
      cout << Error << "\n";
}


main.cpp
//------------------------------------------
#include <iostream>
#include "problem.h"

using namespace std;

int main(void)
{
  CProblem *Problem;
  Problem = new CProblem;
  Problem->SetValues();
  Problem->Solve();
  delete Problem;
  return(0);
}

Для сборки программ на C++ в наборе GNU имеется компилятор – G++. Он отличается от GCC тем, что по умолчанию подключает не стандартную библиотеку C, а стандартную библиотеку C++. Все флаги и опции у G++ такие же точно, как и у GCC.

Воспользуемся компилятором G++.

g++ problem.cpp main.cpp -o kalkul
Обратите внимание, что никаких дополнительных библиотек мы не подключали. Это означает, что математические функции входят в стандартную библиотеку C++. Вообще, в тех случаях, когда программа пишется на C++, рекомендуется использовать именно G++.

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

Создадим Makefile:

kalkul: problem.o main.o
	g++ problem.o main.o -o kalkul
problem.o: problem.cpp problem.h
	g++ -c problem.cpp
main.o: main.cpp problem.h
	g++ -c main.cpp
clean:
	rm -f kalkul problem.o main.o
install:
	cp kalkul /usr/local/bin/kalkul
uninstall:
	rm -f /usr/local/bin/kalkul
И соберём эту же программу снова, но уже «правильным» способом:
make
Инсталлируем её, предварительно зайдя в систему, как суперползователь:
su

make install

exit
Деинсталлируем:
su

make uninstall

exit
И очистим дистрибутив:
make clean


Знакомство с отладчиком gdb

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

Мы рассмотрим отладчик GDB, входящий в комплект программ GNU.

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

Такая компиляция достигается путём добавления флага -g к команде на компиляцию. Н апример, если бы мы собирали программу kalkul без применения Makefile, мы бы дали такую команду:

g++ main.cpp problem.cpp -o kalkul -g
Если же мы пользуемся командой make, то надо поставить опцию CFLAGS=-g. Тогда все команды на компиляцию, содержащиеся в Make-файле, автоматически получат флаг -g.

Или измените файл Makefile, добавивь опцию -g (для программы С++):

kalkul: problem.o main.o
        g++ -g problem.o main.o -o kalkul
problem.o: problem.cpp problem.h
        g++ -g -c problem.cpp
main.o: main.cpp problem.h
        g++ -g -c main.cpp
clean:
        rm -f kalkul problem.o main.o
install:
        cp kalkul /usr/local/bin/kalkul
uninstall:
        rm -f /usr/local/bin/kalkul
Или для программы, написанной на C:
kalkul: calculate.o main.o
        gcc -g calculate.o main.o -o kalkul -lm
calculate.o: calculate.c calculate.h
        gcc -g -c calculate.c
main.o: main.c calculate.h
        gcc -g -c main.c
clean:
        rm -f kalkul calculate.o main.o
install:
        cp kalkul /usr/local/bin/kalkul
uninstall: 
        rm -f /usr/local/bin/kalkul
Давайте возьмём программу, которую мы создали из файлов main.cpp, problem.cpp и problem.h (мы тогда называли этот каталог проекта kalkulcpp). У нас Makefile уже сформирован. Воспользуемся им.

Очистим пакет от результатов предыдущей сборки:

make clean
Соберём программу снова, но уже с включением отладочной информации:
make CFLAGS=-g
Или соберем программу с измененным Makefile (рекомендуется этот вариант):
make
Запустим отладчик GDB, загрузив в него нашу программу для отладки:
gdb ./kalkul
Чтобы запустить программу внутри отладчика,даётся команда run:
run
Чтобы посмотреть исходный код, даётся команда list:
list
Если дать эту команду без параметров, то она первые девять строк исходного кода главного файла (то есть такого, в котором имеется функция main). Чтобы просматривать файл дальше, надо снова набирать list. Чтобы посмотреть конкретные строки, надо указать два параметра: с какой строки начинать просмотр, и с какой строки заканчивать.
list 12,15
Чтобы просмотреть другие файлы проекта, надо перед номерами строк указать название нужного файла и отделить его от номеров строк двоеточием.
list problem.cpp:20,29
Поставим точку останова на строке номер 21. Точка останова – это метка, указывающая, что программа, дойдя до этого места, должна остановиться.
list problem.cpp:20,27
break 21
Посмотреть, где вы поставили точки останова, можно с помощью команды info breakpoints.
info breakpoints
(При желании можно вместо номера строки указать название функции,тогда программа остановится перед входом в функцию.)

Запустим программу.

run
Введём первое число 5 и знак математического действия « + ». Программа дойдёт до точки останова и остановится, выведя нам строку, у которой эта точка расположена.

Нам, конечно, интересно знать,в каком именно месте мы остановились, и что программа уже успела выполнить. Даём команду backtrace.

backtrace
Отладчик выдаёт нам следующую информацию:
#0 CProblem::Calculate (this=0x804b008) at problem.cpp:21
#1 0x08048e00 in CProblem::Solve (this=0x804b008) at problem.cpp:93
#2 0x08048efc in main () at main.cpp:15
Это означается, что мы находимся внутри выполняющейся функции Calculate, являющейся функцией-членом класса CProblem. Она была вызвана из функции Solve того же класса, а та, в свою очередь, из функции main. Таким образом, команда backtrace показывает весь стек вызываемых функций от начала программы до текущего места. Посмотрим, чему же равно на этом этапе значение переменной Numeral.
print Numeral
И нам сразу выводится число 5, которое мы и вводили в программу. (Значение, введённое нами с клавиатуры, присвоилось именно этой переменной.)

Если мы вместо print будем пользоваться командой display, то величина этой переменной будет показываться каждый раз, когда программа останавливается, без специального указания.

display Numeral
Добавим ещё одну точку останова на строке 25 файла problem.cpp.
break problem.cpp:25
Продолжим выполнение программы.
continue
Команда Continue продолжает выполнение программы с текущего адреса. Если бы мы набрали run, программа начала бы выполняться с начала. Поскольку на строке 24 имеется команда cin >> SecondNumeral, то нам придётся ввести второе слагаемое. Введём, например,число 2. После этого программа опять остановится на строке 25 (наша вторая точка останова).

Посмотрим, чему равны значения наших переменных Numeral, SecondNumeral и Operation. Если вы помните, именно такие переменные мы объявляли в классе CProblem.

print Numeral
print Operation
print SecondNumeral
У нас получится 5, « + », 2. Так и должно быть. Но давайте теперь «передумаем» и лучше присвоим переменной SecondNumeral значение 4. Отладчик GDB позволяет прямо во время выполнения программы изменить значение любой переменной.
set SecondNumeral=4
Если не верим, что её значение изменилось,можно проверить.
print SecondNumeral
Теперь мы ожидаетм, что результат будет 9. Давайте выполним программу до конца.
continue
Результат действительно равен 9.

Давайте теперь уберём наши точки останова. Мы, кажется, создали две таких точки. Но это можно проверить.

info breakpoints
Удалим их.
delete 1

delete 2
Унас не должно остаться ни одной точки останова. Проверяем.
info breakpoints
Действительно не осталось ни одной.

Теперь давайте пошагово пройдём всю программу (благо, она у нас небольшая).

Поставим точку останова на десятой строке главного файла.

break main.cpp:10
Запустим программу
run
Дойдя до десятой строчки, она остановится. Теперь проходим её, останавливаясь на каждой строчке, с помощью команды step.
step
Чтобы не набирать каждый раз s-t-e-p, можно просто вводить букву s. Как только программа доходит до команды Problem->SetValues(), она сразу переходит в файл problem.cpp, где находится определение функции-члена CProblem::SetValues() и проходит код этой функции. То же самое, когда она дойдёт до вызова Problem->Solve().

Чтобы при вызове функции, программа не входила в неё, а продолжала дальше выполняться только на текущем уровне стека, вместо step даётся команда next или просто n.

next
Если мы вошли в функцию, но не хотим дальше проходить её по шагам, а хотим, чтобы она отработала и вернула нас на предыдущий уровень стека (то есть, обратно в функцию, вызвавшую её), мы пользуемся командой finish.
finish
Таким образом, можно просмотреть, как выполняется вся программа или любой участок программы. На любом шаге можно проверять значение любой переменной. Чтобы перестать проходить программу по шагам и запустить её до конца, надо дать команду continue.

Дадим короткий список наиболее часто встречающихся команд отладчика GDB. За более подробной информацией вы, конечно, всегда можете обратиться к встроенному описанию программы (info gdb) или руководством по пользованию (man gdb).

Команды отладчика GDB

backtrace – выводит весь путь к текущей точке останова, то есть названия всех функций, начиная от main(); 
иными словами, выводит весь стек функций;

break – устанавливает точку останова; параметром может быть номер строки или название функции;

clear – удаляет все точки останова на текущем уровне стека (то есть в текущей функции);

continue – продолжает выполнение программы от текущей точки до конца;

delete – удаляет точку останова или контрольное выражение;

display – добавляет выражение в список выражений, значения которых отображаются каждый раз при остановке программы;

finish – выполняет программу до выхода из текущей функции; отображает возвращаемое значение,если такое имеется;

info breakpoints – выводит список всех имеющихся точек останова;

info watchpoints – выводит список всех имеющихся контрольных выражений;

list – выводит исходный код; в качестве параметра передаются название файла исходного кода, затем, через двоеточие, 
номер начальной и конечной строки;

next – пошаговое выполнение программы, но, в отличие от команды step, не выполняет пошагово вызываемые функции;

print – выводит значение какого-либо выражения (выражение передаётся в качестве параметра);

run – запускает программу на выполнение;

set – устанавливает новое значение переменной

step – пошаговое выполнение программы;

watch – устанавливает контрольное выражение, программа остановится, как только значение контрольного выражения изменится;


Описание примера

Методические указания к каждой лабораторной работе сопровождаются примерами программ. В каждом таком примере смоделирована среда одной и той же прикладной области, используя системные средства програмного обеспечения ОС в соответствии с темой работы. Ниже описана общая "легенда" выбранной нами прикладной области, которая уточняется далее в каждой конкретной лабораторной работе.

Легенда: Слоны на водопое

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

В нашей "легенде" стадо Слонов ищет воду в саванне. Условия поиска и потребления воды уточняются в каждой задаче отдельно. Каждый Слон представляется отдельным процессом или нитью, но программы, выполняющиеся в процессах-Слонах, идентичны. Наряду с процессами-Слонами, есть процесс-монитор, являющийся родительским для всех процессов-Слонов. Процесс-монитор во всех задачах осуществляет порождение процессов-Слонов, общее управление ими, а также обеспечение взаимодействия между ними при необходимости. Таким образом, процесс-монитор представляет собой "коллективный разум" слоновьего стада, мы назвали его Ганеша.

Каждый отдельный Слон характеризуется такими атрибутами, как:

Программное представление Слона описывается в файле elephant.h, который мы включаем в программы всех наших примеров.

Во всех примерах мы также работаем с одним и тем же стадом - массивом Слонов. Стадо определяется также в файле elephanth.h, который мы также включаем во все примеры.

Значительная часть программных кодов примеров каждой работы будет переноситься в следующие работы. Поэтому такие фрагменты кода оформлены как включаемые файлы (.h-файлы).

По смыслу задач "деятельность" Слонов (поиск воды в саванне и насыщение водой) занимает некоторое время. При этом это время занято реальной деятельностью, а не ожиданием (даже если в программной модели результаты этой деятельности не рассматриваются). В терминах программирования это означает, что процесс, выполняющий эту "деятельность", занимает центральный процессор. Поэтому для моделирования такой "деятельности" не применяются системные вызовы типа sleep, nanosleep, так как они обеспечивают задержку процесса в состоянии ожидания, без использования центрального процессора. Для "занятого ожидания" нами созданы две функции:

Исходные тексты этих функций доступны для вас в каталоге /home/OS/Metod/Lab_2010/Part_2/OS_Examples в файле common/wait.c.

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


Назад