Абстракция данных в языке С++
Б.Страуструп
(*1)
С++ является
надмножеством языка С. Он полностью реализован и уже
использовался в ряде нетривиальных проектов. В настоящее время С++
применяется в более чем сотне мест. Данная статья описывает средства
абстракции данных предоставляемые языком С++. Они включают классы
подобные языкам Simula и обеспечивающие :
- необязательное упрятывание информации,
- необязательную гарантированную инициализацию структур данных,
- необязательное неявное преобразование типов, определяемых
пользователем, и необязательную динамическую типизацию,
- механизм перегрузки имен функций и знаков операций,
- механизм управления памятью, определяемого пользователем.
Показано как могут быть реализованы новые типы данных, например
комплексные числа, как может быть структурирован "объектно-
ориентированный" графический пакет.
Программа, использующая средствa абстракции данных по меньшей
мере
столь же эффективна как и эквивалентная программа не использующая их,
компилятор же более быстрый чем старые С-компиляторы.
1. ВВЕДЕНИЕ.
В данной
статье ставится цель показать как писать программы на С++,
используя "абстракцию данных" (*2). В статье предлагается обсуждение
каждой новой особенности языкa для помощи читателю в осознании места,
занимаемого ею в общем проекте языка техники программированияб которую
она предназначена поддерживать, ошибок и издержек, избежать которых
она предназначенa помочь программисту. Однако поскольку данная статья
не является руководством она не дает полной детализации примитивов
языка; последнее может быть найдено в [3].
________________________________________________________________________
*1 Bjarnх Stroustrup Cand.Scient (Mathematic and Computer Scitnct) 1975
Universit. Aarch Denmark. Ph.D (Computer Science). 1979
Cambrigх University. AT&T Bell Laboratorie 1979
- иследовательские интересы включают : распределенные вычислительные
системы, моделирование, методология программирования, языки
программирования. В настоящее время является членом Computer
Scienc Researc Center, член АСМа и IEEE.
(Рукопись получена 5 августа 1983 г.)
*2 Замечание по
названию С++ : ++ - знак операции инкремента в С, если
этa операция применяется к переменной (обычно к индексу массива
или
указателю) значение переменной увеличивается так, чтобы указывать
на
следующий элемент. Название С++ было предложен Риком Маскитти
(Rici
Mascitti). С++ в дальнейшем следует рассматривать как уточняющее
название, употребляемое либо по формальному поводу, либо во
избежании двусмысленности. Среди посвященных С++ именуется С,
а
язык С, описанный в [1], "старым С". Более короткое обозначение
С
является синтаксической ошибкой. Оно также использовалось как
название одного, не относящегося к теме статьи, языка. Ценители
семантики С найдут, что С++ менее выразительном чем ++С, однако
последнее не является приемлемым названием. Язык не назван D,
так
как является расширением С, а не попыткой решить некие проблемы
изначально присущие базовой структуре языкa С. Название С++ отражает
эволюционный характер изменений относительно старого С. Еще одну
интерпретацию названия С++.(см. [2], Приложение.)
Эволюционизируя из языка С [1] C++ прошел через несколько
промежуточных этапов известных под общим названием "С с
классами"
[4,5]. Первоначальное Simula6 [6,7].
Основным намерением являлось создание возможностей
абстракции при
проектировании больших систем и в то же время полезных
в областях,
где очень важны лаконичность С и его способность выражать
низкоуровневые детали. В связи с этим было специально предусмотрено
чтобы использование классов С, предоставляющих общий и
гибкий
механизм структурирования не приводило в сравнении со старым
С
к издержкам ни по памяти ни по времени исполнения.
Зa исключением таких деталей как введение новых
ключевых слов,
С++ является надмножеством языка С. см. ниже #2."Реализация
и
совместимость". Язык полностью реализован и используется.
Десятки
тысяч строк код написаны и отлажены десятками программистов.
Статья разделяется на три основные части :
1.
Краткое представление идеи абстракции данных.
2.
Полное описание возможностей, предоставляемых для реализаци этой
идеи, при помощи небольших примеров. Это описание
само по себе
распадается на три секции :
a. Базовая техника для упрятывания информации ; доступ к данным,
размещения и инициализации. Классы,
составляющие функции
класса, конструкторы, перегрузки имен
функций представлены
начиная с #3. "Разграничение доступа
к данным".
b. Механизмы и техника создания новых типов с связанными
операциями. Перегрузка знаков операций,
преобразование типов,
определяемых пользователем, ссылки
и операции управления
свободной памятью, представлены начиная
с #8. " Перегрузка
знаков операций и преобразование типов".
c. Механизмы для создания иерархии абстракций для динамической
типизации объектов и для создания
полиморфных классов и функций.
Производные классы и виртуальные функции
представлены, начиная
с #14."Производные классы".
Пункты b и c не зависят непосредственно друг от друга.
3.
В заключение несколько общих замечаний по технике программирова-
ния, реализации языка, эффективности, совместимости
со старым С и
сравнение с другими языками, начиная с #18. "Ввод
и вывод".
Несколько секций помечено словом "отступление"; они содержат
информацию хотя и важную для программиста и представляющую
вероятно
интерес для широкого читателя, но не относящуюся напрямую
к
абстракции данных.
2. АБСТРАКЦИЯ ДАННЫХ.
"Абстракция данных" - популярная и в общем неверно определяемая техника
программирования. Фундаментальная идея состоит в разделении несущественных
деталей реализации подпрограммы и характеристик существенных для
для
корректного ее использования.
Такое разделение может быть выражено через специальный
"интерфейс",
сосредотачивающий описание всех возможных применений программы.
Типичный пример такого интерфейса :
- множество функций, которые могут иметь доступ к структурам
данных, посредством которых представлена "абстракция".
Одна из причин неполноты общепринятого определения состоит в том,
что
любая языковая конструкция, поддерживающая абстракцию данных будет
выражать
только некоторые аспекты фундаментальной идеи при недостаточности
выражения других. Например:
1. Упрятывание интерфейсов - Возможность спецификации
интерфейсов,
препятствующих искажению данных и освобождающих пользователя от
необходимости знать детали реализации.
2. Конструирование интерфейсов - Возможность спецификации
интерфейсов, поддерживающих и навязывающих определенные соглашения
по использованию абстракций.
Примеры включают перегрузку знаков операций и динамическую
типизацию.
3. Конкретизация - Возможность создания и инициализации
одного или более "экземпляров" (переменных объектов копий версий)
одной абстракции.
4. Локализация - Возможность упрощения реализации
абстракции, принимая
во внимание, что весь доступ к ней направляется через ее интерфейс.
Примеры включают упрощение правил видимости и соглашений
по вызову внутри
реализации.
5. Программная среда - Возможность поддержки разработки
программм,
использующих абстракции. Примеры включают : загрузчики, понимающие
абстракции; библиотеки абстракций; отладчики, позволяющие программисту
работать в терминах абстракции.
6 Эффективность - Некоторая конструкция языка должна
быть
"достаточна эффективна" для того, чтобы быть полезной.
Предполагаемая
сфера применения - важный фактор для определения,
какие конструкции должны быть представлен в языке. Напротив,
эффективность конструкций определяет насколько свободно они
могут использоваться в данной программею Эффективность должна
рассматриваться в трех разных контекстах : при компиляции,
связывании и выполнении.
Основной упор при проектировании возможностей абстракции
данных в С
делался на 2 и 3 аспектах, то есть на тех возможностях, которые
позволяют
программисту обеспечивать элегантные и эффективные интерфейсы
к абстракции.
С абстракция данных поддерживает возможность для программиста
определять
новые типы, называемые "классами". Члены класса доступны только
функциям
из явно объявленного набора. Просто упрятывание информации может
быть
достигнуто, например, так :
class data_type
{ // описание данных
/* список функций, которые
могут
использовать описания
данных
("дружественные" функции)
*/
};
где только
"дружественные функции (*1) могут иметь доступ к переменным
класса d a t a _ t y p e в том виде как они определены в описании
данных.
В
качестве альтернативы и часто более элегантно можно определить тип
данных, в котором множество функций, имеющих доступ к переменным
класса,
само является составной частью самого типа:
class object_type {
/* описания, используемые
для реализации object_type
*/
public:
/* описания, специфицирующие
интерфейс с object_type
*/
};
Одна
очевидная не нетривиальная цель многих современных проектов
языков программирования состоит в том, чтобы дать возможность
пользователю определять "абстрактные типы данных" с характеристиками
подобным характеристикам фундаментальных типов данных языка.
Ниже мы покажем, как добавляется тип данных c o m p l e x в язык
С
и при этом к комплексным переменным могут применяться обычные
арифметические операции. Например :
complex a, x, y, z;
a = x/y + 3*z;
Идея
представления объекта черным ящиком в дальнейшем поддерживается
механизмом иерархического конструирования классов из других классов.
Например:
class shape { ... };
class circle : shape { ... };
Класс shape в добавление к тому, что он используется как класс shape
может быть использован просто как circle. Говорят что класс circle
есть
производный класс (*2) с классом shape в качестве своего базового
класса.
Имеется возможность отсрочить разрешение типа объектов, имеющих
общие
базовые классы до времени выполнения. Это позволяет манипулировать
объектами разных типов некоторым общим образом.
3. РАЗГРАНИЧЕНИЕ ДОСТУПА К ДАННЫМ.
Рассмотрим
фрагмент старого С (*3), представляющий реализацию
концепции даты:
struct date { int day, month, year;};
struct date today;
extern void set_date ();
extern void next_date ();
extern void next_today ();
extern void print_date ();
В
приведенном примере нет явной связи между функциями и типом данных, нет
и указаний на то, что данные функции должны быть единственными,
которые
имеют доступ к членам структуры d a t e . Необходимо же иметь
возможность это указать.
____________
(*1) - В оригинале - "friends"ма frienфа functions - прим.переводчика.
(*2) - В
оригинале - deriveфа class, что можно перевести как
"порождаемый". - прим.переводчика.
(*3) - Ключевое слово void определяет функцию, не возвращающую
значения. Оно было введено в С примерно
в 1980 г.
Простой путь сделать это заключается в объявлении типа данных, с которым
может манипулировать только специфическое множество функций. Например:
date my_birthday, today;
set_date (&my_birthday, 30, 12, 1950);
set_date (&today, 23, 6, 1983);
print_date (&today);
next_date (&today);
Дружественные
функции определяются обычным образом. Например :
void next_date (date* d)
{
if
<++d->
/* особый
случай */
}
}
Такое
решение проблемы упрятывания информации просто и часто
достаточно эффективно. Оно не вполне гибко поскольку допускает
доступ всем дружественным функциям к всем переменным типа
Например, невозможно иметь различный набор дружественных функций
для данных m y _ b i r t h d a y . Функция однако может быть
дружественной более чем одному классу. Важность этого будет
продемонстрирована в секции 19. Нет требования, чтобы
дружественная функция могла манипулировать только переменнымим,
передаваемыми как аргументы. Напримерм, в функцию может быть
встроено имя глобальной переменной:
void next_today ()
{
if
<++today.day>
/* особый
случай */
}
}
Защита
данных от функций, не являющихся дружественными, основана на
ограничении использования имен членов класса. Поэтому она может
быть
обойдена путем адресной манипуляции и явного преобразования типов.
Можно получить несколько преимуществ от предоставления
доступа к
данным структуры только явно указанному списку функций.
Любая ошибка, вызывающая недопустимое состояние переменной
типа
d a t e , должна быть вызвана кодом дружественной функции поэтому
первый шаг отладки - локализация - будет завершен еще до того
как программа будет исполнена. Это особый случай общей идеи, что
любое изменение поведения типа d a t e может и должно быть вызвано
изменениями в его дружественных функциях. Другое преимущество
состоит в том, что потенциальному пользователю для того, чтобы
научиться пользоваться таким типом нужно изучить только описания
дружественных функций. Опыт С++ вполне это подтверждает.
4. ОТСТУПЛЕНИЕ: ТИП АРГУМЕНТОВ.
В приведенных
выше примерах были описаны аргументы функции.
Этого нельзя было сделать в старом С. Кроме того не приемлим
синтаксис описания аргумента, использованный для s e t _ d a t e .
В С++ семантика передачи аргументов идентична инициализации.
В частности, выполняются обычные арифметические преобразования.
Описание функции, в которой не указываются типы аргументов
например, n e x t _ t o d e y , означает, что эта функция не
приемлет аргументов вообще. В этом отличие от старого С см. ниже
секцию 2."Реализация и совместимость". Типы аргументов всех описаний
и определения функции должны точно совпадать.
Тем не менее имеется возможность вводить функции с неопределенным
и возможно переменным числом аргументов неопределенных типов,
однако такое отступление от типового контроля должно быть явно указано.
Например :
int
wild ( ... );
int fprintf (FILE*, char* ...);
Многоточие
определяет, что любые (или никакие) аргументы будут
приняты без контроля или преобразования точно так как в старом С.
Например :
wild
(); wild ("asdf" , 10); wild (1.3, "ghjk", wild);
fprintf (stdout, "x=%d" , 10);
fprintf (stderr, "file %s line %d\n", f_name, l_no);
Заметьте,
что первые два аргумента f p r i n t f должны
присутствовать и будут проконтролированы. Следует обратить
внимание однако, что функции с частично определенными типами
аргументов значительно реже используются в С++, чем в старом С
Такие функции преимущественно используются для описания интерфейсов
со старыми С-библиотеками. Вместо этого используются :
- аргументы функции по умолчанию (секция 9),
- перегрузка имен функций (секция 7),
- перегрузка знаков операций (секция 8).
См.также секцию 18.
Как и раньше необъявленные функции могут быть использованы
в предположении, что они возвращают целое значение. Они должны однако,
использоваться согласованно. Например:
undef
(1, "asdf"); undef1(2, "ghjk"); /* правильно */
undef2(1, "asdf"); undef2("ghjk", 2); /* ошибочно */
Несогласованное
использование u n d e f будет обнаружено компилятором.
5. ОБЪЕКТ.
Структура
программы, использующей для разграничения доступа
к представлению типа данных, дружественный механизм классов в
точности такая же как структура программы, не использующей такую
возможность. Отсюда следует, что для облегчения написания
функций, реализующих операции над типом из новой возможности
никаких преимуществ не может быть извлечено. Для многиха типов
может быть получено более элегантное решение путем внедрения
функций в сам новый тип. Например:
class
date {
int day, month, year;
public:
void set (int, int, int);
void next ();
void print ();
};
Функции,
определяемые таким путем, именуются составляющими
функциями (*1) и могут быть вызваны только для определенной
переменной подходящего типа, используя стандартный синтаксис С
для членов структуры. Так как имена функций более не являются
глобальными, они могут быть короче:
my_birtday.print();
today.next();
С
другой стороны, определяя составляющую функцию необходимо
указать как имя функции, так и имя ее класса:
void
date.next()
{
/* особый случай
*/
}
}
О
переменных таких типов часто говорят как об объектах. Объект,
для которого вызывается функция, представляет для нее скрытый аргумент.
В составляющих функциях имена членов класса могут быть использованы
без явной ссылки на объект члену класс того объекта,
для которого функция вызывается. Составляющей функции в
некоторых случаях необходимо явным образом сослаться на данный
объект, например, для того, чтобы вернуть на него указатель. Это
достигается путем указания ключевого слова t h i s ,
обозначающее данный объект в любой функции класса. Таким образом
в составляющей функции t h i s - - d a . эквивалентно dа са .
для любой составляющей функции класса.
Метка p u b l i c делит тело класса на две части. Имена
из первой, "приватной" части могут быть использованы только в
составляющих (и дружественных) функциях. Вторая, "публичная"
часть, составляет интерфейс с объектом класса. Функция класса
может обратиться как к публичным так и приватным членам любого
объектам класса, а не только к членам того объекта, для которого
она была вызвана.
Относительные достоинства дружественных и составляющих
функций будут обсуждаться в секции 1, после того как будет
представлено значительное число примеров. Сейчас будет достаточно
заметить, что дружественная функция не затрагивается "приватно-
публичным" механизмом и оперирует объектом стандартным явным
образом. Составляющая функция, напротив, должна вызываться для
конкретного объекта, она отличает этот объект от всех прочих.
6. СТАТИЧЕСКИЕ ЧЛЕН.
Класс
- это тип, а не объект данных и каждый объект класса
имеет свою копию данных - членов класса. Однако есть приложения,
абстрактную реализацию которых лучше проводить в предположении,
что различные объекты класса разделяют некоторые данные.
Например, для управления задачами в операционной системе или в
модели, часто используется список всех задач :
class
task {
. . .
task* next;
static task* task_chain;
void shedule (int);
void wait (event);
. . .
};
__________
(*1) В оригинале - member functions. - прим.переводчика.
Объявление члена
t a s k _ c h a i n статическим (s t a t i c)
означает, что будет существовать только одна его копия, а не по
одной копии на каждый объект t a s k . Он, однако, по-прежнему
в поле видимости класса t a s k , и доступен "снаружи" только
если он объявлен как p u b l i c . В этом случае его имя может
быть определено именем его класса :
t
a s k :: task_chain
В
составляющих функциях на него можно ссылаться просто как
t a s k _ c h a i n . Использование статических членов класса
может значительно уменьшить необходимость в глобальных
переменных.
Знак операции :: (двойное двоеточие) используется для
указания видимости имени в выражениях. Как унарная операция она
обозначает внешние (глобальные) имена. Например, если в модели
операционной системы функции w a i t класса t a s k необходим
вызов несоставляющей функции w a i t , это может быть сделано
таким образом :
void
task.wait(event e)
{
. . .
::wait(e);
}
7. КОНСТРУКТОР И ПЕРЕГРУЖЕННЫЕ ФУНКЦИИ.
Использование
функцийа тип s e t _ d a t e для
инициализации объектов класса не элегантно и чревато ошибками.
Так как нигде не утверждается, что объект должен быть
инициализирован; программист может забыть сделать это или часто
с равно плачевным результатом, сделать это дважды. Лучший подход
состоита в том, чтобы позволить программисту объявить функцию
явно предназначенную для инициализации объекта. Так как такая
функция конструирует значения данного типа, он называется
конструктором данного класса и опознается в языке благодаря тому,
что имеет то же имя, что и сам класс. Например:
class
date {
. . .
date (int, int, int);
};
Если
класс имеет конструктор, все объекты этого класса
должны быть инициализированы:
date
today = date (23, 6, 1983);
date xmas (25, 12, 0); /* правильная сокращенная форма
*/
date jily4 = today;
date my_birthday;/* неправильно, отсутствует инициализация
*/
Часто
желательно предоставить несколько способов
инициализации объектов класса. Это можно сделать путем
предоставления нескольких конструкторов. Например:
class
date {
. . .
date (int, int, int); /* день месяц год */
date (char*); /* дата в символьном виде */
date (int); /* день,месяц и год подразумеваются текущими
*/
date (); /* умолчание: сегодня */
};
Поскольку
функции-конструкторы различаются типами своих
аргументов, компилятора в каждом случае в состоянии выбрать
правильную функцию:
date
today (4);
date july4 ("July 4, 1983");
date guy ("5 Nov");
date now; /* инициализация по умолчанию */
Применение
конструкторов не ограничивается инициализацией
они могут быть использованы везде, где необходимо иметь объект
данного класса:
date
us_date (int month, int day, int year)
{
return date (day, month, year);
}
. . .
some_function (us_date (12, 24, 1983));
some_function ( date (24, 12, 1983));
Если
несколько функций объявлены с одинаковым именем
говорятб что это имя перегруженою Использование перегруженных
имен функций не ограничивается случаем конструкторов. Однако для
несоставляющих функций их описания должны предваряться указанием,
что имя будет перегружено, например:
overload
print;
void print (int);
void print (char*);
или,
возможно, сокращенно:
overload
void print (int), print (char*);
С
точки зрения компилятора, единственная вещь, общая для
множества функций с одним именнем
говорятб что это имя перегруженою Использование перегруженных
имен функций не ограничивается случаем конструкторов. Однако для
несоставляющих функций их описания должны предваряться указанием,
что имя будет перегружено, например:
overload
print;
void print (int);
void print (char*);
или,
возможно, сокращенно:
overload
void print (int), print (char*);
С
точки зрения компилятора, единственная вещь, общая для
множества функций с одним именмер, вышеприведенное описание
простого конструктора для класса d a t e.
Для аргументов функции с перегруженными именами правила
преобразования типов язык С применяются не полностью.
Преобразования, которые могут исказить информацию, не
d o u b l e. Возможно, однако, предусмотреть разные функции для
целого и вещественного типов. Например:
overlood
print (int), print (double);
Список
функций с перегруженным именем будет просматриваться
по порядку до появления совпадения, таким образом p ri n t (l)
будет вызывать функцию p r i n t для целого типа аргумента, а
p r i n t (l,0) - для функции p r i n t с плавающей точкой.
Если же порядок описаний будет изменен, оба обращения к функции
вызовут функцию p r i n t с плавающей точкой с вещественным
(d o u b l t) представлением аргумента l.
8. ПЕРЕГРУЗКА ЗНАКОВ ОПЕРАЦИЙ И ПРЕОБРАЗОВАНИЕ ТИПОВ.
Некоторые
языки предоставляют тип данных complex
так, что для комплексных чисел программисты могут пользоваться
непосредственно математической нотацией. Так как в С этого типа
нет, то будет явной проверкой возможностей абстракции в том,
насколько он может поддерживать традиционную нотацию для
комплексных чисел (Заметим однако, что complex -
необычный тип данных в том смысле, что при чрезвычайно простом
представлении он имеет весьма строгие традиции по своему
использованию.) Это таким образом в основном проверка мощности
возможностей абстракции языка по имитации общепринятой нотации.
В большинстве случаев внимание проектировщика привлечено
непосредственно к нахождению хорошей реализации абстракции и
подходящего наглядного представления ее пользователями. Цель
проверки состоит в том, возможно ли написать код типа:
complex
x;
complex a = complex (1, 1.23);
complex b = 1;
complex c = pi;
if
(x -= a) x = a + log (b * c) / 2;
Таким образом
для комплексных чисел и для сочетания
комплексной и скалярной констант и переменных должны быть
определены стандартные арифметические операции сравнения.
Ниже приводится
описание очень простого класса c o m p l e x :
class
complex
{ double re, im;
friend complex operator+(complex, complex);
friend complex operator*(complex,
complex);
friend int operator != (complex,complex);
public:
complex() { re = im = 0;}
complex(double r) { re = r; im = 0;
}
complex(double r, double i) { re =
r; im = i; }
};
Знак операции
опознается как имя функции, если ему
предшествует ключевое слово complex. Если операция
используется для типа, определенного как класс, компилятор
сгенерирует обращение подходящей функции, если таковая
определена. Например, для комплексных переменных x, y
сложение x + y будет интерретироваться как operator+(x,y)
согласно вышеприведенному описанию класса complex.
Функция сложения комплексных чисел могла бы быть определена так:
complex
operator+(complex a1, complex a2)
{
return complex(a1.re + a2.re, a1.im + a2.im);
}
Естественно, все имена вида c o m p l e x
перегружены. Для гарантии того, что язык является только
расширяемым, не изменчивым, функции-операции должны иметь хотя
бы одним аргументом объект какого-либо класса. Объявляя функцию-
операцию, программист может приписать стандартному знаку операции
языка С значение, связывающего его с объектами определенного
пользователем типа. Эти знаки операций сохраняют свое место
в синтаксисе С, добавить новые знаки операций невозможно. Таким
образом, невозможно изменить приоритет операций или ввести новый
знак операции (например, **к для возведения в степень). Это
ограничение сохраняет простоту анализа выражений языка С.
Описание
функций для унарных и бинарных операций различаются
по числу их аргументов. Например:
class
complex {
. . .
friend complex operator - (complex);
friend complex operator - (complex,
complex);
};
Проектировщику
класса c o m p l e x для поддержки
смешанной арифметики типа x x + 1, где x x - комплексная
переменная, открыты три пути. Можно просто считать такие
выражения незаконными, так что пользователь будет вынужден явно
записывать преобразование из ???
Другим решением может быть спецификация нескольких комплексных функций
сложения:
complex
operator+(complex,complex);
complex operator+(complex,double);
complex operator+(double,complex);
так, что компилятор
для каждого случая выберет подходящую функцию.
Наконец, если класс включает конструкторы, принимающие
единственный аргумент, они могут быть использованы для
определения преобразования типов из типа своего аргумента тип
своего значения. Так, с учетом вышеприведенного описания класса
c o m p l e x будет автоматически интерпретироваться
как o p e r a t o r + (x x, c o m p l e x ( 1 )).
Последняя возможность нарушает идею многих людей строгой
типизации. Однако, использование второго решения практически
утратит число необходимых функций, первое предоставляет мало
удобств в смысле упрощения нотации для пользователя класса
c o m p l e x . Заметим, что комплексные числа представляют
весьма типичный пример желательности смешанной арифметики. Такие
характерные типы данных не существуют в вакууме. Более того, для
многих типов существует тривиальное преобразование из числовых
и/или строковых констант С в подмножество значений данного типа
(подобно преобразованию числовых констант С в комплексные
значения на вещественной оси).
Подход с использованием дружественных функций был выбран для
того, чтобы использовать составляющие функции в функциях - операциях.
Присущая данному представлению объектов ассиметрия не соответствует
традиционному математическому взгляду на комплексные числа.
9.ОТСТУПЛЕНИЕ : АРГУМЕНТЫ ПО УМОЛЧАНИЮ И ВКЛЮЧАЕМЫЕ ФУНКЦИИ.
Класс c o
m p l e x включает три конструктора, два из
которых просто подставляют нулевое значение (по умолчанию ),
предоставлены программисту для удобства нотации. Использование
перегрузки типично для конструкторов, но также часто
обнаруживается и для других функций. Такая перегрузка, однако,
весьма трудоемкий обходной путь подстановки аргументов по
умолчанию и, в особенности для более сложных конструкторов,
крайне избыточнa. Следовательно должна быть предоставлена
возможность для непосредственного задания аргументов по
умолчанию. Например:
class complex {
. . .
public:
complex(double r=0, double
i=0) { re = r; im = i;}
};
Если завершающий
аргумент отсутствует, может быть использовано
константное выражение по умолчанию. Например :
complex a(1,2);
complex b(1); /* b = complex(1, 0); */
complex c; /* c = complex(0, 0); */
Если составляющая
функция такая, как вышеприведенная функция
c o m p l e x не только описана, но и определена, (то есть
приведено ее тело) в описании класса, то при обращении к ней
может быть выполнена константная подстановка, избегая тем самым
обычных накладных расходов по вызову функции. Константная
подстановка функции не является макроподстановкой, семантика
такой функции идентична семантике других функций. Любая функция
может быть объявлена включаемой (*1) предшествующим указанием
ключевого слова i n l i n e . Включаемые функции могут сделать
описания классов крайне раздутым и они в случае разумного
использованияа повышают эффективность исполнения, но всегда
увеличивают время и память, необходимые для компиляции.
Включаемые функции, таким образом, должны использоваться только в
случае, если ожидается значительное повышение эффективности
исполнения. Они были включены в С++ вследствие опыта
использования макросов в С. В некоторых приложениях макросы
бывают необходимы (и нет возможности определить макрос в составе класса)
но значительно чаще они создают хаос тем, что выглядят
как функции, но не подчиняются ни синтаксису функций, ни правилам
видимости, ни правилам передачи аргументов.
10. УПРАВЛЕНИЕ ПАМЯТЬЮ.
В С++ три
класса памяти : статический, автоматический (стека)
и свободный (динамический). Управление свободной памятью
доступно программисту посредством операций n e w и d e l e t e
Стандартный сборщик мусора не предусмотрен (*2).
Для упрятывания деталей управления свободной памятью очень
удобны конструкторы. Например :
class
string {
char *rep;
string (char *);
-
~string() { delete rep; }
. . .
};
string :: string(char *p)
{
rep = new char[strlen(p) + 1];
strcpy(rep, p);
}
Здесь использование
свободной памяти внедрено в конструктор
s t r i n g ( ) и его дополнение - деструктор ~ s t r i n g ( ).
Деструкторы неявно вызываются, когда объект покидает область
видимости. Они также вызываются, когда объект явно уничтожается
посредством d e l e t e . Для статических объектов деструкторы
вызываются после всех частей программы при завершении.
________________________________________________________________________
(*1) - В оригинале - inline, inline function. - прим. переводчика.
(*2) - Нетрудно, однако, написать реализацию операции n e w со
сборкой мусора так, как это было сделано
для функции выделения
свободной памяти allocate в старом С. В общем
случае
невозможно, просматривая память выполняемой
С-программы, отличить
указатели от других элементов данных, поэтому
сборщик мусора
должен быть консервативен в выборе того,
что подлежит удалению,и
он должен исследовать чрезвычайно большое
количество данных.
Впрочем, это может быть полезно для некоторых
приложений.
Операция n e w ,
которой в качестве аргумента передается тип,
возвращает указатель на объект этого типа; d e l e t e
принимает такой указатель в качестве аргумента.
Сам тип s t r i n g может быть размещен в свободной памяти.
Например:
string
*p = new string("asdf");
delete p;
p = new string("qwerty");
Более
того, классу предоставляется возможность управлять
выделением свободной памяти для своих объектов. Например:
class
node {
int type;
node *1;
node *r;
node () { if (this == 0) this =_node
(); }
-
~node () { free_node (this); this
= 0; }
. . .
};
Для объектов, создаваемых операцией n e w , указатель t h i s
при входе в конструктор будет равен нулю. Если конструктор
не присвоит значение указателю t h i s , будет
использована стандартная функция распределения памяти.
Стандартная функция возврата памяти будет использована в конце
деструктора тогда и только тогда, когда указатель t h i s не
равен нулю. Распределитель памяти, представляемый программистом
для определенного класса или множества классов, может быть много
проще и, по крайней мере, на порядок быстрее стандартного.
Используя конструкторы и деструкторы, проектировщик может
определять такие типы данных, как вышеприведенный s t r i n g ,
у которых размер представления типа может быть переменным, в то
время как размер любой статической и автоматической переменной
должен быть известен при компоновке и компиляции соответственно.
Объект данного класса сам по себе имеет фиксированный размер, но
его класс поддерживает вторичную структуру данных переменного размера.
11. СКРЫТОЕ УПРАВЛЕНИЕ ПАМЯТЬЮ.
Конструкторы
и деструкторы не могут полностью скрыть детали
управления памятью от пользователя класса. Если объект копируется,
либо посредством явного присваиваниям, либо при передаче функции
в качестве аргумента, указатели на вторичную структуру данных
также копируются. Это иногда нежелательно. Рассмотрим проблему
семантики передачи значений для простого типа данных s t r i n g .
Пользователь видит s t r i n g как один объект, однако его
реализация состоит из двух частей, как это приведено выше. После
выполнения присваивания s 1 = s 2 оба объекта s t r i n g
ссылаются на то же самое представление, в то время как ссылка
на память, использованная для старого представления s 1 , теряется.
Во избежании этого оператор присваивания может быть перегружен.
class
string {
char *rep;
void operator = (string);
. . .
};
void string.operator = (string source)
{
if (rep !=source.rep) {
delete rep;
rep = new char[strlen(source.rep)
+ 1];
strcpy (rep,
source.rep);
}
}
Так как функции
нужно модифицировать, объект-приемник (типа
s t r i n g ) лучше всего написать составляющую функцию,
принимающую исходный объект s t r i n g в качестве аргумента.
Тогда присваивание s 1 = s2 будет интерпретироваться как
s 1. o p e r a t o r = ( s 2 ).
Это оставляет в стороне проблему, что делать с инициализацией
и аргументами функции. Рассмотрим:
string
s1 = "asdf";
string s2 = s1;
do_something(s2);
В данном
случае объекты типа s t r i n g s 1 , s 2 и
аргумент функции d o _ s o m e t h i n g остаются с одними тем
же представлением r e p . Стандартное побитовое копирование
совершенно не сохраняет желаемую семантику значений для типа s t r i
n g .
Семантика передачи аргументов инициализации идентична : обе
предполагают копирование объекта в неинициализированную
переменную. Она отличается от семантики присваивания только в
том, что объект, которому присваивается значение,
предположительно содержит значение, а объект инициализации - нет.
В частности, конструкторы используются для передачи аргументов
точно также, кака и при инициализации. Следовательно, нежелательное
побитовое копирование может быть обойдено, если мы определим
конструктор для выполнения подходящей операции копирования.
К сожалению, использование очевидного конструктора
class
string {
...
string (string);
};
ведет к бесконечной
рекурсии, и поэтому незаконно. Для решения
этой проблемы вводится новый тип "ссылка" (*1). Синтаксически он
определяется знаком &, который используется тем же образом, как и
знак указателя *. Если переменная объявлена как имеющая тип &Т
то есть как ссылка на тип Т, она может быть инициализирована как
указателем на тип Т, так и объектом типа Т. В последнем случае
неявно применяется операция взятия адреса &. Например:
int
x;
int &r1 = &x;
int &r2 = x;
Здесь
адрес x присваивается как r1, так и r2. При
использовании ссылка неявно преобразуется, так, например:
r1 = r2
означает копирование
объекта, указываемого r2 в объект,
указываемый r1. Заметим, что инициализация ссылок совершенно
отлична от присваивания им.
_______________________
(*1) - В оригинале - "reference", не путать с "pointer" - указателем.
прим. переводчика.
Используя
ссылки, класс s t r i n g может быть объявлен,
например, так:
clfss
string {
char *rep;
string(char *);
string(string &);
~string();
void operator=(string &);
. . .
};
string(string
&source)
{
rep = new char[strlen(source.rep)
+ 1];
strcpy(rep, source.rep);
}
Инициализация одного
объекта s t r i n g другим и передача
s t r i n g в качестве аргумента будет теперь вызывать
обращение к конструктору s t r i n g ( s t r i n g & ) , который
корректно дублирует представление типа. Операция присваивания для
типа s t r i n g может быть переопределена, используя
преимущества ссылок. Например:
void
string.operator = (string &source)
{
if (this != &source) {
delete rep;
rep = new char[strlen(source.rep)
+ 1];
strcpy(rep, source.rep);
}
}
Данный тип
s t r i n g во многих приложениях не будет
достаточно эффективен. Нетрудно, однако, модифицировать его так,
что представление типа будет копироваться только в случае
необходимости, а в остальных случаях - разделяться.
12. ДАЛЬНЕЙШИЕ СОГЛАШЕНИЯ ПО НОТАЦИИ.
Удивительно,
что ссылки - конструкция, которая во многом
подобна правилам "передачи параметров по имени" во многих языках,
- вводятся преимущественно для того, чтобы дать программисту
возможность определить семантику "передачи параметров по
значению". Ссылки используются также в некоторых других случаях,
включая, конечно,и передачу аргументов "по имени". В частности,
ссылки предоставляют возможность использовать нетривиальные
выражения в левой части операции присваивания. Рассмотрим тип
s t r i n g c операцией выделения подстроки:
class
string {
. . .
void operator=(string &);
void operator= (char *);
string &operator()(int pos, int length);
};
где o p e r a t
o r ( ) обозначает вызов функции.
Например:
string
s1 = "asdf";
snring s2 = "ghjkl";
s1(1,2) = "xyz"; /* s1 = "axyzf" */
s2 = s1(0,3); /* s2 = "axy" */
Оба присваивания
интерпретируются как:
s2.operator=(s1.operator()(0,3));
Функции o
p e r a t o r безразлично, вызывается она в левой
или в правой части присваивания. Заботу об этом берет на себя
функция o p e r a t o r = .
Выборка элемента массива аналогично может быть перегружена
определением функции o p e r a t o r [ ].
13. ОТСТУПЛЕНИЕ: ССЫЛКИ И ПРЕОБРАЗОВАНИЯ ТИПОВ.
Преобразования,
определенные для некоторого класса,
применяются также и к ссылкам. Рассмотрим класс s t r i n g ,
в котором присваивание простой символьной строки не определено,
но есть конструктор для получения объекта s t r i n g из
символьной строки :
class
string {
...
string (char
*);
void operator=(string &);
};
string
s = "asdf";
Присваивание
s
= "ghjk";
законно и будет
иметь желаемый эффект. Оно интерпретируется как
s.operator=((temp.string("ghjk"), &temp))
где t e m p - временная
переменная типа s t r i n g .
Применение конструкторов до взятия адреса, как этого требует
семантика ссылок, обеспечивает то, что для переменных ссылочного
типа выразительная мощь, предоставляемая конструкторами, не будет
теряться. Другими словами, множество значений, допустимое для
функции, ожидающей аргумент типа Т, то же самое, что и допустимое
для функции, ожидающей аргумент типа Т& (ссылку на Т).
14. ПРОИЗВОДНЫЕ КЛАССЫ.
Рассмотрим
проект системы для управления выводом
геометрических фигур на экран терминала. Привлекательный подход
заключается в трактовке каждой фигуры как объекта, который можно
запросить для выполнения определенных действий типа "вращаться" и
"изменить цвет". Каждый объект будет интерпретировать такой
запрос в соответствии со своим типом. Например, алгоритм вращения
круга несомненно будет отличаться (будет проще) алгоритма
вращения треугольника. Что действительно нужно, то это единый
интерфейс к разнообразию сосуществующих реализаций. Не следует
предполагать, что разные виды фигур будут иметь похожее
представление. По сложности они будут отличаться в широких
пределах,и будет жаль, если не удастся использовать естественную
простоту базовых фигур типа круга или треугольникаа из-за того,
что необходимо поддерживать сложные фигуры типа "мышь" или
"Британские острова".
Общий подход
заключается в предоставлении классу s h a p e
возможности определить общие свойства фигур, в частности,
"стандартный интерфейс". Например:
class
shape {
point center;
int color;
shape *next;
static shape *shape_chain;
...
public:
void move (point to) { center = to;
draw(); }
point where() {return center; }
virtual void rotate(int);
virtual void draw();
...
};
Функции,
которые не могут быть реализованы без знания
специфических свойств s h a p e , объявляются виртуальными
(v i r t u a l ). Ожидается, что виртуальные функции будут
определены позже. На этой стадии известен только их тип; этого
достаточно, однако, для контроля правильности их вызова.
Класс, определяющий
конкретную фигуру, может быть определен как:
class
circle: public shape {
float radius;
public:
void rotate(int angle) {}
void draw();
...
};
Здесь определено,
что класс s h a p e является классом
c i r c l e, так как он имеет в своем составе всех членов класса
s h a p e в дополнение к своим собственным. Говорят, что класс
c i r c i e является производным от его "базового класса"
s h a p e . Объекты типа c i r c l l e могут быть теперь описаны
и использованы:
circle
cl;
shape *sh;
point p (100, 30);
cl.draw();
cl.move(p);
sh = *cl;
На самом
деле функция, вызываемая обращением к c l . d r a w()
есть c i r c l e . d r a w (), так как тип c i r c l e
не определяет собственной функции m o v e (), то функция,
вызываемая обращением к c l . m o v e ( p ), есть
s h a p e . m o v e, так как класс c i r c l e наследует ее класс
опять же есть c i r c l e :: d r a w (), несмотря
на то,а что нельзя найти никакой ссылки на класс c i r c l e
в описанииа класса s h a p e . Виртуальнаяа функция
переопределяется, если класс является производным от другого
класса. Каждый объект класса, включающий виртуальные функции,
содержит индикатор типа. Это дает возможность компилятору
находить подходящую виртуальную функцию для каждого вызова, даже
если тип объекта неизвестен во время компиляции. Вызов
виртуальной функции - единственный путь использования скрытого в
классе индикатора типа (класс без виртуальных функций не имеет
такого индикатора).
Фигуры также
могут предоставлять возможности, которые могут
быть использованы только, если программист знает их конкретный
тип. Например:
class
clock_face : public circle {
line hour_hand, minute_hand;
public:
void draw();
void rotate)int);
void set(int, int);
void advance(int);
...
}
Время, которое
показывают часы, может быть установлено
посредством функции s e t ( ) на конкретное значение, также
может быть переведено посредством функции a d v a n c e ( ) на
определенное число минут. Функция d r a w ( ) в классе
c l o c k _ f a c e скрывает c i r c l e :: d r a w ( ) ,
поэтому последняя должна быть вызвана по своему полному имени.
Например:
void
clock_face.draw() {
circle::draw();
hour_hand.draw();
minute_hand.draw();
}
Заметим,
что виртуальная функция должна стать составляющей.
Она не может быть дружественной. При стиле программирования с
использованием дружественных функций не имеется эквивалентной
возможности представленному здесь и в следующей секции
использованию динамической типизации.
15. ОТСТУПЛЕНИЕ: СТРУКТУРЫ И ОБЪЕДИНЕНИЯ.
Конструкции
С s t r u c t и u n i o n допустимы, но они
переросли в классы. Структура есть класс, все члены которого
являются публичными, таким образом
struct s ( ... );
эквивалентно
class
s { hublic5 ... };
Объединение есть
структура, которая может содержать ровно одно
значение в каждый момент времени.
Эти определения означают, что структура или объединение
могут иметьв качестве своих членов составляющие функции.
В частности, они могут быть конструкторами. Например:
union
uu {
int i;
char *p;
uu(int ii) { i = ii; }
uu(char *pp) { p = pp; }
};
Это
снимает большинство проблем, касающихся инициализации
объединений. Например:
uu
u1 = 1;
uu u2 = "asdf";
16. ПОЛИМОРФНЫЕ ФУНКЦИИ.
Используя
производные классы, можно определить интерфейс,
предоставляющий унифицированный доступ к объектам неизвестных
и/или различных классов. Это может быть использовано для
написания полиморфных функций, т.е. таких функций, в которых
алгоритм специфицирован таким образом, что может быть применен к
аргументам различных типов. Например:
void
sort(common *v[], int size)
{
/* сортировка массива элементов типа
common "v[size]"
*/
}
Функции
s o r t необходимо лишь иметь возможность
сравнивать объекты класса c o m m o n для выполнения своей
задачи. Таким образом, если класс c o m m o n имеет виртуальную
функцию c m p r ( ) , s o r t ( ) будет иметь возможность
сортировать массив объектов любого класса, производного от класса
c o m m o n , для которого c m p r ( ) определена. Например:
class
common {
...
virtual int cmpr(common *);
};
class
apple : public common {
...
int key;
int cmpr(common *arg)
{
/* предполагается, что arg
также имеет тип apple */
return (key == k) ? 0 : (key
};
class
orange : public common {
...
int cmpr(common *);
};
Былаа
выбрана функция c m p r ( ) , не более
привлекательная на первый взгляд перегрузка операцииа "
тремя исходами. Для того, чтобы написать функцию s o r t ( ) для
обработки массива объектов класса c o m m o n , а не массива
указателей на объекты класса c o m m o n , потребуется
виртуальная функция s i z e ( ) (вычисление размера).
Если
желательно сравнивать между собой классы a p p l e и
o r a n g e , для функции сравнения потребуется некоторый способ
получения ключа сортировки. Класс c o m m o n мог бы, например,
содержать виртуальную функцию извлечения ключа сортировки.
17. ПОЛИМОРФНЫЕ КЛАССЫ.
Полиморфные
классы определяются тем же образом,а что и
полиморфные функции. Например:
class
set : public common {
class set_mem {
set_mem *next;
object *mem;
set_mem(common
*m, set_mem *n)
{ mem = m; next = n;}
} *tail;
public:
int insert(common *);
int remove(common *);
int member(common *);
set()
{ tail = 0; }
- set()
{ if (tail) error("непустое
множество"); }
}
Таким
образом, класс s e t (для реализации множества)
реализован как связанный список объектов s e t _ m e m , каждый
из которых указывает на класс c o m m o n . Указатели на объекты
(не сами объекты) введены в класс. Для полноты описания класс
s e t сам является производным c o m m o n , таким образом,
можно создать множество множеств. Так как класс s e t
реализован независимо от данных составляющих его объектов,
объект может быть членом двух и более множеств. Такая модель
являетсяа очень общей и может быть использованаа( и в
действительности использовалась) для создания "абстракций" типа
множество, вектор, связанный список, таблица. Наиболее
отличительная черта данной модели для "классов-контейнеров"
состоитв том, что, в общем случае, ни контейнер не зависит от
данных, содержащихся в объектах, ни сами объекты независят от
данных, идентифицирующих их контейнер (контейнеры). Это часто
является важным структурным преимуществом : классы могут
проектироваться и использоваться без забот о том, структуры
данных какого типа необходимы для программы, их использующей.
Наиболее очевидный недостаток состоитв том, что издержки
составляют как минимум один указатель на член класса (два
указателя в рамках вышеприведенного класса s e t для реализации
связанного спискай (*1). Другое преимущество состоитв том,а что
такие классы-контейнеры могут быть способны содержать
гетерогенный набор членов (*2). Если это нежелательно, можно
тривиальныма образом определить производный класс, который
допускает в качестве членов только один конкретный класс.
Например:
class
apple_set: public set {
public:
int insert(apple *a) { return set::insert(a);
}
int remove(apple *a) { return set::remove(a);
}
int member(apple *a) { return set::member(a);
}
};
__________
(*1) Плюс еще один указатель для реализации механизма виртуальных функций.
См.ниже секцию 21. "Эффективность".
(*2) То есть, разных классов. - прим.переводчика.
Заметим,а
что, так как функции класса a p p l e _ s e t не
выполняют никаких действий в дополнение к тем, что выполняют
функции базового класса s e t , они будут полностью (то есть до
полного отсутствия каких бы то ни было издержек) оптимизированы.
Они служат только для того, чтобы обеспечить контроль типов во
время компиляции.
Класс
c o m m o n подходящим набором полиморфных классов
и функций находится в стадии разработки. Предполагается, что он
будет входить в состав стандартной библиотеки.
18. ВВОД И ВЫВОД
С не предусматривает
каких-либо возможностей по поддержке
ввода/вывода. Традиционно программист опирается на библиотечные
функции типа p r i n t f ( ) и s c a n f ( ) . Например, для
того, чтобы напечатать структуру данных, представляющую
комплексное число, можно записать:
printf
("(%g,%g)\n", zz.real, zz.imag);
К несчаcтью,
так как в старом С стандартные функции
ввода/вывода знают только стандартные типы, необходимо печатать
структуру почленно. Зачастую это утомительно и может быть сделано
только в том случае, если члены структуры доступны. Проблема в
общем случае не может быть решена расширением поддержки
определенных пользователем типов и форматов ввода/вывода.
Подход,
принятый в С++, заключается в том, чтобы
предоставить (в "стандартной" библиотеке, не в самом языке)
операцию
типа. Для данного выходного потока c o u t можно записать:
cout
Реализация
класса c o m p l e x определяет
ostream
& operator
return s
Операции
отдельных вызовов для каждого аргумента. Например:
put(cout,
"("); /* невыносимо избыточно */
put(cout,c.real);
put(cout
put(cout, ")\n");
По
сравнению с p r i n t f , имеется потеря управления по
форматированию вывода.В том случае, если необходимо более тонкое
управление, можно использовать "функции форматирования".
Например:
cout
представление своего первого аргумента.
для типа данных i s t r e a m для каждого базового и
определенного пользователем типа. Если операция ввода завершится
неуспешно, поток войдет в состояние ошибки, что приведет
последующие за ним операции к неуспешному завершению. Для
переменной z z любого типа можно написать такой, например, код:
Достойно
удивления, что операции ввода обычно тривиальны для
написания, т.к. всегда имеется в наличии конструктор для
выполнения нетривиальной части работы, аргументы конструктора
(конструкторов) дают хорошее первое приближение формата ввода.
Например:
{
if (!s) return s;
double re = 0, i = 0;
char c1 = 0, c2 = 0, c3 = 0;
if {c1 != '/' || c2 !=','|| c3;
if (c1 != '(' || c3 != 1) ) s.state
= _bad;
if (s) zz = complex(re, im);
return s;
}
Соглашением
для функций, реализующих операции ввода/вывода,
является возвращение аргумента-потока с указанием успешного или
ошибочного завершения (состояния). Данный пример чуть-чуть
излишне прост для реального использования, но приведенная функция
изменит значение аргумента · · и оставит поток в неошибочном
состоянии тогда и только тогда, если будет обнаружено комплексное
число в форме ( d o u b l e , d o u b l e ). Интерпретация
проверки потока на неравенство нулю как проверка его состояния
обеспечивается посредством перегрузки операции ! s для
i s t r e a m . Например, вышеприведенная проверка i f ( s )
интерпретируется как i f ( б
( ) , которая, наконец, проверит s. s t a t e .
Заметим,а
что не происходит потери информации о типе при
использовании
p r i n t f и s c a n f можно избежать большого класса ошибок.
Болееа того,
(определенного пользователем) типа, не затрагивая "стандартные"
классы i s t r e a m и o s t r e a m никоим образом, также
без знания об устройстве этих классов. ¦ s t r e a m может быть
связан с реальным выводным устройством, как и i s t r e a m .
Это значительно расширяет диапазон применения и избавляет от
нужды в функциях s s c a n f и s p r i n t f старого С.
Посимвольные операции p u t ( ) и g e t ( ) также
доступны для потоков ввода/вывода.
19. ДРУЖЕСТВЕННЫЕ И СОСТАВЛЯЮЩИЕ ФУНКЦИИ.
Если
новая операция должна быть добавлена в класс, есть
фактически два пути ее реализации - как дружественной функции или
как составляющей. Зачем предоставляются две альтернативы, и для
какого рода операций каждая из них предпочтительнее ?
Дружественная функция есть совершенно обычная функция,
отличающаяся лишь своим правом на использование приватных имен
членов класса. Программирование с использованием дружественных
функций есть во многом программирование в условиях, когда как
будто бы и нет никакого упрятывания информации.
Подход с использованием
дружественных функций полностью
реализует традиционный математический взгляд на значения, которые могут
быть использованы в вычислениях, присвоены переменным, но никогда
в действительности не модифицируемы. Данное противоречие
разрешается путем использования аргументов-указателей.
Составляющая функция,с другой стороны, связан с
единственным классом и вызывается догом программирование в условиях,
когда как
будто бы и нет никакого упрятывания информации.
Подход с использованием
дружественных функций полностью
реализует традиционный математический взгляд на значения, которые могут
быть использованы в вычислениях, присвоены переменным, но никогда
в действительности не модифицируемы. Данное противоречие
разрешается путем использования аргументов-указателей.
Составляющая функция,с другой стороны, связан с
единственным классом и вызывается дми класса.
В первом приближении следует использовать составляющую
функцию для реализации операций, предположительно меняющих
состояние объекта. Заметим, что преобразование типа в случае,
если оно объявлено, выполняется над аргументами, но не над
объектом, для которого составляющая функция вызывается.
Следовательно,а реализация с использованием составляющих функций
должна быть выбрана для операций, где преобразоваие типа
нежелательно.
Дружественная функция может быть дружественной двум и более
классам,в то время как составляющая функция может быть членом
одного единственного класса. Это делает удобным использование
дружественных функций для реализации операций для двух или более
классов. Например:
class
matrix {
friend matrix operator * (matrix,
vector);
...
};
class vector {
friend matrix operator * (matrix,
vector);
...
};
Необходимо бы было
иметь две составляющих функции -
m a t r i x . o p e r o t o r ( ) и v e c t o r . o p e r a t o r ()
-для достижения того, что обеспечивает дружественная функция
o p e r a t o r * ( ) .
Имя
дружественной функции является глобальным, в то время
как видимость имени составляющей функции ограничена ее классом.
При структурировании большой программы всегда стараются
минимизировать объем глобальной информации. Поэтому необходимо
избегать дружественных функций так же, как глобальных данных.В
идеале все данные должны быть распределены по классами должны
обрабатываться посредством составляющих функций. Однако, на более
детальном уровне программирования это становится утомительными
часто неэффективным; здесь-то и могут заявить о себе дружественные
функции.
Наконец,
если нет очевидных причин для предпочтения одной
реализации операции другой, следует выбрать составляющую функцию.
20. РАЗДЕЛЬНАЯ КОМПИЛЯЦИЯ.
Традиционный
подход С раздельной компиляции был сохранен.
Спецификации типов разделяются путем текстуального включения в
раздельно компилируемые исходные файлы. Нет автоматического
механизма, обеспечивающего гарантию того, что включаемые файлы
содержат полные спецификации типов и что они используются по
назначению. Такого рода проверки должны быть специально запрошены
и выполнены отдельно от процесса компиляции.
Имена внешних переменных
и функций из результирующих объектных файлов
замыкаются редактором связей, который понятия не имеет о типах
данных. Редактор связей, который мог бы проверять типы, оказал бы
неоценимую помощь, и его не так уж трудно было бы подготовить.
Описание класса определяет тип, так что оно может быть
включено в несколько исходных файлов без каких-либо нежелательных
эффектов. Оно должно быть включено в каждый файл, использующий
класс. Обычно составляющая функция не располагается в том же
файле, что и описание класса. Язык не имеет никаких предложений
по поводу расположения составляющих функций.В частности, не
требуется, чтобы все составляющие функции одного класса были бы в
одном файле, или были бы отделены от других описаний.
Так как приватные и публичные части класса физически не
разделены, приватнаяа часть не является в действительности
"скрытой от пользователя класса, как если бы это была идеальная
возможность абстракции данных. Хуже того, любое изменение в
описании класса может потребовать перекомпиляции всех файлов,
использующих его. Очевидно, что, если изменения были только в
приватной части, то перетранслироваться должны были бы только
файлы, содержащие составляющиеаи дружественные функции.
(Добавление новых составляющих функций в большинстве случаев не
делает необходимой перекомпиляцию. Добавление, однако, может
скрыть некоторую внешнюю функцию, используемую некоторыми другими
составляющимиа функциями, что поменяет смысл программы.
сожалению, это редкое событие очень трудно обнаружить.)
Возможность, которая позволила бы определить множество функций
(или множество исходных файлов), которые требуется
перекомпилировать после изменения описания класса, была бы
чрезвычайно полезной. К несчастью, сделать это весьма
нетривиально, без того, чтобы значительно неснизить при этом
скорость компиляции.
21. ЭФФЕКТИВНОСТЬ.
Эффективность
выполнения сгенерированного кода рассматрива-
лась как основная задача механизмов абстракции. Общее предположе-
ние состояло в том, что, если программа может быть сделана более
эффективно без использования классов, многие программисты
предпочли бы более быструю программу. Аналогично, если программа
может быть сделана меньшей по объему без использования классов,
многие программисты предподчтут более компактное представление.
Ниже продемонстрировано, что классы могут быть использованы без
малейшей потери эффективности или компактности представления
данных в сравнении с программами , написанными на "старом" С.
Упор на эффективность привел к отказу от возможностей,
требующих сборки мусора.В компенсацию этого была предусмотрена
возможность перегрузки для того, чтобы позволить полностью
внедрить управление памятью в класс. Болееа того, было
предусмотрено, чтобы программист с легкостью мог предусматривать
специальные функции по управлению свободной памятью. Как было
описано выше, конструкторы и деструкторы могут быть использованы
для того, чтобы управлять созданием и уничтожением объектов
класса. В дополнение к этому могут быть описаны функции
o p e r a t o r n e w ( ) и o p e r a t o r d e l e t e ( )
для переопределения операций n e w и d e l e t e.
Класс, не использующий виртуальные функции, использует
в
точности столько же памяти,а как и структуры С с такими же
данными. Нет никакого скрытого расхода памяти на каждый объект.
Нет и никаких издержек памяти на класс в целом. Составляющая
функция ничем не отличается от других функций по своим
требованиям по памяти. Если класс использует виртуальные функции,
имеется перерасход памяти на один указатель на объект и один
указатель на каждую виртуальную функцию.
Если
вызывается (невиртуальная) составляющаяа функция,
например a b . f ( x ) адрес объекта передается как скрытый
аргумент : f ( & a b , x ). Таким образом, вызов составляющей
функции по меньшей мере столь же эффективен, как и вызов любой
другой функции. Вызов виртуальной функции p - = f ( x ) ,
к примеру , эквивалентен косвенному вызову
и к и Ё - - virtual - ¦ ¦ й й ( p,x ). Обычно это
требует на ¦ обращения к памяти больше, чем вызов эквивалентной
невиртуальной функции.
Если издержки вызова функции неприемлемы для операции над
объектом класса,а операция может быть реализована как включаемая
функция, таким образом достигая такой же эффективности
выполнения, как если бы доступ к объекту осуществлялся
непосредственно.
22. РЕАЛИЗАЦИЯ И СОВМЕСТИМОСТЬ.
Синтаксический
анализатор компилятора С++, c f r o n t м
состоит из программы синтаксического разбора на языке YACC [8]
программы на С++. Классы использовались очень широко. Он
примерно такого же размера, как эквивалентная часть компилятора
РСС для старого С (13,501 строк, включая комментарии).
Он выполняется немного быстрее, но использует больше памяти.
Количество используемой памяти зависит от числа внешних
переменных и размера наибольшей функции. Он не может выполняться
на машинах со 128К-байтовым адресным пространством (типа
PDP-11/71 (*1) ; более подходящее количество требуемой памяти
примерно раза в три больше. Выходом является код во внутреннем
представлении, полностью прошедший типовой контроль. Затем он
может быть преобразован в подходящий ввод для старого и нового
кодогенераторов.В частности, можно получить версию любой С++ -
программы на "старом" С. Это обстоятельство делает тривиальным
перенос c f r o n t в любую систему со старым С-компилятором.
С некоторыми исключениями компилятор С++ понимает старый
С.
Среда времени выполнения, соглашения по редактору связей, методы
спецификации раздельной компиляции остаются без изменений. Самая
главная несовместимость состоит в том, что описание функции,
например:
int
f();
в старом С объявляют
функцию с неизвестным числом аргументов
неизвестных типов.В С++ данное описание определяет,что f не
принимает аргументов. Существует С++ - версия описаний стандартных
библиотек, такжеа разрабатывается программаа для получения
"отсутствующих описаний" для набора исходных файлов. Другое
отличие состоит в том, что в С++ нелокальное имя может быть
использовано только в том файле, где оно определено, если только
оно явно не объявлено внешним ; в старом С нелокальное имя
является общим для всех файлов (многофайловой) программы, если
только оно явно не объявлено статическим. Некоторые
незначительные неприятности могут вызвать имена, совпадающие с
новыми ключевыми словами: c l a s s , c o n s t , d e l e t e,
f r i e n d , i n l i n e , n e w , o p e r a t o r ,
o v e r l o a d , p u b l i c , t h i s , v i r t u a l .
Часто утверждается, что одним из достоинст в С является
то,
что он настолько мал, что любой программист понимает каждую
конструкцию языка. Напротив, такие языки, как PL/1 и Ада,
представляют так, что, якобы, каждый программист пишет на своем
собственном подмножестве языка и весьма с большим трудом понимает
программы, написанные другими. Отсюда следует, что расширять С -
плохо. Этот аргумент против "больших" языков игнорирует тот
простой факт, что зависимости между структурами данныха и
функциями, использующими их, существуют в программе независимо от
того, входят функции или не входят в описание класса.
_________________________
(*1) - Торговая марка Digital Equipment Corporation.
Программы, использующие
классы, имеют тенденцию к тому, чтобы быть
неожиданно короче, чем их неструктурированные эквиваленты (обычно
- от 1 до 10 короче; имело место и 50%-ое сокращение). Более
того, С уже достаточно большой язык для того, чтобы существовали
"субкультуры", использующие подмножества языка,
макровозможности часто используются для создания сомнительных,
маловразумительных вариаций языка С.
Руководство
по c f r o n t всего на 14 длиннее руководства
по старому С, так что усилия по овладению новыми возможностями
языка не должны быть непосильноа велики. В частности, усилия
должны быть незначительны по сравнению с изучением нового языка
со свойствами абстракции данных. Однако в случае, если классы
используются для создания нового типа данных, фактически создается
новый диалект языка. Это ведет к созданию различных несовместимых
"диалектов". Это не так уж сильно отличается от существующего
состояния дел, и есть надежда, что "стандартные" классы,
предоставляющие базовые возможности типа в/в, множеств, таблиц,
строк, графикии т.п.м обретут широкое использование.
23. СРАВНЕНИЕ С ДРУГИМИ ЯЗЫКАМИ.
Сравнение
двух языков занимает целую статью, если не книгу.
Следовательно, в данной секции отражено только несколько
персональных замечаний к основным сторонам различия между
языками. Для создания целостной картины С сам критикуется в том
же духе, что и другие языки.
Конструкции
классов С основаны на оригинальных классах
Simula68 [6,7]. Simulа полагается на сборку мусора как для
объектов класса, так и для записей активации процедур, и не
предоставляет возможностей по перегрузке имен функций и знаков
операций. Это, однако, наиболее красивыйи выразительный язык,и
классы С обязаны больше именно этому, не какому-либо другому
языку.
Smalltalk
[9] - еще один язык с подобными возможностями
виртуальными и контроль типов осуществляется во время выполнения.
Это значит, что там, где базовый класс С предоставляет
фиксированныйа интерфейсс типовым контролем для множества
производных классов, суперкласс языка Smalltalk предлагает
нетипизированное множество возможностей, которое может быть
произвольно модифицировано. Smalltalk полагается на сборку мусора
и динамическое разрешение имен составляющих функций. Он не
предоставляет возможности перегрузки знаков операций в обычном
смысле, но знак операции может быть именем составляющей функции.
Smalltalk предоставляетв высшей степени хорошо организованную
среду для конструирования программ, однако в очень сильной
степени зависит от ресурсов.
Modula-2
[10] предоставляет рудиментарную возможность
абстракции, называемую модулем. Модуль не является типом,
но является единым объектом, содержащим данныеи функции, имеющих
доступ к этим данным. Он в чем-то подобен классу со всеми данными
статическими. Нет возможностей, эквивалентных производным
классам. Он не допускает перегрузки имен функций или знаков
операций. Нет сборки мусора.
Модули
языка mesс [11] отличаются полным и гибким
разделением интерфейса модуля от его реализации. Это дает
возможность раздельной компиляции, но и требует не тривиальной
реализации связывания. Модуль может импортировать и
экспортировать имена как процедур, так и типов. Правила
конкретизации модулей (создание объектов и инициализация)
настолько общи, что делают их элегантными. Использование модулей
приводит к некоторым издержкам по памятии времени исполнения.
Нет возможностей по созданию иерархии модулей и по перегрузке
знаков операций. Mesса основывается на сборке мусора как для
объектов данных, так и для записей активных процедур.
Следовательно, он может эффективно использоваться только там, где
имеется аппаратная поддержка для сборки мусора.
С не предоставляет законченной среды по редактированию,
отладке, управлению раздельной компиляцией или управлению
исходным кодом. Программная среда С под управлением OC UNIX [1,8]
предоставляет набор средств таких возможностей, однако оставляет
желать много лучшего. Не предоставляется сборка мусора. Классы С
отличаются сочетанием возможностей по созданию объектов и
инициализации. Возможности по перегрузке присваивания и передаче
аргументов в С являются уникальными.
24. ЗАКЛЮЧЕНИЕ.
Добавление
классов представляет собой качественный скачок
языка С, наименьшее расширение, которое предоставляет возможности
по абстракции данных для системного программирования. Опыт трех
лет с промежуточными версиями ("С классами") демонстрирует как
полезность классов, так и необходимостьва более общих
возможностях, чем здесь представление. Эффективность как
компилируемого кода, так и самого компилятора, сравнимы (в пользу С++)
со старым С.
ЛИТЕРАТУРА.
1. B.W.Kernighan
and D.M.Ritchi. The С Programing Language,
Englewood Cliffs, NJ: Prentice Hall, 1978.
2. G.Orwell, 1984,
London: Secker and Warburg, 1949.
3. B.Stroustrup.
C++ Referenct Manual Murr. Hill NJ: AT&T Bell
Laboratories CSTR-108, January 1, 1984.
4. B.Stroustrup,
"Classes: An Abstract Data Type Faciliti for the
Language", ACM SIGPLAN Notices 17, No 1 Januar 1982. pp.42-52.
5. B.Strousrup,
"Adding Classes to C An Exercise in Language Evolution",
Software Practice and Experience, 1 (1983),pp.139-161.
6. O-J.Dahl and
C.A.Hoare, Hierarchic Programm Structures
Structured Programming, New York; Academiу Press, 1972,
pp.174-220.
7. O-J.Danl, B.Myrhaug
and K.Nygaard, SIMULA Common Base Language,
Oslo, Norway: Norwegian Computing Center, S-22, 1970.
8. Unix Programmer's,
Manual Murr Hill, NJ: AT&T Bell
Laboratories, 1979.
9. A.Goldberg and
D.Robson, Smalltalk-81 The Language and Its
Implementation, Reading, MA: Addison Wesley, 1983.
10. N.wirth. Programming
in Modula-2. Berlin: Springer-Verlagм 1982.
11. J.C.Michell
et al. Mesс Referenct Manual, Palo Altoм CA:
Xerox PARC CSL-79-3, 1979.
12. В оригинале
статьи ссылка на источника отсутствует. -прим.переводчика.
07/26/89 08:42am.
|