- •Глава 1
- •1.2. Процедурные языки
- •1.3. Языки, ориентированные на данные
- •1.4. Объектно-ориентированные языки
- •1.5. Непроцедурные языки
- •1.6. Стандартизация
- •1.7. Архитектура компьютера
- •1.8. Вычислимость
- •1.9. Упражнения
- •Глава 2
- •2.2. Семантика
- •2.3. Данные
- •2.4. Оператор присваивания
- •2.5. Контроль соответствия типов
- •2.7. Подпрограммы
- •2.8. Модули
- •2.9. Упражнения
- •Глава 3
- •3.1. Редактор
- •3.2. Компилятор
- •3.3. Библиотекарь
- •3.4. Компоновщик
- •3.5. Загрузчик
- •3.6. Отладчик
- •3.7. Профилировщик
- •3.8. Средства тестирования
- •3.9. Средства конфигурирования
- •3.10. Интерпретаторы
- •3.11. Упражнения
- •Глава 4
- •4.1. Целочисленные типы
- •I: Integer; -- Целое со знаком в языке Ada
- •4.2. Типы перечисления
- •4.3. Символьный тип
- •4.4. Булев тип
- •4.5. Подтипы
- •4.6. Производные типы
- •4.7. Выражения
- •4.8. Операторы присваивания
- •4.9. Упражнения
- •Глава 5
- •5.1. Записи
- •5.2. Массивы
- •5.3. Массивы и контроль соответствия типов
- •Подтипы массивов в языке Ada
- •5.5. Строковый тип
- •5.6. Многомерные массивы
- •5.7. Реализация массивов
- •5.8. Спецификация представления
- •5.9. Упражнения
- •Глава 6
- •6.1. Операторы switch и case
- •6.2. Условные операторы
- •6.3. Операторы цикла
- •6.4. Цикл for
- •6.5. «Часовые»
- •6.6. Инварианты
- •6.7. Операторы goto
- •6.8. Упражнения
- •Глава 7
- •7.1. Подпрограммы: процедуры и функции
- •7.2. Параметры
- •7.3. Передача параметров подпрограмме
- •7.4. Блочная структура
- •7.5. Рекурсия
- •7.6. Стековая архитектура
- •7.7. Еще о стековой архитектуре
- •7.8. Реализация на процессоре Intel 8086
- •7.9. Упражнения
- •Глава 8
- •8.1 . Указательные типы
- •8.2. Структуры данных
- •8.3. Распределение памяти
- •8.4. Алгоритмы распределения динамической памяти
- •8.5. Упражнения
- •Глава 9
- •9.1. Представление вещественных чисел
- •9.2. Языковая поддержка вещественных чисел
- •9.3. Три смертных греха
- •Вещественные типы в языке Ada
- •9.5. Упражнения
- •Глава 10
- •10.1. Преобразование типов
- •10.2. Перегрузка
- •10.3. Родовые (настраиваемые) сегменты
- •10.4. Вариантные записи
- •10.5. Динамическая диспетчеризация
- •10.6. Упражнения
- •Глава 11
- •11.1. Требования обработки исключительных ситуаций
- •11.2. Исключения в pl/I
- •11.3. Исключения в Ada
- •11.5. Обработка ошибок в языке Eiffei
- •11.6. Упражнения
- •Глава 12
- •12.1. Что такое параллелизм?
- •12.2. Общая память
- •12.3. Проблема взаимных исключений
- •12.4. Мониторы и защищенные переменные
- •12.5. Передача сообщений
- •12.6. Язык параллельного программирования оссаm
- •12.7. Рандеву в языке Ada
- •12.9. Упражнения
- •Глава 13
- •13.1. Раздельная компиляция
- •13.2. Почему необходимы модули?
- •13.3. Пакеты в языке Ada
- •13.4. Абстрактные типы данных в языке Ada
- •13.6. Упражнения
- •Глава 14
- •14.1. Объектно-ориентированное проектирование
- •В каждом объекте должно скрываться одно важное проектное решение.
- •14.3. Наследование
- •14.5. Объектно-ориентированное программирование на языке Ada 95
- •Динамический полиморфизм в языке Ada 95 имеет место, когда фактический параметр относится к cw-типу, а формальный параметр относится к конкретному типу.
- •14.6. Упражнения
- •Глава 15
- •1. Структурированные классы.
- •15.1. Структурированные классы
- •5.2. Доступ к приватным компонентам
- •15.3. Данные класса
- •15.4. Язык программирования Eiffel
- •Если свойство унаследовано от класса предка более чем одним путем, оно используется совместно; в противном случае свойства реплицируются.
- •15.5. Проектные соображения
- •15.6. Методы динамического полиморфизма
- •15.7. Упражнения
- •5Непроцедурные
- •Глава 16
- •16.1. Почему именно функциональное программирование?
- •16.2. Функции
- •16.3. Составные типы
- •16.4. Функции более высокого порядка
- •16.5. Ленивые и жадные вычисления
- •16.6. Исключения
- •16.7. Среда
- •16.8. Упражнения
- •Глава 17
- •17.2. Унификация
- •17.4. Более сложные понятия логического программирования
- •17.5. Упражнения
- •Глава 18
- •18.1. Модель Java
- •18.2. Язык Java
- •18.3. Семантика ссылки
- •18.4. Полиморфные структуры данных
- •18.5. Инкапсуляция
- •18.6. Параллелизм
- •18.7. Библиотеки Java
- •8.8. Упражнения
1.6. Стандартизация
Следует подчеркнуть значение стандартизации. Если для языка существует стандарт, и если компиляторы его поддерживают, то программы можно переносить с одного компьютера на другой. Когда вы пишете пакет программ, который должен выполняться на разных компьютерах, вы должны строго придерживаться стандарта. Иначе задача сопровождения чрезвычайно усложнится, потому что придется следить за десятками или сотнями машинно-зависимых вопросов.
Стандарты существуют (или находятся в стадии подготовки) для большинства языков, обсуждаемых в этой книге. К сожалению, обычно стандарт предлагается спустя годы после широкого распространения языка и должен сохранять машинно-зависимые странности ранних реализаций. Язык Ada — исключение в том смысле, что стандарты (1983 и 1995) создавались и оценивались одновременно с проектом языка и первоначальной реализацией. Более того, стандарт ориентирован на то, чтобы компиляторы можно было сравнивать по производительности и стоимости, а не только на соответствие стандарту. Компиляторы зачастую могут предупреждать вас, если вы использовали нестандартную конструкцию. Если необходимо использовать такие конструкции, их следует сконцентрировать в нескольких хорошо документированных модулях.
1.7. Архитектура компьютера
Поскольку мы рассматриваем языки программирования с точки зрения их практического использования, мы включаем короткий раздел по архитектуре компьютеров, чтобы согласовать минимальный набор терминов. Компьютер состоит из центрального процессора (ЦП) и памяти (рис. 1.1). Устройства ввода-вывода могут рассматриваться как частный случай памяти.
рис. 1 . 1 . Архитектура компьютера.
Все компоненты компьютера обычно подсоединяются к общей шине. Физически шина — это набор разъемов, соединенных параллельно; логически шина — это спецификация сигналов, которые дают возможность компонентам обмениваться данными. Как показано на рисунке, современные компьютеры могут иметь дополнительные прямые соединения между компонентами для повышения производительности (путем специализации интерфейса и расширения узких мест). С точки зрения программного обеспечения единственное различие состоит в скорости, с которой данные могут передаваться Между компонентами.
В ЦП находится набор регистров (специальных ячеек памяти), в которых выполняется вычисление. ЦП может выполнить любую хранящуюся в памяти команду; в ЦП есть указатель команды, который указывает на расположение очередной команды, которая будет выполняться. Команды разделены на Следующие классы.
• Доступ к памяти. Загрузить (load) содержимое слова памяти в регистр и сохранить (store) содержимое регистра в слове памяти.
• Арифметические команды типа сложить (add) и вычесть (sub). Эти действия выполняются над содержимым двух регистров (или иногда над содержимым регистра и содержимым слова памяти). Результат остается в регистре. Например, команда
add m,N R1,N
складывает содержимое слова памяти N с содержимым регистра R1 и оставляет результат в регистре.
• Сравнить и перейти. ЦП может сравнивать два значения, такие как содержимое двух регистров; в зависимости от результата (равно, больше, и т.д.) указатель команды изменяется, переходя к другой команде. Например:
jump_eq R1.L1
…
L1: ...
заставляет ЦП продолжать вычисление с команды с меткой L1, если со-держимое R1 — ноль; в противном случае вычисление продолжается со следующей команды.
Во многих компьютерах, называемых Компьютерами с Сокращенной Системой команд команд (RISC— Reduced Instruction Set Computers), имеются только такие элементарные команды.Обосновывается это тем, что ЦП, который должен выполнять всего несколько простых команд, может быть очень быстрым. В других компьютерах, известных как CISC (Complex Instruction Set Computers), определен Сложный Набор команд, позволяющий упростить как программирование на языке ассемблера, так и конструкцию компилятора. Обсуждение этих двух подходов выходит за рамки этой книги; у них достаточно много общего, так что выбор не будет иметь для нас существенного значения.
Память — это набор ячеек, в которых можно хранить данные. Каждая ячейка памяти, называемая словом памяти, имеет адрес, а каждое слово состоит из фиксированного числа битов, обычно из 16, 32 или 64 битов. Возможно, что Компьютер умеет загружать и сохранять 8-битовые байты или двойные слова из 64 битов.
Важно знать, какие способы адресации могут использоваться в команде. Самый простой способ — непосредственная адресация, при которой операндявляется частью команды. Значением операнда может быть адрес переменной, и в этом случае мы используем
нотацию С:
load R3, # 54 Загрузить значение 54 в R3 load
R2, &N Загрузить адрес N в R2
Следующий способ — это абсолютная адресация, в которой обычно используется символический адрес переменной:
load R3,54 Загрузить содержимое адреса 54
load R4, N Загрузить содержимое переменной N
Современные компьютеры широко используют индексные регистры. Индексные регистры не обязательно обособлены от регистров, используемых для вычислений; важно, что содержимое индексного регистра может использоваться для вычисления адреса операнда команды. Например:
load R3,54(R2) Загрузить содержимое addr(R2) + 54
load R4, (R1) Загрузить содержимое addr(R1) + О
где первая команда означает «загрузить в регистр R3 содержимое слова памяти, чей адрес получен, добавлением 54 к содержимому (индексного) регистра R2»; вторая команда — это частный случай, когда содержимое регистра R1 используется просто как адрес слова памяти, содержимое которого загружается в R4. Индексные регистры необходимы для эффективной реализации циклов и массивов.
Кэш и виртуальная память
Одна из самых трудных проблем, стоящих перед архитекторами компьютеров, — это приведение в соответствие производительности ЦП и пропускной способности памяти. Быстродействие ЦП настолько велико по сравнению со временем доступа к памяти, что память не успевает поставлять данные, чтобы обеспечить непрерывную работу процессора. Для этого есть две причины: 1) в компьютере всего несколько процессоров (обычно один), и в них можно использовать самую быструю, наиболее дорогую технологию, но объем памяти постоянно наращивается и технология должна быть менее дорогая; 2) скорости настолько высоки, что ограничивающим фактором является быстрота, с которой электрический сигнал распространяется по проводам между ЦП и памятью.
Решением проблемы является использование иерархии блоков памяти, как показано на рис. 1.2. Идея состоит в том, чтобы хранить неограниченное количество команд программы и данных в относительно медленной (и недорогой) памяти и загружать порции необходимых команд и данных в меньший объем быстрой (и дорогой) памяти. Если в качестве медленной памяти ис пользуется диск, а в качестве быстрой памяти — обычная оперативная память с произвольным
доступом (RAM — Random Access Memory), то концепция называется виртуальной памятью или страничной памятью. Если медленной памятью является RAM, а быстрой — RAM, реализованная по более быстрой технологии, то концепция называется кэш-памятью.
Обсуждение этих концепций выходит за рамки этой книги, но программист должен понимать потенциальное воздействие кэша или виртуальной памяти на программу, даже если функционирование этих блоков памяти обеспечивается компьютерными аппаратными средствами или операционной системой и полностью невидимо для программиста. Команды и данные передаются между медленной и быстрой памятью блоками, а не отдельными словами. Это означает, что исполнение последовательно расположенных команд без переходов, так же как и обработка, последовательно расположенных данных (например, просмотр элементов массива), должны быть намного эффективнее, чем исполнение групп команд с переходами и обращения к памяти в случайном порядке, что требует интенсивного обмена блоками информации между различными иерархическими уровнями памяти. Если вы пытаетесь улучшать эффективность программы, то следует противиться искушению писать куски на языках низшего уровня или ассемблере; вместо этого попытайтесь реорганизовать вычисление, приняв во внимание влияние кэша и виртуальной памяти. Перестановка операторов языка высокого уровня не воздействует на переносимость программы, хотя, конечно, улучшение эффективности может теряться при перенесении ее на компьютер с иной архитектурой.