Добавил:
ivanov666
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз:
Предмет:
Файл:Основы разработки программного обеспечения на примере языка С. Учебное пособие для СПО
.pdf
детальное описание структуры данных не загромождает
абстракцию;
компоненты скрытого типа недоступны импортеру.
Скрытие структуры данных в разделе описания повышает абстракцию
самого типа - разработчик не знает никаких деталей, для него тип
представлен неким неделимым единым объектом с набором методов
(процедур) для работы с ним.
С другой стороны, недоступность деталей и структуры скрытого типа
не дает возможности работать с ним в обход заранее заданных функций,
так как исключено непосредственное обращение к компонентам типа и
работа с ними (если не предоставлены соответствующие функции).
Такой подход в некотором смысле "защищает" структуры типа, т.е.
никакими действиями разработчик не может нарушить целостность
типа.
В языке Си скрытый тип может быть описан только при помощи
указателя. Название типа помещается в заголовочный файл для
включения в модули-пользователи, а полное описание типа - в сам
модуль (или его собственный h-файл). Конечно, такая схема реализации
не защищает переменную скрытого типа от вмешательства, но при
соблюдении определенных соглашений обеспечивает повышение
уровня безопасности кода.
Важным преимуществом использования скрытых типов, кроме
"защиты" от нарушения целостности, является возможность их
модификации без изменения использующих скрытый тип программ.
Рассмотрим реализацию скрытого типа "человек" в отдельном модуле.
Как уже говорилось, в Си скрытый тип можно реализовать только при
помощи указателя. Работу с указателем можно упростить, определив
нужную структуру и рассмотрев указатель на нее. В этом случае следует
обеспечить корректное выделение памяти только в начале работы с
указателем и освободить память по окончании. Остальные операции не
потребуют дополнительных манипуляций с памятью.
Чтобы задать тип в отдельном модуле, надо создать h-файл и поместить
в него определение типа и заголовки методов работы с типом. Пусть
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
151

"человек" будет хранить информацию о возрасте (другие поля опустим
для упрощения). Тогда h-файл может выглядеть следующим образом:
/* file: person.h */
struct PEROBJ;
typedef struct PEROBJ *PERSON;
void createPerson(PERSON *p);
void setAge(PERSON p, int newAge);
int getAge(PERSON p);
void deletePerson(PERSON *p);
Мы задали тип как PERSON, который является указателем на PEROBJ.
Описание и структура PEROBJ не раскрывается, поэтому из
заголовочного файла нельзя увидеть структуру типа, а значит, из
импортера не удастся работать с ней напрямую. Сразу определить тип
как
typedef struct PERSON;
нельзя, так как в импортере при определении переменной типа
PERSON получится, что есть переменная неизвестного типа, а в языке
Си это запрещено. В приведенном выше примере эта ситуация
обходится путем определения указателя на неизвестный тип. Тогда в
импортере переменная типа PERSON будет указателем и никаких
ошибок не возникает.
В типе PERSON заданы операции создания и удаления. Они
необходимы при выделении памяти под указатель и при ее
высвобождении. Для всех скрытых типов приходится реализовывать
такие операции. Более того, как уже было сказано, их необходимо
обязательно использовать в импортере, иначе возникнет ошибка работы
с указателем, для хранения значений которого не выделена память. К
сожалению, большинство структурных языков программирования
высокого уровня, и Си в том числе, не обладают никакими средствами
помощи и контроля для создания/удаления переменных
пользовательских типов. Это является одним из недостатков скрытых
типов, который решается лишь в объектно-ориентированных языках.
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
152

Две другие приведенные операции - это операции-селекторы: задание
и считывание возраста. Они необходимы, так как доступа к внутренней
структуре нет и единственный способ работы с возрастом - это
специальные методы. В функцию задания возраста переменная p
передается по значению, несмотря на то, что в ней надо изменить
значение объекта p. Дело в том, что сама переменная p уже является
указателем, а изменения происходят в значении, на которое она
указывает. А вот в функции создания переменная p передана по ссылке,
так как при выделении памяти надо изменить само значение указателя
(ссылки) p на новую область памяти.
Реализация типа помещается в с-файл, который может иметь вид:
/* file: person.c */
#include "person.h"
#include <malloc.h>
struct PEROBJ
{
int age;
};
void createPerson(PERSON *p)
{
*p = malloc(sizeof(struct PEROBJ));
}
void setAge(PERSON p, int newAge)
{
(*p).age = newAge;
}
int getAge(PERSON p)
{
return (*p).age;
}
void deletePerson(PERSON *p)
{
free(*p);
}
Здесь приводится описание типа PEROBJ, с которым происходит
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
153

реальная работа и реализация указанных в h-файле функций. Чтобы
работать с полями PERSON, приходится использовать конструкцию
(*p).age, так как тип реализован указателем на структуру.
Для того чтобы работать с реализованным модулем, надо его
импортировать, определить переменную типа PERSON и не забыть
вызывать функцию создания перед работой с этой переменной;
/* file: test.c */
#include <stdio.h>
#include "person.h"
int main()
{
PERSON p1;
createPerson(&p1);
setAge(p1, 23);
printf("Hi, %d\n", getAge(p1));
deletePerson(&p1);
return 0;
}
Саму переменную age из импортирующего модуля изменить нельзя она недоступна. Это позволяет сохранять целостность типа. В
импортере нельзя написать
(*р1).age = -10;
и сделать переменную р1 некорректной.
В рассмотренном примере возраст хранится как количество лет. А что
если понадобится знать дату рождения и уметь рассчитывать возраст в
зависимости от текущего года? Здесь проявляется одно из важнейших
свойств скрытого типа - простота модификации. Чтобы сохранить дату
рождения, можно добавить новые поля - день месяца, месяц и год
рождения. При этом поле age больше будет не нужно и даже вредно,
так как может запутать. Новые изменения приведут к следующему коду.
В h-файл добавим два прототипа новых функций:
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
154

void setBirthDay(PERSON p, int day, int month, int year);
int getBirthDay(PERSON p);
В c-файле изменим реализацию setAge и getAge и добавим две
новых функции:
#include "person.h"
#include <malloc.h>
#include <time.h>
struct PEROBJ
{
int day; /* 1-31 */
int month; /* 1-12 */
int year; /*1800 – 2100 */
};
void createPerson(PERSON *p)
{
*p = malloc(sizeof(struct PEROBJ));
}
void setAge(PERSON p, int newAge)
{
/* get current date in C format */
time_t timer = time(NULL);
/* convert our date to structure */
struct tm *t = localtime(&timer);
/* set current day */
(*p).day = (*t).tm_mday;
/* set current month (C format:0-11, our is:1-12)*/
(*p).month = (*t).tm_mon+1;
/* set birth year */
/*(curent – newAge, */
/*C format for year is: years from 1900) */
(*p).year = (*t).tm_year+1900-newAge;
}
int getAge(PERSON p)
{
/* get current date in C format */
time_t timer = time(NULL);
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
155

/* convert our date to structure */
struct tm *t = localtime(&timer);
return ((*t).tm_year+1900)-(*p).year;
}
void setBirthDay(PERSON p, int day, int month, int year)
{
(*p).day = day;
(*p).month = month;
(*p).year = year;
}
int getBirthYear(PERSON p)
{
return (*p).year;
}
void deletePerson(PERSON *p)
{
free(*p);
}
Интерпретация переменной age изменена, а ранее реализованные
методы setAge и getAge по-прежнему возвращают возраст в годах.
Все ранее написанные программы, которые использовали наш скрытый
тип PERSON, продолжают работать, как и работали, никаких изменений
в них не требуется. А все новые программы могут использовать
дополнительные методы setBirthDay и getBirthYear.
Пример основной программы, работающей с новым модулем:
/* file: test2.c */
#include <stdio.h>
#include "person.h"
int main()
{
PERSON p1;
createPerson(&p1);
setAge(p1, 23);
printf("Hi, %d\n", getAge(p1));
setBirthDay(p1, 23,05,1982);
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
156

printf("Hi, %d\n", getAge(p1));
printf("Hi, %d\n", getBirthYear(p1));
deletePerson(&p1);
}
Такая модификация стала возможной благодаря тому, что сама
переменная age была скрыта от импортера и не могла использоваться.
Сокрытие внутренней структуры привело не только к дополнительной
защите от случайных изменений, но и дало возможность избежать
прямого обращения к переменной age в импортирующих модулях. Это
важнейшее свойство скрытого типа сильно упрощает модификацию его
реализации впоследствии.
При реализации скрытых типов необходимо учитывать некоторые
особенности.
Во-первых, как уже было сказано, импортирующий модуль не должен
иметь непосредственного доступа к компонентам скрытого типа,
поэтому в реализации скрытого типа нужно предусмотреть все
необходимые для работы с типом операции, т.е. обеспечить полноту
операций. Основные классы возможных операций над скрытым типом
аналогичны операциям над абстрактным типом.
Во-вторых, так как скрытый тип описывается при помощи указателей,
модуль, экспортирующий скрытый тип, в дополнение ко всем типам
операций абстрактного типа всегда должен иметь функции создания,
уничтожения и копирования значения объекта.
В ходе выполнения операций над скрытыми типами возможны
ситуации, когда возникают различные ошибки (нехватка памяти и т.д.),
о которых необходимо сообщать импортирующему скрытый тип модулю.
Важность корректного механизма обработки ошибок, кроме всего
прочего, связана с невозможностью проверки результата выполнения
функции, так как ни структура, ни данные скрытого типа напрямую не
доступны.
Часто применяемые механизмы извещений об ошибках - это коды
возврата функций, использование параметра функции для возврата кода/
сообщения об ошибке или изменение значения глобальной переменной
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
157

модуля. Реже используется дополнительная функция, которая при
вызове сообщает, была ли ошибка при выполнении предпоследней
функции (последней выполнялась она сама). Это далеко не полный
перечень подходов к реализации механизма сообщений об ошибках.
Стоит отметить, что модуль, реализующий скрытый тип, в большинстве
случаев не должен выводить ошибки на экран (стандартное устройство
вывода), чтобы не нарушить формат вывода, которым, ве-роятнее всего,
управляет сама головная программа. Вывод на экран значения скрытого
типа, возможно, покажется очевидной функцией модуля реализации
этого скрытого типа, однако в ходе работы с модулем она практически
не востребована.
Чаще всего головная программа самостоятельно управляет выводом, и
чтобы написать процедуру вывода значения переменной скрытого типа,
надо учесть в ней все возможные особенности вывода (шрифт, цвет,
положение и т.д.), которые использует головная программа. Обычно
они заранее неизвестны.
С другой стороны, получение значений переменной скрытого типа в
виде стандартных типов является удобной функцией, позволяющей в
головном модуле написать свою функцию вывода. Здесь надо
учитывать, что возврат значения в конечном итоге должен
осуществляться в переменные стандартных типов и это преобразование
должно обеспечить "полноту" результата. Т.е. если скрытый тип - это
список студенческой группы, то функция, возвращающая в головной
модуль значение переменной такого типа, должна выдать всю
имеющуюся информацию о каждом студенте.
5.6. Уровни абстракции
Абстракция может рассматриваться на разных уровнях. Высшие уровни
абстракции - это представление пользователя о решении задачи,
низшие уровни абстракции - это возможности языка программирования
и его реализации для конкретного типа аппаратуры.
Рассмотрим в качестве примера абстракции автомобиль. При проектировании автомобиль может рассматриваться на более детальном
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
158

уровне, т.е. как кузов, шасси, двигатель, салон. Проектировщики шасси
будут оперировать терминами "передняя подвеска", "задняя подвеска",
"коробка передач", "карданный вал" и т.п. Хорошей абстракцией может
быть подвеска автомобиля. Для разработчика шасси она определяется
типом, набором рычагов, пружинами, амортизаторами, сайлентблоками, стабилизаторами и, конечно, их взаимным расположением. В
этом случае полезной операцией будет расчет изменения положения и
нагрузок при воздействии внешних сил. Такая абстракция полезна лишь
при разработке самой подвески.
На более детальном уровне всю конструкцию подвески можно
представить в виде набора рычагов с заданной длиной, коэффициентом
жесткости и коэффициентом растяжения. Эта абстракция может
использоваться для расчета любой механической конструкции, не
обязательно связанной с автомобилем. Но даже это еще не уровень
языка программирования.
Приведенный пример проектирования сверху вниз может применяться
как при проектировании структур данных, так и при проектировании
алгоритма управления. Проектирование архитектуры программы
начинается с верхних уровней абстракции и постепенно переходит к
нижним уровням.
Не все абстракции стоит всегда реализовывать на всех уровнях. Много
компонент нижних уровней абстракции зачастую уже доступны в
качестве библиотечных модулей. Использование готовых компонент
экономит ресурсы для решения других задач.
Внимание!
Абстрактный тип данных должен проектироваться абстрактно, т.е.
безотносительно к реализации. Только после того, как абстракция
отражена в виде h-файла, можно приступать к самой реализации.
Зачастую реализация будет накладывать ограничения на абстракцию.
Полностью избавиться от учета особенностей реализации не всегда
удается, но к этому надо стремиться. Например, использование
динамической памяти может снимать ограничения на размеры, но
обусловливать дополнительные проблемы при создании переменных.
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
159

Вопросы и задачи для самостоятельного решения
Как используются абстрактные типы данных?
Что необходимо задать, чтобы полностью определить
абстрактный тип данных?
Какие основные классы операций определяют для абстрактного
типа данных.
Определите набор операций и структуру данных для абстрактного
типа данных "дек".
Определите набор операций и структуру данных для абстрактного
типа данных "линейный список".
Определите набор операций и структуру данных для абстрактного
типа данных "множество печатных символов".
Определите набор операций и структуру данных для абстрактного
типа данных "разряженный вектор произвольной длины".
Напишите спецификацию на модуль, реализующий абстрактный
тип данных: "бинарное дерево".
Основы разработки программного обеспечения на примере языка СС.В. Синицын, О.И. Хлытчиев
160
Соседние файлы в предмете [НЕСОРТИРОВАННОЕ]
