Главная > Информатика, программирование > Опис покажчиків
Опис покажчиків25-01-2012, 11:04. Разместил: tester9 |
Введення Адресація динамічних змінних відбувається через покажчики. У Паскалі можна визначити змінні, які мають тип покажчик, їх значення визначають адресу об'єкта. Для роботи з динамічними змінними в програмі повинні бути передбачені: виділення пам'яті під динамічну змінну; присвоєння вказівником на динамічну змінну адреси виділеної пам'яті (ініціалізація покажчика); звільнення пам'яті після використання динамічної змінної.
1. Покажчики. Опис покажчиків Покажчики - це особливий тип даних. У змінних цього типу зберігаються адреси інших змінних що містять корисну для програми інформацію. На перший погляд може здатися, що використання покажчиків призводить до зайвих витрат пам'яті і до ускладнення програми, а також істоті ускладнює й сам процес програмування. У даній главі ми, наведемо такі приклади використання покажчиків, з яких стане ясно, що всі додаткові витрати на їх зберігання та обробку окуповуються в повній мірі. Робота з вказівниками передбачена не тільки в Pascal, але і в деяких інших мовах програмування. Наприклад, у мові С покажчики використовуються практично в будь-якій програмі. В Pascal роль покажчиків дещо скромніше, і, тим не менш, початківцям програмістам слід засвоїти базові принципи роботи з вказівниками, щоб глибше зрозуміти внутрішній механізм обробки і виконання будь-якої комп'ютерної програми.
2. Покажчики та адреси Відомо, що адресою змінної є адреса першого байта комірки пам'яті, яка під неї відводиться. Для даних структурних типів (масивів і записів) їх адресою вважається адреса першого байта першого елемента. В Turbo Pascal існує можливість прямого доступу до будь байту оперативної пам'яті за його адресою при допомоги визначених у модулі system масивів Mem, MemW і MemL, які дозволяють записати інформацію або прочитати її безпосередньо з комірок пам'яті (Один, два або чотири байти). Це дуже небезпечні дії, тому вони виключені в 32 - розрядних системах програмування. Все ж дамо короткі пояснення для тих, хто працює в середовищі Borland (Turbo) Pascal. В якості індексу в цих масивах використовується адреса, записаний у вигляді, прийнятому в DOS: сегмент: Зсув відносно початку сегменту. Такий дивний спосіб запису адреси пов'язаний з тим, що в операційній системі DOS вся пам'ять розбита на сегменти, розміри яких не перевищують 64 Кбайт. Для отримання абсолютної адреси з пари сегмент Зсув система додає до сегменту справа шістнадцятковий нуль (Це чотири нулі в двійковій системі), а потім складає його зі зміщенням. Таким способом можна адресувати 1 Мбайт пам'яті. Наприклад, початкова адреса відеобуфера запишеться у вигляді $ B800: [fde_1328383079_8093813237_1328383079_8730893312_2573] 0, а звернутися до найпершого його байту можна так: Mem [$ В800: [fde_1328383079_8093813237_1328383079_2833318079_187] 00], до перших двох байтах - MemW [$ B800: [fde_1328383079_8093813237_1328383079_8091723338_7644] 00], до першим чотирьом байтам - MemL [$ B800: [fde_1328383079_8093813237_1328383079_3381329708_4458] 00] Абсолютний адреса, відповідний даній парі, - $ B8000. Ще один приклад для допитливих - оператор mem [0: C]: = mem [0: А]; можна застосувати для примусового очищення буфера клавіатури. Тут адресу маркера кінця буфера клавіатури прирівнюється до адресою його початку. Звичайно, в даному випадку краще скористатися засобами модуля crt. Є ще один спосіб звернення до оперативної пам'яті - використання службового слова absolute при описі змінної. У цьому випадку змінна буде розташовуватися саме по тією адресою в оперативній пам'яті, який вказаний після absolute. Зрозуміло, використання службового слова absolute - настільки ж небезпечний спосіб, як і звернення до пам'яті через зумовлені масиви. Однак absolute може використовуватися і більш безпечним способом, дозволяючи поєднувати в пам'яті два змінні з різними іменами. У мові Pascal є спеціальна операція отримання покажчика на змінну (або процедуру) - вона позначається як @. Мається також еквівалентна їй функція addr. Наприклад, @ x або addr (х) - Адресу змінної х. Є і зворотна операція отримання значення змінної по її адресою, яка позначається знаком ^. Наприклад, р ^ змінна з адресою р. У повсякденній практиці засоби роботи з адресами використовуються досить рідко. Основне призначення покажчиків полягає в тому, щоб забезпечити механізм використання у програмі динамічних змінних. Цей механізм ми і будемо обговорювати докладно в наступних розділах.
3. Опис покажчиків У Pascal є два різних види покажчиків: типізовані і нетипізовані. Типізований покажчик - це покажчик на змінну певного типу, наприклад, цілого, строкового або типу масиву Нетіпізарованний покажчик - це адреса першого байта області пам'яті, в якій може розміщуватися будь-яка інформація поза залежно від її типу. Опис двох видів покажчиків виконується по-різному: var p1: ^ integer; {Покажчик на змінну цілого типу} p2: ^ string; {покажчик на стоку} p3 pointer; {Нетипізований покажчик} Зауважимо що тип pointer сумісний з усіма типами покажчиків. У подальшому викладі для зручності імена всіх покажчиків будемо починати з букви p (pointer). Кожен покажчик розміщується в сегменті даних або в стеку (якщо він оголошений в підпрограмі) та займає там 4 байти. Це додаткові "накладні витрати 'пам'яті. Тому звичайні змінні дуже рідко створюють і знищують динамічно, залишаючи цю можливість для великих сукупностей даних. Чим більше розмір динамічної змінної, тим менше частка накладних витрат. Наприклад, при зберіганні в динамічній пам'яті масивів великих розмірів зайві 4 байти, витрачені на покажчик, несуттєві. покажчик динамічний пам'ять адресація 4. Оголошення покажчиків Як правило, в Турбо Паскалі покажчик зв'язується з певним типом даних. Такі покажчики будемо називати типізований. Для оголошення типизированного покажчика використовується значок А, який поміщається перед відповідним типом, наприклад: var p1: ^ integer; р2: ^ real; type PerconPointer = ^ PerconRecord; PerconRecord = record Name: string; Job: string; Next: PerconPointer end; Зверніть увагу: при оголошенні типу PerconPointer ми послалися на PerconRecord, який попередньо в програмі оголошений не був. Як уже зазначалося, в Турбо Паскалі послідовно проводиться в життя принцип, відповідно до якого перед використанням якого-небудь ідентифікатора він повинен бути описаний. Виняток зроблено тільки для покажчиків, які можуть посилатися на ще не оголошений тип даних. Цей виняток зроблено не випадково. Динамічна пам'ять дає можливість реалізувати широко використовувану в деяких програмах організацію даних у вигляді списків. Кожен елемент списку має у своєму складі покажчик на сусідній елемент, що забезпечує ливість перегляду і корекції списку. Якщо б в Турбо Паскалі не було цього винятку, реалізація списків була б значно ускладнена.
У Турбо Паскалі можна оголошувати покажчик і не пов'язувати його при цьому з будь-яким конкретним типом даних. Для цього служить стандартний тип POINTER, наприклад: var р: pointer; Покажчики такого роду будемо називати нетипізований. Оскільки нетипізовані покажчика не пов'язані з конкретним типом, з їх допомогою зручно динамічно розміщувати дані, структура і тип яких змінюються в ході роботи програми. Як вже говорилося, значеннями покажчиків є адреси змінних в пам'яті, тому варто було б очікувати, що значення одного укаателя можна передавати іншому. Насправді це не зовсім так. У Турбо Паскалі мо...жна передавати значення тільки між покажчиками, пов'язаними з одним і тим же типом даних. Якщо, наприклад, var p1, p2: ^ integer; р3: ^ real; рр: pointer; то присвоювання р1: = р2; цілком припустимо, в то час як р1: = р3; заборонено, оскільки Р1 і Р3 вказують на різні типи даних. Це обмеження, проте, не поширюється на нетипізовані покажчики, тому ми могли б записати pp: = р3; р1: = рр; і тим самим досягти потрібного результату. Читач має право задати питання, чи варто було вводити обмеження і тут же давати кошти для їх обходу. Вся справа в тому, що будь-яке обмеження, з одного боку, вводиться для підвищення надійності програм, а з іншого - зменшує потужність мови, робить його менш придатним для якихось застосувань. У Турбо Паскалі нечисленні виключення у відношенні типів даних надають мові необхідну гнучкість, але їх використання вимагає від програміста додаткових зусиль і таким чином свідчить про цілком усвідомленому дії.
Використання покажчиків Підіб'ємо деякі підсумки. Отже, динамічна пам'ять складає 200 ... 300 Кбайт або більше, її початок зберігається в змінної HEAPORG, a кінець відповідає адресі змінної HEAPEND. Поточний адресу вільної ділянки динамічної пам'яті зберігається в покажчику HEAPPTR. Подивимося, як можна використовувати динамічну пам'ять для розміщення великих масивів даних. Нехай, наприклад, потрібно забезпечити доступ до елементів прямокутної матриці 100х200 типу EXTENDED. Для размщеенія такого масиву вимагається пам'ять 200 000 байт (100 * 200 * 10). Здавалося б, цю проблему можна вирішити наступним чином: var i, j: integer; PtrArr: array [1 .. 100, 1 .. 200] of ^ real; begin for i: = 1 to 100 do for j: = 1 to 200 do new (PtrArr [i, j]); end. Тепер до будь-якого елементу новоствореного динамічного масиву можна звернутися за адресою, наприклад: PtrArr [1,1] ^ : = 0; if PtrArr [i, j * 2] ^> 1 then Згадаймо, однак, що довжина внутрішнього подання покажчика становить 4 байти, тому для розміщення масиву PTRARR буде потрібно 100 * 200 * 4 = 80000 байт, що перевищує розмір сегмента даних (65536 байт), доступний, як уже зазначалося, програмі для статичного розміщення даних. Виходом з положення могла б послужити адресна арифметика, тобто арифметика над покажчиками, тому що в цьому випадку можна було б відмовитися від створення масиву покажчиків PTRARR і обчислювати адресу будь-якого елементу прямокутної матриці безпосередньо перед зверненням до нього. Однак в Турбо Паскалі над покажчиками не визначені жодні операції, крім операцій привласнення і відносини. Тим не менш, вирішити зазначену задачу таки можна. Як ми вже знаємо, будь покажчик складається з двох слів типу WORD, в яких зберігаються сегмент і зсув. У Турбо Паскалі визначені дві вбудовані функції типу WORD, що дозволяють одержати вміст цих слів: SEG (X) - повертає сегментну частина адреси; OFS (X) - повертає зсув. Аргументом Х при зверненні до цих функцій може служити будь-яка змінна, в тому числі і та, на яку вказує покажчик. Наприклад, якщо маємо var р: ^ real; begin new (p); p ^: = 3.14; end то функція SEG (P) поверне сегментну частина адреси, за якою розташовується 4-байтним покажчик Р, в той час як SEG (P ^) - сегмент 6-байтного ділянки купи, в якому зберігається число 3.14. З іншого боку, з допомогою вбудованої функції PTR (SEG, OFS: WORD): POINTER можна створити значення покажчика, сумісний з покажчиками будь-якого типу. Таким чином, можлива така послідовність дій. Спочатку процедурою GETMEM з купи забираються кілька фрагментів відповідної довжини (нагадаю, що за одне звернення до процедурі можна зарезервувати не більше 65521 байт динамічної пам'яті). Для рас сматривать прикладу зручно резервувати фрагменти такої довжини щоб у них могли, наприклад, розміститися рядки прямокутної матриці, тобто 200 * 10 = 2000 байт. Початок кожного фрагмента, тобто фактично початок розміщення в пам'яті кожного рядка, запам'ятовується в масиві PTRSTR, що складається з 100 вказівників. Тепер для доступу до будь елементу рядка потрібно обчислити зміщення цього елемента від початку рядка і сформувати відповідний покажчик: var i, j: integer; PtrStr: array [1 .. 100] of pointer; pr: ^ real; const SizeOfReal = 6; begin for i: = 1 to 100 do GetMem (PtrStr [i], SizeOfReal * 200); {Звернення до елементу матриці [i, j]} pr: = ptr (seg (PtrStr [i] ^), ofs (PtrStr [i] ^) + (j-1) * SizeOfReal); if pr ^> 1 then end Оскільки оператор обчислення адреси PR: = PTR ... буде, судячи з усього, використовуватися в програмі неодноразово, корисно ввести допоміжну функцію GETR, повертаючу значення елемента матриці, і процедуру PUTR, що встановлює нове значення елемента. Кожна з них, у свою чергу, звертається до функції ADDRR для обчислення адреси. Нижче наводиться програма, що створює в пам'яті матрицю з NxM випадкових чисел і обчислює їх середнє значення. program Primer1; const SizeOfReal = 6; {Довжина змінної типу REAL} N = 100; {Кількість стовпців} М = 200; {Кількість рядків} var i, j: integer; PtrStr: array [1 .. N] of pointer; s: real; type RealPoint = ^ Real; {} Function AddrR (i, j: word): RealPoint; {По сегменту i та зміщення j видає адреса речової змінної} begin AddrR: = ptr (seg (PtrStr [i] ^), ofs (PtrStr [i] ^) + (j-1) * SizeOfReal) end; {AddrR} {} Function GetR (i, j: integer): real; {Видає значення речової змінної по сегменту i і зміщення j її адреси} begin GetR: = AddrR (i, j) ^ end; {GetR} {} Procepure PutR (i, j: integer; x: real); {Поміщає в змінну, адреса якої має сегмент i зсув j, речовий значення x} begin AddrR (i, j) ^: = x end; {PutR} {} begin {Main} for i: = 1 to N do begin GetMem (PtrStr [i], M * SizeOfReal); for j: = 1 to M do PutR (i, j, Random) end; s: = 0; for i: = 1 to N do for j: = 1 to M do s: = s + GetR (i, j); WriteLn (s/(N * M): 12:10) end. {Main} У розглянутому прикладі передбачається, що кожен рядок розміщується в купі, починаючи з кордону параграфа, і зсув для кожного покажчика PTRSTR дорівнює нулю. В Насправді при послідовних зверненнях до процедури GETMEM початок чергового фрагмента слід відразу за кінцем попереднього і може не потрапити на кордон сегмента. В результаті, при розміщенні фрагментів максимальної довжини (65521 байт) може виникнути переповнення при обчисленні зміщення останнього байта. |