Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Основы разработки программного обеспечения на примере языка С. Учебное пособие для СПО

.pdf
Скачиваний:
0
Добавлен:
07.09.2026
Размер:
2 Мб
Скачать
☆
a = a - b; } else { b = b - a; } } if (a!= 0) { return a; } else { return b; } } void shorten(Fraction *f) { int div; div = (int)gcd((long)(*f).numerator, (long)(*f).denominator); (*f).numerator = (*f).numerator / div; (*f).denominator = (*f).denominator / div; } void shortenLong(long num, long denum, long *resNum, long *resDenum) { long div; div = gcd(num, denum); *resNum = num / div; *resDenum = denum / div; } int wasErr() { return wasError; } void printF(Fraction f) { printf("%d/%d",f.numerator, f.denominator); }
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
131
Пример использования:
/****************************************************** date: 10 January 2013 description: string reading and encoding sample ******************************************************/ #include <stdio.h> #include "fraction.h" int main() { Fraction f1, f2, f3; f1 = initF(8,16); f2 = initF(2,1); f3 = addF(f1,f2); printf("f1: "); printF(f1); printf("\n"); printf("f2: "); printF(f2); printf("\n"); printf("f3: "); printF(f3); printf("\n"); printf("f1 compare to f2: %d\n", compareF(f1,f2)); printf("To double: %f\n",convertToDoubleF(f3)); printf("Ended."); return 0; } /* Output: f1: 1/2 f2: 2/1 f3: 1/1 f1 compare to f2: -1 To double: 1.000000 Ended. */
Для сокращения дроби в модуле реализована отдельная функция
shorten, которая не указана в заголовочном файле fraction.h и
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
132
является недоступной импортеру. Она используется только внутри самого модуля.
При выполнении арифметических операций возможна ситуация, что и числитель, и знаменатель не "уместятся" в тип int и произойдет переполнение. При этом переполнение может произойти в ходе промежуточных действий, а сам результат функции может "уместиться" в имеющийся тип. Например, умножение
(MAXINT / 2 ) * (2 / MAXINT)
дает результат 1/ 1, что не выходит за границы int, но в промежуточных вычислениях вполне возможно получение значения
2*MAXINT, не укладывающееся в int (здесь MAXINT - максимальное
значение, которое может храниться в int).
Можно было бы придумать такой алгоритм, в котором сначала находится общий делитель знаменателя и числителя и перед умножением на него выполняется деление. Однако этот подход не сработает в случае сложения, например для случая
(MAXINT / 3) + ((MAXINT + 1) / 3) = (2*MAXINT + 1) / 3
если int занимает 4 байта, MAXINT = 2147483647. Это число не делится на 3, и дробь (MAXINT / 3) является сокращенной. Число 2147483648 (2147483647 + 1) тоже не делится на 3, и дробь ((MAXINT
+ 1) / 3) также является сокращенной. Однако дробь (2*MAXINT + 1) / 3 может быть сокращена, так как (2*MAXINT + 1) =
4294967295 делится на 3. При этом результат деления умещается в int.
Пример реализации, лишенной этой проблемы, может быть следующим:
Fraction addF2(Fraction f1, Fraction f2) { Fraction res; long num, resNum; long denum, resDenum;
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
133
num = (f1.numerator*f2.denominator) + (f2.numerator*f1.denominator); denum = f1.denominator*f2.denominator; shortenLong(num, denum, &resNum, &resDenum); res.numerator = (int) resNum; res.denominator = (int) resDenum; if ((res.numerator == resNum) && (res.denominator == resDenum)) { wasError = 0; } else { wasError = 1; res.numerator = 0; res.denominator = 1; } return res; } void shortenLong (long num, long denum, long *resNum, long *resDenum) { long div; div = gcd(num, denum); *resNum = num / div; *resDenum = denum / div; }
Для сокращения значений увеличенной точности реализована отдельная функция shortenLong.
В модуле определяется переменная wasError, которая после каждой операции выставляется в 0, если операция выполнена корректно (т.е. получен ожидаемый результат), и wasError = 1, если операция не привела к ожидаемому результату или в ходе ее выполнения произошла ошибка. Например, при попытке деления дроби на нулевую дробь
wasError выставляется в 1, а результирующая дробь равна 0/1.
5.4. Проблемы абстрактных типов данных
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
134
Модульное проектирование в совокупности с выделением абстрактных типов данных приводит к централизации управления типом в модуле и, как следствие, к более структурированному коду основной программы. Например, в программной системе требуется реализовать операции со стеком, т.е. с абстрактной структурой типа "список" с ограничением доступа (включение и извлечение) только на одном конце. Иногда такую процедуру обслуживания называют LIFO (Last In First Out).
Можно включить соответствующие функции непосредственно в программный код, можно реализовать отдельным модулем. Рассмотрим сначала реализацию ограниченного (по глубине) стека для хранения отдельных символов. Подобная структура часто используется при разборе арифметических выражений со старшинством операций и скобочной структурой.
Две основные операции PUSH и POP (включить и извлечь), очевидно, могут быть определены как void Push_Stack(char Item) и
void Pop_Stack(char *ltem). Такое определение подчеркивает
для программиста тот факт, что обе процедуры изменяют состояние стека.
Если процедуру извлечения определить как функцию char
Pop_Stack (), то изменение стека (удаление из него верхнего
элемента) скорее придется рассматривать как побочный эффект. Т.е., кроме прямого результата - вычисления значения, функция производит еще что- то, возможно, влияющее на ее глобальное окружение. С другой стороны, для вспомогательной функции просмотра элемента вершины стека форма char Check_Top () достаточно очевидна, так как измене-ние состояния стека при ее выполнении не происходит.
Полезно определить пару функций проверки (вычисления) предусловий для операций со стеком. int Is_Stack_Empty() - функция проверки пустого стека, int Is_Stack_Full() - функция проверки полностью заполненного стека.
Is_Stack_Empty () будет являться средством проверки
возможности извлечь элемент из стека или обратиться к его верхнему элементу. Истинность этой функции запрещает применение операций
Check_Top и Pop_Stack. В свою очередь, истинность функции
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
135
Is_Stack_Full не разрешает применять операцию включения Push_Stack.
Проводя анализ введенных пяти операций рассматриваемого абстрактного типа, можно сделать вывод, что мы имеем операции­конструкторы: Push_Stack, Pop_Stack, которые можно рассматривать одновременно и как селекторы. "Чистый" селектор -
Check_Top. Но и его можно еще рассматривать как преобразователь
типа "стек" в "символ", что будет не совсем верно, так как сам стек не является в явном виде параметром операции. Кроме того, имеются типичные операции проверки предусловий Is_Stack_Empty и
Is_Stack_Full.
Чего же у нас нет? Нет операций ввода/вывода - они реализуются с элементами стека стандартными средствами обработки символьных значений. И главное - нет начального конструктора, который задал бы исходное состояние пустого стека. Нет и операций копирования.
Решение об отсутствии всех этих типов операций принято сознательно. Наш стек уникален (можно использовать только один-един- ственный стек, поддерживаемый модулем), и модуль отвечает за его исходное состояние. При этом есть большой соблазн ввести операцию по очистке стека (после нее функция Is_Stack_Empty становится истинной). Ее можно рассматривать как начальный конструктор, так как ей безразлично исходное состояние списка. Но воздержимся от такого решения. Всегда можно воспользоваться операцией извлечения элемента, применяя ее до тех пор, пока стек не опустеет.
По итогам наших рассуждений h-файл для модуля Char_Stack
(char_stack.h) выглядит так:
----- файл char_stack.h ----­void Push_Stack(char Item); /*==================================================== Операция записи символа в стек. Предусловие Is_Stack_Full() == 0 Если предусловие нарушено, состояние стека не изменяется. =====================================================*/
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
136
void Pop_Stack(char *Item); /*==================================================== Операция извлечения символа из стека. Предусловие Is_Stack_Emty() == 0 Если предусловие нарушено, состояние параметра не изменяется. =====================================================*/ char Check_Top(); /*==================================================== Операция проверки символа, находящегося в вершине стека. Предусловие Is_Stack_Emty() == 0 Если предусловие нарушено, возвращается символ с кодом '/0'. =====================================================*/ int Is_Stack_Empty(); /*==================================================== Операция проверки пустого стека. Is_Stack_Emty() == 1, если в стеке нет ни одного элемента. =====================================================*/ int Is_Stack_Full(); /*==================================================== Операция проверки пустого стека. Is_Stack_Full() == 1, если стек заполнен полностью. =====================================================*/
Программный код самого модуля char_stack.c может выглядеть следующим образом:
----- файл char_stack.c ----­#include "char_stack.h" define MAX_ITEM_NUMBER 20 char Stack[MAX_ITEM_NUMBER]; int Top = 0; /* Изначально стек пуст */ void Push_Stack(char Item); /*==================================================== Операция записи символа в стек. =====================================================*/ {
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
137
if (Is_Stack_Full() == 0) /* Если стек не полон */ { /* Включить элемент в стек */ Stack[Top++] = Item; } } void Pop_Stack(char *Item); /*==================================================== Операция извлечения символа из стека. =====================================================*/ { if (Is_Stack_Emty() == 0) /* Если стек не пуст */ { /* Извлечь элемент из стека */ Item* = Stack[--Top]; } } char Check_Top(); /*==================================================== Операция копирования символа из вершины стека. =====================================================*/ { if (Is_Stack_Emty() == 0) /* Если стек не пуст */ { /* Копировать вершину стека */ return( Stack[Top - 1]); } else /* Если стек пуст */ { /* Вернуть символ с нулевым кодом */ return( '\0' ); } } int Is_Stack_Empty(); /*==================================================== Операция проверки пустого стека. =====================================================*/ { return ( Top == 0 ); } int Is_Stack_Full(); /*==================================================== Операция проверки пустого стека.
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
138
=====================================================*/ { return ( Top == MAX_ITEM_NUMBER ); }
Приведенный пример кода выглядит более понятным, нежели реализация стека прямо по ходу программы. Обратим внимание на ряд деталей.
Наличие определений функций предусловий в заголовочном файле, который мы включили в модуль, обеспечивает не только согласованность типов параметров, но и возможность обращения к функциям проверки предусловий Is_Stack_Emty() и
Is_Stack_Full () до их объявления в модуле.
Определение размера стека через константу MAX_ITEM_NUMBER позволяет упростить при необходимости изменение его размера. А применение операций Top++ при записи в стек и --Top при извлечении верхнего элемента обеспечивает компактность записи без потери наглядности.
Локализация в модуле, поддерживающем абстрактный тип всех тонкостей его реализации, освобождает пользователя, импортирующего АТД, от ненужных ему подробностей. Однако вводимые пользователем типы имеют определенные отличия от встроенных в язык типов.
Рассмотренный выше тип "стек" представлял собой статический тип данных. Он был единственным и представлялся самим модулем
Char_Stack. Мы не могли с его помощью создать еще один стек,
например для хранения операндов выражения. Способом реализации типа "стек" с более гибкой структурой может быть стек, реализованный на списке с указателями.
Создание стека - установка указателя на его головной элемент. Пустому стеку соответствует значение указателя NILL. Включение нового элемента - создание структуры: значение, указатель на следующий; копирование включаемого значения в созданный элемент и подключение нового элемента в качестве вершины стека.
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
139
Что является отличительной чертой такого способа реализации АТД?
1. Тип объявляется в модуле-импортере как указатель (как правило, на структуру).
2. Память переменной АТД (указателя) распределена в модуле­импортере.
3. Начальное состояние переменной АТД никак не определено. Обязательно нужен начальный конструктор. До его применения невозможно определение предусловий.
4. Как правило, размещение новых значений в подобном АТД связано с обращением к модулю управления динамической памятью, что, с одной стороны, увеличивает гибкость, снимая статические ограничения на размер структуры, но, с другой стороны, создает опасность динамического выхода за границы допустимой памяти.
Заметим, конкретно для стека возможна и смешанная стратегия реализации. Суть ее в том, что при создании стека отводится память сразу подо все его будущие элементы, так сказать, по максимуму. Это приводит к его первичной инициализации (состояние "пустой") и дает возможность легко следить за переполнением (мы знаем и храним максимально возможное количество элементов).
Для структур такого типа самая опасная ошибка - пропуск операции создания переменной АТД. Дело в том, что модуль реализации абстрактного типа в большинстве случаев может сам определять механизмы для создания переменных своего типа. Для каждого нового типа будет своя новая, отличная от других, операция создания. При этом компилятор не может следить за правильностью ее использования.
Объявив в коде две переменных Stack_1 и Stack_2 типа "стек", пользователь может забыть создать переменную Stack_2. При компиляции эта ошибка не будет обнаружена. Аналогичная ситуация складывается для операций удаления переменных абстрактного типа потому, что если абстрактный тип использует динамическую память, то автоматический "сборщик мусора" не сможет освободить выделенную память за пользователя, который сам не сделал это явно (например, забыл удалить Stack_1).
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
140
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]