DevProg: Блог для программистов

Секреты программирования. Примеры разработки. Обзоры программёрского софта, блогов и форумов и просто полезные советы!

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

Posted by devprog на Январь 15, 2009

Данная статья изначально писалась для форума xaker.name лично мною, но потом перекачевала вот на этот блог. Надеюсь она вам пригодится.

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

Базовые знания

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

Так, каждая ячейка таблицы представляет одну ячейку памяти. У каждой ячейки адрес представлен числами от 0 до N. Так вот, указатель — это просто переменная определённого размера, которая содержит адреса ячеек памяти. Так же, указатель может иметь специальное значение, обозначающее нулевой или пустой адрес, обозначающий то, что указатель сейчас нельзя использовать для обращения к памяти.

 
Как вы знаете, каждый тип переменной имеет свой размер. Так, например, под тип integer — процессор выделяет 2 байта памяти, а под тип byte — 1 байт. Под longint — DWORD — то есть «двойное слово», что означает 4 байта. Так, WORD — это 2 байта, или «слово». Так, один байт — это две цифры от 00 до FF (в шестнадцатеричной системе исчисления). Для каждого типа данных — свой размер в памяти (какой именно, можно узнать в справочнике по языку).

То есть, как вы уже поняли, если смотреть по нашей таблице, и если бы мы описали переменную — указатель на тип Integer, то она бы заняла ровно 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 и только потом останавливается.

Кстати, для того чтобы взять значение непосредственно из ячейки — используется операция разыменовывания указателя, путём приставки в виде символа «*» перед началом переменной.
То есть, если бы мы вместо строки:

cout << lpString << endl;

Написали бы:

cout << *lpString << endl;

То получили бы на выходе всего один символ — «Н».

Приведу примеры на остальных языках:

Delphi

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, в зависимости от языка). Запомните это! Это очень важное правило!

Кстати, обратите внимание, как я вывожу строку:

writeln (string(lpStr^))

Слово 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 и тд. И при обращении к разыменованному указателю, мы уже вольны не указывать, на что именно ссылается наш указатель.

Ну, и, в конце концов, посмотрим пример на языке Ассемблера с использованием распространённого компилятора — MASM32. Я не буду приводить весь код, а покажу только самое важное:

.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 проглотит это без проблем.

і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

Как видите, этот ассемблер справляется с такими конструкциями очень легко.
Ну, вроде с указателями разобрались. Теперь закрепим всё одной удобной таблицей и продолжим работать с указателями.

Указатели — прекрасный инструмент работы со строками

Разработаем программу, которая бы заменяла в строке все символы «*» на пробелы и печатала результат в консоли. Вариант на Си:

#і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 строк. В остальном, в этой программе всё полностью идентично той, которая запрограммирована на Си. С паскалем дела обстоят так же.

Продолжение следует… Читайте новые посты!

Реклама

комментария 3 to “Указатели в программировании”

  1. Славян said

    Отличная статья, все разложено по полочкам, четко и структурировано.Спасибо!

  2. Сарсен said

    Зашибись, с помощью этой статьи наконец то добил, что такое термин «указатель».

  3. Константин said

    спасибо, тебе, мил-человек. с этой статьёй пришло понимание. у других много букв на китайском

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

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход / Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход / Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход / Изменить )

Google+ photo

Для комментария используется ваша учётная запись Google+. Выход / Изменить )

Connecting to %s

 
%d такие блоггеры, как: