Глава 51 Указатели в море памяти



Птице в небе хорошо, а рыбе – в реке. Программы «живут» в оперативной памяти, – дайте им почуять себя там, как рыба в воде, и они обретут беспредельную мощь! Следующие главы продвинут нас к этой цели.

Погружение в оперативную память

Оперативная память содержит миллионы байтовых ячеек, – вы знаете об этом. Каждой ячейке назначен уникальный номер, иначе говоря – адрес. Уникальный – это значит, что все адреса разные, – так нумеруют дома на улицах и квартиры в домах. Первой ячейке памяти присвоен адрес 0, второй – 1 и так далее. Подобно тому, как почтальон находит дом по номеру, процессор обращается к данным по адресам ячеек, где они хранятся.

Прежде, чем «задышать», программа должна перекочевать с диска в оперативную память. Рассказать про это? С включением питания компьютера в дело вступает стартовая программа – загрузчик, прошитый в постоянной памяти материнской платы. Эта программка загружает с диска в оперативную память вашу любимую операционную систему, – ритуал сопровождают загадочный скрип, мигание и попискивание. К тому моменту, когда на экране появляется знакомая картинка, часть памяти занимает операционная система. Дальше все определяют капризы пользователя. Он волен загрузить одну или несколько программ, после чего их размещение в памяти может стать таким, как показано на рис. 115.

Распределением памяти под программы заведует операционная система. По мере запуска тех или иных приложений, система «расселяет» их в свободных областях памяти, а по завершении – «выселяет», освобождая память для других целей. Этот механизм именуется динамическим распределением памяти.

Не применяйте слов, смысла которых не знаете. Что значит «динамический»? «Динамо» – греческое слово и означает силу, мощь. Оттого и полюбилось спортивным клубам, но к нашей теме это толкование не подходит. Сила порождает движение, потому «динамический» стали употреблять в смысле «подвижный», «быстрый». А программисты придали этому слову ещё один оттенок: «изменчивый», «непостоянный», разумея под этим изменчивое размещение в памяти данных и программ, — так будем понимать это слово и мы.



Рис.115 – Распределение оперативной памяти
«Планировка» памяти

Обратимся к правой части рис. 115, где упрощенно показано распределение памяти внутри одной программы. Эта память делится на три части или секции (Section – «отделение»). Одна из них вмещает исполняемый код, то есть процедуры, функции и главную программу. Другая – секция данных – отведена для глобальных переменных, а третья – так называемый стек – для параметров процедур и локальных переменных. Сейчас надо усвоить лишь две простые вещи, а именно:

• все секции программы (как и программа в целом) имеют фиксированные, то есть постоянные размеры, определяемые при компиляции программы;

• все объекты программы – процедуры, функции, переменные – обладают своими «личными» адресами в оперативной памяти; эти адреса определяются при загрузке программы, и потому могут изменяться от одной загрузки к другой.

Рассмотрим пример. Пусть в программе объявлены четыре переменные.


var B : Boolean;     C : char; I : integer;     S : string;


После загрузки программы в оперативную память они «расселятся» в соседних ячейках памяти, начиная с некоторого начального адреса N так, как показано на рис. 116.



Рис.116 – Размещение переменных в оперативной памяти

Первые ячейки этого участка памяти займут однобайтовые переменные булевого и символьного типа. В следующих двух байтах поселится целое число, а далее – в 256 байтах – строковая переменная. Подобная картина наблюдается и при размещении структурных переменных – записей; их поля занимают соседние ячейки. И хотя начальный адрес участка N может изменяться от загрузки к загрузке (его определяет операционная система), относительное размещение переменных в памяти остается тем же.

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

Отныне мы приступаем к освоению средств языка для работы с памятью. Овладев ими, вы откроете себе новые горизонты!

Начнем с нового для нас типа данных – указателя (по-английски – POINTER). Указатели могут хранить адреса переменных, процедур и функций. Нам интересны, прежде всего, указатели на переменные, рассмотрим пример обращения с таким указателем.


var P : ^integer; { указатель на целое }

    N : integer;     { целое }

begin

    P:= @N;     { указателю назначается адрес переменной N }

    P^:= 125;     { переменной присваивается значение через указатель }

    Writeln(N);     { 125 }

end.


В первой строчке объявлен указатель P. Это сделано специальным значком – стрелка вверх «^», – эта стрелка ставится перед именем типа, с которым будет работать указатель. В данном случае указатель P предназначен для хранения адресов переменных типа INTEGER.

Первый из исполняемых операторов


    P:= @N;


заносит в указатель P адрес переменной N. С этого момента указатель P ссылается на то место в памяти, где «живет» переменная N. Обратите внимание на «почтовую собачку» перед N – это операция взятия адреса. То же самое можно сделать функцией взятия адреса.


    P:= Addr(N);


Следующий далее оператор программы


    P^:= 125;


присвоит переменной N значение 125. Из чего это следует? Ведь переменной N в этом операторе нет! Все дело в стрелочке «^», стоящей после указателя P, теперь она играет другую роль. Добавление стрелочки за указателем ведет к тому, что число 125 попадает в область памяти, на которую ссылается указатель P, то есть по месту жительства переменной N.

Таким образом, стрелочка за указателем – это операция разыменования, которая противоположна операции взятия адреса, она превращает указатель в переменную, на которую он ссылается. Поэтому следующие два оператора дают одинаковый результат.


    Writeln(100+P^); { 225 }

    Writeln(100+N); { 225 }


Введите рассмотренный пример в компьютер и проверьте его в действии.

Объявление указателей

Сколько типов данных способен придумать программист? Не сосчитать! И для каждого из них можно объявить свой тип указателя, например:


var PI : ^integer; { указатель на целое }

    PC : ^Char;     { указатель на символ }

    PS : ^String; { указатель на строку }


Памятуя о нашей договоренности объявлять типы в секции TYPE, сделаем это для упомянутых типов-указателей.


type PInt = ^Integer;     { тип указателя на целое }

    PChar = ^Char;     { тип указателя на символ }

    PString = ^String;     { тип указателя на строку }


Как всегда, имя объявляемого типа выбираем по вкусу. Здесь лучше придерживаться традиций, а они рекомендуют начинать названия типов-указателей с буквы «P» (от Pointer – «указатель»).

Объявив тип, можно объявить затем переменные этого типа, например:


var p1, p2 : PInt; { два указателя на целое }

    p3 : PChar; { указатель на символ }


Копирование указателей, пустой указатель

Я сказал, что указатель содержит адрес переменной, стало быть, это число? Да, но не совсем обычное. Нас интересует не само это число, а лишь то, на что оно указывает.

Если нужны несколько указателей на одну и ту же переменную, указатели копируют, например:


    P1 := @X1; { В указатель P1 заносится адрес переменной X1 }

    P2 := P1; { Оба указателя содержат адрес переменной X1 }


Теперь оба указателя ссылаются на переменную X1 (хотя сам по себе адрес переменной X1 нас не интересует). Но копировать можно лишь указатели одного типа, – за этим соответствием следит компилятор.

А на что ссылается указатель, которому не присвоено значение? Как любая неинициализированная переменная, он содержит мусор и указывает «пальцем в небо». Для пометки временно не используемого указателя ему присваивают специальное значение NIL. Это зарезервированное слово языка – подобие нуля для чисел. Значение NIL можно присвоить указателю любого типа, например:


    p1 := nil; { p1 – пустой указатель на целое }

    p3 := nil; { p3 – пустой указатель на символ }


Указатели похожи на письма, а переменные – на дома, куда эти письма адресованы (рис. 117).



Рис.117 – Указатели подобны письмам

Судя по рис. 117, жителям первого дома повезло, – им адресованы два письма. Третий конверт – без адреса, он пуст и подобен указателю, содержащему NIL.

Сравнение и проверка указателей

Поскольку указатели – это не обычные числа, их нельзя вычитать, складывать и сравнивать на «больше» или «меньше». Зато можно сравнивать на равенство и неравенство. В таком сравнении есть смысл: ведь если непустые указатели равны, то ссылаются на одну и ту же переменную. Вот примеры правильных сравнений.


    if p1=p2 then …

    if p1<>p2 then …

    if p3=nil then …


Сравнением с NIL выясняется, свободен ли указатель или ссылается на что-то. Но значение NIL в указатель должен занести программист, само оно там не появится!

Проверить незанятость указателя можно как сравнением с NIL, так и функцией Assigned. Она принимает указатель любого типа, а возвращает булев результат. Вот примеры её применения.


    p1 := @X; p3 := nil;

    Writeln (Assigned(p1));     { true }

    Writeln (Assigned(p3));     { false }


Функция Assigned возвращает FALSE, если указатель содержит NIL.

Разыменование указателей

Этим неуклюжим словом – разыменование – названа операция, обратная взятию адреса. Разыменование превращает указатель в переменную, на которую он ссылается. Операция обозначается следующей за указателем стрелкой вверх «^», вот пример.


    p1 := @X;     { назначение адреса указателю P1 }

    X := 25;

    Writeln (p1^);     { 25 }

    X := 100;

    Writeln (p1^);     { 100 }


Здесь показано, что с изменением переменной X меняется и значение P1^. Иначе говоря, если P1=@X, то P1^=X (а верно ли обратное?).

Итак, указатели дают ещё один способ доступа к переменным, к которым мы обращаемся по именам. В чем же выгода от указателей? – пока её не видно. Но, проявив немного терпения, вы изведаете всю их мощь.

Нетипичный указатель

Типы указателей соотносятся с типами данных, на которые они ссылаются. Но порой нужен универсальный указатель, способный ссылаться на что угодно. Такой указатель объявляют как Pointer, – указатели этого типа нельзя разыменовать, но можно сравнивать между собой и со значением NIL.


var P1, P2 : pointer;     N : integer;     S : string;

begin

    P1:= @N; P2:= @S;

    if P1=P2 then Writeln('Указатели совпадают');

    if P1<>nil then Writeln('Указатель не пустой');

end.


Впрочем, такой указатель можно привести к любому другому типу указателя (преобразовать тип указателя), и тогда возможно разыменование полученной конструкции, например:


type PInt = ^integer; { тип указателя на целое }

var P : pointer;     N : integer;

    P:= @N;

    Writeln( PInt(P)^ ); { печатается значение N }


Примеры с указателями

Рассмотрим пару несложных программ, поясняющих работу указателей, испытайте их на своем компьютере.


{ P_51_1 – Указатели }

var A, B, C : integer;     { целые числа }

p1, p2, p3 :^integer; { указатели на целые числа }

begin

{ Присвоение значений переменным }

A:= 10; B:= 20; C:= 30;

{ Последовательное переключение одного указателя на разные переменные }

p1:= @A; Writeln(p1^);

p1:= @B; Writeln(p1^);

p1:= @C; Writeln(p1^);

{ Настройка трех указателей на одну переменную }

p1:=@B; p2:=p1; p3:=p1;

Writeln(p1^:6, p2^:6, p3^:6);

{ Арифметические действия через указатели }

C:= 2 * p1^;

Writeln(C); { C= 2 * B = 40 }

Readln;

end.


Результат работы этой программы таков.


10

20

30

20 20 20

40


Здесь опять убеждаемся, что разыменованный указатель равнозначен переменной, на которую он ссылается. С ним выполняют те же действия, что и с переменной: ввод, вывод, арифметические операции и так далее.

В программе «P_51_2» мы ещё раз увидим это, а вдобавок исследуем размеры указателей на переменные разных типов, – отличаются ли они?


{ P_51_2 – Указатели разных типов, размеры указателей }

type PBool= ^boolean; { Тип указателя на булевскую переменную }

    PInt = ^integer; { Тип указателя на целое число }

    PStr = ^string; { Тип указателя на строку }

var B : boolean;

I : integer;

S : string;

pB : PBool; { Указатель на булевскую переменную }

pI : PInt; { Указатель на целое число }

pS : PStr; { Указатель на строку }


begin

{ Настройка указателей на переменные }

pB := @B; pI := @I; pS := @S;

{ Присвоение значений переменным через указатели }

pB^ := true;

pI^ := 10;

pS^ := 'Hello!';

{ Распечатка значений переменных }

Writeln(B:6, I:6, S:10);

{ Исследование размеров типов и указателей на них }

Writeln('Boolean = ',SizeOf(Boolean):6, SizeOf(PBool):6);

Writeln('Integer = ',SizeOf(integer):6, SizeOf(PInt ):6);

Writeln('String = ',SizeOf(String ):6, SizeOf(PStr ):6);

Readln;

end.


Вот «продукция» этой программы.


true 10 Hello!

Boolean = 1     4

Integer = 2     4

String = 256 4


Любопытны три последних строки. Они показывают, что размеры указателей на переменные всех типов одинаковы и для 32-разрядных систем составляют 4 байта (подобно тому, как размер конверта не зависит от размера дома, куда он адресован).

В следующей главе мы пожнем первые плоды от применения указателей, а пока подведем итоги.

Итоги

• Память компьютера – это последовательность ячеек, которым назначены уникальные адреса.

• Объекты программы – переменные, процедуры и функции – занимают ячейки памяти, адреса которых можно определить операцией взятия адреса @ или функцией Addr.

• Для хранения адресов применяют переменные особого типа – указатели. Каждому типу переменных соответствует свой тип указателя.

• Перед использованием указателя ему присваивают либо адрес переменной, либо пустое значение NIL.

• С указателями допустимы лишь три операции: копирование, сравнение и разыменование.

• Разыменованный указатель – это переменная, на которую он ссылается в данный момент; с ним можно поступать как с этой переменной.

• Указатели всех типов имеют одинаковый размер, который для 32-разрядных операционных систем составляет 4 байта.

А слабо?

А) Какие ошибки найдет компилятор в следующей программе? Объясните их.


var P1 : ^Integer; P2 : ^String;

    N : Integer;     S : String;

begin

    P1 := @S;

    P2 := @N;

end.


Б) Будет ли работать следующая программа? В чём ошибки?


var P1 : ^Integer;

begin

    P1 := 0;

    P1^ := 30;

    P1 := nil; Writeln(P1^);

end.


В) Откройте программу «P_51_1» и введите в окно обзора переменные P1 и P1^ (комбинацией Ctrl+F7). Выполняя программу по шагам, наблюдайте за переменными. Сделайте то же с программой «P_51_2».

Загрузка...