Всем читателям 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 и т.д.: мы используем операцию «присваивания».
Присвоить можно:
значение того же типа, что и указатель (строка 36, 37)
адрес другой переменной (строка 11, 17)
специальное значение, которое называется пустой указатель и обозначается служебным словом nil (Оно не связано ни с каким объектом, т.е. ни на что фактически не указывает, строка 41)
значение типа, на который указывает указатель (строка 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 к свободному указателю может привести к ошибке.
Достоинства и недостатки указателей
Достоинства указателей
уменьшают объем памяти и сокращает время выполнения программы
позволяют возвращать несколько значений из функции и могут использоваться для передачи информации между функциями
дают возможность изменить размер динамически выделенного блока памяти
позволяют получить доступ к любой ячейки памяти компьютера
помогают создавать сложные структуры данных, такие как связанный список, стек, очереди, деревья, графики и т.д.
Недостатки указателей
выделенный динамически блок памяти необходимо освобождать явно, иначе может произойти утечка памяти
повышают вероятность возникновения ошибок и проблем с памятью. При этом найти и исправить эти ошибки задача не из легких, особенно в объемных программах
сложны для понимания и требуют определенного объема знаний. Программист несет ответственность за эффективное и правильное использование указателей
Для чего нужны указатели?
Если нет особой необходимости использовать указатели, лучше выбирать альтернативный вариант, который практически всегда присутствует. Например, использование ссылок.
Для того, чтобы напрямую работать с памятью
Указатели нужны для того, чтобы можно было напрямую работать с оперативной памятью.
Например, при передаче указателя в функцию компьютер не создаёт её локальную копию, а обращается к ней напрямую.
Для динамического управления памятью
Если нам нужно выделить в памяти некоторую область для хранения своих данных, но стандартные переменные нам не подходят, мы можем использовать указатель. В этом случае мы помещаем в него стартовый адрес ячейки и говорим, сколько байтов после него нужно использовать и что в них положить.
Задачи с применением указателей
Через указатели на указатели посчитать сумму двух чисел и записать в третье.
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.
Напишите функцию 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.
Заключение
В данной статье мы попытались на собственном опыте изучения указателей в Паскале поделиться полученными знаниями и простым языком объяснить базовые понятия, подкрепив их примерами и схемами. Если вы разобрались в теме указателей: получили новые знания или освежили в памяти ранее изученное, значит, цель статьи достигнута
Конечно, мы рассказали не все об указателях и их возможностях. Надеемся, что данная статья помогла Вам найти ответы на интересующие вопросы по рассматриваемой теме и побудила к дальнейшему, более глубокому, изучению указателей не только на языке Паскаль, но и применению их в любых своих программах. До встречи в будущих статьях :)