- Говорят, что в VB6 нет указателей вообще. Это утверждение не верно.
- В VB6 нет способа объявить переменную-указатель, а раз так, нет и способа установить её (сделать так, чтобы она указывала куда-то). Однако указатели сами по себе в VB6 есть. Они там существуют в более безопасном воплощении указателей — в виде ссылок.
- Я думаю уже многие догадались, что речь идёт о ByRef аргументах. ByRef аргумент — по своей сути указатель, и указывать он может на всё-что угодно: на локальную переменную (лежащую в стеке) внутри процедуры, из которой вызывается другая процедура (с аргументом), на какую-то глобальную переменную (лежащую в секции данных). ByRef аргумент может указывать даже на константу. В этом случае константа (её копия) помещается в стек непосредственно перед вызовом, и передаётся указатель на константу (её копию) в стеке.
- На заметку:
Сделано это не просто так. Указатель передаётся именно на копию константы (которая создаётся прямо перед вызовом, и уничтожается сразу же после вызова), а не саму константу, по той причине, что процедура может менять данные, указатель на которые ей передали. Если бы VB просто передавал указатель на константу, а не на её временную копию, константа, переданная By Ref смогла бы измениться, что противоречит здравому смыслу (её название происходит от лат. constantum - постоянный, а тут вдруг на тебе - изменилась). В общем, в таком случае все константы стали бы пред-инициализированными глобальными переменными. Поэтому используется именно копия константы. Поэтому же, константы даже не помещаются в секцию данных, а вшиваются сразу в код (за исключением строковых констант, конечно, которые всё-равно помещаются в секцию данных).
- Объявить в процедуре переменную-указатель мы не можем, но можем объявить аргумент-указатель. Это уже хорошо. Однако этот указатель будет указывать не туда, куда нам хочется, верно? Обладая замечательной функцией PutMem4 (или чуть менее замечательной — CopyMemory) мы имеем возможность «перевешать» такой самодельный указатель туда, куда-нам надо.
- Использование PutMem4 требует от нас две вещи:
1) Знать, куда писать.
2) Знать, что писать.
Куда писать, понятно, — поверх ByRef-аргумента. Что-писать, надеюсь, тоже понятно (адрес, куда должен указывать указатель). - Проблемы начинаются тогда, когда мы хотим определить адрес аргумента-указателя. Да чего там, — скажите вы, — VarPtr же есть. VarPtr, действительно, есть, но кроме него есть ещё и один нюанс.
Дело в том, что VB по-особому кастует указатели к указателям. Скажем, если вы передаёте в функцию переменную lFoo:
... по значению (ByVal) — в стек кладётся значение переменноьй
... по ссылке (ByRef) — в стек кладётся адрес переменной lFoo.
Однако, если lFoo — не локальная переменная, а локальный ByRef-аргумент, в стек попадёт не адрес этого аргумента, а адрес того, на что сам этот ByRef-аргумент ссылается, то есть значение (фактическое) этого ByRef-арумента.
Не совсем понятно? Давайте рассмотрим пример:- Код: Выделить всё
Function Main()
Dim zipa as long
zipa = 123456789
AAA zipa
End Function
Function AAA(ByRef MyArg As Long)
MsgBox CStr(MyArg)
BBB MyArg
End Function
Function BBB(ByRef MyBBBArg As Long)
MsgBox CStr(MyBBBArg)
End Function
Здесь в функцию AAA передаётся не само значение переменной zipa, а ссылка (указатель) на неё. Однако, в AAA при вызове BBB в качестве аргумента MyBBBArg передаётся уже не указатель на AAA->MyArg, а указатель на Main->zipa.
Опять же, если бы VB при использовании ByRef-аргументов везде использовал указатель, мы бы в BBB имели двойной указатель и увидели бы сообщение не "123456789", а какой-нибудь адрес (не просто какой-нибудь, а VarPtr(Main-->zipa) если быть точным)). - К чему я всё это? Да к тому, что получить реальный адрес ByRef-аргумента в стеке нам с помощью VarPtr никогда не удастся (из-за умного кастования). Максимум, что мы получим - адрес переменной (или копии константы), на которую ссылается ByRef-аргумент.
- Тем не менее, получить адрес аргумента в стеке всё-же возможно, хотя и не так красиво.
Если в аргументах перед нашим ByRef-аргументом определить другой аргумент Optional ByVal Stub As Long, то адрес нашего ByRef-аргумента можно получить как VarPtr(Stub) + 4. А адрес следующего адрес следующего — VarPtr(Stub) + 8.- На заметку:
Stub можем сделать не первым, а последним аргументом, и отнимать от его VarPtr числа, кратные 4. Надо только учитывать, что Double-переменные занимают 8 байт стека, а не 4. - Давайте уже попробуем что-нибудь написать:
- Код: Выделить всё
Sub MyCoolProc(Optional ByVal Stub As Long, Optional ByRef MyPointer As SomeType)
' Определить реальный адрес MyPointer с помощью VarPtr - нельзя.
' Но его можно вычислить как VarPtr(Stub) + 4.
End Sub
Теперь я думаю, понятно, что записывая по адресу VarPtr(Stub) + 4 нужные значения, мы можем управлять указателем MyPointer. Причём тип MyPointer-а может быть любым (почти), например, это может быть какой-то пользовательский тип. - Хотим пример? Пожалуйста. Первое, что мне пришло в голову — быстрая замена символов в строке:
- Код: Выделить всё
Public Sub Repl(ByVal sString As String, _
ByVal Search As Integer, _
ByVal Replacement As Integer, _
Optional ByVal Stub As Long, _
Optional ByRef Replacer As Integer)
' По поводу аргументов:
' sString - строка, в которой производится замена
' Search - что ищем
' Replacement - чем заменяем
' Stub - заглушка, Replacer - наш самодельный указатель
Dim CurPos As Long
Dim EndPos As Long
Dim ptrAddress As Long ' Здесь будем хранить VarPtr(Stub)+4
' чтобы каждый раз не считать
CurPos = StrPtr(sString)
EndPos = CurPos + LenB(sString)
ptrAddress = VarPtr(Stub) + 4
Do
PutMem4 ptrAddress, CurPos
' Момент истины:
If Replacer = Search Then Replacer = Replacement ' А вы думали? ;)
CurPos = CurPos + 2 ' Переходим к обработке след. символа
Loop Until CurPos = EndPos
MsgBox sString
End Sub
Выглядит массивно из-за комментов. Без комментов эта же функция выглядит так:- Код: Выделить всё
Public Sub Repl(ByVal sString As String, ByVal Search As Integer, ByVal Replacement As Integer, _
Optional ByVal Stub As Long, Optional ByRef Replacer As Integer)
Dim CurPos As Long
Dim EndPos As Long
Dim ptrAddress As Long
CurPos = StrPtr(sString)
EndPos = CurPos + LenB(sString)
ptrAddress = VarPtr(Stub) + 4
Do
PutMem4 ptrAddress, CurPos
If Replacer = Search Then Replacer = Replacement ' А вы думали? ;)
CurPos = CurPos + 2
Loop Until CurPos = EndPos
MsgBox sString
End Sub
Пробуем:- Код: Выделить всё
Repl "мама мыла раму", AscW("м"), AscW("п")
- Интересно сравнить производительность нашей функции с Replace (стандартной и самодельной):
Замена в строке "мама мыла раму мило" всех "м" на "п", 200000 циклов замены, время усреднённое (10 экспериментов):
Стандартный Replace.................................... 1.443839 сек.
Наш Repl (с указателями)............................ 0.206268 сек. ( )
Самописный Replace на основе Mid$ .......... 2.001647 сек.
(Скопилировано в Native-код. Учитывайте, что Declare Function PutMem4 сжирает 30% времени. Если объявить её (PutMem4) в TLB — будет ещё быстрее).
Итак, мы получили выигрыш в 7-10 раз. Стоило ли ради этого что-то придумывать? Безусловно, да. Наш код с указателями обогнал почти в 10 раз код без указателей, и в 7 раз - функцию, написанную на C++.
Не будем, конечно, докапываться до сишной функции, она гораздо более универсальна чем наша, но всё же. - Другой пример, — оперирование произвольными байтами переменной:
- Код: Выделить всё
Public Sub SomeProc(Optional ByVal stub As Long, Optional ByRef pByte As Byte)
Dim Foo As Long
Foo = &HFF00FF00
PutMem4 VarPtr(stub) + 4, VarPtr(Foo) ' Установили указатель
pByte = &H77
MsgBox Hex$(Foo) ' Будет FF00FF77
' Другой пример - получить 5-ый байт числа Sqrt(3)
' побитовые операции здесь не помогут :)
Dim n As Double
n = Sqr(3)
PutMem4 VarPtr(stub) + 4, VarPtr(n) + 4 ' Установили указатель
MsgBox "5-ый байт числа Sqrt(3) равен: " + CStr(pByte)
End Sub
- Теперь, если у вас есть адрес какой-то структуры в памятя, не делайте CopyMemory этой структуры в User-Defined-тип. CopyMemory — это очень долго (особенно если структура большая). Просто установите UDT-шный указатель на эту структуру и работайте с ней через поля своего пользовательского типа.
- Самые любопытные наверняка хотят спросить: «А вот если нам нужно много указателей (15 штук, скажем), нам что, объявлять у процедуры 15 Optional ByRef аргументов? А не будет ли это влиять на время вызова? А не будет ли из-за этого использоваться лишняя память?»
Начну с лишней памяти. Если бы в VB были указатели — вы бы сделали их локальными переменными - и они бы заняли 15 × 4 байт в стеке. Когда вы передаёте 15 аргументов, в стек помещается 15 dword'ов, так что используется тоже 15×4 байт стека. Так что здесь вы ничего не проигрываете. А вот небольшая задержка к сожалению будет. Если бы мы имели дело с локальными переменным, VB просто бы сделал sub esp, 60. А так, вместо одной инструкции, мы получим 15 пушей. Впрочем, задержка эта минимальна. - Итак, мы разобрались, что с применением не слишком сложной магии в VB появляется поддержка переменных-указателей. Собственно, на этом потенциал нераскрытых возможностей VB не заканчивается. Проделав ту же фишку с Declare Function мы получим ни много, ни мало — вызов функций по указателю средствами языка.
Можно будет взять адрес у GetProcAddress, присвоить его своему указателю и вызвать (используя родной синтаксис языка) нужную функцию.
Правда здесь всё на порядок сложнее, и я пока работаю над реализацией этой задумки.