Книга: Песни о Паскале

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

Глава 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».

Оглавление книги

Оглавление статьи/книги

Генерация: 1.250. Запросов К БД/Cache: 3 / 1
поделиться
Вверх Вниз