Всем читателям habr.com, привет! Мы студенты Технического ВУЗа- Мария и Екатерина, и хотим рассказать о своем опыте работы с указателями на языке программирования Паскаль.

Знакомство с указателями произошло еще на первом курсе, когда нам читали предмет по языку программирования Паскаль. Данная тема нас заинтересовала, поэтому мы изучили множество статей и учебной литературы. Отметим, не нашли ни одной, в которой довольно подробно, понятно и, главное, доступно для людей любого уровня знаний было бы рассказано об использование указателей в Паскале. Безусловно, информация по этой теме имеется в интернете, но она разрознена и большинство авторов сложно доносят информацию для неподготовленного читателя, который только начинает путь программиста.

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

Виды памяти в языке программирования Паскаль

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

 Существует много различных видов оперативной памяти. Все эти виды можно разделить на две подгруппы — статическая память (Static RAM) и динамическая память (Dynamic RAM). Когда говорится о видах памяти, имеются в виду способы организации работы с ней, включая выделение, освобождение памяти и методы доступа.

Статическая память

Статическая память - это память, которая выделяется до начала работы программы, на стадии компиляции и сборки.

Компиляция - это преобразование программы, составленной на исходном языке высокого уровня (одним из которых является Паскаль - процедурно-ориентированный язык программирования высокого уровня), в эквивалентную программу на низкоуровневом языке или машинном коде (Машинный код - это двоичные числа, выражающие команды процессора и данные, которые нужно обработать. Его трудно понять и проводить в нем какие-то корректировки).

Сборка — процесс получения информационного продукта из исходного кода. Чаще всего сборка — исполняемый файл — двоичный файл, содержащий исполняемый код (машинные инструкции) программы или библиотеки.

  • статические переменные имеют фиксированный адрес, известный до запуска программы и не изменяющийся в процессе ее работы.

  • доступ к статическим переменным осуществляется через их имена.

  • статические программные объекты порождаются автоматически перед выполнением программы или подпрограммы, в которой они описаны, и существуют, пока выполнение этой программы или подпрограммы не завершится. Размер статических объектов не изменяется на протяжении всего времени их существования.

Примером статического объекта в языке Паскаль является переменная, описанная в блоке программы или подпрограммы (процедуры, функции).

Приведем пример статического объекта:

var n: integer;
begin
n:=32;
end.

Такое объявление порождает статическую переменную целого типа.

Динамическая память

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

Под динамическую память отводится пространство между статической памятью и стеком. Обычно стек располагается в старших адресах виртуальной памяти и растет в сторону уменьшения адресов. Программа и константные данные размещаются в младших адресах, выше располагаются статические переменные. Пространство выше статических переменных и ниже стека занимает динамическая память:

Динамическую память обычно используют при:

  • обработке больших массивов данных

  • разработке САПР (Система Автоматизации Проектных Работ)

  • временном сохранение данных при работе с графическими и звуковыми средствами ЭВМ

    К таким объектам относят:

  • файлы (текстовые, типизированные, нетипизированные)

  • линейные структуры

    • односвязные (очередь, стек, список и т.д.)

    • многосвязные (многосвязный список)

  • кольцевые структуры (односвязный и многосвязный кольцевые списки)

  • разветвленные структуры (деревья и графы)

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

Указатели простейшие действия с ними

Указатель - это переменная, которая содержит адрес другой переменной (байта памяти).

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

var
  p:^integer;

Где «^» означает, что задаётся указательный тип, а затем идет имя любого стандартного или ранее описанного типа.

Операции над указателями

Для работы с указателем объявим еще одну переменную, но уже не указательного типа (строка 3).

var
  p:^integer;
  n:integer;
  k:^integer;
  k1:integer;
  y1:^integer;
  y2:^integer;
  y3:^integer;
begin
  n:=5;
  p:=@n;
  writeln('адрес n:',@n);
  writeln('значение n:',n);
  writeln('адрес p:',@p);
  writeln('значение p:',p);
  writeln('Разыменование или получения значения по адресу,который содержит p в качестве значения:',p^);
  k:=@k1;
  k^:=9;
  writeln('Разыменование или получения значения по адресу,который содержит k в качестве значения:',k^);
  writeln();
  If k^=p^ then
    begin
    writeln('значения переменных, расположенных по разным адресам, одинаковое');
    writeln('значение p:',p);
    writeln('значение k:',k);
    end;
    
  If k^<>p^ then
    begin
    writeln('значения переменных, расположенных по разным адресам, разные');
    writeln('Разыменование k:',k^);
    writeln('Разыменование p:',p^);
    end;
    
    writeln();
    y2:=@n;
    y1:=y2;
    writeln('Разыменование y1:',y1^);
    writeln('Разыменование y2:',y2^);
    writeln();
    y3:=nil;
    writeln('Значение y3:',y3);
    
 end.
  • В строках 11, 12, 14, 17, 36: мы получаем адрес переменной, используя символ «@».

  • В строках 16, 19, 21, 28 и т.д.: мы получаем значение переменной по её адресу, используя символ «^». Данная операция называется «разыменование».

    (Разыменование  это операция получения значения объекта, адрес которого хранится в указателе).

    Добавим в нашу программу две переменные: типа указатель - k и целое k1 (строки 4,5).

  • В строках 10, 11, 17 и т.д.: мы используем операцию «присваивания».

    Присвоить можно:

    1. значение того же типа, что и указатель (строка 36, 37)

    Результат работы программы
    1. адрес другой переменной (строка 11, 17)

    2. специальное значение, которое называется пустой указатель и обозначается служебным словом nil (Оно не связано ни с каким объектом, т.е. ни на что фактически не указывает, строка 41)

      Результат работы программы
    3. значение типа, на который указывает указатель (строка 18)

Результат работы программы
  • В строке 21: мы сравниваем указатели на «равенство».

  • В строке 28: мы сравниваем указатели на «неравенство».

Процедуры для работы с указателями

Первым шагом после объявления переменной типа указатель (строка 2) является процедура выделения памяти, которая обозначается new(указатель). Данная процедура имеет один параметр (строка 4).

var
  p:^integer;
begin
  new(p);
end.

После применения процедуры new под переменную p выделилась память.

*Для более лучшего усвоения материала будем графически изображать указатели. На схеме точка будет ставиться точка у указателя и рисоваться стрелка для связывания его с соответствующим объектом.

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

В начальный момент выполнения программы переменная p не имеет никакого значения:

После создания динамического объекта указатель на него автоматически присваивается переменной p. Схематично результат изображается следующим образом:

Переменная p теперь "указывает" на объект целого типа, поэтому саму указательную переменную тоже называют указателем. Заметим, что параметр процедуры new однозначно определяет, какого типа объект порождается. В данном случае из описания типа переменной p следует, что порождается объект типа integer. Отметим, что порождаемые объекты не имеют никакого начального значения.

Для освобождения динамического памяти, на которую указывает указатель применяется процедура удаления dispose(указатель) (строка 8). Параметр в этой процедуре должен быть указатель на уже существующий динамический объект, иначе возникнет ошибка.

var
  p:^integer;
begin
  new(p);
  writeln('Адрес указателя: ',@p);
  writeln('Значение указателя: ',p);
  writeln('Разыменование указателя: ',p^);
  dispose(p);
  writeln();
  writeln('Адрес указателя после удаления: ',@p);
  writeln('Значение указателя после удаления: ',p);
  end.
Результат работы программы

После применения объект, указанный в качестве фактического параметра перестает существовать, a указатель на него удаляется из множества значений указательного типа, в результате чего все переменные, содержащие указатель на уничтоженный объект, становятся неопределёнными.

Следует помнить, что повторное применение процедуры dispose к свободному указателю может привести к ошибке.

Достоинства и недостатки указателей

  1. Достоинства указателей

    • уменьшают объем памяти и сокращает время выполнения программы

    • позволяют возвращать несколько значений из функции и могут использоваться для передачи информации между функциями

    • дают возможность изменить размер динамически выделенного блока памяти

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

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

  2. Недостатки указателей

    • выделенный динамически блок памяти необходимо освобождать явно, иначе может произойти утечка памяти

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

    • сложны для понимания и требуют определенного объема знаний. Программист несет ответственность за эффективное и правильное использование указателей

Для чего нужны указатели?

Если нет особой необходимости использовать указатели, лучше выбирать альтернативный вариант, который практически всегда присутствует. Например, использование ссылок.

  1. Для того, чтобы напрямую работать с памятью

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

    Например, при передаче указателя в функцию компьютер не создаёт её локальную копию, а обращается к ней напрямую.

  2. Для динамического управления памятью

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

Задачи с применением указателей

  1. Через указатели на указатели посчитать сумму двух чисел и записать в третье.

var
  num_1, num_2:integer; //два числа, значения которых будут использоваться в сложении 
  sum:integer; //переменная для сохранения рез-та сложения
  x, y:^integer; //переменные для хранения адресов двух чисел
  x1:^^integer; //первое слагаемое
  y1:^^integer; //второе слагаемое

begin
//занесение в переменные числовых значений
num_1:=1;
num_2:=2;
//присваивание переменным типа указатель в кач-ве значения адресов переменных целевого типа
x:=@num_1;
y:=@num_2;
//присваивание переменным типа указатель на указатель в кач-ве значения адресов переменных типа указатель
x1:=@x;
y1:=@y;
//суммирование двух чисел, это получается за счет двойного разыменования переменной типа указатель на указатель на тип integer
sum:=x1^^+y1^^;
writeln('сумма:',sum); 
end.
  1. Напишите функцию swap, которая меняет значения переданных аргументов.

    В Pascal существуют два типа подпрограмм: процедуры и функции (служебные слова: procedure, function). Процедуры после выполнения не возвращают никакое значение из подпрограммы, а функция возвращает результат. При написании подпрограмм важным этапом выступает передача параметров. Выделяют параметры-значения и параметры-переменные.

    • Параметры-значения

      При этом в формальные параметры подпрограммы передаются копии фактических. Перед формальными параметрами нет слова Var. С такими параметрами удобно работать, так как при вызове подпрограммы на их место можно подставить не только переменную, но и константу или выражение. Даже если внутри подпрограммы значение такого параметра меняется, при выходе из нее оно восстанавливается (так как меняется значение не самого параметра, а его копии).

    • Параметры-переменные

      При этом в формальные параметры подпрограммы передаются адреса фактических. Фактические значения по указанному адресу меняются. Перед формальными параметрами указывается слово Var.

      Пример передача значений в подпрограмму со словом var есть в задаче 2.

//Создаем два указателя на целое число и две переменные типа целое число
var
    p1:^integer;
    p2:^integer;
    x:integer;
    y:integer;
    
{Процедура меняет местами значения двух переменных;
 Входные параметры: две целые переменные;}
procedure Swap(var a,b:integer);
//Создаем временную переменную типа целое
var temp:integer;
begin
  //Присваиваем temp значение первой переменной
  temp:=a;
  //Присваиваем первой переменной значение второй
  a:=b;
  //Присваиваем второй переменной значение temp
  b:=temp;
end;

begin
    //Присваиваем целое значение переменным
    x:=5;
    y:=8;
    //Инициализируем указатели
    p1:=@x;
    p2:=@y;
    //Вызываем процедуру, которая меняет местами значения двух переменных, используя операцию разыменовывание
    Swap(p1^, p2^);
    writeln (p1^);
    writeln(p2^);
end.

Заключение

В данной статье мы попытались на собственном опыте изучения указателей в Паскале поделиться полученными знаниями и простым языком объяснить базовые понятия, подкрепив их примерами и схемами. Если вы разобрались в теме указателей: получили новые знания или освежили в памяти ранее изученное, значит, цель статьи достигнута

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