Цель занятия:
Практическое изучение способов объектно-ориентированной обработки очередей данных с использованием простого наследования классов в системе программирования С++ на примере моделирования процесса циклического обслуживания конечного множества элементов.

Формулировка задания:
Требуется разработать программу JOSEPH для моделирования порядка обслуживания конечного набора элементов в условиях задачи Иосифа. В этой задаче предполагается, что заданное конечное число элементов N>1, подлежащих обслуживанию образуют круг, где каждый элемент идентифицируется уникальным порядковым номером от 1 до N. Для обслуживания элементов осуществляется обход круга, начиная с заданного номера. Контроль обхода по кругу осуществляется по счетчику шагов с начальным значением 1. Переход к следующему элементу в круге сопровождается увеличением значения счетчика шагов на единицу.
Когда значение счетчика достигает априори заданной пороговой отметки m>1, соответствующий текущему шагу элемент извлекается из круга, круг смыкается, счетчик инициализируется начальным значением 1 и продолжается обход оставшихся элементов. Обход круга должен продолжаться, пока не извлечены все элементы. Необходимо определить очередность извлечения элементов из круга.
Исходными данными программы joseph являются: начальное число элементов круга обслуживания, пороговое значение счетчика шагов и номер стартового элемента обхода. Исходные данные должны передаваться программе joseph через параметры командной строки ее вызова. Для представления круга обслуживания элементов необходимо использовать структуру очереди с кольцевым буфером, размер и начальное наполнение которой определяется исходными данными. Результат работы программы joseph должен отображать очередность номеров исключения элементов. Номера исключаемых элементов должны отображаться через поток стандартного вывода (stdout). Программа joseph должна быть составлена в системе программирования С++ с использованием инкапсуляции данных и простого наследования классов.

Организация очередей данных.

Очередь (Queue) - это абстрактная структура для работы с последовательностью данных, обслуживание которых происходит в соответствии с дисциплиной FIFO (FIRST IN, FIRST OUT - первым пришел, первым обслужен). Согласно принципу FIFO новые данные добавляются (записываются) в хвост очереди, а наличные данные извлекаются (считываются) из головы очереди. Извлекать данные из очереди и добавлять их в очередь можно неоднократно, пока очередь не пуста и не переполнена, соответственно.
Состояние очереди характеризуется парой указателей: tail - указатель на хвост (конец) очереди и head - указатель на голову (начало) очереди. При изменении содержания очереди за счет чтения или записи данных меняется состояние очереди и соответственно, значение указателей на ее голову или хвост. Новые данные записываются в ячейку очереди, адрес которой определяет значение указателя tail. После этого значение tail изменяется так, чтобы указывать на следующую свободную ячейку очереди. Чтение существующих данных происходит из ячейки, адрес которой определяет указатель head. После этого значение head изменяется так, чтобы указывать на следующий занятый элемент очереди.
Для хранения данных в очереди выделяется специальный буфер qbuf (queue bufer). В простейшем случае буфер очереди реализуется одномерным массивом элементов, длина которого априори устанавливает предельный размер очереди qsize (queue size), задаваемый при создании очереди. Такая организация очереди называется очередью с линейным фиксированным буфером (Line Queue).
Указатели на хвост и голову очереди интерпретируются в ней как целочисленные индексы массива, представляющего буфер очереди. Они могут изменяться в пределах от 0 до (qsize-1). В начальном состоянии очередь пуста и оба указателя имеют нулевое значение (tail=head=0).
Логическую структуру очереди с линейным фиксированным буфером, где размещены 5 элементов # иллюстрирует следующая диаграмма:

Рис. 1.  Логическая структура очереди с линейным фиксированным буфером

Рассмотренный линейный вариант очереди с фиксированным линейным буфером имеет ограниченное применение, т. к. для корректной работы значение указателя головы очереди не должно превышать значения указателя на хвост очереди (head<=tail). Это условие не позволяет добавлять новые элементы, когда хвост очереди исчерпан (tile=qsize), даже если в начале буфера очереди имеются свободные ячейки, после извлечения головных элементов (head>0). Это тупиковое состояние переполнения очереди с линейным фиксированным буфером показано на следующем рисунке, где 8 элементов # занимают конечные ячейки буфера очереди, а в его начале имеются 2 свободные ячейки:

Рис. 2.  Переполнение очереди с линейным фиксированным буфером

Чтобы иметь возможность использовать свободные ячейки в начале буфера очереди, которые образовались после чтения головных элементов, нужно применять более совершенную организацию очереди, известную как очередь с кольцевым фиксированным буфером (RingQueue). Кольцевой фиксированный буфер следует рассматривать как замкнутый линейный буфер qbuf, который после заполнения его до конца может заполняться сначала, если там имеются свободные ячейки. Чтобы превратить линейный буфер в кольцевой достаточно интерпретировать указатели головы и хвоста очереди как целочисленные индексы массива qbuf, взятые по модулю размера буфера очереди (qsize).

head mod qsize и tail mod qsize,

соответственно. Операция mod здесь интерпретируется как вычисление остатка целочисленного деления head и tail на qsize. Таким образом, любой из указателей обнуляется, когда его значение становится равным qsize, т. е. выходит за границы массива буфера очереди, например:

  IF head = qsize THEN head <- 0; 
и IF tail = qsize THEN tail <- 0. 

Жесткое условие корректности непустой очереди с линейным фиксированным буфером (head<tail) в очереди с кольцевым буфером становится необязательным, т. е. состояние, когда указатель хвоста опережает указатель головы очереди (tail<head) также является разрешенным. Оба разрешенных состояния очереди с фиксированным кольцевым буфером показаны на следующем рисунке:

Рис. 3.  Состояния очереди с кольцевым буфером

Для работы с очередью используются 2 классических примитива: ENQUEUE и DEQUEUE, которые обеспечивают добавление (запись) данных в очередь и извлечение (чтение) данных из очереди, соответственно. В очереди с кольцевым буфером они имеют реализацию рассмотренную ниже.
Примитив DEQUEUE возвращает элемент из головы очереди и увеличивает на 1 значение указателя head. Если значение head при этом выходит за границу буфера очереди, то head следует обнулить. Псевдокод примитива DEQUEUE выражают следующие операторы:

  W <- qbuf [head]; 
  head <- head+1 mod qsize; 
  return W; 

где W обозначает элемент данных, извлекаемый из очереди.
Примитив ENQUEUE загружает новый элемент в хвост очереди и увеличивает на 1 значение указателя tail. Если значение tail при этом выходит за границу буфера очереди, то tail следует обнулить. Псевдокод примитива ENQUEUE выражают следующие операторы:

  qbuf [tail] <- V; 
  tail <- tail+1 mod qsize; 
  return; 

где V обозначает элемент данных, извлекаемый из очереди.
Для очереди с фиксированным кольцевым буфером процедура извлечения (чтения) данных необязательно является деструктивной операцией, т. е. извлеченные элементы не уничтожаются в буфере очереди до записи на их место новых данных. Процедура добавления (записи) данных имеет деструктивный эффект, модифицируя содержание буфера очереди. Рассмотренные примитивы DEQUEUE и ENQUEUE обеспечивают корректную обработку очереди с фиксированным кольцевым буфером, когда он не пуст и не переполнен соответственно. Для корректной обработки этих критических ситуаций нужно установить соотношение указателей, при которых кольцевой буфер будет пуст или переполнен.
Критическим состоянием при чтении данных является пустая очередь, где все ячейки свободны. Формальным условием пустой очереди является равенство обоих указателей:

    tail = head 

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

   head = tail+1  mod  qsize

Если выполняется это соотношение, нельзя добавлять элементы в очередь, несмотря на то, что имеется 1 свободная ячейка, на которую указывает текущее значение tail. Иначе критерием переполнения очереди было бы равенство tail=head, которое является истинным также для пустой очереди. Чтобы иметь возможность отличать оба критических состояния очереди с кольцевым буфером, в нем должно содержаться от 0 до (qsize-1) значащих элементов при не менее, чем одной свободной ячейке. Указанную проверку переполнения можно использовать в сочетании с примитивом ENQUEUE, чтобы избежать потери значимости при попытке записи данных, когда исчерпан лимит свободных ячеек.

Алгоритм кругового обслуживания

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

  1. Заготовить пустую очередь с размером кольцевого буфера на 1 больше, чем число обслуживаемых элементов.
  2. Применяя примитив ENQUEUE соответствующее число раз, разместить номера всех обслуживаемых элементов в кольцевом буфере очереди, что эквивалентно расположению элементов по кругу.
  3. Чередуя вызовы примитивов DEQUEUE и ENQUEUE необходимое число раз, добиться чтобы в голове очереди оказался элемент с желаемым номером.
  4. Инициализировать счетчик шагов по кругу значением 1.
  5. Чередовать вызовы примитивов DEQUEUE и ENQUEUE при увеличении значения счетчика шагов, пока оно не достигнет заданного порогового значения. Эти действия эквивалентны обходу круга, чтобы достигнуть элемент, который нужно удалить из очереди. Исключаемый элемент будет находиться в голове очереди.
  6. Используя процедуру DEQUEUE удалить элемент из головы очереди и распечатать его номер.
  7. Проверить выполнение условия пустой очереди (tail=head). Если в очереди еще остались элементы, перейти на шаг 4 для продолжения анализа очередности удаления элементов. Иначе, если очередь пуста, завершить выполнение алгоритма.

Программирование очереди с кольцевым буфером.

Для программирования очереди с кольцевым буфером целесообразно применить принцип инкапсуляции объектно-ориентированного программирования, сосредоточив описание структуры очереди и примитивов ее обработки в отдельном классе RingQueue. Организация класса RingQueue должна быть ориентирована на применение его в качестве базового класса для построения производных классов, обладающих спецификой обработки кольцевой очереди в различных приложениях.
Класс RingQueue должен содержать защищенные (protected) компоненты-данные и общедоступные (public) компонентные методы их обработки. Это обеспечит доступ к данным класса только собственными компонентными методами или компонентными методами производного класса, но исключит возможность непосредственного несанкционированного обращения к ним из любых внешних функций программы.
Чтобы исключить зависимость компонентов класса RingQueue от типа элементов очереди, при его проектировании удобно воспользоваться средствами параметризации классов и функций, поддержка которых обеспечена в любых версиях системы программирования С++, начиная с USL 3.0. Параметризация класса осуществляется с помощью шаблона (template), который декларирует абстрактные типы данных как параметры класса. Они могут быть использованы для абстрактной типизации компонент-данных или кодов возврата и аргументов компонентных методов класса. В данном случае в шаблоне класса RingQueue может быть объявлен абстрактный тип <class QType>, обозначающий тип элементов очереди. Учитывая синтаксические особенности шаблонов классов в системе программирования С++, декларация логической структуры класса RingQueue может иметь следующий формат:

  template <class QType> class RingQueue 
  { 
  protected: 
   /*спецификация компонент-данных*/ 
  public: 
   /*спецификация прототипов компонентных методов*/ 
  }; 

Конкретизация типа параметризованных компонент шаблонного класса должна быть выполнена либо в производном нешаблонном классе или при определении объекта класса, когда он предназначается для непосредственного использования.
В защищенную (protected) часть декларации класса RingQueue целесообразно включить спецификацию следующих компонент-данных*

QType* qbuf:  /*адрес буфера очереди*/ 
 int qsize;    /*размер очереди*/ 
 int head;     /*указатель головы очереди*/ 
 int tail;     /*указатель на хвост очереди*/ 

где абстрактный тип QType является параметром из шаблона класса.
Для доступа к защищенным компонентам-данным в классе RingQueue рекомендуется предусмотреть общедоступные компонентные методы, реализующие примитивы обработки очереди. Декларация их прототипов в общедоступной части декларации класса RingQueue должна иметь следующий формат:
void enqueue(QType); /*добавить элемент в хвост очереди*/
QType dequeue(void); /*извлечь элемент из головы очереди*/
где абстрактный тип QType,использованный для спецификации кода возврата и аргументов этих функций ,является параметром из шаблона класса. Спецификация компонентных методов шаблонного класса должна содержать в своем заголовке ключевое слово template и декларацию параметров класса. Например , внешняя спецификация компонентного метода enqueue, заявленного в параметризованном классе RingQueue может иметь следующий формат.

 template <class QType> void 
 RingQueue <QType> :: enqueue (QType) {.....} 

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

 RingQueue (int);   /*конструктор класса*/ 

Конструктор класса RingQueue должен обнулять указатели очереди, фиксировать размер очереди и динамически выделять память из кучи для буфера очереди, используя операцию new системы программирования С++. Все перечисленные инициализации, кроме операции new, удобно перечислить в списке инициализаций, чтобы упростить исходный код в блоке конструктора.
Для корректного удаления объектов класса RingQueue в нем рекомендуется предусмотреть деструктор ~RingQueue(). Его задача - освобождение памяти, распределенной конструктором для буфера очереди, с помощью операции delete системы программирования С++.
Учитывая простоту исходного кода конструктора и деструктора класса RingQueue, их определение может быть дано непосредственно в теле класса.
Например, дефиниция конструктора может иметь следующий формат в разделе

public класса RingQueue: 
RingQueue (int S) : Qsize(s), head(0), tail(0)   {...}; 

В этом случае конструктор и деструктор по умолчанию будут рассматриваться компилятором С++ как подставляемые (inline) функции. В объектном коде компилятор С++ заменит каждую ссылку на inline-функцию непосредственно ее кодом, увеличивая быстродействие программы.

Программирование кругового обслуживания

В целях построения объектно-ориентированной программной реализации алгоритма кругового обслуживания для задачи Иосифа с помощью кольцевой очереди элементов, целесообразно сформировать класс JoeRing, производный от базового класса RingQueue. Производный класс JoeRing должен наследовать компоненты базового класса RingQueue, содержать собственные частные (private) компоненты-данные и общедоступные (public) компонентные методы, учитывающие специфику обработки круговой очереди в задаче Иосифа, а также определить конкретный тип данных для параметра базового класса, указанный в шаблоне его декларации для абстрактной типизации элементов очереди. В соответствии с правилами реализации принципа наследования в системе программирования С++, декларацию производного класса JoeRing можно специфицировать следующим образом:

 class JoeRing : public RingQueue <unsigned> 
 { 
 private:/*спецификация компонент-данных класса JoeRing*/ 
 public :/*спецификация прототипов собственных компонентных методов*/ 
 }; 

В заголовке приведенной декларации класса JoeRing указано имя базового класса (RingQueue), категория наследования public и конкретизация параметра шаблона базового класса <unsigned>.
Целочисленная (unsigned) конкретизация параметра шаблона базового класса RingQueue соответствует характеру обработки данных в рассматриваемой реализации задачи Иосифа, где элементы обслуживания идентифицируются целочисленными номерами, поэтому естественно определить тип элементов наследованной очереди как unsigned (беззнаковые целые). Соответствующее расширение наследованных компонент шаблонного базового класса до их полного определения автоматически осуществляется компилятором С++.
Категория наследования public, указанная в заголовке декларации класса JoeRing разрешает обращаться к наследованным защищенным компонентам базового класса в компонентных методах производного класса. К наследованным общедоступным компонентам можно обращаться также во внешних функциях программы, вызывая их через объекты производного класса. Защищенные наследованные компоненты во внешних функциях программы недоступны. При использовании более строгой категории наследования private, доступ к наследованным защищенным и общедоступным компонентам базового класса сохраняется только в методах производного класса. Во внешних функциях программы они будут недоступны для объектов производного класса. Таким образом, public-производные классы сохраняют привилегии доступа базового класса, private-производные классы изменяют привилегии доступа на private. Для любой категории наследования непосредственный доступ к частным компонентам базового класса в производном классе запрещен и реализуется через соответствующие общедоступные наследованные методы базового класса. Учитывая приведенные соображения в базовом классе рекомендуется применять защищенные компоненты вместо приватных, чтобы обеспечить доступ к ним в методах производного класса, когда это необходимо.
В спецификацию собственных частных компонент производного класса JoeRing рекомендуется включить объявление 3-х целочисленных (int) полей count, thresold и leader. Компоненты count и thresold следует использовать для хранения текущего и предельного значений, соответственно, счетчика шагов по кругу обслуживания, компонента leader должна указывать номер ведущего элемента, с которого начинается круговой отсчет в задаче Иосифа.
В общедоступную часть объявления класса JoeRing целесообразно включить декларацию прототипов следующих собственных компонентных методов:

 void load(), int busy(), unsigned find() и void round(), 

а также спецификации конструктора и деструктора класса.
Компонентный метод void JoeRing ::load() следует применять для загрузки кольцевого буфера наследованной очереди последовательными номерами элементов обслуживания в диапазоне от 1 до (qsize-1). Для записи номеров элементов в очередь следует использовать наследованный от базового класса RingQueue компонентный метод enqueue(unsigned). Кроме того, компонентный метод load должен обеспечивать циклический сдвиг всех элементов обслуживания в очереди на число позиций заданное компонентой leader. Сдвиг выполняется с целью расположить в голове очереди элемент, с которого должен начинаться обход круга. Для реализации каждой итерации циклического сдвига рекомендуется применять последовательно наследованные от базового класса RingQueue компонентные методы dequeue и enqueue.
Компонентный метод JoeRing ::find() должен обеспечивать поиск очередного удаляемого элемента и возвращать его номер. Для организации поиска рекомендуется инициализировать счетчик шагов (count=1) и поочередно вызывать наследованные методы базового класса RingQueue dequeue и enqueue, пока показания счетчика шагов меньше порогового значения (count<tresold). В результате выполнения этого цикла элемент кандидат для удаления из круга окажется в голове очереди и может быть исключен наследованным от базового класса RingQueue методом dequeue.
Компонентный метод JoeRing ::busy() рекомендуется предусмотреть для оценки текучего заполнения очереди, т. е. число элементов, находящихся в круге обслуживания. Число элементов очереди определяется соотношением значений ее указателей head и tail, наследованных от базового класса RingQueue. Доступ к ним в производном классе разрешен, т. к. они декларированы в базовом классе с модификатором доступа protected. Полученная оценка заполнения передается через код возврата этого компонентного метода.
Компонентный метод JoeRing ::find() должен объединять в своем блоке вызовы остальных компонентных методов класса JoeRing, чтобы реализовать алгоритм кругового обслуживания в задаче Иосифа. Для выполнения подготовительных шагов этого алгоритма следует вызвать компонентный метод load. Чтобы обеспечить круговой обход элементов обслуживания с целью поиска кандидатов для удаления, нужно организовать циклический вызов метода find, который возвращает номер исключаемого элемента. Для его отображения в потоке стандартного вывода (stdout) можно использовать библиотечную функцию printf системы программирования С++. Этот цикл должен продолжаться, пока очередь обслуживания не пуста. Для оценки заполнения очереди элементов следует применять компонентный метод busy.
Обращение к компонентному методу round может быть реализовано в основной функции main программы joseph через предварительно определенный объект класса JoeRing, например, следующим образом:

   joe.round(); 

где joe - объект класса JoeRing, обладающий желаемыми значениями параметров задачи Иосифа (число элементов обслуживания, номер ведущего элемента, пороговое значение счетчика шагов).
Для создания объектов класса JoeRing с требуемыми значениями параметров задачи Иосифа в нем необходимо предусмотреть конструктор с тремя целочисленными аргументами. Конструктор класса JoeRing должен инициализировать собственные приватные компоненты leader и thresold желаемыми значениями и вызывать конструктор базового класса RingQueue <unsigned> для инициализации очереди с наследованным кольцевым буфером достаточного размера, чтобы разместить требуемое число номеров элементов обслуживания. В связи с кольцевой организацией буфера очереди, его размер должен на 1 превышать число размещаемых в нем элементов. Конструктор базового класса RingQueue имеет один целочисленный аргумент, устанавливающий размер очереди данных. Поэтому конструктор производного класса JoeRing также должен иметь соответствующий целочисленный аргумент для передачи конструктору базового класса. Кроме того, конструктор класса JoeRing должен иметь еще два целочисленных аргумента, которые принимают требуемые значения номера ведущего элемента и порогового значения счетчика шагов для инициализации собственных приватных компонент leader и thresold, соответственно.
Все перечисленные инициализирующие действия можно реализовать через список инициализаций, перечислив их после имени в строке дефиниции конструктора класса JoeRing. Поскольку все инициализирующие действия выполняет список инициализаций, тело конструктора класса JoeRing может быть пустым {}.
Учитывая особенности программной реализации конструктора класса JoeRing, его определение целесообразно включить в раздел publlic декларации класса, например, следующим образом:

  class JoeRing : public RingQueue <unsigned> 
  { 
  ................................ 
  public: 
 JoeRing (int n, int l, int m) : RingQueue <unsigned> (n+1), 
     leader (l), thresold (m) {}; 
  ................................ 
  }; 

Рассмотренный конструктор следует использовать для создания объекта класса JoeRing в основной функции main программы Joseph. Необходимые 3 аргумента могут быть получены путем преобразования соответствующих параметров командной строки вызова программы Joseph в целочисленный формат с помощью библиотечной функции atoi системы программирования С++.
Чтобы всегда иметь возможность корректно завершить обработку объектов класса JoeRing в нем рекомендуется предусмотреть деструктор ~JoeRing(). В данном случае деструктор производного класса следует специфицировать, чтобы обеспечить неявный вызов деструктора базового класса RingQueue для корректной выходной обработки наследованных компонент базового класса, в частности, для освобождения памяти, динамически выделенной под буфер очереди. Какая-либо выходная обработка собственных компонент-данных класса JoeRing не является необходимой, поэтому тело деструктора ~JoeRing() целесообразно оставить пустым {}, а его определение включить в декларацию класса.
Определение конструктора и деструктора внутри декларации класса JoeRing принуждает компилятор С++ рассматривать их по умолчанию как подставляемые (inline) функции, обеспечивая автоматическую подстановку объектного кода по адресам их вызовов в выполняемом модуле программы Joseph. Компонентные методы класса JoeRing могут иметь достаточно представительный исходный и, следовательно, объектный код. Поэтому их целесообразно специфицировать вне декларации класса, не превращая в inline-подстановки. Например, определение компонентного метода round вне декларации класса может иметь следующий формат:

 void JoeRing :: round() {/*Исходный код метода*/} 

Исключением может быть компонентный метод busy, который достаточно прост, чтобы определить его внутри декларации класса JoeRing.

Контрольные задания

  1. Рассмотреть вариант задачи Иосифа, где пороговое значение счетчика шагов по кругу обслуживания не является постоянным, а изменяется случайным образом после удаления каждого элемента или является известной функцией его номера.
  2. Изменить программу Joseph таким образом, чтобы вместо целочисленных номеров для обозначения элементов в круге обслуживания задачи Иосифа использовались буквы латинского алфавита. В этом случае исходный набор элементов может быть задан соответствующей строкой символов.
  3. Модифицировать программу Joseph таким образом, чтобы для хранения элементов обслуживания применялась очередь с линейным фиксированным буфером минимально возможного размера.
  4. Разработать класс очередь с файловым буфером (FileQueue), где для хранения элементов очереди используется регулярный файл, обслуживаемый операциями чтения-записи по 2-м файловым указателям (типа File*) или дескрипторам к контексте процесса, один из которых можно получить, если открыть файл по чтению, другой - в режиме добавления в конец файла. При разработке методов класса FileQueue рекомендуется использовать библиотечные функции ввода-вывода системы программирования С или соответствующие вызовы OSUNIX.
  5. Разработать класс сдвиговой очереди (ShiftQueue), организация которой будет напоминать работу сдвигового регистра: после исключения головного элемента все остальные сдвигаются на 1 шаг в направлении головы очереди.
  6. Рассмотреть возможность применения программных каналов OSUNIX для моделирования кругового обслуживания элементов в условиях задачи Иосифа.

Рекомендуемая литература

1. Д. Кнут
Искусство программирования для ЭВМ - т. 1, Основные алгоритмы,
М., Мир, 1976.
2. Б. Мейер, К. Бодуэн
Методы программирования, т. 1, М., Мир,1989.
3. Й.Лэнгсам, М.Огенстайн, А.Тененбаум
Структуры данных для персональных ЭВМ , М., Мир, 1989
4. П. Лукас
С++ под рукой, Киев, НИПФ "Диа-Софт", 1993.
5. Т. Чан
Системное программирование на С++ для UNIX-BHV, Киев, 1997.