Что такое раннее связывание

C++ MythBusters. Миф о виртуальных функциях

В прошлой статье я рассказывал, с какой не всем известной особенностью можно столкнуться при работе с подставляемыми функциями. Статья породила как несколько существенных замечаний, так и многостраничные споры (и даже холивары), начавшиеся с того, что inline-функции вообще лучше не использовать, и перешедшие в стандартную тему C vs. C++ vs. Java vs. C# vs. PHP vs. Haskell vs. …

Сегодня пришла очередь виртуальных функций. И, во-первых, я сразу оговорюсь, что статья моя (в принципе, как и предыдущая) ни в коей мере не претендует на полноту изложения. А во-вторых, как и раньше, эта статья не для профессионалов. Она будет полезна тем, кто уже нормально разбирается в основах C++, но имеет недостаточно опыта, либо же тем, кто не любит читать книжек.

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

Если в материале про inline-методы миф был не совсем очевиден, то в этом — напротив. Собственно, перейдем к «мифу».

Виртуальные функции и ключевое слово virtual

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

pA is B:
B::foo() // потому что в родительском классе A метод foo() помечен как virtual
B::bar() // потому что в родительском классе A метод bar() помечен как virtual
A::baz() // потому что в родительском классе A метод baz() не помечен как virtual

pA is C:
С::foo() // потому что в родительском классе B метод foo() помечен как virtual
B::bar() // потому что в родительском классе B метод bar() не помечен как virtual,
// но он помечен как virtual в классе A, указатель на который мы используем
A::baz() // потому что в классе A метод baz() не помечен как virtual

С невиртуальной функцией baz() всё и так ясно. А вот с логикой вызова виртуальных функций есть неувязочка. Думаю, не стоит говорить, что на самом деле вывод будет следующим:

Вывод: виртуальная функция становится виртуальной до конца иерархии, а ключевое слово virtual является «ключевым» только в первый раз, а в последующие разы оно несет в себе чисто информативную функцию для удобства программистов.

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

Раннее и позднее связывание. Таблица виртуальных функций

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

Встретив ключевое слово virtual, компилятор помечает, что для этого метода должно использоваться позднее связывание: для начала он создает для класса таблицу виртуальных функций, а в класс добавляет новый скрытый для программиста член — указатель на эту таблицу. (На самом деле, насколько я знаю, стандарт языка не предписывает, как именно должен быть реализован механизм виртуальных функций, но реализация на основе виртуальной таблицы стала стандартом де факто.). Рассмотрим этот пруфкод:

Вывод может отличаться в зависимости от платформы, но в моем случае (Win32, msvc2008) он был следующим:

Что можно понять из этого примера. Во-первых, размер «пустого» класса всегда больше нуля, потому что компилятор специально вставляет в него фиктивный член. Как пишет Эккель, «представьте процесс индексирования в массиве объектов нулевого размера, и все станет ясно» 😉 Во-вторых, мы видим, что размер «непустого» класса NotEmptyVirt при добавлении в него виртуальной функции увеличился на стандартный размер указателя на void; а в «пустом» классе EmptyVirt фиктивный член, который компилятор ранее добавлял для приведения класса к ненулевому размеру, был заменен на указатель. В то же время добавление невиртуальной функции в класс на размер не влияет (спасибо nullbie за совет). Имя указателя на таблицу отличается в зависимости от компилятора. К примеру, компилятор называет его __vfptr, а саму таблицу ‘vftable’ (кто не верит, может посмотреть в отладчике 🙂 В литературе указатель на таблицу виртуальных функций принято называть VPTR, а саму таблицу VTABLE, поэтому я буду придерживаться таких же обозначений.

Что представляет собой таблица виртуальных функций и для чего она нужна? Таблица виртуальных функций хранит в себе адреса всех виртуальных методов класса (по сути, это массив указателей), а также всех виртуальных методов базовых классов этого класса.

Таблиц виртуальных функций у нас будет столько, сколько есть классов, содержащих виртуальные функции — по одной таблице на класс. Объекты каждого из классов содержат именно указатель на таблицу, а не саму таблицу! Вопросы на эту тему любят задавать преподаватели, а также те, кто проводит собеседования. (Примеры каверзных вопросов, на которых можно подловить новичков: «если класс содержит таблицу виртуальных функций, то размер объекта класса будет зависеть от количества виртуальных функций, содержащихся в нем, верно?»; «имеем массив указателей на базовый класс, каждый из которых указывает на объект одного из производных классов — сколько у нас будет таблиц виртуальных функций?» и т.д.).

Итак, для каждого класса у нас будет создана таблица виртуальных функций. Каждой виртуальной функции базового класса присваивается подряд идущий индекс (в порядке объявления функций), по которому в последствие и будет определяться адрес тела функции в таблице VTABLE. При наследовании базового класса, производный класс «получает» и таблицу адресов виртуальных функций базового класса. Если какой-либо виртуальный метод в производном классе переопределяется, то в таблице виртуальных функций этого класса адрес тела соответствующего метода просто будет заменен на новый. При добавлении в производный класс новых виртуальных методов VTABLE производного класса расширяется, а таблица базового класса естественно остается такой же, как и была. Поэтому через указатель на базовый класс нельзя виртуально вызвать методы производного класса, которых не было в базовом — ведь базовый класс о них ничего «не знает» (дальше мы все это посмотрим на примере).

Конструктор класса теперь должен делать дополнительную операцию: инициализировать указатель VPTR адресом соответствующей таблицы виртуальных функций. То есть, когда мы создаем объект производного класса, сначала вызывается конструктор базового класса, инициализирующий VPTR адресом «своей» таблицы виртуальных функций, затем вызывается конструктор производного класса, который перезаписывает это значение.

При вызове функции через адрес базового класса (читайте — через указатель на базовый класс) компилятор сначала должен по указателю VPTR обратиться к таблице виртуальных функций класса, а из неё получить адрес тела вызываемой функции, и только после этого делать call.

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

Думаю, на примере все станет понятнее. Рассмотрим следующую иерархию:

Что такое раннее связывание. Смотреть фото Что такое раннее связывание. Смотреть картинку Что такое раннее связывание. Картинка про Что такое раннее связывание. Фото Что такое раннее связывание

В данном случае получим две таблицы виртуальных функций:

Base
0Base::foo()
1Base::bar()
2Base::baz()

и

Inherited
0Base::foo()
1Inherited::bar()
2Base::baz()
3Inherited::qux()

Как видим, в таблице производного класса адрес второго метода был заменен на соответствующий переопределенный. Пруфкод:

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

Также становится понятно, почему виртуальные функции работают только при обращении по адресу объекта (через указатели либо через ссылки). Как я уже сказал, в этой строке
Base * pBase = new Inherited;
происходит повышающее приведение типа: Inherited* приводится к Base*, но в любом случае указатель всего лишь хранит адрес «начала» объекта в памяти. Если же повышающее приведение производить непосредственно для объекта, то он фактически «обрезается» до размера объекта базового класса. Поэтому логично, что для вызова функций «через объект» используется раннее связывание — компилятор и так «знает» фактический тип объекта.

Собственно, это всё. Жду комментариев. Спасибо за внимание.

Источник

BestProg

Данная тема есть продолжением темы:

Содержание

Поиск на других ресурсах:

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

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

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

Необходимые условия для реализации позднего связывания:

Что такое раннее связывание. Смотреть фото Что такое раннее связывание. Смотреть картинку Что такое раннее связывание. Картинка про Что такое раннее связывание. Фото Что такое раннее связывание

Рисунок 1. Позднее и раннее связывание. Отличия

В случае раннего связывания, как только компилятор встречает строку

Как следствие, после вызова

будет вызван метод Print() класса B.

Что такое раннее связывание. Смотреть фото Что такое раннее связывание. Смотреть картинку Что такое раннее связывание. Картинка про Что такое раннее связывание. Фото Что такое раннее связывание

Вызов метода Print() по ссылке на объект класса C

2. Что такое полиморфизм? Динамический полиморфизм

Полиморфизм – это свойство программного кода изменяться в зависимости от ситуации, которая возникает в момент выполнения программы.

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

Использование преимуществ полиморфизма возможно в ситуациях:

3. Для каких элементов класса можно применять полиморфизм?

Полиморфизм можно применять для следующих элементов:

4. Схематическое объяснение полиморфизма

На рисунке 3 демонстрируется применение полиморфизма на примере двух классов.

Что такое раннее связывание. Смотреть фото Что такое раннее связывание. Смотреть картинку Что такое раннее связывание. Картинка про Что такое раннее связывание. Фото Что такое раннее связывание

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

В любой метод может быть передана ссылка на базовый класс. С помощью этой ссылки также можно вызвать методы, свойства которые поддерживают полиморфизм.

Пример.

6. Какие требования накладываются на элемент класса для того, чтобы он поддерживал полиморфизм?

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

7. Использование ключевого слова new в цепочке виртуальных методов. Пример

Как известно, элемент класса, который объявлен виртуальным ( virtual ), передает возможность реализовать полиморфизм в одноименных элементах унаследованных классов. Таким образом, виртуальные элементы образовывают цепочку вниз по иерархии.

Текст демонстрационной программы следующий

На рисунке 4 схематично изображен вызов метода Print() в случае использования ключевого слова new.

Что такое раннее связывание. Смотреть фото Что такое раннее связывание. Смотреть картинку Что такое раннее связывание. Картинка про Что такое раннее связывание. Фото Что такое раннее связывание

Результат работы программы

Из класса Figure унаследовать класс Rectangle (прямоугольник), который содержит следующие поля:

В классе Rectangle реализовать следующие методы и функции:

В функции main() выполнить следующие действия:

Источник

VBA Excel. Раннее и позднее связывание

Раннее и позднее связывание переменных с экземплярами внешних и внутренних объектов в VBA Excel. Преимущества ранней привязки объектов. Примеры кода.

Внутренними называются объекты, которые принадлежат объектной модели Excel (Range, Sheet, Workbook, Chart). Внешние объекты не принадлежат объектной модели Excel. А также новый экземпляр Excel.Application является внешним объектом по отношению к тому экземпляру приложения Excel, из которого он создается.

Раннее связывание

Определение типа объекта при ранней привязке выполняется еще до запуска приложения.

Объявление переменной определенного типа

Объявление переменной с определенным типом объекта:

При объявлении переменной с определенным типом объекта ранняя привязка уже осуществилась, но чтобы начать работу с переменной, ей необходимо присвоить конкретный (для внутренних объектов) или новый (для внешних объектов) экземпляр объекта с помощью ключевого слова Set:

Присвоение переменной объекта при объявлении

Присвоение переменной, при ее объявлении, нового экземпляра внешнего объекта:

Ссылка на библиотеку внешнего объекта

Чтобы использовать раннее связывание для внешнего объекта, необходимо подключить в редакторе VBA Excel ссылку на библиотеку этого объекта, если она еще не подключена. Подключается ссылка на библиотеку в окне «References VBAproject», перейти в которое можно через главное меню редактора: Tools–>References…

Ссылка на библиотеку Microsoft Scripting Runtime, которая необходима для ранней привязки объекта Dictionary:

Что такое раннее связывание. Смотреть фото Что такое раннее связывание. Смотреть картинку Что такое раннее связывание. Картинка про Что такое раннее связывание. Фото Что такое раннее связывание

Ссылка на библиотеку Microsoft Word Object Library, которая необходима для ранней привязки объекта Word.Application:

Что такое раннее связывание. Смотреть фото Что такое раннее связывание. Смотреть картинку Что такое раннее связывание. Картинка про Что такое раннее связывание. Фото Что такое раннее связывание

После выбора библиотеки следует нажать кнопку «OK».

Преимущества ранней привязки

Главное преимущество раннего связывания заключается в возможности использовать при написании кода VBA Excel лист выбора и вставки свойств-методов привязанных объектов (Auto List Members). Лист подсказок отображается автоматически или вызывается сочетанием клавиш «Ctrl+Пробел» или «Ctrl+J».

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

В том, что раннее и позднее связывание работает не только с внешними, но и с внутренними объектами, вы можете убедиться на следующих примерах.

Скопируйте процедуру Primer1 с ранней привязкой объекта Sheet в любой программный модуль:

Источник

Использование раннего и позднего связывания в автоматизации

Сводка

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

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

Дополнительные сведения

Автоматизация это процесс, при котором один программный компонент обменивается данными с другим компонентом программного обеспечения и/или управляет им с помощью модели COM (Component Object Model) Майкрософт. Это основа для большинства межкомпонентных коммуникаций, используемых в таких языках, как Visual Basic или Visual Basic для приложений, и стал нормальной частью большинства программ.

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

Что такое привязка?

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

Чтобы понимать процесс, подумайте о том, что «Binding» в плане публикации книги. Представьте, что ваш код похож на текст книги, где в определенном абзаце вы написали что-то вроде «ознакомьтесь с главой 12, страница x для получения дополнительных сведений.» Вы не знаете, что представляет собой номер страницы до завершения книги, поэтому прежде чем приступить к чтению абзаца, все страницы книги должны быть привязаны друг к другу и вставлен правильный номер страницы в абзац. Прежде чем ссылаться на другие части книги, дождитесь ее припривязки к книге.

Программное обеспечение для привязки аналогично. Код состоит из частей, которые необходимо отсоединить до того, как код будет доступен для чтения. Binding — это операция замены имен функций с адресами памяти (или смещениями памяти, что точнее), где код будет «переход к» при вызове функции. Для COM-объектов Address — это смещение памяти в таблице указателей (называемой v-таблицей), удерживаемой объектом. Когда функция COM привязана, она связывается с помощью v-таблицы.

Структура COM-объекта проста. Если код содержит ссылку на объект, он содержит косвенный указатель на верхнюю часть таблицы v. V-таблица — это массив адресов памяти, где каждая запись является другой функцией, которая может быть вызвана для этого объекта. Чтобы вызвать третью функцию для объекта COM, перейдите по трем записям в таблице, а затем перейдите к расположению памяти, в котором она присутствует. Это приводит к выполнению кода для функции и, по завершении, возвращает сведения о готовности к выполнению следующей строки кода.

В примере выше показано, что происходит при освобождении COM-объекта. Так как все COM-объекты наследуют от IUnknown, первые три записи в таблице являются методами для IUnknown. Когда необходимо освободить объект, код вызывает третью функцию в v-table (IUnknown:: Release).

К счастью, Visual Basic работает в фоновом режиме. Программисту Visual Basic не придется напрямую работать с таблицей v. Однако эта структура связана с тем, как все объекты COM связаны, и важно знать, что такое привязка.

Раннее связывание

Приведенный выше пример называется ранним (или v-табличным) связыванием. Для всех объектов COM такая форма привязки выполняется при каждом вызове интерфейса IUnknown COM-объекта. Но что делать с другими функциями объекта? Как вызвать метод Refresh или его родительское свойство? Это настраиваемые функции, которые обычно являются уникальными для объекта. Если их расположение в таблице v не может быть предполагаемым, как найти адреса функций, необходимые для их вызова?

Ответ, конечно, зависит от того, знаете ли вы заранее, как выглядит таблица v. В противном случае вы можете выполнить такой же процесс раннего связывания для пользовательских методов объекта, как и в методах IUnknown. Как правило, это называется «раннее связывание».

Чтобы использовать раннее связывание с объектом, необходимо знать, как выглядит его таблица v. В Visual Basic это можно сделать, добавив ссылку на библиотеку типов, описывающую объект, его интерфейс (v-table) и все функции, которые можно вызвать для объекта. После этого можно объявить объект как определенный тип, а затем задать и использовать этот объект с помощью v-таблицы. Например, если вы хотите автоматизировать Microsoft Office Excel, используя раннее связывание, необходимо добавить ссылку на библиотеку объектов Microsoft Excel 8,0 из проекта | Диалоговое окно References, а затем объявите переменную как имеющую тип «Excel. Application». После этого все вызовы, выполненные в объектной переменной, будут выполняться с ранней привязкой:

Этот метод прекрасно работает в большинстве случаев, но что делать, если вы не знаете точный объект, который будет использоваться во время создания? Например, что делать, если вам нужно поговорить с несколькими версиями Excel или, возможно, к объекту «Unknown»?

Позднее связывание

COM включает IDispatch. Объекты, реализующие IDispatch, имеют disp-интерфейс (если это единственный поддерживаемый интерфейс) или сдвоенный интерфейс (если у них также есть настраиваемый интерфейс, к которому можно выполнить раннее связывание). Клиенты, которые привязываются к интерфейсу IDispatch, называются «поздней привязкой», так как они определяются во время выполнения с помощью методов IDispatch для их обнаружения. Вернемся к предыдущему примеру книги, представьте, что он похож на сноску, которая направит вам оглавление, где вы хотите «найти» номер страницы в «время чтения» вместо того, чтобы он уже печатался в тексте.

Возможности интерфейса контролируются двумя функциями: Жетидсофнамес и Invoke. Первое сопоставление имен функций (строк) с идентификатором (называемым DISPID), представляющим функцию. Зная идентификатор функции, которую вы хотите вызвать, вы можете вызвать ее с помощью функции Invoke. Такая форма вызова метода называется «поздней привязкой».

Опять же, в Visual Basic способ привязки объекта задается объявлением объекта. Если вы объявили объектную переменную как «объект», на самом деле, поговорите Visual Basic для использования IDispatch и, следовательно, позднее привязку:

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

Важно отметить, что «поздняя привязка» — это функция, которая вызывается, а не так, как она вызывается. Из предыдущего рассмотрения привязки в целом следует обратить внимание на то, что интерфейс IDispatch — это «ранняя привязка:», которая указывает на то, что Visual Basic устанавливает свойство Visible с помощью записи v-table (IDispatch:: Invoke), как и для любого вызова COM. Сам COM-объект отвечает за переадресацию вызова правильной функции, чтобы сделать Excel видимым. Это косвенное действие позволяет скомпилировать клиент Visual Basic (связанный с допустимым адресом функции), но по-прежнему не знает точную функцию, которая фактически выполняет работу.

Привязка DISPID

Некоторые клиенты автоматизации (наиболее заметно для MFC и Visual Basic 3,0, но также Visual Basic 5,0 и 6,0 по отношению к элементам управления ActiveX) используют гибридную форму позднего связывания, называемую привязкой DISPID. Если объект COM известен во время создания, идентификаторы DISPID для вызываемых функций могут кэшироваться и передаваться напрямую в IDispatch:: Invoke без необходимости вызывать Жетидсофнамес во время выполнения. Это может значительно повысить производительность, так как вместо двух вызовов COM на функцию необходимо выполнить только одну.

Привязку DISPID не является возможностью, как правило, можно выбрать в Visual Basic 5,0 или 6,0. Он используется для объектов, на которые ссылается библиотека типов, но не содержит настраиваемого интерфейса (только для объектов с disp-интерфейсом) и для агрегированных элементов управления ActiveX, но в общем случае Visual Basic использует раннее связывание, которое вы обычно используете Привязка DISPID.

Какую форму привязки следует использовать?

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

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

Раннее связывание также обеспечивает безопасность типов. Если у вас есть ссылка на библиотеку типов компонента, Visual Basic предоставляет поддержку IntelliSense, которая помогает правильно кодировать каждую функцию. Visual Basic также предупреждает, если тип данных параметра или возвращаемого значения неверен, при написании и отладке кода сохраняется много времени.

Позднее связывание по-прежнему полезно в ситуациях, когда точный интерфейс объекта неизвестен во время конструирования. Если ваше приложение обращается к нескольким неизвестным серверам или необходимо вызывать функции по имени (например, с помощью функции Visual Basic 6,0 CallByName), необходимо использовать позднее связывание. Позднее связывание также помогает обойти проблемы совместимости между несколькими версиями компонента, которые неправильно изменились или подстроили его интерфейс между версиями.

Преимущества раннего связывания по возможности делают ее наилучшим выбором.

Поддержка совместимости в нескольких версиях

Если вы будете использовать компонент, который не передается вместе с пакетом установки, и не можете быть уверенным в точной версии, с которой вы будете взаимодействовать во время выполнения, следует уделять особое внимание предварительной привязке к интерфейсу, совместимому со всеми версии компонента или (в некоторых случаях) используют позднее связывание для вызова метода, который может существовать в определенной версии, и некорректно, если этот метод отсутствует в версии, установленной в клиентской системе.

Приложения Microsoft Office предоставляют хороший пример таких серверов COM. Приложения Office, как правило, расширяют свои интерфейсы, добавляя новые функции или исправляет предыдущие недостатки между версиями. Если необходимо автоматизировать приложение Office, рекомендуется выполнить предварительную привязывание к самой ранней версии продукта, которая может быть установлена в системе клиента. Например, если необходима возможность автоматизации Excel 95, Excel 97, Excel 2000 и Excel 2002, следует использовать библиотеку типов для Excel 95 (XL5en32. ОЛБ) для поддержания совместимости со всеми тремя версиями.

Приложения Office также демонстрируют, что объектные модели с крупными сдвоенными интерфейсами могут настрадаться от маршалинга на некоторых платформах. Для обеспечения оптимальной работы кода на всех платформах используйте IDispatch.

Ссылки

Дополнительные сведения о COM, v-таблицах и использовании автоматизации можно найти в следующих книгах:

Рожерсон, Дале, внутри COM, МСПРЕСС, ISBN: 1-57231-349-8.

Курланд, Мэтт, Advanced Visual Basic 6, DevelopMent, 0201707128.

Источник

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *