Void f(X x); передача по значению




Скачать 161.91 Kb.
Дата02.08.2016
Размер161.91 Kb.
Лекция 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’ы, которые как правило открываются в монопольном режиме).

Во всех этих языках, где есть ссылочная семантика, то есть нет неявного вызова деструктора, появилась специальная конструкция, внешне похожая на механизм обработки исключений, но имеет другой смысл:
try {

«захват ресурса»

} finally { «освобождение» }
В С++ работа с ресурсами была организована очень удобно:
{ C x

захват ресурса

} деструктор освобождает ресурс
В языках, где нет неявного вызова деструктора (C#, Delphi, Java) работа с ресурсами организуется через эти try-блоки, так как полагаться на деструкторы мы уже не можем. Блок finally выполнится всегда, как бы не закончился try (нормальным образом выйдя через скобку или через return или ненормальным образом, если произошла какая-то аварийная ситуация). И область метода finalize ограничена.

С#: вообще-то есть деструкторы, но это на самом деле финализаторы. И совет: делать метод close и использовать его явно. Например, с помощью конструкции try … finally.

Delphi:
try

...


finally

...


end.


Глава 6. Инкапсуляция. Абстрактные типы данных.

{На госэкзамен из курса Языков Программирования выносятся только два вопроса ООЯП и данная глава}

Инкапсуляция данных- упрятывание или сокрытие данных (операций), то есть доступ со стороны пользователя к ним сокрыт. Это то самое свойство, которое подразумевают в первую очередь, когда говорят о свойствах языка, делающих его объектно ориентированным (а также классические: наследование и динамический полиморфизм).

Множество операций (заданных пользователем или вытекающих из соответствующих структур данных) + Множество значений (структур данных).

Абстрактный ТД (АТД) - что это? Абстрактный класс (АК) в ООЯ ≠ АТД. И их не следует путать. Хотя и существуют АК, которые являются АТД, но так же есть АК, которые не являются АТД (во-первых термин АК применим только для ООЯП, где есть наследование или виртуальные методы, а АТД это взглад на ТД только с точки зрения инкапсуляции, то есть это понятия ортоганальные) и АТД, которые.не являются АК.

АТД- это ТД, где множество операций- только множество операций, которые задаёт пользователь. И мы можен работать с ним только в терминах множества операций, которые задаёт пользователь. С точки зрения пользователя: АТД ≡ множество операций, которые явно определит пользователь. И он не зависит от реализации. И в этом его главное достоинсво. Поэтому концепция АТД, которая появилась в 70-е годы в связи с развитием модульных ЯП, перешла во все современные ЯП.

Мы тут сталкнулись с ситуацией, с которой сталкнулись программисты в конце 60-х годов, когда развивалась дисциплина структурного (или структурированного) программирования- это добровольный отказ от некоторых средств записи кода (неограниченный оператор goto, ограничение выразительных возможностей структурами, имеющими один вход и один выход). Выразительность ЯП падает, но на практиче сокращается время на написание программ, программы легче писать, понимать и, следовательно, сопровождать. Инкапсуляция- это запрет пользователю ТД ссылаться на некоторые операции и, вообще, на множество значений, в случае АТД. Оказывается, что этот запрет благотворно влияет на пользователя. Например ТД стек, где мы не можем иметь произвольный доступ к структуре данных. У него есть указатель на первый свободный элемент в этом массиве: top и пользователя не в коем случае нельзя допускать к произвольной работе с ним, лучше её вообще скрыть, так как изменение её значения приводит к нарушению целостности структуры данных. Благодаря этому программист-пользователь ориентируется на базовые свойства ТД, не отвлекаясь на реализацию. А когда программист начинает разбираться в реализации? Вот три вырожденных случая:

1)недостаток документации (не понятно, как работают те или иные операции ТД)

2)есть ошибки (в реализации ТД)

3)хочет эффективнее

И если ТД спроектирован хорошо, то этих случаев не возникает. Поэтому инкапсуляция- это благо и она реализована во всех ЯП.

Модульные ЯП

Инкапсуляцию иногда называют защита данных, но возникает вопрос: «от кого?». Инкапсуляция- защита данных от дурака, а не от злоумышленника. Почти везде можно легко взломать защиту (получить доступ к данным), ну разве, что в Jave приложили серьёзные усилия, чтобы не позволить программисту снимать эту защиту. В С++ можно использовать указатели и неконтроллируемые их преобразования. Мы знаем реализацию методов ТД и расположение данных в памяти: указатель на ТД с той же структурой, но публичной приводим к указателю на необходимый объект и тем самым получаем полный доступ к нему. То есть просто у пользователя нет возможности сломать структуру данных (объект), а когда делаем что-то не правильно (пытаемся забрать что-то из пустого стека), то получаем исключительную ситуацию. Например, при хорошей защите пользователь не может сломать стек (нарушить его целостность).


Единица защиты:

1)Тип целиком – выбирают все ЯП, которые мы рассматриваем.

2)Отдельные переменные (экземпляры) [разные стратегии защиты для разных объектов одного типа]
Атом защиты:

1)Вся структура данных – Модула-2, Ада

2)Отдельные члены (более гибко) – Оберон, Delphi, Java, C#, C++
Модула-2 и Ада призывают писать в терминах АТД, так как это хорошо. В них составной ТД (то есть реализуется, как правило, на базе записи) либо целиком открыт, либо целиком закрыт.

Модула-2: либо вся структура закрыта, либо открыта.


DEFINITION MODULE M;

TYPE T =


RECORD

... /* описание членов */

END;

операции



END M;
Тут мы скрываем от пользователя только реализацию операций, а структуру данных мы не скрываем.
АТД:

...


TYPE T; /* скрытый ТД (opaque) */

операции над Т; /* про реализацию не известно, над ним можем делать только эти операции */

CONST N; /* означает, что значение этой константы заранее не известно */

END M;
Конкретная структура ТД должна раскрываться, соответственно, в реализации.


IMPLEMENTATION MODULE M

...

END M;
О скрытом ТД в Модуле-2 нам известно только его имя и ничего о его структуре не известно.



Отсюда возникает ограничение: скрытый ТД, с точки зрения реализации, может выглядеть либо как указатель, либо как ТД, совместимый с указателем по памяти (то есть целый – INT). И следующая запись будет ошибочной:
TYPE T = RECORD …
Пример, модуль:
InOut
Где есть, скрытый ТД “файл”:
FileStream
Это INT– аналог файлового дескриптора, то есть где-то в модуле реализации отведён массив структур данных под эти файлы и его размер определяет число файлов, открытых одновременно. И этот ТД “файл”- это индекс соответствующей структуры данных в этом массиве. Но чаще ТД- это указатель на некоторую запись. Но мы вынуждены организовывать всю работу через динамическую память, то есть у нас обязаны быть операции:
Init(VAR X : T);

Destroy(X : T);


Следовательно мы получаем проблему конструирования и разрушения (автоматически не разрешенную в Модуле-2). Пользователь должен не забывать по Init и Destroy. В случае файлов это будет: Open и Close.

Но откуда взялось такое неприятное ограничение? Из-за особенностей раздельной трансляции модулей с языка Модула-2: для компиляции нужны только модули определений (без модулей реализации). Вся необходимая информация для компилятора содержится в модуле определений. Пусть в модуле М описан скрытый тип данных

«TYPE T; »:


M1:


FROM M IMPORT T;

X : T;
Это модуль М1. При его компиляции надо знать размер типа Т. Компилятор видит только модуль определений от М, а там про Т написано только его имя. Отсюда и возникло ограничение на то, что Т- это указатель или тип совместимый по памяти с указателем и компилятор отводит память под тип данных INTEGER (точнее- под тип данных указатель).

Операции к скрытому ТД в Модуле-2: все, описанные в модуле определений, а так же все, которые можно выполнять над указателем или ссылкой: операции сравнения ссылок «=» (равно) и «#» (не равно- в Модуле-2 выглядит именно таким образом); «:=» - это поверхностное присваивание ( присваивание ссылок). С элементами скрытого ТД мы работаем в терминах ссылок на него (на его объекты). Но если хотим делать глубинное копирование, то надо делать:
PROCEDURE clone(X : T) : T;
VAR не надо, так как X- уже ссылка.

Если глубинное сравнение (сравнение по структурам данных):


PROCEDURE IsEqual(X1, X2 : T) : Boolean;
Достоинства:

1)Присутствует понятие АТД.

2)Простая реализация АТД, следовательно простой язык и простой компилятор (в описании Модулы-2 скрытым ТД посвящено полстранички).

Недостатки:

1)Для реализации используем одну и ту же схему- динамическую память

2)Работа происходит в терминах ссылок (но язык не поддерживает проблемы инициализации, копирования и сравнения на равенство/неравенство- все проблемы кладутся на программиста-пользователя).


Aда: другой подход- существует более глубокое понятие инкапсуляции. Здесь мы опять же либо целиком инкапсулируем весь ТД, либо целиком его открываем.

АТД: это либо приватный ТД:

private

либо ограниченный приватный ТД (некоторое обобщение приватного ТД):

limited private

private: похож на скрытый ТД в Модуле-2, программист о нём не знает ничего, кроме набора операций.


package M is type T is private;

операции над Т


Если бы мы здесь как в Модуле-2 поставили END, то сталкнулись бы с ситуацией, когда компилятор когда компилирует модуль М1, который импортирует модуль М, он должен для эффективности знать размер типа данных Т, и поэтому в Модуле-2 работать с элементами АТД можно было только через указатели. А здесь приватная часть, где описание всех приватных ТД (в Аде пакет может служить для определения многих ТД), где раскрывается их структура (и все подструктуры):
private

описание всех приватных ТД

type T is record ... size = 25; end record;

end M;
Есть описание структуры данных, но пользователю это ничего не даст, он не может этим воспользоваться.


use M;

x : T; //компилятор, видя только спецификацию пакета, отведёт

//память и проинициализирует поле size.
Но писать x.size- нельзя, так как структура данных открыта компилятору, но закрыта для программиста пользователя. Плюс такого подхода: полная свобода с точки зрения реализации АТД (ничем не отличается от реализации обычных ТД). А недостаток: если меняем структуру пакета, где находится АТД, то надо перекомпилировать все модули, использующие этот АТД, в отличие от Модулы-2, где надо было перекомпилировать только модуль реализации. Накладные расходы на перекомпиляцию Но при современных мощностях это не существенно. Приватный ТД- это некое расширение скрытого ТД в Модуле-2.

Операции над приватным ТД: все, определённые в описании; «:=» - присваивание; и операции сравнения «=, /=, <=, <, >=»- в зависимости от структуры (компилятор её знает).

В Аде есть приятная особенность: к составным базисным ТД (запись, массив) применимы любые операции сравнения, если они применимы покомпонентно:
array (0 .. 10) of T0;
И если Т0, например, INTEGER, над которым применимы любые операции сравнения, следовательно над любыми массивами такого типа применимы все операции сравнения.
type T is array (range <>) of T1;
Если
X1 : T(0 .. 10);

X2 : T(1 .. 11);

X3 : T(-1 .. 21);
То (Х1, Х2) можно сравнивать, а (Х1, Х3) и (Х2, Х3)- нельзя.


record


a : T1;

b : T2;


end
Если к Т1 и Т2 применимы операции сравнения, то и ко всему типу данных запись они применимы.

Тоже самое относиться и к операции присваивания. И компилятор, зная структуру ТД понимает можно применять такие операции или нельзя.

Ограниченный приватный ТД (limited private). К нему применимы только базовые операции (определённые пользователем), а операции присваивания и сравнения применять нельзя (вне зависимости от структуры)- это полностью абстрактный ТД:

...


type T is limited private;

операции



private

...


type T is ...;

end M;
Применить операции присваивания и сравнения нельзя. В частности ограниченный приватный ТД удобен, когда нельзя просто так применять операцию присваивания указателей (когда в это ТД есть ссылки на другие ТД или динамические ссылки), то есть копировать нельзя, но можно clone (или copy)- настоящее копирование этого ТД. А если пользователь забудит про это и попробует применить операцию присваивания, то компилятор выдаст ему сообщение об ошибке. Эта концепция- настоящий АТД, значительно более абстрактный, чем скрытый ТД в Модуле-2 (он там и не назывался АТД).


База данных защищена авторским правом ©uverenniy.ru 2016
обратиться к администрации

    Главная страница