* Самый простой тип методов -- статические. Хотя они есть во всех ОО-языках, в COM они не попали, и поэтому в VB6 не поддерживаются. Суть здесь в том, что связывание (binding) имени вызываемого метода с конкретным адресом функции выполняется при компиляции; сам вызов ничем не отличается от вызова обычной функции, разве что неявным первым параметром передаётся this. Преимущества статических методов очевидны -- нет никаких накладных расходов на поддержание объектной структуры; классы всего лишь группируют воедино методы для работы с данными одного типа.
Когда один класс наследуется от другого, в памяти собственные данные объекта-потомка располагаются после данных, унаследованных от предка; получается, что объект-предок как бы "вмонтирован" в объект-потомок, причём они начинаются по одному и тому же адресу. В этом случае при преобразовании указателя на один из этих объектов в указатель на другой содержимое этого указателя не меняется -- меняется только его тип. В частности, и новым методам потомка, и унаследованным им от предка приходит один и тот же this.
Когда один класс наследуется от нескольких других, данные всех его предков не в состоянии располагаться по одному и тому же адресу. Один из предков, как и в предыдущем случае, оказывается в начале объекта и имеет тот же this, что и объект-потомок; все остальные предки размещаются в объекте по некоторому смещению. Для этих предков указатель на метод будет занимать 8 байт, 4 из которых содержат собственно адрес функции, а другие 4 -- величину "корректора" (adjustor), который прибавляется к this объекта-потомка, чтобы получить this объекта-предка, ожидаемый его методом. Важно отразить, что коррекция this осуществляется компилятором при вызове метода, потому что он заранее знает, к методу какого класса относится вызов, и поэтому может предсказать, на сколько нужно скорректировать this.
Недостаток статических методов -- невозможность их переопределения в классах-потомках. Переопределение может потребоваться в двух случаях: когда базовый класс реализует некоторую абстрактную функциональность, и ему необходимо, чтобы ключевым методам была сопоставлена конкретная реализация ("переопределение по инициативе предка"), либо когда в классе-потомке требуется изменить (например, расширить) функциональность базового класса ("переопределение по инициативе потомка"). Переопределение первого типа при использовании статических методов вовсе невозможно; при переопределении второго типа внешние вызовы методов класса будут корректно связываться компилятором с классом-потомком, но вызовы изнутри класса-предка, а также из уже скомпилированных модулей, будут продолжать ошибочно связываться с классом-предком. Кроме того, вызовы методов объекта-потомка через указатель на класс-предок также будут связываться с классом-предком.
* Второй тип методов -- "полувиртуальные"; я их выдумал сам, и я не знаю ни одного языка, в который они были бы включены. Идея такая: для каждого переопределяемого метода в базовом классе заводится по защищённому (protected) полю типа "указатель на функцию"; конструктором базового класса эти поля инициализируются указателями на реализации выбранных методов в этом базовом классе. Все вызовы выбранных методов должны производиться через эти поля-указатели; лучше вообще сделать сами методы-реализации приватными.
В этом случае класс-наследник может, перезаписывая в своём конструкторе нужные поля указателями на новые реализации переопределённых методов, связать все вызовы этих методов со своим классом -- и снаружи базового класса, и изнутри него, и даже из уже скомпилированного кода. Вообще, проблемы можно ожидать только с уже скомпилированным кодом и только при множественном наследовании, когда базовый класс окажется в объекте-наследнике по ненулевому смещению; в этом случае при вызове по указателю на метод требуется использование корректора, который невозможно вставить задним числом в уже существующий код. Однако если подумать, то становится ясно, что передать новый объект в уже существующий код можно только приведя его к его классу-предку, и уже при этом приведении this будет скорректирован необходимым образом.
При использовании данной технологии расходуется от 4 до 8 байт на каждый переопределяемый метод в каждом объекте; если переопределяемых методов много, а объектов ещё больше, то это приведёт к очень большому расходу памяти. С другой стороны, это компенсируется гибкостью таких "полувиртуальных" методов: их можно переназначать в процессе выполнения программы, что позволяет объектам динамически менять свой тип. Если поля с указателями на переопределяемые методы сделать не защищёнными, а публичными, то их можно будет использовать как указатели на обработчики событий объекта. Можно придумать и более изощрённые способы использования таких методов. При этом их вызов лишь на один шаг сложнее вызова обычных статических методов: из поля объекта извлекается указатель на функцию, после чего она вызывается обычным образом.
* Виртуальные методы, образующие основу COM, работают похожим образом; только здесь все указатели на переопределяемые методы класса собраны в одну таблицу -- "таблицу виртуальных методов" (VMT) или просто "виртуальную таблицу" (VTbl), и эта таблица хранится в единственном экземпляре для каждого класса. Когда у класса есть виртуальные методы, первые 4 байта каждого объекта этого класса указывают на VTbl. В этом случае с каждым методом ассоциируется не смещение внутри объекта поля с указателем на функцию-реализацию, как в предыдущем примере, а смещение этого поля внутри VTbl. В остальном всё работает точно таким же образом, только ещё на один шаг сложнее: из объекта извлекается указатель на VTbl, из неё извлекается указатель на функцию, и затем эта функция вызывается. Мы получаем все преимущества предыдущего способа при намного меньшем расходе памяти, но и меньшей гибкости: поскольку VTbl общая у всех объектов одного класса, невозможно во время выполнения создать из конкретных объектов "подкласс", перехватывая вызовы их методов.
При наследовании от базового класса, включающего VTbl, новые виртуальные методы дописываются в конец существующей VTbl: таким образом, объект-предок "вмонтирован" в объект-потомок на двух уровнях -- данные предка "вмонтированы" в данные потомка, и VTbl класса-предка "вмонтирована" в VTbl класса-потомка. Если класс-потомок наследуется от нескольких классов-предков, каждый из которых включает свою VTbl, то в составе объекта-потомка будет несколько указателей на VTbl: по одному на каждый базовый класс. В первую из этих VTbl будут дописываться новые виртуальные методы класса-потомка. При этом this для каждого объекта базового класса в составе объекта-потомка соответствует положению в составе объекта-потомка указателя на VTbl этого базового класса. Но теперь на этапе компиляции неизвестно, к какому из классов будут относиться вызовы виртуальных методов, и поэтому скорректировать this при вызове невозможно. Вместо этого в состав объекта-потомка включается набор корректоров-переходников (adjustor thunks), по одному на каждый виртуальный метод каждого базового класса, кроме первого. Каждый такой переходник просто корректирует this и вызывает соответствующий метод базового класса. В этом случае вызов виртуального метода оказывается ещё на один шаг сложнее.
В COM и VB6 связывание методов по VTbl, т.е. в момент вызова, называется "ранним связыванием" (early binding), хотя в терминологии C++ это связывание оказывается как раз поздним -- по сравнению со статическим, которое выполняется при компиляции. Раннее связывание в VB6 используется для классов, которые объявлены в TLB; к ним не относятся формы, юзер-контролы, и все стандартные контролы VB6.
* В Delphi предлагается ещё один тип переопределяемых методов, связываемых в момент вызова -- "динамические". Каждый из динамических методов класса получает 2-байтный идентификатор, и как один из элементов VTbl этого класса хранится указатель на список идентификаторов переопределённых в нём динамических методов, вместе с указателями на новые реализации этих методов. Это несколько экономит память по сравнению с использованием настоящих виртуальных методов, потому что хранятся только указатели на фактически переопределённые методы, а не на все вообще методы, которые возможно переопределить; с другой стороны, здорово снижается скорость вызова таких методов, потому что при каждом вызове необходимо пробежаться по таблицам динамических методов всей цепочки наследования объекта (хорошо ещё, что в Delphi нет множественного наследования, а то приходилось бы обходить всё дерево). В справке Delphi указано, что динамические методы имеет смысл применять тогда, когда переопределяемых методов много, но фактически переопределяются лишь немногие из них. В частности, на динамических методах построена обработка сообщений в VCL: если бы в VTbl каждого класса-контрола хранился полный список обработчиков для всех сообщений, этот список занимал бы неоправданно много памяти. Вместо этого для класса хранится только список тех обработчиков, которые в нём переопределены.
Напоследок -- деталь реализации динамических методов в Delphi: идентификаторы от 1 до $BFFF присваиваются обработчикам сообщений в соответствии с кодом обрабатываемого сообщения; идентификаторы от $C000 до $FFFF назначаются методам, объявленным с ключевым словом dynamic. Указатель на VTbl класса-предка хранится в VTbl класса-потомка среди прочих служебных "псевдо-виртуальных методов"; к ним относится уже названный указатель на таблицу динамических методов, указатель на имя класса, указатель на обработчик сообщений по умолчанию и прочая служебная информация.
Продолжение.