Лекция 13 (лекции (2002))
Описание файла
Файл "Лекция 13" внутри архива находится в папке "лекции (2002)". Документ из архива "лекции (2002)", который расположен в категории "". Всё это находится в предмете "языки программирования" из 7 семестр, которые можно найти в файловом архиве МГУ им. Ломоносова. Не смотря на прямую связь этого архива с МГУ им. Ломоносова, его также можно найти и в других разделах. .
Онлайн просмотр документа "Лекция 13"
Текст из документа "Лекция 13"
Лекция 13
Как же копировать объекты? Почему нужен конструктор копирования в С++ (он и операция присваивания в С++ могут перекрываться)? Вот случаи, когда работает конструктор копирования:
X x = y;
void f(X x); - передача по значению
X f() {return x;}
a = b; - операция присваивания (реальное копирование объектов)
В ссылочных ЯП (Delphi, C#, Java) копирования объектов не происходит, а только копирование ссылок. И в выражении X x = y; «x»- это место для ссылки на объект класса X. Толко при операторе присваивания (a = b) нет инициализации , в отличие от остальных случаев, где работает конструктор копирования. Как же осуществляется глубокое копирование объекта в ЯП с ссылочной семантикой (нам это необходимо, когда мы передаём объект по значению и хотим поменять какое-то состояние этого объекта, а первоначальный объект трогать не хотим. В Delphi, Java и C# в теле можем реализовать специальный метод Clone (он присутствует на вершине иерархии классов и размещает точную копию объекта в оперативной памяти; в случае Delphi мы должны позаботиться, чтобы освободить потом память, а в C# и Java, где автоматическая сборка мусора, он автоматически уйдёт из памяти):
X t = x.Clone();
Он присутствует в любом классе и по умолчанию его реализация- это побитовое копрование.
В С++ надо было переопределять и конструктор копирования ( X(X&) ) и операцию присваивания.
Если хотим запретить копирование (например для обеспечения уникальности объектов):
1)Ссылочные ЯП: переопределяем метод Clone, вызываем исключение «нельзя копировать объект» (встроенное).
2)С++: определить конструктор копирования и оператор присваивания приватными
Понятие «свойства»
Подчёркивает дуализм понятия класса, и, вообще, понятия данных и операций. Свойства- это операции. В некоторых ЯП длина строки хранится явно в качестве её первого байта, она служит данным. Так же удобно в некоторых случаях предоставлять пользователю операции над типом данных в виде чтения и записи какой-то переменной. Например 4 свойства (имеют целый тип) типа данных «окно» (Window):
Window w;
w.x = 0; - на самом деле это достаточно сложная операция (протокол переговоров с вышестоящими владельцами окна, на тему может ли оно вообще имзмениться, может ли именно так, потом ещё системные вызовы на перерисовку и так далее) с точки зрения реализации, но тривиальна с точки зрения пользователя
Свойства (на языковом уровне) поддерживаются Delphi, C# и последних версиях Basic, для хорошей интеграции с интегрированной средой (как и перечислимый тип в С#).
Вообще говоря, оно задаётся таким образом:
T Prop - некое свойство
T.GetProp() {…} - возвращает значение свойства (a = w.y;)
void T.SetProp(T x) {…} - устанавливает свойство
В случае «w.x = 0;» для класса окно вызывается метод «Setx(…);». А в случае «a = w.y;» для данного окна вызывается метод «Gety(…);».
В Java и С++ понятие свойств реализуется именно таким образом, при этом совершенно не важно какая у них реализация: действительно простое присваивание или сложные операции.
Редактор ресурсов в интегрированной среде:
В случае перечислимого типа справа будет комбобокс с соответсвующими значениями, а в случае целых значений- нечто типа редакторского окна.
C#:
class Window {
Int x {
Get() {…}
Set() {…}
}
};
Window w = new Window;
w.x = 1; - значит вызываем метод set
a = w.x; - значит вызываем метод get
Если хотим устанавливать переменную только на чтение: не определяем метод set- это свойство можно считывать, но нельзя записывать. И наоборот для определения только на запись.
В Delphi несколько более сложный синтаксис.. Там есть ключевое слово property:
property x: тип
Прототипы:
Read ___;
Write ___;
Где первый реализован как function():тип; а второй- procedure(x: тип);. В простейшем случае вместо этих процедур может быть указан член-данное соответствующего класса и тогда это свойство будет аналогично считыванию или записи соответствующей переменной. Причём часто в качестве Read используют значения специальных переменных класса, а в качестве Write, конечно указывают процедуру. Такой синтаксис подобен синтаксису языка C#, но тут есть ещё один наворот- после свойства мы можем ещё установить ключевое слово «:default;». Это значит:
T = class
property x: Y;
…
a: T; b:Y;
a.x := b;
a := b; - нет ошибки, так как «х»- умолчательное свойство
И тут вызывается неявный оператор преобразования объекта к свойству
a := b; реально подставляется a.x := b;
b := a; реально подставляется b := a.x;
Это интересное свойство. Понятие «свойства» введено исключительно для удобства программирования в интегрированной среде, к тому же компоненты удобно описывать в качестве свойств. В Java это понятие отсутствует, так как все эти свойства классов и компонент, которые удобно использовать с точки зрения интегрированных сред, лучше описывать не на самом ЯП, а на каком-то специальном языке, например, на языке описания интерфейсов. Есть ODL (Object Definition Language) и IDL (Interface Definition Language) - языки описаний интерфейсов. Для них существуют отображения в конкретные ЯП.
Инициализация объектов
Она очень мощно реализована в С++. В некоторых случаях синтаксис конструкторов не очень хорош. Например, при инициализации статических объектов, которые в C# и Java встречаются очень часто. Каким образом инициализировать статические члены?
С++:
class X {
static int x;
};
Он принадлежит всем сразу объектам. Память для него где-то должна распределяться, значит в одном и только одном (следовательно, по определению не можем этого сделать в заголовочном файле, иначе он будет размещён в каждой единице трансляции) из С++ файлов мы должны написать:
int X::x;
Также, при создании, мы можем проинициализировать:
int X::x = 1;
Это же касается и инициализации констант. В современном синтаксисе языка С++ их можно инициализировать непосредственно при определении. Но в данном случае у нас достаточно тривиальный объект, а что делать в случае сложных объектов? И каким образом программист может управлять инициализацией статических объектов?
class Y {…};
class X {
static Y y;
};
Где-то должно быть:
Y X::y;
Этот объект будет инициализирован до начала выполнения функции main. Как мы уже говорили, конструкторы всех статических объектов выполняются в стандартном прологе. Но инициализация одних статических членов может зависить от других. Самой простой пример- стандартные классы ввода-вывода. В С есть библиотека stdio.h, а в С++ рекомендуется использовать iostream, где есть два класса для ввода/вывода: cin и cout (аналоги stdin и stdout), и эта библиотека написана полностью на языке С++ и полностью удовлетворяет его требованиям. Конструкторы статических объектов работают до начала функции main и где гарантия, что, если один из конструкторов вызывает исключение из-за некой неприятности при инициализации и пытается что-то вывести в cout, cout уже будет открыт? Это проблема, её можно обойти, но очень сложно. Путём введения некого статического члена для класса iostream, который и говорит об инициализированности объекта, и любая операция ввода вывода должна посмотреть, а инициализирован ли класс, используя этот статический объект. Если не инициализирован, то инициализировать класс, открыть cout и только после этого выводить. Очевидный недостаток этого метода: при каждой операции ввода-вывода мы проверяем инициализированность этого класса. В общем случае в С++ это проблема, в основном связанная с механизмом независимой раздельной трансляции. В других языках это попытались исправить.
Java: статический инициализатор:
static int x = 0;
Пишем прямо при определении, компилятор делает инициализацию только один раз при загрузке соответствующего класса в память, а последовательность загрузки классов в память при компиляции в Java определена. Об этом будем говорить, когда будем говорить о раздельной трансляции. Поэтому возможность управления порядком выполнения статических конструкторов у нас есть. Кроме этого, можно писать:
static {блок кода}
- где блок кода, инициализирует все статические перменные.
Например, статический массив объектов:
static int [] a;
static {a = new int[10]; for (int i = 0; i < 10; i++) a[i] = 1;}
Это очень полезная вещь, вкупе с механизмом управления загрузкой классов мы
решаем практически все проблемы.
В С# то же, но вместо статических инициализаторов- статические конструкторы (из-за политической установки быть похожим на С++, но не быть похожим на Java), хотя это тоже самое:
static X() {...}
Он выполняется только один раз, когда класс загружается в память и полностью аналогичен соответствующему статическому блоку кода.
В Delphi нет понятия статических членов, но на самом деле: статический член- это как бы глобальная переменная, локализованная внутри класса, который в данном отношении похож на модуль, который хранит внутри этот экземпляр. В Delphi существует понятие unit и мы просто внутри него заводим эту переменную (если хотим её экспортировать- объявляем её в интерфейсе), и обращаемя к ней не через имя класса (В Java и С# “X.i”, а в С++ “X::i”, а через имя модуля. Здесь глобальные переменные локализованы в рамках unit’a. Но когда они инициализируются?
unit M;
interface
x: Y;
//Обращаемся из других: M.x- похоже на статический член класса. Если //хотим сделать её не публичной, то просто надо перенести её в //implementation.
Implementation
initialization //выполняется тогда, когда модуль загружается в //память (~ конструктор статического объекта),
//тут инициализируем переменную «х»
...
finalization //выполняется тогда, когда модуль выгружается //из памяти (~ деструктор статического объекта)
end M.
В Delphi как и во всех языках с зависимой трансляцией определён порядок загрузки модулей в память и программист может им воспользоваться. Так как в Delphi нет автоматического вызова конструкторов и деструкторов, то понадобилась специальная конструкция для статических членов, то есть переменных, описанных либо в интерфейсе, либо в реализации.
Деструкторы
Деструкторы нужны только в C++ и Delphi. Деструктор- это нечто обратное конструктору, и если первый превращает кусок памяти в объект, то второй делает наоборот после него может работать менеджер динамической памяти и эту память утилизировать.
Delphi:
X.FREE
Это единственная возможность удалить объект класса из динамической памяти.
В C++ деструкторы вызываются автоматически, когда объекты уходят из памяти. В случае статических объектов: в стандартном эпилоге (после выхода из функции main) или при выходе из блока, а в случае динамических объектов при вызове метода delete.
В Java нет деструкторов, так как объекты там имеют ссылочную семантику. Объекты прекращают своё существование, когда работает сборщик мусора, а когда он работает- никто не знает. Известно только, что если количество ссылок на объект равно нулю, то сборщик мусора имеет право в любой момент удалить его из памяти, но не обязан. Хотя во всех языках с автоматической сборкой мусора (C#, Java) существует специальный системный вызов, который форсируют сборку мусора (типа flushall, который все буферыфайлов синхронизирует с диском), но им лучше не злоупотреблять, так.как он сильно тормозит работу программы. Существует специальный метод- аналог деструктора:
void finalize() {...}
Его вызовет сборщик мусора, когда объект уходит из памяти. Но мы не можем предсказать, когда этот метод будет вызван, более того, если программа кончится до того, как у нас освобождены все объекты (сборщик мусора имеет право не освобождать объекты если программа у нас всё-равно кончается), то для некоторых объектов этот метод вообще не будет вызван. Поэтому, в случае, если мы захватываем критически важные системные ресурсы, в конструкторе класса Х или во время его функционирования, то тогда использовать метод finalize не рекомендуется. Лучше использовать метод класса:
close() {...}
и, вообще говоря, вызывать его явно. В Delphi нет автоматической сборки мусора и программист должен вызывать деструкторы явно сам, освобождая ресурсы. Здесь тоже самое, но не любой класс захватывает критически важные системные ресурсы (например comport’ы, которые как правило открываются в монопольном режиме).
Во всех этих языках, где есть ссылочная семантика, то есть нет неявного вызова деструктора, появилась специальная конструкция, внешне похожая на механизм обработки исключений, но имеет другой смысл: