Указатели в программировании.

Тема в разделе "Статьи", создана пользователем rijy, 3 янв 2009.

  1. rijy
    rijy Новичок
    Симпатии:
    33
    Статья написана для нашего форума нашим жестяным программером Robo. За что ему ОГРОМНОЕ спасибо!

    На написание данной заметки меня толкнуло то, что в учебниках по программированию не уделяется достаточного внимания такой важной теме, как указатели. То есть, внимание, то конечно, уделяется, но вот принцип обучения работы с указателями или не очень понятен или, что ещё хуже, вообще не ясен. Таким образом, на разных форумах и конференциях по программированию каждый третий начинающий программист просит помочь ему разобраться с этой темой. Эта небольшая заметка постарается восполнить этот пробел. Все примеры будут на разных языках программирования - так чтобы всем было понятно, независимо от того, какой язык вы изучаете. Я так же предполагаю, что вы знакомы хотя бы с одним из трёх языков программирования - это Pascal \ Delphi, C++ и Ассемблер.

    Базовые знания
    ​
    Для того чтобы понять, что такое указатель и усвоить основные принципы работы с ними, нужно хорошо представлять, что из себя, представляет память. В общем, случае, память можно представить в виде отдельных ячеек, расположенных в одном большом блоке или даже если вспомнить математику – в виде матрицы (вектором) – строки. Каждая такая ячейка памяти имеет свой адрес, по которому она расположена. Наглядно, память можно представить так:

    [​IMG]
    ​


    Так, каждая ячейка таблицы представляет одну ячейку памяти. У каждой ячейки адрес представлен числами от 0 до N. Так вот, указатель - это просто переменная определённого размера, которая содержит адреса ячеек памяти. Так же, указатель может иметь специальное значение, обозначающее нулевой или пустой адрес, обозначающий то, что указатель сейчас нельзя использовать для обращения к памяти.
    Как вы знаете, каждый тип переменной имеет свой размер. Так, например, под тип integer - процессор выделяет 2 байта памяти, а под тип byte - 1 байт. Под longint - DWORD - то есть "двойное слово", что означает 4 байта. Так, WORD - это 2 байта, или "слово". Так, один байт - это две цифры от 00 до FF (в шестнадцатеричной системе исчисления). Для каждого типа данных - свой размер в памяти (какой именно, можно узнать в справочнике по языку).
    То есть, как вы уже поняли, если смотреть по нашей таблице, и если бы мы описали переменную - указатель на тип Integer, то она бы заняла ровно 2 байта в памяти и наша таблица, то есть память, приняла бы следующий вид:

    [​IMG]
    ​

    Где 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
    Как видите, этот ассемблер справляется с такими конструкциями очень легко.
    Ну, вроде с указателями разобрались. Теперь закрепим всё одной удобной таблицей и продолжим работать с указателями.

    [​IMG]
    ​
    Таблица 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 и в этом поле содержится длинна строки. Выглядит это вот так:

    [​IMG]
    ​

    А так как нам они не нужны, мы просто их отнимаем и всё. Хотя если не отнимать - всё будет работать конечно. Но я вам просто показал, каков формат Delphi строк. В остальном, в этой программе всё полностью идентично той, которая запрограммирована на Си. С паскалем дела обстоят так же.

    Продолжение следует… Rob.
     
    Последнее редактирование: 3 янв 2009
    3 янв 2009
    1 человеку нравится это.
  2. Хел
    Хел Guest
    а чего роб сам не выложил ее?(
    +1 за артикл, давно еще когда с си знакомился долго мучался с указателями =(
     
    3 янв 2009
  3. rijy
    rijy Новичок
    Симпатии:
    33
    роб празднует)

    з.ы. давно кстати от тебя нифига не видели..
     
    3 янв 2009
  4. Хел
    Хел Guest
  5. rijy
    rijy Новичок
    Симпатии:
    33
    Хел, статьи! статьи!
     
    4 янв 2009
  6. Хел
    Хел Guest
    rijy,
    о чем?(
     
    4 янв 2009
  7. ~|~евто|-|
    ~|~евто|-| Silentium Новичок
    Симпатии:
    32
    Есть пару сомнительных моментов :) (например с тем что символ строки это байт :) или то что параметры запихиваются в стек слева направо, и т.д.)
    Но в общем очень и очень не плохо :) И для новичков самое оно)))

    Robo молодец :)
     
    5 янв 2009
  8. Хел
    Хел Guest
    ~|~евто|-|,
    тут смотря как смотреть)
    char это байт
     
    5 янв 2009
  9. Robo
    Robo Guest
    так что наоборот справа на лево )

    Добавлено через 6 минут
    а байт это и есть символ кстати )

    Добавлено через 13 минут
    а если не верити мну так верьти Yar Tikhiy который сказал что:

     
    Последнее редактирование модератором: 5 янв 2009
    5 янв 2009
    1 человеку нравится это.
  10. ~|~евто|-|
    ~|~евто|-| Silentium Новичок
    Симпатии:
    32
    Хел, 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 :) (проверить не могу, паскаля нет)
     
    Последнее редактирование: 6 янв 2009
    6 янв 2009
  11. Robo
    Robo Guest
    Собственно, нет. Паскаль не ругается и даже не должен, проверенно несколько раз. Это - правило работы с указателями в паскале. То есть, когда он не нужен - необходимо освобождать. Сверился с учебниками Немнюгина - так же.
     
    6 янв 2009
  12. ~|~евто|-|
    ~|~евто|-| Silentium Новичок
    Симпатии:
    32
    Robo, пардон :)
    Просто Delphi ведет себя адекватно и ругается :)

    Дело в том, что освобождать когда не нужен необходимо. Проблема лишь в том, что освобождать нечего) Память в куче НЕ ВЫДЕЛЯЛАСЬ :) Поэтому и смутили эти строки :) Паскаль скорее всего делает проверку в функции Dispose и выходит ничего не делая)
     
    8 янв 2009
  13. Хел
    Хел Guest
    кстати роб на делфи показал как удобно с помощью указателей работать со строками вот пример на си:
    Код:
    #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;
    }
    

    поэтому очевидно что удобнее использовать указатели чтобы например не объявлять лишнюю переменную счетчик
     
    12 янв 2009
    1 человеку нравится это.
  14. DjDram
    DjDram Новичок
    Симпатии:
    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;

    [​IMG]
     
    Последнее редактирование: 21 июл 2009
    21 июл 2009
  15. Robo
    Robo Guest
    Скорее всего твоя подопытная прграмма исполюзует динамическое распределение памяти (DMA). Короче - программа при каждом запуске меняет адреса или адрес для нужного тебе значения. Например деньги или здоровье в играх, для некоторых адрес денег (назовём так) никогда не меняется, а для других он всегда разный.

    Так, для программ времён win95/9x DMA не использовалось, а сейчас - повсеместно. Ну а для того, чтобы найти нужный адрес в памяти, тебе нужно понять, как это делает сама программа\игра. А делает она это, считывая указатель указателя (или указатель адреса), того самого, который и держит значение! В отличии от самих нужных тебе адресов переменных, адреса указателей никогда не меняются. Тренер\редактор памяти для DMA должен сначала считывать из указателя адрес интересующего значения, а только потом его менять, а иначе толку нема!

    Перечитал, вроде всё понятно...
     
    5 авг 2009
    3 пользователям это понравилось.
Загрузка...