Статья написана для нашего форума нашим жестяным программером Robo. За что ему ОГРОМНОЕ спасибо! На написание данной заметки меня толкнуло то, что в учебниках по программированию не уделяется достаточного внимания такой важной теме, как указатели. То есть, внимание, то конечно, уделяется, но вот принцип обучения работы с указателями или не очень понятен или, что ещё хуже, вообще не ясен. Таким образом, на разных форумах и конференциях по программированию каждый третий начинающий программист просит помочь ему разобраться с этой темой. Эта небольшая заметка постарается восполнить этот пробел. Все примеры будут на разных языках программирования - так чтобы всем было понятно, независимо от того, какой язык вы изучаете. Я так же предполагаю, что вы знакомы хотя бы с одним из трёх языков программирования - это Pascal \ Delphi, C++ и Ассемблер. Базовые знания Для того чтобы понять, что такое указатель и усвоить основные принципы работы с ними, нужно хорошо представлять, что из себя, представляет память. В общем, случае, память можно представить в виде отдельных ячеек, расположенных в одном большом блоке или даже если вспомнить математику – в виде матрицы (вектором) – строки. Каждая такая ячейка памяти имеет свой адрес, по которому она расположена. Наглядно, память можно представить так: Так, каждая ячейка таблицы представляет одну ячейку памяти. У каждой ячейки адрес представлен числами от 0 до N. Так вот, указатель - это просто переменная определённого размера, которая содержит адреса ячеек памяти. Так же, указатель может иметь специальное значение, обозначающее нулевой или пустой адрес, обозначающий то, что указатель сейчас нельзя использовать для обращения к памяти. Как вы знаете, каждый тип переменной имеет свой размер. Так, например, под тип integer - процессор выделяет 2 байта памяти, а под тип byte - 1 байт. Под longint - DWORD - то есть "двойное слово", что означает 4 байта. Так, WORD - это 2 байта, или "слово". Так, один байт - это две цифры от 00 до FF (в шестнадцатеричной системе исчисления). Для каждого типа данных - свой размер в памяти (какой именно, можно узнать в справочнике по языку). То есть, как вы уже поняли, если смотреть по нашей таблице, и если бы мы описали переменную - указатель на тип Integer, то она бы заняла ровно 2 байта в памяти и наша таблица, то есть память, приняла бы следующий вид: Где AA и BB - это первый и второй байт числа! То есть - 2 байта. Вот таким образом наша программа и узнаёт, сколько байт взять с адреса переменной, для успешной операции. Постойте-ка, а как же строка?! Строка - это просто напросто массив байт, который заканчивается нулевым байтом. Вот поэтому, нулевому байту - процессор и определяет, до какого места считывать строку из памяти. Для чего используются указатели? Как вы знаете, вызов процедур и функций в языках программирования выполняется следующим образом: 1. Сначала параметры, которые должны передаться в процедуру, помещаются в стек (Стек - это область хранения временных и\или локальных переменных и адресов). 2. Процедура извлекает данные из стека в обратном порядке 3. Выполняет свою работу, используя извлечённые из стека параметры А теперь допустим, что мы написали функцию или процедуру, которой, например, передаются два параметра, строка и число. Так, сначала процессор запишет в стек число, а уже потом строку. Число, например, займёт в стеке 4 байта, что не так критично, а вот строка займёт множество байт, потому что каждый её символ - это один байт. Если сказать точнее, то строка - это массив байт. Так, если например наша строка состоит из 300 или даже 3000 символов? Правильно, в стеке эти 3000 байт займут около 3-х килобайт! Это уже достаточно критично, так как будет затрачиваться достаточное количество ресурсов, как процессора, так и памяти?! Так вот, чтобы не попасть в эту ситуацию, достаточно передать в функцию только указатель на строку, который, кстати, займёт в стеке всего 4 байта (DWORD). Мы как бы говорим нашей функции, где искать нашу строку, то есть, говорим ей адрес начала строки. Код: #іncludе <іostrеam.h> іnt maіn(int argc, char *argv[]) { char *lpString; // Обьявим переменную-указатель на тип char char String[] = "Наша новая строка"; // Обьявим массив (строку) cout << String << endl; // Выводим строку через переменную lpString = &String[0]; // Присвоим указателю начальный адрес String cout << lpString << endl; // Выводим строку через указатель return 0; // Конец программы } Сама переменная типа указатель объявляется в С++ как обычно, только после указания типа переменной необходимо поставить знак астериска - "*". В примере, я указываю, что объявляю указатель с именем lpString для типа char. Сначала вывожу текст обычно, через переменную, потом, присваиваю указателю lpString адрес переменной String в памяти, для этого используется операция взятия адреса - амперсанд ("&"). Тем самым, переменная указатель, получает адрес начала строки в памяти. То есть. как бы устанавливается в нашем случае на ячейку 0 (см.таблицу). И только потом я вывожу строку через указатель, то есть через адрес. Функция cout выводит всё то что было с адреса 0 до последнего символа, пока не встретит в памяти нулевой байт - 0x00 и только потом останавливается. Кстати, для того чтобы взять значение непосредственно из ячейки - используется операция разыменовывания указателя, путём приставки в виде символа "*" перед началом переменной. То есть, если бы мы вместо строки: Написали бы: То получили бы на выходе всего один символ - "Н". Приведу примеры на остальных языках: Делфи: Код: program Project2; uses SysUtils,Windows; var String1:array[0..11] of Char = 'Hello World'+#0; // Обьявляем массив+нуль-байт lpString:Pointer = @String1; // Присваиваем указателю адрес массива begin MessageBox (0,lpString,String1,0); //выводим окно с текстом но из разных источников end. В данной программе, мы выводим окно посредствам функции MessageBox из WindowsAPI, где заголовок выводим из массива, а главный текст окна - из адреса указателя. Разыменовывание указателя в паскале и делфи можно выполнить с помощью знака "^". Паскаль: Код: program test1; var str:string; lpStr:pointer; begin lpStr:=@str; str:='Pascal pointer!'; writeln (string(lpStr^)); readln; dispose(lpStr); lpStr:=nil; end. В секции объявлений переменных указываем, что переменная lpStr имеет тип указателя - то есть указывает на какую-то переменную. Как вы наверное уже поняли, операция взятия адреса в паскале осуществляется добавлением знака "@" непосредственно перед той переменной, чей адрес необходимо взять. Так, вы можете видеть, как я присваиваю адрес переменной str указателю lpStr. И затем уже, вывожу строку на дисплей с помощью разыменованного указателя. Указатель необходимо уничтожить функцией Dispose сразу после того, как необходимость в нём отпадает. И затем присвоить ему значение нуль (null, nil, 0x00, в зависимости от языка). Запомните это! Это очень важное правило! Кстати, обратите внимание, как я вывожу строку: Слово string в данном случае указывает на то, что в памяти по этому адресу у нас лежит именно строка, а не число и ничто либо другое. Этого можно избежать, определяя, например, новые типы указателей на целое или строку. Разберём ещё один пример на паскале: Код: program test1; uses crt; type lpStr=^string; lpInt=^integer; lpReal=^real; var str:string; Pointer_String:lpStr; Pointer_Integer:lpInt; Pointer_Real:lpReal; vString:String; vInteger:Integer; vReal:Real; begin clrscr; vString := 'Where is my pointer?'; vInteger := 100; vReal := 456.3000; Pointer_String := @vString; Pointer_Integer := @vInteger; Pointer_Real := @vReal; writeln (Pointer_String^); writeln (Pointer_Integer^); writeln (Pointer_Real^); … … тут мы чистим наши указатели… … Тут, мы явно указываем типы указателей. То есть, например, lpStr - это указатель на тип String и тд. И при обращении к разыменованному указателю, мы уже вольны не указывать, на что именно ссылается наш указатель. Добавлено через 10 минут Ну, и, в конце концов, посмотрим пример на языке Ассемблера с использованием распространённого компилятора - MASM32. Я не буду приводить весь код, а покажу только самое важное: АССЕМБЛЕР MASM Код: .data String db "Assembly string!",00 ; Обьявили массив с нулевым байтом на конце .data? lpString dd ? ; переменная (4 байта) указатель, в ней будет адрес String .code start: push offset String ; кладём в стек смещение переменной String pop [lpString] ; вытаскиваем её из стека и кладём в lpString push 0 ; вызываем окно push [lpString] ; c указателями на строки push [lpString] push HWND_DESKTOP call MessageBox Адрес переменной String мы получаем посредствам директивы OFFSET через стек. Стек я использую потому, что в нём такие операции занимают меньше времени, чем если бы мы написали mov [lpString], offset String. Хотя ошибку эта запись не вызовет, и её можно использовать, если вам так удобно. Обратите внимание на скобки ( [] )! Ими как вы знаете, мы указываем процессору, с какими значениями оперировать, то есть, если мы пишем String - это адрес строки, а если бы написали [string] - имели ввиду её значение, то есть текст, или число, в зависимости от типа. Но единственная причина, почему мы не написали просто mov [lpString],string - это потому, что такая конструкция категорически не приемлема для компилятора MASM, хотя FASM проглотит это без проблем. FASM: Код: іncludе 'D:\FASM\include\win32ax.inc' .data Count dd 00h; lpString dd 00h; string db "Hello Wordl",00 .code start: mov [lpString],string ; в lpString кладём адрес string invoke MessageBox,0,[lpString],0,0 invoke ExitProcess,0 .end start Как видите, этот ассемблер справляется с такими конструкциями очень легко. Ну, вроде с указателями разобрались. Теперь закрепим всё одной удобной таблицей и продолжим работать с указателями. Таблица 1 - Принцип взятия значения переменной, на которую указывает PTR, и принцип взятия начального адреса переменной в памяти. Указатели - прекрасный инструмент работы со строками Разработаем программу, которая бы заменяла в строке все символы "*" на пробелы и печатала результат в консоли. Вариант на Си: Код: #іncludе <іostrеam.h> #іncludе <wіndows.h> voіd main() { char* lpCharString; char szText[]="1*2*3*4*5*6*7*8*9*0"; int a; lpCharString = &szText[0]; int size; for (size = lstrlen (lpCharString); size != 0; size--,lpCharString++) { if (*lpCharString == '*') *lpCharString = 0x20; cout << *lpCharString; } } Ничего сложного в ней как видите - нет. Единственное, стоит сказать, что в этой программе, я использую указатель на Char чтобы проверять в цикле значение каждого символа, то есть каждого байта на предмет совпадения с символом-звёздочкой и заменять значения необходимых байтов на пробелы (0x20). В цикле, я увеличиваю адрес на который указывает указатель, для того чтобы он при следующей итерации указывал на следующий символ в строке ровно до тех пор, пока переменная size не будет равна нулю! Давайте посмотрим вариант на Delphi: Код: program Pointers; {$APPTYPE CONSOLE} uses SysUtils,Windows; var szStr: array [0..20] of Char; lpStr:^Char; iSize:Integer; Counter:Integer; begin szStr:= '1*2*3*4*5*6*7*8*9*0'; lpStr:= @szStr; iSize:= Length(szStr); for Counter:=1 to iSize-2 do begin if lpStr^ = '*' then begin lpStr^ := ' '; end; write (lpStr^); inc (lpStr); end; Readln; end. Как видите тоже всё достаточно удобно и наглядно. Но одну вещь я всё - таки объясню. Видите, где в цикле я указываю -2 от общей длины? Так вот, формат Delphi строк таков, что перед самой строкой стоит поле типа WORD и в этом поле содержится длинна строки. Выглядит это вот так: А так как нам они не нужны, мы просто их отнимаем и всё. Хотя если не отнимать - всё будет работать конечно. Но я вам просто показал, каков формат Delphi строк. В остальном, в этой программе всё полностью идентично той, которая запрограммирована на Си. С паскалем дела обстоят так же. Продолжение следует… Rob.
а чего роб сам не выложил ее?( +1 за артикл, давно еще когда с си знакомился долго мучался с указателями =(
rijy, https://xaker.name/forvb/showthread.php?goto=newpost&t=13559 и https://xaker.name/forvb/showthread.php?goto=newpost&t=13562 =(
Есть пару сомнительных моментов (например с тем что символ строки это байт или то что параметры запихиваются в стек слева направо, и т.д.) Но в общем очень и очень не плохо И для новичков самое оно))) Robo молодец
так что наоборот справа на лево ) Добавлено через 6 минут а байт это и есть символ кстати ) Добавлено через 13 минут а если не верити мну так верьти Yar Tikhiy который сказал что:
Хел, Robo, вы говорите о ASCII. Ну, а я к тому, что современное человечество уже юникодом пользуется В сишнике это t_char, если я не ошибаюсь) Поэтому и написал, что сомнительно. Я же не говорю, что это не правильно) По поводу параметров функции. Я постоянно это путаю, поэтому могу перепутать. Но я точно знаю, что Delphi и С их засовывают в обратном порядке) Т.е. если С справа-налево(как мне кажется и есть правильно), то делфи слева-направо) Добавлено через 3 часа 13 минут Почитал, еще раз Внимательно Ошибочку раскопал) на строчке Код: program test1; var str:string; lpStr:pointer; begin lpStr:=@str; str:='Pascal pointer!'; writeln (string(lpStr^)); readln; dispose(lpStr); lpStr:=nil; end. Во время работы он тебя грязно обругает Ошибкой 200, если я правильно помню Ты пытаешься чистить часть памяти, которая находиться не в куче... Т.е. пытаешься удалить не динамическую переменную Он тебе в ответ на это такое должен рассказать... Dispose используется только после явного или неявного вызова New (проверить не могу, паскаля нет)
Собственно, нет. Паскаль не ругается и даже не должен, проверенно несколько раз. Это - правило работы с указателями в паскале. То есть, когда он не нужен - необходимо освобождать. Сверился с учебниками Немнюгина - так же.
Robo, пардон Просто Delphi ведет себя адекватно и ругается Дело в том, что освобождать когда не нужен необходимо. Проблема лишь в том, что освобождать нечего) Память в куче НЕ ВЫДЕЛЯЛАСЬ Поэтому и смутили эти строки Паскаль скорее всего делает проверку в функции Dispose и выходит ничего не делая)
кстати роб на делфи показал как удобно с помощью указателей работать со строками вот пример на си: Код: #include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { char arr[]="h-e-l-k-e-r-n"; char *parr=arr; for(; *parr; *parr++) if(*parr=='-') *parr=' '; printf("%s", arr); system("PAUSE"); return 0; } а вот аналог без использования указателей: Код: #include <stdio.h> #include <stdlib.h> int main(int argc, char *argv[]) { char arr[]="h-e-l-k-e-r-n"; int i; for(i=0; i<=strlen(arr); i++) if(arr=='-') arr=' '; printf("%s", arr); system("PAUSE"); return 0; } поэтому очевидно что удобнее использовать указатели чтобы например не объявлять лишнюю переменную счетчик
Всем привет возникла проблемка не могу найти решение Есть программа я пишу что то типо плагина к ней (на дельфи) нахожу хэндл окна программы, id процесса, открываю процесс, считываю слово из памяти процесса... Так вот адрес слова всегда разный например то такой 001A87F8 то такой 0152BD82 как мне программного на дельфи найти адрес моего слова в памяти чтоб не вписывать его постоянно. Если открыть exe программы в том же hiew то постоянный адрес в exe у моего слова 0036D0C8 (RVA) и .0076D0C8 (VA), а в памяти всегда разный. вот моя процедура procedure TForm1.Button1Click(Sender: TObject); var hProc, PID, numberRead : DWORD; hWnd : THandle; // Хэндл окна IpBuf : PWideChar; // буфер куда поместиться наше значение len : integer; ipBase: ^dword; // точка входа в памяти Buf : PChar; Write:cardinal; New:string; // новое значение begin hWnd:=Findwindow('TfChatClient',nil); // получаем хэндл окна программы Edit1.Text:=inttostr(hWnd); // заносим в edit Handle GetWindowThreadProcessId(hWnd, @PID); //получаем pid зная хэндл окошка интересующей нас программы Edit2.Text:=inttostr(PID); // заносим в edit ID hProc:=OpenProcess(PROCESS_ALL_ACCESS, False, PID); // подключаемся к процессу зная его ID if hProc <> 0 then // условие проверки подключения к процессу try len := 200; ipBuf :=AllocMem(len); Edit3.Text:=inttostr(hProc); // заносим в edit Process ipBase:=ptr($0152BD82); // адрес слова в памяти как его узнать ? ReadProcessMemory(hProc, ipBase, ipBuf, len, numberRead); // чтение из памяти строки Edit4.Text:=WideCharToString(ipbuf); // заносим в edit Buffer New:=Edit5.Text; // присваевываем наше слово переменной StringToWideChar(New, ipBuf, len); // заносим в буфер WriteProcessMemory(hProc, ipBase, ipBuf, 15, write); //меняем старое значения слова на новое finally CloseHandle(hProc); // отсоединяемся от процесса FreeMem(ipBuf); // освобождаем память end; end;
Скорее всего твоя подопытная прграмма исполюзует динамическое распределение памяти (DMA). Короче - программа при каждом запуске меняет адреса или адрес для нужного тебе значения. Например деньги или здоровье в играх, для некоторых адрес денег (назовём так) никогда не меняется, а для других он всегда разный. Так, для программ времён win95/9x DMA не использовалось, а сейчас - повсеместно. Ну а для того, чтобы найти нужный адрес в памяти, тебе нужно понять, как это делает сама программа\игра. А делает она это, считывая указатель указателя (или указатель адреса), того самого, который и держит значение! В отличии от самих нужных тебе адресов переменных, адреса указателей никогда не меняются. Тренер\редактор памяти для DMA должен сначала считывать из указателя адрес интересующего значения, а только потом его менять, а иначе толку нема! Перечитал, вроде всё понятно...