§ 11. Списки
1. Поняття про динамічну пам’ять, вказівники та динамічні змінні. Розрізняють звичайну (статичну) та динамічну організації пам’яті комп’ютера. В оперативній пам’яті можна розмістити обмежену кількість даних, зокрема, змінних. Коли змінні оголошують у розділі var, система надає їм певний обсяг пам’яті, навіть якщо не всі змінні будуть використані у програмі. Пам’ять, надана змінним, вивільняється лише після виконання програми. Однак є задачі, де заздалегідь невідомо, скільки змінних потрібно для їхнього розв’язування, а отже, який обсяг пам’яті слід зарезервувати. У цьому випадку, а також, якщо заздалегідь знають, що даних буде багато, застосовують динамічну організацію пам’яті. Принцип динамічної організації пам’яті полягає в тому, що змінні займають пам’ять за необхідністю, опрацьовуються і в потрібний момент вивільняють пам’ять. Такi змiннi називаються динамiчними. Для роботи з динамiчними змiнними використовують тип даних — вказiвник. Якщо iм’я статичної змінної задає адресу даного в оперативній пам’яті, то вказівник на динамічну змінну — лише тип даного, а не його розташування в пам’яті. Тип даних вказівник описують за допомогою символу ^ у розділі type так:
type <назва типу> = ^<базовий тип>;
Конкретні вказівники на динамічні змінні оголошують, як звичайно, у розділі var:
var <список вказівників на змінні> : <назва типу>;
Приклад. Розглянемо описи типів вказівників і оголошення вказівників на динамічні змінн
|
type VkazNaCili = ^integer; VkazNaMasyv = ^ array [1..100] of real; VkazNaZapys = ^Zapys; var c1, c2 : VkazNaCili; mas1, mas2 : VkazNaMasyv; zap1, zap2 : VkazNaZapys; |
На етапі компіляції пам’ять для масивів та записів тут не надається (але сам вказівник займатиме в пам’яті 4 байти). Пам’ять для даних, про можливість появи яких попереджає вказівник, буде надана на етапі виконання програми за допомогою процедури new:
new(<вказівник на змінну>);
Тільки тепер утворилася динамічна змінна, ім’я якої має такий вигляд:
<вказівник на змінну>^ .
Розрізняють операції над вказівником на динамічну змінну та операції над самою динамічною змінною. З динамічною змінною можна виконувати операції, визначені для даних відповідного базового типу. Над вказівниками визначені дві операції переадресації
1) <вказівник 1> := <вказівник 2>; 2) <вказівник> := nil;
а також процедури, зокрема, new та dispose. У результаті виконання першої команди переадресації вказівник 1 буде містити адресу тієї ж ділянки пам’яті, що й вказівник 2, тобто вони вказуватимуть на одне й те ж дане. У результаті виконання другої команди присвоєння вказівник не вказуватиме на конкретне дане (він стає вільним - nil). Після опрацювання динамічної змінної пам’ять можна вивільнити за допомогою процеду
dispose(<вказівник на динамічну змінну>) .
Приклад. Розглянемо програму Vkazivnyky і її графічну ілюстрацію на рис. 1
|
program Vkazivnyky; var c1, c2 : ^integer; begin new (c1); new (c2); c1^ := 5; c2^ := 7; writeln(c1^,c2^); c1 := c2; writeln(c1^,c2^); c2 := nil; dispose(c2); writeln(c1^); end. |
|
{Оголошуємо два вказівники} {Резервуємо пам’ять для цілого числа} {Резервуємо пам’ять для цілого числа} {Змінна c1^ отримує значення 5} {Змінна c2^ отримує значення 7} {Виводимо 5 та 7} {Переадресація} {Виводимо 7 та 7} {Вказівник c2 занулюємо} {Пам’ять, надану для c2, вивільняємо} {Виводимо 7} |
Рис. 1. Графічна інтерпретація дій з
вказівниками
Довідка.
За допомогою динамічних змінних можна
розв’язати задачу почергового
опрацювання одною програмою деякої
кількості великих масивів (якщо усі
масиви ввести в пам’ять одночасно
неможливо). Задачу розв’язують так.
Оголошують потрібну кількість
вказівників на масиви, наприклад,
var
mas1, mas2,... : ^array...
Створюють new(mas1)
і опрацьовують динамічні змінні:
mas1^[1], mas1^[2], ...,mas1^[i],... Вивільняють
пам’ять: dispose(mas1).
Створюють і опрацьовують елементи
другого масиву: mas2^[i] і т. ін.
2. Поняття про список.
Розглянемо структуру даних —
однонаправлений
(однозв’язний) список.
Список
— це скінченна сукупність даних одного
типу, між якими налагоджено зв’язок.
Елемент (однонаправленого) списку
складається з двох частин: самого
даного (даних) та вказівника на наступний
елемент списку. Для опису такої структури
використовують тип даних запис і
тип даних вказівник таким чином:
|
type <назва елемента списку> = ^<запис>; <запис> = record <поле даного> : <тип даного>; <поле вказівника> : <назва елемента списку> end; |
Приклад. Розглянемо файл, у якому є дані про ріки і назвемо його Riky.pas (див. задачу з § 10). Тип запису про річку назвемо rika і поставимо йому у відповідність елемент списку такого типу:
|
type elspysku =^rika; rika = record nazva : string[11]; dov : integer; pl : longint; dali : elspysku; end; var element, pershyj, poperednij, novyj : elspysku; |
Тут
element — вказівник на поточний елемент
списку, element^ — динамічна змінна типу
запис, element^.dov — динамічна змінна типу
integer, яка набуває значення довжини річки,
а element^.dali — вказівник на наступний
елемент списку. Звідси випливає, що
element^.dali^.dov — це довжина наступної річки,
а element^. dali^. dali — вказівник на ще наступну
річку і т. ін.
Задача.
Є файл даних на диску і ще одне дане, яке
треба долучити до файлу. На
базі старого файлу створити новий файл
з цим даним на початку.
Розглянемо один із способів розв’язування
задачі. Дані слід ввести з файлу в
оперативну пам’ять, опрацювати і
створити на носії інший файл. Під
опрацюванням будемо розуміти вилучення
даних чи записування додаткових даних
тощо.
Щоб ввести всі дані з файлу в
оперативну пам’ять, хотілося б
використати структуру даних —
масив. Але кількість даних у файлі
заздалегідь невідома, тому звичайний
масив використати не можна. Навіть
якщо кількість даних відома (або якщо
використати динамічний масив,
який ми тут не розглядаємо), то працювати
з таким масивом нераціонально. Адже,
щоб вставити в середину масиву
новий елемент, потрібно зміщувати «хвіст
масиву». Отже, для розв’язування задачі
потрібна інша структура даних. Така
структура даних є, і це є список.
Програма
SpysokRik розв’язує поставлену задачу і
демонструє основні прийоми опрацювання
списку. Елементи списку опрацьовують
один за одним за допомогою циклу. Спочатку
за допомогою циклу створють список і
вводять у нього дані з файлу. Специфіка
циклу у цій програмі така: після його
завершення буде створено зайвий
(останній) елемент списку. Його слід
ліквідувати, заздалегідь оголосивши
ще один вказівник (на попередній елемент
списку) і прийнявши poperednij^.dali:= nil.
Суттєва перевага списків полягає
саме в тому, що вилучити зафіксований
(тобто вибраний згідно з деякою умовою)
елемент можна за допомогою одної команди
переадресації вигляду poperednij^.dali
:= zafix^.dali.
Виводимо список на екран. Створюємо
новий елемент списку і вводимо в його
поля дані, наприклад, так:
Дніпро
(робимо п’ять пропусків і натискаємо
клавішу вводу)
2201 (натискаємо клавішу
вводу)
504000 (натискаємо клавішу вводу).
Новий елемент
зробимо першим у списку, тобто поставимо
його перед першим елементом існуючого
списку. Знову ж таки перевага списку
над масивом відчутна: щоб вставити новий
елемент після зафіксованого, потрібно
лише дві команди переадресації і жодної
«брудної роботи з хвостом». Вказівник
нового елемента переадресовуємо
туди, куди показував вказівник
зафіксованого елемента, а вказівник
зафіксованого елемента «втикаємо» в
новий елемент:
novyj^.dali := zafix^.dali;
zafix^.dali := novyj;
Знайдіть у програмі місце, де новий
елемент записується у список перед
першим. Так роблять зміни у списку, знову
переглядають його на екрані і виводять
у файл Riky2.pas. Переконайтеся, що
файл створено правильно. Не виходячи з
середовища, засобами меню File
та Open
відкрийте файл Riky2.pas і перегляньте його.
Розгляньте
рис. 2 і програму SpysokRik. Виконайте програму
та поекспериментуйте з нею.
Рис 2. Графічна інтерпретація списку і
дій з його елементами
|
program SpysokRik; uses Сrt; type elspysku = ^rika; rika = record nazva : string[11]; dov : integer; pl : longint; dali : elspysku; end; var element, pershyj, poperednij, novyj : elspysku; Myfile, Myfile2 : text; procedure StvorytySpysok(VAR Myfile : text); begin new(element); pershyj := element; while not eof(Myfile) do begin poperednij := element; with element^ do readln (Myfile, nazva, dov, pl); new (element^.dali); element := element^.dali end; {Вилучаємо останнiй зайвий елемент} poperednij^.dali := nil; end; procedure VyvestyNaEkran; begin writeln(’Створено такий список:’); writeln; element := pershyj; while element <> nil do begin with element^ do writeln(nazva:11, dov:8, pl:12); element := element^.dali end; end; procedure StvorytyNovyjElement; begin new(novyj); writeln; writeln (’Створюємо новий елемент’); with novyj^ do begin write(’Введiть назву – 11 символiв: ’); readln(nazva); write(’Введiть довжину рiчки: ’); readln(dov); write(’Введiть площу басейну: ’); readln(pl) end; writeln end; procedure VyvestyuFile (var Myfile : text); begin element := pershyj; while element <> nil do begin with element^ do writeln(Myfile, nazva:11, dov:8, pl:12); element := element^.dali end; writeln; writeln (’Список занесено у файл. Кiнець роботи’) end; {Кінець розділу процедур} begin {Основна програма} clrscr; assign (Myfile, ’riky.pas’); assign (Myfile2, ’riky2.pas’); reset (Myfile); StvorytySpysok (Myfile); VyvestyNaEkran; StvorytyNovyjElement; {Додаємо до списку новий елемент, новий елемент робимо першим} element := pershyj; novyj^.dali := element; pershyj := novyj; VyvestyNaEkran; rewrite (Myfile2); VyvestyuFile (Myfile2); close (Myfile); close (Myfile2); repeat until keypressed end. |
3. Ще раз про стек та чергу. Стек — це структура даних, у якій елемент, записаний останнім, зчитують (він є доступний до опрацювання) першим. Принцип «останній прийшов — перший пішов» використовується в багатьох технічних пристроях і побуті: згадайте ріжок від автомата, посадку пасажирів у вагон з одними дверима тощо. Стек використовують у програмуванні для реалізації рекурсії. Рекурсія виконується так: спочатку усі виклики нагромаджуються (аналогія така: пружина стискається), а потім виконуються вкладені процедури чи функції (пружина розпрямляється). Черга — це структура даних, у якій елемент, записаний першим, зчитують першим. Тут діє принцип «перший прийшов — перший пішов», добре відомий з побуту: черга за квитками тощо. Максимально допустимі розміри стеку і черги — важливі характеристики реалізації мови програмування, які визначають коло задач, які можна розв’язати. Стеки та черги описують і створюють у пам’яті за допомогою списків. Відповідні описи і приклади процедур їх опрацювання можна відшукати в довідниках. Завдання. Розв’яжіть задачу № 21, застосувавши список для вставляння у файл нового елемента після елемента з номером i mod 4 + 1.
