лекции (2004) (1160823), страница 16
Текст из файла (страница 16)
String &operator = (String&);
};
String(String &S)
{
body = new char [S.Length() + 1];
strcpy(body, S.body);
}
String &operator = (String& S)
{
delete [] body;
body = new char [S.Length() + 1];
strcpy(body, S.body);
}
// если копировать строку саму в себя, то будет ошибка
3. Конструктор преобразования
X(T)
X(T&)
X(const T&)
Т отличен от Х!
В языке Си++ допустимы неявные преобразования (это преобразования, которые вставляются не программистом, а компилятором). Стоит заметить, что неявные преобразования - довольно опасная вещь.
class Vector {
....
Vector (int size);//конструктор, кол-во объектов в данном классе
T& operator [] (int i);//оператор индексирования
}
Vector V(20);
Vector X(10);
похоже на:
T V[20];
T X[10];
но в отличие от обычных массивов :
V = X;
V = 3; // ошибка, но не с точки зрения компилятора - он считает, что это V = Vector(3), отыскивая пользовательские конструкторы копирования;
Для этих случаев в Си++ создан модификатор explicit - невозможность использования неявных преобразований.
В языке Java разрешено всего два неявных преобразования: преобразование к типу данных Object и вызов метода ToString.
Нет неявных преобразований в Delphi, Ada.
Возникает вопрос: почему в языке Си++ неявные преобразования очень сильно распространены?
Рассмотрим пример вычисления выражения:
A = B + C * exp(I*X);
На языке Си без неявных преобразований:
A = Plus(B, Mult(C, EXP(Mult(I,X)))).
При добавлении комплексного типа данных, проблема при перегрузке операторов: большое кол-во вариантов, из-за возможных комбинаций типов.
При использовании неявных преобразований все становится проще: 11 конструкторов преобразования и варианты ф-ий.
Си# неявные преобразования разрешены.
4. Все остальные конструкторы (никакой особой семантики нет).
2).Деструктор
Деструкторы бывают двух видов:
1. Деструктор умолчания;
2. Пользовательский деструктор.
Посмотрим, как дела обстоят в остальных языках.
Cи++ все конструкторы.
В языке Cи# и Java есть конструктор умолчания. А вот конструктора копирования у них нет! Потому что у них нет побитовой семантики по определению - вместо объектов там все операции идут с указателями на объекты (a = b) - ссылочная семантика.
А для копирования есть метод Object Clone()- возвращает ссылку на объект. Для копирования- переопределить Object Clone().
Конструкторы преобразования в Java и Delphi отсутствуют, в Cи# преобразование делается исключительно при помощи операторов преобразования.
В силу ссылочной семантики конструктор вызывается явно.
Си# - base(...);
Java - Super(); от smalltalk
Во всех этих языках есть инициализаторы при членах (T x = e), выполняется она сверху вниз(в порядке, в котором они расположены в тексте).
static {...блок с любыми присваиваниями статическим членам класса вызывается, когда инициализируется первый статический член данного класса}; - статический инициализатор.
Если в языке Cи# есть у конструктора static, то это статический конструктор и он аналогичен статическому инициализатору в языке Java . Этот статический конструктор - конструктор умолчания .Т.к. инициализация явная и никаких параметров нет.
Для класса vector конструктор умолчания нецелесообразен, т.к. нет кол-ва элементов по умолчанию.
Если кто-то берёт и унаследует класс Vector, создавая класс X (c переменной int &k ). Должна быть специальная конструкция для вызова конструкторов баз и подчленов (Vector(20), k(i)).
В Си++ следующая семантика:
конструктор:
X(int i):Vector(20), k(i) {
k = i;// операция присваивания--> нужна явная инициализация
}
...
Си# - base(...);- в Си нельзя, т.к. баз может быть много.
Java - Super();
В языке Delphi немного другой синтаксис. Есть специальные ключевые слова constructor и destructor. Как правило, их называют Create и Free соответственно (сделано это по примеру объекта TObject).
Нет умолчания, т.к. все явно.
i:X;
i := X.Create(..);
i.Free;
Inherited имя метода - вызов родительского метода(конструктор и деструктор, соблюдая порядок).
Т.к. нет множественного наследования.
С деструкторами всё тоже просто - есть только обычные деструкторы (деструкторы умолчания есть только в Си++!). Явный вызов в Delphi - i.Free .
В языке Java есть защищенный метод void finalize() {...} - вызывается тогда, когда объект уходит из памяти, когда его уничтожает сборщик мусора. Объект удаляется из памяти, когда не хватает места, но он может быть и не удален--> гарантировать время освобождения объекта нельзя.
Идеологически работы с ресурсами в Си++:
Конструктор - захват ресурса, деструктор - освобождение ресурса. И каждому ресурсу ставится в соответствие какой-то класс.
смена курсора на песочные часы:
{
CWaitCursor c;// смена курсора с помощью конструктора и деструктора
long op
}
Java и Cи#
try {
...
} finally
{
.. // здесь код будет выполнен в любом случае, даже если в блоке try произошло исключение
}
Т.е.
try {
X = захват
} finally {
if (X != null)
X.Dispose();
}
C# IDisposable Dispose();- вызывается, если ресурс надо освободить, ибо деструктор может быть и не вызван.
using (инициализация объекта - выражение - захват ресурса: X = new XX())
{
блок: эквивалентно
try {X = new XX();
...
} finally {
if (X != null) X.Dispose;
}
}- гарантирует, что объект будет освобожден.
Языки программирования.Лекция 14.
Классы (продолжение).
Мы говорили о специальных функциях-членах. Многие привлекательные черты, обусловленные наличием классов, связаны именно с наличием специальных функций.
Си++ отличается от Си несколькими плюсами, причём двумя ():
-
Первый плюс - наличие механизма классов. Класс - языковая конструкция как тип и как контейнер для данных других типов, функций и т.п.
-
Второй плюс – наличие объектно-ориентированных свойств: наследования, динамического полморфизма и т.п.
В прошлый раз мы разобрали конструкторы (вызывается автоматически компилятором, когда объект создается) и деструкторы (вызывается автоматически компилятором, когда объект уходит). Конструктор решает проблемы инициализации, копирования и преобразования.
3).Оператор преобразования.
-
Преобразование - оператор небезопасный, явные преобразования никаких проблем никогда не вызывают:
a b // разных типов
a = T(b)// явное преобразование
Вспомним конструктор преобразования X(T), который делает соответственно из объекта Т объект класса Х.
T a;
X b;
пример1.
int i;
double d;
d = i;// трактуется, как d = (double)i; если i соответственно типа int, а d типа double
В Си++ есть понятие стандартных неявных преобразований, тут мы видим стандартное преобразование, которое определено в языке, и семантика которого подробно расписана. Аналогично в Си++ есть обратные преобразования – из double в int (может привести к потери информации).
пример2.
Vector(int) // int - количество элементов в векторе (см.предыдущую лекцию)
V v;
v = 5; //трактуется как неявное преобразование
Компилятор не находит никакого стандартного преобразования из int в vector, оно не может быть стандартным (встроенным в компилятор, поскольку vector принадлежит не языку, а стандартной библиотеке). Компилятор ищет пользовательское преобразование, которое может быть соответствующим конструктором Vector(int) - и он его находит (никакого осмысленного преобразования из int в vector нет, это преобразование случайно).
В общем случае эта конструкция не безопасна. Это решается специальным указанием explicit (явный), ставится перед соответствующим конструктором (Х(Т), Х(Т&), X(const T&)), это означает, что этот конструктор теряет свойства конструктора-преобразования. Он может вызываться только явно. По умолчанию этого указания нет и это плохо.
В Си++ появились соответствующие преобразования, потому что некоторые типы данных ввести в язык совершенно безопасным образом, и так, чтобы они не отличались от стандартных – это без неявных операций преобразований сделать не возможно. В большинстве случаев операторы преобразования нужны, когда есть устоявшийся тип данных, у которого есть набор операций, который частично пересекается с набором стандартных операций ЯП.
Примеры:
-
Тип данных complex
-
Тип данных множество
Наиболее частые преобразования – это преобразования из строк языка Си: char* (ASCIZ) в String и обратно.
-
Посмотрим, как ещё организованы преобразования.
Неявные преобразования задаваемы пользователем есть только в Си++ и в Си#. В Дельфи и Java есть неявное преобразование в строковой тип. В Java для базового типа Object есть преобразование toString, которое может быть переопределено для любого класса. Другие неявные преобразования, определяемые пользователем, отсутствуют. Как следствие в Java тип данных complex нельзя ввести таким же удобным способом как в Си++.
Конструкторы преобразования не могут служить универсальным средством преобразования, потому что они всегда из какого-то тд Т (стандартного или пользовательского) делают новый тд Х, тд который мы описываем и описываем его конструктор.
char* T -> String X
А для обратного преобразования мы уже можем не написать преобразование. Если объект Т не является классом, либо он является классом, который мы не можем модифицировать (библиотечный, без исходного текста). Мы можем использовать специальные функции, которые называются операторами преобразования.
В Си++:
class X {
operator type(); //возвращаемого тип нет, так как соответствующее имя типа само по себе говорит какой тип возвращается
//нет аргумента, точнее есть один – this
//обязательно не статическая функция-член
};
пример: для string – char*
class String {
operator char* ();
}
Стандартная ошибка начинающего программиста – наивная реализация тд String, когда возвращает char*, если речь идет о временном объекте (неявное преобразование), то компилятор запомнит char*, дальше она будет использоваться, а объекта уже нет. Так как по временные объекты уничтожаются не при выходе из блока (как было в проекте языка), как только объект не нужен. Рассмотри пример. Пусть есть некоторая функция void f(X); заметим, что Х передается по значению. Есть объект T t; мы хотим вызвать f(t) – формально типы параметра t и Х не совпадают. Но компилятор прежде, чем выдать сообщение об ошибке поищет соответствующий конструктор X(T). Если он его нашел то, надо создать временный объект, который передается по значению: f(X(T)). Где нужно уничтожать этот временный объект? f(t) здесь ; (до точки-с-запятой)
пример:
Пусть f(char* s);
String s;
f(s); = f((char*) s); //с помощью операторов преобразования
С помощью оператора-преобразования мы можем
1. преобразовывать наши новые классы в стандартные тд
2. можно сделать следующее:
Base – базовый класс,
Derived - выведен из Base,
Derived -> Base // такое преобразование у нас есть
Base -> Derived // конструктором быть задано не может, может быть задана оператором преобразования внутри класса Derived
В Си#:
В языке Cи# тоже есть операторы неявного преобразования. Существует ряд ограничений:
1. Конструкторы преобразования здесь отсутствуют.
2. static implicit/explicit operator тип1 (тип2 имя)
а) Операторы преобразования задаются в виде статических членов
б) класса:тип1 и тип2 не могут быть стандартными типами (+ типами из стандартной библиотеки).
в) наличие implicit/explicit
Если преобразование (Т1)Е; объявлено как explicit, то:
T1 x;
T2 y;
x = y; // ERROR, но если было бы implicit то разрешалось – был бы сгенерирован временный объект, и ссылка на него присваивалась х
x = (T1)y; // ok
г) explicit указан по умолчанию
4).Свойства.
Ещё одна языковая конструкция – свойства (property). Они наиболее ярко иллюстрируют дуализм операций и данных, иногда сказать, где данные, где операции – очень тяжело, все зависит от уровня абстракции. Свойства относятся только к классам. С точки зрения пользователя свойства проявляют себя как член-данное (переменная, нестатический или статический член данных):
X a;
a.x = e;
b = a.x;
С точки зрения программиста - это две функции: get() и set(value)
a.x = e; // a.setX(e);
b = a.x; // b = a.getX();
Иначе говоря, для каждого свойства определена пара функций – одна читает значение, другая его устанавливает.
Свойства удобны: