лекции (2007) (1160825), страница 9
Текст из файла (страница 9)
Сomplex operator * (Complex a &, Complex b &) {return Complex(a.Re * b.Re, a.Im * b.Im);}
Теперь выражения с комплексными числами можно будет записывать просто как "a + b * c" и т.п. Но что делать, если какое-либо из чисел a, b или c не является комплексным, а имеет тип, например, int? Перегружать необходимые операторы для всех комбинаций возможных типов? Ведь возможных комбинаций могут быть десятки. Гораздо разумнее использовать неявные преобразования типов, а значит, и конструкторы преобразования. Для рассматриваемого примера достаточно определить всего один конструктор преобразования (для полной гибкости их потребуется несколько), к тому же для большей универсальности можно использовать параметры по умолчанию:
Complex (int a = 0, int b = 0) {Re = a; Im = b;}
Теперь все операции над комплексными числами можно записывать в виде вроде "a + b * c".
Однако и конструкторов преобразования не всегда достаточно. Например, может потребоваться преобразование из типа Complex, скажем, обратно в тип float (). Тогда в классе Complex необходимо определить оператор преобразования:
operator float () {return Re;}
Теперь можно переводить Complex во float:
float a;
Complex b(1, 1);
a = b;
Однако с неявными преобразованиями следует быть осторожными. Например, если программист совершит ошибку и присвоит объекту своего типа значение какого-то другого типа, то он может и не получить сообщение об ошибке: если есть конструктор преобразования, то он сработает, и вместо старого объекта, который будет потерян, дальше пойдёт новый, чего программист не хотел. Поэтому в C++ введено ключевое слово "explicit" которое показывает, что explicit-конструктор не будет вызываться при неявных преобразованиях (только явно).
С деструкторами всё проще, чем с конструкторами (в С++ деструктор в классе X описывается как ~X() {...} ): при уничтожении объекта класса иногда может потребоваться, например, освободить динамическую память, или выполнить какое-либо дополнительное действие (например, уменьшить счётчик объектов этого класса). Тогда имеет смысл писать деструктор.
В C++ конструкторы и деструкторы выполняются:
1) для статических объектов: конструкторы - до начала выполнения программы, деструкторы - после её завершения;
2) для квазистатических объектов: конструкторы - при входе в блок, деструкторы - при выходе из него;
3) для динамических объектов: конструкторы - при вызове new, деструкторы - при вызове delete;
Это всё относится к языку C++. В остальных языках всё проще.
Например, в Java: запрещены неявные преобразования, поэтому нет конструкторов преобразования. Запрещено и перекрытие операций, поэтому нет и конструкторов копирования; копирование в Java - это просто побитовое копирование. Если необходимо глубокое копирование, то необходимо использовать метод clone (), перекрыв его.
Глава 6. Инкапсуляция. Абстрактные типы данных.
Абстрактный ТД = множество операций; абстрактный класс - это, вообще говоря, не то же самое, что АТД.
Инкапсуляция - это сокрытие данных или операций для того, чтобы запретить к ним доступ извне. Кроме того, существует такое понятие, как защита. Оно, в свою очередь, состоит из двух вложенных понятий:
1) Единица защиты - под ней понимают либо весь тип данных (все объекты одного типа защищены одинаково), либо отдельный его экземпляр (возможны разные стратегии защиты для разных объектов одного типа)
2) Атом защиты - это либо, опять же, весь тип данных (как в языках Ada и Modula-2 - либо полностью открыт, либо полностью закрыт), либо отдельные его элементы-члены (как во всех основных объектно-ориентированных ЯП, а также в языке Oberon)
Сначала рассмотрим подход к инкапсуляции в модульных языках. В языке Modula-2 атомом защиты является весь ТД. Вот как это выглядит:
Открытый вариант:
DEFINITION MODULE M;
TYPE T = ...;
...
END M;
Закрытый вариант (cкрытый тип данных в Modula-2 называется "opaque"):
DEFINITION MODULE M;
TYPE T;
...
END M;
IMPLEMENTATION MODULE M;
TYPE T = ...;
...
END M;
То есть в случае, когда тип открыт, скрытой остаётся только реализация операций, а структура ТД остаётся открытой; если же ТД является скрытым, то о нём известно только его имя. В связи с этим возникает проблема выделения памяти под скрытые типы данных. Поэтому на скрытые типы данных в Modula-2 накладывается дополнительное ограничение: они должны либо выглядеть, как указатели, либо быть совместимы с указателями по памяти (как, например, целый тип данных INTEGER). Кроме того, при такой реализации к скрытым ТД могут применимы не только операции, описанные в модуле определений, но и операции, обычно производимые над указателями: присваивание (поверхностное) и сравнение "равно - не равно".
В Ada атомом защиты тоже является ТД, но проблема выделения памяти под скрытые типы данных решена более изящно: структура данных описывается в специальной private-части определения, использовать которую может только компилятор. При этом ТД помечается ключевым словом "private" (приватный тип) или "limited private" (ограниченный приватный тип). Различие между ними в том, что к типам, помеченным как limited private, применимы только операции, определённые в описании, а к private-типам помимо них применимы ещё и операции присваивания и сравнения. Таким образом, только ограниченные приватные типы абсолютно точно воплощают концепцию абстрактных типов данных, так как именно в них разрешены только явно описанные операции.
Вот так, например, может выглядеть определение и реализация стека на языке Ada:
package Stacks is
...
type Stack is limited private;
procedure Pop (S : in out Stack ; X : out T);
procedure Push (S : in out Stack ; X : in T);
...
private
type Stack is access;
type Link is record
x : T;
next : Stack;
end record;
type Stack is access Link;
end Stacks;
package body Stacks is
...
procedure Pop (S : in out Stack ; X : out T) is ...
procedure Push (S : in out Stack ; X : in T) is ...
...
private
N: const Integer := 128;
type Stack is record
st_body : array (1..N) of T;
top : Integer := 1;
end record;
end Stacks;
В языке Oberon, в отличие от Modula-2 и Ada, атомом защиты является не весь тип данных, а его элементы. При этом те элементы, которые необходимо открыть, помечаются с помощью "*" и "*-" ("*-" означает, что элемент доступен только на чтение). Вот как выглядит определение всё того же стека в Oberon:
MODULE Stacks
TYPE Stack* = RECORD
Body : ARRAY 128 OF T;
Top : INTEGER;
Visible_element* : INTEGER;
END Stack;
...
END Stacks
...
IMPORT Stacks;
ST : M.T;
Таким образом, в Oberon АТД реализуется достаточно просто - необходимо закрыть доступ ко всем членам-данным и оставить открытым набор операций.
В языках с классами принято говорить не о видимости, а о правах доступа: структура всего класса видна, а вот использовать можно только те члены класса, к которым есть доступ. В C++ для управления доступом используются модификаторы "public", "private" и "protected". Соответственно, к public-членам доступ есть везде, к private-членам доступ возможен только внутри класса, а к protected-членам доступ возможен внутри класса и унаследованных от него классов. Эта схема очень проста, но не всегда удобна. Например, можно рассмотреть ситуацию, когда есть класс Complex и необходимо определить операцию сложения двух комплексных чисел a и b. Для этой операции необходимо иметь доступ к целой и вещественной части комплексных чисел, а значит, необходимо либо отказываться от инкапсуляции этих членов, что не есть хорошо, либо реализовать эту операцию "несимметрично", как a.operator+(b). Но при таком подходе не получится реализовать сложение без побочного эффекта, не меняющее оба своих операнда, а возвращающее третье значение.
Для преодоления этой проблемы в C++ реализован механизм дружественности ("friend"). Друзья класса - это функции или другие классы, которые описаны либо глобально, либо в других классах. Друзья имеют полный доступ ко всем членам класса:
class Complex
{
...
friend Complex& operator+ (Complex& a, Complex &b);
friend void Other_class_1 :: f ();
friend Other_class_2;
...
}
Необходимо заметить, что отношение дружественности не является транзитивным, то есть если класс Y - друг класса X, а класс Z - друг класса Y, то класс Z не становится другом класса X.
Примерно такая же схема инкапсуляции, только усовершенствованная, используется и в C#, и в Java, и в Delphi. Недостатком системы инкапсуляции в C++ была некоторая разрозненность файловой системы: код программы группируется только по классам и файлам. В современных объектно-ориентированных ЯП широко используется такое понятие, как проект; это некоторая совокупность логически близких файлов. На языковом уровне такое межфайловое объединение реализовано с помощью таких понятий, как пространство имён в C# ("namespace"), пакет в Java ("package") и модуль в Delphi ("unit"). Разумеется, во всех этих языках присутствуют атрибуты public, private и protected, с небольшими отличиями в свойствах.
Глава 7. Раздельная трансляция.
Различают несколько видов трансляции:
1) Пошаговая трансляция - программа подаётся компилятору небольшими частями, и он последовательно ("пошагово") эти части транслирует. Пример - транслятор языка Basic, где на каждом шаге обрабатывается один оператор языка. Пошаговая трансляция применяется только в языках с простой структурой, так как она крайне неэффективна. Самый простой пример - обработка циклов; цикл будет транслироваться столько раз, сколько в нём итераций:
LET I = 1
...
PRINT I
...
FOR I = 1 TO N
...
NEXT I
2) Инкрементная трансляция - эволюционное развитие пошаговой. Программа, опять же, разбиваются на части, и каждая часть транслируется отдельно. Кроме того, "части" при инкрементной трансляции обычно больше, чем при пошаговой, и могут быть в определённом смысле логически завершёнными (например, когда встречается цикл, имеет смысл размещать его целиком в одной "части").
3) Цельная трансляция - компилятор сразу получает для трансляции всю программу. Очевидно, что такой подход неприменим для промышленного программирования, а может использоваться только для небольших программ. Цельная трансляция реализована, например, в языке Pascal.
4) Раздельная трансляция - программа разбивается на физически независимые части-модули (которые принято называть "единицами компиляции", ЕК), и эти части предъявляются компилятору. Раздельная трансляция разделяется на раздельную независимую трансляцию и раздельную зависимую трансляцию.
При независимой трансляции компилятору единовременно доступна только одна единица компиляции; соответственно, при зависимой трансляции помимо обрабатываемого модуля компилятору доступна информация о других модулях. Тем не менее, в обоих случаях для единицы компиляции возникает такое понятие, как "контекст трансляции" (КТ) - это совокупность имён (возможно, с какой-то дополнительной информацией о них), которые необходимы для трансляции этой единицы компиляции. Различие состоит в том, что при независимой трансляции программист должен сам налаживать межмодульные связи с помощью языковых средств (например, "extern" в C++), а при зависимой трансляции компилятор сам создаёт, хранит и обновляет соответствующие КТ. Контексты трансляции хранятся в так называемой трансляционной библиотеке (ТБ), а сами модули - в программной библиотеке (ПБ). Таким образом, при независимой трансляции компилятор обрабатывает только ЕК (в которые программист добавляет КТ), а при зависимой трансляции компилятор взаимодействует и с ЕК, и со всей трансляционной библиотекой, обновляя программную библиотеку.