Опубликован: 23.10.2005 | Доступ: свободный | Студентов: 4086 / 201 | Оценка: 4.44 / 4.19 | Длительность: 33:04:00
Специальности: Программист
Лекция 16:

Эмуляция объектной технологии в не ОО-средах

< Лекция 15 || Лекция 16: 1234 || Лекция 17 >

ОО-программирование и язык C

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

Немного контекста

Язык C создавался в AT&T's Bell Laboratories как машинонезависимый язык для написания операционных систем. Первая версия ОС Unix была написана на языке ассемблера, но вскоре потребовалась ее переносимая версия. Для решения этой задачи в 1970 г. и был создан язык С. Он вырос из идей языка BCPL. Как и С, это язык высокого уровня (благодаря управляющим структурам, похожим на структуры в Algol или Pascal), машинно-ориентированный (из-за возможности манипулировать данными на самом низком уровне через адреса, указатели и байты) и переносимый (поскольку машинно-ориентированные концепции охватывают широкий круг типов компьютеров). Язык С появился вовремя. В конце 70-х операционная система Unix использовалась во многих университетах, и вместе с ней распространялся С. В 80-х гг. началась революция микрокомпьютеров, и С был готов служить ей как lingua franca (язык франков), поскольку был более масштабируемым, чем Basic, и более гибким, чем Pascal. Система Unix тоже была коммерчески успешна, и с ней р ядом шел С. Через несколько лет он стал доминирующим языком в больших и самых активных сегментах компьютерной индустрии.

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

  • Язык С покончил с состоянием закостенелости, существовавшим в мире языков программирования приблизительно до 1980 г. В то время никто не хотел слышать (особенно после коммерческой неудачи Algol) ни о чем другом, кроме священной тройки: Fortran в науке, Cobol в бизнесе и PL/I в сфере могущества IBM. Вне академических кругов любые попытки предложить другие решения встречались как предложения еще одного сорта Колы. Язык С разрушил это состояние, дал возможность думать о языке программирования как о чем-то, выбираемом из большого каталога. (Несколько лет спустя сам С настолько укрепил позиции, что в некоторых кругах выбор свелся к нему одному, но такова судьба многих успешных ниспровергателей.)
  • Переносимость языка С и его близость к машине делали его привлекательным в качестве языка написания компиляторов. Этот подход использовался при создании компиляторов для языков C++ и Objective-C. За ними последовали компиляторы для других языков. Преимущества для создателей компиляторов и их пользователей очевидны: переносимость (компиляторы С есть почти для любой компьютерной архитектуры), эффективность (оптимизация, реализуемая хорошим компилятором), и простота интеграции с универсальными инструментами и компонентами, основанными на С.

Стандарт ANSI для С впервые был опубликован в 1990 году. Более ранняя версия языка известна как K&R (по инициалам авторов первой книги по С - Кернигана и Ритчи). Со временем противоречие между двумя взглядами на С - как на язык программирования высокого уровня и переносимый язык ассемблера - стало более явным. Эволюция стандарта сделала язык более типизированным и, следовательно, менее удобным для использования в качестве целевого кода компилятора. Было даже объявлено, что следующие версии будут иметь понятие класса, сглаживая отличие от C++ и Java.

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

Основные положения

Дисциплинарный подход применим к языку С, как и к любому другому языку. За его пределами для реализации модульности можно использовать понятие файла. Файл - это понятие языка С, балансирующее на границе между языком и операционной системой. Файл - единица компиляции, он может содержать функции и данные. Некоторые функции могут быть скрытыми от других файлов, другие - общедоступны. Это прямой путь к инкапсуляции: файл может содержать все элементы, относящиеся к реализации одного или более абстрактных объектов, или абстрактного типа данных. Благодаря понятию файла, С достигает уровня инкапсулирующего языка, как Ada или Modula-2. В сравнении с Ada здесь нет универсальности и отличия между интерфейсом и и реализацией.

Обычная техника программирования на С не расположена к ОО-принципам. Большинство программ С используют "файлы заголовков", описывающих разделяемые данные. Любой файл, нуждающийся в данных, получает доступ к ним через директиву " include " (управляемую встроенным препроцессором С):

#include <header.h>

где header.h - это имя файла заголовка ( h - обычный суффикс для таких имен файлов). Это эквивалентно копированию файла заголовка в точке появления директивы. В результате, традиция С, если не сам язык, дает возможность модулям клиента получить доступ к структурам данных через их физические представления, что явно противоречит принципам скрытия информации и абстракции данных. Однако возможно использовать файлы заголовка более дисциплинированным путем, скорее насаждая, а не нарушая абстракцию данных. Они могут даже помочь продвинуться к определению модулей интерфейса в стиле, обсуждаемом для языка Ada в предыдущей лекции.

Эмуляция объектов

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

С внешней точки зрения "каждый объект имеет доступ к операциям, применимым к нему". Возможно, это немного наивно, но не является концептуально неверным. Язык С буквально поддерживает это понятие! Экземпляр "структуры" языка С (эквивалент записи в Pascal) может содержать среди своих полей указатели на функции.

Объект С со ссылками на функцию

Рис. 16.1. Объект С со ссылками на функцию

Например, структурный тип REAL_STACK можно объявить так:

typedef struct
{
/* Экспортируемые компоненты */
void (*remove) ();
void (*put) ();
float (*item) ();
BOOL (*empty) ();
/* Закрытые компоненты (реализация) */
int count;
float representation [MAXSIZE];
}
REAL_STACK;

Фигурные скобки {...} ограничивают компоненты структуры; float задает вещественный тип; процедуры объявляются как функции с типом результата void ; комментарии берутся в скобки /* и *?/. Важный символ *? служит для разыменования указателей. В практике программирования на С, чтобы все работало, принято добавлять достаточное количеств указателей, если это не помогает, то всегда можно попробовать добавить один или парочку символов &. Если и это не дает результата, всегда найдется кто-нибудь, кто сможет помочь.

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

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

my_stack.remove = C_remove

В эмулируемом классе remove не имеет необходимого для нее аргумента. Для доступа к соответствующему стеку следует объявить функцию C_remove так:

C_remove (s)
REAL_STACK s;
    {
... Реализация операции remove ...
    }

Тогда клиент сможет применить remove к стеку my_stack:

my_stack.remove (my_stack)

В общем случае, подпрограмма rout, имеющая n аргументов в эмулируемом классе, порождает функцию C_rout с n+1 аргументами. Вызов ОО-подпрограммы:

x.rout (arg1, arg2, ..., argn)

эмулируется как:

x.C_rout (x, arg1, arg2, ..., argn)

Эмулирующие классы

Описанная техника будет работать в определенных пределах. Ее даже можно расширить для эмуляции наследования.

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

Для снижения потерь заметим, что подпрограммы одинаковы для всех экземпляров класса. Поэтому для каждого класса можно ввести структуру данных периода выполнения, дескриптор класса, содержащий ссылки на подпрограммы. Его можно реализовать как связный список или массив. Требования к пространству значительно уменьшаются: вместо n*m указателей можно иметь их n+m, где n - число подпрограмм, а m - число объектов, как показано на рис. 16.2.

Объекты C, разделяющие дескриптор класса

Рис. 16.2. Объекты C, разделяющие дескриптор класса

Это приводит к незначительным временным потерям, но экономия пространства и простота стоят этого.

В этой технике нет секрета. Именно она сделала С полезным в качестве средства реализации для компиляторов ОО-языков, начиная с Objective-C и C++ в начале 80-х. Способность использовать указатели функций, в сочетании с идеей группирования этих указателей в дескриптор класса, разделяемый произвольным числом экземпляров, служит первым шагом к реализации ОО-техники.

Конечно, это только первый шаг, и нужно еще найти способы реализации наследования (особенно непросто множественное наследование), универсальности, исключений, утверждений и динамического связывания. Объяснения потребовали бы отдельной книги. Отметим лишь одно важное свойство, выводимое из всего, что мы видели до сих пор. Реализация динамического связывания требует доступа к типу каждого объекта во время выполнения для нахождения нужного варианта компонента f в динамически связываемом вызове x.f (...) (написанном здесь в ОО-нотации). Другими словами: в дополнение к официальным полям объекту необходимо дополнительное внутреннее поле, порождаемое компилятором и указывающее на тип объекта. Описанный подход показывает возможную реализацию такого поля - как указателя на дескриптор класса. По этой причине на рис. 16.2 для такого поля используется ярлык type.

OO C: оценка

Обсуждение показало, что в С есть технические способы введения ОО-идей. Но это еще не значит, что программисты должны их использовать. Как и в случае с языком Fortran, эмуляция - это некоторое насилие над языком. Сила языка С - в его доступности как "структурного языка ассемблера" (последователя BCPL и PL/360, созданного Виртом), переносимого, разумно простого и эффективно интерпретируемого. Его базисные понятия далеки от ОО-проектирования.

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

< Лекция 15 || Лекция 16: 1234 || Лекция 17 >