М. Бен-Ари - Языки программирования. Практический сравнительный анализ (2000) (1160781), страница 44
Текст из файла (страница 44)
private:
void echo(char с);
friend class Keyboard; // Разрешить классу Keyboard вызывать echo
};
class Keyboard {
private:
void put_key(char c);
friend class Display; // Разрешить классу Display вызывать put_key
};
Использование механизма friend позволяет избежать как создания неоправданно большого числа открытых (public) подпрограмм, так и объединения двух классов в один большой класс только потому, что они имеют одну-един-ственную общую операцию.
С помощью friend можно также решить проблему синтаксиса, связанную с тем фактом, что подпрограмма в классе C++ имеет отличимый получатель, такой как obj1 при вызове obj1.proc(obj2). Это привносит в подпрограммы асимметрию, в противном случае они были бы симметричны по параметрам. Стандартный пример — перегрузка арифметических операций. Предположим, что мы хотим перегрузить «+» для комплексных чисел и в то же время позволить операции неявно преобразовать параметр с плавающей точкой в комплексное значение:
complex operator + (float);
complex operator + (complex);
Рассмотрим выражение х + у, где одна из переменных (х или у) может быть с плавающей точкой, а другая комплексной. Первое объявление правильно для комплексного х и плавающего у, потому что х+у эквивалентно x.operator+(y), и, стало быть, будет диспетчеризованно отличимому получателю комплексного типа. Однако второе объявление для х+у, где х имеет тип с плавающей точкой, приведет к попытке диспетчеризоваться к операции с плавающей точкой, но операция была объявлена в комплексном классе.
Решение состоит в том, чтобы объявить эти операции как «друзей» класса, а не как операции класса:
friend complex operator + (complex, complex);
friend complex operator + (complex, float);
friend complex operator + (float, complex);
Хотя эта конструкция популярна в языке C++, на самом деле существует лучшее решение, при котором не требуется friend.
Оператор «+=» можно определить как функцию-член (см. справочное руководство, стр. 249), а затем «+» можно определить как обычную функцию за пределами класса:
complex operator + (float left, complex right)
{
complex result = complex(left);
result + = right; // Результат является отличимым получателем
return result;
}
Спецификаторы доступа в языке C++
Когда один класс порождается из другого, мы вправе спросить, имеет ли производный класс доступ к компонентам базового класса. В следующем примере database (база данных) объявлена как приватная, поэтому она недоступна в производном классе:
class Airplanes {
private:
Airplane_Data database [100];
};
class Jets : public Airplanes {
void process Jet(int);
};
void Jets::process_jet(int i)
{
Airplane_Data d = database[i] ; // Ошибка, нет доступа!
};
Если объявлен экземпляр класса Jets, он будет содержать память для database, но этот компонент недоступен для любой подпрограммы в производном классе.
Есть три спецификатора доступа в языке C++:
• Общий (public) компонент доступен для любого пользователя класса.
• Защищенный (protected) компонент доступен внутри данного класса и внутри производного класса.
• Приватный компонент доступен только внутри класса.
В примере, если database просто защищенный, а не приватный член класса, к нему можно обращаться из производного класса Jets:
class Airplanes {
protected:
Airplane_Data database[100];
};
class Jets : public Airplanes {
void process_jet(int);
};
void Jets::process_jet(int i)
{
Airplane_Data d = database[i]; // Правильно, в производном классе
};
Однако это не очень хорошая идея, потому что она ставит под удар абстракцию. Вероятно, было бы лучше даже для производного класса манипулировать унаследованными компонентами, используя общие или защищенные подпрограммы. Тогда, если внутреннее представление изменяется, нужно изменить только несколько подпрограмм.
Язык C++ допускает изменение доступности компонентов класса при объявлении производного класса. Обычно порождение бывает общим (public). Так было во всех наших примерах, и при этом сохранялась доступность, заданная в базовом класс. Однако вы также можете задать приватное порождение, тогда и общие, и защищенные компоненты становятся приватными:
class Airplanes {
protected:
Airplane_Data database [100];
};
class Jets : private Airplanes { // Приватное порождение
void process_jet(int);
};
void Jets::process_jet(int i)
{
Airplane_Data d = database[i]; // Ошибка, нет доступа
};
Пакеты-дети в языке Ada
В языке Ada только тело пакета имеет доступ к приватным объявлениям. Это делает невозможным непосредственное совместное использование пакетами приватных объявлений так, как это можно делать в языке C++ с защищенными объявлениями. В языке Ada 95 для совместного использования приватных объявлений доставлено специальное средство структурирования, так называемые пакеты-дети (child packages). Здесь мы ограничим обсуждение пакетов-детей только для этой цели, хотя они чрезвычайно полезны в любой ситуации, когда вы хотите расширить существующий пакет без его изменения или перекомпиляции.
Зададим приватный тип Airplane_Data, определенный в пакете:
package Airptane_Package is
type Airplane_Data is tagged private;
private
type Airplane_Data is tagged
record
ID:String(1..80);
Speed: Integer range 0.. 1000;
Altitude: Integer 0.. 100;
end record;
end Airplane_Package;
Этот тип может быть расширен в пакете-ребенке:
package Airplane_Package.SST_Package is
type SST_Data is tagged private;
procedure Set_Speed(A: in out SST_Data; I: in Integer);
private
type SST.Data is new Airplane_Data with
record
Mach: Float;
end record;
end Airplane_Package.SST_Package;
Если задан пакет P1 и его ребенок Р1 .Р2, то Р2 принадлежит области родителя Р1, как если бы он был объявлен сразу после спецификации родителя. Внутри закрытой части и теле ребенка видимы приватные объявления родителя:
package body Airplane_Package.SST_Package is
procedure Set_Speed(A: in out SST_Data; I: in Integer) is
begin
A.Speed := I; -- Правильно, приватное поле в родителе
end Set_Speed;
end Airplane_Package.SST_Package;
Конечно, общая часть ребенка не может обращаться к закрытой части родителя, иначе ребенок мог бы раскрыть секреты родительского пакета.
15.3. Данные класса
Конструкторы и деструкторы
Конструктор (constructor) — это подпрограмма, которая вызывается, когда создается объект класса; когда объект уничтожается, вызывается деструктор (destructor). Фактически, каждый объект (переменная), определенный в каком-либо языке, требует выполнения некоторой обработки при создании и уничтожении переменной хотя бы для выделения и освобождения памяти. В объектно-ориентированных языках программист может задать такую обработку.
Конструкторы и деструкторы в языке C++ могут быть определены для любого класса; фактически, если вы не определяете их сами, компилятор обеспечит предусмотренные по умолчанию. Синтаксически конструктор — это подпрограмма с именем класса, а деструктор — то же имя с префиксным символом «~»:
class Airplanes {
private:
C++ |
int current_airplanes;
public:
Airplanes(int i = 0): current_airplanes(i) {};
~Airplanes();
};
После создания базы данных Airplanes число самолетов получает значение параметра i, который по умолчанию имеет значение ноль:
Airplanes а1 (15); // current_airplanes =15
Airplanes a2; //current_airplanes = О
Когда база данных удаляется, будет выполнен код деструктора (не показанный). Можно определить несколько конструкторов, которые перегружаются на сигнатурах параметров:
class Airplanes {
public:
Airplanes(int i = 0): current_airplanes(i) {};
C++ |
~Airptartes();
};
Airplanes a3(5,6); // current_airplanes = 11
В языке C++ также есть конструктор копирования (copy constructor), который дает возможность программисту задать свою обработку для случая, когда объект инициализируется значением существующего объекта или, в более общем случае, когда один объект присваивается другому. Полное определение конструкторов и деструкторов в языке C++ довольно сложное; более подробно см. гл. 12 справочного руководства по языку C++.
В языке Ada 95 явные конструкторы и деструкторы обычно не объявляются. Для простой инициализации переменных достаточно использовать значения по умолчанию для полей записи:
type Airplanes is tagged
record
Current_Airplanes: Integer := 0;
end record;
Ada |
или дискриминанты (см. раздел 10.4):
type Airplanes(lnitial: Integer) is tagged
record
Current_Airplanes: Integer := Initial;
end record;
Программист может определить свои обработчики, порождая тип из абстрактного типа, называемого управляемым (Controlled). Этот тип обеспечивает абстрактные подпрограммы для Инициализации (Initialization), Завершения (Finalization) и Корректировки (Adjust) для присваивания, которые вы можете заместить нужными вам программами. За деталями нужно обратиться к пакету Ada. Finalization, описанному в разделе 7.6 справочного руководства по языку Ada.
Class-wide-объекты
Память распределяется для каждого экземпляра класса:
C++ |
chars[100];
};
С с1,с2; //по 100 символов для с1 и с2
Иногда полезно иметь переменную, которая является общей для всех экземпляров класса. Например, чтобы присвоить порядковый номер каждому экземпляру, можно было бы завести переменную last для записи последнего присвоенного номера. В языке Ada это явно делается с помощью включения обычного объявления переменной в теле пакета:
package body P is
Last: Integer := 0;
Ada |
в то время как в языке'C++ нужно воспользоваться другим синтаксисом:
class С {
C++ |
chars[100];
};
int C::last = 0; // Определение, доступное за пределами файла
Спецификатор static в данном случае означает, что будет заведен один CW-объект*. Вы должны явно определить компонент static за пределами определения класса. Обратите внимание, что статический (static) компонент класса имеет внешнее связывание и может быть доступен из других файлов, в отличие от статического объявления в области файла.
Преобразование вверх и вниз
В разделе 14.4 мы описали, как в языке C++ значение порожденного класса может быть неявно преобразовано в значение базового класса. Это называется преобразованием вверх (up-conversion), потому что преобразование делается вверх от потомка к любому из его предков. Это также называется сужением (narrowing), Потому что производный тип «широкий» (так как он имеет дополнительные поля), в то время как базовый тип «узкий», он имеет только поля, которые являются общими для всех типов в производном семействе. Запомните, что преобразование вверх происходит только, когда значение производного типа непосредственно присваивается переменной базового типа, а не когда указатель присваивается от одной переменной другой.
Преобразование вниз (down-conversion) от значения базового типа к значению производного типа не допускается, поскольку мы не знаем, какие значения включить в дополнительные поля. Рассмотрим, однако, указатель на базовый тип: