Знакомство с указателями в Паскале

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

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

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

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

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

14e613c4923b2ff74c5d7ae7e3e4b25d.png

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

8fc411847eb0efdf973f3461d62d48f7.png

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

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

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

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

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

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

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

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

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

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

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

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

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

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

4a20fe4ea0906d523777dec2e2151826.png

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

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('значение p:',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)

Результат работы программыРезультат работы программыfe3d3cc6c68b5114304abdd56c2cb424.png

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

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

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

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

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

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

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

ae77e1a35fdf0364956fe4fab4eb5f0d.png

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

32b615e4674c6ce87108a6cea0f49000.png

Переменная 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.

Заключение

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

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

© Habrahabr.ru