
- •Глава 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. Упражнения
5.3. Массивы и контроль соответствия типов
Возможно, наиболее общая причина труднообнаруживаемых ошибок — это индексация, которая выходит за границы массива:
inta[10],
C |
i<= 10; i
a[i] = 2*i;
Цикл будет выполнен и для i = 10, но последним элементом массива является а[9].
Причина распространенности этого типа ошибки в том, что индексные выражения могут быть произвольными, хотя допустимы только индексы, попадающие в диапазон, заданный в объявлении массива. Самая простая ошибка может привести к тому, что индекс получит значение, которое выходит за этот диапазон. Серьезность возникающей ошибки в том, что присваивание a[i] (если i выходит за допустимый диапазон) вызывает изменение некоторой случайной ячейки памяти, возможно, даже в области операционной системы. Даже если аппаратная защита допускает изменение данных только в области вашей собственной программы, ошибку будет трудно найти, так как она проявится в другом месте, а именно в командах, которые используют измененную память.
Рассмотрим случай, когда числовая ошибка заставляет переменную speed получить значение 20 вместо 30:
C |
speed = (х+у)/3; /*Вычислить среднее! */
Проявлением ошибки является неправильное значение speed, и причина (деление на 3 вместо 2) находится здесь же, в команде, которая вычисляет speed. Это проявление непосредственно связано с ошибкой и, используя контрольные точки или точки наблюдения, можно быстро локализовать ошибку. В следующем примере:
inta[10];
C |
for(i = 0;i<= 10; i ++)
a[i] = 2*j;
переменная speed является жертвой того факта, что она была чисто случайно объявлена как раз после а и, таким образом, была изменена совершенно посторонней командой. Вы можете днями прослеживать вычисление speed и не найти ошибку.
Решение подобных проблем состоит в проверке операции индексации над массивами с тем, чтобы гарантировать соблюдение границ. Любая попытка превысить границы массива рассматривается как нарушение контроля соответствия типов. Впервые проверка индексов была предложена в языке Pascal:
pascal |
type A_Type = array[0..9] of Integer;
A: A_Type;
A[10]:=20; (*Ошибка*)
При контроле соответствия типов ошибка обнаруживается сразу же, на своем месте, а не после того, как она «затерла» некоторую «постороннюю» память; целый класс серьезных ошибок исчезает из программ. Точнее, такие ошибки становятся ошибками этапа компиляции, а не ошибками этапа выполнения программы.
Конечно, ничего не дается просто так, и существуют две проблемы контроля соответствия типов для массивов. Первая — увеличение времени выполнения, которое является ценой проверок (мы обсудим это в одном из следующих разделов). Вторая проблема — это противоречие между способом, которым мы работаем с массивами, и способом работы контроля соответствия типов. Рассмотрим следующий пример:
pascal |
type B_Type= array[0..8] of Real;
А: А_Туре: (* Переменные-массивы *)
В: В_Туре;
procedure Sort(var P: А_Туре); (* Параметр-массив *)
sort(A); (* Правильно*) sort(B); (* Ошибка! *)
Два объявления типов определяют два различных типа. Тип фактического параметра процедуры должен соответствовать типу формального параметра, поэтому кажется, что необходимы две разные процедуры Sort, каждая для своего типа. Это не соответствует нашему интуитивному понятию массива и операций над массивом, потому что при тщательном программировании процедур, аналогичных Sort, их делают не зависящими от числа элементов в массиве; границы массива должны быть просто дополнительными параметрами. Обратите внимание, что эта проблема не возникает в языках Fortran или С потому, что в них нет параметров-массивов! Они просто передают адрес начала массива, а программист отвечает за правильное определение и использование границ массива.
В языке Ada изящно решена эта проблема. Тип массива в Ada определяется исключительно сигнатурой, т. е. типом индекса и типом элемента. Такой тип называется типом массива без ограничений. Чтобы фактически объявить массив, необходимо добавить к типу ограничение индекса:
Ada |
type A_Type is array(lnteger range о) of Float;
-- Объявление типа массива без ограничений
А: А_Туре(0..9); — Массив с ограничением индекса
В: А_Туре(0..8); — Массив с ограничением индекса
Сигнатура А_Туре — одномерный массив с индексами типа integer и компонентами типа Float; границы индексов не являются частью сигнатуры.
Как и в языке Pascal, операции индексации полностью контролируются:
Ada |
В(9) := 20.5; -- Ошибка, индекс изменяется в пределах 0..8
Важность неограниченных массивов становится очевидной, когда мы рассматриваем параметры процедуры. Так как тип (неограниченного) массива-параметра определяется только сигнатурой, мы можем вызывать процедуру с любым фактическим параметром этого типа независимо от индексного ограничения:
Ada |
— Тип параметра: неограниченный массив
Sort(A); -- Типом А является А_Туре
Sort(B); -- Типом В также является А_Туре
Теперь возникает вопрос: как процедура Sort может получить доступ к границам массива? В языке Pascal границы были частью типа и таким образом были известны внутри процедуры. В языке Ada ограничения фактического параметра-массива автоматически передаются процедуре во время выполнения и могут быть получены через функции, называемые атрибутами. Если А произвольный массив, то:
• A'First — индекс первого элемента А.
• A'Last — индекс последнего элемента А.
• A'Length — число элементов в А.
• A'Range — эквивалент A'First.. A'Last.
Например:
Ada |
for I in P'Range loop
for J in 1+1 .. P'Lastloop
end Sort;
Использование атрибутов массива позволяет программисту писать чрезвычайно устойчивое к изменениям программное обеспечение: любое изменение границ массива автоматически отражается в атрибутах.
Подводя итог, можно сказать: контроль соответствия типов для массивов — мощный инструмент для улучшения надежности программ; однако определение границ массива не должно быть частью статического определения типа.