авторефераты диссертаций БЕСПЛАТНАЯ БИБЛИОТЕКА РОССИИ

КОНФЕРЕНЦИИ, КНИГИ, ПОСОБИЯ, НАУЧНЫЕ ИЗДАНИЯ

<< ГЛАВНАЯ
АГРОИНЖЕНЕРИЯ
АСТРОНОМИЯ
БЕЗОПАСНОСТЬ
БИОЛОГИЯ
ЗЕМЛЯ
ИНФОРМАТИКА
ИСКУССТВОВЕДЕНИЕ
ИСТОРИЯ
КУЛЬТУРОЛОГИЯ
МАШИНОСТРОЕНИЕ
МЕДИЦИНА
МЕТАЛЛУРГИЯ
МЕХАНИКА
ПЕДАГОГИКА
ПОЛИТИКА
ПРИБОРОСТРОЕНИЕ
ПРОДОВОЛЬСТВИЕ
ПСИХОЛОГИЯ
РАДИОТЕХНИКА
СЕЛЬСКОЕ ХОЗЯЙСТВО
СОЦИОЛОГИЯ
СТРОИТЕЛЬСТВО
ТЕХНИЧЕСКИЕ НАУКИ
ТРАНСПОРТ
ФАРМАЦЕВТИКА
ФИЗИКА
ФИЗИОЛОГИЯ
ФИЛОЛОГИЯ
ФИЛОСОФИЯ
ХИМИЯ
ЭКОНОМИКА
ЭЛЕКТРОТЕХНИКА
ЭНЕРГЕТИКА
ЮРИСПРУДЕНЦИЯ
ЯЗЫКОЗНАНИЕ
РАЗНОЕ
КОНТАКТЫ


Pages:     | 1 || 3 | 4 |   ...   | 5 |

«Пышкин Е.В. СТРУКТУРНОЕ ПРОЕКТИРОВАНИЕ: ОСНОВАНИЕ И РАЗВИТИЕ МЕТОДОВ С примерами на языке C++ Учебное пособие ...»

-- [ Страница 2 ] --

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

Например, в C# для этой цели используется язык XML (eXtensible Markup Language), при этом документирование кода выглядит примерно так:

/// summary /// Plain triangle construction and processing class /// /summary /// remarks /// see cref="Point"Point/see /// /remarks public class Triangle { //...

/// /// Triangle construction by using three points /// param name = "p1"First point reference/param /// param name = "p2"Second point reference/param /// param name = "p3"Third point reference/param /// returnsReturns true, if successful /// Otherwise returns false /// /returns public boolean ConstructTriangle( Point p1, Point p2, Point p3 ) { //...

} } Упрощенная схема извлечения документации из исходных текстов проекта представлена на рис. 1.11.

Файлы с исходным текстом Эту роль часто выполняет Doc Parser компилятор Описание правил (XML-Schema) XML-документация Web Report Generator Документация в форме связанных HTML-страниц Рис. 1.11. Документирующие комментарии Унификация форматов документирующих комментариев позволяет обеспечить их машинную обработку и автоматизировать генерацию документации к разрабатываемому ПО, например, в форме связанных HTML-страниц с единым пользовательским интерфейсом. Некоторые подробности интеграции кода и документации обсуждаются также в учебном пособии [Пышкин, 2005].

Языки программирования C и C++: цели разработки и назначение История возникновения языка C++ не может рассматриваться в отрыве от его прямого предшественника – языка C. Появление же языка C во многом было связано с актуальными задачами системного программирования, стоявшими перед разработчиками в 60-70-е года двадцатого века.

Историческим прототипом языка C является язык BCPL (Basic Combined Programming Language), разработанным в 60-е годы Мартином Ричардсом из Кембриджского университета. В 1970 году сотрудники Bell Laboratories создали вариант языка BCPL, названный B, который был использован при разработке операционной системы Unix для компьютера PDP-11 фирмы DEC. В этом диалекте BCPL не было типов данных, его единственным объектом было машинное слово, не различались целые значения и значения с плавающей точкой, не обеспечивались удобные способы выполнения операций над этими данными.

В 1972 году Д. Ритчи разработал язык C, в котором присутствовали типы данных, и была обеспечена возможность конструирования и использования сложных типов (массивов и структур). Существенный вклад в развитие языка C и строгое формулирование его синтаксиса и семантики внес Б. Керниган, сформулировавший основные концепции, отражаемые языком C. Это, в первую очередь, компактность и мобильность. При этом реализация в языке C базовых идей структурного программирования, его наглядность по сравнению с ассемблерами вкупе с возможностью использования ассемблерного кода в программах на C быстро сделало язык C самым распространенным средством системного программирования [Troy, 1986].

Язык C++ был спроектирован для того, чтобы предоставить разработчикам мощный и гибкий инструмент, обеспечивающий модель памяти и вычислительного процесса, которая была бы пригодна для использования на большинстве современных платформ [Stroustrup, 2000].

Язык C++ призван был также обеспечить использование проектных решений, полученных на языке C. За исключением некоторых деталей язык C может рассматриваться как подмножество языка C++, однако в C++ обеспечивается более строгий контроль типов.

Язык C++ в существенной степени ориентирован на мультипарадигменное программирование, позволяя объединить гибкость и эффективность языка C для системного программирования с объектно ориентированными возможностями организации программ. C++ не вынуждает программиста следовать какой-то определенной модели программирования (в отличие, например, от языка Java), и это является одной из его конструктивных особенностей. Б. Страуструп отмечал, что он не хотел, чтобы пользователи языка были вынуждены делать что-то жестко определенное: «Из истории мы знавали множество идеалистов, пытавшихся заставить людей делать что-либо во имя их же блага и оказывавшихся виновными в самых страшных трагедиях».

Описывая язык C++, Б. Страуструп квалифицирует его как язык программирования общего назначения с уклоном в сторону системного программирования, который:

лучше, чем С (поскольку обеспечивает более строгую проверку соответствия программы процедурной парадигме проектирования);

поддерживает абстракцию данных;

поддерживает ООП;

поддерживает обобщенное программирование.

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

1.2. «Алгоритмы + структуры данных = программы»

Наиболее емкое неформальное определение структурно процедурного подхода к программированию дал, вероятно, Н. Вирт в своей знаменитой монографии, название которой вынесено в заголовок раздела [Wirth, 1976]. Программа, реализующая процедурную парадигму программирования, представляет собой комбинацию описаний и действий (предписаний, команд, инструкций). Прежде, чем любой программный объект (переменная, константа, тип или функция) может быть использован, в программе должно присутствовать описание этого программного объекта.

Описания программных объектов Рассмотрим некоторое простое описание на языке C++.

int a;

В связи с любым описанием необходимо рассмотреть два базовых понятия: ячейка памяти и адрес ячейки памяти. Ячейка памяти – это то место, где хранится представление переменной в некотором коде. Размер этой ячейки и способ интерпретации кода определяет тип переменной (рис. 1.12).

Дополнительный код a 10000000 00000000 00000000 4 байта int Стандартный целый тип в 32-разрядной архитектуре Рис. 1.12. Задачи, решаемые описанием переменной В соответствии с концепцией типа в определении Хоара, тип определяет класс значений, которые могут принимать переменная или выражение. При работе с языком программирования знание типа позволяет обнаруживать бессмысленные конструкции и решать вопрос о методе представления данных и преобразования их в ЭВМ [Dahl, Dijkstra, Hoare, 1972].

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

Таким образом, описание данных необходимо для решения трех основных задач:

1. Определить необходимое количество памяти для хранения этих данных.

2. Определить способ представления значения в памяти компьютера (код и правило декодирования цепочки битов, хранящихся в этой памяти).

3. Задать символическое имя, соответствующее адресу в памяти.

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

Для представленного примера расшифровка следующая: a является переменной, которая может принимать целые значения, представляемые дополнительным двоичным кодом, длина которого зависит от архитектуры компьютера (например, 16 разрядов, или 2 байта,– для 16-разрядных процессоров, 32 разряда, или 4 байта,– для 32-разрядных процессоров).

Определение действий над программными объектами Действия над программными объектами определяются посредством элементарных операций над данными и высокоуровневых инструкций языка.

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

int a, b;

printf( "%d", a+b );

// Выражение a+b не имеет побочных эффектов a = a + b;

// Один побочный эффект // (изменяется значение переменной a) a = ++b;

// Два побочных эффекта // (изменяются значения переменных a и b) Выражение, завершающееся точкой с запятой, является отдельной инструкцией программы.

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

Присваивание Одной из фундаментальных операций вычислительного процесса является присваивание (рис. 1.13):

int a = 10;

// Переменная a инициализирована значением int b = 25;

// Переменная b инициализирована значением // NB! Инициализация НЕ является присваиванием // Здесь: a=10, // b= b = a;

// Присваивание // Здесь: a=10, // b= Особенность присваивания заключается в том, что в различных языках программирования присваивание может быть инструкцией языка (statement) или операцией (operator). Например, в языке Pascal присваивание – это инструкция языка, обеспечивающая занесение в одну ячейку памяти копии значения, хранящегося в другой ячейке памяти.

a 00000000 00000000 00000000 До присваивания b 00000000 00000000 00000000 b = a;

a 00000000 00000000 00000000 После присваивания копирование b 00000000 00000000 00000000 Рис. 1.13. Присваивание В языках C/C++ присваивание – это операция языка. В результате вычисления операции получается значение (совпадающее с присваиваемым значением). Это значение не обязательно совпадает со значением переменной, расположенной справа от операции присваивания, после вычисления выражения. Рассмотрите примеры двух выражений:

int a = 10;

int b = 25;

b = a;

// Значение операции присваивания (10) совпадает // со значением переменной a (10) b = a++;

// Значение операции присваивания (10) НЕ совпадает // со значением переменной a (11) Подробнее операции языка обсуждаются в следующих разделах.

Классификация операций В зависимости от того, на какой особенности применения операции делается акцент, операции, поддерживаемые C и C++, можно классифицировать по-разному.

По числу операндов операции можно классифицировать следующим образом:

одноместные операции (например, операция взятии с обратным знаком, операция обращения по адресу, операции увеличения и уменьшения на единицу, операция sizeof, операция «запятая»);

двуместные операции (например, операции сложения и вычитания, логические операции И и ИЛИ, операция сравнения, операция присваивания);

трехместная (тернарная) операция (такая в языке C++ одна – условная операция).

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

По категории операндов операции можно подразделить на ссылочные (например, операция выбора элемента массива, операции выбора поля структуры), арифметические (сложение, вычитание, умножение, деление, остаток от деления), логические (И, ИЛИ, НЕ) и операции отношения (равенство, неравенство, строгие и нестрогие отношения порядка).

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

Наконец, операции могут иметь или не иметь побочные эффекты в виде изменения значения операнда, участвующего в операции. К операциям с побочным эффектом относятся следующие операции:

операции увеличения и уменьшения на единицу;

операции присваивания и составного присваивания.

В связи с организацией вычисления выражений вводятся понятия приоритета операций и правил группирования операций (ассоциативность). Приоритет определяет очередность выполнения операций в выражении. Из арифметических операций самый высокий приоритет имеют одноместные операции, самый низкий – операция присваивания. Приоритет операций, поддерживаемых языком, определяется правилами языка. Наиболее полное описание операций, поддерживаемых C++, см. в [Stroustrup, 2000].

Группирование операций определяет порядок выполнения операций равного приоритета. Операции называются правоассоциативными, если они выполняются справа налево, например, все одноместные операции и операции присваивания – правоассоциативны.

Операции называются левоассоциативными, если выполняются слева направо, например, все двуместные арифметические операции, операции отношения, двуместные логические операции группируются слева направо.

Основываясь на взаимном приоритете операций и правилах группирования, компилятор в процессе распознавания выражения строит синтаксическое дерево выражения, определяющее его структуру (рис. 1.14).

ЧТЕНИЕ b a = a + b * * + * a ЗАПИСЬ a + = Структура выражения - Порядок выполнения операций синтаксическое дерево машинного кода Рис. 1.14. Синтаксическое дерево выражения Использование скобок позволяет изменить взаимный приоритет двуместных операций (изменять приоритет одноместных операций никакого смысла нет – он и так самый высокий). Изменение приоритета также может быть наглядно представлено в форме синтаксического дерева выражения (см. рис. 1.15).

ЧТЕНИЕ a a = ( a + b ) * + b + * ЗАПИСЬ a * = Рис. 1.15. Изменение приоритета В [Sebesta, 2002] указывается, что языки, допускающие использование скобок при записи выражений, могут вообще обходится без правил приоритета, предоставляя программисту самому определить требуемый порядок вычислений. При этом программистам не приходилось бы запоминать правила приоритета и группирования, однако написание выражений стало бы более утомительным, а сами выражения – более громоздкими. Язык, использующий такой подход, все же существует – это язык APL.

Высокоуровневые инструкции языка Для управления порядком выполнения операций над данными используются инструкции языка. Чем выше абстрактная модель вычислительного устройства, реализуемая языком программирования, тем большее число операций может быть объединено в составе одной инструкции языка. Основные управляющие конструкции языков структурного программирования рассматриваются далее в гл. 3.

Понятие об определении и объявлении Описание может являться определением (и одновременно объявлением), а может быть только объявлением. Между понятиями «определение программного объекта» (definition) и «объявление программного объекта» (declaration) существует принципиальная разница, которую нужно понимать.

Глобальная область видимости int a;

Определение Область видимости отдельного модуля extern int a;

Объявление Доступ к объекту из глобальной области видимости a = 10;

Рис. 1.16. Объявление и определение Определение всегда связано с выделением памяти. Наличие в тексте определения объекта означает, что для данного объекта при загрузке программы будет выделена память, для обращения к которой в программе используется имя определяемого объекта (например, имя переменной).

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

Очевидно, что любое определение автоматически является объявлением.

Требование объявления объекта является более мягким, чем требование определения (объект должен быть известен в некотором модуле, но не обязательно именно в этом модуле определен). Понятие объявления тесно связано с понятием «область видимости». Объявление объекта делает его доступным в пределах области видимости этого объекта (рис. 1.16).

Информацию об областях видимости и классах памяти программных объектов см. в разд. 3.4.

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

Определение императивного программирования Методология императивного программирования основана на принципе конструирования программ как описаний последовательного изменения состояния вычислителя пошаговым образом. Для императивного программирования характерна полная определенность и контролируемость переходов из одного состояния в другое [Одинцов, 2002].

Императивное программирование хорошо подходит для решения задач, характеризующихся не очень сложной логической структурой, для которых определение последовательности исполнения команд является наиболее естественным решением. На базе императивного программирования разработано множество управляющих программ (драйверов, компонентов встраиваемого ПО), обширная библиотека численных методов (например, на языке FORTRAN), программы для вычислительных машин с параллельной архитектурой.

Императивная методология лежит в основе таких языков как FORTRAN, Pascal, Algol, C.

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

Целевая архитектура Большинство императивных языков программирования разрабатывались на основе модели архитектуры компьютера, названной фон Неймановской, по имени одного из ее авторов – американского математика родом из Будапешта Джона (Яноша) фон Неймана.

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

данные и операции над данными.

В основе архитектуры фон Неймана лежит представление ЭВМ как композиции процессорного блока (выполняющего команды) и блока оперативной памяти (рис. 1.17). При этом основными элементами блока процессора является арифметико-логическое устройство (выполняющее операции) и устройство управления (обеспечивающее итерационную процедуру выполнения команд).

Арифметико-логическое устройство Arithmetic Logical Unit Оперативная память Memory Unit Устройство управления Control Unit Процессорный блок Периферийные устройства Рис. 1.17. Элементы архитектуры фон Неймана Одним из важнейших принципов, заложенных в фон Неймановскую модель, является хранение в одной и той же оперативной памяти и данных, и команд для обработки этих данных.

Основной структурной единицей оперативной памяти является регистр – устройство для хранения цепочки битов (например, если регистр способен хранить четырехбайтовое слово, говорят о 32-разрядном регистре).

Оперативная память состоит из большого числа регистров (см. рис. 1.18).

Каждый регистр имеет уникальный числовой адрес. Использование специального регистра адреса позволяет обратиться к любому регистру памяти. Память с такой организацией управления называется памятью с произвольным доступом (Random Access Memory – RAM). Размер регистра адреса определяет, сколько ячеек памяти может адресовать этот регистр.

Например, используя 10 разрядов, можно адресовать 210 регистров памяти.

Основные два режима управления оперативной памятью – это управление в режиме чтения и в режиме записи. В режиме чтения управление памятью организовано следующим образом:

1. В регистр адреса записывается адрес считываемого регистра.

2. На блок управления подается управляющий сигнал READ.

3. Информация перемещается из адресуемого регистра в регистр данного.

4. Блок управления выставляет сигнал готовности, после чего содержимое регистра данного может пересылаться по назначению.

Регистр адреса WRITE Блок READY управления READ...

Регистр данных N- Рис. 1.18. Принципы организации оперативной памяти В режиме записи последовательность работы следующая:

1. В регистр адреса записывается адрес регистра, в который нужно записать цепочку битов.

2. В регистр данных загружается исходная цепочка битов.

3. На блок управления подается управляющий сигнал WRITE.

4. Информация перемещается из регистра данных в адресуемый регистр памяти.

5. Блок управления выставляет сигнал готовности.

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

+ Операционный _ блок...

RA RB Рис. 1.19. Арифметико-логическое устройство Основное назначение арифметико-логического устройства (АЛУ) состоит в преобразовании информации посредством выполнения простейших арифметических и логических операций (сложения, вычитания, умножения, деления, сравнений). Классическая структура АЛУ предполагает наличие трех компонентов: операционный блок и двух регистров. Исходные данные операции (см. рис. 1.19) помещаются в регистры RA и RB. Результат операции помещается в регистр RA, поэтому АЛУ такой структуры называют АЛУ накапливающего типа.

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

Команда называется одноадресной, если содержит только один операнд (рис. 1.20,а). Команда называется двухадресной, если содержит два операнда (рис. 1.20,б).

Код операции Адрес операнда (а) Одноадресная команда Код операции Адрес операнда-1 Адрес операнда- (б) Двухадресная команда Рис. 1.20. Одноадресные и двухадресные команды Любая двухадресная команда может быть представлена в виде последовательности одноадресных команд, например для сложения содержимого двух ячеек оперативной памяти VAR1 и VAR2 и помещения результата в ячейку RES, требуется реализовать следующую последовательность действий:

ЧТЕНИЕ VAR1 ‘ИЗ РЕГИСТРА RAM VAR1 В РЕГИСТР АЛУ RA + VAR2 ‘ИЗ РЕГИСТРА RAM VAR2 В РЕГИСТР АЛУ RB К ‘РЕЗУЛЬТАТ СЛОЖЕНИЯ ПОМЕЩАЕТСЯ В RA ЗАПИСЬ RES ‘ИЗ RA В РЕГИСТР RAM RES Для организации правильного порядка выполнения команд структура устройства управления (control unit) предполагает использование двух специальных регистров: регистра текущей команды (РТК) и регистра адреса следующей команды (РАСК). Алгоритм работы устройства управления состоит в следующем:

1. Очередная команда (адрес которой находится в РАСК) передается из оперативной памяти в РТК устройства управления (задействован цикл чтения данных из оперативной памяти), см. рис. 1.21.

2. Содержимое РАСК увеличивается на 1. То есть теперь в РАСК действительно хранится адрес следующей команды.

3. Выполняется команда из РТК. Возможен побочный эффект – модификация содержимого РАСК (это происходит в том случае, если выполняемая команда является командой условной или безусловной передачи управления).

4. Если выполненная в п.3 команда не является командой останова, перейти к п.1, иначе – прекратить вычисления.

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

3. Выполнение команды из РТК с возможной модификацией РАСК АЛУ Регистр адреса РТК РАСК Регистр данных Оперативная память Устройство управления 1. Извлечение очередной команды 2. Увеличение содержимого РАСК на Рис. 1.21. Алгоритм работы устройства управления Для иллюстрации процесса выполнения программы вычислительной машиной с архитектурой фон Неймана, рассмотрим простую программу сложения двух чисел, написанную на учебном машинно-ориентированном языке [Лекарев, 1992]. Комментарии, начинающиеся с символов К и апострофа, поясняют смысл символических команд.

К СЕГМЕНТ ДАННЫХ VAR1 ЦЕЛПАМ 1 ‘ПЕРВОЕ СЛАГАЕМОЕ (1 ячейка целого типа) К ‘VAR1 – символический адрес ячейки RAM VAR2 ЦЕЛПАМ 1 ‘ВТОРОЕ СЛАГАЕМОЕ (1 ячейка целого типа) К ‘VAR2 – символический адрес ячейки RAM RES ЦЕЛПАМ 1 ‘РЕЗУЛЬТАТ (1 ячейка целого типа) К ‘RES – символический адрес ячейки RAM К СЕГМЕНТ КОМАНД СТАРТ ‘НАЧАЛО РАБОТЫ (ТОЧКА ВХОДА) ВВОД VAR1 ‘ИЗ УСТРОЙСТВА ВВОДА В РЕГИСТР RAM VAR ВВОД VAR2 ‘ИЗ УСТРОЙСТВА ВВОДА В РЕГИСТР RAM VAR ЧТЕНИЕ VAR1 ‘ИЗ РЕГИСТРА RAM VAR1 В РЕГИСТР АЛУ RA + VAR2 ‘ИЗ РЕГИСТРА RAM VAR2 В РЕГИСТР АЛУ RB К ‘РЕЗУЛЬТАТ СЛОЖЕНИЯ ПОМЕЩАЕТСЯ В RA ЗАПИСЬ RES ‘ИЗ RA В РЕГИСТР RAM RES ВЫВОД RES ‘ИЗ RES НА УСТРОЙСТВО ВЫВОДА СТОП ‘ОСТАНОВКА ПРОГРАММЫ Рис. 1.22 иллюстрирует отдельные состояния вычислителя в ходе выполнения этой программы в предположении, что из устройства ввода считываются значения 10 (для переменной VAR1) и 20 (для переменной VAR2). При этом для наглядности вместо кодов операций изображены символические наименования, а вместо двоичных чисел – десятичные.

Начальное состояние VAR1 0 ? RA RB VAR2 1 ?

Регистр RES 2 ? АЛУ адреса 3 СТАРТ 4 ВВОД 5 ВВОД 6 ЧТЕНИЕ 7+ 1 РТК 8 ЗАПИСЬ 9 ВЫВОД 10 СТОП РАСК Регистр данных...

Оперативная память Устройство управления Перед выполнением команды ЧТЕНИЕ VAR 1 VAR1 0 10 RA RB VAR2 1 Регистр RES 2 ? АЛУ адреса 3 СТАРТ 4 ВВОД 5 ВВОД 6 ЧТЕНИЕ 7+ 1 РТК ВВОД 8 ЗАПИСЬ 9 ВЫВОД 10 СТОП РАСК Регистр данных...

Оперативная память Устройство управления После считывания команды ЧТЕНИЕ VAR1 из памяти 6 VAR1 0 10 RA RB VAR2 1 Регистр RES 2 ? АЛУ адреса 3 СТАРТ 4 ВВОД 5 ВВОД 6 ЧТЕНИЕ 7+ 1 ЧТЕНИЕ РТК 8 ЗАПИСЬ 9 ВЫВОД ЧТЕНИЕ 10 СТОП РАСК Регистр данных...

Оперативная память Устройство управления Рис. 1.22. Состояния вычислителя После выполнения команды ЧТЕНИЕ VAR 0 VAR1 0 10 RA 10 RB VAR2 1 Регистр RES 2 ? АЛУ адреса 3 СТАРТ 4 ВВОД 5 ВВОД 6 ЧТЕНИЕ 7+ 1 РТК ЧТЕНИЕ 8 ЗАПИСЬ 9 ВЫВОД 10 СТОП РАСК Регистр данных...

Оперативная память Устройство управления После выполнения команды + VAR 1 VAR1 0 10 RA 30 RB VAR2 1 Регистр RES 2 ? АЛУ адреса 3 СТАРТ 4 ВВОД 5 ВВОД 6 ЧТЕНИЕ 7+ 1 РТК + 8 ЗАПИСЬ 9 ВЫВОД 10 СТОП РАСК Регистр данных...

Оперативная память Устройство управления После выполнения команды ЗАПИСЬ RES 2 VAR1 0 10 RA 30 RB VAR2 1 Регистр RES 2 30 АЛУ адреса 3 СТАРТ 4 ВВОД 5 ВВОД 6 ЧТЕНИЕ 7+ 1 РТК ЗАПИСЬ 8 ЗАПИСЬ 9 ВЫВОД 10 СТОП РАСК Регистр данных...

Оперативная память Устройство управления Рис. 1.22 (продолжение) Таким образом, главными элементами императивных языков программирования, являются следующие элементы:

переменные, моделирующие ячейки оперативной памяти;

операции над переменными, включая важнейшую операцию присваивания, основанную на пересылке данных из одной ячейки в другую;

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

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

Процедурная парадигма В литературе встречаются два эквивалентных термина, обозначающих обсуждаемый здесь предмет: процедурное программирование и функционально-ориентированное программирование (не путать с функциональным программированием). Появление второго наименования, вероятно, обусловлено развитием технологий программирования, ориентированных на языки C и C++, где понятие «процедура», как правило, не используется.

Существенная часть основания процедурной парадигмы – структурно-императивное программирование – было рассмотрено выше.

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

Абстракции процедурного программирования Процедурное программирование имеет дело с тремя основными видами абстракций: абстракции процессов, абстракции управляющих конструкций и абстракции данных. Абстракции процессов представлены процедурами и функциями, абстракции управляющих конструкций представлены комплектом основных управляющих конструкций структурного программирования (см. гл. 3), абстракции данных представлены структурами данных.

Использование в наименовании парадигмы определения «процедурная» показывает приоритетность абстракций процессов в процедурных языках. Не случайно в первых языках программирования основными высокоуровневыми понятиями были именно подпрограмма (subroutine) и ее категории – процедура и функция. Обособление элементов реализации в виде отдельных подпрограмм позволяет обеспечить следующие преимущества исходного текста:

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

Исключение дублирования кода (в этом случае функциональное или процедурное обособление является обязательным).

Упрощение построения модульной структуры на основе группирования процедур или функций по принципу общности выполняемых действий или обрабатываемых данных.

Возможность распределения участков кода между различными проектировщиками.

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

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

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

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

Таким образом, программист должен найти средства выражения не только для вычислительных алгоритмов, но и для обрабатываемых данных, используя существующие в современных процедурных и гибридных языках изобразительные средства (см. гл. 5—6).

Начало исследований в связи с совершенствованием абстракций управляющих конструкций приходится на 90-е гг., когда несовершенство управляющих конструкций большинства языков высокого уровня (не претерпевших существенных изменений за десятилетия развития вычислительной техники и областей ее применения) стало отмечаться многими специалистами. В литературе эта ситуация обычно называется методологическим разрывом между состоянием теории программирования и запросами практики проектирования, имеющей дело с возрастающей сложностью разрабатываемого программного обеспечения [Лекарев, 1997].

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

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

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

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

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

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

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

Процедурное программирование иерархично по своей природе.

Решение задачи средствами процедурного программирования всегда связано с построением иерархии процедур и функций (рис. 1.23).

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

main OpenFile CloseFile ProcessText ScanLex GelLit GetSynterm LexFirst LexNext LexFinish Рис. 1.23. Иерархия функций Модульность в процедурном программировании Сложный проект обычно содержит большое число функциональных блоков. Рассмотрение программы как единого набора процедур и функций, не позволяет обеспечить адекватного сложности задачи уровня организации программы и управления вычислительным процессом. Поэтому происходит группирование функций в соответствии с общностью решаемых задач (рис. 1.24).

Наиболее распространенной единицей модульности чаще всего является отдельный файл (module, unit). При этом говорят о модульности на физическом уровне. Однако при построении сложных проектов физического уровня часто оказывается недостаточно, поскольку относительно обособленные наборы функций могут относиться к одной подобласти предметной области проекта.

Main main File OpenFile CloseFile ProcessText Litera Lexema ScanLex GelLit GetSynterm LexFirst LexNext LexFinish Рис. 1.24. Функции и модули Например, комплект средств по считыванию символов, определению синтерма считанной литеры и конструирования лексем входного текста (см. гл. 8) удобно реализовать в виде отдельных физических модулей проекта, хотя все они и относятся к построению лексического анализатора (рис. 1.25).

Main main File ProcessText OpenFile CloseFile Litera Lexema LEXER GelLit ScanLex GetSynterm LexFirst LexNext LexFinish Рис. 1.25. Логический уровень модульности Для выражения понятия модульности на уровне, учитывающем логические связи между функциями и обрабатываемыми им данными, во многих языках программирования реализованы механизмы пространств имен. Пространство имен (namespace) является абстракцией более высокого уровня, поскольку, в общем случае, одно пространство имен может объединять объекты, определенные в разных файлах, с другой стороны, в одном файле могут быть определены объекты из нескольких пространств имен. Таким образом, концепции физической и логической модульности являются ортогональными (рис. 1.26).

Module1 Module2 Module Namespace A Namespace B Рис. 1.26. Пространства имен Подробно реализация пространств имен в ряде современных языков программирования (в том числе, в C++), рассматривается в учебном пособии [Пышкин, 2005].

Глава 3. Организация процедурного кода Важнейшим развитием императивной методологии является методология структурно-императивного программирования, или просто – структурное программирование. И. Одинцов характеризует структурное программирование как подход, заключающийся в задании хорошей топологии императивных программ (см. [Одинцов, 2002]), что подразумевает следование ряду проектных принципов:

Отказ от безусловной передачи управления (или, по крайней мере, ужесточение правил использования).

Функционально-иерархическая декомпозиция.

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

Использование для связи между модулями механизма связывания через параметры и возвращаемые значения процедур и функций.

Независимая компиляция модулей.

Ограничение использования переменных с глобальной областью видимости.

Иерархическое проектирование по принципу «сверху – вниз».

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

3.1. Алгоритмы: определения и способы записи Понятие алгоритма является одним из основополагающих понятий информатики. В начале изучения информатики и основ программирования обычно достаточно руководствоваться определением алгоритма «в интуитивном смысле», например, таким: алгоритм – это однозначно определенная на некотором языке конечная последовательность предписаний (инструкций, команд), задающая порядок исполнения элементарных операций для систематического решения задачи.

Неформальность этого определения связана с тем, что некоторые его элементы, в свою очередь, формально не определены. В рамках нашего курса этого неформального определения вполне достаточно. С формальными моделями алгоритмов и основными положениями теории алгоритмов читатель может ознакомиться в соответствующей литературе, например, в учебном пособии [Карпов, 2003]. Здесь выделим одно важное положение, заключающееся в том что любое разумное определение алгоритма, которое может быть предложено в будущем, окажется эквивалентным уже известным определениям, что означает, по сути, предположение об адекватности понятий алгоритма в интуитивном смысле и алгоритма в точном смысле в одном из существующих эквивалентных формализмов.

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

Систематизируя это описание, В.Ф. Мелехин выделяет 7 параметров, характеризующих алгоритм:

1. Совокупность возможных исходных данных.

2. Совокупность возможных результатов.

3. Совокупность возможных промежуточных результатов.

4. Правило начала процесса обработки данных.

5. Правило непосредственной обработки.

6. Правило окончания обработки.

7. Правило извлечения результата.

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

Существует множество способов записи алгоритма. Рассмотрим наиболее распространенные способы записи алгоритмов, используемые в проектной практике, на примере алгоритма нахождения наибольшего общего делителя двух чисел X и Y.

Текстуальная форма записи 1. Скопировать значение X во вспомогательную переменную x.

2. Скопировать значение Y во вспомогательную переменную y.

3. Если xy, перейти к п. 4, иначе – к п. 7.

4. Если xy, перейти к п. 5, иначе – к п. 6.

5. Записать в x результат вычисления выражения x-y и перейти к п. 3.

6. Записать в y результат вычисления выражения y-x и перейти к п. 3.

7. Конец. x – результат работы.

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

Схема алгоритма Схемы алгоритмов (flowcharts) являются визуальным формализмом, используемым уже в течение десятилетий. Правила изображения схем алгоритмов регулируются различными международными и национальными стандартами, в том числе международным стандартом ISO 5807-85 и российским [ГОСТ 19.701-90]. Схема алгоритма решения задачи поиска наибольшего общего делителя представлена на рис. 3.1.

НОД( X, Y ) Алгоритм поиска наибольшего общего делителя X, Y - исходные данные x := X y := Y да x не равно y да нет xy нет y := y - x x := x - y x - искомое число x Рис. 3.1. Схема алгоритма В монографии [Лекарев, 1997] отмечается, что если для простой задачи схемы алгоритмов обеспечивают безусловную наглядность, то по мере роста сложности программы «его логическая структура начинает «тонуть» в «клубке спагетти», в который постепенно превращается схема алгоритма».

Еще в первом издании книги [Brooks, 1995], вышедшей в свет в 1975 году, Ф. Брукс замечает, что «пошаговая блок-схема является досадным анахронизмом, пригодным только для новичков в алгоритмическом мышлении», и большинство программистов, если это необходимо, рисуют схему алгоритма на основе уже законченной программы.

В статье [Шалыто, Туккель, 2001] авторы утверждают о плохой наглядности и графической избыточности схем алгоритмов даже для простых итерационных алгоритмов. Для полноты картины следует заметить, что ряд специалистов полагает, что плохи не схемы алгоритмов как таковые, а стандарты, в которых, в действительности, не учтены принципы структуризации программ, сформулированные Э. Дейкстрой [Паронджанов, 1999].

Диаграммы Нэсси-Шнейдермана Ряд ученых предлагали различные способы борьбы с логической запутанностью, свойственной схемам алгоритмов и текстуальным описаниям. Один из методов был предложен И. Нэсси и Б. Шнейдерманом в 1973 году [Nassi, Shneiderman, 1973].

Запись алгоритма вычисления наибольшего общего делителя двух положительных чисел представлена на рис. 3.2.

NSD-диаграмма алгоритма Основные элементы NSD НОД( X, Y ) NSD-программа НОД( X, Y ) Title x := X Body y := Y Процесс x не равно y Ветвление x y?

Condition?

True False True False True Clause False Clause x := x - y y := y - x Цикл с Do clause предусловием вывод x Body Рис. 3.2. Диаграммы Нэсси-Шнейдермана Основной недостаток диаграмм Нэсси-Шнейдермана определяется их геометрией: для сложных программ либо внешний прямоугольник будет очень большим, либо внутренние элементы – слишком маленькими.

В заключение отметим, что диаграммы Нэсси-Шнейдермана являются методически очень удобными при обсуждении областей видимости и классов памяти программных объектов (см. разд. 3.4).

Диаграммы Дейкстры В работе по структурному программированию, являющейся частью книги [Dahl, Dijkstra, Hoare, 1972], Э. Дейкстра, по существу, определил альтернативный схемам программ визуальный формализм, который, по не вполне понятным причинам, не был востребован разработчиками стандартов на схемы алгоритмов и программ. Пример записи алгоритма с использованием диаграмм Дейкстры представлен на рис. 3.3.

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

Исходя из того, что соответствующие диаграммы и сами принципы структурного программирования обнаруживаются в одной и той же книге, В.Д. Паронджанов делает вывод об одновременности появления текстовой и визуальной моделей структурного программирования, хотя самого «визуального программирования» как понятия и концепции на тот момент не существовало. Может быть, именно поэтому и «сам отец-основатель, обычно весьма настойчивый в продвижении и популяризации своих идей, отнесся к своему видеоструктурному детищу с удивительным безразличием и ни разу не выступил с предложением о закреплении структурной идеи в стандартах на блок-схемы» [Паронджанов, 1999].

НОД( X, Y ) x := X y := Y x не равно y нет да вывод x xy нет да x := x - y y := y - x Рис. 3.3. Диаграммы Дейкстры Псевдокод Во многих случаях эффективной формой текстуальной записи алгоритма, абстрагированной от конкретного языка, является псевдокод:

НОД( X, Y ) x:=X;

y:=Y;

пока( x y ) повторять если( x y ) то x:=x-y;

иначе y:=y-x;

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

Запись в форме программы на языке программирования Наконец, алгоритм может быть записан в форме законченной программы на некотром языке программирования. Листинг 3. иллюстрирует возможное решение задачи в форме функции на языке C++.

Листинг 3.1. Функция вычисления наибольшего общего делителя int grtCmnDivsor( int X, int Y ) { int x = X;

int y = Y;

while( x != y ) { if( x y ) x = x – y;

else y = y – x;

} return x;

} ДРАКОН-схемы Идея ограничения топологии схем программ с целью их лучшей структуризации и формализации лежит в основе визуального языка программирования ДРАКОН и построенного на его основе шампур-метода как абстрактной визуальной модели программы [Паронджанов, 1999].

Пример простой ДРАКОН-схемы представлен на рис. 3.4.

НОД( X, Y ) Вывод x := X x y := Y нет x не равно y да нет xy да y := y - x x := x - y Вывод Рис. 3.4. ДРАКОН-схема Визуальная модель программирования на базе шампур-метода и языка ДРАКОН вкратце рассматривается в гл. 7.

Р-схемы Р-схемы являются центральным элементом визуального ядра Р-технологии, разработанной в институте кибернетики Академии наук Украины [Вельбицкий, 1984, 1985]. Р-схема программы нахождения наибольшего общего делителя показана на рис. 3.5.

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

Визуальная часть Р-схемы, по существу, эквивалентна расширенной сети переходов (Augmented Transition Network – ATN) с тем отличием, что в основе ATN лежит формализм автомата Мура, в то время как Р-схема основывается на модели автомата Мили.

Структуры Р-схем Основные элементы Базовая структура Вершина Р-схемы условие Дуга действие Специальная Дуга специальная структура Р-схема алгоритма НОД( X, Y ) вывод x x := X y := Y x=y xy x := x - y y := y - x Рис. 3.5. Р-схема Несомненным успехом разработчиков Р-технологии является факт принятия стандарта на Р-схемы [ГОСТ 19.005-85], и использование этой технологии в многочисленных проектах под руководством В.М. Глушкова.

Определенным недостатком Р-схем является тот факт, что все внимание концентрируется на представлении потока управления, потоки данных никак явно не отображаются.

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

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

Конечный автомат и способы его задания В разд. 1.1 было введено понятие преобразователя информации. Пусть преобразователь информации имеет n входов и p выходов, то есть определены вектор входных сигналов X ( x1, x2,..., xn ) и вектор выходных сигналов Y ( y1, y 2,..., y p ). Зависимость, реализуемая преобразователем информации, может быть функциональной или алгоритмической.

Функциональная зависимость имеет место, если значения выходных переменных полностью определяются значениями входных переменных и никак не зависят от предыстории. Реализацией функционального преобразователя информации (его иногда называют автоматом без памяти) является комбинационная схема – сеть логических элементов.

Однако результат преобразования входных сигналов в выходные может зависеть и от предыстории процесса обработки. Такие преобразователи называются автоматами с памятью. В этом случае выходные переменные находятся в алгоритмической зависимости от входных переменных, а для представления предыстории требуются дополнительные переменные Q ( q1, q2,..., qs ), представляющие собой внутренние состояния автомата. Если множество внутренних состояний конечно (на математическом языке это означает, что количество классов эквивалентности входных историй конечно), то такой алгоритмический преобразователь информации и называют конечным автоматом.


Абстрактный конечный автомат с выделенным начальным состоянием определяют как шестерка объектов In, Out, State, S0, FTrans, FOut, где:

In = { Ini }, i=1..n – входной алфавит;

Out = { Outj }, j=1..p – выходной алфавит;

State = { Statek }, k=1..s – множество состояний;

S0 – начальное состояние (элемент множества State);

FTrans : StateInState – функции перехода, устанавливающие зависимость перехода автомата из одного состояния в другое от входного сигнала, например Statek+1=FTrans( Statek, Ini ) – функция перехода из состояния Statek в состояние Statek+1 при входном сигнале Ini;

FOut : StateInOut – функции выхода, устанавливающие зависимость выходного сигнала от текущего состояния и сигнала на входе, например, Outj=FOut( Statek, Ini ) – функция выхода Outj,.формируемого при активации перехода из Statek при входном сигнале Ini.

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

На рис. 3.6 приведен граф переходов конечного автомата, распознающего идентификаторы, разделенные пробельными символами.

Автомат имеет два конечных состояния: «успех», соответствующее символу «конец файла» во входном потоке, и «ошибка», соответствующее ситуации, если во входном потоке обнаружен некорректный символ (не цифра, не буква, не пробел и не «конец файла»).

пробел вывод буква | цифра буква регсл рег S0 NXTLIT NOALP NOALP ERROR EOF EOF вывод рег1 Регистрация первой литеры STOP регсл Регистрация очередной литеры Завершение региcтрации вывод идентификатора и вывод Рис. 3.6. Граф переходов конечного автомата Автомат, определяемый таким образом, называют автоматом Мили.

Существует также модель автомата, называемая автоматом Мура, отличающаяся от автомата Мили тем, что выходная функция зависит только от множества состояний, то есть FOut : StateOut. По вычислительной мощности автомат Мура полностью эквивалентен автомату Мили.

Недостатки и развитие автоматной модели Главным недостатком автоматной модели является отсутствие параметров, в том числе – отсутствие понятия времени. Другими недостатками являются: отсутствие иерархии состояний, обобщения переходов, средств выражения прерываний и продолжения нормальной работы после их обработки. Кроме того, в классической модели не определена семантика взаимодействия конечных автоматов [Карпов, 2003].

Указанных недостатков лишены диаграммы состояний, предложенные Д. Харелом [Harel, 1988], а также SWITCH-технология – подход, основывающийся на программировании реактивных систем в форме сети взаимодействующих автоматов [Шалыто, 1998] (см. гл.7).

Определение конечного автомата, дополненное условием необязательного продвижения по входной цепочке, положено в основу среды разветвленного управления визуального формализма проектирования ПО на базе L-сети, предложенного, теоретически обоснованного и реализованного М.Ф. Лекаревым [Lekarev, 1993], [Лекарев, 1997].

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

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

3.2. Основные управляющие конструкции структурно императивного программирования Основными управляющими конструкциями структурного программирования являются последовательные вычисления (напрямую обусловлено императивной методологией), разветвления и повторяющиеся вычисления (циклы).

Для реализации разветвлений большинство структурно ориентированных языков поддерживают два типа инструкций: условная инструкция (if statement) и мультиветвление (switch statement).

Условная инструкция Условная инструкция (рис. 3.7) позволяет реализовать варианты продолжения исполнения программы в зависимости от истинности или ложности некоторого условия.

Порядок выполнения условной инструкции состоит в следующем:

вычисляется значение условного выражения;

если в двоичном представлении результата есть хотя бы один ненулевой разряд, выполняется ветвь «ДА», иначе (в двоичном представлении все биты – нулевые) – исполняется ветвь «НЕТ».

Если в каком-либо из разветвлений требуется записать несколько инструкций, применяется синтаксис составной инструкции (в C++ это последовательность инструкций, ограниченных фигурными скобками, см.

пример сокращенной формы использования условной инструкции на рис. 3.7).

Условная-инструкция ::= сокращенная форма выражение инструкция- ( ) if полная форма инструкция- else сокращенная полная форма форма False False выражение выражение True True инструкция-1 инструкция-1 инструкция- int x, y;

int x, y;

if( x == y ) if( x y ) x = x - y;

{ else y = y - x;

printf( “%d”, x );

return x;

} Рис. 3.7. Инструкция if Частные случаи В тех случаях, когда в тексте программы хотят подчеркнуть логический характер проверяемого условия, довольно часто встречается следующая форма записи:

if( x ) { // Действия, выполняемые в том случае, когда x!= //...

} Такая запись оправдана, если переменная, используемая в условном выражении, является носителем информации логического характера (особенно в программах, где в качестве логических переменных используются целочисленные переменные), например:

int NameIsFound;

// Признак обнаружения имени в таблице имен:

// 1 – ДА, имя найдено // 0 – НЕТ, имя не найдено //...

if( NameIsFound ) { // Обработка случая, когда имя найдено //...

} Для проверки «отрицательного» условия естественной выглядит следующая форма записи:

if( !NameIsFound ) { // Обработка случая, когда имя не найдено //...

} Отметим, что в большинстве языков для работы с данными логической природы следует использовать булевский тип (язык C является исключением). В C++ эквивалентные фрагменты с использованием булевского типа могут быть записаны следующим образом:

bool NameIsFound;

// Признак обнаружения имени в таблице имен:

// true – ДА, имя найдено // false – НЕТ, имя не найдено //...

if( NameIsFound ) { // Обработка случая, когда имя найдено //...

} if( !NameIsFound ) { // Обработка случая, когда имя не найдено //...

} Многие программисты предпочитают явную запись проверяемого условия с использованием булевских констант:

if( true == NameIsFound ) { // Обработка случая, когда имя найдено //...

} if( false == NameIsFound ) { // Обработка случая, когда имя не найдено //...

} Если же проверяемая величина может принимать множество осмысленных значений (то есть имеет скорее арифметический, а не логический характер), более естественно использовать полную форму записи условного выражения, например:

int NumName;

// Номер имени в таблице:

// 0 – ИМЯ отсутствует в таблице имен // положительное число – уникальный номер //...

if( NumName != 0 ) // Следует предпочесть записи if( NumName ) { // Обработка случая, когда имя есть в таблице //...

} Для программных объектов со сложной внутренней структурой смысл операции ! может быть определен явно. Обычно в этом случае речь идет об определении корректности или некорректности функционирования объекта в некотором специфичном для этого объекта смысле, например:

#include iostream using namespace std void main() { ifstream inputFile;

// Файловый поток для ввода данных // Открыть входной поток inputFile.open( "input.txt" );

// Проверить успешность открытия if( !inputFile ) { // Обработать ошибку в связи с неудачным открытием файла //...

} //...

} Сложные условия и логические операции В некоторых случаях требуется записать условное выражение, соответствующее сложному условию, например, проверить попадание значения в некоторый интервал (рис. 3.8).

if ( (-3) = x = 2) -3 x Либо 0, либо Если записать как в математике, Всегда получается ошибка Рис. 3.8. Сложное условие Как явствует из рисунка, попытка записать сложное условие по правилам математики ведет к семантической ошибке (то есть такой ошибке, которая не может быть установлена транслятором). В действительности, требуется несколько иное, а именно: одновременное выполнение двух простых условий: 3 x и x 2. Таким образом, сложное условие является логической комбинацией простых условий. Логическая связка, используемая в данном случае (одновременное выполнение двух условий) называется логическим «И», или конъюнкцией. В языках C/С++ предусмотрена специальная операция:


if( -3=x && x2 ) { // x находится в требуемом интервале //...

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

if( (-3=x) && (x2) ) Кроме логического «И», в распоряжении программиста на C/C++ имеются также: операция логического «ИЛИ», или дизъюнкция (выполнение хотя бы одного из простых условий), и операция отрицания. Правила вычислений с использованием перечисленных логических операций сведены в табл. 3.1.

Таблица 3.1. Логические операции a b !a a && b a || b false false true false false false true true false true true false false false true true true false true true Следует обратить внимание на важную особенность вычисления логических выражений: вычисления продолжаются только до тех пор, пока нет ясности относительно значения выражения в целом. Поясним сказанное на примере (листинг.3.2):

Листинг 3.2. Порядок выполнения логических операций #include stdio.h void main() { int a = 1, b = 1, c = 1;

if( ++a || ++b || ++ c ) { printf( "a=%d ", a );

printf( "b=%d ", b );

printf( "c=%d\n", c );

} } В результате исполнения этой программы на консоль будет выведен следующий текст:

a=2b=1c= Видно, что значения переменных b и c не изменились!

Вложенные условные инструкции При последовательной проверке нескольких условий иногда приходится прибегать к вложенным условным инструкциям (рис. 3.9).

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

Максимум из трёх чисел нет да ab bc ac нет да нет да max := c max := b max := c max := a int a, b, c;

int max;

if( a b ) if( a c ) max = a;

else max = c;

else if( b c ) max = b;

else max = c;

Рис. 3.9. Вложенные if Конструкция if…else if Во многих практических случаях явное вложение условных инструкций можно заменить особой организацией последовательности выполнения условных инструкций (см. рис. 3.10).

да Усл.1 if нет да Усл.2 else if нет...

......

да Усл.N else if нет else int a, b, c;

int max;

if ( (a b) && (a c) ) max = a;

else if (bc) max = b;

else max = c;

Рис. 3.10. Конструкция if …else if Подобная форма записи позволяет свести код к совокупности последовательно выполняемых проверок, каждой из которых соответствует свой вариант продолжения.

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

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

Синтаксис инструкции while, реализующей семантику такого цикла в языке C++, представлен на рис. 3.11. Там же изображены две равносильные формы графической записи цикла с помощью схемы алгоритма.

Инструкция-while ::= условие инструкция ) ( while условие ::= выражение True False Условие Условие True Инструкция False Тело цикла Инструкция Рис. 3.11. Инструкция while Решение задачи поиска наибольшего общего делителя с использованием цикла while, было приведено в подразделе «Запись алгоритма в форме текста программы» разд. 3.1.

Цикл с постусловием В некоторых случаях условие продолжения цикла удобно проверять не до, а после совершения очередной итерации. Такой цикл называется циклом с постусловием. Синтаксис и семантика цикла с постусловием представлены на рис. 3.12.

Инструкция-do-while ::= инструкция условие ( ) do while Тело цикла Инструкция False Условие True Рис. 3.12. Инструкция do … while Классическим примером использования цикла с постусловием является реализация опроса внешнего устройства или простого пользовательского ввода (листинг 3.3).

Листинг 3.3. Цикл с постусловием int inputValue;

bool entered;

do { printf( "Enter integer value:" );

int ret = scanf( "%d”, &inputValue );

if( ret == 1 ) { entered = true;

} else { printf( "Error. This is not integer value!\n” );

entered = false;

} } while( !entered );

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

Преобразование одной формы записи в другую обычно не составляет труда (см. листинг 3.4).

Листинг 3.4. Преобразование цикла do..while к циклу while int inputValue;

bool entered = false;

while( !entered ) { printf( "Enter integer value:" );

int ret = scanf( "%d”, &inputValue );

if( ret == 1 ) { entered = true;

} else { printf( "Error. This is not integer value!\n” );

} } В заключение следует заметить, что в большинстве языков цикл с постусловием реализован идентично модели C/C++. Исключением является язык Pascal, в котором используется конструкция repeat..until, причем проверяемое условие является условием в ы х о д а из цикла (листинг 3.5).

Листинг 3.5. Реализация цикла с постусловием в языке Pascal var inputValue : integer;

entered : boolean;

repeat write( 'Enter integer value:' );

{$I-} readln( inputValue );

{$I+} if IOResult = 0 then entered = true else begin writeln( 'Error. This is not integer value!' );

entered = false end until( entered );

{ Условие выхода } Цикл с фиксированным числом повторений и фиксированным шагом Иногда число повторений цикла при условии фиксированного шага заранее известно: поднимаясь по лестнице и наступая на каждую ступеньку, мы сделаем столько шагов, сколько ступеней на лестнице.

В таких случаях наиболее адекватным выражением алгоритма является цикл, в котором явно определяются три основных элемента, организующих итерацию – управляющие элементы цикла (рис. 3.13):

1. Начальное состояние: определяется однократно до начала выполнения цикла (обычно соответствует записи начального значения в управляющую переменную цикла).

2. Условие продолжения: проверяется каждый раз перед выполнением очередной итерации (обычно связано с проверкой значения управляющей переменной).

3. Модификация состояния управления циклом (обычно соответствует изменению значения управляющей переменной).

Инициализация Обычно связано с управляющих анализом управляющих данных данных False Условие True Инструкция Модификация управляющих данных Рис. 3.13. Цикл с фиксированным числом повторений В языке C++ семантику цикла с фиксированным числом повторений реализует инструкция for (см. рис. 3.14).

Инструкция-for ::= инициализация ( ;

for условие ;

модификация инструкция ) Рис. 3.14. Инструкция for В заголовке инструкции for помещаются все три управляющих элемента цикла. Цикл for удобно использовать для обработки одномерных массивов:

// Функция поиска значения максимального элемента в одномерном // массиве вещественных чисел double FindMax( double *array, int size ) { double max = array[ 0 ];

for( int ix = 1;

ix size;

ix++ ) { if( array[ ix ] max ) max = array[ ix ];

} return max;

} Дополнительную наглядность записи цикла for обеспечивает запись управляющих выражений в отдельных строчках текста программы:

for( int ix = 1;

// Инициализация переменной цикла ix size;

// Проверка условия продолжения ix++ ) // Модификация переменной цикла { if( array[ ix ] max ) max = array[ ix ];

} Эквивалентная функция, использующая цикл while, позволяет прояснить порядок действий, выполняемых в ходе вычислений:

double FindMax( double *array, int size ) { double max = array [ 0 ];

int ix = 1;

// Инициализация переменной цикла while( ix size ) // Проверка условия продолжения { if( array[ ix ] max ) max = array[ ix ];

ix++;

// Модификация переменной цикла } return max;

} В C++ все элементы заголовка цикла for являются необязательными, в некоторых случаях бывает удобно опускать некоторые из них или даже все (см. ниже подраздел «Цикл с нестандартной структурой»).

while( cond ) for( int ix =0;

do { ix n;

{ f1();

ix++ ans = quest();

cond = f2();

{ } while ( ans!=YES ) } array[ix] = 0;

} Цикл Цикл Цикл ОТВЕТ ix = while ( cond ) ix n Инициализация массива f1() array[ix] = 0 ans = quest() cond = f2() ix++ ans == YES Цикл Цикл 2 Цикл ОТВЕТ Рис. 3.15. Циклы на схемах алгоритмов Современный стандарт на схемы алгоритмов рекомендует использовать для обозначения всех разновидностей циклов унифицированную нотацию (рис. 3.15). Конкретные особенности реализации цикла указываются в соответствующем графическом примитиве или в поле комментария.

Цикл с нестандартной структурой В ряде случаев условие, определяющее необходимость прекращения цикла, удобно проверять не в начале или конце итерации, а где-то посередине (см. рис. 3.16 и листинг 3.6).

Рис. 3.16. Цикл с нестандартной структурой Листинг 3.6. Цикл с нестандартной структурой for( ;

;

) { SendRequest();

GetResponse();

if( ResponseIsBad() ) break;

ProcessResponse();

} Для выхода из цикла в этом случае удобно использовать специальную инструкцию языка – инструкцию break.

Наряду с инструкцией break, C++ содержит также инструкцию continue, позволяющую обойти ряд инструкций в теле цикла (рис. 3.17).

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

Листинг 3.7. Преобразование цикла с нестандартной структурой bool InProgress = true;

while( InProgress ) { SendRequest();

GetResponse();

if( ResponseIsBad() ) { InProgress = false;

continue;

} ProcessResponse();

} Очевидно, что по наглядности эта реализация проигрывают варианту с принятием решения непосредственно в точке проверки «критического»

условия.

Действие break и continue Действие break и continue в цикле for в цикле while Инициализация управляющих данных False False Условие Условие True True break break constinue constinue Другие Другие инструкции инструкции Модификация управляющих данных Рис. 3.17 Инструкции break и continue Рекомендации по использованию break и continue Инструкцию break следует использовать для организации циклов с нестандартной структурой, в том числе и таких циклов, в которых проверяется последовательно сразу несколько условий.

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

Аккуратное применение continue позволяет иногда получать элегантные немногословные решения. В листинге 3.8 представлено определение функции, осуществляющей пропуск комментария в исходном тексте, написанном по правилам языка C.

Листинг 3.8. Функция пропуска комментария в сканируемом тексте на языке C static FILE * fin;

// Файл, содержащий текст для анализа static char litera;

// Очередная читаемая литера /* * Функция пропуска комментария * (вызывается после обнаружения последовательности * "наклонная черта" + "звездочка") * Возвращает true, если конец комментария обнаружен, * false, если обнаружен преждевременный конец файла */ bool PassComment( void ) { // Ищем пару "*/" // Цикл ожидания '*' for( ;

;

) { litera = fgetc( fin );

if( litera == EOF ) return false;

switch( litera ) { case '*' : // Цикл ожидания '/' после '*' while( 1 ) { litera = fgetc( fin );

if( litera == EOF ) return false;

switch( litera ) { case '*': continue;

// Для while( 1 ):

// Продолжить // ожидание '/' case '/': return true;

// Завершить работу default : break;

// Выйти из // ветвления } break;

// Выйти из цикла while( 1 ) } continue;

// Для for( ;

;

) // Продолжить ожидание '*' default : break;

// Выйти из ветвления и...

} //... продолжить ожидание '*' } } Об инструкции switch см. далее в этом разделе.

Циклы, основанные на структурах данных Некоторые языки поддерживают дополнительный вид циклических структур, основанный на структурах данных и используемый для просмотра содержимого наборов объектов (коллекций, списков, множеств и т. п.). В языках Visual Basic, Perl, C# подобный цикл называется цикл foreach (по наименованию соответствующей инструкции языка).

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

Пример использования цикла foreach на языке C# иллюстрирует листинг 3.9.

Листинг 3.9. Цикл для обработки ассоциативного массива в языке C# using System;

using System.Collections;

public class MainClass { public static void Main() { // Хеш-таблица для хранения фамилий преподавателей // и читаемых курсов HashTable courses = new HashTable();

persons.Add("Мелехин","Архитектура вычислительных систем");

persons.Add("Бабко","Теория автоматичиского управления");

persons.Add("Давыдов","Теория и технология программирования");

persons.Add("Павловский","Микропроцессорные системы");

persons.Add("Ицыксон","Технологии компьютерных сетей");

foreach( string teacher in courses ) { Console.Writeline( teacher + " читает " + courses[teacher] );

} } } Подробнее о реализации hash-таблиц и ассоциативных массивов см. в разд. 5.4.

Использование определяемых пользователем итераций играет важную роль в ООП. В С++ итераторы для типов, определенных пользователем, реализуются в виде дружественных функций, вложенных классов или в виде отдельных классов, а соответствующие циклы конструируются на базе стандартных циклов for и while (см. [Пышкин, 2005]).

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

Так, программа, соответствующая алгоритму цикла с нестандартным условием (см. рис. 3.16 и листинг 3.6) в соответствии со строгими требованиями структурного программирования должна быть переписана в следующей форме:

bool InProgress = true;

while( InProgress ) { SendRequest();

GetResponse();

if( ResponseIsBad() ) InProgress = false else { ProcessResponse();

} } Нетрудно убедиться, что в большинстве универсальных алгоритмических языков инструкция goto все-таки присутствует. Замечая, что код, содержащий goto, редко является самым быстрым и самым компактным из возможных реализаций, Д. Кнут, тем не менее, утверждал, что формальное устранение goto может в ряде случаев приводить к громоздкому коду [Knuth, 1974]. По мнению Д. Кнута, использование goto может оказаться оправданным и в хорошо структурированных программах, например, при выходе из нескольких вложенных циклов или при написании процедур, которые распределяют ресурсы, выполняют некоторые операции (с возможными разветвлениями), а затем – освобождают занятые ресурсы.

При этом освобождение ресурсов целесообразно осуществлять в одном месте (рис. 3.18). Безусловно, немотивированного использования goto следует избегать, но не ценой ясности программы.

В ряде языков существуют заменители goto типа рассмотренных выше инструкций break и continue в C/C++ (например, инструкция exit в языках Ada и Modula-2). Б. Керниган указывал на следующие преимущества заместителей по отношению к goto: во-первых, они, как правило, не требуют метки, во-вторых, передают управление в конкретную точку, исключая некорректное или нежелательное использование.

Get Get Release Release Release Release Рис. 3.18. Goto и управление ресурсами В.Д. Паронджанов суммирует варианты организации управляющей структуры в табл. 3.2, которая здесь дополнена ссылками на «авторитеты» и примерами языков, реализующих представленные варианты.

Таблица 3.2. Goto и заместители Апологеты Использование Использование Примеры языков дискуссии goto заместителей Дейкстра нет нет PDL, BLISS Кнут да нет Algol, Fortran Керниган да да C, Ada, Pascal Вирт нет да Modula- Вообще, следует иметь в виду, что в соответствии с теоремой о структурировании, любая программа может быть преобразована к структурированной программе, составленной из элементов базисного множества {последовательность, ветвление, цикл} (строгую формулировку и доказательство см. в. [Linger, Mills, Witt, 1979, §4.4.3]).

Из приведенной теоремы вовсе не следует, что такое преобразование вполне тривиально. В [McConnell S., 1993] приводится пример вполне разумного участка кода, где написать ясный корректный эквивалент, не использующий goto, не так уж просто (листинг 3.10):

Листинг 3.10. Задача на устранение goto if( StatusOK ) { if( DataIsAvailable ) { ImportantVar = x;

goto EXTRA_WORK;

} } else { ImportantVar = GetValue();

EXTRA_WORK:

/* * Большой участок кода *...

*/ } Для этого примера характерна достаточно запутанная логика, и С. МакКоннелл замечает: «если вы думаете, что можете легко переписать этот код без goto, попросите кого-нибудь проверить ваше решение!

Некоторые опытные программисты сделали это неправильно» (разумеется, мы исключаем вариант с дублированием большого участка кода как абсолютно неприемлемый).

В книге «Understanding the Professional Programmer»

Д. Вейнберг пишет, что если вы видите рекламу «опытного программиста», учтите, что обычно имеется в виду однолетний или максимум двухлетний стаж работы.

Однажды, просматривая ряд программ, он обнаружил примеры, свидетельствующие о том, что «опытным программистом»

может оказаться:

1. Некто, не знающий, что такое остаток от деления нацело.

Примечание во избежание упреков: в русской традиции используют также «Вайнберг». Думаю, что это различие сродни различиям в транслитерации фамилий Dijkstra (Дейкстра и Дийкстра), Hoare (Хоор и Хоар) др. Впрочем, главное заключается в том, чтобы было понятно, о ком идет речь!



Pages:     | 1 || 3 | 4 |   ...   | 5 |
 





 
© 2013 www.libed.ru - «Бесплатная библиотека научно-практических конференций»

Материалы этого сайта размещены для ознакомления, все права принадлежат их авторам.
Если Вы не согласны с тем, что Ваш материал размещён на этом сайте, пожалуйста, напишите нам, мы в течении 1-2 рабочих дней удалим его.