Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
ООП(ПОИТ)(Сурков).doc
Скачиваний:
24
Добавлен:
03.05.2019
Размер:
937.98 Кб
Скачать

Тема 13. Подсчет ссылок. Механизм подсчета ссылок. Представление интерфейса в памяти. Применение интерфейса для доступа к объекту dll-библиотеки.

Механизм подсчета ссылок

Механизм подсчета ссылок на объект предназначен для автоматического уничтожения неиспользуемых объектов. Неиспользуемым считается объект, на который не ссылается ни одна интерфейсная переменная.

Подсчет ссылок на объект обеспечивают методы _AddRef и _Release интерфейса IInterface. При копировании значения интерфейсной переменной вызывается метод _AddRef, а при уничтожении интерфейсной переменной — метод _Release. Вызовы этих методов генерируются компилятором автоматически:

var

Intf, Copy: IInterface;

begin

...

Copy := Intf; // Copy._Release; Intf._AddRef;

Intf := nil; // Intf._Release;

end; // Copy._Release

Стандартная реализация методов _AddRef и _Release находится в классе TInterfacedObject. Она достаточно проста и вы легко разберетесь с ней, читая комментарии в исходном тексте.

type

TInterfacedObject = class(TObject, IInterface)

...

FRefCount: Integer; // Счетчик ссылок

function _AddRef: Integer; stdcall;

function _Release: Integer; stdcall;

...

end;

function TInterfacedObject._AddRef: Integer;

begin

Result := InterlockedIncrement(FRefCount); // Увеличение счетчика ссылок

end;

function TInterfacedObject._Release: Integer;

begin

Result := InterlockedDecrement(FRefCount); // Уменьшение счетчика ссылок

if Result = 0 then // Если ссылок больше нет, то

Destroy; // уничтожение объекта

end;

Заметим, что функции InterlockedIncrement и InterlockedDecrement просто увеличивают значение целочисленной переменной на единицу. В отличие от обычного оператора сложения, они обеспечивают атомарное изменение значения переменной, что очень важно для правильной работы распараллеленных (многопоточных) программ.

Приведенную выше реализацию методов _AddRef и _Release автоматически получают все наследники класса TInterfacedObject, в том числе и классы TTextReader, TDelimitedReader и TFixedReader. Поэтому неиспользуемые объекты классов TDelimitedReader и TFixedReader тоже автоматически уничтожаются при работе с ними через интерфейсные переменные:

var

Obj: TDelimitedReader;

Intf, Copy: ITextReader;

begin

Obj := TDelimitedReader.Create('MyData.del', ';');

Intf := Obj; // Obj._AddRef -> Obj.FRefCount = 1

Copy := Intf; // Obj._AddRef -> Obj.FRefCount = 2

...

Intf := nil; // Obj._Release -> Obj.FRefCount = 1

Copy := nil; // Obj._Release -> Obj.FRefCount = 0 -> Obj.Destroy

Obj.Free; // Ошибка! Объект уже уничтожен и переменная Obj указывает в никуда

end;

Обратите внимание, что объектные переменные не учитываются при подсчете ссылок. Поэтому мы настоятельно рекомендуем избегать смешивания интерфейсных и объектных переменных. Если вы планируете использовать объект через интерфейс, то лучше всего результат работы конструктора сразу присвоить интерфейсной переменной:

var

Intf: ITextReader;

begin

Intf := TDelimitedReader.Create('MyData.del', ';'); // FRefCount = 1

...

Intf := nil; // FRefCount = 0 -> Destroy

end;

Если интерфейс является входным параметром подпрограммы, то при вызове подпрограммы создается копия интерфейсной переменной с вызовом метода _AddRef:

procedure LoadItems(R: ITextReader);

begin

...

end;

var

Reader: ITextReader;

begin

...

LoadItems(Reader); // Создается копия переменной Reader и вызывается Reader._AddRef

end;

Копия не создается, если входной параметр описан с ключевым словом const:

procedure LoadItems(const R: ITextReader);

begin

...

end;

var

Reader: ITextRedaer;

begin

...

LoadItems(Reader); // Копия не создается, метод _AddRef не вызывается

end;

Интерфейсная переменная уничтожается при выходе из области действия переменной, а это значит, что у нее автоматически вызывается метод _Release:

var

Intf: ITextRedaer;

begin

Intf := TDelimitedReader.Create('MyData.del', ';');

...

end; // Intf._Release

Представление интерфейса в памяти

Глубокое понимание работы интерфейсов требует знания их технической реализации. Поэтому вам необходимо разобраться в том, как представляется интерфейс в оперативной памяти компьютера, и что стоит за операторами Intf := Obj и Intf.NextLine.

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

Интерфейсная переменная хранит ссылку на скрытое поле объекта, которое содержит указатель на таблицу методов интерфейса. Когда интерфейсной переменной присваивается значение объектой переменной,

Intf := Obj; // где Intf: ITextReader и Obj: TTextReader

к адресу объекта добавляется смещение до скрытого поля внутри объекта и этот результат заносится в интерфейсную переменную. Чтобы убедиться в сказанном, посмотрите в отладчике значения Pointer(Obj) и Pointer(Intf) сразу после выполнения оператора Intf := Obj. Эти значения будут разными! Причина в том, что объектная ссылка указывает на начало объекта, а интерфейсная ссылка — на скрытое поле внутри объекта.

Рисунок 11. Представление интерфейса в памяти

Алгоритм вызова метода интерфейса такой же, как алгоритм вызова метода класса. Когда через интерфейсную переменную выполняется вызов метода,

Intf.NextLine;

реализуется следующий алгоритм:

  1. Из интерфейсной переменной извлекается адрес (по нему хранится адрес таблицы методов интерфейса);

  2. По полученному адресу извлекается адрес таблицы методов интерфейса;

  3. На основании порядкового номера метода в интерфейсе из таблицы извлекается адрес соответствующей подпрограммы;

  4. Вызывается код, находящийся по этому адресу. Этот код является переходником от метода интерфейса к методу объекта. Его задача — восстановить из ссылки на интерфейс значение указателя Self (путем вычитания заранее известного значения) и выполнить прямой переход на код метода класса.

Обычными средствами процедурного программирования этот алгоритм реализуется так:

type

TMethodTable = array[0..9999] of Pointer;

TNextLineFunc = function (Self: ITextReader): Boolean;

var

Intf: ITextReader; // интерфейсная переменна

IntfPtr: Pointer; // адрес внутри интерфейсной переменной

TablePtr: ^TMethodTable; // указатель на таблицу методов интерфейса

MethodPtr: Pointer; // указатель на метод

begin

...

IntfPtr := Pointer(Intf); // 1) извлечение адреса из интерфейсной переменной

TablePtr := Pointer(IntfPtr^); // 2) извлечение адреса таблицы методов интерфейса

MethodPtr := TablePtr^[3]; // 3) извлечение адреса нужного метода из таблицы

TNextLineFunc(MethodPtr)(Intf); // 4) вызов метода через переходник

...

end.

Вся эта сложность скрыта в языке Delphi за понятием интерфейса. Причем несмотря на такое количество операторов в примере, вызов метода через интерфейс в машинном коде выполняется весьма эффективно (всего несколько инструкций процессора), поэтому в подавляющем большинстве случаев потерями на вызов можно пренебречь.

Применение интерфейса для доступа к объекту DLL-библиотеки

Если вы поместите свой класс в DLL-библиотеку, то при необходимости использовать его в главной программе столкнетесь с проблемой. Подключение модуля с классом к главной программе приведет к включению в нее кода всех методов класса, т.е. задача выделения класса в DLL-библиотеку не будет решена. Если же не подключить модуль с описанием класса, главная программа вообще не будет знать о существовании класса, и воспользоваться классом будет невозможно. Эта проблема решается с помощью интерфейсов. Покажем это на примере модуля ReadersUnit.

Сначала вынесем описание интерфейса ITextReader в отдельный модуль (например, ReaderIntf), чтобы этот модуль в дальнейшем можно было подключить к главной программе:

unit ReadersIntf;

interface

type

ITextReader = interface(IInterface)

...

end;

implementation

end.

Затем удалим описание интерфейса из модуля ReadersUnit, а вместо него подключим модуль ReaderIntf:

unit ReadersUnit;

interface

uses

ReaderIntf;

...

Наконец включим скорректированный модуль ReadersUnit в DLL-библиотеку, которую назовем ReadersLib:

library ReadersLib;

uses

SysUtils, Classes, ReadersUnit;

{$R *.res}

begin

end.

Вроде бы все готово, и теперь в главной программе достаточно подключить модуль ReaderIntf и работать с объектами через интерфейс ITextReader (рисунок 12).

Рисунок 12. Схема получения программы и DLL-библиотеки

Для создания объектов классов находящихся в DLL-библиотеке определим специальную функцию и экспортируем ее:

library ReadersLib;

...

function GetDelimitedReader(const FileName: string;

const Delimiter: Char = ';'): ITextReader;

begin

Result := TDelimitedReader.Create(FileName, Delimiter);

end;

exports

GetDelimitedReader;

begin

end.

В главной программе импортируйте функцию GetDelimitedReader, чтобы с ее помощью создавать объекты класса TDelimitedReader:

program Example;

uses

ReadersIntf;

function GetDelimitedReader(const FileName: string;

const Delimiter: Char = ';'): ITextReader;

external 'ReadersLib.dll' name 'GetDelimitedReader';

var

Intf: ITextReader;

begin

Intf := GetDelimitedReader;

...

end.

Теперь вы знаете, как разместить объекты в DLL-библиотеке.

Тема 14. Принципы модульного программирования на языке C++. Пространства имен.

Принципы модульного программирования на языке С++

В языке С++ очень бедные средства модульного программирования, поэтому для достижения модульности программ, следует придерживаться определенных принципов.

Роль программного интерфейса модуля играет h-файл, а cpp-файл — роль реализации этого модуля. Внутрь h-файла включаются h-файлы других модулей, необходимые для компиляции интерфейсной части. Внутрь cpp-файла включаются h-файлы других модулей, необходимые для компиляции cpp- и h-файлов интерфейсной части модуля.

Очевидно, что программисту при включении h-файла другого модуля предоставляется выбор: подключить его в h-файле модуля или в cpp-файле. В данном случае предпочтение следует отдавать части реализации модуля (cpp-файл).

При подключении h-файла следует придерживаться следующей схемы: предположим, что наш модуль называется SysModule и состоит из двух частей: SysModule.h и SysModule.cpp. Рекомендуется следующая схема подключения:

SysModule.h:

#include "Config.h" // наш файл конфигурации

// подключается первым во всех h-файлах

// всех наших проектов

#include "Другой стандартный модуль"

#include "Другой наш модуль"

SysModule.cpp:

#include "Файл предкомпилированных заголовков"

#include "Еще один наш модуль"

#include "Другой стандартный модуль"

#include "SysModule.h" // подключается последним

Поскольку один и тот же h-файл может одновременно включатся в другие h-файлы и несколько раз подключаться при компиляции одного и того же cpp-файла, его следует защищать от повторной компиляции. Для этого в начале любого h-файла вставляются следующие директивы компилятора:

#ifndef __SysModule_h__

#define __SysModule_h__

...

#endif //__SysModule_h__

Таким образом, в том случае, когда файл подключается несколько раз, скомпилируется он только один раз.

Внимание! Согласно стандарту ISO, любой h- и cpp-файл в С++ должен заканчиваться символом перевода строки.

Пространства имен

В больших проектах наблюдается серьезная проблема — конфликт идентификаторов. Она решается с помощью пространства имен.

namespace Sys

{

int var;

void Proc();

}

Внутри пространства имен обращение к определенным внутри переменным и подпрограммам можно осуществлять, используя неполную форму записи:

var = 10;

Proc();

за пределами – надо использовать полную форму записи:

Sys::var = 10;

Sys::Proc();

Для того чтобы избежать возможного конфликта идентификаторов, все определения внутри модуля следует помещать в пространство имен. Следует давать небольшой буквенный идентификатор, который будет соответствовать префиксу файла.

Существует возможность открыть пространство имен таким образом, чтобы можно было использовать неполную форму записи. Для этого надо написать строку:

using namespace Sys;

Но следует отметить, что данная конструкция является причиной многих ошибок, поэтому так писать не стоит.

Существует второй способ открыть пространство имен — это открыть его для конкретного определения:

using Sys::Proc();

...

Proc();

...

Но рекомендуется использовать Sys::Proc();

Идентификаторы, объявленные вне пространства имен, относятся к так называемому глобальному пространству имен, доступ к которым осуществляется с помощью оператора ::

::Funk();

Для того чтобы была возможность закрыть доступ к данным и подпрограммам внутри данного пространства существует пространство имен без имени:

namespace

{

...

}

Пространства имен могут быть вложенными:

namespace Sys

{

namespace Local

{

int var;

...

}

...

}

Sys::Local::var = 10;

Замечание! Когда возникает желание объявить переменный тип данных или подпрограмму внутри пространства имен, а реализовать за пределами (или наоборот), следует поступать так:

SysModul.h: SysModul.cpp:

namespace Sys namespace Sys

{ {

int Proc(); int Proc();

} {

...

};

}